diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a148a556..d3af7cb7 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -44,6 +44,11 @@ jobs: run: | echo "//registry.npmjs.org/:_authToken=$NPM_TOKEN" >> ~/.npmrc + - name: Dependency licence guard + # Invoked directly rather than through pnpm: it is lockfile-only and needs + # no node_modules, so this fails fast before the install. + run: node scripts/check-dependency-licenses.cjs + - name: Install dependencies run: pnpm install diff --git a/apps/builder/src/export/__tests__/__snapshots__/ExportSnapshotTests.test.ts.snap b/apps/builder/src/export/__tests__/__snapshots__/ExportSnapshotTests.test.ts.snap index f995ad67..905219ca 100644 --- a/apps/builder/src/export/__tests__/__snapshots__/ExportSnapshotTests.test.ts.snap +++ b/apps/builder/src/export/__tests__/__snapshots__/ExportSnapshotTests.test.ts.snap @@ -341,7 +341,7 @@ export default function GeneratedForm({ adapter, isWalletConnected }: GeneratedF exports[`Export Snapshot Tests > evm Export Snapshots > should match snapshot for package.json structure > package-json-evm 1`] = ` { "dependencies": { - "@openzeppelin/adapter-evm": "^3.0.0", + "@openzeppelin/adapter-evm": "^4.0.0", "@openzeppelin/ui-components": "^3.8.2", "@openzeppelin/ui-react": "^3.3.1", "@openzeppelin/ui-renderer": "^3.4.1", diff --git a/apps/builder/src/export/versions.ts b/apps/builder/src/export/versions.ts index 12558e93..10719c36 100644 --- a/apps/builder/src/export/versions.ts +++ b/apps/builder/src/export/versions.ts @@ -6,11 +6,11 @@ */ export const packageVersions = { - '@openzeppelin/adapter-evm': '3.0.0', - '@openzeppelin/adapter-midnight': '2.2.0', + '@openzeppelin/adapter-evm': '4.0.0', + '@openzeppelin/adapter-midnight': '4.0.0', '@openzeppelin/adapter-polkadot': '3.0.0', - '@openzeppelin/adapter-solana': '2.2.0', - '@openzeppelin/adapter-stellar': '3.0.0', + '@openzeppelin/adapter-solana': '4.0.0', + '@openzeppelin/adapter-stellar': '4.0.0', '@openzeppelin/ui-react': '3.3.1', '@openzeppelin/ui-renderer': '3.4.1', '@openzeppelin/ui-storage': '1.2.4', diff --git a/package.json b/package.json index cb5cc581..7882f1e9 100644 --- a/package.json +++ b/package.json @@ -48,7 +48,8 @@ "outdated": "pnpm outdated -r", "export-app": "node apps/builder/src/export/cli/export-app.cjs", "preinstall": "npx only-allow pnpm", - "install-pnpm": "npm install -g pnpm" + "install-pnpm": "npm install -g pnpm", + "check-licenses": "node scripts/check-dependency-licenses.cjs" }, "lint-staged": { "*.{js,jsx,ts,tsx,cjs,mjs}": "pnpm fix-all" diff --git a/scripts/check-dependency-licenses.cjs b/scripts/check-dependency-licenses.cjs new file mode 100644 index 00000000..c85212ed --- /dev/null +++ b/scripts/check-dependency-licenses.cjs @@ -0,0 +1,121 @@ +#!/usr/bin/env node +/** + * Fails if a dependency family we removed for licensing reasons reappears in the + * install tree. + * + * Two families were stripped, and both are easy to reintroduce by accident because + * neither is a direct dependency -- they arrive transitively and a grep of source + * code will not surface them: + * + * - `@trezor/*` is licensed under the Trezor Reference Source License, which grants + * "reference use" within one company and excludes redistribution. It arrives via + * `@creit.tech/stellar-wallets-kit`, which declares it as a hard dependency. + * - `@reown/*` moved to the Reown Community License at 1.8.3: commercial fees above + * 500 monthly active users, a clause requiring all use to connect to Reown's + * gateway, and a confidentiality clause. It arrives via + * `@walletconnect/ethereum-provider`, which `@wagmi/connectors` declares as a hard + * dependency. `@walletconnect/*` itself is Apache-2.0, but it is the only route + * Reown takes into the tree, so it is banned too. + * + * Both are removed by `readPackage` hooks in `.pnpmfile.cjs`. This guard checks the + * outcome (nothing in the lockfile) *and* the mechanism (the hooks are still + * wired), so deleting a hook fails loudly even if the committed lockfile happens to + * be clean. + * + * Deliberately dependency-free and lockfile-only: it runs before `pnpm install` and + * needs no network. + */ + +const fs = require('node:fs'); +const path = require('node:path'); + +const BANNED_SCOPES = [ + { + scope: '@trezor/', + licence: 'Trezor Reference Source License (no redistribution)', + arrivesVia: '@creit.tech/stellar-wallets-kit', + }, + { + scope: '@reown/', + licence: 'Reown Community License (fees above 500 MAU)', + arrivesVia: '@walletconnect/ethereum-provider', + }, + { + scope: '@walletconnect/', + licence: 'Apache-2.0 itself, but the only route @reown/* takes into the tree', + arrivesVia: '@wagmi/connectors', + }, +]; + +const REQUIRED_HOOKS = ['stripTrezorDependencies', 'stripWalletConnectDependencies']; + +const repoRoot = path.resolve(__dirname, '..'); +const lockfilePath = path.join(repoRoot, 'pnpm-lock.yaml'); +const pnpmfilePath = path.join(repoRoot, '.pnpmfile.cjs'); + +const problems = []; + +// 1. The outcome: nothing from a banned family may appear in the lockfile. +if (!fs.existsSync(lockfilePath)) { + problems.push(`Missing ${path.relative(repoRoot, lockfilePath)} -- cannot verify dependencies.`); +} else { + const lines = fs.readFileSync(lockfilePath, 'utf8').split('\n'); + + for (const { scope, licence, arrivesVia } of BANNED_SCOPES) { + const hits = []; + lines.forEach((line, index) => { + if (line.includes(scope)) { + hits.push({ line: index + 1, text: line.trim() }); + } + }); + + if (hits.length > 0) { + problems.push( + `${hits.length} lockfile reference(s) to ${scope}*\n` + + ` licence: ${licence}\n` + + ` usually arrives via: ${arrivesVia}\n` + + hits + .slice(0, 5) + .map((hit) => ` pnpm-lock.yaml:${hit.line}: ${hit.text.slice(0, 100)}`) + .join('\n') + + (hits.length > 5 ? `\n ... and ${hits.length - 5} more` : '') + ); + } + } +} + +// 2. The mechanism: the strip hooks must still be wired into readPackage. +if (!fs.existsSync(pnpmfilePath)) { + problems.push(`Missing ${path.relative(repoRoot, pnpmfilePath)} -- the strip hooks live there.`); +} else { + const pnpmfile = fs.readFileSync(pnpmfilePath, 'utf8'); + for (const hook of REQUIRED_HOOKS) { + // Defined and actually called, not just present as a dead function. + // The call must be a statement, so anchor to line start -- otherwise the + // function declaration itself satisfies the "is it called" test. + const defined = pnpmfile.includes(`function ${hook}(`); + const called = new RegExp(`^\\s+${hook}\\(pkg`, 'm').test(pnpmfile); + if (!defined || !called) { + problems.push( + `.pnpmfile.cjs no longer ${defined ? 'calls' : 'defines'} ${hook}().\n` + + ' Without it the banned packages return to the install tree on the next resolution.' + ); + } + } +} + +if (problems.length > 0) { + console.error('\nāœ– Dependency licence check failed\n'); + for (const problem of problems) { + console.error(` - ${problem}\n`); + } + console.error( + 'These families were removed deliberately. If a change legitimately needs one of\n' + + 'them, that is a licensing decision -- raise it rather than relaxing this check.\n' + + 'To restore the intended state: keep the .pnpmfile.cjs hooks, delete\n' + + 'node_modules/.pnpm-workspace-state-v1.json, then re-run pnpm install.\n' + ); + process.exit(1); +} + +console.log('āœ“ Dependency licence check passed (no @trezor/*, @reown/* or @walletconnect/*)');