From 4eb2ff4c43c95551d57eb16e6bb77f916c2843ca Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 22 Oct 2026 04:40:40 +0000 Subject: [PATCH 1/2] fix(cli,create-objectstack): scaffolds declare vitest ~4.1.12, outside GHSA-qxg6-89mg-rqv8 SCAFFOLD_VITEST_RANGE moves from ~4.0.0 to ~4.1.12; the blank template's devDependencies.vitest and overrides.vitest are re-stamped from it by scripts/sync-scaffold-emission-policy.mjs. The npm override (unchanged in shape) is what keeps npm 10 installing vitest 4.1.12's contradictory optional peer set; measured on npm 10.9.4, 10.9.9 and 11.21.1. A floor pin keeps the range at or above the advisory's fixed release. Claude-Session: https://claude.ai/code/session_01AFrKwndmrCLtF5V9hNtrw3 Co-authored-by: Claude --- .changeset/25165-scaffold-vitest-advisory.md | 15 +++ packages/cli/src/commands/init.ts | 99 +++++++++++-------- .../cli/test/scaffold-npm-overrides.test.ts | 47 +++++++-- .../src/template-consistency.test.ts | 11 +++ .../src/templates/blank/package.json | 4 +- 5 files changed, 128 insertions(+), 48 deletions(-) create mode 100644 .changeset/25165-scaffold-vitest-advisory.md diff --git a/.changeset/25165-scaffold-vitest-advisory.md b/.changeset/25165-scaffold-vitest-advisory.md new file mode 100644 index 00000000000..855d07f0d94 --- /dev/null +++ b/.changeset/25165-scaffold-vitest-advisory.md @@ -0,0 +1,15 @@ +--- +'@objectstack/cli': patch +'create-objectstack': patch +--- + +fix(cli,create-objectstack): new projects declare vitest `~4.1.12`, outside GHSA-qxg6-89mg-rqv8, and still install on npm 10 + +Clause-②: no +Metadata-protocol: none + +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. + +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. + +**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. diff --git a/packages/cli/src/commands/init.ts b/packages/cli/src/commands/init.ts index d00dc1d8790..47a2518d9a2 100644 --- a/packages/cli/src/commands/init.ts +++ b/packages/cli/src/commands/init.ts @@ -344,9 +344,10 @@ export const SCAFFOLD_PNPM_RANGE = '>=10.15'; // `^4.0.0` over `^4.0.18`: no recorded decision exists for either, both were // written in the same 2026-02-07 commit, and `^4.0.18` claims a PATCH-level // floor nothing justifies while being strictly the narrower of the two. (The -// vitest range has since moved to `~4.0.0` because `^4.0.0` stopped -// installing on npm 10. That was an install failure, not a support decision; -// the comment on `SCAFFOLD_VITEST_RANGE` holds the reading.) +// vitest range has since moved twice: to `~4.0.0` when `^4.0.0` stopped +// installing on npm 10, then to `~4.1.12` for a security advisory on +// `@vitest/mocker`. Neither was a support decision; the comment on +// `SCAFFOLD_VITEST_RANGE` holds the reading.) // // ⚠️ Neither choice changes what a scaffolded project INSTALLS. Measured // 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'; /** * The vitest range a scaffolded project declares when its template tests. * - * `~4.0.0`, not `^4.0.0`, because `^4.0.0` does not INSTALL on npm 10, the npm - * that Node 22 bundles. Measured 2026-10-12, in an empty directory with a fresh - * `--cache` and a manifest holding only this one devDependency (the full matrix - * is in the PR that set this value): on npm 10.9.4 and 10.9.9, `npm install` - * exits 1 with `Cannot read properties of null (reading 'edgesOut')` from - * arborist's `#loadPeerSet`, and npm 11.21.0 installs it. `~4.0.0` installed on - * all three (vitest 4.0.18, vite 7.3.7), and so did both CLI-emitted plugin - * manifests. The cost: a scaffold gets vitest 4.0.18 rather than 4.1.x. + * `~4.1.12`: the floor is the first 4.x release outside GHSA-qxg6-89mg-rqv8, + * which affects `@vitest/mocker` from 2.1.0-beta.7 up to the fix in 4.1.12. + * vitest depends on its own `@vitest/mocker` at the exact same version, so the + * vitest floor IS the mocker floor. The tilde admits patch releases only, as + * the previous `~4.0.0` did, because both npm 10 failures recorded below + * arrived with a new minor's peer graph. ⛔ Never move the floor back below + * 4.1.12 to get around a future npm 10 failure: no 4.0.x fix exists, and + * `test/scaffold-npm-overrides.test.ts` pins the floor. * - * ⚠️ The range alone stopped being enough, so it is also emitted as an npm - * OVERRIDE — see {@link SCAFFOLD_NPM_OVERRIDES}, which is built from this - * constant, so the declared range and the override cannot disagree. + * The range installs on npm 10, the npm Node 22 bundles, only TOGETHER with + * the npm override — see {@link SCAFFOLD_NPM_OVERRIDES}, built from this + * constant, so the declared range and the override cannot disagree. vitest + * 4.1.12 itself peers `@vitest/browser-webdriverio ^4.1.11`, whose only match, + * 4.1.11, peers `vitest: 4.1.11` exactly, and npm 10's arborist crashes on + * that set. Measured 2026-10-22, each manifest in an empty directory with a + * fresh `--cache`, the `@objectstack/*` ranges resolving to the published + * 18.0.0 (blank template, `os init -t plugin`, `os create plugin`): * - * The cause is upstream, in the peer graph. Every vitest 4.1.x admits vite 8. - * vite 8 declares an optional `@vitejs/devtools` peer, which reaches - * `@vitejs/devtools-vitest`, which peers `vitest@*`. npm 10 resolves that peer - * to vitest's `latest` release, whose exact-version peers then contradict the - * vitest at the root, and npm 10's arborist crashes on it. vitest 4.0.x admits - * only vite `^6 || ^7`, and no vite 6 or 7 release declares the devtools peer, - * so the `vitest@*` peer never enters the graph. + * `~4.1.12` + override npm 10.9.4, 10.9.9, exit 0; one vitest and one + * 11.21.1 `@vitest/mocker`, both 4.1.12; + * `npm audit` lists no vitest or + * `@vitest/*` entry + * `~4.1.12`, no override npm 10.9.9 exit 1, `Cannot read properties + * of null (reading 'edgesOut')` + * `~4.0.0` + override npm 10.9.9, 11.21.1 exit 0 on vitest and + * `@vitest/mocker` 4.0.18, which + * `npm audit` reports * - * ⛔ Not `^5.0.0`, although it installs today. It installs only because it - * resolves to exactly the version `latest` names: pinned to 5.0.0 or 5.0.2 it - * crashes the same way. So it would break again, with no change here, the day - * `latest` moves past 5.x. ⛔ Not a shipped `.npmrc` with `legacy-peer-deps` - * either, which changes peer resolution for every npm user of the scaffold. + * The install does not depend on where vitest's `latest` tag points: `latest` + * is 5.0.5, outside this range, and the range installs. The override routes + * every `vitest` edge in the npm graph to this range, the optional peers deep + * in the closure included. * - * The re-check that lets this move back: run `npm install` of - * `{"devDependencies":{"vitest":"^4.0.0"}}` (or the range you want) in an empty - * directory with a fresh `--cache`, on the newest npm 10 and on npm 11. When - * both exit 0, widen this. + * ⛔ Not the 5.x line, although `^5.0.5` + override installed on the same + * matrix and cleared the advisory too. vitest 5 declares `engines.node` + * `^22.12.0 || ^24.0.0 || >=26.0.0`, narrower than the `>=22.0.0` the CLI and + * `create-objectstack` declare, and this repo's own suites run vitest 4. + * Moving a scaffold to a new major is its own change. + * ⛔ Not a shipped `.npmrc` with `legacy-peer-deps` either, which changes peer + * resolution for every npm user of the scaffold. + * + * The re-check before moving this: `npm install` of each emitted manifest in + * an empty directory with a fresh `--cache`, on the newest npm 10 and on npm + * 11, then `npm audit` of the installed tree. */ -export const SCAFFOLD_VITEST_RANGE = '~4.0.0'; +export const SCAFFOLD_VITEST_RANGE = '~4.1.12'; /** * The npm `overrides` block every STANDALONE scaffold emits: `os init`'s three @@ -429,11 +443,16 @@ export const SCAFFOLD_VITEST_RANGE = '~4.0.0'; * Why it exists. vitest 4.1.12 (published 2026-10-15) peers * `@vitest/browser-webdriverio ^4.1.11`, and that package's only match, 4.1.11, * peers `vitest: 4.1.11` exactly — a peer set that contradicts itself. A - * scaffold reaches it even with vitest pinned to `~4.0.0` at its root, through - * OPTIONAL `vitest` peers deeper in the closure (`@objectstack/metadata-core` - * peers `^4.0.0`, better-auth peers `^2 || ^3 || ^4`), which npm 10 resolves to - * vitest's newest 4.x. Measured 2026-10-16, each manifest in an empty - * directory with a fresh `--cache`, against the published `18.0.0-next.2`: + * scaffold reaches it through its own root vitest, now that the range is + * `~4.1.12`, and, in a template that declares no vitest, through OPTIONAL + * `vitest` peers deeper in the closure (better-auth peers vitest optionally), + * which npm 10 resolves to vitest's newest 4.x. Re-measured 2026-10-22 against + * the published 18.0.0 with the range at `~4.1.12`: WITHOUT this block, `os + * init -t app`, `-t plugin` and `os create plugin` each exit 1 on npm 10.9.9 + * with the crash below; WITH it, those three and `-t empty` exit 0 on npm + * 10.9.9 and 11.21.1 (the full matrix is on `SCAFFOLD_VITEST_RANGE`). The + * first reading, 2026-10-16, each manifest in an empty directory with a fresh + * `--cache`, against the published `18.0.0-next.2`, with the range at `~4.0.0`: * * without the override npm 10.9.9 `os init -t app`, `-t plugin` and * `-t empty` all exit 1 with `Cannot @@ -450,10 +469,10 @@ export const SCAFFOLD_VITEST_RANGE = '~4.0.0'; * vitest, the override equals that declaration — npm refuses an override that * differs from the direct dependency's own spec (`EOVERRIDE`). * - * The cost, measured on an emitted `-t app` (npm 10.9.9): a later bare `npm - * install -D vitest` is refused with `EOVERRIDE: Override for vitest@* - * conflicts with direct dependency`; `npm install -D vitest@~4.0.0` (this - * range) installs. Loud and self-describing, where the alternative was a + * The cost, measured on an emitted `-t app` (npm 10.9.9, re-measured with the + * range at `~4.1.12`): a later bare `npm install -D vitest` is refused with + * `EOVERRIDE: Override for vitest@* conflicts with direct dependency`; `npm + * install -D vitest@~4.1.12` (this range) installs. Loud and self-describing, where the alternative was a * project that could not install at all. pnpm ignores npm's `overrides` field, * so pnpm users see no change. * diff --git a/packages/cli/test/scaffold-npm-overrides.test.ts b/packages/cli/test/scaffold-npm-overrides.test.ts index 20f76e9de73..96d0c817d4a 100644 --- a/packages/cli/test/scaffold-npm-overrides.test.ts +++ b/packages/cli/test/scaffold-npm-overrides.test.ts @@ -14,15 +14,20 @@ * `Cannot read properties of null (reading 'edgesOut')`. Measured 2026-10-16: * `os init -t app`, `-t plugin` and `-t empty` all failed `npm install` on npm * 10.9.9; with `"overrides": { "vitest": "~4.0.0" }` all of them, and `os - * create plugin`, installed on npm 10.9.9 and npm 11.21.1. The full reading is - * on `SCAFFOLD_NPM_OVERRIDES` in `src/commands/init.ts`. + * create plugin`, installed on npm 10.9.9 and npm 11.21.1. The range has since + * moved to `~4.1.12` (GHSA-qxg6-89mg-rqv8 has no 4.0.x fix), and the override + * still decides it: re-measured 2026-10-22, without it `os init -t app`, `-t + * plugin` and `os create plugin` each crash npm 10.9.9, and with it the + * emissions install on npm 10.9.4, 10.9.9 and 11.21.1. The full readings are on `SCAFFOLD_VITEST_RANGE` and + * `SCAFFOLD_NPM_OVERRIDES` in `src/commands/init.ts`. * * ## What is asserted, and why no range is written down here * - * Every expectation is DERIVED from the exported constants, never a literal: - * a pin comparing against `'~4.0.0'` would stay green on a tree where the - * constant moved and one emission was left behind — the drift it exists to - * catch. + * Every equality is DERIVED from the exported constants, never a literal: a + * pin comparing against a spelled-out range would stay green on a tree where + * the constant moved and one emission was left behind — the drift it exists + * to catch. The one literal is a LOWER bound, the advisory's fixed release, in + * the floor pin at the end of this file. * * 1. Each emission's `overrides.vitest` IS `SCAFFOLD_VITEST_RANGE`. * 2. Where an emission also declares `vitest` directly, the two are equal — @@ -108,3 +113,33 @@ describe('npm overrides in every standalone scaffold manifest', () => { expect(manifest).not.toHaveProperty('overrides'); }); }); + +/** + * The floor every scaffold's vitest range must keep: the first release of each + * major outside GHSA-qxg6-89mg-rqv8 (`@vitest/mocker` 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; read from + * the npm registry's advisory endpoint, 2026-10-22; the same floors clear + * GHSA-xp26-3454-mjgp, GHSA-82fw-gwwq-j7x9 and vitest's own + * GHSA-5xrq-8626-4rwp by that reading). vitest depends on its own + * `@vitest/mocker` at the exact same version, so a vitest floor is a mocker + * floor. A major with no row here is refused rather than passed: moving the + * scaffold to it is the moment to read the advisory for that line. + */ +const FIXED_FLOOR_BY_MAJOR: Readonly> = { + 4: [4, 1, 12], + 5: [5, 0, 3], +}; + +describe('the scaffold vitest range stays outside GHSA-qxg6-89mg-rqv8', () => { + it('its lowest admitted version is at or above the advisory fix for its major', () => { + // Only the two spellings the scaffold uses: an unreadable range is a + // failure here, never a pass. + const match = /^[~^](\d+)\.(\d+)\.(\d+)$/.exec(SCAFFOLD_VITEST_RANGE); + expect(match, `SCAFFOLD_VITEST_RANGE "${SCAFFOLD_VITEST_RANGE}" is not a ~X.Y.Z or ^X.Y.Z range`).not.toBeNull(); + const floor = match!.slice(1).map(Number) as [number, number, number]; + const fixed = FIXED_FLOOR_BY_MAJOR[floor[0]]; + expect(fixed, `no advisory floor is recorded for vitest ${floor[0]}.x — read the advisory, then add the row`).toBeDefined(); + const atOrAbove = floor[1] !== fixed![1] ? floor[1] > fixed![1] : floor[2] >= fixed![2]; + expect(atOrAbove, `SCAFFOLD_VITEST_RANGE "${SCAFFOLD_VITEST_RANGE}" admits ${floor.join('.')}, below the fix ${fixed!.join('.')}`).toBe(true); + }); +}); diff --git a/packages/create-objectstack/src/template-consistency.test.ts b/packages/create-objectstack/src/template-consistency.test.ts index 8c0afa5d2bf..118a59c203c 100644 --- a/packages/create-objectstack/src/template-consistency.test.ts +++ b/packages/create-objectstack/src/template-consistency.test.ts @@ -208,6 +208,17 @@ describe('bundled template declared version surfaces', () => { // "vitest": "~4.0.0" npm 10.9.x / 11.21.x exit 0, every one the same tree // as npm 11 installs without it // + // The range has since moved to `~4.1.12`: 4.0.18's `@vitest/mocker` is inside + // GHSA-qxg6-89mg-rqv8, which has no 4.0.x fix. Re-measured 2026-10-22 on this + // template against the published 18.0.0, each leg with a fresh `--cache`: + // + // "~4.1.12", no override npm 10.9.9 exit 1, the crash (vitest 4.1.12 + // itself peers the contradiction now) + // "~4.1.12" + override npm 10.9.4 / 10.9.9 exit 0, one vitest and one + // / 11.21.1 @vitest/mocker, both 4.1.12; `npm + // audit` lists no vitest entry, and + // `npm test` passes on npm 10.9.9 + // // The override routes every `vitest` edge in the npm graph to the range this // project declares, the vitest npm 11 already dedupes the whole tree to. pnpm // ignores npm's `overrides` field (measured on 10.28.0): pnpm users see no change. diff --git a/packages/create-objectstack/src/templates/blank/package.json b/packages/create-objectstack/src/templates/blank/package.json index 1bbbff43ce0..0a7732d3ced 100644 --- a/packages/create-objectstack/src/templates/blank/package.json +++ b/packages/create-objectstack/src/templates/blank/package.json @@ -28,9 +28,9 @@ "@objectstack/skills": "^18.0.0", "@objectstack/verify": "^18.0.0", "typescript": "^5.3.0", - "vitest": "~4.0.0" + "vitest": "~4.1.12" }, "overrides": { - "vitest": "~4.0.0" + "vitest": "~4.1.12" } } From f355f41a2c524a97eca689928c22d2cd5c7b801e Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 22 Oct 2026 05:35:55 +0000 Subject: [PATCH 2/2] docs(deployment): the npm 10 edgesOut fix prescribes the vitest override at ~4.1.12 The troubleshooting entry told an npm 10 project to add overrides.vitest ~4.0.0, which installs vitest 4.0.18, inside GHSA-qxg6-89mg-rqv8, and contradicts the range the scaffolds now ship. It now prescribes ~4.1.12, 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. Claude-Session: https://claude.ai/code/session_01AFrKwndmrCLtF5V9hNtrw3 Co-authored-by: Claude --- content/docs/deployment/troubleshooting.mdx | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/content/docs/deployment/troubleshooting.mdx b/content/docs/deployment/troubleshooting.mdx index 8777b802677..dc3b74b9fa9 100644 --- a/content/docs/deployment/troubleshooting.mdx +++ b/content/docs/deployment/troubleshooting.mdx @@ -12,19 +12,21 @@ Solutions to the most common issues encountered when working with ObjectStack. ### "Cannot read properties of null (reading 'edgesOut')" from npm install -**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`. +**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). -**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. +**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. **Fix** (either one): 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. -2. **Stay on npm 10 and pin vitest** in your project's root `package.json`. Then run `npm install` again and commit the lockfile: +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: ```json title="package.json" - "overrides": { "vitest": "~4.0.0" } + "overrides": { "vitest": "~4.1.12" } ``` + 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. + 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. ---