diff --git a/.agents/inbox/2026-10-27-router-upgrades-need-current-consumer-contracts.md b/.agents/inbox/2026-10-27-router-upgrades-need-current-consumer-contracts.md new file mode 100644 index 00000000..803c9e0d --- /dev/null +++ b/.agents/inbox/2026-10-27-router-upgrades-need-current-consumer-contracts.md @@ -0,0 +1,87 @@ +# Validate the installed router contract and real navigation controls + +On 2026-10-27, the #186 router work started at dev +`8712389e24f567f5b3e6ed132c1ac5d4dafb21fa`. A fresh-cache production audit +reported five moderate entries: Router/DOM and UUID/federation/subgraph. The +[navigation advisory](https://github.com/advisories/GHSA-wrjc-x8rr-h8h6) and +[hydration advisory](https://github.com/advisories/GHSA-337j-9hxr-rhxg) both +identify 7.18.0 as fixed. The selected Router/DOM 7.18.4 retains React 18 and the +existing declarative BrowserRouter; its Node 20 minimum fits supported Node 22/24. +The [versioned migration guide](https://reactrouter.com/7.18.4/upgrading/v6) +was checked. No multi-segment splat route, data router or component-local lazy +page declaration was found in the live app routes. + +The first navigation test copied an older upstream normalization expectation. +Router 6.30.6 failed three mixed-separator cases; installed 7.18.4 rejected all four +external-looking destinations instead of navigating to a normalized internal +path. That four-case failure was a test-contract mismatch, not an application +regression. Inspection of the installed `validateNavigationTarget` established +the actual rejection boundary. The corrected control captures the thrown error +and requires the route to remain unchanged. Reinstalling the original locked 6.30.6 +then running the final control produced **4 failures and 1 history pass**; +the 7.18.4 control plus existing role guards produced **9 passes**. + +The first six actual-browser cases produced **4 failures and 2 passes**, with no +retries. The two failing assumptions on each viewport were that the sign-in +page always selected Password, and that Admin opened its Users tab. The current +UI preserves the sign-in method and opens Feedback & operations. The corrected +test explicitly selects Password and Users, retains the exact route/content +assertions, and checks that denied users cannot mount the administration heading. +History, lazy-page navigation, viewer guards and fresh-context callback routing +remain required. Final browser and integrated results belong in the PR. + +Corrective rule: use the installed consumer and current UI to establish a +regression contract, then run the exact final negative control against the old +locked consumer. Preserve the earlier failing logs; do not treat a test rewrite +or a successful install as proof that navigation remains usable. The lock +regression checks every Router/DOM resolution, not just the manifest range. + +Limits: MemoryRouter tests establish library navigation behavior, while the +actual-browser suite covers GraphDone routes at 1440px and 390px. No hostile +application-controlled destination, SSR hydration surface, live provider +callback, deployed revision or broad security clearance is claimed. Test tokens +are confined to disposable local/CI accounts; failure traces are not public +credential samples. Local evidence is retained in +`/tmp/graphdone-resolution-20261027/router-*` and this record travels with the fix. + +At frozen `3e5a761dc0b067ed3827dd29fca50d45fb5d3952`, the corrected browser suite +passed 6/6 without retries, the full web suite passed 340/340 and the harness +passed 167/167. Review found the inventory overstated the installed-version guard: +it checked locked ranges and installed-pair equality, which could accept two +equally stale installed copies. The guard now requires each actual web consumer +to satisfy the fixed range and match its resolved lock entry. The inventory +states that exact scope. Refresh the changed harness and final CI before merge. + +## Empty-account CI correction + +Composed Router head `14a4fcc` [CI 40584829418](https://github.com/GraphDone/GraphDone-Core/actions/runs/40584829418) +completed 50 browser cases in 854 seconds: 48 passed, two viewer cases failed, +plus the harness's failed-process row. This was not a process-budget timeout. +Retained screenshots show the authenticated empty workspace at both widths. +The desktop assertion incorrectly required a populated graph's `view-content`; +the phone login helper did not recognize an authenticated account without a +graph selector. The local fixture had graphs and hid both assumptions. + +Use the existing authenticated `workspace-content` container for account-level +route checks and keep login-form exclusion. Router cases additionally assert +`me.role === VIEWER` through the real API before and after the fresh-context +callback. A standalone real-browser control recognizes an empty workspace, +rejects a password form even with workspace markup, and rejects a loading page. +With the old helper, the empty-workspace positive failed deterministically; +this helper correction adds no retry, wait or timeout. Original CI artifacts +remain evidence, and final current-head browser/full CI gates still apply. + +The first empty-workspace/password-form negative used a zero-height empty main; +independent review measured it invisible, so that case could pass for the wrong +reason. It now retains visible content and asserts both the workspace and the +password field are visible before requiring an unauthenticated result. + +Gate capacity is separate from that failure: focus CI's 44-case new-user sequence +passed in 892 seconds, only eight below the 900-second process budget; UUID's +same suite took 768 seconds. Required LOD and route acceptance now run as separate +blocking sequences after the 42-case new-user suite. All 51 cases remain required +(42 existing, two LOD, seven route/helper controls), with unchanged per-action +and per-test deadlines, one worker and zero retries. CI runs and uploads each +result independently; pr/full profiles also include both new sequences. CLI +contract tests execute the enrollment/trace arguments. Do not describe this as +fixing the original two viewer failures or count superseded runs as passes. diff --git a/.agents/issues/INDEX.md b/.agents/issues/INDEX.md index 5476627d..f0c5a0d8 100644 --- a/.agents/issues/INDEX.md +++ b/.agents/issues/INDEX.md @@ -2,6 +2,7 @@ | Symptom | Canonical lesson and regression | | --- | --- | +| A router major update passes install but path handling or route UI assertions differ | [Validate installed navigation and current UI contracts](../inbox/2026-10-27-router-upgrades-need-current-consumer-contracts.md); `routerNavigation.test.tsx`, `router-navigation.spec.ts`, dependency guard | | Status-note text lands in the title and Add remains disabled after quick edit opens | [Delayed autofocus must preserve chosen focus](../inbox/2026-10-27-delayed-autofocus-must-preserve-chosen-focus.md); `QuickEditSurface.test.tsx` controls the pending frame | | Minimap zoom or overview inspection is overwritten while a newly loaded graph settles | [Explicit camera input must stop automatic framing](../inbox/2026-10-27-explicit-camera-input-must-stop-auto-fit.md); timer-controlled `semantic-lod.spec.ts` negative control | | Dense-graph browser case exhausts its timeout after slow API fixture creation; cleanup errors disappear | [Bound fixture setup separately and fail on cleanup errors](../inbox/2026-10-27-cold-lod-fixtures-need-separate-budgets.md); `tests/e2e/graph/semantic-lod.spec.ts` | diff --git a/.agents/issues/security-audit-index.md b/.agents/issues/security-audit-index.md index f1b6a2a7..8a387fd5 100644 --- a/.agents/issues/security-audit-index.md +++ b/.agents/issues/security-audit-index.md @@ -2,6 +2,7 @@ | Symptom | Evidence and corrective rule | | --- | --- | +| Router advisory requires a major update and older upstream navigation expectations differ | [Installed consumer and route compatibility](../inbox/2026-10-27-router-upgrades-need-current-consumer-contracts.md); every locked Router path plus desktop/phone route gates | | Fresh production audit flags Undici through optional SQLite build tooling without a direct application import | [Trace the native-build consumer and patch its actual resolved copy](../inbox/2026-10-24-undici-native-build-dependency.md); `tests/lib/dependencies/undici.test.mjs` rejects affected locked and installed consumer versions | | Green production audit hides development-tool findings or package counts are treated as exploit counts | [Audit scope lesson](../inbox/2026-10-23-dependency-audit-scope.md); [dated security scope](../../docs/quality/SECURITY_SCOPE_2026-10-23.md) | | Nested lint glob stalls the process | `tests/lib/dependencies/minimatch.test.mjs`; scoped parser matcher override and bounded child-process regression | diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 73eae2d3..e389bb08 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -187,6 +187,16 @@ jobs: env: TEST_URL: http://localhost:3127 CI: true + - name: Semantic LOD acceptance + run: xvfb-run --auto-servernum node tests/run-unified.mjs --sequence semantic-lod --out test-artifacts/ci-semantic-lod + env: + TEST_URL: http://localhost:3127 + CI: true + - name: Route compatibility acceptance + run: xvfb-run --auto-servernum node tests/run-unified.mjs --sequence router-navigation --out test-artifacts/ci-router-navigation + env: + TEST_URL: http://localhost:3127 + CI: true - name: Mobile UI audit (layout / contrast / dialogs across every screen) run: node tests/run-unified.mjs --sequence mobile --out test-artifacts/ci-mobile env: diff --git a/docs/quality/SECURITY_SCOPE_2026-10-23.md b/docs/quality/SECURITY_SCOPE_2026-10-23.md index 5f212ff1..56f67cde 100644 --- a/docs/quality/SECURITY_SCOPE_2026-10-23.md +++ b/docs/quality/SECURITY_SCOPE_2026-10-23.md @@ -45,6 +45,29 @@ Follow-up implementation on 2026-10-23: [PR #169](https://github.com/GraphDone/G Source locations are relative to `packages/server/src/` and anchored to the snapshot above. Local, secret-free evidence is under `/tmp/graphdone-residual-*-audit.json`, `/tmp/graphdone-minimatch-*.log`, and `/tmp/graphdone-residual-auth-probe.log`; the latter reports only `parallelMagicLinkSuccesses: 2` and `arbitraryCaptchaAccepted: true`. +## Router correction on 2026-10-27 + +The dedicated #186 router change replaces Router/DOM 6.30.6 with 7.18.4, removes +the old `@remix-run/router` resolution and retains the existing declarative SPA. +Official advisories [GHSA-wrjc-x8rr-h8h6](https://github.com/advisories/GHSA-wrjc-x8rr-h8h6) +and [GHSA-337j-9hxr-rhxg](https://github.com/advisories/GHSA-337j-9hxr-rhxg) +identify 7.18.0 as fixed. Existing app routes use fixed destinations, with no +observed SSR hydration path; upgrading is not a claim that a live exploit was +demonstrated. The DOM compatibility package is retained to keep imports stable. + +The dependency regression checks every locked path and the actual web consumers +against both the patched range and their resolved lock entries. Navigation +controls reject external-looking mixed separators and preserve internal query, +fragment and history behavior; existing role guards remain covered. Six required +browser cases exercise public and authenticated deep links, reload, lazy pages, +desktop links/phone navigation, back/forward, role rejection and a fresh-context +login callback. The callback uses an owned password-issued token and does not +establish provider/email authentication. Initial negative and fixture-contract +failures are retained in the [dated lesson](../../.agents/inbox/2026-10-27-router-upgrades-need-current-consumer-contracts.md). +Fresh full-suite, browser, audit and final composition results are recorded on +the reviewable PR; UUID, CSS evidence, tooling and hosted acceptance remain +separate work. + ## 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. diff --git a/package-lock.json b/package-lock.json index 6559eba8..213cef61 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2584,15 +2584,6 @@ "integrity": "sha512-b1UQwcEZ4yCnMCD8DAL1VlbvBJE9/IX4FTIp7BG1xYpf29SLazLSrqUkj4w7Y5y7cCVP6E5tcqqcI0xemPkHug==", "license": "BSD-3-Clause" }, - "node_modules/@remix-run/router": { - "version": "1.23.4", - "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.23.4.tgz", - "integrity": "sha512-q7j5geK7xs3UJSdm9/iytUNclBnLmYx1EnSeCFXHPeutdqgIMeFeHtUZgS3EhlKxdBEAu8OwtJCwmLrEzpSs7Q==", - "license": "MIT", - "engines": { - "node": ">=14.0.0" - } - }, "node_modules/@rolldown/pluginutils": { "version": "1.0.0-beta.27", "dev": true, @@ -11174,38 +11165,6 @@ "node": ">=0.10.0" } }, - "node_modules/react-router": { - "version": "6.30.6", - "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.30.6.tgz", - "integrity": "sha512-5HfK7k5im7LTOB0EqCQmfvy4C13G92Ssj1VTmouTK3AJvyjKTnFuCV0vcMAD/JS+JC4DvDIBRrlAeJIFjh5VWg==", - "license": "MIT", - "dependencies": { - "@remix-run/router": "1.23.4" - }, - "engines": { - "node": ">=14.0.0" - }, - "peerDependencies": { - "react": ">=16.8" - } - }, - "node_modules/react-router-dom": { - "version": "6.30.6", - "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.30.6.tgz", - "integrity": "sha512-0RHKZz7wwffvkU+2MFVT2NnjK44ssLEV+m0CAJaS2Ksmorrwj7WxH00jO0SOCW26/tINUnJHToXblDs33I38YQ==", - "license": "MIT", - "dependencies": { - "@remix-run/router": "1.23.4", - "react-router": "6.30.6" - }, - "engines": { - "node": ">=14.0.0" - }, - "peerDependencies": { - "react": ">=16.8", - "react-dom": ">=16.8" - } - }, "node_modules/react-syntax-highlighter": { "version": "16.1.1", "resolved": "https://registry.npmjs.org/react-syntax-highlighter/-/react-syntax-highlighter-16.1.1.tgz", @@ -11777,6 +11736,12 @@ "node": ">= 0.8.0" } }, + "node_modules/set-cookie-parser": { + "version": "2.7.3", + "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.3.tgz", + "integrity": "sha512-irZHtTqurLna57KQG/s+fzOwv/J6XSH4cs8M/vsrY3uCaQ3S6+QJguAH0WdN47SXlk82voMZ4naWMQf8fdEoIg==", + "license": "MIT" + }, "node_modules/set-function-length": { "version": "1.2.2", "license": "MIT", @@ -15928,7 +15893,7 @@ "react": "^18.2.0", "react-dom": "^18.2.0", "react-markdown": "^10.1.0", - "react-router-dom": "^6.20.0", + "react-router-dom": "^7.18.4", "react-syntax-highlighter": "^16.1.1", "remark-gfm": "^4.0.1", "tailwindcss": "^3.3.0", @@ -16331,6 +16296,19 @@ "node": "18 || 20 || >=22" } }, + "packages/web/node_modules/cookie": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.1.1.tgz", + "integrity": "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, "packages/web/node_modules/eslint": { "version": "9.39.5", "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.5.tgz", @@ -16531,6 +16509,44 @@ "url": "https://github.com/sponsors/isaacs" } }, + "packages/web/node_modules/react-router": { + "version": "7.18.4", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.18.4.tgz", + "integrity": "sha512-PUPQcMhMGRAslLcvtlPz/kmzBEWPhLdgLFrL7pLNepBL6dX0lWj4WD2cUYVgYCuT3jxvghYFg81cDTj44DhetQ==", + "license": "MIT", + "dependencies": { + "cookie": "^1.0.1", + "set-cookie-parser": "^2.6.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "react": ">=18", + "react-dom": ">=18" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + } + } + }, + "packages/web/node_modules/react-router-dom": { + "version": "7.18.4", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.18.4.tgz", + "integrity": "sha512-yrfmJHIpDG7taCpqKjT1G5B6q3O2K+RN8/fgNf0lTjCwiPbQ0ei6vXX9ZjQR+7ld8Tr7Z5xmyMnZ8YJrphWQUw==", + "license": "MIT", + "dependencies": { + "react-router": "7.18.4" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "react": ">=18", + "react-dom": ">=18" + } + }, "packages/web/node_modules/ts-api-utils": { "version": "2.5.0", "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.5.0.tgz", diff --git a/packages/web/package.json b/packages/web/package.json index f011b528..d2e4e70d 100644 --- a/packages/web/package.json +++ b/packages/web/package.json @@ -26,7 +26,7 @@ "react": "^18.2.0", "react-dom": "^18.2.0", "react-markdown": "^10.1.0", - "react-router-dom": "^6.20.0", + "react-router-dom": "^7.18.4", "react-syntax-highlighter": "^16.1.1", "remark-gfm": "^4.0.1", "tailwindcss": "^3.3.0", diff --git a/packages/web/src/lib/routerNavigation.test.tsx b/packages/web/src/lib/routerNavigation.test.tsx new file mode 100644 index 00000000..5244f71a --- /dev/null +++ b/packages/web/src/lib/routerNavigation.test.tsx @@ -0,0 +1,45 @@ +import { render, screen, fireEvent, waitFor } from '@testing-library/react'; +import { describe, expect, it } from 'vitest'; +import { useState } from 'react'; +import { MemoryRouter, Route, Routes, useLocation, useNavigate } from 'react-router-dom'; + +function Navigation({ destination }: { destination: string }) { + const navigate = useNavigate(); + const location = useLocation(); + const [error, setError] = useState(''); + return <> + + + + + {error &&
{error}
} +