11import { readFileSync } from 'node:fs'
22import { dirname , join , resolve } from 'node:path'
33import { fileURLToPath } from 'node:url'
4+ import semver from 'semver'
45import { describe , expect , it } from 'vitest'
56import { parse as parseYaml } from 'yaml'
67
@@ -16,6 +17,27 @@ const read = (p: string) => readFileSync(join(REPO_ROOT, p), 'utf8')
1617const readJson = ( p : string ) => JSON . parse ( read ( p ) )
1718const readYaml = ( p : string ) => parseYaml ( read ( p ) )
1819
20+ // Map every package in the lockfile's `packages:` section to its resolved
21+ // versions. Keys there are bare `name@version` (peer suffixes live under
22+ // `snapshots:`), and scoped names keep their leading `@`, so split on the
23+ // LAST `@`; strip any stray peer suffix defensively.
24+ const resolvedVersionsByName = ( ) : Map < string , string [ ] > => {
25+ const lock = readYaml ( 'pnpm-lock.yaml' ) as {
26+ packages ?: Record < string , unknown >
27+ }
28+ const byName = new Map < string , string [ ] > ( )
29+ for ( const key of Object . keys ( lock . packages ?? { } ) ) {
30+ const at = key . lastIndexOf ( '@' )
31+ if ( at <= 0 ) continue // no scope-only or malformed keys
32+ const name = key . slice ( 0 , at )
33+ const version = key . slice ( at + 1 ) . split ( '(' ) [ 0 ]
34+ const list = byName . get ( name )
35+ if ( list ) list . push ( version )
36+ else byName . set ( name , [ version ] )
37+ }
38+ return byName
39+ }
40+
1941describe ( 'supply chain — pnpm configuration' , ( ) => {
2042 it ( 'packageManager is pnpm ≥ 10.26 (needed for blockExoticSubdeps)' , ( ) => {
2143 const pm = readJson ( 'package.json' ) . packageManager as string
@@ -46,6 +68,44 @@ describe('supply chain — pnpm configuration', () => {
4668 expect ( Array . isArray ( allow ) ) . toBe ( true )
4769 expect ( allow . length ) . toBeLessThanOrEqual ( 3 )
4870 } )
71+
72+ it ( 'minimumReleaseAgeExclude contains only first-party packages' , ( ) => {
73+ // The cooldown exclusion list exists for first-party packages that ship
74+ // on their own release cadence. Third-party security fixes must use the
75+ // one-off bypass (`pnpm install --config.minimum-release-age=0` with an
76+ // exact pin) instead — a name-scoped exclusion exempts every future
77+ // release of the package. See SKILL.md "Bypass the install cooldown".
78+ const ws = readYaml ( 'pnpm-workspace.yaml' ) as {
79+ minimumReleaseAgeExclude ?: string [ ]
80+ }
81+ const FIRST_PARTY = [ / ^ @ p r i s m a - n e x t \/ / , / ^ @ c i p h e r s t a s h \/ / ]
82+ for ( const entry of ws . minimumReleaseAgeExclude ?? [ ] ) {
83+ expect (
84+ FIRST_PARTY . some ( ( re ) => re . test ( entry ) ) ,
85+ `"${ entry } " is not a first-party cooldown exclusion` ,
86+ ) . toBe ( true )
87+ }
88+ } )
89+
90+ it ( 'security overrides stay range-scoped and remain a small allowlist (≤12 entries)' , ( ) => {
91+ // Every override must be scoped to the advisory's vulnerable range
92+ // (`pkg@<range>`), never a blanket `pkg` pin — a blanket pin silently
93+ // rewrites versions outside the vulnerable range forever. The count cap
94+ // mirrors onlyBuiltDependencies: growth forces a conscious review.
95+ const ws = readYaml ( 'pnpm-workspace.yaml' ) as {
96+ overrides ?: Record < string , string >
97+ }
98+ const selectors = Object . keys ( ws . overrides ?? { } )
99+ expect ( selectors . length ) . toBeLessThanOrEqual ( 12 )
100+ for ( const selector of selectors ) {
101+ // A version-scoped selector has an `@` after the package name
102+ // (position > 0 handles `@scope/pkg@range`).
103+ expect (
104+ selector . lastIndexOf ( '@' ) > 0 ,
105+ `override "${ selector } " is not scoped to a version range` ,
106+ ) . toBe ( true )
107+ }
108+ } )
49109} )
50110
51111describe ( 'supply chain — registry pinning (.npmrc)' , ( ) => {
@@ -90,6 +150,69 @@ describe('supply chain — pnpm-lock.yaml integrity', () => {
90150 }
91151 expect ( offenders ) . toEqual ( [ ] )
92152 } )
153+
154+ it ( 'every security override actually took effect (nothing left in a vulnerable range)' , ( ) => {
155+ // The shape test ("security overrides stay range-scoped") proves the
156+ // selectors are well-formed; this proves they *worked*. For each override
157+ // `selector -> target`, no package may resolve to a version that still
158+ // matches the vulnerable `selector` yet fails the `target` — that pair is
159+ // exactly a silent regression (e.g. a re-resolve demoting fast-uri below
160+ // its pin, un-fixing the advisory). Note `target` can sit inside its own
161+ // selector range (js-yaml@>=4.0.0 <5 -> 4.2.0 normalises all 4.x to the
162+ // patched release), so the check is "matched but not raised", not the
163+ // stricter "no version matches the selector".
164+ const ws = readYaml ( 'pnpm-workspace.yaml' ) as {
165+ overrides ?: Record < string , string >
166+ }
167+ const overrides = Object . entries ( ws . overrides ?? { } )
168+ // Guard the vacuous case: an empty/removed block would pass every loop
169+ // below with zero iterations.
170+ expect ( overrides . length ) . toBeGreaterThan ( 0 )
171+
172+ const byName = resolvedVersionsByName ( )
173+ const offenders : string [ ] = [ ]
174+ for ( const [ selector , target ] of overrides ) {
175+ const at = selector . lastIndexOf ( '@' )
176+ const name = selector . slice ( 0 , at )
177+ const vulnerableRange = selector . slice ( at + 1 )
178+ for ( const version of byName . get ( name ) ?? [ ] ) {
179+ if (
180+ semver . satisfies ( version , vulnerableRange ) &&
181+ ! semver . satisfies ( version , target )
182+ ) {
183+ offenders . push (
184+ `${ name } @${ version } still matches vulnerable "${ vulnerableRange } " (override target "${ target } " not applied)` ,
185+ )
186+ }
187+ }
188+ }
189+ expect ( offenders ) . toEqual ( [ ] )
190+ } )
191+
192+ it ( 'package.json has no top-level `overrides` (pnpm only reads pnpm-workspace.yaml)' , ( ) => {
193+ // pnpm silently ignores a top-level npm-format `overrides` block; the
194+ // security pins must live in pnpm-workspace.yaml `overrides`. Guards
195+ // against the block being moved back here, where it would look applied
196+ // but do nothing.
197+ const pkg = readJson ( 'package.json' ) as { overrides ?: unknown }
198+ expect ( pkg . overrides ) . toBeUndefined ( )
199+ } )
200+
201+ it ( '@anthropic-ai/sdk resolves to the peer-pinned patched version (≥ 0.106.0)' , ( ) => {
202+ // Not an override but a peer-resolution pin: packages/wizard depends on
203+ // @anthropic -ai/sdk@^0.106.0 to force the auto-installed peer of
204+ // @anthropic -ai/claude-agent-sdk past the advisory-vulnerable 0.81.0
205+ // (GHSA-p7fg-763f-g4gf). The override-effect test cannot cover a peer
206+ // pin, so assert the resolved version directly.
207+ const versions = resolvedVersionsByName ( ) . get ( '@anthropic-ai/sdk' ) ?? [ ]
208+ expect ( versions . length ) . toBeGreaterThan ( 0 )
209+ for ( const version of versions ) {
210+ expect (
211+ semver . gte ( version , '0.106.0' ) ,
212+ `@anthropic-ai/sdk@${ version } is below the patched 0.106.0` ,
213+ ) . toBe ( true )
214+ }
215+ } )
93216} )
94217
95218describe ( 'supply chain — CI hardening (.github/workflows/tests.yml)' , ( ) => {
0 commit comments