Repository navigation
routine 2026-10-05 (2): README rendering can't stall the server, copy buttons that speak and fail loudly, client-bundle audit, header hardening, 404s that say who asked - #15
Merged
Conversation
… cubically marked 18.0.14 matched the gap before a link destination with `\s*`, which overlaps the destination class on unicode whitespace, so `[](` followed by a run of U+00A0 backtracked cubically (markedjs/marked#4106). Through this site's own renderMarkdown, 4 KB of it took 10.3 s, all on the server's one thread — one README edit in any of the seven repos could stall every page. On 18.1.0 the same input renders in 0.1 ms. All 87 documents the site renders today (7 READMEs, the 76 live posts, the 4 test fixtures) come out byte-identical on both versions. test/markdown-backtracking.test.ts renders 8 KB of three unicode spaces through both the README and post paths in under 500 ms; on 18.0.14 it does not finish inside 60 s. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…owser
axe-core 4.14.0 turns `label-content-name-mismatch` on by default (WCAG 2.5.3,
label in name). It compares a control's accessible name with its visible
text, which jsdom cannot determine, so in `npm run a11y` it only ever came back
undecided — on the copy buttons of every project page — and no pass judged it.
It moves to the layout list: skipped by name in scripts/check-a11y.mjs, run
in scripts/check-a11y-browser.mjs at 1280 and 390 px. Today's pages pass
("[copy]" sits at the start of "Copy …"). Proved it bites: with CodeBlock's
label planted as "Download …", the browser pass fails with 12 violations,
one per project page per width.
Both passes clean over 95 pages, including the 76 live posts. README
names the new rule beside the other browser-only ones.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
READMEs are fetched and prepared at request time on the server's one thread. Seven patterns in shared/markdown/readme.ts and slug.ts were quadratic on input that opens something and never closes it: a global regex is retried at every offset, and each of these could scan from one start past where the next one began. At 100 KB: absolutize, a run of `[` 5.2 s → 0.5 ms absolutize, a run of `` 1.0 s → 0.2 ms slugify, a run of `<` 5.0 s → 0.9 ms renderMarkdown, 100 KB heading 2.5 s → 34 ms A link label now stops at the next `[`, a tag at the next `<`, a URL may end at the end of the input, and the logo test moved out of the pattern into a callback. All 7 READMEs (prepared markdown and HTML) and the 76 live posts are byte-identical before and after. The edges that move are pinned in the test: `` is now read as text and a link, the way marked renders it, and a stray `<` before a tag stays out of it, which is what GitHub's slug of that heading is made from. test/markdown-backtracking.test.ts times each input: 11 cases, every one failing on the old patterns. ROADMAP gains the item, with the marked fix from the previous commit. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Pressing [copy] changed the visible text to [copied], but the button's
accessible name is its aria-label ("Copy to clipboard: …", "Copy <label>"),
so a screen reader announced nothing (WCAG 4.1.3, status messages).
app/components/CodeLine.vue and CodeBlock.vue each render an empty
`role="status"` sr-only region on the server, so it exists before it
changes, and it reads "Copied to clipboard" for the same 1.6 s as the
visible label. CodeBlock's sits beside its figcaption, not in it, so it
is never part of the figure's name.
Checked in headless Chromium over CDP with clipboard permission: after
activation the polite live region's text in the accessibility tree is
"Copied to clipboard", the clipboard holds the command (and the
config.toml block for CodeBlock), and both reset after 1.6 s. Pages are
unchanged to the eye: the pixel difference against the previous build is
in the masthead's animated glass only, and the same size as between two
earlier builds. check, a11y and a11y:browser clean over 95 pages.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Every live response carried `x-powered-by: Nuxt`. Nuxt's renderer, payload and island handlers set it themselves after route rules apply, so no rule can take it off; server/plugins/powered-by.ts removes it in Nitro's `beforeResponse`, which runs after any handler. It tells a scanner which framework's advisories to try first and nothing else. `Cross-Origin-Opener-Policy: same-origin` joins the `/**` headers in nuxt.config.ts: no page keeps a handle on a window it opens, and a cross-origin page that opens this site gets none into it. scripts/check-site.mjs fails any response with x-powered-by and any page without the policy. Against the previous build: 94 failures for each. After: gone from pages, the 404 page, /healthz, /api, the XML routes, static files and HEAD; check, a11y and a11y:browser clean over 95 pages; routes unchanged; client navigation home → onyums → its blog in the browser with no console errors; screenshots differ only in the masthead's animated glass, as between any two builds. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
When `navigator.clipboard.writeText` was refused (denied permission, insecure context, a frame that forbids it) the copy buttons did nothing: still `[copy]`, nothing announced, and the visitor pasted whatever their clipboard held before. Found when the built-in browser pane refused it while the previous commit was being tested. app/composables/useCopy.ts is now the one copy of the logic CodeLine and CodeBlock each carried. On refusal it selects the code element's contents, so the platform's copy shortcut finishes the job, shows `[selected]` for 4 s and announces "Could not copy. The text is selected; copy it with your keyboard." in the status region. Checked in headless Chromium over CDP: - permission granted: `[copied]`, "Copied to clipboard", the clipboard holds the command / the config.toml block, reset after 1.6 s - permission denied (`NotAllowedError`): `[selected]`, the message above, the selection is exactly the command without its `$` sigil (and the whole config.toml block), reset after 4 s, no overflow at 390 px (screenshot in the run's scratch dir) check, a11y, a11y:browser clean over 95 pages; 226 tests; routes unchanged; page screenshots unchanged. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Nuxt 4.6.0 was published an hour into tonight's run. A trial in a scratch worktree was clean on every gate (typecheck, 226 tests, build, audit:runtime, check, a11y, a11y:browser; 23 routes as on 4.5.2; byte-identical page sizes; one JSON log line per request) and changes nothing visible: one CSS line height rounds to 16 px instead of 15.98. It stays a `[ ]` with that evidence: a 420-commit minor with a CLI major, an hour old, should not go out in an unattended deploy. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The request log has two writers: `afterResponse` for answered requests, and the `error` hook for requests that threw, every 404 and 500. Its comment promised "same shape as the line above", but the error line stopped at `via`: no `ip`, `ua` or `ref`. In fifteen hours of live log that was 627 lines, nearly all scanners probing `.env`, `.php` and `.git`, and the only lines with no address or user agent. server/plugins/request-log.ts builds both from one `requester(event)`. Checked on a built server with a forwarded address, user agent and referer: a 200, a page 404, an API 404 and a HEAD 404 all carry the three fields; an onion-marked request keeps `ip: null` and `via: onion`; each request is still exactly one line. Gate: typecheck, 226 tests, build, check, a11y, a11y:browser; routes and pages unchanged. README says the fields are on every line. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The runtime audit asked the registry about what the server ships and said plainly that the client bundle was not covered: it is built without sourcemaps (rightly; they would be served), so there was nothing to read its packages from. scripts/lib/client-packages.ts is a Vite plugin applied only to the client environment. In generateBundle it maps every module with rendered code to its package and that package's own version, and a Nitro `compiled` hook in nuxt.config.ts writes the list to .output/server/client-packages.json: inside the image, never under public/, and a 404 from the server at every spelling tried. scripts/audit-runtime.mjs adds those packages to its registry query and fails when the file is missing or empty, rather than passing with the browser half unasked. Today: 32 client packages, 14 of which the server audit never saw; 79 asked in all; clean. Proved it bites: a planted marked@4.0.9 fails with its two advisories; a missing and an empty inventory both fail. Gate: typecheck, 226 tests, build, check, a11y, a11y:browser (95 pages, both widths); routes and pages unchanged. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
npm run sync, authenticated, all 8 projects including splimes: issue counts and one push date moved. test/snapshot.test.ts passes. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
… the splimes merge Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
physics515
added a commit
that referenced
this pull request
Oct 8, 2026
…24 CI actions (#17) * feat(deps): Nuxt 4.6.0 Lands the roadmap's "Nuxt 4.6 after a soak" item, two and a half days after release. 4.6.0's security section covers the internal error route being reachable from outside, unhandled error data reaching the error page, and error-render recursion tracked by a client-controllable header. package.json, package-lock.json: nuxt ^4.6.0, then npm dedupe. ROADMAP.md: the item ticked, with what was checked. Typecheck, 236 tests, build, audit:runtime, check, a11y and a11y:browser clean; every route answers as on 4.5.2 with same-sized bodies; the image builds on node 24.21. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * fix(deps): shell-quote 1.12.0, past its quote() injection advisory Only @nuxt/devtools' launch-editor depends on it, and devtools is disabled and never ships, so nothing reachable changes. It is a lockfile-only, in-range patch that clears a critical from npm audit. package-lock.json: shell-quote 1.10.0 -> 1.12.0. ROADMAP.md: the devtools upstream item records it, and the two advisories left (braces, node-forge) that have no patched release. Typecheck, build and audit:runtime clean; every route answers as before. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * docs(roadmap): vue-router 5.4, after a soak and a browser check Research from this run: 5.4.0 changes client-side scroll and hash restoration defaults, which no server gate exercises. https://github.com/vuejs/router/releases/tag/v5.4.0 Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * chore(data): refresh the fallback snapshot Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * ci: every action on its first Node 24 major GitHub forces Node 20 actions onto Node 24 and annotates each run as deprecated. Moved: actions/checkout v5, actions/setup-node v5, docker/build-push-action v7, docker/setup-buildx-action v4, docker/login-action v4, docker/metadata-action v6. None of their breaking changes touch this repo: the inputs they removed are unused, and setup-node's automatic cache needs a packageManager field package.json does not have. .github/workflows/{ci,release,links}.yml, ROADMAP.md. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Nightly routine, 2026-10-05 (2), started 21:10 CDT. This is the second run today; the morning run is #14.
Rebased onto
mainataa0220c, which includes two commits another session pushed during this run: splimes, and its ownsource-map-jsoverride (see "Concurrent work" below).Increments
1. marked 18.1.0: link destinations no longer backtrack cubically (
53e9fa7)Files:
package.json,package-lock.json,test/markdown-backtracking.test.ts(new).Problem: READMEs are fetched at request time and rendered on the server's one thread. marked 18.0.14 backtracked cubically on
[](followed by a run of unicode whitespace (markedjs/marked#4106). Through the site's ownrenderMarkdown, 4 KB of U+00A0 took 10.3 s. One README edit could have stalled every page.Fix: marked 18.1.0, published today. The same input now takes 0.1 ms.
Evidence:
check,a11yanda11y:browserclean over 95 pages, including the live posts.2. axe-core 4.14, with its new default rule judged in the browser (
fd89785)Files:
package.json,package-lock.json,scripts/check-a11y{,-browser}.mjs, README.Problem: axe 4.14 turns on
label-content-name-mismatch(WCAG 2.5.3) by default. jsdom cannot tell what text is visible, so the rule only ever came back "incomplete" on the copy buttons. Nothing was actually judging it.Fix: the rule moves to the real-browser pass.
Evidence:
aria-labelplanted inCodeBlock, the browser pass fails with 12 violations (one per project page, per width).3. The README patterns run in linear time (
31d878e)Files:
shared/markdown/readme.ts,shared/markdown/slug.ts,test/markdown-backtracking.test.ts, ROADMAP.Problem: eight of the site's own regexes over fetched README text were quadratic on input that opens something and never closes it. At 100 KB:
absolutize, a run of[slugify, a run of<stripLeadingLogolazyImages<imgrunsThe eighth pattern is the
<a href>rule the splimes commit added tonight, which had the same shape (100 KB of<a: 711 ms). I made it linear while resolving the rebase.Fix: a link label stops at the next
[, a tag stops at the next<, and a URL may end at the end of the input. The logo test now runs in a callback instead of the pattern.Evidence:
main'sreadme.ts) and the 76 posts are byte-identical before and after., and a stray<before a tag.4. A copy button announces that it copied (
9acb737)Files:
app/components/CodeLine.vue,app/components/CodeBlock.vue, ROADMAP.Problem: the button's text changes to
[copied], but its name is pinned byaria-label, so a screen reader heard nothing (WCAG 4.1.3).Fix: each component now has an empty
role="status"region, rendered on the server.Evidence: checked over CDP with clipboard access granted.
CodeBlock's region sits outside itsfigcaption, so it doesn't change the figure's name.5. No
x-powered-by, and aCross-Origin-Opener-Policy(3b5eaff)Files:
server/plugins/powered-by.ts(new),nuxt.config.ts,scripts/check-site.mjs, ROADMAP.Problem: Nuxt's renderer sets
x-powered-by: Nuxton every page after route rules apply, so a route rule cannot remove it.Fix:
beforeResponse.same-originis added to the/**route rule.npm run checknow fails on either one being wrong.Evidence:
x-powered-byis gone from pages, the 404 page, the API, the XML routes, static files and HEAD responses.6. A refused clipboard selects the text and says so (
f551840)Files:
app/composables/useCopy.ts(new), both copy components, ROADMAP.Problem: found while testing #4, when the browser pane's own clipboard refused the write. On a refused write, the button did nothing, so the visitor would paste whatever their clipboard already held.
Fix:
useCopyis now the one copy of the logic both components carried. On refusal it selects the text, shows[selected]and announces how to finish the copy.Evidence: checked over CDP with the permission denied (
NotAllowedError).$.7. A failed request's log line says who asked (
893eee5)Files:
server/plugins/request-log.ts, README, ROADMAP.Problem: the error-hook line covers every 404 and 500. It promised the same shape as the success line but stopped at
via. In 15 h of live log, 627 lines (almost all scanners) had noip,uaorref.Fix: both lines now share one
requester()function.Evidence: on a built server, with a forwarded address, user agent and referer:
ip: null.8.
audit:runtimecovers the client bundle (962f2a5, READMEf1735aa)Files:
scripts/lib/client-packages.ts(new),nuxt.config.ts,scripts/audit-runtime.mjs, README, ROADMAP.Problem: the roadmap said it outright: the client bundle had no sourcemaps, so the audit had nothing to read its packages from.
Fix:
.output/server/client-packages.json. That file is inside the image but never underpublic/, and the server answers 404 for it.Evidence:
marked@4.0.9, on a missing inventory and on an empty one.marked@17never reaches a browser.Dropped in the rebase: a source-map-js 1.2.2 lockfile bump
audit:runtimewent red tonight on GHSA-68fv-2mgg-jv7q. I fixed it with a lockfile bump, then the other session landed the same fix as an override onmain. The rebase dropped my commit as already upstream, and the audit is clean on the rebased tree.Also: fallback snapshot refreshed (
665c58f)npm run sync(8 projects): issue counts and one push date moved. Snapshot test passes.Final verification (rebased tree, combined)
Checks:
npm cion the merged lockfile.audit:runtime: 79 packages (server and client), clean.cargo audit --deny yanked --deny unsound(onion): only the two known unmaintained warnings.check: 96 pages, 153 URLs, no problems.a11y: 97 pages, clean.a11y:browser: 97 pages at 1280 and 390 px, no violations and no console errors.Real status codes:
/healthz,/,/projects,/news,/status,/sitemap.xml,/robots.txt,/releases.xml,/news.xml,/api/projectsand/api/projects/splimes: 200./projects/<slug>(8): 301. Their/aboutand/blog: 200./projects/no-such-project,/no-such-page,/api/projects/nope,/api/admin/postsand onion-marked/admin: 404./onion-frame: 503, expected locally because there is no gateway./healthz: 200.Screenshots: home, Skidbladnir, Nisaba and splimes at desktop and mobile, headless, in the run's scratch dir. I also looked at the splimes page and home in the built-in browser pane: no overflow, and no relative links left in the splimes README.
Concurrent work
Another Claude session worked in this same checkout during the run. It pushed
4278d22(splimes) andaa0220c(source-map-js override) straight tomain, and redeployed the live container at 22:37.It also killed my local verification servers twice, on two different ports, partway through
a11y:browser. I worked around it by serving a copy of the build from a distinct path. Those kills were external, not a site fault: the same pages served fine in isolation, and the full pass is clean.The live container runs as uid 100 and was unaffected.
Research
[ ]to land after a soak, not in an unattended deploy of an hour-old release.@nuxt/devtools:npm auditreports critical advisories, patched only in simple-git 4. Devtools pins^3.36.0, and Nuxt 4.6 resolves the same version. Not reachable here (devtools: { enabled: false }, nothing of it ships). Added as an upstream[ ].7.1.0-dev). vue-tsc 3.3.12 is current. Tailwind is still 4.3.3, Nitro 3 is still beta. GitHub REST: nothing affecting the site; the 2026rate_limitchange removed a field it never reads (https://github.blog/changelog/2026-05-19-removal-of-code_scanning_upload-field-from-rate_limit-api-endpoint/). No new signal from these.Done vs open
Eight increments landed, plus the snapshot refresh.
New
[ ]:Unchanged and still owner-gated or upstream:
basic-automation.github.io, and contrast together with axecolor-contrast.STOP REASON: no workable next item, at ~2 h 20 min, short of the 4 h bar. After eight increments, sweeps of the live logs, response headers, regex surfaces, the npm and cargo advisories, the client bundle and the external links turned up nothing more. The one new item, Nuxt 4.6, was deliberately held back. I didn't make up work to fill the clock.
Recommended next step
mainand killednode .output/server/index.mjsprocesses mid-run.🤖 Generated with Claude Code