Skip to content

Commit d0c56a2

Browse files
fix(cli,create-objectstack): scaffolds override uuid below 12.0.0 to ^11.1.1, out of GHSA-w5hq-g745-h8pq (#25197)
Fixes #25174 Clause-②: no Metadata-protocol: none ## What this changes Every standalone scaffold's npm `overrides` block gains a `uuid` entry that routes every `uuid` edge below 12.0.0 to `^11.1.1`, out of GHSA-w5hq-g745-h8pq (`uuid` below 11.1.1). The scaffolds are the `create-objectstack` blank template, `os init -t app`, `-t plugin` and `-t empty`, and `os create plugin` (standalone). - `packages/cli/src/commands/init.ts`: two new constants, `SCAFFOLD_UUID_OVERRIDE_SELECTOR` (the npm override key: `uuid@`, a less-than sign, `12.0.0`) and `SCAFFOLD_UUID_OVERRIDE_RANGE` (`^11.1.1`). `SCAFFOLD_NPM_OVERRIDES` is built from them and from `SCAFFOLD_VITEST_RANGE`, so `os init` and `os create` emit it with no other code change. - `scripts/sync-scaffold-emission-policy.mjs`: one new `POLICY_STAMPS` row stamps the value into the bundled template from the constant (the same path PR #25173 used for vitest). The field regex now escapes the key, because the selector contains `.`. The self-test fixtures declare the new constant and key, with one new assertion (61 assertions in total). - `packages/create-objectstack/src/templates/blank/package.json`: the stamped entry, written by the script's write run. - Tests: `packages/cli/test/scaffold-npm-overrides.test.ts` pins the map, the target's floor (at or above the fix for its major) and the selector's bound (the major right above the target). `packages/cli/test/scaffold-emission-policy.e2e.test.ts` (nightly tier) adds a check that the on-ramp, `os init` and `os create` emit the SAME whole `overrides` block. The sync script's `--check` compares values only and cannot see a key spelled differently from the constant. - Changeset: `@objectstack/cli` and `create-objectstack`, patch. It includes the one-line fix for existing projects and for other npm consumers. `@objectstack/rest` is not changed: no route at its level reaches npm consumers (measured below). ## Route measurements (npm 10.9.9 and npm 11.21.1) Method: each manifest in its own empty directory under the scratchpad, with a fresh `--cache` per leg, and one install tree at a time. The `@objectstack/*` ranges `^18.0.0` resolve to the published 18.0.0. The `os init` / `os create` manifests were emitted by this tree's source CLI (`bin/run-dev.js`), and the blank template was taken from the tree. `uuid` was read from `node_modules`, then `npm audit --json` was run. Two npm 10 audits and one npm 11 audit hit a registry endpoint error (400 on `audits/quick` after the bulk endpoint failed, and a 503 on `advisories/bulk`). Those were re-run, and only completed audits are counted. | Route | Manifests | npm 10.9.9 | npm 11.21.1 | |---|---|---|---| | Baseline (main at b6e8707, with #25173) | blank, init -t app, -t plugin, -t empty | exit 0, `uuid` 8.3.2, audit 9 moderate, all GHSA-w5hq-g745-h8pq | same | | Baseline | os create plugin (no `@objectstack/rest` in its closure) | exit 0, no `uuid`, audit 0 | same | | A: newer exceljs | n/a | no such release: `latest` is 4.4.0 (2023-10-19), and `4.4.1-prerelease.0` also declares `uuid ^8.3.0` | same | | B: `overrides` inside the published `@objectstack/rest` (18.0.0 repacked with `overrides.uuid ^11.1.1`, installed as a dependency) | bare consumer | exit 0, `uuid` 8.3.2, audit 3 moderate (rest, exceljs, uuid): **ignored** | same, **ignored** | | C: scaffold `overrides`, selector `uuid@` + less-than `12.0.0` → `^11.1.1` (**chosen**) | blank, init -t app, -t plugin, -t empty | exit 0, `uuid` 11.1.1, audit 0 | same | | C | os create plugin | exit 0, no `uuid`, audit 0 (the entry is inert) | same | | C': the same with a bare `uuid` key | bare `@objectstack/rest` consumer | exit 0, `uuid` 11.1.1, audit 0 | same | | D: replace the dependency (`exceljs` aliased to the `exceljs-community@5.2.1` fork, as a published swap would resolve) | blank; bare consumer | exit 0, no `uuid`, audit 0 | same | Follow-up on a consumer of the published `@objectstack/rest` 18.0.0, root override as in C or C'. A later `npm install uuid@14` exits 0 under C (declares `^14.0.3`, exceljs keeps 11.1.1), and exits 1 with `EOVERRIDE` under C'. A spec-less `npm install uuid` exits 1 with `EOVERRIDE: Override for uuid@* conflicts with direct dependency` under both. Same on both npm versions. This is why the key is a selector and not bare `uuid`. **Runtime under route C (H3), measured rather than read from a changelog.** On the installed blank and `os init -t app` trees, on both npm versions, exceljs resolved `uuid` 11.1.1. A buffered write with a `dataBar` rule ran: `CfRuleExtXform.prepare` calls `uuid.v4()`, and the sheet carried 2 generated ids. The streaming `WorkbookWriter` ran, the shape `createXlsxStream` bridges onto HTTP. A buffered `load` ran. `@objectstack/rest`'s own `parseXlsxToRows` read both workbooks back to the expected rows. exceljs calls no other `uuid` export. In the monorepo, the same pairing (exceljs 4.4.0 on `uuid` 11.1.1, through the workspace override) runs the 16 rest test files that touch xlsx: 16 passed, 737 tests. ## Why C, by the four axes - **Actual business need:** the measured reach is every fresh npm-installed scaffold (9 entries). C closes exactly that reach, and B does not reach it at all. D also closes it for every npm consumer, not only scaffolds. - **Long-term soundness:** C uses the mechanism this repo already uses for the same advisory (the workspace's pnpm override, same selector and range), so a scaffold resolves the `uuid` the monorepo's exceljs suites run on. It has a written retirement re-check on the constant. D would move a published runtime dependency onto a fork. The measured candidate is maintained by one person, was created 2026-08-09 and declares `engines.node >=22.12.0`, narrower than `@objectstack/rest`'s `>=22.0.0`. That is a supply-chain decision, not a dependency bump. - **Hard for AI to get wrong:** one constant feeds all three scaffolders. The value is stamped, and the key is held equal by the nightly e2e check. Floor and selector pins red when the advisory boundary is left. Both ablations are below. - **Startup focus:** no new dependency, and no change to any published package's runtime. It adds one map entry and one stamp row. ## Acceptance notes - **Residual reach, by design and stated in the changeset:** an npm project that installs `@objectstack/rest` other than through a fresh scaffold still audits GHSA-w5hq-g745-h8pq. This covers projects made before this change and hand-written projects, and the bare consumer measured 3 moderate entries on npm 10.9.9 and 11.21.1. npm reads `overrides` only from the root manifest (route B), so only replacing `exceljs` (route D) reaches them. That choice is raised for the maintainer in the report on #25174, and is not made here. - Severity, as each source states it (H4): the `pnpm-workspace.yaml` comment says "high". The npm registry's bulk advisory endpoint says `moderate`, with a CVSS 3.1 vector scoring 7.5. `npm audit` in the scaffolds says moderate. GitHub's global advisory API was not readable from this session (403, session-bound). - `scripts/sync-scaffold-emission-policy.mjs`'s `devDependencies.vitest` row comment still says the constant holds why the range is `~4.0.0`. The range has been `~4.1.12` since PR #25173. The comment is stale, has no runtime effect and is outside this change. Carrier: none. ## Verification All readings are at head `50d7f9c25`, which is this branch with `origin/main` `f7d740376` merged in. The two incoming commits touch `cli` migrate code and `platform-objects` translations, and none of these 6 paths. After the merge: `pnpm install` was unchanged (no lockfile or manifest moved), the non-docs build was 76/76, and `packages/runtime/.objectstack` was cleared. - `pnpm --filter @objectstack/cli exec vitest run --project unit --maxWorkers=2` under the verify lock: VERDICT command-exit 0, 323 test files passed, 4772 tests passed. The CLI integration tier is left to CI. The one integration-tier file in this diff is in the nightly tier, so it was run on its own: `OS_TEST_TIERS=nightly … vitest run --project integration test/scaffold-emission-policy.e2e.test.ts`, 1 file, 11 tests passed. - `pnpm --filter create-objectstack test`: 18 files, 278 tests passed. `pnpm --filter @objectstack/cli typecheck && pnpm --filter create-objectstack typecheck`: VERDICT command-exit 0. The cli `tsc -p tsconfig.test.json --listFiles` lists both edited cli test files. - `@objectstack/rest` xlsx export and import tests (the 16 files under `packages/rest/src` that touch xlsx or exceljs): 16 files, 737 tests passed. `@objectstack/rest` is unchanged; these run exceljs on `uuid` 11.1.1 through the workspace override. - `node scripts/sync-scaffold-emission-policy.mjs --self-test`: exit 0, 61 assertions. Its `--check` exited 1 on an unstamped key (`overrides.uuid@`… is `"0.0.0-unstamped"` but the policy declares `"^11.1.1"`). The write run then stamped it (1 value rewritten), and `--check` exited 0. - eslint `--no-inline-config` over the 4 changed code files: exit 0. Control-byte scan over the 6 changed files: no hits. - **Ablations**, run at `e7cf02f09` with the fix committed first, through `scripts/ablation-replace.mjs` in wrap mode. The later commits change only comments in `init.ts`. The tests import `../src`, so no `dist/` is on their path. 1. `SCAFFOLD_UUID_OVERRIDE_RANGE` changed from `^11.1.1` to `^11.1.0` (anchor 1 to 0, blob `827cf1e96177` to `33bed46cc4ef`). Result: `Tests 1 failed | 13 passed (14)`, failing on `admits 11.1.0, below the fix 11.1.1`. Restored: blob equals HEAD, `git diff HEAD` empty. 2. `SCAFFOLD_UUID_OVERRIDE_SELECTOR` with its bound moved to `13.0.0` (anchor 1 to 0, blob `827cf1e96177` to `98fd9e5105ca`). Result: unit `1 failed | 13 passed (14)` (`expected 13 to be 12`), and nightly e2e `1 failed | 10 passed (11)` (`npx create-objectstack must emit the shared npm overrides block`). That second failure is the key-spelling drift the stamp script cannot see. Restored: blob equals HEAD, `git diff HEAD` empty. - Gates: `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack` derived 103 commands over 6 paths, against merge base `f7d740376`. Each ran byte for byte, with its exit code recorded before any pipe: all 103 exited 0. `--ran`: `103 derived, 103 run, 0 NOT-MEASURED, 0 UNRUN`, exit 0. This includes `check:scaffold-emission-policy`, `check:template-version-sync`, `check:cross-package-test-inputs` and `check:pm-dispatch-gates`. `check-governed-merges --branch`: 0 of 6 paths governed, 220 changed lines. --- _Generated by [Claude Code](https://claude.ai/code/session_01AFrKwndmrCLtF5V9hNtrw3)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 2919ddb commit d0c56a2

6 files changed

Lines changed: 208 additions & 12 deletions

File tree

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
---
2+
'@objectstack/cli': patch
3+
'create-objectstack': patch
4+
---
5+
6+
fix(cli,create-objectstack): new projects override `uuid` below 12.0.0 to `^11.1.1`, so `npm audit` lists no GHSA-w5hq-g745-h8pq entry
7+
8+
Clause-②: no
9+
Metadata-protocol: none
10+
11+
`@objectstack/rest` depends on `exceljs ^4.4.0`, and exceljs 4.4.0, its newest release, depends on `uuid ^8.3.0`. GHSA-w5hq-g745-h8pq affects `uuid` below 11.1.1, so `npm audit` in a fresh npm-installed scaffold reported 9 moderate entries, all from that one advisory. The npm `overrides` block every standalone scaffold emits now carries `"uuid@<12.0.0": "^11.1.1"`: the `create-objectstack` blank template, `os init` (`-t app`, `-t plugin`, `-t empty`) and `os create plugin`. A new project resolves `uuid` 11.1.1 and `npm audit` lists no `uuid` entry, measured on npm 10.9.9 and 11.21.1. exceljs calls only `uuid`'s `v4()`, which `uuid` 11 keeps; exceljs's buffered and streaming writers, its loader and `@objectstack/rest`'s `parseXlsxToRows` were run on the installed tree. pnpm ignores npm's `overrides` field.
12+
13+
The selector stops at `<12.0.0`, so a project can still add a newer `uuid` of its own: `npm install uuid@14` installs. A spec-less `npm install uuid` is refused with `EOVERRIDE` (npm reads it as `uuid@*`, which meets the selector); name the major instead.
14+
15+
**An existing project made by an earlier `create-objectstack`, `os init` or `os create plugin`**, or any other npm project that installs `@objectstack/rest`, directly or through `@objectstack/runtime`, `@objectstack/cli`, `@objectstack/plugin-auth` or `@objectstack/verify`: add `"uuid@<12.0.0": "^11.1.1"` to the `overrides` block of its root `package.json` and run `npm install`. npm reads `overrides` only from the root manifest, so no `@objectstack/*` release can apply it for you.

‎packages/cli/src/commands/init.ts‎

Lines changed: 82 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -434,13 +434,89 @@ export const SCAFFOLD_TYPESCRIPT_RANGE = '^5.3.0';
434434
*/
435435
export const SCAFFOLD_VITEST_RANGE = '~4.1.12';
436436

437+
/**
438+
* The npm `overrides` SELECTOR that routes `uuid` out of GHSA-w5hq-g745-h8pq,
439+
* and the range it routes to ({@link SCAFFOLD_UUID_OVERRIDE_RANGE}).
440+
*
441+
* The advisory affects `uuid` below 11.1.1 (and 12.0.0 and 13.0.0, fixed in
442+
* 12.0.1 and 13.0.1). A scaffold reaches it through ONE edge:
443+
* `@objectstack/cli`, `@objectstack/runtime`, `@objectstack/plugin-auth` and
444+
* `@objectstack/verify` depend on `@objectstack/rest`, which depends on
445+
* `exceljs ^4.4.0`, which depends on `uuid ^8.3.0`. exceljs
446+
* 4.4.0 is its newest release, and its only later one, `4.4.1-prerelease.0`,
447+
* declares `uuid ^8.3.0` too, so no exceljs upgrade leaves the 8.x line.
448+
*
449+
* Why the scaffold and not `@objectstack/rest` itself: npm reads `overrides`
450+
* only from the ROOT manifest of an install. Measured 2026-10-22, each manifest
451+
* in an empty directory with a fresh `--cache`: the published
452+
* `@objectstack/rest` 18.0.0 repacked with `"overrides": { "uuid": "^11.1.1" }`
453+
* and installed as a dependency still resolves `uuid` 8.3.2 on npm 10.9.9 and
454+
* 11.21.1, and `npm audit` reports the same 3 entries as the unmodified
455+
* package. The monorepo's own `pnpm-workspace.yaml` override (the same
456+
* selector and range as here) has the same limit: it never reaches an npm
457+
* consumer. This block reaches the projects the scaffolders create, and no
458+
* other npm consumer of `@objectstack/rest`.
459+
*
460+
* The selector is `uuid@<12.0.0`, not a bare `uuid`, so that it rewrites only
461+
* edges that can resolve below the fix, and it stops at the major boundary so
462+
* that a later advisory on the 11.x line moves the range alone. It also leaves
463+
* a project free to add a newer `uuid` of its own. Measured on a consumer of
464+
* the published `@objectstack/rest` 18.0.0, npm 10.9.9 and 11.21.1: with this
465+
* selector, a later `npm install uuid@14` exits 0, declares `^14.0.3`, and
466+
* exceljs keeps `uuid` 11.1.1; with a bare `uuid` key it exits 1 with
467+
* `EOVERRIDE`. A spec-less `npm install uuid` exits 1 with `EOVERRIDE:
468+
* Override for uuid@* conflicts with direct dependency` under either key,
469+
* because npm reads it as `uuid@*`, which meets `<12.0.0`. Naming the major is
470+
* the fix, the same cost the `vitest` entry carries.
471+
*
472+
* Safe at runtime because exceljs calls exactly one `uuid` export, `v4()`, with
473+
* no arguments (`lib/xlsx/xform/sheet/cf-ext/cf-rule-ext-xform.js`), and
474+
* `uuid` 11 keeps it. The monorepo's own suites already run exceljs on `uuid`
475+
* 11.1.1 through the workspace override.
476+
*/
477+
export const SCAFFOLD_UUID_OVERRIDE_SELECTOR = 'uuid@<12.0.0';
478+
479+
/**
480+
* The `uuid` range {@link SCAFFOLD_UUID_OVERRIDE_SELECTOR} routes to: the
481+
* `legacy-11` line from its first release outside GHSA-w5hq-g745-h8pq, the
482+
* same target as the workspace override, so a scaffold resolves the `uuid` the
483+
* monorepo's own exceljs suites run on.
484+
*
485+
* Measured 2026-10-22, each manifest in an empty directory with a fresh
486+
* `--cache`, the `@objectstack/*` ranges resolving to the published 18.0.0
487+
* (blank template, `os init -t app`, `-t plugin` and `-t empty`):
488+
*
489+
* without the entry npm 10.9.9, 11.21.1 exit 0, `uuid` 8.3.2; `npm
490+
* audit` reports 9 moderate entries,
491+
* all GHSA-w5hq-g745-h8pq
492+
* with the entry npm 10.9.9, 11.21.1 exit 0, `uuid` 11.1.1; `npm
493+
* audit` lists no `uuid` entry
494+
*
495+
* `os create plugin` has no `@objectstack/rest` in its closure and audits
496+
* clean either way; the entry is inert there. Under the entry, exceljs's
497+
* buffered write (with a rule that calls `v4()`), its streaming
498+
* `WorkbookWriter`, its buffered load and `@objectstack/rest`'s own
499+
* `parseXlsxToRows` ran on the installed tree, on both npm versions.
500+
*
501+
* The re-check that lets this go: `npm ls uuid --all` in an emitted `-t app`
502+
* install WITHOUT the entry, on the newest npm 10 and on npm 11. When no path
503+
* resolves a `uuid` inside the advisory's range, delete it here, in
504+
* {@link SCAFFOLD_NPM_OVERRIDES}, and its `POLICY_STAMPS` row together.
505+
*/
506+
export const SCAFFOLD_UUID_OVERRIDE_RANGE = '^11.1.1';
507+
437508
/**
438509
* The npm `overrides` block every STANDALONE scaffold emits: `os init`'s three
439510
* templates, `os create plugin` outside the monorepo, and — by generation, not
440-
* import — `create-objectstack`'s bundled template (the `overrides.vitest` row
441-
* in `scripts/sync-scaffold-emission-policy.mjs`'s `POLICY_STAMPS`).
511+
* import — `create-objectstack`'s bundled template (the `overrides.vitest` and
512+
* `overrides.uuid@<12.0.0` rows in `scripts/sync-scaffold-emission-policy.mjs`'s
513+
* `POLICY_STAMPS`).
514+
*
515+
* It carries two entries with two reasons. `uuid` is the advisory route on
516+
* {@link SCAFFOLD_UUID_OVERRIDE_SELECTOR}. The rest of this comment is about
517+
* `vitest`.
442518
*
443-
* Why it exists. vitest 4.1.12 (published 2026-10-15) peers
519+
* Why the `vitest` entry exists. vitest 4.1.12 (published 2026-10-15) peers
444520
* `@vitest/browser-webdriverio ^4.1.11`, and that package's only match, 4.1.11,
445521
* peers `vitest: 4.1.11` exactly — a peer set that contradicts itself. A
446522
* scaffold reaches it through its own root vitest, now that the range is
@@ -487,6 +563,7 @@ export const SCAFFOLD_VITEST_RANGE = '~4.1.12';
487563
*/
488564
export const SCAFFOLD_NPM_OVERRIDES: Readonly<Record<string, string>> = Object.freeze({
489565
vitest: SCAFFOLD_VITEST_RANGE,
566+
[SCAFFOLD_UUID_OVERRIDE_SELECTOR]: SCAFFOLD_UUID_OVERRIDE_RANGE,
490567
});
491568

492569
/** The `@types/node` range a scaffolded project declares. */
@@ -570,7 +647,8 @@ export function renderScaffoldPackageJson(
570647
dependencies: template.dependencies,
571648
devDependencies: template.devDependencies,
572649
// Every template, whether or not it declares vitest itself — without it
573-
// npm 10 cannot install any of them (see SCAFFOLD_NPM_OVERRIDES).
650+
// npm 10 cannot install any of them, and `npm audit` reports the `uuid`
651+
// advisory exceljs pulls in (see SCAFFOLD_NPM_OVERRIDES).
574652
overrides: { ...SCAFFOLD_NPM_OVERRIDES },
575653
};
576654
}

