Repository navigation
Commit 40ecdf9
feat(cli): the console static plugin takes a cloudflareWebAnalytics option that adds the beacon to the console document (#30961)
<!-- claude-projects-attribution: {"github_login":"os-elon-musk"} -->
_Requested by **Elon** · [project
thread](https://claude.ai/code/project/chan_01WedDzV26nniwxv7jC88B9D?thread=cmsg_01WedDzV26nniwxv7jC88B9DR4cMbxTcpbSHbpgshgVCDr)_
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`。不传,页面和安全策略逐字节不变。
- **为什么改**:目前没人知道托管控制台有多少人打开、从哪里来。cloud#3590 要加免费、无 cookie 的 Cloudflare
访问统计,但控制台页面和它的安全策略归框架,cloud 只能等框架给一个选项。做成选项而不是环境变量,是为了 cloud
只在控制平面传它,租户平面永远不带。
- **风险与回滚**:默认关闭,不传选项的部署零变化(测试逐字节钉住)。`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/console` subpath. All of it is in
`packages/cli/src/utils/console.ts`:
1. **Refused at boot.** `start()` reads the option once, beside
`readExtraConnectSources` and `readMapStyleOrigin`. A token that is not
exactly 32 hexadecimal digits (either case), or a missing or non-string
token, throws `Console 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.
2. **The beacon tag.** `readIndexHtml` writes, after the capability
global and before `stampScriptNonce`, a deferred script element for
`https://static.cloudflareinsights.com/beacon.min.js` whose
`data-cf-beacon` attribute holds `JSON.stringify({ token })`, escaped
with the file's existing `escapeHtml` (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.
3. **`connect-src`.** `consoleContentSecurityPolicy` takes
`cloudflareWebAnalytics: boolean` and adds
`https://cloudflareinsights.com` (`CLOUDFLARE_WEB_ANALYTICS_ORIGIN`)
after the storage origin and before the operator's
`OS_CONSOLE_CSP_CONNECT_SRC` sources. The policy jsdoc documents it the
way `TURNSTILE_ORIGIN` is documented.
4. **`script-src` is 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.ts` pins the subpath's exact
signatures, so its `CreateConsoleStaticPluginKeepsItsRatifiedSignature`
fixture carries the widened options type.
## Tests
New `describe('the host\'s cloudflareWebAnalytics option')` in
`packages/cli/src/utils/console.content-security-policy.test.ts`, on the
real plugin and a real `HonoHttpServer`, as the rest of that file.
`mount()` gained a `plugin` field for the plugin's own options.
- With the option: exactly one beacon tag, carrying the header's nonce,
`defer` and the exact `src`; its `data-cf-beacon`, decoded as a browser
decodes it, is `{ token }`; five script tags, all nonced; `connect-src`
is `'self'`, the default map style host and the report origin;
`script-src` is exactly `'self'`, the nonce and `'wasm-unsafe-eval'`;
the header never names `static.cloudflareinsights.com`.
- Beside `OS_CONSOLE_CSP_CONNECT_SRC`: the operator's source follows the
report origin.
- Byte-identity, three ways (no options, the other two options only, the
key left `undefined`): the served document and the policy, nonce written
as `NONCE`, equal literals read from `main` at `f108e6d2` (captured with
the unmodified module before the change; the literal was then checked
equal to that capture).
- Refusal: nine malformed tokens (empty, 31 and 33 hex digits, 32
non-hex characters, whitespace-padded, an attribute breakout, a JSON
breakout, a number, `null`) and a missing token each reject the boot;
the message never contains the token.
- Breakout: the exact start tag, with the JSON's quotes as `"`,
followed by its own end tag.
## Acceptance notes
- **`script-src` and the beacon: NOT MEASURED from this container.** The
card asks whether the beacon loads anything further from
`static.cloudflareinsights.com`. `curl
https://static.cloudflareinsights.com/beacon.min.js` is 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 its
`src` and 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's `no`.** Re-derived
from the diff, as the claim asks: the change widens the public surface
of a published subpath, the options bag of `createConsoleStaticPlugin`,
which `published-subpath-console.pin.test.ts` pins by shape. That is the
widening the line asks about (「本卡放宽接受集或扩大公开面吗」); nothing existing is
narrowed.
- **Changeset level `patch`.** AGENTS.md Post-Task Checklist step 3: the
level follows the metadata-protocol kind alone, and `Metadata-protocol:
none` ships `patch`, 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.
- **Docs.** No page under `content/docs/` lists the console static
plugin's options (`rootRedirect`, `isDev` and the plugin name have zero
hits there), so the option is documented in the function's jsdoc and the
changeset only.
- **Tag placement.** The tag is written right after the head start tag,
the same seam the base tag and the capability global use, so a shell
without a head tag gets none of the three. `defer` keeps it from
blocking the parse; its `src` is absolute, so its position before the
base tag changes nothing.
## Verification
All at `cbef4f627b`, the head this PR opens with (branched from `main`
`f108e6d2`; `origin/main` has since moved 5 commits, none touching
`packages/cli/src/utils/console.ts` or 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's
`VERDICT command-exit 0`; `check:test-typecheck: OK`).
- `pnpm --filter @objectstack/cli build`, then `vitest 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.ts` carries `cloudflareWebAnalytics`.
- Whole `unit` project (`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 missing
`packages/cli/dist` ("packages/cli is not built"), both green after the
build above. The `integration` layer is declared to CI.
- `node scripts/pm/dispatch-gates.mjs --commands --repo
objectstack-ai/objectstack` derived 62 commands; all ran, plus `pnpm
lint`, `pnpm check:cli-command-ids` and `pnpm check:adr-symbol-anchors`:
65 exit 0. `check:dual-build-cjs-loads` and `check:i18n-coverage` first
exited 3 (PREREQUISITE NOT MET, no whole-tree dist) and exited 0 after
`turbo 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.
<details><summary>Ablations: each pin red once, then restored
(scripts/ablation-replace.mjs; the CLI unit project imports src/, so no
dist leg)</summary>
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, blob `96f2c47898c3` → a new blob) and every
restore read `blob == HEAD (96f2c47)` and `git diff HEAD` empty.
- A1, the option writes no tag (the head insertion replaced by `void
beaconTag;`): `Tests 2 failed | 64 passed`.
- A2, the report origin added without the option (the `if
(input.cloudflareWebAnalytics)` guard dropped): `Tests 16 failed | 50
passed` (the three byte-identity cases and every exact `connect-src`
assertion in the file).
- A3, any token accepted (`CLOUDFLARE_WEB_ANALYTICS_TOKEN` replaced by a
pattern that matches every string): `Tests 7 failed | 59 passed` (the
seven string tokens; the number and `null` still refuse on type).
- A4, the attribute unescaped (`escapeHtml` dropped): `Tests 2 failed |
64 passed`.
- The pin's own control: `published-subpath-console.pin.test.ts` with
the old options type restored reds `TS2344: 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.
</details>
<details><summary>65 commands, exit codes at cbef4f6</summary>
```
node scripts/check-adr-0087-registration.mjs --base origin/main :: exit 0
node scripts/check-adr-0087-registration.mjs --self-test :: exit 0
node scripts/check-changeset-no-major.mjs --base origin/main :: exit 0
node scripts/check-closing-keyword-parity.mjs :: exit 0
node scripts/check-closing-keyword-parity.mjs --self-test :: exit 0
node scripts/check-comment-mask-adoption.mjs :: exit 0
node scripts/check-comment-mask-adoption.mjs --self-test :: exit 0
node scripts/check-comment-mask-corpus.mjs :: exit 0
node scripts/check-dts-emitted.mjs --self-test :: exit 0
node scripts/check-empty-changeset.mjs --base origin/main :: exit 0
node scripts/check-empty-changeset.mjs --self-test :: exit 0
node scripts/check-issue-citations.mjs :: exit 0
node scripts/check-plugin-teardown-shape.mjs :: exit 0
node scripts/check-plugin-teardown-shape.mjs --self-test :: exit 0
node scripts/ci/assert-turbo-remote-cache.mjs --self-test :: exit 0
node scripts/ci/plan-test-core-shards.mjs --self-test :: exit 0
node scripts/ci/prune-turbo-cache.mjs --self-test :: exit 0
node scripts/ci/snapshot-next.mjs --self-test :: exit 0
node scripts/docs-audit/check-affected-docs.mjs :: exit 0
node scripts/docs-audit/check-drift-comment.mjs :: exit 0
node scripts/pm/release-rehearsal-clone.mjs --self-test :: exit 0
node scripts/release-pending-publish.mjs --self-test :: exit 0
pnpm --filter @objectstack/spec run check:duration-unit-keys :: exit 0
pnpm check:changeset-gate-self-tests :: exit 0
pnpm check:ci-filter-parity :: exit 0
pnpm check:cli-test-child-env :: exit 0
pnpm check:closed-ceremony-text :: exit 0
pnpm check:cross-package-test-inputs :: exit 0
pnpm check:dispatcher-error-vocabulary :: exit 0
pnpm check:doc-authoring :: exit 0
pnpm check:driver-memory-census :: exit 0
pnpm check:dts-closure :: exit 0
pnpm check:engine-double-contract :: exit 0
pnpm check:gitlink-declared :: exit 0
pnpm check:i18n :: exit 0
pnpm check:i18n-walk-parity :: exit 0
pnpm check:issue-citations :: exit 0
pnpm check:keyed-text-bounds :: exit 0
pnpm check:lean-entry-closure :: exit 0
pnpm check:logger-receiver-detach :: exit 0
pnpm check:nul-bytes :: exit 0
pnpm check:objectql-double-limit :: exit 0
pnpm check:org-identifier :: exit 0
pnpm check:page-declaration-shape :: exit 0
pnpm check:platform-object-tenancy-census :: exit 0
pnpm check:published-files :: exit 0
pnpm check:query-options-erasure :: exit 0
pnpm check:refd-timer-probe :: exit 0
pnpm check:registry-log-declared :: exit 0
pnpm check:rest-log-spy-declared :: exit 0
pnpm check:slot-lookup :: exit 0
pnpm check:sourcemap-no-sources-content :: exit 0
pnpm check:system-context-census :: exit 0
pnpm check:test-source-alias :: exit 0
pnpm check:tier-file-adoption :: exit 0
pnpm check:type-check-coverage :: exit 0
pnpm check:type-check-debt :: exit 0
pnpm check:undeclared-dep-imports :: exit 0
pnpm check:watch-hint-literal :: exit 0
pnpm check:where-matcher :: exit 0
pnpm lint :: exit 0
pnpm check:cli-command-ids :: exit 0
pnpm check:adr-symbol-anchors :: exit 0
pnpm check:dual-build-cjs-loads :: exit 0
pnpm check:i18n-coverage :: exit 0
```
</details>
---
_Generated by [Claude
Code](https://claude.ai/code/session_016WV4cxGr3fDvcDkCLUwZAo)_
Co-authored-by: Claude <noreply@anthropic.com>1 parent ea41c49 commit 40ecdf9
4 files changed
Lines changed: 236 additions & 3 deletions
File tree
- .changeset
- packages/cli
- src/utils
- test
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
86 | 86 | | |
87 | 87 | | |
88 | 88 | | |
| 89 | + | |
| 90 | + | |
89 | 91 | | |
90 | 92 | | |
91 | 93 | | |
| |||
107 | 109 | | |
108 | 110 | | |
109 | 111 | | |
110 | | - | |
| 112 | + | |
111 | 113 | | |
112 | 114 | | |
113 | 115 | | |
| |||
561 | 563 | | |
562 | 564 | | |
563 | 565 | | |
| 566 | + | |
| 567 | + | |
| 568 | + | |
| 569 | + | |
| 570 | + | |
| 571 | + | |
| 572 | + | |
| 573 | + | |
| 574 | + | |
| 575 | + | |
| 576 | + | |
| 577 | + | |
| 578 | + | |
| 579 | + | |
| 580 | + | |
| 581 | + | |
| 582 | + | |
| 583 | + | |
| 584 | + | |
| 585 | + | |
| 586 | + | |
| 587 | + | |
| 588 | + | |
| 589 | + | |
| 590 | + | |
| 591 | + | |
| 592 | + | |
| 593 | + | |
| 594 | + | |
| 595 | + | |
| 596 | + | |
| 597 | + | |
| 598 | + | |
| 599 | + | |
| 600 | + | |
| 601 | + | |
| 602 | + | |
| 603 | + | |
| 604 | + | |
| 605 | + | |
| 606 | + | |
| 607 | + | |
| 608 | + | |
| 609 | + | |
| 610 | + | |
| 611 | + | |
| 612 | + | |
| 613 | + | |
| 614 | + | |
| 615 | + | |
| 616 | + | |
| 617 | + | |
| 618 | + | |
| 619 | + | |
| 620 | + | |
| 621 | + | |
| 622 | + | |
| 623 | + | |
| 624 | + | |
| 625 | + | |
| 626 | + | |
| 627 | + | |
| 628 | + | |
| 629 | + | |
| 630 | + | |
| 631 | + | |
| 632 | + | |
| 633 | + | |
| 634 | + | |
| 635 | + | |
| 636 | + | |
| 637 | + | |
| 638 | + | |
| 639 | + | |
| 640 | + | |
| 641 | + | |
| 642 | + | |
| 643 | + | |
| 644 | + | |
| 645 | + | |
| 646 | + | |
| 647 | + | |
| 648 | + | |
| 649 | + | |
| 650 | + | |
| 651 | + | |
| 652 | + | |
| 653 | + | |
| 654 | + | |
| 655 | + | |
| 656 | + | |
| 657 | + | |
| 658 | + | |
| 659 | + | |
| 660 | + | |
| 661 | + | |
| 662 | + | |
| 663 | + | |
| 664 | + | |
| 665 | + | |
| 666 | + | |
| 667 | + | |
| 668 | + | |
| 669 | + | |
| 670 | + | |
| 671 | + | |
| 672 | + | |
| 673 | + | |
| 674 | + | |
| 675 | + | |
| 676 | + | |
| 677 | + | |
| 678 | + | |
| 679 | + | |
| 680 | + | |
| 681 | + | |
| 682 | + | |
| 683 | + | |
| 684 | + | |
| 685 | + | |
| 686 | + | |
| 687 | + | |
| 688 | + | |
| 689 | + | |
| 690 | + | |
| 691 | + | |
| 692 | + | |
| 693 | + | |
| 694 | + | |
| 695 | + | |
| 696 | + | |
| 697 | + | |
| 698 | + | |
| 699 | + | |
| 700 | + | |
| 701 | + | |
| 702 | + | |
| 703 | + | |
| 704 | + | |
| 705 | + | |
| 706 | + | |
| 707 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
849 | 849 | | |
850 | 850 | | |
851 | 851 | | |
| 852 | + | |
| 853 | + | |
| 854 | + | |
| 855 | + | |
| 856 | + | |
852 | 857 | | |
853 | 858 | | |
854 | 859 | | |
855 | 860 | | |
856 | 861 | | |
| 862 | + | |
857 | 863 | | |
858 | 864 | | |
859 | 865 | | |
| |||
867 | 873 | | |
868 | 874 | | |
869 | 875 | | |
| 876 | + | |
870 | 877 | | |
871 | 878 | | |
872 | 879 | | |
| |||
886 | 893 | | |
887 | 894 | | |
888 | 895 | | |
| 896 | + | |
| 897 | + | |
| 898 | + | |
| 899 | + | |
| 900 | + | |
| 901 | + | |
| 902 | + | |
| 903 | + | |
| 904 | + | |
| 905 | + | |
| 906 | + | |
| 907 | + | |
| 908 | + | |
| 909 | + | |
| 910 | + | |
| 911 | + | |
| 912 | + | |
| 913 | + | |
| 914 | + | |
| 915 | + | |
| 916 | + | |
| 917 | + | |
| 918 | + | |
| 919 | + | |
| 920 | + | |
| 921 | + | |
| 922 | + | |
| 923 | + | |
| 924 | + | |
| 925 | + | |
| 926 | + | |
| 927 | + | |
| 928 | + | |
| 929 | + | |
| 930 | + | |
| 931 | + | |
| 932 | + | |
| 933 | + | |
| 934 | + | |
| 935 | + | |
| 936 | + | |
| 937 | + | |
| 938 | + | |
| 939 | + | |
| 940 | + | |
| 941 | + | |
| 942 | + | |
| 943 | + | |
| 944 | + | |
| 945 | + | |
| 946 | + | |
889 | 947 | | |
890 | 948 | | |
891 | 949 | | |
| |||
995 | 1053 | | |
996 | 1054 | | |
997 | 1055 | | |
| 1056 | + | |
| 1057 | + | |
| 1058 | + | |
| 1059 | + | |
| 1060 | + | |
| 1061 | + | |
| 1062 | + | |
| 1063 | + | |
998 | 1064 | | |
999 | | - | |
| 1065 | + | |
| 1066 | + | |
| 1067 | + | |
| 1068 | + | |
1000 | 1069 | | |
1001 | 1070 | | |
1002 | 1071 | | |
| |||
1017 | 1086 | | |
1018 | 1087 | | |
1019 | 1088 | | |
| 1089 | + | |
| 1090 | + | |
1020 | 1091 | | |
1021 | 1092 | | |
1022 | 1093 | | |
| |||
1097 | 1168 | | |
1098 | 1169 | | |
1099 | 1170 | | |
| 1171 | + | |
| 1172 | + | |
| 1173 | + | |
| 1174 | + | |
1100 | 1175 | | |
1101 | 1176 | | |
1102 | 1177 | | |
| |||
1117 | 1192 | | |
1118 | 1193 | | |
1119 | 1194 | | |
| 1195 | + | |
1120 | 1196 | | |
1121 | 1197 | | |
1122 | 1198 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
327 | 327 | | |
328 | 328 | | |
329 | 329 | | |
330 | | - | |
| 330 | + | |
331 | 331 | | |
332 | 332 | | |
333 | 333 | | |
| |||
0 commit comments