Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
# Reconcile updated advisory ranges with the already locked consumer

The October 23 inventory and #186 recorded three moderate CSS-chain entries.
On 2026-10-27, fresh-cache `npm audit --omit=dev --json` on dev
`8712389e24f567f5b3e6ed132c1ac5d4dafb21fa` reported five moderate entries:
Router/DOM and UUID/federation/subgraph, with no CSS-chain entry.

The [official selector-parser advisory](https://github.com/advisories/GHSA-rj75-hqrm-r3gf)
was read directly and via `gh api /advisories/GHSA-rj75-hqrm-r3gf`.
Its `updated_at` is `2026-10-26T14:07:57Z`, `withdrawn_at` is null, and its
affected ranges are `<6.1.5` and `>=7.0.0 <7.1.6`, with fixes 6.1.5 and 7.1.6.
`npm ls postcss-selector-parser postcss-nested tailwindcss --all` resolves
Tailwind 3.4.19 and postcss-nested 6.2.0 to parser 6.1.5. This was already locked
in commit `36c02c020832cafd8f67b9fae85d126827e89a98`, dated 2026-10-23:
`git show 36c02c0:package-lock.json` contains the same parser version.

The historical audit remains evidence of what that run reported. The current
official fixed ranges and actual consumer now establish that this locked CSS
path is patched. There is no dependency update or reclassification in this
follow-up. Do not force a Tailwind major migration or attribute the older audit
to an unproved cache/publication cause merely to make the historical count agree.

The dedicated regression checks both maintained fixed branches and every locked
parser path, then resolves the actual Tailwind and postcss-nested consumers.
It verifies flat-selector round trips, nested selector output, and generated
base/responsive Tailwind utilities. The flat input is bounded at 2048 classes;
this is semantic coverage, not a claimed CPU-exhaustion benchmark. Fresh build,
integrated browser checks and exact-head results belong in the PR discussion.

Limits: trusted build-time CSS was the inspected application surface. This is
clearance of the named advisory for the current resolution, not full CSS/npm/OS
security or deployed-site acceptance. Router, UUID and dev-tool work have their
own PRs. Preserve fresh dated audit payloads as metadata can change independently
of the lockfile. Local evidence: `/tmp/graphdone-resolution-20261027/css-*`.
1 change: 1 addition & 0 deletions .agents/issues/INDEX.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

| Symptom | Canonical lesson and regression |
| --- | --- |
| An old audit lists a dependency that the current advisory identifies as patched | [Reconcile advisory backports with the existing lock](../inbox/2026-10-27-reconcile-advisory-backports-with-locked-consumers.md); `selector-parser.test.mjs` |
| Phone idle-motion sample includes one camera jump immediately after rotation while physics is already at rest | [Wait for edits and camera resize readiness](../inbox/2026-10-27-motion-sampling-needs-camera-readiness.md); controlled ResizeObserver delivery in `graph-motion.spec.ts` |
| Animated zoom stops near the overview exit threshold instead of reaching its target | [Title refresh must not own the camera](../inbox/2026-10-27-lod-title-refresh-must-not-own-camera.md); frame-controlled `semantic-lod.spec.ts` negative control |
| Disposable Git fixture teardown reports ENOTEMPTY under .git after a synchronous commit | [Prevent detached fixture maintenance](../inbox/2026-10-27-disposable-git-fixtures-must-own-maintenance.md); `tests/lib/runner/unified-cli.test.mjs` |
Expand Down
1 change: 1 addition & 0 deletions .agents/issues/security-audit-index.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

| Symptom | Evidence and corrective rule |
| --- | --- |
| Historical CSS audit findings disappear while the lock is unchanged | [Reconcile official backport ranges with actual consumers](../inbox/2026-10-27-reconcile-advisory-backports-with-locked-consumers.md); locked-path and Tailwind/parser regressions |
| 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) |
Expand Down
20 changes: 20 additions & 0 deletions docs/quality/SECURITY_SCOPE_2026-10-23.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,26 @@ 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`.

## CSS advisory reconciliation on 2026-10-27

The historical three CSS entries above remain the result of the October 23
inventory. On October 27, the [official advisory](https://github.com/advisories/GHSA-rj75-hqrm-r3gf)
lists parser 6.1.5 and 7.1.6 as patched; affected ranges are `<6.1.5` and
`>=7.0.0 <7.1.6`. Its metadata was updated on October 26 and it is not withdrawn.
Live dev `8712389` already locks parser 6.1.5 for Tailwind 3.4.19 and
postcss-nested 6.2.0, the same parser resolution present in `36c02c0` on October 23.
The fresh-cache production audit has no CSS-chain finding. This follow-up does
not alter any dependency declaration, lockfile or production/dev classification.

The dedicated guard checks every locked parser path and both actual CSS
consumers. Semantic tests preserve flat and nested selectors and generated
responsive Tailwind utilities; they are not a timing benchmark. This resolves
the named advisory for the current locked chain, without forcing a Tailwind
migration. Keep earlier audit counts as dated observations and do not infer a
cache cause. The [canonical lesson](../../.agents/inbox/2026-10-27-reconcile-advisory-backports-with-locked-consumers.md)
records commands, sources and limits. Final build/browser/CI evidence belongs
to the separate CSS review. Router, UUID and toolchain work remain separate.

## Router correction on 2026-10-27

The dedicated #186 router change replaces Router/DOM 6.30.6 with 7.18.4, removes
Expand Down
58 changes: 58 additions & 0 deletions tests/lib/dependencies/selector-parser.test.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import assert from 'node:assert/strict';
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 webRequire = createRequire(new URL('packages/web/package.json', root));
const tailwindRequire = createRequire(webRequire.resolve('tailwindcss'));
const nestedRequire = createRequire(tailwindRequire.resolve('postcss-nested'));
const semver = require('semver');
const patched = version => semver.valid(version) !== null && semver.satisfies(version, '>=6.1.5 <7.0.0 || >=7.1.6');

test('selector advisory guard recognizes the fixed maintained branches', () => {
for (const version of ['6.1.4', '6.1.5-beta.0', '7.0.0', '7.1.5', '7.1.6-beta.0', 'invalid']) assert.equal(patched(version), false, version);
for (const version of ['6.1.5', '7.1.6', '7.1.7']) assert.equal(patched(version), true, version);
});

test('every locked and actual Tailwind selector parser contains the backport', async () => {
const lock = JSON.parse(await readFile(new URL('package-lock.json', root), 'utf8'));
const paths = Object.entries(lock.packages).filter(([path]) => /(^|\/)node_modules\/postcss-selector-parser$/.test(path));
assert.ok(paths.length > 0);
for (const [path, entry] of paths) assert.ok(patched(entry.version), `${path}: ${entry.version}`);
for (const consumer of [tailwindRequire, nestedRequire]) assert.ok(patched(consumer('postcss-selector-parser/package.json').version));
});

test('actual CSS consumers preserve flat and nested selector semantics', async () => {
const input = '.graph:hover > .node[data-kind="task"]:is(.selected, .pending)';
for (const consumer of [tailwindRequire, nestedRequire]) {
const parser = consumer('postcss-selector-parser');
assert.equal(parser().processSync(input), input);
const flat = '.node'.repeat(2048);
const ast = parser().astSync(flat);
let count = 0;
ast.walkClasses(() => count++);
assert.equal(count, 2048);
assert.equal(ast.toString(), flat);
}
const postcss = webRequire('postcss');
const nested = tailwindRequire('postcss-nested');
const output = await postcss([nested()]).process('.graph { & > .node { color: white; } }', { from: undefined });
const selectors = [];
output.root.walkRules(rule => selectors.push(rule.selector));
assert.deepEqual(selectors, ['.graph > .node']);
});

test('installed Tailwind builds base and responsive utilities through its parser', async () => {
const postcss = webRequire('postcss');
const tailwind = webRequire('tailwindcss');
const output = await postcss([tailwind({ content: [{ raw: '<div class="gap-2 text-sm md:flex"></div>', extension: 'html' }], corePlugins: { preflight: false } })])
.process('@tailwind utilities;', { from: undefined });
const selectors = [];
const media = [];
output.root.walkRules(rule => selectors.push(rule.selector));
output.root.walkAtRules('media', rule => media.push(rule.params));
for (const selector of ['.gap-2', '.text-sm', '.md\\:flex']) assert.ok(selectors.includes(selector), selector);
assert.ok(media.includes('(min-width: 768px)'));
});
Loading