diff --git a/backend/.env.test b/backend/.env.test index a0a7e19a..e243d933 100644 --- a/backend/.env.test +++ b/backend/.env.test @@ -8,4 +8,5 @@ STELLAR_HORIZON_URL=https://horizon-testnet.stellar.org SOROBAN_RPC_URL=https://soroban-test.stellar.org:443 STELLAR_ISSUER_PUBLIC_KEY=GBSXA7IC23YOWSJHJNMVO4K66LZAMVLOUVM2ATCSJJRZ74UCE7IPJLAO STELLAR_ISSUER_SECRET_KEY=SAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +DATABASE_READ_REPLICA_URL=postgresql://postgres:postgres@localhost:5432/web3-student-lab_test?schema=public LOG_LEVEL=error diff --git a/backend/package-lock.json b/backend/package-lock.json index adc90666..69110d83 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -30,10 +30,12 @@ "ioredis": "^5.10.1", "json2csv": "^6.0.0-alpha.2", "jsonwebtoken": "^9.0.2", + "marked": "^9.1.6", "openai": "^6.32.0", "pg": "^8.20.0", "prisma": "^7.8.0", "qrcode": "^1.5.4", + "sanitize-html": "^2.17.5", "socket.io": "^4.8.3", "swagger-jsdoc": "^6.3.0", "swagger-ui-express": "^5.0.1", @@ -44,6 +46,7 @@ "zod": "^4.3.6" }, "devDependencies": { + "@as-integrations/express4": "^1.1.2", "@types/bcryptjs": "^2.4.6", "@types/cors": "^2.8.19", "@types/express": "^5.0.6", @@ -51,6 +54,7 @@ "@types/jsonwebtoken": "^9.0.10", "@types/node": "^25.5.0", "@types/qrcode": "^1.5.5", + "@types/sanitize-html": "^2.16.1", "@types/supertest": "^7.2.0", "@types/swagger-jsdoc": "^6.0.4", "@types/swagger-ui-express": "^4.1.8", @@ -401,6 +405,20 @@ "node": ">=16" } }, + "node_modules/@as-integrations/express4": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@as-integrations/express4/-/express4-1.1.2.tgz", + "integrity": "sha512-PGeMcwoOKdYnZ4LtsmM7aLNoel3tbK8wKnfyahdRau1qb7wLbuaXB35zg3w34Ov4bm3WJtO3yzd8Bw5jVE+aIQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20" + }, + "peerDependencies": { + "@apollo/server": "^4.0.0 || ^5.0.0", + "express": "^4.0.0" + } + }, "node_modules/@babel/code-frame": { "version": "7.29.7", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz", @@ -4287,17 +4305,6 @@ "@types/node": "*" } }, - "node_modules/@types/ioredis-mock": { - "version": "8.2.7", - "resolved": "https://registry.npmjs.org/@types/ioredis-mock/-/ioredis-mock-8.2.7.tgz", - "integrity": "sha512-YsGiaOIYBKeVvu/7GYziAD8qX3LJem5LK00d5PKykzsQJMLysAqXA61AkNuYWCekYl64tbMTqVOMF4SYoCPbQg==", - "dev": true, - "license": "MIT", - "peer": true, - "peerDependencies": { - "ioredis": ">=5" - } - }, "node_modules/@types/istanbul-lib-coverage": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz", @@ -4444,14 +4451,14 @@ "dev": true, "license": "MIT" }, - "node_modules/@types/react": { - "version": "19.2.16", - "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.16.tgz", - "integrity": "sha512-esJiCAnl0kfpNdE69f3So4WJUXy95dLZydX0KwK46riIHDzHM7O9Vtf9xCHW0PXIqvgqNrswl522kA/5yx+F4w==", + "node_modules/@types/sanitize-html": { + "version": "2.16.1", + "resolved": "https://registry.npmjs.org/@types/sanitize-html/-/sanitize-html-2.16.1.tgz", + "integrity": "sha512-n9wjs8bCOTyN/ynwD8s/nTcTreIHB1vf31vhLMGqUPNHaweKC4/fAl4Dj+hUlCTKYgm4P3k83fmiFfzkZ6sgMA==", + "dev": true, "license": "MIT", - "peer": true, "dependencies": { - "csstype": "^3.2.2" + "htmlparser2": "^10.1" } }, "node_modules/@types/send": { @@ -6113,19 +6120,18 @@ "node": ">= 8" } }, - "node_modules/csstype": { - "version": "3.2.3", - "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", - "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", - "license": "MIT", - "peer": true - }, "node_modules/dataloader": { "version": "2.2.3", "resolved": "https://registry.npmjs.org/dataloader/-/dataloader-2.2.3.tgz", "integrity": "sha512-y2krtASINtPFS1rSDjacrFgn1dcUuoREVabwlOGOe4SdxenREqwjwjElAdwvbGM7kgZz9a3KVicWR7vcz8rnzA==", "license": "MIT" }, + "node_modules/dayjs": { + "version": "1.11.21", + "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.21.tgz", + "integrity": "sha512-98IT+HOahAisibz/yjKbzuOBwYcjJ7BCLPzARyHiyEBmRz4fatF+KPJszEHXsGYjUG234aH/cOjW1wwTbKUZlA==", + "license": "MIT" + }, "node_modules/debug": { "version": "4.4.3", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", @@ -6171,7 +6177,6 @@ "version": "4.3.1", "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", - "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" @@ -6301,6 +6306,73 @@ "node": ">=6.0.0" } }, + "node_modules/dom-serializer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", + "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", + "license": "MIT", + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.2", + "entities": "^4.2.0" + }, + "funding": { + "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" + } + }, + "node_modules/dom-serializer/node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/domelementtype": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", + "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "license": "BSD-2-Clause" + }, + "node_modules/domhandler": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz", + "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", + "license": "BSD-2-Clause", + "dependencies": { + "domelementtype": "^2.3.0" + }, + "engines": { + "node": ">= 4" + }, + "funding": { + "url": "https://github.com/fb55/domhandler?sponsor=1" + } + }, + "node_modules/domutils": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.2.2.tgz", + "integrity": "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==", + "license": "BSD-2-Clause", + "dependencies": { + "dom-serializer": "^2.0.0", + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3" + }, + "funding": { + "url": "https://github.com/fb55/domutils?sponsor=1" + } + }, "node_modules/dotenv": { "version": "17.4.2", "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.4.2.tgz", @@ -6525,6 +6597,18 @@ } } }, + "node_modules/entities": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-7.0.1.tgz", + "integrity": "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, "node_modules/env-paths": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-3.0.0.tgz", @@ -7480,6 +7564,25 @@ "dev": true, "license": "MIT" }, + "node_modules/htmlparser2": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-10.1.0.tgz", + "integrity": "sha512-VTZkM9GWRAtEpveh7MSF6SjjrpNVNNVJfFup7xTY3UpFtm67foy9HDVXneLtFVt4pMz5kZtgNcvCniNFb1hlEQ==", + "funding": [ + "https://github.com/fb55/htmlparser2?sponsor=1", + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "license": "MIT", + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3", + "domutils": "^3.2.2", + "entities": "^7.0.1" + } + }, "node_modules/http-errors": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", @@ -7739,6 +7842,15 @@ "node": ">=6" } }, + "node_modules/is-plain-object": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-5.0.0.tgz", + "integrity": "sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/is-promise": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", @@ -8654,6 +8766,15 @@ "integrity": "sha512-Xq9nH7KlWZmXAtodXDDRE7vs6DU1gTU8zYDHDiWLSip45Egwq3plLHzPn27NgvzL2r1LMPC1vdqh98sQxtqj4A==", "license": "MIT" }, + "node_modules/launder": { + "version": "1.7.1", + "resolved": "https://registry.npmjs.org/launder/-/launder-1.7.1.tgz", + "integrity": "sha512-mU6WRz5EusL9ZZuiZ5SO4Y6C0P9PAUR9iwdb6bzj4KDihm28DiHFw+/yk9DBH4f+Pv1wuzQ4e2jV3oQ7mkIqvw==", + "license": "MIT", + "dependencies": { + "dayjs": "^1.11.7" + } + }, "node_modules/leven": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", @@ -8887,6 +9008,18 @@ "tmpl": "1.0.5" } }, + "node_modules/marked": { + "version": "9.1.6", + "resolved": "https://registry.npmjs.org/marked/-/marked-9.1.6.tgz", + "integrity": "sha512-jcByLnIFkd5gSXZmjNvS1TlmRhCXZjIzHYlaGkPlLIekG55JDR2Z4va9tZwCiP+/RDERiNhMOFu01xd6O5ct1Q==", + "license": "MIT", + "bin": { + "marked": "bin/marked.js" + }, + "engines": { + "node": ">= 16" + } + }, "node_modules/math-intrinsics": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", @@ -9104,6 +9237,24 @@ "node": ">=8.0.0" } }, + "node_modules/nanoid": { + "version": "3.3.15", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.15.tgz", + "integrity": "sha512-y7Wygv/7mEOvxTuEQDB8StXdMRBWf1kR/tlhAzBRUFkB2jfcLOAxO/SHmOO2zgz1pVgK29/kyupn059/bCHdjA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, "node_modules/napi-postinstall": { "version": "0.3.4", "resolved": "https://registry.npmjs.org/napi-postinstall/-/napi-postinstall-0.3.4.tgz", @@ -9295,13 +9446,6 @@ } } }, - "node_modules/openapi-types": { - "version": "12.1.3", - "resolved": "https://registry.npmjs.org/openapi-types/-/openapi-types-12.1.3.tgz", - "integrity": "sha512-N4YtSYJqghVu4iek2ZUvcN/0aqH1kRDuNqzcycDxhOUpg7GdvLa2F3DgS6yBNhInhv2r/6I0Flkn7CqL8+nIcw==", - "license": "MIT", - "peer": true - }, "node_modules/p-limit": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", @@ -9379,6 +9523,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/parse-srcset": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/parse-srcset/-/parse-srcset-1.0.2.tgz", + "integrity": "sha512-/2qh0lav6CmI15FzA3i/2Bzk2zCgQhGMkvhOhKNcBVQ1ldgpbfiNTVslmooUmWJcADi1f1kIeynbDRVzNlfR6Q==", + "license": "MIT" + }, "node_modules/parseurl": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", @@ -9570,7 +9720,6 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", - "dev": true, "license": "ISC" }, "node_modules/picomatch": { @@ -9638,6 +9787,34 @@ "node": ">= 0.4" } }, + "node_modules/postcss": { + "version": "8.5.15", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.15.tgz", + "integrity": "sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.12", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, "node_modules/postgres": { "version": "3.4.7", "resolved": "https://registry.npmjs.org/postgres/-/postgres-3.4.7.tgz", @@ -9989,29 +10166,6 @@ "destr": "^2.0.5" } }, - "node_modules/react": { - "version": "19.2.7", - "resolved": "https://registry.npmjs.org/react/-/react-19.2.7.tgz", - "integrity": "sha512-HNe9WslTbXmFK8o8cmwgAeJFSBvt1bPdHCVKtaaV+WlAN36mpT4hcRpwbf3fY56ar2oIXzsBpOAiIRHAdY0OlQ==", - "license": "MIT", - "peer": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/react-dom": { - "version": "19.2.7", - "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.7.tgz", - "integrity": "sha512-t0BRVXvbiE/o20Hfw669rLbMCDWtYZLvmJigy2f0MxsXF+71pxhR3xOkspmsO8h3ZlNzyibAmtCa3l4lYKk6gQ==", - "license": "MIT", - "peer": true, - "dependencies": { - "scheduler": "^0.27.0" - }, - "peerDependencies": { - "react": "^19.2.7" - } - }, "node_modules/react-is-18": { "name": "react-is", "version": "18.3.1", @@ -10237,12 +10391,32 @@ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", "license": "MIT" }, - "node_modules/scheduler": { - "version": "0.27.0", - "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", - "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==", + "node_modules/sanitize-html": { + "version": "2.17.5", + "resolved": "https://registry.npmjs.org/sanitize-html/-/sanitize-html-2.17.5.tgz", + "integrity": "sha512-ZmU1joGRrvoyctKIiuwUxqR6moLoU2Wk+2bMccN6f7UwhAmwYDvWziqPxRDDN2Qip62NqnIrVrT9akbL6Wretg==", "license": "MIT", - "peer": true + "dependencies": { + "deepmerge": "^4.2.2", + "escape-string-regexp": "^4.0.0", + "htmlparser2": "^10.1.0", + "is-plain-object": "^5.0.0", + "launder": "^1.7.1", + "parse-srcset": "^1.0.2", + "postcss": "^8.3.11" + } + }, + "node_modules/sanitize-html/node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } }, "node_modules/scrypt-js": { "version": "3.0.1", @@ -10597,6 +10771,15 @@ "node": ">=0.10.0" } }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/source-map-support": { "version": "0.5.13", "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.13.tgz", @@ -11427,7 +11610,7 @@ "version": "5.9.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", - "devOptional": true, + "dev": true, "license": "Apache-2.0", "bin": { "tsc": "bin/tsc", diff --git a/backend/src/generator/generator.service.ts b/backend/src/generator/generator.service.ts index bbb8163d..2905b28c 100644 --- a/backend/src/generator/generator.service.ts +++ b/backend/src/generator/generator.service.ts @@ -1,7 +1,6 @@ import OpenAI from 'openai'; -import logger from '../utils/logger.js'; -import dotenv from 'dotenv'; import { cbManager } from '../lib/circuit-breaker/CircuitBreakerManager.js'; +import logger from '../utils/logger.js'; // dotenv.config(); // Skip in Docker Compose - use environment variables instead @@ -34,24 +33,26 @@ export class GeneratorService { async generateProjectIdea( theme: string, techStack: string[], - difficulty: string + difficulty: string, + customRpcUrl?: string ): Promise { return this.breaker.execute( async () => { const prompt = ` As an expert Web3 and Software Architect, generate a unique and innovative hackathon project idea. - + Theme: ${theme} Technology Stack: ${techStack.join(', ')} Target Difficulty: ${difficulty} - + ${customRpcUrl ? `\nIf this project will interact with a blockchain, prefer using the following RPC endpoint: ${customRpcUrl}` : ''} + Return the response in a structured JSON format with the following keys: - title: A catchy name for the project. - description: A detailed description of the project and its value proposition. - keyFeatures: An array of 3-5 core functionalities. - recommendedTech: An array of tools and libraries that would be useful. - difficulty: The suggested level (Beginner, Intermediate, or Advanced). - + Ensure the idea is practical for a 48-hour hackathon but still innovative. `; diff --git a/backend/src/licenses/data.ts b/backend/src/licenses/data.ts new file mode 100644 index 00000000..5f59b5f2 --- /dev/null +++ b/backend/src/licenses/data.ts @@ -0,0 +1,1231 @@ +/** + * Open Source License Data + * + * Comprehensive dataset of the most commonly used open source licenses, + * including their permissions, conditions, limitations, and use case suitability. + */ +import type { License } from './types.js'; + +export const licenses: License[] = [ + // ===== PERMISSIVE LICENSES ===== + { + id: 'mit', + name: 'MIT License', + fullName: 'MIT License', + spdxId: 'MIT', + category: 'permissive', + description: + 'The MIT License is a permissive free software license originating at the Massachusetts Institute of Technology. It permits reuse within proprietary software provided the license copy is included.', + summary: + 'A short, permissive license that allows almost any use, as long as the copyright notice is preserved.', + permissions: { + commercialUse: true, + modification: true, + distribution: true, + privateUse: true, + patentUse: false, + trademarkUse: false, + sublicense: true, + warranty: false, + }, + conditions: { + includeCopyright: true, + includeLicense: true, + stateChanges: false, + discloseSource: false, + sameLicense: false, + includeNotice: true, + includeInstallInstructions: false, + networkUseDisclosure: false, + }, + limitations: { + liability: true, + warranty: true, + trademark: false, + patentRetaliation: false, + useRestriction: false, + }, + useCaseSuitability: { + personal: 'recommended', + commercial: 'recommended', + saas: 'recommended', + library: 'recommended', + documentation: 'recommended', + educational: 'recommended', + }, + popularity: 'very-high', + recommendedFor: [ + 'Small to medium projects', + 'Libraries and frameworks', + 'Commercial products', + 'Web applications', + 'Desktop applications', + ], + notRecommendedFor: [ + 'Projects requiring strong copyleft protection', + 'Projects where you want to prevent proprietary use', + ], + url: 'https://opensource.org/licenses/MIT', + tags: ['popular', 'simple', 'permissive', 'business-friendly'], + }, + { + id: 'apache-2.0', + name: 'Apache License 2.0', + fullName: 'Apache License, Version 2.0', + spdxId: 'Apache-2.0', + category: 'permissive', + description: + 'A permissive license whose main conditions require preservation of copyright and license notices. Contributors provide an express grant of patent rights.', + summary: + 'Like MIT but with an explicit patent grant from contributors. Good for projects that may involve patents.', + permissions: { + commercialUse: true, + modification: true, + distribution: true, + privateUse: true, + patentUse: true, + trademarkUse: false, + sublicense: true, + warranty: false, + }, + conditions: { + includeCopyright: true, + includeLicense: true, + stateChanges: true, + discloseSource: false, + sameLicense: false, + includeNotice: true, + includeInstallInstructions: false, + networkUseDisclosure: false, + }, + limitations: { + liability: true, + warranty: true, + trademark: true, + patentRetaliation: true, + useRestriction: false, + }, + useCaseSuitability: { + personal: 'recommended', + commercial: 'recommended', + saas: 'recommended', + library: 'recommended', + documentation: 'recommended', + educational: 'recommended', + }, + popularity: 'very-high', + recommendedFor: [ + 'Projects with patent concerns', + 'Large corporate projects', + 'Libraries and frameworks', + 'Projects requiring a patent grant', + ], + notRecommendedFor: [ + 'Developers wanting a simpler, shorter license', + 'Projects that want to avoid trademark clauses', + ], + url: 'https://www.apache.org/licenses/LICENSE-2.0', + tags: ['popular', 'permissive', 'patent', 'corporate-friendly'], + }, + { + id: 'bsd-2-clause', + name: 'BSD 2-Clause License', + fullName: 'BSD 2-Clause "Simplified" License', + spdxId: 'BSD-2-Clause', + category: 'permissive', + description: + 'A permissive license that comes in two variants. The BSD 2-clause license allows redistribution and use as long as the copyright and license notices are retained.', + summary: + 'A simple permissive license similar to MIT but with slightly different wording around endorsement.', + permissions: { + commercialUse: true, + modification: true, + distribution: true, + privateUse: true, + patentUse: false, + trademarkUse: false, + sublicense: true, + warranty: false, + }, + conditions: { + includeCopyright: true, + includeLicense: true, + stateChanges: false, + discloseSource: false, + sameLicense: false, + includeNotice: true, + includeInstallInstructions: false, + networkUseDisclosure: false, + }, + limitations: { + liability: true, + warranty: true, + trademark: false, + patentRetaliation: false, + useRestriction: false, + }, + useCaseSuitability: { + personal: 'recommended', + commercial: 'recommended', + saas: 'recommended', + library: 'recommended', + documentation: 'recommended', + educational: 'recommended', + }, + popularity: 'high', + recommendedFor: [ + 'Projects wanting MIT-like terms', + 'Projects wanting to restrict endorsement', + 'University projects', + ], + notRecommendedFor: [ + 'Projects with patent concerns', + ], + url: 'https://opensource.org/licenses/BSD-2-Clause', + tags: ['permissive', 'simple', 'academic'], + }, + { + id: 'bsd-3-clause', + name: 'BSD 3-Clause License', + fullName: 'BSD 3-Clause "New" or "Revised" License', + spdxId: 'BSD-3-Clause', + category: 'permissive', + description: + 'The BSD 3-clause license allows redistribution and use in source and binary forms, with or without modification, provided the conditions are met. It adds a clause prohibiting the use of the project contributors\' names for promotion.', + summary: + 'Like BSD 2-Clause but with an additional clause prohibiting endorsement misuse.', + permissions: { + commercialUse: true, + modification: true, + distribution: true, + privateUse: true, + patentUse: false, + trademarkUse: false, + sublicense: true, + warranty: false, + }, + conditions: { + includeCopyright: true, + includeLicense: true, + stateChanges: false, + discloseSource: false, + sameLicense: false, + includeNotice: true, + includeInstallInstructions: false, + networkUseDisclosure: false, + }, + limitations: { + liability: true, + warranty: true, + trademark: true, + patentRetaliation: false, + useRestriction: false, + }, + useCaseSuitability: { + personal: 'recommended', + commercial: 'recommended', + saas: 'recommended', + library: 'recommended', + documentation: 'recommended', + educational: 'recommended', + }, + popularity: 'high', + recommendedFor: [ + 'Projects where endorsement protection matters', + 'University and research projects', + 'Government projects', + ], + notRecommendedFor: [ + 'Projects wanting simpler terms (use MIT)', + ], + url: 'https://opensource.org/licenses/BSD-3-Clause', + tags: ['permissive', 'academic', 'endorsement-protection'], + }, + { + id: 'isc', + name: 'ISC License', + fullName: 'ISC License', + spdxId: 'ISC', + category: 'permissive', + description: + 'The ISC license is functionally equivalent to the MIT License but uses simpler, shorter language. It removes some wording that some consider unnecessary.', + summary: + 'A permissive license functionally identical to MIT but with simpler, shorter text.', + permissions: { + commercialUse: true, + modification: true, + distribution: true, + privateUse: true, + patentUse: false, + trademarkUse: false, + sublicense: true, + warranty: false, + }, + conditions: { + includeCopyright: true, + includeLicense: true, + stateChanges: false, + discloseSource: false, + sameLicense: false, + includeNotice: true, + includeInstallInstructions: false, + networkUseDisclosure: false, + }, + limitations: { + liability: true, + warranty: true, + trademark: false, + patentRetaliation: false, + useRestriction: false, + }, + useCaseSuitability: { + personal: 'recommended', + commercial: 'recommended', + saas: 'recommended', + library: 'recommended', + documentation: 'recommended', + educational: 'recommended', + }, + popularity: 'high', + recommendedFor: [ + 'npm packages and Node.js projects', + 'Small projects wanting minimal license text', + ], + notRecommendedFor: [ + 'Projects needing explicit patent grants', + ], + url: 'https://opensource.org/licenses/ISC', + tags: ['permissive', 'minimal', 'npm'], + }, + { + id: 'unlicense', + name: 'The Unlicense', + fullName: 'The Unlicense', + spdxId: 'Unlicense', + category: 'public-domain', + description: + 'A license with no conditions whatsoever which dedicates works to the public domain. Unlicensed works, modifications, and larger works may be distributed under different terms and without source code.', + summary: + 'Dedicates the work to the public domain. No restrictions on use whatsoever.', + permissions: { + commercialUse: true, + modification: true, + distribution: true, + privateUse: true, + patentUse: true, + trademarkUse: true, + sublicense: true, + warranty: false, + }, + conditions: { + includeCopyright: false, + includeLicense: false, + stateChanges: false, + discloseSource: false, + sameLicense: false, + includeNotice: false, + includeInstallInstructions: false, + networkUseDisclosure: false, + }, + limitations: { + liability: true, + warranty: true, + trademark: false, + patentRetaliation: false, + useRestriction: false, + }, + useCaseSuitability: { + personal: 'recommended', + commercial: 'recommended', + saas: 'recommended', + library: 'recommended', + documentation: 'recommended', + educational: 'recommended', + }, + popularity: 'medium', + recommendedFor: [ + 'Projects where you want maximum adoption', + 'Trivial code snippets', + 'Anti-copyright projects', + ], + notRecommendedFor: [ + 'Projects in jurisdictions without public domain', + 'Projects needing attribution protection', + ], + url: 'https://unlicense.org/', + tags: ['public-domain', 'maximally-permissive', 'no-attribution'], + }, + { + id: 'cc0-1.0', + name: 'CC0 1.0 Universal', + fullName: 'Creative Commons CC0 1.0 Universal', + spdxId: 'CC0-1.0', + category: 'public-domain', + description: + 'CC0 enables creators to waive all copyright and related rights in their works to the fullest extent allowed by law, effectively placing them in the public domain.', + summary: + 'A public domain dedication that waives all copyright rights worldwide.', + permissions: { + commercialUse: true, + modification: true, + distribution: true, + privateUse: true, + patentUse: false, + trademarkUse: false, + sublicense: true, + warranty: false, + }, + conditions: { + includeCopyright: false, + includeLicense: false, + stateChanges: false, + discloseSource: false, + sameLicense: false, + includeNotice: false, + includeInstallInstructions: false, + networkUseDisclosure: false, + }, + limitations: { + liability: true, + warranty: true, + trademark: false, + patentRetaliation: false, + useRestriction: false, + }, + useCaseSuitability: { + personal: 'recommended', + commercial: 'recommended', + saas: 'recommended', + library: 'recommended', + documentation: 'recommended', + educational: 'recommended', + }, + popularity: 'high', + recommendedFor: [ + 'Data and databases', + 'Creative works and content', + 'Scientific datasets', + 'Projects wanting maximum reuse', + ], + notRecommendedFor: [ + 'Software (use a software-specific license)', + 'Projects needing attribution', + ], + url: 'https://creativecommons.org/publicdomain/zero/1.0/', + tags: ['public-domain', 'data', 'creative-works'], + }, + + // ===== WEAK COPYLEFT LICENSES ===== + { + id: 'lgpl-3.0', + name: 'GNU LGPLv3', + fullName: 'GNU Lesser General Public License v3.0', + spdxId: 'LGPL-3.0-only', + category: 'weak-copyleft', + description: + 'The LGPL is a weaker copyleft license that allows linking into proprietary projects as a library, as long as the library itself remains LGPL-licensed and users can modify it.', + summary: + 'Lets you use the library in proprietary projects but modifications to the library itself must stay LGPL.', + permissions: { + commercialUse: true, + modification: true, + distribution: true, + privateUse: true, + patentUse: true, + trademarkUse: false, + sublicense: false, + warranty: false, + }, + conditions: { + includeCopyright: true, + includeLicense: true, + stateChanges: true, + discloseSource: true, + sameLicense: true, + includeNotice: true, + includeInstallInstructions: false, + networkUseDisclosure: false, + }, + limitations: { + liability: true, + warranty: true, + trademark: false, + patentRetaliation: true, + useRestriction: false, + }, + useCaseSuitability: { + personal: 'recommended', + commercial: 'possible', + saas: 'recommended', + library: 'recommended', + documentation: 'not-recommended', + educational: 'recommended', + }, + popularity: 'high', + recommendedFor: [ + 'Software libraries', + 'Frameworks', + 'Plugins for proprietary software', + ], + notRecommendedFor: [ + 'Standalone applications', + 'Projects that want strong copyleft', + ], + url: 'https://www.gnu.org/licenses/lgpl-3.0.html', + tags: ['copyleft', 'library-friendly', 'lgpl'], + }, + { + id: 'lgpl-2.1', + name: 'GNU LGPLv2.1', + fullName: 'GNU Lesser General Public License v2.1', + spdxId: 'LGPL-2.1-only', + category: 'weak-copyleft', + description: + 'The LGPL version 2.1 is a weaker copyleft license that allows proprietary applications to link to the library. Modifications to the library itself must be released under LGPL.', + summary: + 'Similar to LGPLv3 but without the explicit patent grant. Often used for older libraries.', + permissions: { + commercialUse: true, + modification: true, + distribution: true, + privateUse: true, + patentUse: false, + trademarkUse: false, + sublicense: false, + warranty: false, + }, + conditions: { + includeCopyright: true, + includeLicense: true, + stateChanges: true, + discloseSource: true, + sameLicense: true, + includeNotice: true, + includeInstallInstructions: false, + networkUseDisclosure: false, + }, + limitations: { + liability: true, + warranty: true, + trademark: false, + patentRetaliation: false, + useRestriction: false, + }, + useCaseSuitability: { + personal: 'recommended', + commercial: 'possible', + saas: 'recommended', + library: 'recommended', + documentation: 'not-recommended', + educational: 'recommended', + }, + popularity: 'high', + recommendedFor: [ + 'Older projects already using LGPLv2.1', + 'Libraries where LGPLv3 is too restrictive', + ], + notRecommendedFor: [ + 'New projects (prefer LGPLv3 for patent protection)', + ], + url: 'https://www.gnu.org/licenses/lgpl-2.1.html', + tags: ['copyleft', 'library-friendly', 'lgpl', 'legacy'], + }, + { + id: 'mpl-2.0', + name: 'Mozilla Public License 2.0', + fullName: 'Mozilla Public License 2.0', + spdxId: 'MPL-2.0', + category: 'weak-copyleft', + description: + 'The MPL is a hybrid license that requires modifications to files that are MPL-licensed to be released under MPL, but allows larger works to combine MPL-licensed files with proprietary files.', + summary: + 'File-level copyleft. Changes to MPL files must be shared, but new files can be proprietary.', + permissions: { + commercialUse: true, + modification: true, + distribution: true, + privateUse: true, + patentUse: true, + trademarkUse: false, + sublicense: false, + warranty: false, + }, + conditions: { + includeCopyright: true, + includeLicense: true, + stateChanges: true, + discloseSource: true, + sameLicense: false, + includeNotice: true, + includeInstallInstructions: false, + networkUseDisclosure: false, + }, + limitations: { + liability: true, + warranty: true, + trademark: false, + patentRetaliation: true, + useRestriction: false, + }, + useCaseSuitability: { + personal: 'recommended', + commercial: 'possible', + saas: 'recommended', + library: 'recommended', + documentation: 'not-recommended', + educational: 'recommended', + }, + popularity: 'high', + recommendedFor: [ + 'Projects wanting to allow proprietary integration', + 'File-level copyleft protection', + 'Projects with patent concerns', + ], + notRecommendedFor: [ + 'Projects wanting full copyleft protection', + 'Simple scripts or small projects', + ], + url: 'https://www.mozilla.org/en-US/MPL/2.0/', + tags: ['copyleft', 'file-level', 'hybrid', 'mozilla'], + }, + + // ===== COPYLEFT LICENSES ===== + { + id: 'gpl-3.0', + name: 'GNU GPLv3', + fullName: 'GNU General Public License v3.0', + spdxId: 'GPL-3.0-only', + category: 'copyleft', + description: + 'The GPL is a strong copyleft license that requires anyone who distributes modified versions or derivative works to release the complete source code under the same license.', + summary: + 'Strong copyleft: if you distribute the software, you must share your source under GPLv3.', + permissions: { + commercialUse: true, + modification: true, + distribution: true, + privateUse: true, + patentUse: true, + trademarkUse: false, + sublicense: false, + warranty: false, + }, + conditions: { + includeCopyright: true, + includeLicense: true, + stateChanges: true, + discloseSource: true, + sameLicense: true, + includeNotice: true, + includeInstallInstructions: true, + networkUseDisclosure: false, + }, + limitations: { + liability: true, + warranty: true, + trademark: false, + patentRetaliation: true, + useRestriction: false, + }, + useCaseSuitability: { + personal: 'recommended', + commercial: 'possible', + saas: 'recommended', + library: 'not-recommended', + documentation: 'restricted', + educational: 'recommended', + }, + popularity: 'very-high', + recommendedFor: [ + 'Projects ensuring all derivatives remain free', + 'Desktop applications', + 'Embedded software', + ], + notRecommendedFor: [ + 'Libraries (use LGPL instead)', + 'Projects designed for SaaS use (no distribution trigger)', + 'Commercial products wanting to keep source proprietary', + ], + url: 'https://www.gnu.org/licenses/gpl-3.0.html', + tags: ['copyleft', 'strong-copyleft', 'fsf', 'gpl'], + }, + { + id: 'gpl-2.0', + name: 'GNU GPLv2', + fullName: 'GNU General Public License v2.0', + spdxId: 'GPL-2.0-only', + category: 'copyleft', + description: + 'The GPL version 2 is a strong copyleft license that requires source code distribution for any distributed modifications. It lacks the patent and anti-TiVoization clauses of GPLv3.', + summary: + 'Strong copyleft like GPLv3 but without patent grant or anti-Tivoization provisions.', + permissions: { + commercialUse: true, + modification: true, + distribution: true, + privateUse: true, + patentUse: false, + trademarkUse: false, + sublicense: false, + warranty: false, + }, + conditions: { + includeCopyright: true, + includeLicense: true, + stateChanges: true, + discloseSource: true, + sameLicense: true, + includeNotice: true, + includeInstallInstructions: false, + networkUseDisclosure: false, + }, + limitations: { + liability: true, + warranty: true, + trademark: false, + patentRetaliation: false, + useRestriction: false, + }, + useCaseSuitability: { + personal: 'recommended', + commercial: 'possible', + saas: 'recommended', + library: 'not-recommended', + documentation: 'restricted', + educational: 'recommended', + }, + popularity: 'very-high', + recommendedFor: [ + 'Legacy projects already using GPLv2', + 'Linux kernel modules', + 'Projects preferring older licensing terms', + ], + notRecommendedFor: [ + 'New projects (prefer GPLv3 for patent protection)', + 'Libraries or frameworks', + ], + url: 'https://www.gnu.org/licenses/gpl-2.0.html', + tags: ['copyleft', 'strong-copyleft', 'fsf', 'gpl', 'legacy'], + }, + { + id: 'agpl-3.0', + name: 'GNU AGPLv3', + fullName: 'GNU Affero General Public License v3.0', + spdxId: 'AGPL-3.0-only', + category: 'network-copyleft', + description: + 'The AGPL is the strongest copyleft license. It extends GPLv3 to close the "SaaS loophole" — if you modify the software and run it on a network (as a web service), users who interact with it over the network have the right to access the source code.', + summary: + 'Strongest copyleft: if you run modified code as a web service, users can request the source.', + permissions: { + commercialUse: true, + modification: true, + distribution: true, + privateUse: true, + patentUse: true, + trademarkUse: false, + sublicense: false, + warranty: false, + }, + conditions: { + includeCopyright: true, + includeLicense: true, + stateChanges: true, + discloseSource: true, + sameLicense: true, + includeNotice: true, + includeInstallInstructions: true, + networkUseDisclosure: true, + }, + limitations: { + liability: true, + warranty: true, + trademark: false, + patentRetaliation: true, + useRestriction: false, + }, + useCaseSuitability: { + personal: 'recommended', + commercial: 'restricted', + saas: 'recommended', + library: 'restricted', + documentation: 'restricted', + educational: 'recommended', + }, + popularity: 'high', + recommendedFor: [ + 'SaaS and web applications', + 'Network services', + 'Projects wanting maximum copyleft protection', + 'Server-side software', + ], + notRecommendedFor: [ + 'Client-side libraries', + 'Desktop applications with no network interaction', + 'Commercial SaaS products wanting closed source', + ], + url: 'https://www.gnu.org/licenses/agpl-3.0.html', + tags: ['copyleft', 'strongest-copyleft', 'saas', 'network'], + }, + + // ===== OTHER LICENSES ===== + { + id: 'epl-2.0', + name: 'Eclipse Public License 2.0', + fullName: 'Eclipse Public License 2.0', + spdxId: 'EPL-2.0', + category: 'weak-copyleft', + description: + 'The EPL is a weak copyleft license used primarily for Eclipse Foundation projects. It allows combining EPL-licensed code with proprietary code in larger works.', + summary: + 'Weak copyleft used by Eclipse Foundation. Patent retaliation clause included.', + permissions: { + commercialUse: true, + modification: true, + distribution: true, + privateUse: true, + patentUse: true, + trademarkUse: false, + sublicense: false, + warranty: false, + }, + conditions: { + includeCopyright: true, + includeLicense: true, + stateChanges: true, + discloseSource: true, + sameLicense: true, + includeNotice: true, + includeInstallInstructions: false, + networkUseDisclosure: false, + }, + limitations: { + liability: true, + warranty: true, + trademark: false, + patentRetaliation: true, + useRestriction: false, + }, + useCaseSuitability: { + personal: 'recommended', + commercial: 'possible', + saas: 'recommended', + library: 'recommended', + documentation: 'not-recommended', + educational: 'recommended', + }, + popularity: 'medium', + recommendedFor: [ + 'Eclipse Foundation projects', + 'Commercial-friendly copyleft projects', + 'Java/IDE plugins', + ], + notRecommendedFor: [ + 'Non-Eclipse ecosystem projects', + 'Projects outside the Java ecosystem', + ], + url: 'https://www.eclipse.org/legal/epl-2.0/', + tags: ['copyleft', 'eclipse', 'java'], + }, + { + id: 'bsl-1.1', + name: 'Business Source License 1.1', + fullName: 'Business Source License 1.1', + spdxId: 'BUSL-1.1', + category: 'other', + description: + 'The BUSL is a source-available license that grants rights to use, modify, and share the software for non-production uses. Production use requires a commercial license. It converts to a standard open source license (usually GPL or Apache) after a change date.', + summary: + 'Source-available license with a time-limited restriction on production use. Converts to open source later.', + permissions: { + commercialUse: false, + modification: true, + distribution: true, + privateUse: true, + patentUse: false, + trademarkUse: false, + sublicense: false, + warranty: false, + }, + conditions: { + includeCopyright: true, + includeLicense: true, + stateChanges: true, + discloseSource: true, + sameLicense: false, + includeNotice: true, + includeInstallInstructions: false, + networkUseDisclosure: false, + }, + limitations: { + liability: true, + warranty: true, + trademark: false, + patentRetaliation: false, + useRestriction: true, + }, + useCaseSuitability: { + personal: 'recommended', + commercial: 'restricted', + saas: 'restricted', + library: 'restricted', + documentation: 'not-recommended', + educational: 'recommended', + }, + popularity: 'medium', + recommendedFor: [ + 'Commercial open source business models', + 'Projects with a dual-license strategy', + 'Companies wanting to monetize while being transparent', + ], + notRecommendedFor: [ + 'Standard open source projects', + 'Projects wanting OSI approval (BUSL is not OSI-approved)', + ], + url: 'https://mariadb.com/bsl11/', + tags: ['source-available', 'commercial', 'time-limited'], + }, + { + id: 'cc-by-4.0', + name: 'Creative Commons Attribution 4.0', + fullName: 'Creative Commons Attribution 4.0 International', + spdxId: 'CC-BY-4.0', + category: 'permissive', + description: + 'CC BY allows others to distribute, remix, adapt, and build upon the material in any medium or format, so long as attribution is given to the creator. It is the least restrictive of the CC licenses.', + summary: + 'Allows any use as long as attribution is provided. Best for documentation and creative works.', + permissions: { + commercialUse: true, + modification: true, + distribution: true, + privateUse: true, + patentUse: false, + trademarkUse: false, + sublicense: true, + warranty: false, + }, + conditions: { + includeCopyright: true, + includeLicense: false, + stateChanges: true, + discloseSource: false, + sameLicense: false, + includeNotice: true, + includeInstallInstructions: false, + networkUseDisclosure: false, + }, + limitations: { + liability: true, + warranty: true, + trademark: false, + patentRetaliation: false, + useRestriction: false, + }, + useCaseSuitability: { + personal: 'recommended', + commercial: 'recommended', + saas: 'recommended', + library: 'not-recommended', + documentation: 'recommended', + educational: 'recommended', + }, + popularity: 'very-high', + recommendedFor: [ + 'Documentation', + 'Educational materials', + 'Creative works', + 'Blog posts and articles', + ], + notRecommendedFor: [ + 'Software code (use a software license)', + 'Projects wanting no-attribution use', + ], + url: 'https://creativecommons.org/licenses/by/4.0/', + tags: ['creative-commons', 'attribution', 'documentation'], + }, + { + id: 'cc-by-sa-4.0', + name: 'Creative Commons Attribution-ShareAlike 4.0', + fullName: 'Creative Commons Attribution-ShareAlike 4.0 International', + spdxId: 'CC-BY-SA-4.0', + category: 'copyleft', + description: + 'CC BY-SA lets others remix, adapt, and build upon the material for any purpose, even commercially, as long as they credit the creator and license their new creations under identical terms.', + summary: + 'Attribution required and derivative works must use the same license. The "copyleft" CC license.', + permissions: { + commercialUse: true, + modification: true, + distribution: true, + privateUse: true, + patentUse: false, + trademarkUse: false, + sublicense: false, + warranty: false, + }, + conditions: { + includeCopyright: true, + includeLicense: true, + stateChanges: true, + discloseSource: false, + sameLicense: true, + includeNotice: true, + includeInstallInstructions: false, + networkUseDisclosure: false, + }, + limitations: { + liability: true, + warranty: true, + trademark: false, + patentRetaliation: false, + useRestriction: false, + }, + useCaseSuitability: { + personal: 'recommended', + commercial: 'possible', + saas: 'possible', + library: 'not-recommended', + documentation: 'recommended', + educational: 'recommended', + }, + popularity: 'high', + recommendedFor: [ + 'Documentation requiring share-alike', + 'Wikipedia content', + 'Educational course materials', + ], + notRecommendedFor: [ + 'Software code', + 'Projects with patent concerns', + ], + url: 'https://creativecommons.org/licenses/by-sa/4.0/', + tags: ['creative-commons', 'copyleft', 'share-alike'], + }, + { + id: 'zlib', + name: 'Zlib License', + fullName: 'Zlib License', + spdxId: 'Zlib', + category: 'permissive', + description: + 'The zlib license is a very short, permissive license used for the zlib compression library. It allows the software to be used, modified, and distributed freely.', + summary: + 'A very short, simple permissive license. Used primarily for compression libraries.', + permissions: { + commercialUse: true, + modification: true, + distribution: true, + privateUse: true, + patentUse: false, + trademarkUse: false, + sublicense: true, + warranty: false, + }, + conditions: { + includeCopyright: true, + includeLicense: true, + stateChanges: false, + discloseSource: false, + sameLicense: false, + includeNotice: true, + includeInstallInstructions: false, + networkUseDisclosure: false, + }, + limitations: { + liability: true, + warranty: true, + trademark: false, + patentRetaliation: false, + useRestriction: false, + }, + useCaseSuitability: { + personal: 'recommended', + commercial: 'recommended', + saas: 'recommended', + library: 'recommended', + documentation: 'recommended', + educational: 'recommended', + }, + popularity: 'medium', + recommendedFor: [ + 'Embedded software and drivers', + 'Game development', + 'Projects wanting minimal license text', + ], + notRecommendedFor: [ + 'Projects needing patent protection', + ], + url: 'https://opensource.org/licenses/Zlib', + tags: ['permissive', 'minimal', 'embedded'], + }, + { + id: 'postgresql', + name: 'PostgreSQL License', + fullName: 'PostgreSQL License', + spdxId: 'PostgreSQL', + category: 'permissive', + description: + 'The PostgreSQL license is a permissive license similar to the MIT License. It allows the software to be used, modified, and distributed with no restrictions beyond preserving the copyright and disclaimer.', + summary: + 'A permissive license similar to MIT used by the PostgreSQL database.', + permissions: { + commercialUse: true, + modification: true, + distribution: true, + privateUse: true, + patentUse: false, + trademarkUse: false, + sublicense: true, + warranty: false, + }, + conditions: { + includeCopyright: true, + includeLicense: true, + stateChanges: false, + discloseSource: false, + sameLicense: false, + includeNotice: true, + includeInstallInstructions: false, + networkUseDisclosure: false, + }, + limitations: { + liability: true, + warranty: true, + trademark: false, + patentRetaliation: false, + useRestriction: false, + }, + useCaseSuitability: { + personal: 'recommended', + commercial: 'recommended', + saas: 'recommended', + library: 'recommended', + documentation: 'recommended', + educational: 'recommended', + }, + popularity: 'medium', + recommendedFor: [ + 'Database-related projects', + 'Projects following the PostgreSQL model', + ], + notRecommendedFor: [ + 'Projects needing explicit patent grants', + ], + url: 'https://opensource.org/licenses/PostgreSQL', + tags: ['permissive', 'database'], + }, +]; + +/** + * Compatibility matrix defining how licenses relate to each other. + * Only non-trivial compatibilities are listed. Licenses not listed + * should be treated as "conditional" (requires legal review). + */ +export const compatibilityMatrix: Array<{ + licenseA: string; + licenseB: string; + compatibility: 'compatible' | 'conditional' | 'incompatible'; + conditions?: string; +}> = [ + // MIT compatibility + { licenseA: 'mit', licenseB: 'apache-2.0', compatibility: 'compatible', conditions: 'When combining, the Apache NOTICE file must be preserved.' }, + { licenseA: 'mit', licenseB: 'bsd-2-clause', compatibility: 'compatible' }, + { licenseA: 'mit', licenseB: 'bsd-3-clause', compatibility: 'compatible' }, + { licenseA: 'mit', licenseB: 'isc', compatibility: 'compatible' }, + { licenseA: 'mit', licenseB: 'unlicense', compatibility: 'compatible' }, + { licenseA: 'mit', licenseB: 'cc0-1.0', compatibility: 'compatible' }, + { licenseA: 'mit', licenseB: 'zlib', compatibility: 'compatible' }, + { licenseA: 'mit', licenseB: 'postgresql', compatibility: 'compatible' }, + { licenseA: 'mit', licenseB: 'gpl-2.0', compatibility: 'compatible', conditions: 'MIT code can be included in GPLv2 projects (GPLv2 accepts MIT-licensed code). However, the combined work must be distributed under GPLv2.' }, + { licenseA: 'mit', licenseB: 'gpl-3.0', compatibility: 'compatible', conditions: 'MIT code can be included in GPLv3 projects. Combined work must be under GPLv3. GPLv3 is compatible with MIT.' }, + { licenseA: 'mit', licenseB: 'agpl-3.0', compatibility: 'compatible', conditions: 'MIT code can be included in AGPLv3 projects. Combined work must be under AGPLv3.' }, + { licenseA: 'mit', licenseB: 'lgpl-3.0', compatibility: 'compatible', conditions: 'MIT code can be used in LGPLv3 projects. The LGPL library must remain under LGPL, but the MIT code can be used freely.' }, + { licenseA: 'mit', licenseB: 'mpl-2.0', compatibility: 'compatible', conditions: 'MIT-licensed files can be included in MPL-2.0 projects. MPL applies only to modified MPL files.' }, + + // Apache 2.0 compatibility + { licenseA: 'apache-2.0', licenseB: 'bsd-2-clause', compatibility: 'compatible' }, + { licenseA: 'apache-2.0', licenseB: 'bsd-3-clause', compatibility: 'compatible' }, + { licenseA: 'apache-2.0', licenseB: 'isc', compatibility: 'compatible' }, + { licenseA: 'apache-2.0', licenseB: 'unlicense', compatibility: 'compatible' }, + { licenseA: 'apache-2.0', licenseB: 'cc0-1.0', compatibility: 'compatible' }, + { licenseA: 'apache-2.0', licenseB: 'mit', compatibility: 'compatible', conditions: 'Apache 2.0 has additional patent-related terms that may affect the combined work. Include NOTICE file.' }, + { licenseA: 'apache-2.0', licenseB: 'gpl-2.0', compatibility: 'incompatible', conditions: 'Apache 2.0 and GPLv2 are incompatible. GPLv2 does not include the patent grant required by Apache 2.0.' }, + { licenseA: 'apache-2.0', licenseB: 'gpl-3.0', compatibility: 'compatible', conditions: 'Apache 2.0 is compatible with GPLv3. GPLv3 includes a patent retaliation clause that satisfies Apache 2.0 requirements. Combined work must be under GPLv3.' }, + { licenseA: 'apache-2.0', licenseB: 'agpl-3.0', compatibility: 'compatible', conditions: 'Apache 2.0 is compatible with AGPLv3. Combined work must be under AGPLv3.' }, + { licenseA: 'apache-2.0', licenseB: 'lgpl-3.0', compatibility: 'compatible', conditions: 'Apache 2.0 code can be linked with LGPLv3 libraries. The LGPL component must remain under LGPL.' }, + { licenseA: 'apache-2.0', licenseB: 'mpl-2.0', compatibility: 'compatible', conditions: 'Apache 2.0 files can be included in MPL-2.0 projects with proper attribution.' }, + + // GPL compatibilities + { licenseA: 'gpl-3.0', licenseB: 'gpl-2.0', compatibility: 'incompatible', conditions: 'GPLv2 and GPLv3 are incompatible. GPLv2-only code cannot be combined with GPLv3 code. Use "GPLv2 or later" to bridge.' }, + { licenseA: 'gpl-3.0', licenseB: 'agpl-3.0', compatibility: 'incompatible', conditions: 'GPLv3 and AGPLv3 are not directly compatible. AGPL code cannot be incorporated into GPLv3 projects due to the additional network use requirement.' }, + { licenseA: 'gpl-3.0', licenseB: 'lgpl-3.0', compatibility: 'compatible', conditions: 'GPLv3 is compatible with LGPLv3. LGPLv3 code can be incorporated into GPLv3 projects (the result is under GPLv3).' }, + { licenseA: 'gpl-3.0', licenseB: 'mpl-2.0', compatibility: 'conditional', conditions: 'MPL-2.0 code can be included in GPLv3 projects under the GPL\'s terms, but the MPL-licensed files must remain available under MPL.' }, + { licenseA: 'gpl-2.0', licenseB: 'lgpl-2.1', compatibility: 'compatible', conditions: 'GPLv2 is compatible with LGPLv2.1. LGPLv2.1 code can be incorporated into GPLv2 projects.' }, + + // AGPL compatibilities + { licenseA: 'agpl-3.0', licenseB: 'lgpl-3.0', compatibility: 'compatible', conditions: 'AGPLv3 is compatible with LGPLv3. Combined work must be under AGPLv3.' }, + { licenseA: 'agpl-3.0', licenseB: 'mpl-2.0', compatibility: 'conditional', conditions: 'MPL-2.0 code can be included in AGPLv3 projects, but MPL files must remain available under MPL.' }, + + // LGPL compatibilities + { licenseA: 'lgpl-3.0', licenseB: 'lgpl-2.1', compatibility: 'incompatible', conditions: 'LGPLv3 and LGPLv2.1 are not compatible. Upgrade to LGPLv3 or use "LGPLv2.1 or later".' }, + { licenseA: 'lgpl-3.0', licenseB: 'mpl-2.0', compatibility: 'conditional', conditions: 'LGPLv3 and MPL-2.0 can be combined with careful consideration of the licensing terms for each file.' }, +]; + +/** + * Use case recommendations with detailed guidance. + */ +export const useCaseGuidance: Record = { + personal: { + description: 'Personal or hobby projects, not intended for commercial distribution.', + topPicks: ['mit', 'isc', 'unlicense'], + warnings: [ + 'Even for personal projects, choosing a license matters if you ever share the code publicly.', + ], + }, + commercial: { + description: 'Commercial software products distributed to customers.', + topPicks: ['mit', 'apache-2.0', 'bsd-3-clause'], + warnings: [ + 'Avoid GPL-family licenses if you do not want to release your source code.', + 'LGPL is acceptable if you only link to the library without modifying it.', + 'Apache 2.0 provides patent protection for both contributors and users.', + ], + }, + saas: { + description: 'Software provided as a hosted service (no distribution to customers).', + topPicks: ['mit', 'apache-2.0', 'agpl-3.0'], + warnings: [ + 'GPL does not trigger on SaaS usage (no distribution occurs).', + 'Only AGPL triggers source disclosure for SaaS/network use.', + 'If your project uses AGPL libraries, you must offer source to your users.', + ], + }, + library: { + description: 'Software libraries or frameworks intended to be used by other projects.', + topPicks: ['mit', 'apache-2.0', 'lgpl-3.0', 'mpl-2.0'], + warnings: [ + 'Strong copyleft (GPL) libraries discourage adoption by proprietary projects.', + 'LGPL is designed specifically for libraries and allows proprietary linking.', + 'MIT/BSD libraries have the widest adoption potential.', + ], + }, + documentation: { + description: 'Documentation, guides, tutorials, and written content.', + topPicks: ['cc-by-4.0', 'cc-by-sa-4.0', 'cc0-1.0'], + warnings: [ + 'Use Creative Commons licenses for documentation, not software licenses.', + 'CC BY-SA is used by Wikipedia and is share-alike.', + 'CC0 dedicates work to the public domain with no restrictions.', + ], + }, + educational: { + description: 'Educational materials, course content, and academic projects.', + topPicks: ['mit', 'cc-by-4.0', 'gpl-3.0', 'cc0-1.0'], + warnings: [ + 'MIT is recommended for code examples that students should freely reuse.', + 'CC BY is standard for educational content and lecture materials.', + 'GPL ensures any improvements by students remain open.', + ], + }, +}; diff --git a/backend/src/licenses/license.routes.ts b/backend/src/licenses/license.routes.ts new file mode 100644 index 00000000..04f102fb --- /dev/null +++ b/backend/src/licenses/license.routes.ts @@ -0,0 +1,469 @@ +/** + * Open Source License Guide - Express Routes + * + * REST API endpoints for the Open Source License Guide module. + * All endpoints are accessible under /api/v1/licenses. + */ +import { Request, Response, Router } from 'express'; +import logger from '../utils/logger.js'; +import * as licenseService from './license.service.js'; + +const router = Router(); + +/** + * @openapi + * /api/v1/licenses: + * get: + * summary: List all open source licenses with optional filtering + * description: Returns a paginated list of open source licenses. Supports filtering by category, use case, search text, permissions, and conditions. + * tags: [Licenses] + * parameters: + * - in: query + * name: category + * schema: + * type: string + * enum: [permissive, copyleft, weak-copyleft, network-copyleft, public-domain, other] + * - in: query + * name: useCase + * schema: + * type: string + * enum: [personal, commercial, saas, library, documentation, educational] + * - in: query + * name: search + * schema: + * type: string + * - in: query + * name: allowsCommercial + * schema: + * type: boolean + * - in: query + * name: allowsModification + * schema: + * type: boolean + * - in: query + * name: requiresDisclosure + * schema: + * type: boolean + * - in: query + * name: requiresSameLicense + * schema: + * type: boolean + * - in: query + * name: page + * schema: + * type: integer + * default: 1 + * - in: query + * name: limit + * schema: + * type: integer + * default: 50 + * responses: + * 200: + * description: A paginated list of licenses + */ +router.get('/', (req: Request, res: Response) => { + try { + const { + category, + useCase, + search, + allowsCommercial, + allowsModification, + requiresDisclosure, + requiresSameLicense, + popularity, + tags, + page, + limit, + } = req.query; + + const filter: Record = {}; + if (category) filter.category = category; + if (useCase) filter.useCase = useCase; + if (search) filter.search = search; + if (allowsCommercial !== undefined) filter.allowsCommercial = allowsCommercial === 'true'; + if (allowsModification !== undefined) filter.allowsModification = allowsModification === 'true'; + if (requiresDisclosure !== undefined) filter.requiresDisclosure = requiresDisclosure === 'true'; + if (requiresSameLicense !== undefined) filter.requiresSameLicense = requiresSameLicense === 'true'; + if (popularity) filter.popularity = popularity; + if (tags) filter.tags = (tags as string).split(','); + + const pageNum = Math.max(1, parseInt(page as string, 10) || 1); + const limitNum = Math.min(100, Math.max(1, parseInt(limit as string, 10) || 50)); + + const result = licenseService.getLicenses( + Object.keys(filter).length > 0 ? (filter as any) : undefined, + pageNum, + limitNum + ); + res.json(result); + } catch (error) { + logger.error('Error fetching licenses:', error); + res.status(500).json({ status: 'error', error: 'Failed to fetch licenses' }); + } +}); + +/** + * @openapi + * /api/v1/licenses/meta: + * get: + * summary: Get license guide metadata + * tags: [Licenses] + * responses: + * 200: + * description: Guide metadata including total count, categories, and version + */ +router.get('/meta', (_req: Request, res: Response) => { + try { + const result = licenseService.getGuideMeta(); + res.json(result); + } catch (error) { + logger.error('Error fetching license guide meta:', error); + res.status(500).json({ status: 'error', error: 'Failed to fetch guide metadata' }); + } +}); + +/** + * @openapi + * /api/v1/licenses/categories: + * get: + * summary: Get all license categories with counts + * tags: [Licenses] + * responses: + * 200: + * description: List of categories with license counts + */ +router.get('/categories', (_req: Request, res: Response) => { + try { + const result = licenseService.getCategories(); + res.json(result); + } catch (error) { + logger.error('Error fetching license categories:', error); + res.status(500).json({ status: 'error', error: 'Failed to fetch categories' }); + } +}); + +/** + * @openapi + * /api/v1/licenses/by-category: + * get: + * summary: Get licenses grouped by category + * tags: [Licenses] + * responses: + * 200: + * description: Licenses grouped by their category + */ +router.get('/by-category', (_req: Request, res: Response) => { + try { + const result = licenseService.getLicensesByCategory(); + res.json(result); + } catch (error) { + logger.error('Error fetching licenses by category:', error); + res.status(500).json({ status: 'error', error: 'Failed to fetch licenses by category' }); + } +}); + +/** + * @openapi + * /api/v1/licenses/use-cases: + * get: + * summary: Get all use cases with guidance and top picks + * tags: [Licenses] + * responses: + * 200: + * description: All use cases with recommendations + */ +router.get('/use-cases', (_req: Request, res: Response) => { + try { + const result = licenseService.getAllUseCases(); + res.json(result); + } catch (error) { + logger.error('Error fetching use cases:', error); + res.status(500).json({ status: 'error', error: 'Failed to fetch use cases' }); + } +}); + +/** + * @openapi + * /api/v1/licenses/recommend/{useCase}: + * get: + * summary: Get license recommendations for a specific use case + * tags: [Licenses] + * parameters: + * - in: path + * name: useCase + * required: true + * schema: + * type: string + * enum: [personal, commercial, saas, library, documentation, educational] + * responses: + * 200: + * description: Recommended licenses for the given use case + * 400: + * description: Invalid use case + */ +router.get('/recommend/:useCase', (req: Request, res: Response) => { + try { + const { useCase } = req.params; + const validUseCases = ['personal', 'commercial', 'saas', 'library', 'documentation', 'educational']; + if (!validUseCases.includes(useCase)) { + res.status(400).json({ status: 'error', error: `Invalid use case '${useCase}'. Valid values: ${validUseCases.join(', ')}` }); + return; + } + const result = licenseService.getRecommendations(useCase as any); + res.json(result); + } catch (error) { + logger.error('Error fetching recommendations:', error); + res.status(500).json({ status: 'error', error: 'Failed to fetch recommendations' }); + } +}); + +/** + * @openapi + * /api/v1/licenses/quick-recommend: + * get: + * summary: Quick license recommendation based on simple questionnaire + * tags: [Licenses] + * parameters: + * - in: query + * name: wantsCommercial + * schema: + * type: boolean + * default: true + * - in: query + * name: wantsModifications + * schema: + * type: boolean + * default: true + * - in: query + * name: wantsPatentProtection + * schema: + * type: boolean + * default: false + * - in: query + * name: acceptsCopyleft + * schema: + * type: boolean + * default: false + * - in: query + * name: isLibrary + * schema: + * type: boolean + * default: false + * responses: + * 200: + * description: Sorted list of recommended licenses + */ +router.get('/quick-recommend', (req: Request, res: Response) => { + try { + const { + wantsCommercial = 'true', + wantsModifications = 'true', + wantsPatentProtection = 'false', + acceptsCopyleft = 'false', + isLibrary = 'false', + } = req.query; + + const result = licenseService.quickRecommend( + wantsCommercial === 'true', + wantsModifications === 'true', + wantsPatentProtection === 'true', + acceptsCopyleft === 'true', + isLibrary === 'true' + ); + res.json(result); + } catch (error) { + logger.error('Error in quick recommend:', error); + res.status(500).json({ status: 'error', error: 'Failed to get quick recommendations' }); + } +}); + +/** + * @openapi + * /api/v1/licenses/compare: + * get: + * summary: Compare two licenses side by side + * tags: [Licenses] + * parameters: + * - in: query + * name: a + * required: true + * schema: + * type: string + * description: First license ID + * - in: query + * name: b + * required: true + * schema: + * type: string + * description: Second license ID + * responses: + * 200: + * description: Detailed comparison between two licenses + * 400: + * description: Missing or invalid license IDs + */ +router.get('/compare', (req: Request, res: Response) => { + try { + const { a, b } = req.query; + + if (!a || !b) { + res.status(400).json({ status: 'error', error: 'Both license IDs (a and b) are required' }); + return; + } + + const result = licenseService.compareLicenses(a as string, b as string); + if (result.status === 'error') { + res.status(404).json(result); + return; + } + res.json(result); + } catch (error) { + logger.error('Error comparing licenses:', error); + res.status(500).json({ status: 'error', error: 'Failed to compare licenses' }); + } +}); + +/** + * @openapi + * /api/v1/licenses/compatibility: + * get: + * summary: Check compatibility between two licenses + * tags: [Licenses] + * parameters: + * - in: query + * name: a + * required: true + * schema: + * type: string + * description: First license ID + * - in: query + * name: b + * required: true + * schema: + * type: string + * description: Second license ID + * responses: + * 200: + * description: Compatibility status between two licenses + */ +router.get('/compatibility', (req: Request, res: Response) => { + try { + const { a, b } = req.query; + + if (!a || !b) { + res.status(400).json({ status: 'error', error: 'Both license IDs (a and b) are required' }); + return; + } + + const result = licenseService.checkCompatibility(a as string, b as string); + if (result.status === 'error') { + res.status(404).json(result); + return; + } + res.json(result); + } catch (error) { + logger.error('Error checking compatibility:', error); + res.status(500).json({ status: 'error', error: 'Failed to check compatibility' }); + } +}); + +/** + * @openapi + * /api/v1/licenses/:licenseId/compatible: + * get: + * summary: Find all licenses compatible with a given license + * tags: [Licenses] + * parameters: + * - in: path + * name: licenseId + * required: true + * schema: + * type: string + * responses: + * 200: + * description: List of licenses with compatibility status + */ +router.get('/:licenseId/compatible', (req: Request, res: Response) => { + try { + const { licenseId } = req.params; + const result = licenseService.getCompatibleLicenses(licenseId); + if (result.status === 'error') { + res.status(404).json(result); + return; + } + res.json(result); + } catch (error) { + logger.error('Error fetching compatible licenses:', error); + res.status(500).json({ status: 'error', error: 'Failed to fetch compatible licenses' }); + } +}); + +/** + * @openapi + * /api/v1/licenses/spdx/{spdxId}: + * get: + * summary: Get a license by its SPDX identifier + * tags: [Licenses] + * parameters: + * - in: path + * name: spdxId + * required: true + * schema: + * type: string + * responses: + * 200: + * description: License details + * 404: + * description: License not found + */ +router.get('/spdx/:spdxId', (req: Request, res: Response) => { + try { + const { spdxId } = req.params; + const result = licenseService.getLicenseBySpdxId(spdxId); + if (result.status === 'error') { + res.status(404).json(result); + return; + } + res.json(result); + } catch (error) { + logger.error('Error fetching license by SPDX ID:', error); + res.status(500).json({ status: 'error', error: 'Failed to fetch license' }); + } +}); + +/** + * @openapi + * /api/v1/licenses/{licenseId}: + * get: + * summary: Get a single license by its ID + * tags: [Licenses] + * parameters: + * - in: path + * name: licenseId + * required: true + * schema: + * type: string + * responses: + * 200: + * description: License details + * 404: + * description: License not found + */ +router.get('/:licenseId', (req: Request, res: Response) => { + try { + const { licenseId } = req.params; + const result = licenseService.getLicenseById(licenseId); + if (result.status === 'error') { + res.status(404).json(result); + return; + } + res.json(result); + } catch (error) { + logger.error('Error fetching license:', error); + res.status(500).json({ status: 'error', error: 'Failed to fetch license' }); + } +}); + +export default router; diff --git a/backend/src/licenses/license.service.ts b/backend/src/licenses/license.service.ts new file mode 100644 index 00000000..873be147 --- /dev/null +++ b/backend/src/licenses/license.service.ts @@ -0,0 +1,546 @@ +/** + * Open Source License Guide Service + * + * Core business logic for the Open Source License Guide module. + * Handles license querying, filtering, comparison, recommendations, + * and compatibility checking. + */ +import { + compatibilityMatrix, + licenses, + useCaseGuidance, +} from './data.js'; +import type { + License, + LicenseCategory, + LicenseComparison, + LicenseCompatibility, + LicenseFilter, + LicenseGuideMeta, + LicensesApiResponse, + UseCase, +} from './types.js'; + +/** Pagination defaults */ +const DEFAULT_PAGE = 1; +const DEFAULT_LIMIT = 50; + +/** + * Get all licenses with optional filtering. + */ +export function getLicenses( + filter?: LicenseFilter, + page = DEFAULT_PAGE, + limit = DEFAULT_LIMIT +): LicensesApiResponse { + let filtered = [...licenses]; + + if (filter) { + // Filter by category + if (filter.category) { + filtered = filtered.filter((l) => l.category === filter.category); + } + + // Filter by use case suitability + if (filter.useCase) { + filtered = filtered.filter( + (l) => l.useCaseSuitability[filter.useCase!] !== 'restricted' + ); + } + + // Text search (name, fullName, description, tags) + if (filter.search) { + const q = filter.search.toLowerCase(); + filtered = filtered.filter( + (l) => + l.name.toLowerCase().includes(q) || + l.fullName.toLowerCase().includes(q) || + l.description.toLowerCase().includes(q) || + l.spdxId.toLowerCase().includes(q) || + l.tags.some((t) => t.toLowerCase().includes(q)) + ); + } + + // Permissions filters + if (filter.allowsCommercial !== undefined) { + filtered = filtered.filter((l) => l.permissions.commercialUse === filter.allowsCommercial); + } + if (filter.allowsModification !== undefined) { + filtered = filtered.filter((l) => l.permissions.modification === filter.allowsModification); + } + + // Conditions filters + if (filter.requiresDisclosure !== undefined) { + filtered = filtered.filter((l) => l.conditions.discloseSource === filter.requiresDisclosure); + } + if (filter.requiresSameLicense !== undefined) { + filtered = filtered.filter((l) => l.conditions.sameLicense === filter.requiresSameLicense); + } + + // Popularity filter + if (filter.popularity) { + filtered = filtered.filter((l) => l.popularity === filter.popularity); + } + + // Tag filter + if (filter.tags && filter.tags.length > 0) { + filtered = filtered.filter((l) => + filter.tags!.some((tag) => l.tags.includes(tag)) + ); + } + } + + // Pagination + const total = filtered.length; + const totalPages = Math.max(1, Math.ceil(total / limit)); + const safePage = Math.max(1, Math.min(page, totalPages)); + const startIndex = (safePage - 1) * limit; + const paginated = filtered.slice(startIndex, startIndex + limit); + + return { + status: 'success', + data: paginated, + pagination: { + page: safePage, + limit, + total, + totalPages, + }, + }; +} + +/** + * Get a single license by its ID. + */ +export function getLicenseById(id: string): LicensesApiResponse { + const license = licenses.find((l) => l.id === id); + if (!license) { + return { + status: 'error', + error: `License '${id}' not found`, + }; + } + return { + status: 'success', + data: license, + }; +} + +/** + * Get licenses by SPDX identifier. + */ +export function getLicenseBySpdxId(spdxId: string): LicensesApiResponse { + const license = licenses.find((l) => l.spdxId === spdxId); + if (!license) { + return { + status: 'error', + error: `License with SPDX ID '${spdxId}' not found`, + }; + } + return { + status: 'success', + data: license, + }; +} + +/** + * Get all available license categories. + */ +export function getCategories(): LicensesApiResponse<{ category: LicenseCategory; count: number }[]> { + const counts: Record = {}; + for (const license of licenses) { + counts[license.category] = (counts[license.category] || 0) + 1; + } + const data = Object.entries(counts).map(([category, count]) => ({ + category: category as LicenseCategory, + count, + })); + return { status: 'success', data }; +} + +/** + * Compare two licenses and generate a detailed comparison. + */ +export function compareLicenses(licenseAId: string, licenseBId: string): LicensesApiResponse { + const a = licenses.find((l) => l.id === licenseAId); + const b = licenses.find((l) => l.id === licenseBId); + + if (!a || !b) { + const missing: string[] = []; + if (!a) missing.push(licenseAId); + if (!b) missing.push(licenseBId); + return { + status: 'error', + error: `License(s) not found: ${missing.join(', ')}`, + }; + } + + // Find compatibility + let compatibility: LicenseCompatibility = { + licenseA: a.id, + licenseB: b.id, + compatibility: 'conditional', + conditions: 'No explicit compatibility data. Legal review recommended.', + }; + + // Check both directions + const directMatch = + compatibilityMatrix.find( + (c) => + (c.licenseA === a.id && c.licenseB === b.id) || + (c.licenseA === b.id && c.licenseB === a.id) + ); + if (directMatch) { + compatibility = { + licenseA: a.id, + licenseB: b.id, + compatibility: directMatch.compatibility, + conditions: directMatch.conditions, + }; + } + + // Find similarities + const similarities: string[] = []; + if (a.category === b.category) { + similarities.push(`Both are ${a.category} licenses`); + } + if (a.permissions.commercialUse === b.permissions.commercialUse) { + similarities.push('Both allow commercial use'); + } + if (a.permissions.modification === b.permissions.modification) { + similarities.push('Both allow modification'); + } + if (a.conditions.includeCopyright === b.conditions.includeCopyright) { + similarities.push('Both require copyright notice retention'); + } + if (a.conditions.discloseSource === b.conditions.discloseSource) { + similarities.push(a.conditions.discloseSource + ? 'Both require source disclosure' + : 'Neither requires source disclosure'); + } + if (a.conditions.sameLicense === b.conditions.sameLicense) { + similarities.push(a.conditions.sameLicense + ? 'Both require same-license distribution' + : 'Neither requires same-license distribution'); + } + + // Find differences + const differences: string[] = []; + if (a.category !== b.category) { + differences.push(`${a.name} is a ${a.category} license, while ${b.name} is ${b.category}`); + } + if (a.permissions.patentUse !== b.permissions.patentUse) { + differences.push(a.permissions.patentUse + ? `${a.name} includes an explicit patent grant; ${b.name} does not` + : `${b.name} includes an explicit patent grant; ${a.name} does not`); + } + if (a.permissions.sublicense !== b.permissions.sublicense) { + differences.push(a.permissions.sublicense + ? `${a.name} allows sublicensing; ${b.name} does not` + : `${b.name} allows sublicensing; ${a.name} does not`); + } + if (a.conditions.sameLicense !== b.conditions.sameLicense) { + differences.push(a.conditions.sameLicense + ? `${a.name} requires derivatives under the same license (copyleft); ${b.name} does not` + : `${b.name} requires derivatives under the same license (copyleft); ${a.name} does not`); + } + if (a.conditions.discloseSource !== b.conditions.discloseSource) { + differences.push(a.conditions.discloseSource + ? `${a.name} requires source disclosure; ${b.name} does not` + : `${b.name} requires source disclosure; ${a.name} does not`); + } + if (a.conditions.networkUseDisclosure !== b.conditions.networkUseDisclosure) { + differences.push(a.conditions.networkUseDisclosure + ? `${a.name} requires source disclosure for network use (SaaS); ${b.name} does not` + : `${b.name} requires source disclosure for network use (SaaS); ${a.name} does not`); + } + + // Generate recommendation + const recommendation = generateComparisonRecommendation(a, b, compatibility); + + return { + status: 'success', + data: { + licenseA: a, + licenseB: b, + similarities, + differences, + compatibility, + recommendation, + }, + }; +} + +/** + * Get license recommendations for a specific use case. + */ +export function getRecommendations(useCase: UseCase): LicensesApiResponse<{ + useCase: string; + description: string; + topPicks: License[]; + warnings: string[]; +}> { + const guidance = useCaseGuidance[useCase]; + if (!guidance) { + return { + status: 'error', + error: `Unknown use case: '${useCase}'`, + }; + } + + const topPicks = guidance.topPicks + .map((id) => licenses.find((l) => l.id === id)) + .filter((l): l is License => l !== undefined); + + return { + status: 'success', + data: { + useCase, + description: guidance.description, + topPicks, + warnings: guidance.warnings, + }, + }; +} + +/** + * Get all use cases with their guidance. + */ +export function getAllUseCases(): LicensesApiResponse { + return { + status: 'success', + data: useCaseGuidance, + }; +} + +/** + * Check compatibility between two license IDs. + */ +export function checkCompatibility( + licenseAId: string, + licenseBId: string +): LicensesApiResponse { + const a = licenses.find((l) => l.id === licenseAId); + const b = licenses.find((l) => l.id === licenseBId); + + if (!a || !b) { + const missing: string[] = []; + if (!a) missing.push(licenseAId); + if (!b) missing.push(licenseBId); + return { + status: 'error', + error: `License(s) not found: ${missing.join(', ')}`, + }; + } + + // Check both directions + const match = compatibilityMatrix.find( + (c) => + (c.licenseA === a.id && c.licenseB === b.id) || + (c.licenseA === b.id && c.licenseB === a.id) + ); + + if (match) { + return { + status: 'success', + data: { + licenseA: a.id, + licenseB: b.id, + compatibility: match.compatibility, + conditions: match.conditions, + }, + }; + } + + return { + status: 'success', + data: { + licenseA: a.id, + licenseB: b.id, + compatibility: 'conditional', + conditions: 'No explicit compatibility data. Legal review recommended.', + }, + }; +} + +/** + * Get guide metadata. + */ +export function getGuideMeta(): LicensesApiResponse { + const categories = new Set(); + const useCases = new Set(); + + for (const license of licenses) { + categories.add(license.category); + for (const [useCase, _suitability] of Object.entries(license.useCaseSuitability)) { + useCases.add(useCase as UseCase); + } + } + + return { + status: 'success', + data: { + totalLicenses: licenses.length, + categories: Array.from(categories), + useCases: Array.from(useCases), + lastUpdated: '2026-01-01', + version: '1.0.0', + }, + }; +} + +/** + * Get licenses grouped by category. + */ +export function getLicensesByCategory(): LicensesApiResponse> { + const grouped: Partial> = {}; + for (const license of licenses) { + if (!grouped[license.category]) { + grouped[license.category] = []; + } + grouped[license.category]!.push(license); + } + return { + status: 'success', + data: grouped as Record, + }; +} + +/** + * Quick lookup: find suitable licenses based on a simple questionnaire. + */ +export function quickRecommend( + wantsCommercial: boolean, + wantsModifications: boolean, + wantsPatentProtection: boolean, + acceptsCopyleft: boolean, + isLibrary: boolean +): LicensesApiResponse { + let candidates = [...licenses]; + + // Must allow commercial use + if (wantsCommercial) { + candidates = candidates.filter((l) => l.permissions.commercialUse); + } + + // Must allow modifications + if (wantsModifications) { + candidates = candidates.filter((l) => l.permissions.modification); + } + + // Must have patent grant + if (wantsPatentProtection) { + candidates = candidates.filter((l) => l.permissions.patentUse); + } + + // Filter by copyleft acceptance + if (!acceptsCopyleft) { + candidates = candidates.filter((l) => l.category === 'permissive' || l.category === 'public-domain'); + } + + // Filter for libraries + if (isLibrary && acceptsCopyleft) { + candidates = candidates.filter( + (l) => l.category === 'permissive' || l.category === 'public-domain' || l.category === 'weak-copyleft' + ); + } + + // Sort by popularity + type PopularityLevel = 'very-high' | 'high' | 'medium' | 'low'; + const popularityRank: Record = { + 'very-high': 0, + high: 1, + medium: 2, + low: 3, + }; + candidates.sort( + (a, b) => popularityRank[a.popularity as PopularityLevel] - popularityRank[b.popularity as PopularityLevel] + ); + + return { + status: 'success', + data: candidates, + }; +} + +/** + * Find licenses that are compatible with a given license. + */ +export function getCompatibleLicenses(licenseId: string): LicensesApiResponse<{ license: License; compatibility: LicenseCompatibility }[]> { + const license = licenses.find((l) => l.id === licenseId); + if (!license) { + return { + status: 'error', + error: `License '${licenseId}' not found`, + }; + } + + const results: { license: License; compatibility: LicenseCompatibility }[] = []; + + for (const other of licenses) { + if (other.id === licenseId) continue; + + const match = compatibilityMatrix.find( + (c) => + (c.licenseA === license.id && c.licenseB === other.id) || + (c.licenseA === other.id && c.licenseB === license.id) + ); + + results.push({ + license: other, + compatibility: { + licenseA: license.id, + licenseB: other.id, + compatibility: match?.compatibility ?? 'conditional', + conditions: match?.conditions ?? 'No explicit compatibility data. Legal review recommended.', + }, + }); + } + + // Sort: compatible first, then conditional, then incompatible + const rank = { compatible: 0, conditional: 1, incompatible: 2 }; + results.sort((a, b) => rank[a.compatibility.compatibility] - rank[b.compatibility.compatibility]); + + return { + status: 'success', + data: results, + }; +} + +// ---- Helpers ---- + +/** + * Generate a human-readable comparison recommendation. + */ +function generateComparisonRecommendation( + a: License, + b: License, + compat: LicenseCompatibility +): string { + if (compat.compatibility === 'incompatible') { + return `${a.name} and ${b.name} are **incompatible**. You cannot combine code under these licenses in the same project. Consider using one license only, or switching to a compatible alternative.`; + } + if (compat.compatibility === 'conditional') { + return `${a.name} and ${b.name} have conditional compatibility. ${compat.conditions || 'Legal review is recommended before combining these licenses.'}`; + } + // compatible + if (a.conditions.sameLicense || b.conditions.sameLicense) { + return `${a.name} and ${b.name} are compatible. However, if one requires same-license distribution (copyleft), the combined work must be distributed under that license. ${compat.conditions || ''}`; + } + return `${a.name} and ${b.name} are compatible. Both are permissive licenses that can be freely combined. ${compat.conditions || ''}`; +} + +export default { + getLicenses, + getLicenseById, + getLicenseBySpdxId, + getCategories, + compareLicenses, + getRecommendations, + getAllUseCases, + checkCompatibility, + getGuideMeta, + getLicensesByCategory, + quickRecommend, + getCompatibleLicenses, +}; diff --git a/backend/src/licenses/types.ts b/backend/src/licenses/types.ts new file mode 100644 index 00000000..6cc5ea3e --- /dev/null +++ b/backend/src/licenses/types.ts @@ -0,0 +1,128 @@ +/** + * Open Source License Guide Types + * + * Defines the structure for license information used throughout the + * User Dashboard's Open Source License Guide module. + */ + +/** The main categories of open source licenses */ +export type LicenseCategory = 'permissive' | 'copyleft' | 'weak-copyleft' | 'network-copyleft' | 'public-domain' | 'other'; + +/** Compatibility level between two licenses */ +export type CompatibilityLevel = 'compatible' | 'conditional' | 'incompatible'; + +/** Use case recommendation for a license */ +export type UseCase = 'personal' | 'commercial' | 'saas' | 'library' | 'documentation' | 'educational'; + +/** Whether a license is suitable for a given use case */ +export type Suitability = 'recommended' | 'possible' | 'not-recommended' | 'restricted'; + +/** Permissions granted by a license */ +export interface LicensePermissions { + commercialUse: boolean; + modification: boolean; + distribution: boolean; + privateUse: boolean; + patentUse: boolean; + trademarkUse: boolean; + sublicense: boolean; + warranty: boolean; +} + +/** Conditions required by a license */ +export interface LicenseConditions { + includeCopyright: boolean; + includeLicense: boolean; + stateChanges: boolean; + discloseSource: boolean; + sameLicense: boolean; + includeNotice: boolean; + includeInstallInstructions: boolean; + networkUseDisclosure: boolean; +} + +/** Limitations of a license */ +export interface LicenseLimitations { + liability: boolean; + warranty: boolean; + trademark: boolean; + patentRetaliation: boolean; + useRestriction: boolean; +} + +/** Compatibility information between two licenses */ +export interface LicenseCompatibility { + licenseA: string; + licenseB: string; + compatibility: CompatibilityLevel; + conditions?: string; +} + +/** A full license entry in the guide */ +export interface License { + id: string; + name: string; + fullName: string; + spdxId: string; + category: LicenseCategory; + description: string; + summary: string; + permissions: LicensePermissions; + conditions: LicenseConditions; + limitations: LicenseLimitations; + useCaseSuitability: Record; + popularity: 'very-high' | 'high' | 'medium' | 'low'; + recommendedFor: string[]; + notRecommendedFor: string[]; + url: string; + version?: string; + year?: string; + jurisdiction?: string; + tags: string[]; +} + +/** Filter parameters for querying licenses */ +export interface LicenseFilter { + category?: LicenseCategory; + useCase?: UseCase; + search?: string; + allowsCommercial?: boolean; + allowsModification?: boolean; + requiresDisclosure?: boolean; + requiresSameLicense?: boolean; + popularity?: string; + tags?: string[]; +} + +/** Comparison result between two licenses */ +export interface LicenseComparison { + licenseA: License; + licenseB: License; + similarities: string[]; + differences: string[]; + compatibility: LicenseCompatibility; + recommendation: string; +} + +/** Guide metadata */ +export interface LicenseGuideMeta { + totalLicenses: number; + categories: LicenseCategory[]; + useCases: UseCase[]; + lastUpdated: string; + version: string; +} + +/** API response wrapper */ +export interface LicensesApiResponse { + status: 'success' | 'error'; + data?: T; + message?: string; + error?: string; + pagination?: { + page: number; + limit: number; + total: number; + totalPages: number; + }; +} diff --git a/backend/src/routes/generator/generator.routes.ts b/backend/src/routes/generator/generator.routes.ts index 71b5b6ca..129f9f12 100644 --- a/backend/src/routes/generator/generator.routes.ts +++ b/backend/src/routes/generator/generator.routes.ts @@ -1,10 +1,10 @@ // @ts-nocheck +import { randomUUID } from 'crypto'; import { Request, Response, Router } from 'express'; import { GeneratorService } from '../../generator/generator.service.js'; -import logger from '../../utils/logger.js'; import { getRandomProjectIdea, mockProjectIdeas } from '../../generator/mockData.js'; -import { randomUUID } from 'crypto'; import { storageService } from '../../services/storage/index.js'; +import logger from '../../utils/logger.js'; const router = Router(); const generatorService = new GeneratorService(); @@ -21,7 +21,21 @@ const slugify = (value: string): string => */ router.post('/generate', async (req: Request, res: Response) => { try { - const { theme, techStack, difficulty, persistToStorage, queuedPersist } = req.body; + const { theme, techStack, difficulty, persistToStorage, queuedPersist, customRpcUrl } = req.body; + + // Validate optional custom RPC URL + if (customRpcUrl) { + try { + const url = new URL(customRpcUrl); + if (url.protocol !== 'http:' && url.protocol !== 'https:') { + res.status(400).json({ error: 'customRpcUrl must use http or https' }); + return; + } + } catch (err) { + res.status(400).json({ error: 'customRpcUrl is not a valid URL' }); + return; + } + } if (!theme || !techStack || !difficulty) { res.status(400).json({ error: 'Theme, techStack, and difficulty are required' }); @@ -29,8 +43,13 @@ router.post('/generate', async (req: Request, res: Response) => { } // Try AI generation first, fallback to mock data if it fails - try { - const projectIdea = await generatorService.generateProjectIdea(theme, techStack, difficulty); + try { + const projectIdea = await generatorService.generateProjectIdea( + theme, + techStack, + difficulty, + customRpcUrl + ); if (persistToStorage) { const projectId = `${slugify(theme)}-${Date.now()}-${randomUUID().slice(0, 8)}`; diff --git a/backend/src/routes/index.ts b/backend/src/routes/index.ts index 7a1160e2..7ca5b6c6 100644 --- a/backend/src/routes/index.ts +++ b/backend/src/routes/index.ts @@ -2,6 +2,7 @@ import { Router } from 'express'; import dashboardRoutes from '../dashboard/dashboard.routes.js'; import feedbackRouter from '../feedback/feedback.routes.js'; +import licenseRoutes from '../licenses/license.routes.js'; import userRouter from '../user/routes.js'; import analyticsRouter from './analytics.routes.js'; import authRoutes from './auth/auth.routes.js'; @@ -38,6 +39,7 @@ router.use('/contracts', contractRouter); router.use('/notifications', notificationRouter); router.use('/notifications/preferences', notificationPreferencesRouter); router.use('/security', securityRouter); +router.use('/licenses', licenseRoutes); router.use('/generator', generatorRouter); router.use('/export', exportRouter); router.use('/webhooks', webhooksRouter); diff --git a/backend/tests/generator.service.test.ts b/backend/tests/generator.service.test.ts new file mode 100644 index 00000000..a668c163 --- /dev/null +++ b/backend/tests/generator.service.test.ts @@ -0,0 +1,57 @@ +import { jest } from '@jest/globals'; + +const mockCreate = jest.fn(); + +jest.mock('openai', () => { + return { + default: jest.fn().mockImplementation(() => ({ + chat: { + completions: { + create: mockCreate, + }, + }, + })), + }; +}); + +describe('GeneratorService - custom RPC integration', () => { + beforeEach(() => { + mockCreate.mockClear(); + }); + + it('passes customRpcUrl into the AI prompt when provided', async () => { + process.env.OPENAI_API_KEY = 'test-key'; + + const mockResponse = { + choices: [ + { + message: { + content: JSON.stringify({ + title: 'RPC Test Project', + description: 'Test description', + keyFeatures: ['f1'], + recommendedTech: ['Node.js'], + difficulty: 'Intermediate', + }), + }, + }, + ], + }; + + mockCreate.mockResolvedValue(mockResponse as never); + + // Dynamic import to ensure mock is applied + const mod = await import('../src/generator/generator.service.js'); + const GeneratorService = mod.GeneratorService; + const svc = new GeneratorService(); + + const customRpc = 'https://custom-rpc.example:443'; + const result = await svc.generateProjectIdea('Theme', ['React'], 'Intermediate', customRpc); + + expect(mockCreate).toHaveBeenCalled(); + const calledArg = mockCreate.mock.calls[0][0]; + const userMessage = calledArg.messages[1].content; + expect(userMessage).toContain(customRpc); + expect(result).toHaveProperty('title', 'RPC Test Project'); + }); +}); diff --git a/backend/tests/licenses.test.ts b/backend/tests/licenses.test.ts new file mode 100644 index 00000000..bff3aa74 --- /dev/null +++ b/backend/tests/licenses.test.ts @@ -0,0 +1,503 @@ +import express, { type Express } from 'express'; +import request from 'supertest'; +import { licenses } from '../src/licenses/data.js'; +import licenseRoutes from '../src/licenses/license.routes.js'; +import * as licenseService from '../src/licenses/license.service.js'; +import type { License } from '../src/licenses/types.js'; + +// Minimal Express app using only the license router +function createTestApp(): Express { + const app = express(); + app.use('/api/v1/licenses', licenseRoutes); + return app; +} + +const app = createTestApp(); + +// ============================================================ +// License Service Unit Tests (no database required) +// ============================================================ + +describe('License Service', () => { + describe('getLicenses', () => { + it('returns all licenses with pagination by default', () => { + const result = licenseService.getLicenses(); + expect(result.status).toBe('success'); + expect(result.data).toBeDefined(); + expect(result.data!.length).toBeGreaterThan(0); + expect(result.pagination).toBeDefined(); + expect(result.pagination!.total).toBe(licenses.length); + expect(result.pagination!.page).toBe(1); + }); + + it('filters licenses by category', () => { + const result = licenseService.getLicenses({ category: 'permissive' }); + expect(result.status).toBe('success'); + result.data!.forEach((l: License) => { + expect(l.category).toBe('permissive'); + }); + }); + + it('filters licenses by copyleft category', () => { + const result = licenseService.getLicenses({ category: 'copyleft' }); + expect(result.status).toBe('success'); + result.data!.forEach((l: License) => { + expect(l.category).toBe('copyleft'); + }); + }); + + it('filters licenses by use case suitability', () => { + const result = licenseService.getLicenses({ useCase: 'commercial' }); + expect(result.status).toBe('success'); + result.data!.forEach((l: License) => { + expect(l.useCaseSuitability.commercial).not.toBe('restricted'); + }); + }); + + it('searches licenses by name', () => { + const result = licenseService.getLicenses({ search: 'MIT' }); + expect(result.status).toBe('success'); + expect(result.data!.length).toBeGreaterThan(0); + }); + + it('filters by commercial use permission', () => { + const result = licenseService.getLicenses({ allowsCommercial: true }); + expect(result.status).toBe('success'); + result.data!.forEach((l: License) => { + expect(l.permissions.commercialUse).toBe(true); + }); + }); + + it('filters by modification permission', () => { + const result = licenseService.getLicenses({ allowsModification: true }); + expect(result.status).toBe('success'); + result.data!.forEach((l: License) => { + expect(l.permissions.modification).toBe(true); + }); + }); + + it('filters by source disclosure requirement', () => { + const result = licenseService.getLicenses({ requiresDisclosure: true }); + expect(result.status).toBe('success'); + result.data!.forEach((l: License) => { + expect(l.conditions.discloseSource).toBe(true); + }); + }); + + it('filters by same-license requirement', () => { + const result = licenseService.getLicenses({ requiresSameLicense: true }); + expect(result.status).toBe('success'); + result.data!.forEach((l: License) => { + expect(l.conditions.sameLicense).toBe(true); + }); + }); + + it('supports pagination', () => { + const result = licenseService.getLicenses(undefined, 1, 3); + expect(result.status).toBe('success'); + expect(result.data!.length).toBeLessThanOrEqual(3); + expect(result.pagination!.page).toBe(1); + expect(result.pagination!.limit).toBe(3); + expect(result.pagination!.total).toBe(licenses.length); + }); + + it('handles page overflow gracefully', () => { + const result = licenseService.getLicenses(undefined, 999, 50); + expect(result.status).toBe('success'); + expect(result.pagination!.page).toBeLessThanOrEqual(result.pagination!.totalPages); + }); + }); + + describe('getLicenseById', () => { + it('returns a license by its id', () => { + const result = licenseService.getLicenseById('mit'); + expect(result.status).toBe('success'); + expect(result.data!.id).toBe('mit'); + expect(result.data!.name).toBe('MIT License'); + }); + + it('returns error for unknown license id', () => { + const result = licenseService.getLicenseById('non-existent'); + expect(result.status).toBe('error'); + expect(result.error).toContain('not found'); + }); + }); + + describe('getLicenseBySpdxId', () => { + it('returns a license by its SPDX ID', () => { + const result = licenseService.getLicenseBySpdxId('MIT'); + expect(result.status).toBe('success'); + expect(result.data!.spdxId).toBe('MIT'); + }); + + it('returns error for unknown SPDX ID', () => { + const result = licenseService.getLicenseBySpdxId('UNKNOWN'); + expect(result.status).toBe('error'); + }); + }); + + describe('getCategories', () => { + it('returns all categories with counts', () => { + const result = licenseService.getCategories(); + expect(result.status).toBe('success'); + expect(result.data!.length).toBeGreaterThan(0); + result.data!.forEach((cat) => { + expect(cat.category).toBeDefined(); + expect(cat.count).toBeGreaterThan(0); + }); + const categories = result.data!.map((c) => c.category); + expect(categories).toContain('permissive'); + expect(categories).toContain('copyleft'); + expect(categories).toContain('weak-copyleft'); + expect(categories).toContain('network-copyleft'); + }); + }); + + describe('compareLicenses', () => { + it('compares two licenses and returns similarities and differences', () => { + const result = licenseService.compareLicenses('mit', 'gpl-3.0'); + expect(result.status).toBe('success'); + expect(result.data!.licenseA.id).toBe('mit'); + expect(result.data!.licenseB.id).toBe('gpl-3.0'); + expect(result.data!.similarities.length).toBeGreaterThan(0); + expect(result.data!.differences.length).toBeGreaterThan(0); + expect(result.data!.recommendation).toBeDefined(); + }); + + it('compares two similar licenses', () => { + const result = licenseService.compareLicenses('mit', 'bsd-2-clause'); + expect(result.status).toBe('success'); + expect(result.data!.similarities.length).toBeGreaterThan(0); + }); + + it('returns error for missing license', () => { + const result = licenseService.compareLicenses('mit', 'non-existent'); + expect(result.status).toBe('error'); + }); + + it('compares GPLv2 and Apache 2.0 as incompatible', () => { + const result = licenseService.compareLicenses('apache-2.0', 'gpl-2.0'); + expect(result.status).toBe('success'); + expect(result.data!.compatibility.compatibility).toBe('incompatible'); + }); + + it('compares MIT and GPLv3 as compatible', () => { + const result = licenseService.compareLicenses('mit', 'gpl-3.0'); + expect(result.status).toBe('success'); + expect(result.data!.compatibility.compatibility).toBe('compatible'); + }); + }); + + describe('getRecommendations', () => { + it('returns recommendations for commercial use', () => { + const result = licenseService.getRecommendations('commercial'); + expect(result.status).toBe('success'); + expect(result.data!.useCase).toBe('commercial'); + expect(result.data!.topPicks.length).toBeGreaterThan(0); + expect(result.data!.warnings.length).toBeGreaterThan(0); + }); + + it('returns recommendations for saas use', () => { + const result = licenseService.getRecommendations('saas'); + expect(result.status).toBe('success'); + expect(result.data!.useCase).toBe('saas'); + expect(result.data!.topPicks.length).toBeGreaterThan(0); + }); + + it('returns error for unknown use case', () => { + const result = licenseService.getRecommendations('unknown' as any); + expect(result.status).toBe('error'); + }); + + it('returns recommendations for library use', () => { + const result = licenseService.getRecommendations('library'); + expect(result.status).toBe('success'); + const ids = result.data!.topPicks.map((l: License) => l.id); + expect(ids).toContain('mit'); + expect(ids).toContain('lgpl-3.0'); + }); + }); + + describe('checkCompatibility', () => { + it('returns compatible for MIT and Apache 2.0', () => { + const result = licenseService.checkCompatibility('mit', 'apache-2.0'); + expect(result.status).toBe('success'); + expect(result.data!.compatibility).toBe('compatible'); + }); + + it('returns incompatible for Apache 2.0 and GPLv2', () => { + const result = licenseService.checkCompatibility('apache-2.0', 'gpl-2.0'); + expect(result.status).toBe('success'); + expect(result.data!.compatibility).toBe('incompatible'); + }); + + it('returns conditional for unknown pair', () => { + const result = licenseService.checkCompatibility('zlib', 'bsl-1.1'); + expect(result.status).toBe('success'); + expect(result.data!.compatibility).toBe('conditional'); + }); + }); + + describe('getGuideMeta', () => { + it('returns guide metadata', () => { + const result = licenseService.getGuideMeta(); + expect(result.status).toBe('success'); + expect(result.data!.totalLicenses).toBe(licenses.length); + expect(result.data!.categories.length).toBeGreaterThan(0); + expect(result.data!.useCases.length).toBeGreaterThan(0); + expect(result.data!.version).toBeDefined(); + expect(result.data!.lastUpdated).toBeDefined(); + }); + }); + + describe('getLicensesByCategory', () => { + it('groups licenses by category', () => { + const result = licenseService.getLicensesByCategory(); + expect(result.status).toBe('success'); + const categories = Object.keys(result.data!); + expect(categories).toContain('permissive'); + expect(categories).toContain('copyleft'); + for (const category of categories) { + expect(result.data![category as keyof typeof result.data].length).toBeGreaterThan(0); + } + }); + }); + + describe('quickRecommend', () => { + it('recommends permissive licenses for commercial without copyleft', () => { + const result = licenseService.quickRecommend(true, true, false, false, false); + expect(result.status).toBe('success'); + result.data!.forEach((l: License) => { + expect(l.permissions.commercialUse).toBe(true); + expect(l.category === 'permissive' || l.category === 'public-domain').toBe(true); + }); + }); + + it('includes copyleft licenses when user accepts them', () => { + const result = licenseService.quickRecommend(true, true, false, true, false); + expect(result.status).toBe('success'); + const hasCopyleft = result.data!.some((l: License) => + l.category === 'copyleft' || l.category === 'weak-copyleft' || l.category === 'network-copyleft' + ); + expect(hasCopyleft).toBe(true); + }); + + it('filters by patent protection requirement', () => { + const result = licenseService.quickRecommend(true, true, true, true, false); + expect(result.status).toBe('success'); + result.data!.forEach((l: License) => { + expect(l.permissions.patentUse).toBe(true); + }); + }); + + it('filters out strong copyleft for libraries', () => { + const result = licenseService.quickRecommend(true, true, false, true, true); + expect(result.status).toBe('success'); + result.data!.forEach((l: License) => { + expect(l.category).not.toBe('copyleft'); + expect(l.category).not.toBe('network-copyleft'); + }); + }); + }); + + describe('getCompatibleLicenses', () => { + it('returns compatibility info for all other licenses', () => { + const result = licenseService.getCompatibleLicenses('mit'); + expect(result.status).toBe('success'); + expect(result.data!.length).toBe(licenses.length - 1); + expect(result.data![0].compatibility.compatibility).toBe('compatible'); + }); + + it('returns error for unknown license', () => { + const result = licenseService.getCompatibleLicenses('unknown'); + expect(result.status).toBe('error'); + }); + }); +}); + +// ============================================================ +// License API Route Integration Tests +// ============================================================ + +describe('License API Routes', () => { + describe('GET /api/v1/licenses', () => { + it('returns all licenses', async () => { + const response = await request(app).get('/api/v1/licenses').expect(200); + expect(response.body.status).toBe('success'); + expect(response.body.data.length).toBeGreaterThan(0); + expect(response.body.pagination).toBeDefined(); + }); + + it('filters by category', async () => { + const response = await request(app) + .get('/api/v1/licenses?category=permissive') + .expect(200); + expect(response.body.status).toBe('success'); + response.body.data.forEach((l: any) => { + expect(l.category).toBe('permissive'); + }); + }); + + it('supports search', async () => { + const response = await request(app) + .get('/api/v1/licenses?search=MIT') + .expect(200); + expect(response.body.status).toBe('success'); + expect(response.body.data.length).toBeGreaterThan(0); + }); + + it('filters by commercial use', async () => { + const response = await request(app) + .get('/api/v1/licenses?allowsCommercial=true') + .expect(200); + expect(response.body.status).toBe('success'); + response.body.data.forEach((l: any) => { + expect(l.permissions.commercialUse).toBe(true); + }); + }); + }); + + describe('GET /api/v1/licenses/meta', () => { + it('returns guide metadata', async () => { + const response = await request(app).get('/api/v1/licenses/meta').expect(200); + expect(response.body.status).toBe('success'); + expect(response.body.data.totalLicenses).toBeGreaterThan(0); + expect(response.body.data.version).toBe('1.0.0'); + }); + }); + + describe('GET /api/v1/licenses/categories', () => { + it('returns categories with counts', async () => { + const response = await request(app).get('/api/v1/licenses/categories').expect(200); + expect(response.body.status).toBe('success'); + expect(Array.isArray(response.body.data)).toBe(true); + expect(response.body.data.length).toBeGreaterThan(0); + }); + }); + + describe('GET /api/v1/licenses/by-category', () => { + it('returns licenses grouped by category', async () => { + const response = await request(app).get('/api/v1/licenses/by-category').expect(200); + expect(response.body.status).toBe('success'); + expect(response.body.data.permissive).toBeDefined(); + expect(response.body.data.copyleft).toBeDefined(); + expect(response.body.data['weak-copyleft']).toBeDefined(); + }); + }); + + describe('GET /api/v1/licenses/use-cases', () => { + it('returns all use cases', async () => { + const response = await request(app).get('/api/v1/licenses/use-cases').expect(200); + expect(response.body.status).toBe('success'); + expect(response.body.data.commercial).toBeDefined(); + expect(response.body.data.library).toBeDefined(); + }); + }); + + describe('GET /api/v1/licenses/recommend/:useCase', () => { + it('returns recommendations for a valid use case', async () => { + const response = await request(app) + .get('/api/v1/licenses/recommend/commercial') + .expect(200); + expect(response.body.status).toBe('success'); + expect(response.body.data.useCase).toBe('commercial'); + expect(response.body.data.topPicks.length).toBeGreaterThan(0); + }); + + it('returns 400 for invalid use case', async () => { + const response = await request(app) + .get('/api/v1/licenses/recommend/invalid') + .expect(400); + expect(response.body.status).toBe('error'); + }); + }); + + describe('GET /api/v1/licenses/quick-recommend', () => { + it('returns filtered recommendations', async () => { + const response = await request(app) + .get('/api/v1/licenses/quick-recommend?wantsCommercial=true&acceptsCopyleft=false') + .expect(200); + expect(response.body.status).toBe('success'); + response.body.data.forEach((l: any) => { + expect(l.permissions.commercialUse).toBe(true); + }); + }); + }); + + describe('GET /api/v1/licenses/compare', () => { + it('compares two licenses', async () => { + const response = await request(app) + .get('/api/v1/licenses/compare?a=mit&b=gpl-3.0') + .expect(200); + expect(response.body.status).toBe('success'); + expect(response.body.data.licenseA.id).toBe('mit'); + expect(response.body.data.licenseB.id).toBe('gpl-3.0'); + expect(response.body.data.differences.length).toBeGreaterThan(0); + }); + + it('returns 400 when missing params', async () => { + const response = await request(app) + .get('/api/v1/licenses/compare') + .expect(400); + expect(response.body.status).toBe('error'); + }); + }); + + describe('GET /api/v1/licenses/compatibility', () => { + it('checks compatibility between two licenses', async () => { + const response = await request(app) + .get('/api/v1/licenses/compatibility?a=mit&b=apache-2.0') + .expect(200); + expect(response.body.status).toBe('success'); + expect(response.body.data.compatibility).toBe('compatible'); + }); + + it('returns error for missing license', async () => { + const response = await request(app) + .get('/api/v1/licenses/compatibility?a=mit&b=nonexistent') + .expect(404); + expect(response.body.status).toBe('error'); + }); + }); + + describe('GET /api/v1/licenses/:licenseId', () => { + it('returns a license by ID', async () => { + const response = await request(app).get('/api/v1/licenses/mit').expect(200); + expect(response.body.status).toBe('success'); + expect(response.body.data.id).toBe('mit'); + }); + + it('returns 404 for unknown license', async () => { + const response = await request(app) + .get('/api/v1/licenses/unknown') + .expect(404); + expect(response.body.status).toBe('error'); + }); + }); + + describe('GET /api/v1/licenses/spdx/:spdxId', () => { + it('returns a license by SPDX ID', async () => { + const response = await request(app).get('/api/v1/licenses/spdx/MIT').expect(200); + expect(response.body.status).toBe('success'); + expect(response.body.data.spdxId).toBe('MIT'); + }); + }); + + describe('GET /api/v1/licenses/:licenseId/compatible', () => { + it('returns all compatible licenses for MIT', async () => { + const response = await request(app) + .get('/api/v1/licenses/mit/compatible') + .expect(200); + expect(response.body.status).toBe('success'); + expect(response.body.data.length).toBeGreaterThan(0); + expect(response.body.data[0].compatibility.compatibility).toBe('compatible'); + }); + + it('returns 404 for unknown license', async () => { + const response = await request(app) + .get('/api/v1/licenses/unknown/compatible') + .expect(404); + expect(response.body.status).toBe('error'); + }); + }); +}); diff --git a/frontend/HACKATHON_IDEA_GENERATOR_README.md b/frontend/HACKATHON_IDEA_GENERATOR_README.md index f58e5f98..ad2ac967 100644 --- a/frontend/HACKATHON_IDEA_GENERATOR_README.md +++ b/frontend/HACKATHON_IDEA_GENERATOR_README.md @@ -30,6 +30,10 @@ existing [`generatorAPI`](src/lib/api.ts) in `src/lib/api.ts` — no new client introduced. `buildGeneratorParams()` maps the UI filters to the request shape the endpoint already accepts (`{ theme, techStack, difficulty }`). +The backend endpoint also accepts an optional `customRpcUrl` string in the +request body; when provided the AI prompt will be instructed to prefer that RPC +endpoint for any blockchain interactions mentioned in the generated idea. + ## Filtering - **Difficulty** — `Beginner | Intermediate | Advanced` (matches `ProjectIdea`).