diff --git a/.agents/inbox/2026-10-27-federation-consumers-bound-uuid-upgrades.md b/.agents/inbox/2026-10-27-federation-consumers-bound-uuid-upgrades.md new file mode 100644 index 00000000..eec78399 --- /dev/null +++ b/.agents/inbox/2026-10-27-federation-consumers-bound-uuid-upgrades.md @@ -0,0 +1,79 @@ +# Validate federation consumers before replacing their UUID dependency + +On 2026-10-27, work for #186 started from dev +`ef495eff15ed7aac51e72dc7a1db1bb96d2b3695` with Node 22.18.0/npm 10.8.2. +`npm explain uuid` identified Core's direct UUID 9.0.1 and +`@neo4j/graphql@5.12.15 → @apollo/subgraph@2.14.4 → +@apollo/federation-internals@2.14.4 → uuid@9.0.1`. +Server UUID 11.1.1 and Mermaid UUID 14.0.3 were separate, already patched paths. + +A bounded direct call on the baseline gave the same result for UUID v3 and v5: +a one-byte output buffer was modified and no error was thrown. The advisory +[GHSA-w5hq-g745-h8pq](https://github.com/advisories/GHSA-w5hq-g745-h8pq) +identifies 11.1.1 as patched. Inspected application calls use unbuffered v4; +federation's `src/operations.ts` uses unbuffered v1 for deferred-directive keys. +This source observation does not establish safety for future callers. + +## Cause and correction + +Updating subgraph to 2.15.1 removed the old federation dependency, but a real +`Neo4jGraphQL.getSubgraphSchema()` probe with a federation v2.3 `@link` failed: +`TypeError: doc.definitions is not iterable` in `concatAST`. The installed +subgraph's `modulesFromSDL` no longer normalizes Neo4j's single +`{ typeDefs, resolvers }` input. The initial probe also omitted `@link`; that +fixture error was corrected before observing this compatibility failure. + +Retain subgraph/federation 2.14.4. Upgrade Core directly to UUID ^11.1.1 and +scope the transitive override to `@apollo/federation-internals@2.14.4` only. +UUID 11 retains CommonJS support; UUID 12 removes it. The existing server UUID +copy deduplicates to 11.1.1; Mermaid remains 14.0.3. No other package version +changes are part of this fix. + +Adding an override and running `npm install --package-lock-only` initially left +the old root UUID resolution. The lock and buffer regression failed, exposing +that stale result. `npm update uuid --package-lock-only --legacy-peer-deps` +then `npm ci --legacy-peer-deps --no-audit --no-fund` installed 1,220 packages; +`npm ls uuid @apollo/subgraph --all` confirmed the intended consumer paths. +Do not infer the installed result from an override declaration alone. + +## Evidence and limits + +`node --test tests/lib/dependencies/uuid.test.mjs`: **4 passed**, covering every +locked UUID, actual consumer-relative CommonJS and ESM imports, valid generated +IDs, invalid v3/v5/v6 buffer bounds without partial writes, and real Neo4j +federation schema generation. An attempted `_service` execution without a driver +was a fixture error; schema generation/printing needs no database. Database-backed +execution remains the separate real-Neo4j contract gate. + +A fresh-cache `npm audit --omit=dev --json` after the correction reported +**2 moderate router entries and 1 high Undici entry**, with no UUID/federation +finding. Undici is separately addressed by #194; that audit preceded composition with +merged dev #194. This count is dated dependency metadata, not a live exposure or +full-security claim. Complete runtime, browser and final CI results belong in +the associated PR discussion; no merge/deployment is established here. + +Corrective rule: upgrade the narrowest compatible consumer chain, keep the +broken-upstream evidence, test both module systems and the actual schema API, +and verify the resolved lock and installed paths after a clean install. Revisit +the version-scoped override when federation changes; do not widen it silently. + +## Review and composed validation + +Independent review found that a simple `>=11.1.1` lock guard would wrongly +accept affected 12.0.0 and 13.0.0. The regression now uses the complete advisory +union and affected/fixed controls on every listed major, including malformed +versions and an unfixed prerelease. The selected 11.1.1/14.0.3 versions were +already patched; the correction prevents a future false green. + +Composition `aba24a34d193b60e2318cd5b197f66e64f320215` incorporates reviewed dev +`32149ee61ea5436831d95d76e4c4b6dc2db619a3`. A clean install and fresh installed +production audit reported **2 moderate router entries and 0 high/critical**. +All four package unit suites passed with one task/worker at a time, retaining +their original timeouts, and the composed harness passed 167/167 before the +additional advisory-range control. An earlier concurrent web run hit the +existing five-second save-test deadline under host load; its single-worker +rerun passed 326/326. The following earlier harness ran while composition was +changed and is discarded as provenance, not counted as validation. Freeze the +source/dependencies throughout a run and use bounded local concurrency. The +final advisory-test change requires its own harness run and exact-head CI; +results are recorded in the PR discussion. No failed run was erased. diff --git a/.agents/issues/INDEX.md b/.agents/issues/INDEX.md index 42ef9dbe..fafada95 100644 --- a/.agents/issues/INDEX.md +++ b/.agents/issues/INDEX.md @@ -20,3 +20,4 @@ | `mergePullRequest` refuses a workflow update without OAuth `workflow` scope | [Recorded API refusal and same-account SSH identity check](../inbox/2026-10-23-pr-base-ref-is-not-live-tip.md#a-workflow-scoped-api-refusal-needs-its-own-evidence); authentication alone does not prove write authorization or merge completion | | MCP tool calls succeed but the stdio client reports JSON parse errors from startup, CPU or consistency messages | [Successful MCP calls do not prove a clean stdio protocol](../inbox/2026-10-23-mcp-stdio-diagnostics.md); `packages/mcp-server/tests/neo4j-contract.test.ts` checks real subprocess traffic and stderr diagnostics | | `dev` has an owner-authorized admin exemption; a review-rule blocker or docs-only change tempts skipping validation | [Dev admin integration playbook](../playbooks/dev-admin-integration.md); [dated authorization and merge evidence](../inbox/2026-10-23-dev-admin-integration.md). Admin authority is not a non-author approval; verify final branch CI before closeout | +| A federation upgrade removes UUID findings but breaks Neo4j schema generation; an override leaves the old lock resolution | [Validate federation consumers and installed UUID paths](../inbox/2026-10-27-federation-consumers-bound-uuid-upgrades.md); `tests/lib/dependencies/uuid.test.mjs` | diff --git a/.agents/issues/security-audit-index.md b/.agents/issues/security-audit-index.md index 039affee..f1b6a2a7 100644 --- a/.agents/issues/security-audit-index.md +++ b/.agents/issues/security-audit-index.md @@ -7,3 +7,4 @@ | Nested lint glob stalls the process | `tests/lib/dependencies/minimatch.test.mjs`; scoped parser matcher override and bounded child-process regression | | A fresh audit reports Zod high severity after an earlier green scan; bounded collections still walk rejected elements | [Zod consumer and bounded-validation lesson](../inbox/2026-10-23-zod-constrained-collections.md); recheck the resolved SDK dependency and use an isolated audit cache without inferring why older metadata differed | | Browser login lockout is mistaken for server throttling; one-use email token succeeds concurrently | [Remaining authentication work](../../docs/quality/SECURITY_SCOPE_2026-10-23.md#remaining-authentication-work) | +| A federation upgrade removes UUID findings but breaks Neo4j schema generation; an override leaves the old lock resolution | [Validate federation consumers and installed UUID paths](../inbox/2026-10-27-federation-consumers-bound-uuid-upgrades.md); `tests/lib/dependencies/uuid.test.mjs` | diff --git a/docs/quality/SECURITY_SCOPE_2026-10-23.md b/docs/quality/SECURITY_SCOPE_2026-10-23.md index 07012d82..5f212ff1 100644 --- a/docs/quality/SECURITY_SCOPE_2026-10-23.md +++ b/docs/quality/SECURITY_SCOPE_2026-10-23.md @@ -48,3 +48,18 @@ Source locations are relative to `packages/server/src/` and anchored to the snap ## Coverage still needed The folder tests do not establish all generated Graph/WorkItem/Edge permissions, concurrent deletion integrity, MCP/agent grant revocation, share-link scopes, session/cookie policy, uploads, backups, email delivery, or deployed proxy controls. Those need isolated contracts and user-level tests with current roles, multiple accounts, races, and negative cases. Dependency counts, passing smoke tests, and absence of a discovered source sink are separate pieces of evidence; none is a full security assessment. + +## UUID/federation follow-up on 2026-10-27 + +The #186 UUID correction starts from dev `ef495eff15ed7aac51e72dc7a1db1bb96d2b3695`. +Core's direct UUID 9.0.1 becomes ^11.1.1; federation-internals 2.14.4 has a +version-scoped UUID 11.1.1 override. Server deduplicates to that patched copy; +Mermaid retains 14.0.3. Subgraph/federation remain 2.14.4 after a 2.15.1 schema +compatibility failure. The [consumer lesson](../../.agents/inbox/2026-10-27-federation-consumers-bound-uuid-upgrades.md) +records negative controls, exact paths, clean install and four dependency +regressions. The pre-composition fresh-cache production audit had two moderate router +entries and high Undici. After composing merged dev #194 at `aba24a34d193b60e2318cd5b197f66e64f320215`, +a clean install and a second fresh-cache installed audit reported **two moderate +router entries, zero high/critical entries**, and no UUID/federation finding. +The earlier inventory above remains historical. No Tailwind declaration was reclassified. +This change does not close the router, CSS, dev-tool or hosted acceptance work. diff --git a/package-lock.json b/package-lock.json index 0a9f0196..6559eba8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13216,14 +13216,16 @@ } }, "node_modules/uuid": { - "version": "9.0.1", + "version": "11.1.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-11.1.1.tgz", + "integrity": "sha512-vIYxrBCC/N/K+Js3qSN88go7kIfNPssr/hHCesKCQNAjmgvYS2oqr69kIufEG+O4+PfezOH4EbIeHCfFov8ZgQ==", "funding": [ "https://github.com/sponsors/broofa", "https://github.com/sponsors/ctavan" ], "license": "MIT", "bin": { - "uuid": "dist/bin/uuid" + "uuid": "dist/esm/bin/uuid" } }, "node_modules/vary": { @@ -14237,7 +14239,7 @@ "license": "MIT", "dependencies": { "neo4j-driver": "^5.15.0", - "uuid": "^9.0.1" + "uuid": "^11.1.1" }, "devDependencies": { "@types/uuid": "^9.0.7", @@ -15911,19 +15913,6 @@ "typescript": ">=4.8.4" } }, - "packages/server/node_modules/uuid": { - "version": "11.1.1", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-11.1.1.tgz", - "integrity": "sha512-vIYxrBCC/N/K+Js3qSN88go7kIfNPssr/hHCesKCQNAjmgvYS2oqr69kIufEG+O4+PfezOH4EbIeHCfFov8ZgQ==", - "funding": [ - "https://github.com/sponsors/broofa", - "https://github.com/sponsors/ctavan" - ], - "license": "MIT", - "bin": { - "uuid": "dist/esm/bin/uuid" - } - }, "packages/web": { "name": "@graphdone/web", "version": "0.3.1-alpha", diff --git a/package.json b/package.json index b176a83b..f9d3b6a0 100644 --- a/package.json +++ b/package.json @@ -103,6 +103,9 @@ "@graphql-tools/utils": "12.1.0", "@typescript-eslint/typescript-estree@6.21.0": { "minimatch": "9.0.7" + }, + "@apollo/federation-internals@2.14.4": { + "uuid": "11.1.1" } } } diff --git a/packages/core/package.json b/packages/core/package.json index 465b90a4..49be98b6 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -14,7 +14,7 @@ "clean": "rm -rf dist coverage" }, "dependencies": { - "uuid": "^9.0.1", + "uuid": "^11.1.1", "neo4j-driver": "^5.15.0" }, "devDependencies": { diff --git a/tests/lib/dependencies/uuid.test.mjs b/tests/lib/dependencies/uuid.test.mjs new file mode 100644 index 00000000..b0590895 --- /dev/null +++ b/tests/lib/dependencies/uuid.test.mjs @@ -0,0 +1,83 @@ +import assert from 'node:assert/strict'; +import { spawnSync } from 'node:child_process'; +import { readFile } from 'node:fs/promises'; +import { createRequire } from 'node:module'; +import test from 'node:test'; + +const root = new URL('../../../', import.meta.url); +const require = createRequire(new URL('package.json', root)); +const coreRequire = createRequire(new URL('packages/core/package.json', root)); +const federationRequire = createRequire(require.resolve('@apollo/federation-internals')); +const semver = require('semver'); +const affectedRange = '<11.1.1 || >=12.0.0 <12.0.1 || >=13.0.0 <13.0.1'; +const hasBufferFix = version => Boolean(semver.valid(version)) + && !semver.satisfies(version, affectedRange, { includePrerelease: true }); + +test('the advisory guard rejects affected releases on every maintained major', () => { + for (const version of ['9.0.1', '11.1.0', '11.1.1-beta.0', '12.0.0', '13.0.0', 'invalid']) { + assert.equal(hasBufferFix(version), false, version); + } + for (const version of ['11.1.1', '12.0.1', '13.0.1', '14.0.3']) { + assert.equal(hasBufferFix(version), true, version); + } +}); + +test('every locked UUID path has the buffered-output fix', async () => { + const lock = JSON.parse(await readFile(new URL('package-lock.json', root), 'utf8')); + const paths = Object.entries(lock.packages).filter(([path]) => /(^|\/)node_modules\/uuid$/.test(path)); + assert.ok(paths.length > 0); + for (const [path, entry] of paths) { + assert.ok(hasBufferFix(entry.version), `${path}: ${entry.version}`); + } +}); + +test('Core CommonJS and ESM consumers generate valid unbuffered UUIDs', () => { + const cjs = coreRequire('uuid'); + const ids = Array.from({ length: 100 }, () => cjs.v4()); + assert.equal(new Set(ids).size, ids.length); + for (const id of ids) { + assert.ok(cjs.validate(id)); + assert.equal(cjs.version(id), 4); + } + const esm = spawnSync(process.execPath, ['--input-type=module', '-e', ` + import assert from 'node:assert/strict'; + import { v4, validate, version } from 'uuid'; + const id = v4(); + assert.ok(validate(id)); + assert.equal(version(id), 4); + `], { cwd: new URL('packages/core/', root), encoding: 'utf8', timeout: 5000 }); + assert.equal(esm.error, undefined); + assert.equal(esm.status, 0, esm.stderr); + const federation = federationRequire('uuid'); + assert.equal(federation.version(federation.v1()), 1); +}); + +test('Core and federation UUID reject invalid buffers without partially writing', () => { + for (const consumer of [coreRequire, federationRequire]) { + const uuid = consumer('uuid'); + for (const name of ['v3', 'v5', 'v6']) { + for (const [length, offset] of [[1, 0], [16, 1], [16, -1]]) { + const buffer = new Uint8Array(length).fill(42); + const original = buffer.slice(); + const args = name === 'v6' ? [{}, buffer, offset] : ['graphdone', uuid[name].DNS, buffer, offset]; + assert.throws(() => uuid[name](...args), RangeError, `${name}: length=${length} offset=${offset}`); + assert.deepEqual(buffer, original); + } + } + } +}); + +test('Neo4j builds its federation schema through the installed Apollo consumer', async () => { + const { Neo4jGraphQL } = require('@neo4j/graphql'); + const { printSubgraphSchema } = require('@apollo/subgraph'); + const neo = new Neo4jGraphQL({ typeDefs: ` + extend schema @link(url: "https://specs.apollo.dev/federation/v2.3", import: ["@key"]) + type WorkItem @key(fields: "id") { id: ID! @id title: String! } + ` }); + const schema = await neo.getSubgraphSchema(); + const sdl = printSubgraphSchema(schema); + assert.match(sdl, /type WorkItem\b/); + assert.match(sdl, /@key\(fields: "id"\)/); + assert.ok(schema.getQueryType().getFields()._service); + assert.ok(schema.getQueryType().getFields()._entities); +});