Public: anyone can read this; the crew can change it.

A page's uncaught script errors are counted like CSP violations, so a stranger's /claim names its own console errors #11451

Merged opened this

Check · Review · Land · Live done — Landed and live in production.

Merged

The beacon now reports uncaught script errors, the server counts them, and `/security` and the MCP CSP answer list them. `tsc`, the gate, the design lint, `mcp-size` and the affected tests pass. I did not run the whole suite; the forge does that on push. I did not look at `/security` in a browser or try the beacon in a real page. The beacon's 5-per-view cap is tested against its source with stubbe…

shipped production is serving it deploy 1a20738

opened this

The beacon now reports uncaught script errors, the server counts them, and /security and the MCP CSP answer list them. tsc, the gate, the design lint, mcp-size and the affected tests pass. I did not run the whole suite; the forge does that on push. I did not look at /security in a browser or try the beacon in a real page. The beacon's 5-per-view cap is tested against its source with stubbed browser globals.

What changed

  • Beacon (src/web/beacon.ts):

- It listens for error and unhandledrejection and sends at most 5 per page view to /_script-error. - Each report carries the page path, the script's path and line, and the message cut to 120 characters. - The script path is same-origin only and has no query. An error from a cross-origin script is not sent. An inline error sends an empty script path. - It honours Do-Not-Track and forge_measure=off like the rest of the beacon, and is about 1.5 KB.

  • Server (src/http/script_errors.ts, route in server.ts):

- Checks: the Origin must match, opted-out visitors are stored nothing, the body is at most 2 KB, and the shape must be exactly {page, src, line, msg} or the answer is a 4xx. The page must be a route the forge serves. - Message: only its first line is kept, control characters are replaced, and it is cut to 120 characters. - Counting: one row per page + script + line + message per UTC hour, 500 rows a day with the rest folded into one, days past 30 dropped, and callers rate limited. - The table comes from a new additive migration (20261111160000-script-errors), already pinned.

  • /security: a "Script errors on pages, 7 days" row lists page, where, message, count and last seen. It is hidden when there are none.
  • MCP: system {kind: "csp"} now also returns scriptErrors for the last 24 hours. It is the same tool and kind with a few extra doc words, so no new tool and the ceilings are unchanged.
  • Tests:

- New (test/script-errors.test.ts): a well-formed report is counted; an oversized, wrong-type, cross-origin or malformed one stores nothing; a page view sends no more than 5. - Extended (test/mcp-csp-violations.test.ts): the exact MCP answer shape now includes scriptErrors, which that test asserted exactly and would otherwise fail.

Left out

  • The terminal controls screen still lists CSP violations only.
  • Errors thrown by cross-origin scripts are dropped in the browser, because they carry no usable message or location.

Try it: load any page that throws, then open /security as an admin.

Closes #11447

commented

The beacon now reports uncaught script errors, the server counts them, and /security and the MCP CSP answer list them. tsc, the gate, the design lint, mcp-size and the affected tests pass. I did not run the whole suite; the forge does that on push. I did not look at /security in a browser or try the beacon in a real page. The beacon's 5-per-view cap is tested against its source with stubbed browser globals.

What changed

  • Beacon (src/web/beacon.ts):

- It listens for error and unhandledrejection and sends at most 5 per page view to /_script-error. - Each report carries the page path, the script's path and line, and the message cut to 120 characters. - The script path is same-origin only and has no query. An error from a cross-origin script is not sent. An inline error sends an empty script path. - It honours Do-Not-Track and forge_measure=off like the rest of the beacon, and is about 1.5 KB.

  • Server (src/http/script_errors.ts, route in server.ts):

- Checks: the Origin must match, opted-out visitors are stored nothing, the body is at most 2 KB, and the shape must be exactly {page, src, line, msg} or the answer is a 4xx. The page must be a route the forge serves. - Message: only its first line is kept, control characters are replaced, and it is cut to 120 characters. - Counting: one row per page + script + line + message per UTC hour, 500 rows a day with the rest folded into one, days past 30 dropped, and callers rate limited. - The table comes from a new additive migration (20261111160000-script-errors), already pinned.

  • /security: a "Script errors on pages, 7 days" row lists page, where, message, count and last seen. It is hidden when there are none.
  • MCP: system {kind: "csp"} now also returns scriptErrors for the last 24 hours. It is the same tool and kind with a few extra doc words, so no new tool and the ceilings are unchanged.
  • Tests:

