cursedbelt-server 4.11.0 → 4.11.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/server/analytics/webAnalytics.d.ts +32 -3
- package/dist/server/analytics/webAnalytics.js +40 -6
- package/dist/server/master-lock/guard.d.ts +8 -3
- package/dist/server/master-lock/guard.js +8 -3
- package/package.json +1 -1
- package/src/server/analytics/injectedScripts.spec.ts +11 -7
- package/src/server/analytics/webAnalytics.spec.ts +26 -7
- package/src/server/analytics/webAnalytics.ts +43 -6
- package/src/server/incidents/incidents.spec.ts +5 -1
- package/src/server/master-lock/guard.ts +8 -3
|
@@ -48,7 +48,29 @@ export declare const WEB_ANALYTICS_SITE_TAG = "d1bea8562544421fbd2db5f11b74d1cc"
|
|
|
48
48
|
/** The `cursedalchemy.com` site's `data-cf-beacon` token. This is what the tag carries. */
|
|
49
49
|
export declare const WEB_ANALYTICS_SITE_TOKEN = "bd88d63678b64bc89f9702f48846bc88";
|
|
50
50
|
/**
|
|
51
|
-
* The
|
|
51
|
+
* 🔴 The `version` the beacon's config must carry, or NOTHING is recorded.
|
|
52
|
+
*
|
|
53
|
+
* Measured 2026-09-22, after the RUM API showed every manual-snippet shell recording ZERO
|
|
54
|
+
* pageviews from 2026-09-19 on — collections, music, roms, flix, station and desk, which had
|
|
55
|
+
* been 130–290 a day between them — while the hosts still on the edge's injection kept
|
|
56
|
+
* recording. The beacon copies this value into its payload as `versions.fl`, and
|
|
57
|
+
* `cloudflareinsights.com/cdn-cgi/rum` answers a payload without `fl` with a **404 that carries
|
|
58
|
+
* no CORS header**, which a browser prints as a CORS refusal and nobody reads as "recorded
|
|
59
|
+
* nothing". Same token, same src, driven in a real browser from a `*.cursedalchemy.com` origin:
|
|
60
|
+
* `{"token": …}` → 404 every time; `{"version": "2024.11.0", "token": …}` → 204 every time,
|
|
61
|
+
* whether the script path was pinned or not. Any value was accepted; this one is what the
|
|
62
|
+
* edge's own injection carries, so it is the value Cloudflare is known to send.
|
|
63
|
+
*
|
|
64
|
+
* Cloudflare's own `site_info` snippet OMITS it, so "exactly the vendor's text" was the
|
|
65
|
+
* defect, not the safety it looked like. `tools/check-web-analytics.ts` reds a hand copy
|
|
66
|
+
* without it.
|
|
67
|
+
*/
|
|
68
|
+
export declare const WEB_ANALYTICS_BEACON_VERSION = "2024.11.0";
|
|
69
|
+
/** The `data-cf-beacon` value — one spelling for the tag, the bootstrap and every hand copy. */
|
|
70
|
+
export declare function webAnalyticsBeaconConfig(token?: string): string;
|
|
71
|
+
/**
|
|
72
|
+
* The snippet as Cloudflare's own `site_info` endpoint spells it — plus the `version` key it
|
|
73
|
+
* leaves out, without which nothing is recorded (see {@link WEB_ANALYTICS_BEACON_VERSION}).
|
|
52
74
|
*
|
|
53
75
|
* 🔴 The single-quoted attributes and the space after `"token":` are Cloudflare's, not a
|
|
54
76
|
* style choice, and they are kept because this string is COMPARED against what an app's
|
|
@@ -104,7 +126,14 @@ export declare function webAnalyticsBootstrap(token?: string): string;
|
|
|
104
126
|
*
|
|
105
127
|
* Deliberately NOT a substring test against {@link WEB_ANALYTICS_TAG}: a shell is authored by
|
|
106
128
|
* hand and a formatter is allowed to re-wrap the tag across lines or re-order its attributes.
|
|
107
|
-
* What must be true is that the beacon's source is there
|
|
108
|
-
* token — the
|
|
129
|
+
* What must be true is that the beacon's source is there, that it is carrying the right
|
|
130
|
+
* token, and that its config carries a `version` — the three things that decide whether a
|
|
131
|
+
* pageview is recorded at all. The third was missed until 2026-09-22 and cost four days of
|
|
132
|
+
* every manual shell's data; see {@link WEB_ANALYTICS_BEACON_VERSION}.
|
|
109
133
|
*/
|
|
110
134
|
export declare function htmlCarriesWebAnalytics(html: string, token?: string): boolean;
|
|
135
|
+
/**
|
|
136
|
+
* Every `data-cf-beacon` config in `html` names a `version` — and there is at least one.
|
|
137
|
+
* Read up to the config's closing brace, so key order and a formatter's spacing do not matter.
|
|
138
|
+
*/
|
|
139
|
+
export declare function beaconConfigsCarryVersion(html: string): boolean;
|
|
@@ -48,7 +48,31 @@ export const WEB_ANALYTICS_SITE_TAG = "d1bea8562544421fbd2db5f11b74d1cc";
|
|
|
48
48
|
/** The `cursedalchemy.com` site's `data-cf-beacon` token. This is what the tag carries. */
|
|
49
49
|
export const WEB_ANALYTICS_SITE_TOKEN = "bd88d63678b64bc89f9702f48846bc88";
|
|
50
50
|
/**
|
|
51
|
-
* The
|
|
51
|
+
* 🔴 The `version` the beacon's config must carry, or NOTHING is recorded.
|
|
52
|
+
*
|
|
53
|
+
* Measured 2026-09-22, after the RUM API showed every manual-snippet shell recording ZERO
|
|
54
|
+
* pageviews from 2026-09-19 on — collections, music, roms, flix, station and desk, which had
|
|
55
|
+
* been 130–290 a day between them — while the hosts still on the edge's injection kept
|
|
56
|
+
* recording. The beacon copies this value into its payload as `versions.fl`, and
|
|
57
|
+
* `cloudflareinsights.com/cdn-cgi/rum` answers a payload without `fl` with a **404 that carries
|
|
58
|
+
* no CORS header**, which a browser prints as a CORS refusal and nobody reads as "recorded
|
|
59
|
+
* nothing". Same token, same src, driven in a real browser from a `*.cursedalchemy.com` origin:
|
|
60
|
+
* `{"token": …}` → 404 every time; `{"version": "2024.11.0", "token": …}` → 204 every time,
|
|
61
|
+
* whether the script path was pinned or not. Any value was accepted; this one is what the
|
|
62
|
+
* edge's own injection carries, so it is the value Cloudflare is known to send.
|
|
63
|
+
*
|
|
64
|
+
* Cloudflare's own `site_info` snippet OMITS it, so "exactly the vendor's text" was the
|
|
65
|
+
* defect, not the safety it looked like. `tools/check-web-analytics.ts` reds a hand copy
|
|
66
|
+
* without it.
|
|
67
|
+
*/
|
|
68
|
+
export const WEB_ANALYTICS_BEACON_VERSION = "2024.11.0";
|
|
69
|
+
/** The `data-cf-beacon` value — one spelling for the tag, the bootstrap and every hand copy. */
|
|
70
|
+
export function webAnalyticsBeaconConfig(token = WEB_ANALYTICS_SITE_TOKEN) {
|
|
71
|
+
return `{"version": "${WEB_ANALYTICS_BEACON_VERSION}", "token": "${token}"}`;
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* The snippet as Cloudflare's own `site_info` endpoint spells it — plus the `version` key it
|
|
75
|
+
* leaves out, without which nothing is recorded (see {@link WEB_ANALYTICS_BEACON_VERSION}).
|
|
52
76
|
*
|
|
53
77
|
* 🔴 The single-quoted attributes and the space after `"token":` are Cloudflare's, not a
|
|
54
78
|
* style choice, and they are kept because this string is COMPARED against what an app's
|
|
@@ -62,7 +86,7 @@ export const WEB_ANALYTICS_SITE_TOKEN = "bd88d63678b64bc89f9702f48846bc88";
|
|
|
62
86
|
export function webAnalyticsTag(token = WEB_ANALYTICS_SITE_TOKEN) {
|
|
63
87
|
return ("<!-- Cloudflare Web Analytics -->" +
|
|
64
88
|
`<script type='module' src='${WEB_ANALYTICS_BEACON_SRC}' ` +
|
|
65
|
-
`data-cf-beacon='
|
|
89
|
+
`data-cf-beacon='${webAnalyticsBeaconConfig(token)}'></script>` +
|
|
66
90
|
"<!-- End Cloudflare Web Analytics -->");
|
|
67
91
|
}
|
|
68
92
|
/** The fleet's tag, for the one site this generation serves. */
|
|
@@ -111,7 +135,7 @@ export function webAnalyticsBootstrap(token = WEB_ANALYTICS_SITE_TOKEN) {
|
|
|
111
135
|
`\t\tvar beacon = document.createElement("script");`,
|
|
112
136
|
`\t\tbeacon.type = "module";`,
|
|
113
137
|
`\t\tbeacon.src = "${WEB_ANALYTICS_BEACON_SRC}";`,
|
|
114
|
-
`\t\tbeacon.setAttribute("data-cf-beacon", '
|
|
138
|
+
`\t\tbeacon.setAttribute("data-cf-beacon", '${webAnalyticsBeaconConfig(token)}');`,
|
|
115
139
|
`\t\tdocument.head.appendChild(beacon);`,
|
|
116
140
|
"\t}",
|
|
117
141
|
"</script>",
|
|
@@ -122,9 +146,19 @@ export function webAnalyticsBootstrap(token = WEB_ANALYTICS_SITE_TOKEN) {
|
|
|
122
146
|
*
|
|
123
147
|
* Deliberately NOT a substring test against {@link WEB_ANALYTICS_TAG}: a shell is authored by
|
|
124
148
|
* hand and a formatter is allowed to re-wrap the tag across lines or re-order its attributes.
|
|
125
|
-
* What must be true is that the beacon's source is there
|
|
126
|
-
* token — the
|
|
149
|
+
* What must be true is that the beacon's source is there, that it is carrying the right
|
|
150
|
+
* token, and that its config carries a `version` — the three things that decide whether a
|
|
151
|
+
* pageview is recorded at all. The third was missed until 2026-09-22 and cost four days of
|
|
152
|
+
* every manual shell's data; see {@link WEB_ANALYTICS_BEACON_VERSION}.
|
|
127
153
|
*/
|
|
128
154
|
export function htmlCarriesWebAnalytics(html, token = WEB_ANALYTICS_SITE_TOKEN) {
|
|
129
|
-
return html.includes(WEB_ANALYTICS_BEACON_SRC) && html.includes(token);
|
|
155
|
+
return html.includes(WEB_ANALYTICS_BEACON_SRC) && html.includes(token) && beaconConfigsCarryVersion(html);
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* Every `data-cf-beacon` config in `html` names a `version` — and there is at least one.
|
|
159
|
+
* Read up to the config's closing brace, so key order and a formatter's spacing do not matter.
|
|
160
|
+
*/
|
|
161
|
+
export function beaconConfigsCarryVersion(html) {
|
|
162
|
+
const configs = [...html.matchAll(/data-cf-beacon[^{]{0,24}(\{[^}]*\})/g)].map((m) => m[1] ?? "");
|
|
163
|
+
return configs.length > 0 && configs.every((c) => /"version"\s*:\s*"[^"]+"/.test(c));
|
|
130
164
|
}
|
|
@@ -51,9 +51,14 @@ export interface MasterLockGuardOptions extends LockPageOptions {
|
|
|
51
51
|
* loosening it: nothing loads from anywhere but this origin, the form posts nowhere (the
|
|
52
52
|
* script does the POST), and the page cannot be framed.
|
|
53
53
|
*
|
|
54
|
-
* 🔴 **EXPORTED so that nothing has to copy it
|
|
55
|
-
*
|
|
56
|
-
*
|
|
54
|
+
* 🔴 **EXPORTED so that nothing has to copy it** — and exporting it is not the same as nothing
|
|
55
|
+
* copying it. This sentence used to read *"`apps/collections` had a hand-typed duplicate … until
|
|
56
|
+
* 2026-09-18"*, and it was false the day it was written: the export landed, and BOTH re-typed
|
|
57
|
+
* copies stayed — one in `apps/collections/scripts/injectedScripts.test.ts` and one three
|
|
58
|
+
* directories from here in `../analytics/injectedScripts.spec.ts`. They were collapsed onto this
|
|
59
|
+
* constant on 2026-09-21. A policy that is typed twice can be loosened in one place and stay
|
|
60
|
+
* green in the other, which is a wall that no longer refuses what its own test says it does.
|
|
61
|
+
* It is also what
|
|
57
62
|
* `beaconNeverReachesTheWall.spec.ts` measures the beacon against: the wall refuses
|
|
58
63
|
* `static.cloudflareinsights.com`, so the analytics tag and this response are mutually
|
|
59
64
|
* exclusive BY CONSTRUCTION rather than by anyone remembering. The owner's reason, 2026-09-18:
|
|
@@ -20,9 +20,14 @@ const noStore = { "cache-control": "no-store, no-cache, must-revalidate", pragma
|
|
|
20
20
|
* loosening it: nothing loads from anywhere but this origin, the form posts nowhere (the
|
|
21
21
|
* script does the POST), and the page cannot be framed.
|
|
22
22
|
*
|
|
23
|
-
* 🔴 **EXPORTED so that nothing has to copy it
|
|
24
|
-
*
|
|
25
|
-
*
|
|
23
|
+
* 🔴 **EXPORTED so that nothing has to copy it** — and exporting it is not the same as nothing
|
|
24
|
+
* copying it. This sentence used to read *"`apps/collections` had a hand-typed duplicate … until
|
|
25
|
+
* 2026-09-18"*, and it was false the day it was written: the export landed, and BOTH re-typed
|
|
26
|
+
* copies stayed — one in `apps/collections/scripts/injectedScripts.test.ts` and one three
|
|
27
|
+
* directories from here in `../analytics/injectedScripts.spec.ts`. They were collapsed onto this
|
|
28
|
+
* constant on 2026-09-21. A policy that is typed twice can be loosened in one place and stay
|
|
29
|
+
* green in the other, which is a wall that no longer refuses what its own test says it does.
|
|
30
|
+
* It is also what
|
|
26
31
|
* `beaconNeverReachesTheWall.spec.ts` measures the beacon against: the wall refuses
|
|
27
32
|
* `static.cloudflareinsights.com`, so the analytics tag and this response are mutually
|
|
28
33
|
* exclusive BY CONSTRUCTION rather than by anyone remembering. The owner's reason, 2026-09-18:
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "cursedbelt-server",
|
|
3
|
-
"version": "4.11.
|
|
3
|
+
"version": "4.11.1",
|
|
4
4
|
"license": "ISC",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"description": "The app-facing Bun/Hono server tier of the cursedbelt split \u2014 storage, sharing, activity, guard, sync. React-free; cursedbelt-core below it.",
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { describe, expect, test } from "bun:test";
|
|
2
2
|
import { foreignScriptSources, scriptsRefusedByCsp } from "./injectedScripts";
|
|
3
|
+
import { MASTER_LOCK_CSP } from "../master-lock/guard";
|
|
3
4
|
import { WEB_ANALYTICS_BEACON_ORIGIN, WEB_ANALYTICS_TAG } from "./webAnalytics";
|
|
4
5
|
|
|
5
6
|
const PAGE = "https://collections.cursedalchemy.com/";
|
|
@@ -61,14 +62,17 @@ describe("scriptsRefusedByCsp", () => {
|
|
|
61
62
|
/**
|
|
62
63
|
* 🔴 The failure path this whole file exists for — the owner's console line, turned into a
|
|
63
64
|
* refusal. `script-src 'self'` with something injecting is exactly what he pasted.
|
|
65
|
+
*
|
|
66
|
+
* 🔴 **`MASTER_LOCK_CSP` itself, never a hand-typed copy of it.** `guard.ts:91` exports the
|
|
67
|
+
* policy for exactly this reason and says so — *"EXPORTED so that nothing has to copy it"* —
|
|
68
|
+
* and until 2026-09-21 the two readers that mattered both re-typed the string anyway: this
|
|
69
|
+
* spec, and a byte-identical fork of this whole file in `apps/collections`. A policy that is
|
|
70
|
+
* typed twice can be loosened in one place and stay green in the other, which would leave
|
|
71
|
+
* this test passing about a CSP the wall no longer serves. Importing it means relaxing the
|
|
72
|
+
* real policy REDS here, which is the only version of this test worth having.
|
|
64
73
|
*/
|
|
65
|
-
test("
|
|
66
|
-
const refused = scriptsRefusedByCsp(
|
|
67
|
-
"default-src 'none'; style-src 'self'; script-src 'self'; connect-src 'self'; " +
|
|
68
|
-
"img-src data:; form-action 'none'; base-uri 'none'; frame-ancestors 'none'",
|
|
69
|
-
beacon,
|
|
70
|
-
PAGE,
|
|
71
|
-
);
|
|
74
|
+
test("the wall's own CSP refuses the injected beacon and names its origin", () => {
|
|
75
|
+
const refused = scriptsRefusedByCsp(MASTER_LOCK_CSP, beacon, PAGE);
|
|
72
76
|
expect(refused.map((s) => s.origin)).toEqual([WEB_ANALYTICS_BEACON_ORIGIN]);
|
|
73
77
|
});
|
|
74
78
|
|
|
@@ -2,27 +2,46 @@ import { describe, expect, test } from "bun:test";
|
|
|
2
2
|
import {
|
|
3
3
|
WEB_ANALYTICS_BEACON_ORIGIN,
|
|
4
4
|
WEB_ANALYTICS_BEACON_SRC,
|
|
5
|
+
WEB_ANALYTICS_BEACON_VERSION,
|
|
5
6
|
WEB_ANALYTICS_SITE_TAG,
|
|
6
7
|
WEB_ANALYTICS_SITE_TOKEN,
|
|
7
8
|
WEB_ANALYTICS_TAG,
|
|
9
|
+
beaconConfigsCarryVersion,
|
|
8
10
|
htmlCarriesWebAnalytics,
|
|
11
|
+
webAnalyticsBeaconConfig,
|
|
9
12
|
webAnalyticsBootstrap,
|
|
10
13
|
webAnalyticsTag,
|
|
11
14
|
} from "./webAnalytics";
|
|
12
15
|
|
|
13
16
|
/**
|
|
14
17
|
* 🔴 The snippet, as Cloudflare's own `GET /accounts/<acct>/rum/site_info/list` returned it on
|
|
15
|
-
* 2026-09-18 for `cursedalchemy.com`, unescaped
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
+
* 2026-09-18 for `cursedalchemy.com`, unescaped — and it records NOTHING. Measured 2026-09-22:
|
|
19
|
+
* the RUM endpoint 404s every payload this config produces, and four days of every manual
|
|
20
|
+
* shell's pageviews went with it while this test pinned the vendor's text as the safe choice.
|
|
18
21
|
*/
|
|
19
22
|
const FROM_CLOUDFLARE =
|
|
20
23
|
`<!-- Cloudflare Web Analytics --><script type='module' src='https://static.cloudflareinsights.com/beacon.min.js' ` +
|
|
21
24
|
`data-cf-beacon='{"token": "bd88d63678b64bc89f9702f48846bc88"}'></script><!-- End Cloudflare Web Analytics -->`;
|
|
22
25
|
|
|
26
|
+
/** The vendor's snippet with the one key it leaves out, which is what the fleet ships. */
|
|
27
|
+
const WHAT_RECORDS = FROM_CLOUDFLARE.replace(`'{"token": `, `'{"version": "2024.11.0", "token": `);
|
|
28
|
+
|
|
23
29
|
describe("the fleet's beacon", () => {
|
|
24
|
-
test("is
|
|
25
|
-
expect(
|
|
30
|
+
test("is Cloudflare's snippet plus the `version` the endpoint requires", () => {
|
|
31
|
+
expect(WHAT_RECORDS).not.toBe(FROM_CLOUDFLARE);
|
|
32
|
+
expect(WEB_ANALYTICS_TAG).toBe(WHAT_RECORDS);
|
|
33
|
+
expect(webAnalyticsBeaconConfig()).toBe(`{"version": "${WEB_ANALYTICS_BEACON_VERSION}", "token": "${WEB_ANALYTICS_SITE_TOKEN}"}`);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
test("🔴 a config without `version` is refused — the vendor's own text among them", () => {
|
|
37
|
+
expect(htmlCarriesWebAnalytics(FROM_CLOUDFLARE)).toBe(false);
|
|
38
|
+
expect(beaconConfigsCarryVersion(FROM_CLOUDFLARE)).toBe(false);
|
|
39
|
+
// Two beacons on one page, one of them without it, is still a page that records nothing.
|
|
40
|
+
expect(beaconConfigsCarryVersion(WHAT_RECORDS + FROM_CLOUDFLARE)).toBe(false);
|
|
41
|
+
// The edge's own spelling — no spaces, other keys after — is accepted.
|
|
42
|
+
expect(beaconConfigsCarryVersion(`data-cf-beacon='{"version":"2024.11.0","token":"x","r":1,"spa":2}'`)).toBe(true);
|
|
43
|
+
// And a page with no config at all is not vacuously fine.
|
|
44
|
+
expect(beaconConfigsCarryVersion("<html></html>")).toBe(false);
|
|
26
45
|
});
|
|
27
46
|
|
|
28
47
|
test("carries the site TOKEN, never the site TAG", () => {
|
|
@@ -79,7 +98,7 @@ describe("the hostname guard", () => {
|
|
|
79
98
|
for (const host of ["cursedalchemy.com", "collections.cursedalchemy.com", "vault.cursedalchemy.com"]) {
|
|
80
99
|
const { appended, attrs } = runBootstrap(host);
|
|
81
100
|
expect(appended, `${host} should load the beacon`).toEqual([WEB_ANALYTICS_BEACON_SRC]);
|
|
82
|
-
expect(attrs["data-cf-beacon"]).toBe(
|
|
101
|
+
expect(attrs["data-cf-beacon"]).toBe(webAnalyticsBeaconConfig());
|
|
83
102
|
}
|
|
84
103
|
});
|
|
85
104
|
|
|
@@ -113,7 +132,7 @@ describe("htmlCarriesWebAnalytics", () => {
|
|
|
113
132
|
// A formatter is allowed to break the attributes across lines; the check must survive it.
|
|
114
133
|
const rewrapped =
|
|
115
134
|
`<script\n\ttype="module"\n\tsrc="${WEB_ANALYTICS_BEACON_SRC}"\n` +
|
|
116
|
-
`\tdata-cf-beacon='
|
|
135
|
+
`\tdata-cf-beacon='${webAnalyticsBeaconConfig()}'\n></script>`;
|
|
117
136
|
expect(htmlCarriesWebAnalytics(rewrapped)).toBe(true);
|
|
118
137
|
});
|
|
119
138
|
|
|
@@ -53,7 +53,33 @@ export const WEB_ANALYTICS_SITE_TAG = "d1bea8562544421fbd2db5f11b74d1cc";
|
|
|
53
53
|
export const WEB_ANALYTICS_SITE_TOKEN = "bd88d63678b64bc89f9702f48846bc88";
|
|
54
54
|
|
|
55
55
|
/**
|
|
56
|
-
* The
|
|
56
|
+
* 🔴 The `version` the beacon's config must carry, or NOTHING is recorded.
|
|
57
|
+
*
|
|
58
|
+
* Measured 2026-09-22, after the RUM API showed every manual-snippet shell recording ZERO
|
|
59
|
+
* pageviews from 2026-09-19 on — collections, music, roms, flix, station and desk, which had
|
|
60
|
+
* been 130–290 a day between them — while the hosts still on the edge's injection kept
|
|
61
|
+
* recording. The beacon copies this value into its payload as `versions.fl`, and
|
|
62
|
+
* `cloudflareinsights.com/cdn-cgi/rum` answers a payload without `fl` with a **404 that carries
|
|
63
|
+
* no CORS header**, which a browser prints as a CORS refusal and nobody reads as "recorded
|
|
64
|
+
* nothing". Same token, same src, driven in a real browser from a `*.cursedalchemy.com` origin:
|
|
65
|
+
* `{"token": …}` → 404 every time; `{"version": "2024.11.0", "token": …}` → 204 every time,
|
|
66
|
+
* whether the script path was pinned or not. Any value was accepted; this one is what the
|
|
67
|
+
* edge's own injection carries, so it is the value Cloudflare is known to send.
|
|
68
|
+
*
|
|
69
|
+
* Cloudflare's own `site_info` snippet OMITS it, so "exactly the vendor's text" was the
|
|
70
|
+
* defect, not the safety it looked like. `tools/check-web-analytics.ts` reds a hand copy
|
|
71
|
+
* without it.
|
|
72
|
+
*/
|
|
73
|
+
export const WEB_ANALYTICS_BEACON_VERSION = "2024.11.0";
|
|
74
|
+
|
|
75
|
+
/** The `data-cf-beacon` value — one spelling for the tag, the bootstrap and every hand copy. */
|
|
76
|
+
export function webAnalyticsBeaconConfig(token: string = WEB_ANALYTICS_SITE_TOKEN): string {
|
|
77
|
+
return `{"version": "${WEB_ANALYTICS_BEACON_VERSION}", "token": "${token}"}`;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* The snippet as Cloudflare's own `site_info` endpoint spells it — plus the `version` key it
|
|
82
|
+
* leaves out, without which nothing is recorded (see {@link WEB_ANALYTICS_BEACON_VERSION}).
|
|
57
83
|
*
|
|
58
84
|
* 🔴 The single-quoted attributes and the space after `"token":` are Cloudflare's, not a
|
|
59
85
|
* style choice, and they are kept because this string is COMPARED against what an app's
|
|
@@ -68,7 +94,7 @@ export function webAnalyticsTag(token: string = WEB_ANALYTICS_SITE_TOKEN): strin
|
|
|
68
94
|
return (
|
|
69
95
|
"<!-- Cloudflare Web Analytics -->" +
|
|
70
96
|
`<script type='module' src='${WEB_ANALYTICS_BEACON_SRC}' ` +
|
|
71
|
-
`data-cf-beacon='
|
|
97
|
+
`data-cf-beacon='${webAnalyticsBeaconConfig(token)}'></script>` +
|
|
72
98
|
"<!-- End Cloudflare Web Analytics -->"
|
|
73
99
|
);
|
|
74
100
|
}
|
|
@@ -121,7 +147,7 @@ export function webAnalyticsBootstrap(token: string = WEB_ANALYTICS_SITE_TOKEN):
|
|
|
121
147
|
`\t\tvar beacon = document.createElement("script");`,
|
|
122
148
|
`\t\tbeacon.type = "module";`,
|
|
123
149
|
`\t\tbeacon.src = "${WEB_ANALYTICS_BEACON_SRC}";`,
|
|
124
|
-
`\t\tbeacon.setAttribute("data-cf-beacon", '
|
|
150
|
+
`\t\tbeacon.setAttribute("data-cf-beacon", '${webAnalyticsBeaconConfig(token)}');`,
|
|
125
151
|
`\t\tdocument.head.appendChild(beacon);`,
|
|
126
152
|
"\t}",
|
|
127
153
|
"</script>",
|
|
@@ -133,12 +159,23 @@ export function webAnalyticsBootstrap(token: string = WEB_ANALYTICS_SITE_TOKEN):
|
|
|
133
159
|
*
|
|
134
160
|
* Deliberately NOT a substring test against {@link WEB_ANALYTICS_TAG}: a shell is authored by
|
|
135
161
|
* hand and a formatter is allowed to re-wrap the tag across lines or re-order its attributes.
|
|
136
|
-
* What must be true is that the beacon's source is there
|
|
137
|
-
* token — the
|
|
162
|
+
* What must be true is that the beacon's source is there, that it is carrying the right
|
|
163
|
+
* token, and that its config carries a `version` — the three things that decide whether a
|
|
164
|
+
* pageview is recorded at all. The third was missed until 2026-09-22 and cost four days of
|
|
165
|
+
* every manual shell's data; see {@link WEB_ANALYTICS_BEACON_VERSION}.
|
|
138
166
|
*/
|
|
139
167
|
export function htmlCarriesWebAnalytics(
|
|
140
168
|
html: string,
|
|
141
169
|
token: string = WEB_ANALYTICS_SITE_TOKEN,
|
|
142
170
|
): boolean {
|
|
143
|
-
return html.includes(WEB_ANALYTICS_BEACON_SRC) && html.includes(token);
|
|
171
|
+
return html.includes(WEB_ANALYTICS_BEACON_SRC) && html.includes(token) && beaconConfigsCarryVersion(html);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Every `data-cf-beacon` config in `html` names a `version` — and there is at least one.
|
|
176
|
+
* Read up to the config's closing brace, so key order and a formatter's spacing do not matter.
|
|
177
|
+
*/
|
|
178
|
+
export function beaconConfigsCarryVersion(html: string): boolean {
|
|
179
|
+
const configs = [...html.matchAll(/data-cf-beacon[^{]{0,24}(\{[^}]*\})/g)].map((m) => m[1] ?? "");
|
|
180
|
+
return configs.length > 0 && configs.every((c) => /"version"\s*:\s*"[^"]+"/.test(c));
|
|
144
181
|
}
|
|
@@ -30,7 +30,11 @@ const stuckIngest = (failed = 42): IncidentReport => ({
|
|
|
30
30
|
title: "collections: every queued upload is failing",
|
|
31
31
|
whatBroke: "The scheduled ingest ran and every one of its items failed.",
|
|
32
32
|
whatItBlocks: "Nothing new reaches the collections app, and the queue keeps growing.",
|
|
33
|
-
|
|
33
|
+
// 🔴 Not a `cd ~/…`. This is a PUBLISHED library: a fixture here is read by whoever
|
|
34
|
+
// installs it, and the string that stood until 2026-09-22 named a directory on one
|
|
35
|
+
// machine that had not existed for days. `check-paths`'s `deadCd` rule is what found
|
|
36
|
+
// it, and a fixture is the one kind of violation that can simply be reworded.
|
|
37
|
+
whatToDo: "Run the storage-drift sweep for this tenant and clear the failed uploads.",
|
|
34
38
|
facts: { failed, pending: failed },
|
|
35
39
|
});
|
|
36
40
|
|
|
@@ -80,9 +80,14 @@ const noStore = { "cache-control": "no-store, no-cache, must-revalidate", pragma
|
|
|
80
80
|
* loosening it: nothing loads from anywhere but this origin, the form posts nowhere (the
|
|
81
81
|
* script does the POST), and the page cannot be framed.
|
|
82
82
|
*
|
|
83
|
-
* 🔴 **EXPORTED so that nothing has to copy it
|
|
84
|
-
*
|
|
85
|
-
*
|
|
83
|
+
* 🔴 **EXPORTED so that nothing has to copy it** — and exporting it is not the same as nothing
|
|
84
|
+
* copying it. This sentence used to read *"`apps/collections` had a hand-typed duplicate … until
|
|
85
|
+
* 2026-09-18"*, and it was false the day it was written: the export landed, and BOTH re-typed
|
|
86
|
+
* copies stayed — one in `apps/collections/scripts/injectedScripts.test.ts` and one three
|
|
87
|
+
* directories from here in `../analytics/injectedScripts.spec.ts`. They were collapsed onto this
|
|
88
|
+
* constant on 2026-09-21. A policy that is typed twice can be loosened in one place and stay
|
|
89
|
+
* green in the other, which is a wall that no longer refuses what its own test says it does.
|
|
90
|
+
* It is also what
|
|
86
91
|
* `beaconNeverReachesTheWall.spec.ts` measures the beacon against: the wall refuses
|
|
87
92
|
* `static.cloudflareinsights.com`, so the analytics tag and this response are mutually
|
|
88
93
|
* exclusive BY CONSTRUCTION rather than by anyone remembering. The owner's reason, 2026-09-18:
|