‎packages/cli/test/scaffold-emission-policy.e2e.test.ts‎

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,7 @@ import { childEnv } from './helpers/serve-process.js';
7878
import {
7979
renderScaffoldPackageJson,
8080
renderScaffoldTsconfig,
81+
SCAFFOLD_NPM_OVERRIDES,
8182
SCAFFOLD_PNPM_RANGE,
8283
SCAFFOLD_TSCONFIG_INCLUDE_WITH_ROOT_CONFIG,
8384
SCAFFOLD_TYPES_NODE_RANGE,
@@ -421,6 +422,25 @@ describe('the on-ramp emits the same policy — measured by DRIVING it', () => {
421422
]);
422423
});
423424

425+
it('emits ONE npm overrides block across ALL THREE scaffolders, and it IS SCAFFOLD_NPM_OVERRIDES', () => {
426+
// The on-ramp receives its block by GENERATION, value by value, from
427+
// `POLICY_STAMPS` rows whose KEYS are spelled in the stamp script, the
428+
// `uuid@<12.0.0` selector among them. Its `--check` holds each VALUE to
429+
// the policy and cannot see a key spelled differently from the constant
430+
// `os init` and `os create` emit from. Comparing whole blocks here can.
431+
const expected = { ...SCAFFOLD_NPM_OVERRIDES };
432+
// Control: the block names more than vitest, so a comparison that only
433+
// ever met `{ vitest }` cannot pass for this one.
434+
expect(Object.keys(expected).length).toBeGreaterThan(1);
435+
for (const { id, manifest } of allScaffolderManifests()) {
436+
expect(
437+
manifest.overrides,
438+
`${id} must emit the shared npm overrides block. The on-ramp reaches it by generation: ` +
439+
'run `pnpm gen:scaffold-emission-policy`, and keep each POLICY_STAMPS override key equal to its constant',
440+
).toEqual(expected);
441+
}
442+
});
443+
424444
it('carries the committed template through unchanged — the generator ran, the build copied', () => {
425445
// The one place the committed template is read, and deliberately as a
426446
// CONSEQUENCE rather than as the expectation: the drive above already

‎packages/cli/test/scaffold-npm-overrides.test.ts‎

Lines changed: 53 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,8 @@
22

33
/**
44
* PIN — every standalone manifest `os init` and `os create` emit carries the
5-
* npm `overrides` block, built from the one vitest constant.
5+
* npm `overrides` block, built from the vitest constant and the two `uuid`
6+
* constants (the `uuid` entry is an advisory route; its pins are at the end).
67
*
78
* ## The defect
89
*
@@ -26,8 +27,8 @@
2627
* Every equality is DERIVED from the exported constants, never a literal: a
2728
* pin comparing against a spelled-out range would stay green on a tree where
2829
* the constant moved and one emission was left behind — the drift it exists
29-
* to catch. The one literal is a LOWER bound, the advisory's fixed release, in
30-
* the floor pin at the end of this file.
30+
* to catch. The literals are LOWER bounds, the advisories' fixed releases, in
31+
* the floor pins at the end of this file.
3132
*
3233
* 1. Each emission's `overrides.vitest` IS `SCAFFOLD_VITEST_RANGE`.
3334
* 2. Where an emission also declares `vitest` directly, the two are equal —
@@ -47,6 +48,8 @@ import { describe, it, expect } from 'vitest';
4748
import {
4849
renderScaffoldPackageJson,
4950
SCAFFOLD_NPM_OVERRIDES,
51+
SCAFFOLD_UUID_OVERRIDE_RANGE,
52+
SCAFFOLD_UUID_OVERRIDE_SELECTOR,
5053
SCAFFOLD_VITEST_RANGE,
5154
TEMPLATES,
5255
} from '../src/commands/init.js';
@@ -84,8 +87,10 @@ describe('npm overrides in every standalone scaffold manifest', () => {
8487
expect(direct.map((m) => m.id)).toEqual(expect.arrayContaining(['os init -t plugin', 'os create plugin']));
8588
});
8689

87-
it('the override map is built from the vitest constant', () => {
90+
it('the override map is built from the vitest and uuid constants, and nothing else', () => {
8891
expect(SCAFFOLD_NPM_OVERRIDES.vitest).toBe(SCAFFOLD_VITEST_RANGE);
92+
expect(SCAFFOLD_NPM_OVERRIDES[SCAFFOLD_UUID_OVERRIDE_SELECTOR]).toBe(SCAFFOLD_UUID_OVERRIDE_RANGE);
93+
expect(Object.keys(SCAFFOLD_NPM_OVERRIDES).sort()).toEqual(['vitest', SCAFFOLD_UUID_OVERRIDE_SELECTOR].sort());
8994
expect(Object.isFrozen(SCAFFOLD_NPM_OVERRIDES)).toBe(true);
9095
});
9196

@@ -143,3 +148,47 @@ describe('the scaffold vitest range stays outside GHSA-qxg6-89mg-rqv8', () => {
143148
expect(atOrAbove, `SCAFFOLD_VITEST_RANGE "${SCAFFOLD_VITEST_RANGE}" admits ${floor.join('.')}, below the fix ${fixed!.join('.')}`).toBe(true);
144149
});
145150
});
151+
152+
/**
153+
* The `uuid` override must keep routing exceljs's `uuid ^8.3.0` (reached
154+
* through `@objectstack/rest`) out of GHSA-w5hq-g745-h8pq. The advisory affects
155+
* `uuid` below 11.1.1, and 12.0.0 and 13.0.0; the fixed releases are 11.1.1,
156+
* 12.0.1 and 13.0.1 (read from the npm registry's bulk advisory endpoint,
157+
* 2026-10-22).
158+
* Two properties, both read off the constants:
159+
*
160+
* 1. The TARGET's lowest admitted version is at or above the fix for its
161+
* major. A major with no row is refused: moving the target there is the
162+
* moment to read the advisory for that line.
163+
* 2. The SELECTOR is `uuid@<M.0.0` with M the major right above the target,
164+
* so every edge that can resolve below the fix is rewritten (the
165+
* exceljs edge is `^8.3.0`), and a later advisory on the target line
166+
* moves the target alone.
167+
*/
168+
const UUID_FIXED_FLOOR_BY_MAJOR: Readonly<Record<number, readonly [number, number, number]>> = {
169+
11: [11, 1, 1],
170+
12: [12, 0, 1],
171+
13: [13, 0, 1],
172+
};
173+
174+
describe('the scaffold uuid override stays outside GHSA-w5hq-g745-h8pq', () => {
175+
it('its target admits nothing below the advisory fix for its major', () => {
176+
const match = /^\^(\d+)\.(\d+)\.(\d+)$/.exec(SCAFFOLD_UUID_OVERRIDE_RANGE);
177+
expect(match, `SCAFFOLD_UUID_OVERRIDE_RANGE "${SCAFFOLD_UUID_OVERRIDE_RANGE}" is not a ^X.Y.Z range`).not.toBeNull();
178+
const floor = match!.slice(1).map(Number) as [number, number, number];
179+
const fixed = UUID_FIXED_FLOOR_BY_MAJOR[floor[0]];
180+
expect(fixed, `no advisory floor is recorded for uuid ${floor[0]}.x — read the advisory, then add the row`).toBeDefined();
181+
const atOrAbove = floor[1] !== fixed![1] ? floor[1] > fixed![1] : floor[2] >= fixed![2];
182+
expect(
183+
atOrAbove,
184+
`SCAFFOLD_UUID_OVERRIDE_RANGE "${SCAFFOLD_UUID_OVERRIDE_RANGE}" admits ${floor.join('.')}, below the fix ${fixed!.join('.')}`,
185+
).toBe(true);
186+
});
187+
188+
it('its selector covers every version below the target line, bounded at the next major', () => {
189+
const selector = /^uuid@<(\d+)\.0\.0$/.exec(SCAFFOLD_UUID_OVERRIDE_SELECTOR);
190+
expect(selector, `SCAFFOLD_UUID_OVERRIDE_SELECTOR "${SCAFFOLD_UUID_OVERRIDE_SELECTOR}" is not uuid@<M.0.0`).not.toBeNull();
191+
const targetMajor = Number(/^\^(\d+)\./.exec(SCAFFOLD_UUID_OVERRIDE_RANGE)?.[1]);
192+
expect(Number(selector![1])).toBe(targetMajor + 1);
193+
});
194+
});

‎packages/create-objectstack/src/templates/blank/package.json‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
"vitest": "~4.1.12"
3232
},
3333
"overrides": {
34-
"vitest": "~4.1.12"
34+
"vitest": "~4.1.12",
35+
"uuid@<12.0.0": "^11.1.1"
3536
}
3637
}