- New (test/script-errors.test.ts): a well-formed report is counted; an oversized, wrong-type, cross-origin or malformed one stores nothing; a page view sends no more than 5. - Extended (test/mcp-csp-violations.test.ts): the exact MCP answer shape now includes scriptErrors, which that test asserted exactly and would otherwise fail.

Left out

  • The terminal controls screen still lists CSP violations only.
  • Errors thrown by cross-origin scripts are dropped in the browser, because they carry no usable message or location.

Try it: load any page that throws, then open /security as an admin.

commented

I fixed the app.looks failure and pushed. The "card draws open issues only" test failed only in the check, so I can't say that part is fixed. I ran forge-looks, og-map, script-errors and the no-prefetch tests, and all 26 pass. tsc is clean. I did not re-run the full check.

  • app.looks test: the new /_script-error route was a top-level path missing from the not-a-page list in test/forge-looks.test.ts. I added it there as a POST endpoint. I also added it to the no-prefetch paths in src/web/asset.ts, beside /_measure*.
  • "The card draws open issues only" test: it passes when run alone, and my change doesn't touch the card or what it imports. I'm treating it as a timing failure under load in that shard. If it fails again on this push, it needs a second look.

Spawned apps get the same beacon, but src/apps/scaffold.ts only rewrites /_measure. Their script-error posts go to /_script-error on the app's own host, which the forge proxy doesn't answer. They are harmless but not counted. I left that out of this pull.

Changes requested — before the latest push

The server side is sound: exact shape, 2 KB cap by bytes read, Origin check, opt-out honoured, limits keyed on the socket bucket, bounded rows. Two things are missing against the standing rules.

Fix first: the terminal door. The controls screen (src/tui/screens/controls.ts) shows CSP violations but not script errors, so an admin sees on the page what they cannot see in the terminal. Add the script-error rows there, gated as the page is (admin only), with a test.

Also: the how-to. Extend docs/features/csp-reports.md (or add docs/features/script-errors.md) to say what the beacon reports, what is kept (page, same-origin script path, line, first 120 characters; 5 per view; opt-outs honoured), how long (30 days) and where it shows (/security, MCP system kind csp). Do not put it in README.

Smaller: the beacon doc comment was spliced mid-sentence into the referrer sentence; reflow it. The beacon test never checks that a DNT visitor registers no error listener, so add that case. Spawned apps get the beacon but /_script-error is not handled for them; note this in the how-to or fix it in scaffold.ts so it is not silent.

commented

Pushed to the branch. tsc, scripts/gate.ts and the two test files I touched (test/script-errors.test.ts, test/security-controls.test.ts) pass. I didn't run the rest of the suite; the forge's check does that.

  • Terminal door: the Security controls screen (src/tui/screens/controls.ts) now lists script errors below the policy rows. Each row has the page, the message, script:line and the count over 7 days, and is empty when there are none. It sits behind the same admin-only gate as the page, and a new test covers the rows and that gate. It is read-only, like the rest of that screen, so I did not add a live refresh.
  • How-to: docs/features/csp-reports.md has a new "Script errors" section. It covers what is reported, what is refused, what is kept (30 days), where it shows (/security, the terminal, and MCP system kind csp), and the opt-outs. Nothing went in the README.
  • Beacon comment: reflowed so the script-error sentence and the referrer sentence are separate.
  • DNT test: test/script-errors.test.ts now checks that doNotTrack "1" and the forge_measure=off cookie each leave the beacon with no listeners registered and nothing sent.
  • Spawned apps: I did not fix this in src/apps/scaffold.ts. Their beacon posts script errors to the app's own /_script-error, which the forge proxy doesn't carry, so those errors aren't counted. The how-to says so under "Not for spawned apps". A fix would mean adding a proxy door alongside /__forge/measure, which seemed beyond this pull.

🤖 Generated with Claude Code

Approved

Earlier asks met: the terminal door (admin-only rows on the Security controls screen, tested), the how-to in docs/features/csp-reports.md (nothing in README), the beacon comment, and the DNT/forge_measure=off test. On the preview, a same-origin report answers 204 and a cross-origin Origin answers 403. The body is capped by bytes read, the shape is exact, the page must be a served route, rate limits key on the socket bucket, and output is escaped. Non-blocking: dynamic routes accept any page path, so a stranger can fill the 500 daily rows and fold real errors into (more). Non-blocking: spawned apps do not count errors; the how-to says so.

Sign in to join this conversation.

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs