Repository navigation
feat(cli): the console static plugin takes a cloudflareWebAnalytics option that adds the beacon to the console document - #30961
Conversation
…ption that adds the beacon to the console document
createConsoleStaticPlugin(distPath, { cloudflareWebAnalytics: { token } })
writes a deferred script tag for Cloudflare Web Analytics' beacon into the
document before the nonce pass, so it carries the document's nonce, with the
token as its escaped data-cf-beacon JSON, and adds https://cloudflareinsights.com
to connect-src. script-src is unchanged: the nonce admits the tag. A token that
is not 32 hexadecimal digits refuses the boot without echoing the value. Left
out, the document and the policy are byte-identical to before, pinned against
the output read at f108e6d. The published-subpath pin carries the widened
options type.
Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016WV4cxGr3fDvcDkCLUwZAo
📓 Docs Drift Check7 anchor(s) derived from 1 changed package(s); no hand-written page names any of them, so this run has nothing to list — not a clean bill of health. This check sees only pages that NAME a derived anchor: one that documents this change in prose, or enumerates it in an authoring dialect, names none and stays invisible to it on every run. What this run could not see
Coarse fallback — 35 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin d6993f1a8df61e10f72ef1b934b8d8cd4098d523 && git checkout d6993f1a8df61e10f72ef1b934b8d8cd4098d523
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 6ddf827236902c0ccaee56b65431a4c8a9c7ac09 cbef4f627b5af28c49e2e8f39844aec3f83068f4 && git checkout -B drift-repro 6ddf827236902c0ccaee56b65431a4c8a9c7ac09 && git merge --no-ff cbef4f627b5af28c49e2e8f39844aec3f83068f4
node scripts/docs-audit/affected-docs.mjs --json 6ddf827236902c0ccaee56b65431a4c8a9c7ac09 |
|
domain:server#5 · os-elon-musk · session_016WV4cyvF7SP65frVFAYLv7 Contract reviewServed-tier: Owed by the ① Derived judgments
② Semver levelOne changeset, ③ Boundary flags
No must-fix. Implemented-by: VERDICT: PASS |
|
domain:server#5 · os-elon-musk · session_016WV4cyvF7SP65frVFAYLv7 席位核对 · ACCEPTPR #30961 · head 维护者速读(席位校正)The body's six lines stand as written, read against the diff. 席位核对
|
Requested by Elon · project thread
domain:server#5 · os-elon-musk · session_016WV4cxGr3fDvcDkCLUwZAo
Fixes #30936
Clause-②: yes (widening)
Metadata-protocol: none
维护者速读
createConsoleStaticPlugin新增一个选项cloudflareWebAnalytics: { token }。传了它,控制台页面就带上 Cloudflare Web Analytics 的统计脚本(带页面的 nonce),安全策略的connect-src多放行一个上报地址https://cloudflareinsights.com。不传,页面和安全策略逐字节不变。script-src没有放宽,统计脚本靠 nonce 加载。token 必须是 32 位十六进制,否则启动即拒绝,且报错不回显 token。回滚即 revert。packages/cli/src/utils/console.ts的readCloudflareWebAnalyticsToken与cloudflareWebAnalyticsBeaconScript(校验与转义);② 同文件readIndexHtml里写入脚本的位置(必须在打 nonce 之前);③consoleContentSecurityPolicy的connect-src一行。pnpm --filter @objectstack/cli exec vitest run --project unit src/utils/console.content-security-policy.test.ts,预期 66 个测试全绿。What changes
createConsoleStaticPlugin(distPath, options?)takes one more option,cloudflareWebAnalytics?: { token: string }, also on the published@objectstack/cli/consolesubpath. All of it is inpackages/cli/src/utils/console.ts:start()reads the option once, besidereadExtraConnectSourcesandreadMapStyleOrigin. A token that is not exactly 32 hexadecimal digits (either case), or a missing or non-string token, throwsConsole static: the cloudflareWebAnalytics option's token is …, which is not a Cloudflare Web Analytics site token. …. The message gives the value's length and kind, never the value, so a credential passed there by mistake does not land in the boot log.readIndexHtmlwrites, after the capability global and beforestampScriptNonce, a deferred script element forhttps://static.cloudflareinsights.com/beacon.min.jswhosedata-cf-beaconattribute holdsJSON.stringify({ token }), escaped with the file's existingescapeHtml(so the JSON's quotes are"). The nonce pass then stamps it like every other script in the document. The token is fixed at boot, so the comment's invariant (nothing read at request time is written before the stamp) still holds.connect-src.consoleContentSecurityPolicytakescloudflareWebAnalytics: booleanand addshttps://cloudflareinsights.com(CLOUDFLARE_WEB_ANALYTICS_ORIGIN) after the storage origin and before the operator'sOS_CONSOLE_CSP_CONNECT_SRCsources. The policy jsdoc documents it the wayTURNSTILE_ORIGINis documented.script-srcis unchanged. No host is added for the beacon (the card's open point; triage 6685354963). See Acceptance notes for what was and was not measured.Without the option nothing is written: no tag, no origin, and the document and the header are byte-identical to
main.test/published-subpath-console.pin.test.tspins the subpath's exact signatures, so itsCreateConsoleStaticPluginKeepsItsRatifiedSignaturefixture carries the widened options type.Tests
New
describe('the host\'s cloudflareWebAnalytics option')inpackages/cli/src/utils/console.content-security-policy.test.ts, on the real plugin and a realHonoHttpServer, as the rest of that file.mount()gained apluginfield for the plugin's own options.deferand the exactsrc; itsdata-cf-beacon, decoded as a browser decodes it, is{ token }; five script tags, all nonced;connect-srcis'self', the default map style host and the report origin;script-srcis exactly'self', the nonce and'wasm-unsafe-eval'; the header never namesstatic.cloudflareinsights.com.OS_CONSOLE_CSP_CONNECT_SRC: the operator's source follows the report origin.undefined): the served document and the policy, nonce written asNONCE, equal literals read frommainatf108e6d2(captured with the unmodified module before the change; the literal was then checked equal to that capture).null) and a missing token each reject the boot; the message never contains the token.", followed by its own end tag.Acceptance notes
script-srcand the beacon: NOT MEASURED from this container. The card asks whether the beacon loads anything further fromstatic.cloudflareinsights.com.curl https://static.cloudflareinsights.com/beacon.min.jsis refused by this environment's egress proxy (CONNECT tunnel failed, response 403), so the beacon's own code could not be read here. What holds without that reading: a nonce source admits the element that carries it, whatever itssrcand across redirects, so the beacon script itself loads under this policy. Whether it injects a further script element (which would carry no nonce and be refused) is the open point, and it stays with the seat's decision card, as triage 6685354963 and the claim direct. A browser load of a console served with the option, with the devtools console open for CSP reports, answers it in one step wherever that host is reachable.Clause-②: yes (widening), against the claim'sno. Re-derived from the diff, as the claim asks: the change widens the public surface of a published subpath, the options bag ofcreateConsoleStaticPlugin, whichpublished-subpath-console.pin.test.tspins by shape. That is the widening the line asks about (「本卡放宽接受集或扩大公开面吗」); nothing existing is narrowed.patch. AGENTS.md Post-Task Checklist step 3: the level follows the metadata-protocol kind alone, andMetadata-protocol: noneshipspatch, widening included.src/console.ts's header and the pin's docblock say a widening of that subpath is a "minor-bumped act"; that text predates the level ruling and AGENTS.md wins where they disagree. Noted, not changed here.content/docs/lists the console static plugin's options (rootRedirect,isDevand the plugin name have zero hits there), so the option is documented in the function's jsdoc and the changeset only.deferkeeps it from blocking the parse; itssrcis absolute, so its position before the base tag changes nothing.Verification
All at
cbef4f627b, the head this PR opens with (branched frommainf108e6d2;origin/mainhas since moved 5 commits, none touchingpackages/cli/src/utils/console.tsor either test file here, so it is not merged in).pnpm --filter @objectstack/cli exec vitest run --project unit src/utils/console.content-security-policy.test.ts:Tests 66 passed (66).pnpm --filter @objectstack/cli typecheck: exit 0 (the lock'sVERDICT command-exit 0;check:test-typecheck: OK).pnpm --filter @objectstack/cli build, thenvitest run --project unit test/published-subpath-console.pin.test.ts test/published-subpath-hook-body.pin.test.ts:Tests 29 passed (29);dist/utils/console.d.tscarriescloudflareWebAnalytics.unitproject (vitest run --project unit --maxWorkers=2, before the CLI build): 387/389 files, 5469 tests passed; the two failures were the two published-subpath pins refusing on a missingpackages/cli/dist("packages/cli is not built"), both green after the build above. Theintegrationlayer is declared to CI.node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstackderived 62 commands; all ran, pluspnpm lint,pnpm check:cli-command-idsandpnpm check:adr-symbol-anchors: 65 exit 0.check:dual-build-cjs-loadsandcheck:i18n-coveragefirst exited 3 (PREREQUISITE NOT MET, no whole-tree dist) and exited 0 afterturbo run build --concurrency=2(lock VERDICT 0).dispatch-gates.mjs --ran: exit 0, "62 derived famil(ies) accounted for — 62 run, 0 NOT-MEASURED".node scripts/check-commit-card-trailers.mjs --range origin/main..HEAD: exit 0.Ablations: each pin red once, then restored (scripts/ablation-replace.mjs; the CLI unit project imports src/, so no dist leg)
Each run:
ablation-replace.mjs --file packages/cli/src/utils/console.ts --anchor … --replacement … -- vitest run --project unit src/utils/console.content-security-policy.test.ts; every mutation landed (anchor 1 → 0, blob96f2c47898c3→ a new blob) and every restore readblob == HEAD (96f2c47898c3)andgit diff HEADempty.void beaconTag;):Tests 2 failed | 64 passed.if (input.cloudflareWebAnalytics)guard dropped):Tests 16 failed | 50 passed(the three byte-identity cases and every exactconnect-srcassertion in the file).CLOUDFLARE_WEB_ANALYTICS_TOKENreplaced by a pattern that matches every string):Tests 7 failed | 59 passed(the seven string tokens; the number andnullstill refuse on type).escapeHtmldropped):Tests 2 failed | 64 passed.published-subpath-console.pin.test.tswith the old options type restored redsTS2344: Type 'false' does not satisfy the constraint 'true'on the signature equality (Tests 1 failed | 13 passed), so the pin reads the rebuilt declaration. Restored: blob == HEAD.65 commands, exit codes at cbef4f6
Generated by Claude Code