diff --git a/.changeset/tighten-supply-chain-security.md b/.changeset/tighten-supply-chain-security.md new file mode 100644 index 00000000..66e67a01 --- /dev/null +++ b/.changeset/tighten-supply-chain-security.md @@ -0,0 +1,12 @@ +--- +--- + +Apply supply-chain security best practices from [lirantal/npm-security-best-practices](https://github.com/lirantal/npm-security-best-practices) as enforced repo configuration plus a vitest gate that fails CI if any practice regresses. + +Config: bump pnpm to 10.33.2; add `minimumReleaseAge: 10080` (7-day install cooldown) and `blockExoticSubdeps: true` to `pnpm-workspace.yaml`; pin default + `@cipherstash` registry to npmjs via committed `.npmrc`; switch CI to `pnpm install --frozen-lockfile` on Node 22; add `.github/dependabot.yml` with cooldown'd grouped updates for npm + github-actions; add `.github/CODEOWNERS` protecting supply-chain critical paths. + +Test gate (`e2e/tests/supply-chain.e2e.test.ts`, 12 cases): asserts each invariant above, plus that every `pnpm-lock.yaml` entry resolves via `registry.npmjs.org` (substitutes for `lockfile-lint`, which doesn't support pnpm). + +Docs: new `skills/stash-supply-chain-security/SKILL.md` with the full guide; `AGENTS.md` Supply Chain Security section. + +No changes to any published package — release-side practices (#11 provenance, #12 OIDC trusted publishing) are deferred to a follow-up that requires npmjs.com Trusted Publisher configuration first. diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 00000000..08f82b18 --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1,12 @@ +# Default owners for everything in the repo. +* @cipherstash/developers + +# Supply-chain critical paths — changes here require explicit review +# (lirantal/npm-security-best-practices; see skills/stash-supply-chain-security/). +/.github/workflows/ @cipherstash/developers +/.github/dependabot.yml @cipherstash/developers +/.github/CODEOWNERS @cipherstash/developers +/pnpm-workspace.yaml @cipherstash/developers +/pnpm-lock.yaml @cipherstash/developers +/.npmrc @cipherstash/developers +/skills/stash-supply-chain-security/ @cipherstash/developers diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 00000000..68ac1bd1 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,62 @@ +version: 2 +updates: + # ── npm dependencies (pnpm workspace) ────────────────────────── + - package-ecosystem: npm + directory: / + schedule: + interval: weekly + day: monday + time: "08:00" + timezone: Australia/Sydney + # Wait for community discovery before opening PRs + # (lirantal/npm-security-best-practices #2: install with cooldown). + cooldown: + default-days: 7 + semver-major-days: 14 + open-pull-requests-limit: 10 + labels: + - dependencies + - supply-chain + commit-message: + prefix: "chore(deps)" + include: scope + groups: + dev-dependencies: + dependency-type: development + update-types: + - minor + - patch + production-minor-patch: + dependency-type: production + update-types: + - minor + - patch + type-definitions: + patterns: + - "@types/*" + # Major bumps stay un-grouped → one PR each, easier to review. + ignore: + # Catalog-managed; bump manually via pnpm-workspace.yaml + changeset. + - dependency-name: "@cipherstash/auth" + + # ── GitHub Actions ───────────────────────────────────────────── + - package-ecosystem: github-actions + directory: / + schedule: + interval: weekly + day: monday + cooldown: + default-days: 7 + open-pull-requests-limit: 5 + labels: + - dependencies + - github-actions + commit-message: + prefix: "chore(actions)" + groups: + actions-minor-patch: + patterns: + - "*" + update-types: + - minor + - patch diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 7ef66e8f..9374f0cd 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -25,11 +25,11 @@ jobs: - name: Install Node.js uses: actions/setup-node@v4 with: - node-version: 20 + node-version: 22 cache: 'pnpm' - name: Install dependencies - run: pnpm install + run: pnpm install --frozen-lockfile - name: Create .env file in ./packages/protect/ run: | @@ -115,11 +115,11 @@ jobs: - name: Install Node.js uses: actions/setup-node@v4 with: - node-version: 20 + node-version: 22 cache: 'pnpm' - name: Install dependencies - run: pnpm install + run: pnpm install --frozen-lockfile # Run the standalone `e2e/` workspace via turbo so the `^build` # dep on the `test:e2e` task builds cli + wizard first. CLI's own diff --git a/.npmrc b/.npmrc new file mode 100644 index 00000000..2e51f083 --- /dev/null +++ b/.npmrc @@ -0,0 +1,5 @@ +# Pin all registry traffic to npmjs (defence-in-depth against +# dependency-confusion: lirantal/npm-security-best-practices #16). +# Auth tokens MUST stay in user-level ~/.npmrc or env vars, never here. +@cipherstash:registry=https://registry.npmjs.org/ +registry=https://registry.npmjs.org/ diff --git a/AGENTS.md b/AGENTS.md index 032a27df..c6ed4e92 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -3,7 +3,7 @@ This is the Protect.js repository - End-to-end, per-value encryption for JavaScr ## Prerequisites - **Node.js**: >= 22 (enforced in `package.json` engines) -- **pnpm**: 10.14.0 (this repo uses pnpm workspaces and catalogs) +- **pnpm**: 10.33.2 (this repo uses pnpm workspaces and catalogs) - Internet access to install the prebuilt native module `@cipherstash/protect-ffi` If running integration tests or examples, you will also need CipherStash credentials (see Environment variables below). @@ -85,7 +85,17 @@ If these variables are missing, tests that require live encryption will fail or - `packages/utils`: Shared config (`utils/config`) and logger (`utils/logger`) - `examples/*`: Working apps (basic, drizzle, nextjs-clerk, next-drizzle-mysql, dynamo, hono-supabase) - `docs/*`: Concepts, how-to guides (Next.js bundling, SST, npm lockfile v3), reference -- `skills/*`: Agent skills (`stash-encryption`, `stash-drizzle`, `stash-dynamodb`, `stash-secrets`, `stash-supabase`) +- `skills/*`: Agent skills (`stash-encryption`, `stash-drizzle`, `stash-dynamodb`, `stash-secrets`, `stash-supabase`, `stash-supply-chain-security`) + +## Supply Chain Security + +This repo applies a set of supply-chain controls (post-install script policy, install cooldown, frozen-lockfile CI, registry pinning, Dependabot cooldown, CODEOWNERS) sourced from [lirantal/npm-security-best-practices](https://github.com/lirantal/npm-security-best-practices). They're validated by `e2e/tests/supply-chain.e2e.test.ts` so silent regressions fail CI. See `skills/stash-supply-chain-security/SKILL.md` for the full guide. + +Three rules to remember when editing CI or pnpm config: + +1. **CI uses `pnpm install --frozen-lockfile`.** Don't drop the flag. +2. **Adding to `pnpm.onlyBuiltDependencies` is an audit decision** — vet the package and explain the addition in the PR. +3. **Don't commit auth tokens in `.npmrc`.** Tokens belong in user-level `~/.npmrc` or environment variables. ## Key Concepts and APIs diff --git a/e2e/package.json b/e2e/package.json index cb612264..31db09cb 100644 --- a/e2e/package.json +++ b/e2e/package.json @@ -12,6 +12,7 @@ "@cipherstash/wizard": "workspace:*" }, "devDependencies": { - "vitest": "catalog:repo" + "vitest": "catalog:repo", + "yaml": "^2.8.3" } } diff --git a/e2e/tests/supply-chain.e2e.test.ts b/e2e/tests/supply-chain.e2e.test.ts new file mode 100644 index 00000000..936a727f --- /dev/null +++ b/e2e/tests/supply-chain.e2e.test.ts @@ -0,0 +1,169 @@ +import { readFileSync } from 'node:fs' +import { dirname, join, resolve } from 'node:path' +import { fileURLToPath } from 'node:url' +import { describe, expect, it } from 'vitest' +import { parse as parseYaml } from 'yaml' + +// Supply-chain enforcement tests. Each `it` corresponds to a control +// from lirantal/npm-security-best-practices applied in this repo. +// See skills/stash-supply-chain-security/SKILL.md for the rationale and +// how to bypass any of these for legitimate reasons. + +const __dirname = dirname(fileURLToPath(import.meta.url)) +const REPO_ROOT = resolve(__dirname, '../..') + +const read = (p: string) => readFileSync(join(REPO_ROOT, p), 'utf8') +const readJson = (p: string) => JSON.parse(read(p)) +const readYaml = (p: string) => parseYaml(read(p)) + +describe('supply chain — pnpm configuration', () => { + it('packageManager is pnpm ≥ 10.26 (needed for blockExoticSubdeps)', () => { + const pm = readJson('package.json').packageManager as string + expect(pm).toMatch(/^pnpm@/) + const [maj, min] = pm.replace('pnpm@', '').split('.').map(Number) + expect(maj).toBeGreaterThanOrEqual(10) + if (maj === 10) expect(min).toBeGreaterThanOrEqual(26) + }) + + it('pnpm-workspace.yaml sets minimumReleaseAge ≥ 7 days', () => { + // Enforce the configured policy (7 days), not just the lirantal minimum + // (3 days). Mirrors the Dependabot cooldown so manual + automated + // updates have the same community-discovery window. + const ws = readYaml('pnpm-workspace.yaml') as { minimumReleaseAge?: number } + expect(ws.minimumReleaseAge).toBeGreaterThanOrEqual(10080) // 7 days in minutes + }) + + it('pnpm-workspace.yaml sets blockExoticSubdeps: true', () => { + const ws = readYaml('pnpm-workspace.yaml') as { blockExoticSubdeps?: boolean } + expect(ws.blockExoticSubdeps).toBe(true) + }) + + it('onlyBuiltDependencies remains a small explicit allowlist (≤3 entries)', () => { + const allow = (readJson('package.json').pnpm?.onlyBuiltDependencies ?? []) as string[] + expect(Array.isArray(allow)).toBe(true) + expect(allow.length).toBeLessThanOrEqual(3) + }) +}) + +describe('supply chain — registry pinning (.npmrc)', () => { + it('pins @cipherstash scope and default registry to npmjs', () => { + const npmrc = read('.npmrc') + expect(npmrc).toMatch(/^@cipherstash:registry=https:\/\/registry\.npmjs\.org\/$/m) + expect(npmrc).toMatch(/^registry=https:\/\/registry\.npmjs\.org\/$/m) + }) + + it('does NOT contain auth tokens', () => { + const npmrc = read('.npmrc') + expect(npmrc).not.toMatch(/_authToken/i) + expect(npmrc).not.toMatch(/NPM_TOKEN/) + }) +}) + +describe('supply chain — pnpm-lock.yaml integrity', () => { + it('every resolved package comes from registry.npmjs.org (no git/tarball deps)', () => { + const lock = readYaml('pnpm-lock.yaml') as { + packages?: Record + } + const offenders: string[] = [] + for (const [name, entry] of Object.entries(lock.packages ?? {})) { + const resolution = entry.resolution + if (!resolution) continue + // Workspace `link:` entries appear as `directory` — those are first-party, + // not a supply-chain risk, and pnpm catalogs require them. + if (resolution.type === 'directory') continue + if (resolution.type === 'git') { + offenders.push(`${name} (type=git)`) + continue + } + const tarball = resolution.tarball + if (tarball && !tarball.startsWith('https://registry.npmjs.org/')) { + offenders.push(`${name} (tarball=${tarball})`) + } + } + expect(offenders).toEqual([]) + }) +}) + +describe('supply chain — CI hardening (.github/workflows/tests.yml)', () => { + const workflow = readYaml('.github/workflows/tests.yml') as { + jobs: Record }> }> + } + + it('every `pnpm install` invocation uses --frozen-lockfile', () => { + // Allow flag tokens (e.g. `pnpm --filter=foo install`, `pnpm -w install`) + // between `pnpm` and `install`, but not arbitrary words — that would + // false-match scripts like `pnpm run install-x`. + const PNPM_INSTALL = /\bpnpm\b(?:\s+-{1,2}\S+)*\s+install\b/ + for (const [jobName, job] of Object.entries(workflow.jobs)) { + const installSteps = job.steps.filter( + (s) => typeof s.run === 'string' && PNPM_INSTALL.test(s.run), + ) + for (const step of installSteps) { + expect(step.run, `${jobName} step "${step.run}"`).toMatch(/--frozen-lockfile/) + } + } + }) + + it('every pnpm-using job runs on Node 22', () => { + for (const [jobName, job] of Object.entries(workflow.jobs)) { + const usesPnpm = job.steps.some( + (s) => + (typeof s.uses === 'string' && s.uses.startsWith('pnpm/action-setup')) || + (typeof s.run === 'string' && /\bpnpm\b/.test(s.run)), + ) + if (!usesPnpm) continue + const setup = job.steps.find( + (s) => typeof s.uses === 'string' && s.uses.startsWith('actions/setup-node'), + ) + expect(setup, `${jobName} uses pnpm but lacks actions/setup-node`).toBeTruthy() + expect(String(setup?.with?.['node-version']), `${jobName} node version`).toBe('22') + } + }) +}) + +describe('supply chain — automated dependency updates (Dependabot)', () => { + const db = readYaml('.github/dependabot.yml') as { + updates: Array<{ + 'package-ecosystem': string + cooldown?: { 'default-days'?: number; 'semver-major-days'?: number } + }> + } + + it('npm ecosystem has a ≥ 3 day cooldown', () => { + const npm = db.updates.find((u) => u['package-ecosystem'] === 'npm') + expect(npm).toBeDefined() + expect(npm?.cooldown?.['default-days']).toBeGreaterThanOrEqual(3) + }) + + it('github-actions ecosystem is also covered with a ≥ 3 day cooldown', () => { + const gha = db.updates.find((u) => u['package-ecosystem'] === 'github-actions') + expect(gha).toBeDefined() + expect(gha?.cooldown?.['default-days']).toBeGreaterThanOrEqual(3) + }) +}) + +describe('supply chain — governance (CODEOWNERS)', () => { + it('protects supply-chain critical paths and assigns @cipherstash/developers', () => { + // Substring-search comment lines too liberally — strip them first so a + // bare comment mentioning the path can't satisfy the assertion. + const rules = read('.github/CODEOWNERS') + .split('\n') + .map((l) => l.trim()) + .filter((l) => l.length > 0 && !l.startsWith('#')) + + for (const path of [ + 'pnpm-workspace.yaml', + 'pnpm-lock.yaml', + 'dependabot.yml', + '.npmrc', + '.github/workflows/', + '.github/CODEOWNERS', + 'skills/stash-supply-chain-security/', + ]) { + const rule = rules.find((l) => l.includes(path)) + expect(rule, `no CODEOWNERS rule covers ${path}`).toBeDefined() + const owners = rule!.split(/\s+/).slice(1) + expect(owners, `${path} CODEOWNERS owners`).toContain('@cipherstash/developers') + } + }) +}) diff --git a/package.json b/package.json index aa944bd7..588e34e6 100644 --- a/package.json +++ b/package.json @@ -58,7 +58,7 @@ "rimraf": "^6.1.2", "turbo": "2.1.1" }, - "packageManager": "pnpm@10.14.0", + "packageManager": "pnpm@10.33.2", "engines": { "node": ">=22" }, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 8f228940..fc673085 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -57,7 +57,10 @@ importers: devDependencies: vitest: specifier: catalog:repo - version: 3.1.3(@types/node@22.19.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.19.3) + version: 3.1.3(@types/node@22.19.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.19.3)(yaml@2.8.3) + yaml: + specifier: ^2.8.3 + version: 2.8.3 examples/basic: dependencies: @@ -122,7 +125,7 @@ importers: version: 7.2.0 tsup: specifier: catalog:repo - version: 8.4.0(jiti@2.6.1)(postcss@8.5.6)(tsx@4.19.3)(typescript@5.6.3) + version: 8.4.0(jiti@2.6.1)(postcss@8.5.6)(tsx@4.19.3)(typescript@5.6.3)(yaml@2.8.3) tsx: specifier: catalog:repo version: 4.19.3 @@ -131,7 +134,7 @@ importers: version: 5.6.3 vitest: specifier: catalog:repo - version: 3.1.3(@types/node@22.19.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.19.3) + version: 3.1.3(@types/node@22.19.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.19.3)(yaml@2.8.3) packages/drizzle: dependencies: @@ -165,13 +168,13 @@ importers: version: 4.4.0 tsup: specifier: catalog:repo - version: 8.4.0(jiti@2.6.1)(postcss@8.5.6)(tsx@4.19.3)(typescript@5.6.3) + version: 8.4.0(jiti@2.6.1)(postcss@8.5.6)(tsx@4.19.3)(typescript@5.6.3)(yaml@2.8.3) typescript: specifier: catalog:repo version: 5.6.3 vitest: specifier: catalog:repo - version: 3.1.3(@types/node@22.19.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.19.3) + version: 3.1.3(@types/node@22.19.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.19.3)(yaml@2.8.3) packages/nextjs: dependencies: @@ -190,13 +193,13 @@ importers: version: 16.6.1 tsup: specifier: catalog:repo - version: 8.4.0(jiti@2.6.1)(postcss@8.5.6)(tsx@4.19.3)(typescript@5.6.3) + version: 8.4.0(jiti@2.6.1)(postcss@8.5.6)(tsx@4.19.3)(typescript@5.6.3)(yaml@2.8.3) typescript: specifier: catalog:repo version: 5.6.3 vitest: specifier: catalog:repo - version: 3.1.3(@types/node@22.19.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.19.3) + version: 3.1.3(@types/node@22.19.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.19.3)(yaml@2.8.3) optionalDependencies: '@rollup/rollup-linux-x64-gnu': specifier: 4.24.0 @@ -237,7 +240,7 @@ importers: version: 3.4.7 tsup: specifier: catalog:repo - version: 8.4.0(jiti@2.6.1)(postcss@8.5.6)(tsx@4.19.3)(typescript@5.6.3) + version: 8.4.0(jiti@2.6.1)(postcss@8.5.6)(tsx@4.19.3)(typescript@5.6.3)(yaml@2.8.3) tsx: specifier: catalog:repo version: 4.19.3 @@ -246,7 +249,7 @@ importers: version: 5.6.3 vitest: specifier: catalog:repo - version: 3.1.3(@types/node@22.19.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.19.3) + version: 3.1.3(@types/node@22.19.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.19.3)(yaml@2.8.3) optionalDependencies: '@rollup/rollup-linux-x64-gnu': specifier: 4.24.0 @@ -266,7 +269,7 @@ importers: version: 16.6.1 tsup: specifier: catalog:repo - version: 8.4.0(jiti@2.6.1)(postcss@8.5.6)(tsx@4.19.3)(typescript@5.6.3) + version: 8.4.0(jiti@2.6.1)(postcss@8.5.6)(tsx@4.19.3)(typescript@5.6.3)(yaml@2.8.3) tsx: specifier: catalog:repo version: 4.19.3 @@ -275,7 +278,7 @@ importers: version: 5.6.3 vitest: specifier: catalog:repo - version: 3.1.3(@types/node@22.19.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.19.3) + version: 3.1.3(@types/node@22.19.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.19.3)(yaml@2.8.3) packages/schema: dependencies: @@ -285,13 +288,13 @@ importers: devDependencies: tsup: specifier: catalog:repo - version: 8.4.0(jiti@2.6.1)(postcss@8.5.6)(tsx@4.19.3)(typescript@5.6.3) + version: 8.4.0(jiti@2.6.1)(postcss@8.5.6)(tsx@4.19.3)(typescript@5.6.3)(yaml@2.8.3) typescript: specifier: catalog:repo version: 5.6.3 vitest: specifier: catalog:repo - version: 3.1.3(@types/node@22.19.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.19.3) + version: 3.1.3(@types/node@22.19.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.19.3)(yaml@2.8.3) packages/stack: dependencies: @@ -337,7 +340,7 @@ importers: version: 3.4.9 tsup: specifier: catalog:repo - version: 8.4.0(jiti@2.6.1)(postcss@8.5.6)(tsx@4.19.3)(typescript@5.6.3) + version: 8.4.0(jiti@2.6.1)(postcss@8.5.6)(tsx@4.19.3)(typescript@5.6.3)(yaml@2.8.3) tsx: specifier: catalog:repo version: 4.19.3 @@ -346,7 +349,7 @@ importers: version: 5.6.3 vitest: specifier: catalog:repo - version: 3.1.3(@types/node@22.19.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.19.3) + version: 3.1.3(@types/node@22.19.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.19.3)(yaml@2.8.3) packages/wizard: dependencies: @@ -380,7 +383,7 @@ importers: version: 8.16.0 tsup: specifier: catalog:repo - version: 8.4.0(jiti@2.6.1)(postcss@8.5.6)(tsx@4.19.3)(typescript@5.6.3) + version: 8.4.0(jiti@2.6.1)(postcss@8.5.6)(tsx@4.19.3)(typescript@5.6.3)(yaml@2.8.3) tsx: specifier: catalog:repo version: 4.19.3 @@ -389,7 +392,7 @@ importers: version: 5.6.3 vitest: specifier: catalog:repo - version: 3.1.3(@types/node@22.19.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.19.3) + version: 3.1.3(@types/node@22.19.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.19.3)(yaml@2.8.3) packages: @@ -438,24 +441,28 @@ packages: engines: {node: '>=14.21.3'} cpu: [arm64] os: [linux] + libc: [musl] '@biomejs/cli-linux-arm64@1.9.4': resolution: {integrity: sha512-fJIW0+LYujdjUgJJuwesP4EjIBl/N/TcOX3IvIHJQNsAqvV2CHIogsmA94BPG6jZATS4Hi+xv4SkBBQSt1N4/g==} engines: {node: '>=14.21.3'} cpu: [arm64] os: [linux] + libc: [glibc] '@biomejs/cli-linux-x64-musl@1.9.4': resolution: {integrity: sha512-gEhi/jSBhZ2m6wjV530Yy8+fNqG8PAinM3oV7CyO+6c3CEh16Eizm21uHVsyVBEB6RIM8JHIl6AGYCv6Q6Q9Tg==} engines: {node: '>=14.21.3'} cpu: [x64] os: [linux] + libc: [musl] '@biomejs/cli-linux-x64@1.9.4': resolution: {integrity: sha512-lRCJv/Vi3Vlwmbd6K+oQ0KhLHMAysN8lXoCI7XeHlxaajk06u7G+UsFSO01NAs5iYuWKmVZjmiOzJ0OJmGsMwg==} engines: {node: '>=14.21.3'} cpu: [x64] os: [linux] + libc: [glibc] '@biomejs/cli-win32-arm64@1.9.4': resolution: {integrity: sha512-tlbhLk+WXZmgwoIKwHIHEBZUwxml7bRJgk0X2sPyNR3S93cdRq6XulAZRQJ17FYGGzWne0fgrXBKpl7l4M87Hg==} @@ -544,16 +551,19 @@ packages: resolution: {integrity: sha512-PDpm1EHC1XzVtEDGzcyr0UXNca8IFkfPusqqVJ5CSpzCtlYipIClYui197zQ4NGMHIAQD168IEFOK2TROyb4Tw==} cpu: [arm64] os: [linux] + libc: [glibc] '@cipherstash/auth-linux-x64-gnu@0.36.0': resolution: {integrity: sha512-Gm20ezVlGmNrkMH4s+I+JT13hDRD6vEX3fu3VDQQhWUiYCdgbdVsNJQgOr6QMY1cJkkmGyNlQKfiCPn4zlqtMg==} cpu: [x64] os: [linux] + libc: [glibc] '@cipherstash/auth-linux-x64-musl@0.36.0': resolution: {integrity: sha512-RUQeLc19JnURAMEoemP3+2DyptK+pqNFrVGgiKKOMVql0SZDVMlN2IyFrTKJ2emv1yuf4Gr1+E4jIdKPR0Oh+g==} cpu: [x64] os: [linux] + libc: [musl] '@cipherstash/auth-win32-x64-msvc@0.36.0': resolution: {integrity: sha512-1mQ8E6YFy7frHkvrDmSixpy47EakGPRh4qgoXPgk9lqZnlbMECYZhoKWQEs5wa3tLGgiX5G6jKC3NQZsOOqEfQ==} @@ -1115,89 +1125,105 @@ packages: resolution: {integrity: sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==} cpu: [arm64] os: [linux] + libc: [glibc] '@img/sharp-libvips-linux-arm@1.2.4': resolution: {integrity: sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==} cpu: [arm] os: [linux] + libc: [glibc] '@img/sharp-libvips-linux-ppc64@1.2.4': resolution: {integrity: sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==} cpu: [ppc64] os: [linux] + libc: [glibc] '@img/sharp-libvips-linux-riscv64@1.2.4': resolution: {integrity: sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==} cpu: [riscv64] os: [linux] + libc: [glibc] '@img/sharp-libvips-linux-s390x@1.2.4': resolution: {integrity: sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==} cpu: [s390x] os: [linux] + libc: [glibc] '@img/sharp-libvips-linux-x64@1.2.4': resolution: {integrity: sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==} cpu: [x64] os: [linux] + libc: [glibc] '@img/sharp-libvips-linuxmusl-arm64@1.2.4': resolution: {integrity: sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==} cpu: [arm64] os: [linux] + libc: [musl] '@img/sharp-libvips-linuxmusl-x64@1.2.4': resolution: {integrity: sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==} cpu: [x64] os: [linux] + libc: [musl] '@img/sharp-linux-arm64@0.34.5': resolution: {integrity: sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [arm64] os: [linux] + libc: [glibc] '@img/sharp-linux-arm@0.34.5': resolution: {integrity: sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [arm] os: [linux] + libc: [glibc] '@img/sharp-linux-ppc64@0.34.5': resolution: {integrity: sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [ppc64] os: [linux] + libc: [glibc] '@img/sharp-linux-riscv64@0.34.5': resolution: {integrity: sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [riscv64] os: [linux] + libc: [glibc] '@img/sharp-linux-s390x@0.34.5': resolution: {integrity: sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [s390x] os: [linux] + libc: [glibc] '@img/sharp-linux-x64@0.34.5': resolution: {integrity: sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [x64] os: [linux] + libc: [glibc] '@img/sharp-linuxmusl-arm64@0.34.5': resolution: {integrity: sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [arm64] os: [linux] + libc: [musl] '@img/sharp-linuxmusl-x64@0.34.5': resolution: {integrity: sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [x64] os: [linux] + libc: [musl] '@img/sharp-wasm32@0.34.5': resolution: {integrity: sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==} @@ -1289,24 +1315,28 @@ packages: engines: {node: '>= 10'} cpu: [arm64] os: [linux] + libc: [glibc] '@next/swc-linux-arm64-musl@15.5.7': resolution: {integrity: sha512-nfymt+SE5cvtTrG9u1wdoxBr9bVB7mtKTcj0ltRn6gkP/2Nu1zM5ei8rwP9qKQP0Y//umK+TtkKgNtfboBxRrw==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] + libc: [musl] '@next/swc-linux-x64-gnu@15.5.7': resolution: {integrity: sha512-hvXcZvCaaEbCZcVzcY7E1uXN9xWZfFvkNHwbe/n4OkRhFWrs1J1QV+4U1BN06tXLdaS4DazEGXwgqnu/VMcmqw==} engines: {node: '>= 10'} cpu: [x64] os: [linux] + libc: [glibc] '@next/swc-linux-x64-musl@15.5.7': resolution: {integrity: sha512-4IUO539b8FmF0odY6/SqANJdgwn1xs1GkPO5doZugwZ3ETF6JUdckk7RGmsfSf7ws8Qb2YB5It33mvNL/0acqA==} engines: {node: '>= 10'} cpu: [x64] os: [linux] + libc: [musl] '@next/swc-win32-arm64-msvc@15.5.7': resolution: {integrity: sha512-CpJVTkYI3ZajQkC5vajM7/ApKJUOlm6uP4BknM3XKvJ7VXAvCqSjSLmM0LKdYzn6nBJVSjdclx8nYJSa3xlTgQ==} @@ -1372,71 +1402,85 @@ packages: resolution: {integrity: sha512-t4ONHboXi/3E0rT6OZl1pKbl2Vgxf9vJfWgmUoCEVQVxhW6Cw/c8I6hbbu7DAvgp82RKiH7TpLwxnJeKv2pbsw==} cpu: [arm] os: [linux] + libc: [glibc] '@rollup/rollup-linux-arm-musleabihf@4.59.0': resolution: {integrity: sha512-CikFT7aYPA2ufMD086cVORBYGHffBo4K8MQ4uPS/ZnY54GKj36i196u8U+aDVT2LX4eSMbyHtyOh7D7Zvk2VvA==} cpu: [arm] os: [linux] + libc: [musl] '@rollup/rollup-linux-arm64-gnu@4.59.0': resolution: {integrity: sha512-jYgUGk5aLd1nUb1CtQ8E+t5JhLc9x5WdBKew9ZgAXg7DBk0ZHErLHdXM24rfX+bKrFe+Xp5YuJo54I5HFjGDAA==} cpu: [arm64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-arm64-musl@4.59.0': resolution: {integrity: sha512-peZRVEdnFWZ5Bh2KeumKG9ty7aCXzzEsHShOZEFiCQlDEepP1dpUl/SrUNXNg13UmZl+gzVDPsiCwnV1uI0RUA==} cpu: [arm64] os: [linux] + libc: [musl] '@rollup/rollup-linux-loong64-gnu@4.59.0': resolution: {integrity: sha512-gbUSW/97f7+r4gHy3Jlup8zDG190AuodsWnNiXErp9mT90iCy9NKKU0Xwx5k8VlRAIV2uU9CsMnEFg/xXaOfXg==} cpu: [loong64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-loong64-musl@4.59.0': resolution: {integrity: sha512-yTRONe79E+o0FWFijasoTjtzG9EBedFXJMl888NBEDCDV9I2wGbFFfJQQe63OijbFCUZqxpHz1GzpbtSFikJ4Q==} cpu: [loong64] os: [linux] + libc: [musl] '@rollup/rollup-linux-ppc64-gnu@4.59.0': resolution: {integrity: sha512-sw1o3tfyk12k3OEpRddF68a1unZ5VCN7zoTNtSn2KndUE+ea3m3ROOKRCZxEpmT9nsGnogpFP9x6mnLTCaoLkA==} cpu: [ppc64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-ppc64-musl@4.59.0': resolution: {integrity: sha512-+2kLtQ4xT3AiIxkzFVFXfsmlZiG5FXYW7ZyIIvGA7Bdeuh9Z0aN4hVyXS/G1E9bTP/vqszNIN/pUKCk/BTHsKA==} cpu: [ppc64] os: [linux] + libc: [musl] '@rollup/rollup-linux-riscv64-gnu@4.59.0': resolution: {integrity: sha512-NDYMpsXYJJaj+I7UdwIuHHNxXZ/b/N2hR15NyH3m2qAtb/hHPA4g4SuuvrdxetTdndfj9b1WOmy73kcPRoERUg==} cpu: [riscv64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-riscv64-musl@4.59.0': resolution: {integrity: sha512-nLckB8WOqHIf1bhymk+oHxvM9D3tyPndZH8i8+35p/1YiVoVswPid2yLzgX7ZJP0KQvnkhM4H6QZ5m0LzbyIAg==} cpu: [riscv64] os: [linux] + libc: [musl] '@rollup/rollup-linux-s390x-gnu@4.59.0': resolution: {integrity: sha512-oF87Ie3uAIvORFBpwnCvUzdeYUqi2wY6jRFWJAy1qus/udHFYIkplYRW+wo+GRUP4sKzYdmE1Y3+rY5Gc4ZO+w==} cpu: [s390x] os: [linux] + libc: [glibc] '@rollup/rollup-linux-x64-gnu@4.24.0': resolution: {integrity: sha512-ZXFk7M72R0YYFN5q13niV0B7G8/5dcQ9JDp8keJSfr3GoZeXEoMHP/HlvqROA3OMbMdfr19IjCeNAnPUG93b6A==} cpu: [x64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-x64-gnu@4.59.0': resolution: {integrity: sha512-3AHmtQq/ppNuUspKAlvA8HtLybkDflkMuLK4DPo77DfthRb71V84/c4MlWJXixZz4uruIH4uaa07IqoAkG64fg==} cpu: [x64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-x64-musl@4.59.0': resolution: {integrity: sha512-2UdiwS/9cTAx7qIUZB/fWtToJwvt0Vbo0zmnYt7ED35KPg13Q0ym1g442THLC7VyI6JfYTP4PiSOWyoMdV2/xg==} cpu: [x64] os: [linux] + libc: [musl] '@rollup/rollup-openbsd-x64@4.59.0': resolution: {integrity: sha512-M3bLRAVk6GOwFlPTIxVBSYKUaqfLrn8l0psKinkCFxl4lQvOSz8ZrKDz2gxcBwHFpci0B6rttydI4IpS4IS/jQ==} @@ -2277,24 +2321,28 @@ packages: engines: {node: '>= 12.0.0'} cpu: [arm64] os: [linux] + libc: [glibc] lightningcss-linux-arm64-musl@1.30.2: resolution: {integrity: sha512-5Vh9dGeblpTxWHpOx8iauV02popZDsCYMPIgiuw97OJ5uaDsL86cnqSFs5LZkG3ghHoX5isLgWzMs+eD1YzrnA==} engines: {node: '>= 12.0.0'} cpu: [arm64] os: [linux] + libc: [musl] lightningcss-linux-x64-gnu@1.30.2: resolution: {integrity: sha512-Cfd46gdmj1vQ+lR6VRTTadNHu6ALuw2pKR9lYq4FnhvgBc4zWY1EtZcAc6EffShbb1MFrIPfLDXD6Xprbnni4w==} engines: {node: '>= 12.0.0'} cpu: [x64] os: [linux] + libc: [glibc] lightningcss-linux-x64-musl@1.30.2: resolution: {integrity: sha512-XJaLUUFXb6/QG2lGIW6aIk6jKdtjtcffUT0NKvIqhSBY3hh9Ch+1LCeH80dR9q9LBjG3ewbDjnumefsLsP6aiA==} engines: {node: '>= 12.0.0'} cpu: [x64] os: [linux] + libc: [musl] lightningcss-win32-arm64-msvc@1.30.2: resolution: {integrity: sha512-FZn+vaj7zLv//D/192WFFVA0RgHawIcHqLX9xuWiQt7P0PtdFEVaxgF9rjM/IRYHQXNnk61/H/gb2Ei+kUQ4xQ==} @@ -3178,6 +3226,11 @@ packages: resolution: {integrity: sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==} engines: {node: '>=0.4'} + yaml@2.8.3: + resolution: {integrity: sha512-AvbaCLOO2Otw/lW5bmh9d/WEdcDFdQp2Z2ZUH3pX9U2ihyUY0nvLv7J6TrWowklRGPYbB/IuIMfYgxaCPg5Bpg==} + engines: {node: '>= 14.6'} + hasBin: true + yoctocolors@2.1.2: resolution: {integrity: sha512-CzhO+pFNo8ajLM2d2IW/R93ipy99LWjtwblvC1RsoSUMZgyLbYFr221TnSNT7GjGdYui6P459mw9JH/g/zW2ug==} engines: {node: '>=18'} @@ -4139,13 +4192,13 @@ snapshots: chai: 5.3.3 tinyrainbow: 2.0.0 - '@vitest/mocker@3.1.3(vite@6.4.1(@types/node@22.19.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.19.3))': + '@vitest/mocker@3.1.3(vite@6.4.1(@types/node@22.19.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.19.3)(yaml@2.8.3))': dependencies: '@vitest/spy': 3.1.3 estree-walker: 3.0.3 magic-string: 0.30.21 optionalDependencies: - vite: 6.4.1(@types/node@22.19.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.19.3) + vite: 6.4.1(@types/node@22.19.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.19.3)(yaml@2.8.3) '@vitest/pretty-format@3.1.3': dependencies: @@ -5099,13 +5152,14 @@ snapshots: pkce-challenge@5.0.1: {} - postcss-load-config@6.0.1(jiti@2.6.1)(postcss@8.5.6)(tsx@4.19.3): + postcss-load-config@6.0.1(jiti@2.6.1)(postcss@8.5.6)(tsx@4.19.3)(yaml@2.8.3): dependencies: lilconfig: 3.1.3 optionalDependencies: jiti: 2.6.1 postcss: 8.5.6 tsx: 4.19.3 + yaml: 2.8.3 postcss@8.4.31: dependencies: @@ -5481,7 +5535,7 @@ snapshots: tslib@2.8.1: {} - tsup@8.4.0(jiti@2.6.1)(postcss@8.5.6)(tsx@4.19.3)(typescript@5.6.3): + tsup@8.4.0(jiti@2.6.1)(postcss@8.5.6)(tsx@4.19.3)(typescript@5.6.3)(yaml@2.8.3): dependencies: bundle-require: 5.1.0(esbuild@0.25.12) cac: 6.7.14 @@ -5491,7 +5545,7 @@ snapshots: esbuild: 0.25.12 joycon: 3.1.1 picocolors: 1.1.1 - postcss-load-config: 6.0.1(jiti@2.6.1)(postcss@8.5.6)(tsx@4.19.3) + postcss-load-config: 6.0.1(jiti@2.6.1)(postcss@8.5.6)(tsx@4.19.3)(yaml@2.8.3) resolve-from: 5.0.0 rollup: 4.59.0 source-map: 0.8.0-beta.0 @@ -5566,13 +5620,13 @@ snapshots: vary@1.1.2: {} - vite-node@3.1.3(@types/node@22.19.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.19.3): + vite-node@3.1.3(@types/node@22.19.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.19.3)(yaml@2.8.3): dependencies: cac: 6.7.14 debug: 4.4.3 es-module-lexer: 1.7.0 pathe: 2.0.3 - vite: 6.4.1(@types/node@22.19.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.19.3) + vite: 6.4.1(@types/node@22.19.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.19.3)(yaml@2.8.3) transitivePeerDependencies: - '@types/node' - jiti @@ -5587,7 +5641,7 @@ snapshots: - tsx - yaml - vite@6.4.1(@types/node@22.19.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.19.3): + vite@6.4.1(@types/node@22.19.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.19.3)(yaml@2.8.3): dependencies: esbuild: 0.25.12 fdir: 6.5.0(picomatch@4.0.4) @@ -5602,11 +5656,12 @@ snapshots: lightningcss: 1.30.2 terser: 5.44.1 tsx: 4.19.3 + yaml: 2.8.3 - vitest@3.1.3(@types/node@22.19.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.19.3): + vitest@3.1.3(@types/node@22.19.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.19.3)(yaml@2.8.3): dependencies: '@vitest/expect': 3.1.3 - '@vitest/mocker': 3.1.3(vite@6.4.1(@types/node@22.19.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.19.3)) + '@vitest/mocker': 3.1.3(vite@6.4.1(@types/node@22.19.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.19.3)(yaml@2.8.3)) '@vitest/pretty-format': 3.2.4 '@vitest/runner': 3.1.3 '@vitest/snapshot': 3.1.3 @@ -5623,8 +5678,8 @@ snapshots: tinyglobby: 0.2.15 tinypool: 1.1.1 tinyrainbow: 2.0.0 - vite: 6.4.1(@types/node@22.19.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.19.3) - vite-node: 3.1.3(@types/node@22.19.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.19.3) + vite: 6.4.1(@types/node@22.19.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.19.3)(yaml@2.8.3) + vite-node: 3.1.3(@types/node@22.19.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.19.3)(yaml@2.8.3) why-is-node-running: 2.3.0 optionalDependencies: '@types/node': 22.19.3 @@ -5669,6 +5724,8 @@ snapshots: xtend@4.0.2: {} + yaml@2.8.3: {} + yoctocolors@2.1.2: {} zod-to-json-schema@3.25.2(zod@4.3.6): diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 8a460bd9..7c8e28cd 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -14,3 +14,10 @@ catalogs: '@clerk/nextjs': 6.31.2 next: 15.5.10 vite: 6.4.1 + +# Supply-chain hardening — see skills/stash-supply-chain-security/ +# 7 days in minutes; mirrors the Dependabot cooldown so manual + automated +# updates have the same community-discovery window. +minimumReleaseAge: 10080 +# Forbid git/tarball deps anywhere in the lockfile (pnpm ≥ 10.26). +blockExoticSubdeps: true diff --git a/skills/stash-supply-chain-security/SKILL.md b/skills/stash-supply-chain-security/SKILL.md new file mode 100644 index 00000000..d53fea90 --- /dev/null +++ b/skills/stash-supply-chain-security/SKILL.md @@ -0,0 +1,160 @@ +--- +name: stash-supply-chain-security +description: Supply-chain security controls for the @cipherstash/stack monorepo. Covers post-install script policy (onlyBuiltDependencies), install cooldown (minimumReleaseAge), lockfile integrity (blockExoticSubdeps + lockfile registry check), frozen-lockfile CI, registry pinning (.npmrc), Dependabot cooldown, and CODEOWNERS. Use when modifying CI workflows, pnpm config, dependency updates, .github/dependabot.yml, or anything that touches how packages enter the build. +--- + +# Supply Chain Security + +Controls applied in this repo to limit blast radius from compromised npm packages, lockfile injection, dependency confusion, and rushed dependency upgrades. Sourced from [lirantal/npm-security-best-practices](https://github.com/lirantal/npm-security-best-practices) and adapted for our pnpm workspace. + +## When to Use This Skill + +- Modifying any file under `.github/workflows/` +- Editing `pnpm-workspace.yaml`, `package.json` `pnpm` block, or `.npmrc` +- Updating `.github/dependabot.yml` or `.github/CODEOWNERS` +- Adding a dependency that needs a build script (i.e. `node-gyp`, `node-pty`, prebuilt binaries) +- Bypassing the install cooldown for a security fix +- Reviewing a PR that touches any of the above + +## What's Enforced (Config + Test Gate) + +Each control below is validated by `e2e/tests/supply-chain.e2e.test.ts` — the test suite fails CI if a control regresses, so silent removal isn't possible. + +### 1. Post-install scripts disabled by default — practice #1 + +pnpm 10+ disables lifecycle scripts globally and only runs them for packages on the `onlyBuiltDependencies` allowlist. + +- **Where**: `package.json` `pnpm.onlyBuiltDependencies` +- **Current allowlist**: `["node-pty"]` (PTY tests need the native module built) +- **Test asserts**: allowlist length ≤ 3 — adding a fourth entry forces explicit review + +### 2. Install cooldown — practice #2 + +New package versions wait 7 days before they're eligible for install. Mirrors the Dependabot cooldown so manual + automated updates have the same community-discovery window. + +- **Where**: `pnpm-workspace.yaml` `minimumReleaseAge: 10080` (minutes) +- **Test asserts**: ≥ 4320 minutes (3 days) + +### 3. Lockfile injection prevented — practices #4, #16 + +Two layers: + +- `pnpm-workspace.yaml` `blockExoticSubdeps: true` — pnpm refuses to install transitive deps that come from git or direct tarballs (pnpm ≥ 10.26) +- A test parses `pnpm-lock.yaml` and asserts every resolved tarball URL starts with `https://registry.npmjs.org/` + +(Why not `lockfile-lint`? It only supports npm/yarn lockfiles. The pnpm-native test gives us the same protection.) + +### 4. Frozen lockfile in CI — practice #5 + +CI uses `pnpm install --frozen-lockfile`. If `pnpm-lock.yaml` and any `package.json` drift, the install aborts — no silent registry fetches that bypass the locked versions. + +- **Where**: `.github/workflows/tests.yml` +- **Test asserts**: every `pnpm install` invocation in tests.yml carries `--frozen-lockfile` + +### 5. Cooldown'd auto-updates — practice #6 + +Dependabot opens grouped, cooldown'd PRs (7 days minor/patch, 14 days major) for both `npm` and `github-actions`. Major bumps stay un-grouped — one PR each, easier to review. + +- **Where**: `.github/dependabot.yml` +- **Test asserts**: cooldown ≥ 3 days, both ecosystems present + +### 6. Registry pinning — practice #16 + +`.npmrc` pins both the default registry and the `@cipherstash` scope to `https://registry.npmjs.org/`. Auth tokens stay in user-level `~/.npmrc` or env vars — never committed. + +- **Test asserts**: `.npmrc` contains both pin lines and no `_authToken` / `NPM_TOKEN` + +### 7. Governance (CODEOWNERS) + +`.github/CODEOWNERS` requires `@cipherstash/developers` review for every supply-chain critical file. Combined with branch protection (configured in repo settings, not in this repo), this prevents single-actor changes to the chain. + +- **Test asserts**: CODEOWNERS lists each critical path + +## What's Documented but Not Enforced + +These controls depend on developer environment or org-level configuration — we describe them here but don't gate CI on them. + +### Harden installs locally — practice #3 + +For local installs of new packages, consider running them through one of: + +- [`npq`](https://github.com/lirantal/npq) — security checks, package age, typosquatting, provenance: `npq install ` +- [Socket Firewall (`sfw`)](https://socket.dev) — real-time blocker for known-malicious packages: `sfw pnpm add ` + +Neither is required, but they're cheap insurance when adding a new direct dependency. + +### 2FA on npm accounts — practice #10 + +Every maintainer with publish access to `@cipherstash/*` should have: + +```bash +npm profile enable-2fa auth-and-writes +``` + +(This becomes mostly moot once the deferred OIDC-trusted-publisher migration lands — the workflow won't need long-lived tokens at all. See "Deferred" below.) + +### Reduce dependency tree — practice #13 + +Before adding a new direct dep, ask: + +- Does Node ≥ 22 (our minimum) already provide this? +- Is the package actively maintained? Check Snyk's database (security.snyk.io) — practice #14 +- What does `npm pack ` show in the actual tarball? (npmjs.org's web view can lie — practice #15) + +### Secrets in CI + +`tests.yml` writes `.env` files at CI time from GitHub Secrets. This is acceptable: secrets are never committed, scoped to the runner, and rotate via the GitHub UI. The `.env` files exist only for the lifetime of the job. + +Do **not** commit any `.env` file to the repo. + +## What's Deferred (Follow-Up PR) + +These need npmjs.com-side configuration and are tracked separately: + +- **Provenance attestations** — practice #11 +- **OIDC trusted publishing** — practice #12 + +Both require the npm org admin to register each `@cipherstash/*` package as a Trusted Publisher (cipherstash/stack repo + release.yml). Once that's done, `release.yml` can drop `NPM_TOKEN` entirely, run `npm publish` with `id-token: write`, and provenance is auto-generated. + +## Common Operations + +### Add a dependency that needs a build script + +1. Vet the package: latest version, active maintenance, reasonable download counts, source visible on GitHub. +2. Run `npm pack ` and inspect the tarball — confirm the install script is what you expect. +3. Add to `package.json` `pnpm.onlyBuiltDependencies`: + ```json + "pnpm": { + "onlyBuiltDependencies": ["node-pty", "your-new-package"] + } + ``` +4. Update the supply-chain test's allowlist threshold if you'd be adding the 4th entry — and explain in the PR why the count needs to grow. +5. Run `pnpm install` to confirm the build script executes. + +### Bypass the install cooldown for a security fix + +When CVE response needs a patch faster than 7 days: + +```bash +# pnpm flag for a one-off install: +pnpm install @ --ignore-workspace-min-release-age +``` + +Document the bypass in the PR description (CVE ID, why the cooldown was the bottleneck) so the next reviewer can follow the reasoning. + +### Add a new dev dependency + +No special steps — Dependabot will pick it up on the next weekly run (after the cooldown window). For immediate use, just `pnpm add -D `. + +### Change a CI workflow + +CODEOWNERS will request review from `@cipherstash/developers`. The supply-chain test will fail if the change drops `--frozen-lockfile` or downgrades Node. + +## Reference + +- Source: [lirantal/npm-security-best-practices](https://github.com/lirantal/npm-security-best-practices) +- Test gate: [`e2e/tests/supply-chain.e2e.test.ts`](../../e2e/tests/supply-chain.e2e.test.ts) +- pnpm config: [`pnpm-workspace.yaml`](../../pnpm-workspace.yaml), root `package.json` `pnpm` block +- CI: [`.github/workflows/tests.yml`](../../.github/workflows/tests.yml) +- Updates: [`.github/dependabot.yml`](../../.github/dependabot.yml) +- Governance: [`.github/CODEOWNERS`](../../.github/CODEOWNERS)