‎scripts/sync-scaffold-emission-policy.mjs‎

Lines changed: 36 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -164,6 +164,20 @@ export const POLICY_STAMPS = [
164164
// direct `devDependencies.vitest` (`EOVERRIDE`), so the two rows above and
165165
// here must move together, and only generation makes them.
166166
},
167+
{
168+
key: 'overrides.uuid@<12.0.0',
169+
block: 'overrides',
170+
field: 'uuid@<12.0.0',
171+
constant: 'SCAFFOLD_UUID_OVERRIDE_RANGE',
172+
// The npm override that routes exceljs's `uuid ^8.3.0` (through
173+
// `@objectstack/rest`) out of GHSA-w5hq-g745-h8pq. The field is
174+
// `SCAFFOLD_UUID_OVERRIDE_SELECTOR`, spelled here as the key the template
175+
// declares; `os init` / `os create` emit it from that constant. This
176+
// script's `--check` compares VALUES only, so the key spellings are held
177+
// equal by `packages/cli/test/scaffold-emission-policy.e2e.test.ts`
178+
// (nightly tier), which compares the on-ramp's whole `overrides` block
179+
// with theirs.
180+
},
167181
];
168182

169183
/**
@@ -332,7 +346,9 @@ export function stampPolicy(text, policy, { label = TEMPLATE_PKG_FILE } = {}) {
332346
}
333347
const span = findBlockSpan(out, stamp.block, label);
334348
const block = out.slice(span.start, span.end);
335-
const field = new RegExp(`("${stamp.field}"\\s*:\\s*)"([^"]*)"`, 'g');
349+
// Escaped: an npm override selector (`uuid@<12.0.0`) carries `.`, which
350+
// unescaped would match any character.
351+
const field = new RegExp(`("${escapeRe(stamp.field)}"\\s*:\\s*)"([^"]*)"`, 'g');
336352
const hits = [...block.matchAll(field)];
337353
if (hits.length !== 1) {
338354
throw new Error(
@@ -630,8 +646,14 @@ const SELF_TEST_POLICY = {
630646
SCAFFOLD_PNPM_RANGE: '>=99.1',
631647
SCAFFOLD_TYPESCRIPT_RANGE: '^9.9.9',
632648
SCAFFOLD_VITEST_RANGE: '~8.8.0',
649+
SCAFFOLD_UUID_OVERRIDE_RANGE: '^7.7.7',
650+
};
651+
const STALE = {
652+
SCAFFOLD_PNPM_RANGE: '>=1.0',
653+
SCAFFOLD_TYPESCRIPT_RANGE: '^1.0.0',
654+
SCAFFOLD_VITEST_RANGE: '^1.1.0',
655+
SCAFFOLD_UUID_OVERRIDE_RANGE: '^1.2.3',
633656
};
634-
const STALE = { SCAFFOLD_PNPM_RANGE: '>=1.0', SCAFFOLD_TYPESCRIPT_RANGE: '^1.0.0', SCAFFOLD_VITEST_RANGE: '^1.1.0' };
635657

636658
function policySource(policy = SELF_TEST_POLICY) {
637659
return [
@@ -642,6 +664,8 @@ function policySource(policy = SELF_TEST_POLICY) {
642664
'',
643665
`export const SCAFFOLD_VITEST_RANGE = '${policy.SCAFFOLD_VITEST_RANGE}';`,
644666
'',
667+
`export const SCAFFOLD_UUID_OVERRIDE_RANGE = '${policy.SCAFFOLD_UUID_OVERRIDE_RANGE}';`,
668+
'',
645669
].join('\n');
646670
}
647671

@@ -661,7 +685,10 @@ function templatePkg(policy, { overrideVitest } = {}) {
661685
typescript: policy.SCAFFOLD_TYPESCRIPT_RANGE,
662686
vitest: policy.SCAFFOLD_VITEST_RANGE,
663687
},
664-
overrides: { vitest: overrideVitest ?? policy.SCAFFOLD_VITEST_RANGE },
688+
overrides: {
689+
vitest: overrideVitest ?? policy.SCAFFOLD_VITEST_RANGE,
690+
'uuid@<12.0.0': policy.SCAFFOLD_UUID_OVERRIDE_RANGE,
691+
},
665692
},
666693
null,
667694
2,
@@ -813,6 +840,12 @@ function selfTest() {
813840
}),
814841
'BOTH vitest stamps — devDependencies.vitest and overrides.vitest — now read the policy',
815842
);
843+
// The selector key carries `@`, `<` and `.`: the rewrite must land on it
844+
// exactly, never on a span a looser pattern happened to match.
845+
ok(
846+
after.every((t) => JSON.parse(t).overrides['uuid@<12.0.0'] === '^7.7.7'),
847+
'the uuid override, keyed by its npm selector `uuid@<12.0.0`, now reads the policy',
848+
);
816849
ok(runFixture(script, ['--check']).status === 0, '--check is green after the rewrite (idempotent)');
817850
},
818851
{ templatePolicy: STALE },

0 commit comments

Comments
 (0)