Skip to content

Commit b6e8707

Browse files
fix(cli,create-objectstack): scaffolds declare vitest ~4.1.12, outside GHSA-qxg6-89mg-rqv8, and still install on npm 10 (#25173)
Fixes #25165 Clause-②: no Metadata-protocol: none ## What this does - `SCAFFOLD_VITEST_RANGE` in `packages/cli/src/commands/init.ts` moves from `~4.0.0` to `~4.1.12`. `os init -t plugin` and `os create plugin` declare it in `devDependencies`. `SCAFFOLD_NPM_OVERRIDES`, which is built from it, puts it in the `overrides` of every standalone `os init` / `os create plugin` manifest. The override's shape is unchanged. - The blank template's `devDependencies.vitest` and `overrides.vitest` are re-stamped from that constant by the write run of `scripts/sync-scaffold-emission-policy.mjs`, the same run `create-objectstack`'s `build` invokes. Both scaffolders read one constant, and no value is kept by hand. - A floor pin in `packages/cli/test/scaffold-npm-overrides.test.ts`: the range's lowest admitted version must be at or above the advisory fix for its major (4.1.12 on 4.x, 5.0.3 on 5.x). A major with no row is refused, not passed. - The comments on the constant, the override and the template test carry the new measurement. - `.changeset/25165-scaffold-vitest-advisory.md` bumps `@objectstack/cli` and `create-objectstack` at `patch` and gives the one-line move for existing projects. - `content/docs/deployment/troubleshooting.mdx` (patch round): the npm 10 `edgesOut` entry's fix 2 prescribed `overrides.vitest` `~4.0.0`, which installs vitest 4.0.18, inside the advisory. It now prescribes `~4.1.12`. The entry names vitest 4.1.12's contradictory optional peer as the cause, says a direct `vitest` devDependency must move with the override (`EOVERRIDE`), and says how an existing `~4.0.0` project moves. ## The advisory I read the advisory from the npm registry's bulk advisory endpoint (`registry.npmjs.org/-/npm/v1/security/advisories/bulk`, 2026-10-22). This container's egress proxy refuses `api.osv.dev` (403 at CONNECT). - **GHSA-qxg6-89mg-rqv8**, `@vitest/mocker`: affected from 2.1.0-beta.7 up to the fix in 4.1.12, and from 5.0.0-beta.1 up to the fix in 5.0.3. That confirms the card's range. - **Also on the baseline.** `npm audit` of a baseline scaffold (`~4.0.0`, which resolves vitest and `@vitest/mocker` to 4.0.18) also lists: - GHSA-82fw-gwwq-j7x9 (`@vitest/mocker`, from 2.1.0 up to the fix in 4.1.11); - GHSA-5xrq-8626-4rwp (`vitest`, critical, from 4.0.0 up to the fix in 4.1.0). - **What the new floor clears.** It clears all three, and GHSA-xp26-3454-mjgp, which affects exactly 4.1.11 on the 4.x line. ## Route measurements **Method.** - Each manifest was installed in its own empty directory, with a fresh `--cache` for every leg. - The `@objectstack/*` ranges are `^18.0.0`, which resolves to the published 18.0.0. - After each install I read `vitest` and every `@vitest/mocker` copy from `node_modules`, then ran `npm audit --json`. **Manifests.** - The blank template. For route B its bytes equal this PR's stamped template. - The `os init -t plugin`, `os create plugin`, `os init -t app` and `os init -t empty` manifests. Their dependency blocks are byte-equal to what this branch's source emits, driven through `packages/cli/bin/run-dev.js`. **npm versions.** 10.9.9 (the newest 10.x), 10.9.4 (the npm bundled with this container's Node 22) and 11.21.1 (the newest 11.x). In the table, "4.1.12" means the tree holds exactly one `vitest` and one `@vitest/mocker`, both at that version. | Route | Manifest | npm 10.9.9 | npm 11.21.1 | npm 10.9.4 | |:--|:--|:--|:--|:--| | Baseline: `~4.0.0` + override | blank | exit 0 · 4.0.18 · audit lists the 3 advisories above | not measured | not measured | | Baseline | `os init -t plugin`, `os create plugin` | exit 0 · 4.0.18 · audit lists them | exit 0 · 4.0.18 · audit lists them | not measured | | A: `~4.1.12`, no override | blank, `os init -t plugin`, `os create plugin` | **exit 1**, `Cannot read properties of null (reading 'edgesOut')` | blank: exit 0 · 4.1.12 | not measured | | A | `os init -t app` (declares no vitest), no override | **exit 1**, `edgesOut` | not measured | not measured | | **B, chosen: `~4.1.12` + override** | blank, `os init -t plugin`, `os create plugin` | exit 0 · 4.1.12 · audit: no `vitest` / `@vitest/*` entry | exit 0 · the same | exit 0 · the same | | B | `os init -t app`, `os init -t empty` | exit 0 · no vitest installed · audit: none | exit 0 · the same | not measured | | C: `^5.0.5`, no override | blank | **exit 1**, `edgesOut` | exit 0 · 5.0.5 at the root plus a nested 4.1.12 | not measured | | D: `^5.0.5` + override | blank, `os init -t plugin`, `os create plugin` | exit 0 · 5.0.5 · audit: no vitest entry | exit 0 · the same | not measured | | D′: exact `5.0.4` + override (simulates `latest` having moved past the range) | blank | exit 0 · 5.0.4 | exit 0 · 5.0.4 | not measured | `os create plugin` audits to 0 entries on routes B and D. The other manifests keep 9 moderate entries on every route, and all 9 trace to one advisory: GHSA-w5hq-g745-h8pq on `uuid` 8.x, reached through `exceljs` from `@objectstack/rest` (see Acceptance notes). ## Why route B holds on npm 10 - **The crash comes from vitest 4.1.12's own peer set.** It peers `@vitest/browser-webdriverio ^4.1.11`, and that package's only release, 4.1.11, peers `vitest: 4.1.11` exactly. With the range at `~4.1.12`, the root vitest reaches that peer set directly, so route A crashes every manifest that declares vitest. `-t app` crashes too, through optional vitest peers deeper in its closure. - **The `vitest` override alone settles it.** With `overrides.vitest` equal to the declared range, npm 10.9.4 and 10.9.9 install every manifest. No `@vitest/browser-webdriverio` override is needed. The override is the existing `SCAFFOLD_NPM_OVERRIDES` mechanism, unchanged in shape. - **It does not depend on where `latest` points.** `latest` is vitest 5.0.5, outside `~4.1.12`, and the range still installs. The old comment's reading against `^5.0.0` ("installs only because it resolves to exactly the version `latest` names") was taken without an override. With the override, an exact pin below `latest` (5.0.4) installs too. - **#24462's canary.** On npm 10.9.9 the full blank template (every template file, with this PR's `package.json`): - installs with exit 0; - passes `npm test`: `RUN v4.1.12`, `Test Files 1 passed (1)`, `Tests 1 passed (1)`; - builds with `npm run build`, exit 0, `Artifact: dist/objectstack.json`. The canary installs published artifacts, so it reads this change only after a release. - **The plugin scaffolds.** Each was emitted from this branch's source and installed on npm 10.9.9 (vitest and `@vitest/mocker` 4.1.12). `npm run build` and `npm run typecheck` exit 0 for both. Their `npm test` result is in Acceptance notes. ## Why 4.1.x and not 5.x Routes B and D both install on npm 10 and npm 11, and both clear the advisory. On the four axes: - **Business need.** The scaffold's test (`note.test.ts`) needs nothing from vitest 5. vitest 5.0.5 declares `engines.node` `^22.12.0 || ^24.0.0 || >=26.0.0`, which is narrower than the `>=22.0.0` that `@objectstack/cli` and `create-objectstack` declare. vitest 4.1.12 declares `^20.0.0 || ^22.0.0 || >=24.0.0`, the same as 4.0.18. So only route D gives a new project on Node 22.0 to 22.11 an engine warning, or a refusal under `engine-strict`. - **Long-term.** The monorepo's own manifests run `vitest ^4.1.11`, re-locked to 4.1.12 for the same advisory family (#25163). A scaffold on that line runs the vitest the platform is tested with. Moving to 5 is a deliberate change of its own, for the monorepo and the scaffolds together. - **AI error-proofing.** A patch-line move inside major 4 changes no API that a generated test can call. The floor pin turns a move below the fix into a red test instead of a quiet regression. - **Startup scope.** This is the smallest change that clears the advisory: one constant, its stamp and its pin. ## Tests (head `4eb2ff4c4`) - **CLI unit tier.** `pnpm --filter @objectstack/cli exec vitest run --project unit --maxWorkers=2`, run under the verify lock. - First run: `VERDICT command-exit 1`, `Test Files 2 failed | 320 passed (322)`, `Tests 4728 passed | 29 skipped`. - Both failures were `test/published-subpath-console.pin.test.ts` and `test/published-subpath-hook-body.pin.test.ts` refusing an unbuilt `packages/cli/dist`: a missing build, not a test failure. - After the package build, those two files plus `test/scaffold-npm-overrides.test.ts` (verbose): `VERDICT command-exit 0`, `Test Files 3 passed (3)`, `Tests 41 passed (41)`. That run includes the new floor case: `its lowest admitted version is at or above the advisory fix for its major`. - The integration tier is left to CI: the diff touches no spawn entry and no integration-tier file. - **create-objectstack.** `pnpm --filter create-objectstack test`: `Test Files 18 passed (18)`, `Tests 278 passed (278)`. - **Typecheck.** `pnpm --filter @objectstack/cli typecheck && pnpm --filter create-objectstack typecheck`: `VERDICT command-exit 0`. `tsc -p tsconfig.test.json --listFiles` in `packages/cli` lists `scaffold-npm-overrides.test.ts` once. `create-objectstack`'s `tsc --listFiles` lists `template-consistency.test.ts` once. - **ESLint.** `pnpm exec eslint --no-inline-config` over the 3 changed TypeScript files: exit 0. - **Ablation of the floor pin.** The fix was committed first, and the mutation ran through `scripts/ablation-replace.mjs` in wrap mode. - Mutation: `SCAFFOLD_VITEST_RANGE = '~4.1.12'` became `'~4.0.0'`. The anchor went from 1 to 0 hits, and the blob changed from `47a2518d9a21` to `8da3d5de0b48`. - Result: `Tests 1 failed | 11 passed (12)`, with `SCAFFOLD_VITEST_RANGE "~4.0.0" admits 4.0.0, below the fix 4.1.12`. The 11 derived-equality cases stay green under the mutation, which is why the floor pin is needed. - Restore: the blob equals the HEAD blob (`47a2518d9a21`), `git diff HEAD` is empty and `git status` is clean. - The test imports `../src/commands/init.js`, so no `dist` is on its path. ## Gates (head `4eb2ff4c4`) - **Derivation.** `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack`: 84 commands for 5 paths against merge base `425c743fd`. - **Runs.** Each command was run byte for byte, with its exit code captured before any pipe. - First pass: 80 commands exited 0. - `check:dual-build-cjs-loads`, `check:i18n`, `check:i18n-coverage` and `check:i18n-walk-parity` exited 3 (PREREQUISITE NOT MET: no built `dist`). - After a full package build (`turbo run build`, 75/75 tasks, under the lock), all four re-ran to exit 0. - **Reconciliation.** `--ran`: `84 derived, 84 run, 0 NOT-MEASURED, 0 UNRUN`, exit 0. - **Governed surfaces.** `node scripts/pm/check-governed-merges.mjs --branch claude/issue-25165-scaffold-vitest-advisory`: exit 0, 0 of 5 paths governed, 176 changed lines. - **The stamp gates.** Both are in the derived set and exited 0: `check:scaffold-emission-policy` (the template and the constant agree) and `check:template-version-sync`. ## Patch round (head `f355f41a2`) - `origin/main` was merged first (`a22074c89`), then the troubleshooting page was fixed (`f355f41a2`). The diff is now 6 paths against merge base `ab488d9b7`. - `dispatch-gates --commands` derives 112 commands. Each was run with its exit code captured before any pipe, after a full package build (75/75 tasks). All 112 exited 0. `--ran` reports `112 run, 0 NOT-MEASURED`, exit 0. - `check-governed-merges`: 0 of 6 paths governed, 186 changed lines. - On the merged head, `test/scaffold-npm-overrides.test.ts` passes 12/12 and `create-objectstack` passes 278/278. - CI on `f355f41a2`: 39 check-runs succeeded and 7 were skipped. They include `Check Documentation Links`, `Build Docs`, `Scaffold outside the monorepo, install, build` and `Check Changeset`. ## Acceptance notes - **A stale comment.** In `scripts/sync-scaffold-emission-policy.mjs`, the comment on the `devDependencies.vitest` row still says the constant "holds why it is `~4.0.0`". It is a comment outside the file surface, and the script reads the constant's value, not the comment. - **The remaining audit entries.** A fresh scaffold's other `npm audit` entries all come from GHSA-w5hq-g745-h8pq on `uuid` 8.x, through `exceljs` `^4.4.0` in the published `@objectstack/rest`. The workspace overrides `uuid` in `pnpm-workspace.yaml`, which npm consumers never read. That is not this card. - **The plugin scaffolds' `npm test`.** `os init -t plugin` and `os create plugin` emit a `test` script but no test file, so `npm test` exits 1 with "No test files found". It does the same on the baseline 4.0.18 (measured on `-t plugin`). That is not this card. - **origin/main** (`ab488d9b7`) was merged into the branch in the patch round (merge commit `a22074c89`). It had no conflicts. --- _Generated by [Claude Code](https://claude.ai/code/session_01AFrKwndmrCLtF5V9hNtrw3)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 72a302a commit b6e8707

6 files changed

Lines changed: 134 additions & 52 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 declare vitest `~4.1.12`, outside GHSA-qxg6-89mg-rqv8, and still install on npm 10
7+
8+
Clause-②: no
9+
Metadata-protocol: none
10+
11+
The vitest range every scaffold declares moves from `~4.0.0` to `~4.1.12`: the `create-objectstack` blank template, `os init -t plugin` and `os create plugin` (in `devDependencies`), and the npm `overrides` block that every standalone scaffold emits, `os init -t app` and `-t empty` included. `~4.0.0` resolved to vitest and `@vitest/mocker` 4.0.18, inside GHSA-qxg6-89mg-rqv8 (`@vitest/mocker` from 2.1.0-beta.7 up to the fix in 4.1.12), which has no 4.0.x fix. A new project now resolves vitest and `@vitest/mocker` 4.1.12 or a later 4.1.x patch, and `npm audit` of a fresh scaffold lists no vitest or `@vitest/*` advisory.
12+
13+
npm 10, the npm Node 22 bundles, still installs every scaffold. The `overrides.vitest` entry, which equals `devDependencies.vitest`, is what makes that hold: it settles vitest 4.1.12's contradictory optional peer (`@vitest/browser-webdriverio`), which npm 10 otherwise crashes on with `Cannot read properties of null (reading 'edgesOut')`. Measured on npm 10.9.4, 10.9.9 and 11.21.1. pnpm ignores npm's `overrides` field.
14+
15+
**An existing project made by an earlier `create-objectstack`, `os init` or `os create plugin`** keeps its own `~4.0.0`. To move it, set `overrides.vitest` to `~4.1.12` in `package.json`, and `devDependencies.vitest` too where the project declares it, then run `npm install`. npm refuses an override that differs from the direct dependency (`EOVERRIDE`), so the two move together.

‎content/docs/deployment/troubleshooting.mdx‎

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,19 +12,21 @@ Solutions to the most common issues encountered when working with ObjectStack.
1212

1313
### "Cannot read properties of null (reading 'edgesOut')" from npm install
1414

15-
**Symptom:** npm exits 1 with `npm error Cannot read properties of null (reading 'edgesOut')` while installing `@objectstack/cli` or `@objectstack/plugin-auth`. This includes the install step of `npm create objectstack` and `npm install -g @objectstack/cli`.
15+
**Symptom:** npm exits 1 with `npm error Cannot read properties of null (reading 'edgesOut')` while installing `@objectstack/cli` or `@objectstack/plugin-auth`. This includes `npm install -g @objectstack/cli`, and `npm install` in a project whose `package.json` has no `vitest` override (fix 2).
1616

17-
**Cause:** npm 10, the npm that Node 22 ships, crashes on an optional `vitest` peer in the auth library's dependency tree: the newest vitest 4 has no matching `@vitest/browser-webdriverio` release. npm 11 and pnpm install the same packages, and `npm ci` from a committed lockfile is not affected.
17+
**Cause:** npm 10, the npm that Node 22 ships, crashes on the newest vitest 4, 4.1.12. Its optional peer `@vitest/browser-webdriverio ^4.1.11` has one matching release, 4.1.11, and that release requires exactly vitest 4.1.11. npm 10 reaches vitest through an optional `vitest` peer in the auth library's dependency tree, or through your project's own `vitest`. npm 11 and pnpm install the same packages, and `npm ci` from a committed lockfile is not affected.
1818

1919
**Fix** (either one):
2020

2121
1. **Use npm 11** (it needs Node 22.9+): run `npm install -g npm@11`, then repeat the install. This is the only fix for `npm install -g`, because a global install reads no `overrides`. In a Dockerfile, install under a pinned npm 11, as the [self-built runtime recipe](/docs/deployment/self-hosting#docker-official-image--the-standard-path) does.
22-
2. **Stay on npm 10 and pin vitest** in your project's root `package.json`. Then run `npm install` again and commit the lockfile:
22+
2. **Stay on npm 10 and pin vitest** in your project's root `package.json`. The override puts every `vitest` in the tree, optional peers included, on one range, and npm 10 installs it. Then run `npm install` again and commit the lockfile:
2323

2424
```json title="package.json"
25-
"overrides": { "vitest": "~4.0.0" }
25+
"overrides": { "vitest": "~4.1.12" }
2626
```
2727

28+
If `devDependencies` lists `vitest`, set it to the same `~4.1.12`: npm refuses an override that differs from a direct dependency (`EOVERRIDE`). A project created by `npm create objectstack`, `os init` or `os create plugin` already has this block. If yours reads `~4.0.0`, an earlier scaffold's value, change both to `~4.1.12`: `~4.0.0` installs vitest 4.0.18, which is affected by security advisory GHSA-qxg6-89mg-rqv8.
29+
2830
This entry, the npm 11 steps in [Self-hosting](/docs/deployment/self-hosting), and the npm 10 notes in [Getting Started](/docs/getting-started#prerequisites) and [Your First Project](/docs/getting-started/your-first-project#prerequisites) can be removed once `@vitest/browser-webdriverio` publishes the version that `npm view vitest dist-tags` lists as `V4`, and a plain npm 10 install of `@objectstack/plugin-auth` passes.
2931

3032
---

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

Lines changed: 59 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -344,9 +344,10 @@ export const SCAFFOLD_PNPM_RANGE = '>=10.15';
344344
// `^4.0.0` over `^4.0.18`: no recorded decision exists for either, both were
345345
// written in the same 2026-02-07 commit, and `^4.0.18` claims a PATCH-level
346346
// floor nothing justifies while being strictly the narrower of the two. (The
347-
// vitest range has since moved to `~4.0.0` because `^4.0.0` stopped
348-
// installing on npm 10. That was an install failure, not a support decision;
349-
// the comment on `SCAFFOLD_VITEST_RANGE` holds the reading.)
347+
// vitest range has since moved twice: to `~4.0.0` when `^4.0.0` stopped
348+
// installing on npm 10, then to `~4.1.12` for a security advisory on
349+
// `@vitest/mocker`. Neither was a support decision; the comment on
350+
// `SCAFFOLD_VITEST_RANGE` holds the reading.)
350351
//
351352
// ⚠️ Neither choice changes what a scaffolded project INSTALLS. Measured
352353
// against the registry: `^5.3.0` and `^5.8.0` both resolve to typescript
@@ -386,39 +387,52 @@ export const SCAFFOLD_TYPESCRIPT_RANGE = '^5.3.0';
386387
/**
387388
* The vitest range a scaffolded project declares when its template tests.
388389
*
389-
* `~4.0.0`, not `^4.0.0`, because `^4.0.0` does not INSTALL on npm 10, the npm
390-
* that Node 22 bundles. Measured 2026-10-12, in an empty directory with a fresh
391-
* `--cache` and a manifest holding only this one devDependency (the full matrix
392-
* is in the PR that set this value): on npm 10.9.4 and 10.9.9, `npm install`
393-
* exits 1 with `Cannot read properties of null (reading 'edgesOut')` from
394-
* arborist's `#loadPeerSet`, and npm 11.21.0 installs it. `~4.0.0` installed on
395-
* all three (vitest 4.0.18, vite 7.3.7), and so did both CLI-emitted plugin
396-
* manifests. The cost: a scaffold gets vitest 4.0.18 rather than 4.1.x.
390+
* `~4.1.12`: the floor is the first 4.x release outside GHSA-qxg6-89mg-rqv8,
391+
* which affects `@vitest/mocker` from 2.1.0-beta.7 up to the fix in 4.1.12.
392+
* vitest depends on its own `@vitest/mocker` at the exact same version, so the
393+
* vitest floor IS the mocker floor. The tilde admits patch releases only, as
394+
* the previous `~4.0.0` did, because both npm 10 failures recorded below
395+
* arrived with a new minor's peer graph. ⛔ Never move the floor back below
396+
* 4.1.12 to get around a future npm 10 failure: no 4.0.x fix exists, and
397+
* `test/scaffold-npm-overrides.test.ts` pins the floor.
397398
*
398-
* ⚠️ The range alone stopped being enough, so it is also emitted as an npm
399-
* OVERRIDE — see {@link SCAFFOLD_NPM_OVERRIDES}, which is built from this
400-
* constant, so the declared range and the override cannot disagree.
399+
* The range installs on npm 10, the npm Node 22 bundles, only TOGETHER with
400+
* the npm override — see {@link SCAFFOLD_NPM_OVERRIDES}, built from this
401+
* constant, so the declared range and the override cannot disagree. vitest
402+
* 4.1.12 itself peers `@vitest/browser-webdriverio ^4.1.11`, whose only match,
403+
* 4.1.11, peers `vitest: 4.1.11` exactly, and npm 10's arborist crashes on
404+
* that set. Measured 2026-10-22, each manifest in an empty directory with a
405+
* fresh `--cache`, the `@objectstack/*` ranges resolving to the published
406+
* 18.0.0 (blank template, `os init -t plugin`, `os create plugin`):
401407
*
402-
* The cause is upstream, in the peer graph. Every vitest 4.1.x admits vite 8.
403-
* vite 8 declares an optional `@vitejs/devtools` peer, which reaches
404-
* `@vitejs/devtools-vitest`, which peers `vitest@*`. npm 10 resolves that peer
405-
* to vitest's `latest` release, whose exact-version peers then contradict the
406-
* vitest at the root, and npm 10's arborist crashes on it. vitest 4.0.x admits
407-
* only vite `^6 || ^7`, and no vite 6 or 7 release declares the devtools peer,
408-
* so the `vitest@*` peer never enters the graph.
408+
* `~4.1.12` + override npm 10.9.4, 10.9.9, exit 0; one vitest and one
409+
* 11.21.1 `@vitest/mocker`, both 4.1.12;
410+
* `npm audit` lists no vitest or
411+
* `@vitest/*` entry
412+
* `~4.1.12`, no override npm 10.9.9 exit 1, `Cannot read properties
413+
* of null (reading 'edgesOut')`
414+
* `~4.0.0` + override npm 10.9.9, 11.21.1 exit 0 on vitest and
415+
* `@vitest/mocker` 4.0.18, which
416+
* `npm audit` reports
409417
*
410-
* ⛔ Not `^5.0.0`, although it installs today. It installs only because it
411-
* resolves to exactly the version `latest` names: pinned to 5.0.0 or 5.0.2 it
412-
* crashes the same way. So it would break again, with no change here, the day
413-
* `latest` moves past 5.x. ⛔ Not a shipped `.npmrc` with `legacy-peer-deps`
414-
* either, which changes peer resolution for every npm user of the scaffold.
418+
* The install does not depend on where vitest's `latest` tag points: `latest`
419+
* is 5.0.5, outside this range, and the range installs. The override routes
420+
* every `vitest` edge in the npm graph to this range, the optional peers deep
421+
* in the closure included.
415422
*
416-
* The re-check that lets this move back: run `npm install` of
417-
* `{"devDependencies":{"vitest":"^4.0.0"}}` (or the range you want) in an empty
418-
* directory with a fresh `--cache`, on the newest npm 10 and on npm 11. When
419-
* both exit 0, widen this.
423+
* ⛔ Not the 5.x line, although `^5.0.5` + override installed on the same
424+
* matrix and cleared the advisory too. vitest 5 declares `engines.node`
425+
* `^22.12.0 || ^24.0.0 || >=26.0.0`, narrower than the `>=22.0.0` the CLI and
426+
* `create-objectstack` declare, and this repo's own suites run vitest 4.
427+
* Moving a scaffold to a new major is its own change.
428+
* ⛔ Not a shipped `.npmrc` with `legacy-peer-deps` either, which changes peer
429+
* resolution for every npm user of the scaffold.
430+
*
431+
* The re-check before moving this: `npm install` of each emitted manifest in
432+
* an empty directory with a fresh `--cache`, on the newest npm 10 and on npm
433+
* 11, then `npm audit` of the installed tree.
420434
*/
421-
export const SCAFFOLD_VITEST_RANGE = '~4.0.0';
435+
export const SCAFFOLD_VITEST_RANGE = '~4.1.12';
422436

423437
/**
424438
* The npm `overrides` block every STANDALONE scaffold emits: `os init`'s three
@@ -429,11 +443,16 @@ export const SCAFFOLD_VITEST_RANGE = '~4.0.0';
429443
* Why it exists. vitest 4.1.12 (published 2026-10-15) peers
430444
* `@vitest/browser-webdriverio ^4.1.11`, and that package's only match, 4.1.11,
431445
* peers `vitest: 4.1.11` exactly — a peer set that contradicts itself. A
432-
* scaffold reaches it even with vitest pinned to `~4.0.0` at its root, through
433-
* OPTIONAL `vitest` peers deeper in the closure (`@objectstack/metadata-core`
434-
* peers `^4.0.0`, better-auth peers `^2 || ^3 || ^4`), which npm 10 resolves to
435-
* vitest's newest 4.x. Measured 2026-10-16, each manifest in an empty
436-
* directory with a fresh `--cache`, against the published `18.0.0-next.2`:
446+
* scaffold reaches it through its own root vitest, now that the range is
447+
* `~4.1.12`, and, in a template that declares no vitest, through OPTIONAL
448+
* `vitest` peers deeper in the closure (better-auth peers vitest optionally),
449+
* which npm 10 resolves to vitest's newest 4.x. Re-measured 2026-10-22 against
450+
* the published 18.0.0 with the range at `~4.1.12`: WITHOUT this block, `os
451+
* init -t app`, `-t plugin` and `os create plugin` each exit 1 on npm 10.9.9
452+
* with the crash below; WITH it, those three and `-t empty` exit 0 on npm
453+
* 10.9.9 and 11.21.1 (the full matrix is on `SCAFFOLD_VITEST_RANGE`). The
454+
* first reading, 2026-10-16, each manifest in an empty directory with a fresh
455+
* `--cache`, against the published `18.0.0-next.2`, with the range at `~4.0.0`:
437456
*
438457
* without the override npm 10.9.9 `os init -t app`, `-t plugin` and
439458
* `-t empty` all exit 1 with `Cannot
@@ -450,10 +469,10 @@ export const SCAFFOLD_VITEST_RANGE = '~4.0.0';
450469
* vitest, the override equals that declaration — npm refuses an override that
451470
* differs from the direct dependency's own spec (`EOVERRIDE`).
452471
*
453-
* The cost, measured on an emitted `-t app` (npm 10.9.9): a later bare `npm
454-
* install -D vitest` is refused with `EOVERRIDE: Override for vitest@*
455-
* conflicts with direct dependency`; `npm install -D vitest@~4.0.0` (this
456-
* range) installs. Loud and self-describing, where the alternative was a
472+
* The cost, measured on an emitted `-t app` (npm 10.9.9, re-measured with the
473+
* range at `~4.1.12`): a later bare `npm install -D vitest` is refused with
474+
* `EOVERRIDE: Override for vitest@* conflicts with direct dependency`; `npm
475+
* install -D vitest@~4.1.12` (this range) installs. Loud and self-describing, where the alternative was a
457476
* project that could not install at all. pnpm ignores npm's `overrides` field,
458477
* so pnpm users see no change.
459478
*

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

Lines changed: 41 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -14,15 +14,20 @@
1414
* `Cannot read properties of null (reading 'edgesOut')`. Measured 2026-10-16:
1515
* `os init -t app`, `-t plugin` and `-t empty` all failed `npm install` on npm
1616
* 10.9.9; with `"overrides": { "vitest": "~4.0.0" }` all of them, and `os
17-
* create plugin`, installed on npm 10.9.9 and npm 11.21.1. The full reading is
18-
* on `SCAFFOLD_NPM_OVERRIDES` in `src/commands/init.ts`.
17+
* create plugin`, installed on npm 10.9.9 and npm 11.21.1. The range has since
18+
* moved to `~4.1.12` (GHSA-qxg6-89mg-rqv8 has no 4.0.x fix), and the override
19+
* still decides it: re-measured 2026-10-22, without it `os init -t app`, `-t
20+
* plugin` and `os create plugin` each crash npm 10.9.9, and with it the
21+
* emissions install on npm 10.9.4, 10.9.9 and 11.21.1. The full readings are on `SCAFFOLD_VITEST_RANGE` and
22+
* `SCAFFOLD_NPM_OVERRIDES` in `src/commands/init.ts`.
1923
*
2024
* ## What is asserted, and why no range is written down here
2125
*
22-
* Every expectation is DERIVED from the exported constants, never a literal:
23-
* a pin comparing against `'~4.0.0'` would stay green on a tree where the
24-
* constant moved and one emission was left behind — the drift it exists to
25-
* catch.
26+
* Every equality is DERIVED from the exported constants, never a literal: a
27+
* pin comparing against a spelled-out range would stay green on a tree where
28+
* 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.
2631
*
2732
* 1. Each emission's `overrides.vitest` IS `SCAFFOLD_VITEST_RANGE`.
2833
* 2. Where an emission also declares `vitest` directly, the two are equal —
@@ -108,3 +113,33 @@ describe('npm overrides in every standalone scaffold manifest', () => {
108113
expect(manifest).not.toHaveProperty('overrides');
109114
});
110115
});
116+
117+
/**
118+
* The floor every scaffold's vitest range must keep: the first release of each
119+
* major outside GHSA-qxg6-89mg-rqv8 (`@vitest/mocker` from 2.1.0-beta.7 up to
120+
* the fix in 4.1.12, and from 5.0.0-beta.1 up to the fix in 5.0.3; read from
121+
* the npm registry's advisory endpoint, 2026-10-22; the same floors clear
122+
* GHSA-xp26-3454-mjgp, GHSA-82fw-gwwq-j7x9 and vitest's own
123+
* GHSA-5xrq-8626-4rwp by that reading). vitest depends on its own
124+
* `@vitest/mocker` at the exact same version, so a vitest floor is a mocker
125+
* floor. A major with no row here is refused rather than passed: moving the
126+
* scaffold to it is the moment to read the advisory for that line.
127+
*/
128+
const FIXED_FLOOR_BY_MAJOR: Readonly<Record<number, readonly [number, number, number]>> = {
129+
4: [4, 1, 12],
130+
5: [5, 0, 3],
131+
};
132+
133+
describe('the scaffold vitest range stays outside GHSA-qxg6-89mg-rqv8', () => {
134+
it('its lowest admitted version is at or above the advisory fix for its major', () => {
135+
// Only the two spellings the scaffold uses: an unreadable range is a
136+
// failure here, never a pass.
137+
const match = /^[~^](\d+)\.(\d+)\.(\d+)$/.exec(SCAFFOLD_VITEST_RANGE);
138+
expect(match, `SCAFFOLD_VITEST_RANGE "${SCAFFOLD_VITEST_RANGE}" is not a ~X.Y.Z or ^X.Y.Z range`).not.toBeNull();
139+
const floor = match!.slice(1).map(Number) as [number, number, number];
140+
const fixed = FIXED_FLOOR_BY_MAJOR[floor[0]];
141+
expect(fixed, `no advisory floor is recorded for vitest ${floor[0]}.x — read the advisory, then add the row`).toBeDefined();
142+
const atOrAbove = floor[1] !== fixed![1] ? floor[1] > fixed![1] : floor[2] >= fixed![2];
143+
expect(atOrAbove, `SCAFFOLD_VITEST_RANGE "${SCAFFOLD_VITEST_RANGE}" admits ${floor.join('.')}, below the fix ${fixed!.join('.')}`).toBe(true);
144+
});
145+
});

‎packages/create-objectstack/src/template-consistency.test.ts‎

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -208,6 +208,17 @@ describe('bundled template declared version surfaces', () => {
208208
// "vitest": "~4.0.0" npm 10.9.x / 11.21.x exit 0, every one the same tree
209209
// as npm 11 installs without it
210210
//
211+
// The range has since moved to `~4.1.12`: 4.0.18's `@vitest/mocker` is inside
212+
// GHSA-qxg6-89mg-rqv8, which has no 4.0.x fix. Re-measured 2026-10-22 on this
213+
// template against the published 18.0.0, each leg with a fresh `--cache`:
214+
//
215+
// "~4.1.12", no override npm 10.9.9 exit 1, the crash (vitest 4.1.12
216+
// itself peers the contradiction now)
217+
// "~4.1.12" + override npm 10.9.4 / 10.9.9 exit 0, one vitest and one
218+
// / 11.21.1 @vitest/mocker, both 4.1.12; `npm
219+
// audit` lists no vitest entry, and
220+
// `npm test` passes on npm 10.9.9
221+
//
211222
// The override routes every `vitest` edge in the npm graph to the range this
212223
// project declares, the vitest npm 11 already dedupes the whole tree to. pnpm
213224
// ignores npm's `overrides` field (measured on 10.28.0): pnpm users see no change.

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,9 +28,9 @@
2828
"@objectstack/skills": "^18.0.0",
2929
"@objectstack/verify": "^18.0.0",
3030
"typescript": "^5.3.0",
31-
"vitest": "~4.0.0"
31+
"vitest": "~4.1.12"
3232
},
3333
"overrides": {
34-
"vitest": "~4.0.0"
34+
"vitest": "~4.1.12"
3535
}
3636
}

0 commit comments

Comments
 (0)