cursedbelt-server 4.3.0 → 4.5.0

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.
@@ -0,0 +1,18 @@
1
+ /**
2
+ * `cursedbelt-server/analytics` — where the fleet's web-analytics beacon is DEFINED, and the
3
+ * invariant that decides which responses may carry it.
4
+ *
5
+ * Two files, and the pairing is the point:
6
+ *
7
+ * · `webAnalytics.ts` is the one copy of the tag. An app's public shell pastes it; nothing
8
+ * generates ten of them.
9
+ * · `injectedScripts.ts` is the general rule — nothing that arrives in a response's HTML may
10
+ * be refused by the CSP on that same response — which is what makes "the wall never carries
11
+ * the beacon" a CHECK rather than a sentence. `../master-lock/beaconNeverReachesTheWall.spec.ts`
12
+ * is that check.
13
+ *
14
+ * Both are pure string work with no imports at all, so this subpath costs a consumer nothing
15
+ * and drags no peer.
16
+ */
17
+ export { type InjectedScript, foreignScriptSources, scriptsRefusedByCsp, } from "./injectedScripts";
18
+ export { WEB_ANALYTICS_BEACON_ORIGIN, WEB_ANALYTICS_BEACON_SRC, WEB_ANALYTICS_SITE_TAG, WEB_ANALYTICS_SITE_TOKEN, WEB_ANALYTICS_TAG, htmlCarriesWebAnalytics, webAnalyticsTag, } from "./webAnalytics";
@@ -0,0 +1,18 @@
1
+ /**
2
+ * `cursedbelt-server/analytics` — where the fleet's web-analytics beacon is DEFINED, and the
3
+ * invariant that decides which responses may carry it.
4
+ *
5
+ * Two files, and the pairing is the point:
6
+ *
7
+ * · `webAnalytics.ts` is the one copy of the tag. An app's public shell pastes it; nothing
8
+ * generates ten of them.
9
+ * · `injectedScripts.ts` is the general rule — nothing that arrives in a response's HTML may
10
+ * be refused by the CSP on that same response — which is what makes "the wall never carries
11
+ * the beacon" a CHECK rather than a sentence. `../master-lock/beaconNeverReachesTheWall.spec.ts`
12
+ * is that check.
13
+ *
14
+ * Both are pure string work with no imports at all, so this subpath costs a consumer nothing
15
+ * and drags no peer.
16
+ */
17
+ export { foreignScriptSources, scriptsRefusedByCsp, } from "./injectedScripts";
18
+ export { WEB_ANALYTICS_BEACON_ORIGIN, WEB_ANALYTICS_BEACON_SRC, WEB_ANALYTICS_SITE_TAG, WEB_ANALYTICS_SITE_TOKEN, WEB_ANALYTICS_TAG, htmlCarriesWebAnalytics, webAnalyticsTag, } from "./webAnalytics";
@@ -0,0 +1,54 @@
1
+ /**
2
+ * Does a page's own CSP refuse a script that arrived in that page's own HTML?
3
+ *
4
+ * ## The invariant, which names no vendor
5
+ *
6
+ * > No script that reaches the browser inside a response's HTML may be refused by the CSP on
7
+ * > that same response.
8
+ *
9
+ * Every part of the arrangement may move and this stays right. Turn an edge injection off and
10
+ * the foreign scripts disappear — green. Decide a third-party script is wanted and allow its
11
+ * host — green. Harden a page with `script-src 'self'` while something is still injecting into
12
+ * it — RED, and it names the origin, which is the console line the owner would otherwise find
13
+ * himself during an incident.
14
+ *
15
+ * ## 🔴 Why it lives in this package
16
+ *
17
+ * It was written in `apps/collections` on 2026-09-18 and it was right there, but it could only
18
+ * ever see one app — and the response it was written about, the master-lock wall, is served by
19
+ * THIS package to every app that mounts the guard. One copy here is what makes the two halves
20
+ * of `webAnalytics.ts`'s decision checkable in the same place: the wall refuses the beacon, and
21
+ * a public shell carries it.
22
+ *
23
+ * ## 🔴 Conservative on purpose
24
+ *
25
+ * Every ambiguity resolves to PERMITTED. A source expression this file cannot parse, a
26
+ * `'strict-dynamic'` that changes what host lists even mean, a CSP with no script directive at
27
+ * all — all of them pass. A deployed smoke decides whether an app's `scripts/deploy.ts` rolls
28
+ * back; a false red there discards good work over a CSP grammar corner, so the only thing that
29
+ * fails here is a script with NO source expression that could admit it.
30
+ */
31
+ /** An absolute script URL found in the HTML, with the origin already resolved. */
32
+ export interface InjectedScript {
33
+ /** Exactly as it appeared in the markup, for the failure message. */
34
+ src: string;
35
+ /** `https://static.cloudflareinsights.com`, resolved against the page's own URL. */
36
+ origin: string;
37
+ }
38
+ /**
39
+ * Every `<script src=…>` in `html` whose origin is NOT `pageUrl`'s.
40
+ *
41
+ * Inline scripts are ignored: they are the page's own (most shells here ship one, a pre-paint
42
+ * colour-scheme script) and a CSP that refuses them is a different finding from this one.
43
+ * `data:` and `blob:` are ignored for the same reason — neither is a host anything could have
44
+ * injected.
45
+ */
46
+ export declare function foreignScriptSources(html: string, pageUrl: string): InjectedScript[];
47
+ /**
48
+ * Which of `scripts` the `content-security-policy` header would refuse.
49
+ *
50
+ * `null`/absent/empty header means no policy, which refuses nothing. The directive consulted
51
+ * is `script-src-elem`, then `script-src`, then `default-src` — the browser's own fallback
52
+ * order for a `<script src>` element.
53
+ */
54
+ export declare function scriptsRefusedByCsp(csp: string | null | undefined, scripts: InjectedScript[], pageUrl: string): InjectedScript[];
@@ -0,0 +1,138 @@
1
+ /**
2
+ * Does a page's own CSP refuse a script that arrived in that page's own HTML?
3
+ *
4
+ * ## The invariant, which names no vendor
5
+ *
6
+ * > No script that reaches the browser inside a response's HTML may be refused by the CSP on
7
+ * > that same response.
8
+ *
9
+ * Every part of the arrangement may move and this stays right. Turn an edge injection off and
10
+ * the foreign scripts disappear — green. Decide a third-party script is wanted and allow its
11
+ * host — green. Harden a page with `script-src 'self'` while something is still injecting into
12
+ * it — RED, and it names the origin, which is the console line the owner would otherwise find
13
+ * himself during an incident.
14
+ *
15
+ * ## 🔴 Why it lives in this package
16
+ *
17
+ * It was written in `apps/collections` on 2026-09-18 and it was right there, but it could only
18
+ * ever see one app — and the response it was written about, the master-lock wall, is served by
19
+ * THIS package to every app that mounts the guard. One copy here is what makes the two halves
20
+ * of `webAnalytics.ts`'s decision checkable in the same place: the wall refuses the beacon, and
21
+ * a public shell carries it.
22
+ *
23
+ * ## 🔴 Conservative on purpose
24
+ *
25
+ * Every ambiguity resolves to PERMITTED. A source expression this file cannot parse, a
26
+ * `'strict-dynamic'` that changes what host lists even mean, a CSP with no script directive at
27
+ * all — all of them pass. A deployed smoke decides whether an app's `scripts/deploy.ts` rolls
28
+ * back; a false red there discards good work over a CSP grammar corner, so the only thing that
29
+ * fails here is a script with NO source expression that could admit it.
30
+ */
31
+ /**
32
+ * Every `<script src=…>` in `html` whose origin is NOT `pageUrl`'s.
33
+ *
34
+ * Inline scripts are ignored: they are the page's own (most shells here ship one, a pre-paint
35
+ * colour-scheme script) and a CSP that refuses them is a different finding from this one.
36
+ * `data:` and `blob:` are ignored for the same reason — neither is a host anything could have
37
+ * injected.
38
+ */
39
+ export function foreignScriptSources(html, pageUrl) {
40
+ const page = new URL(pageUrl);
41
+ const found = [];
42
+ const seen = new Set();
43
+ const tag = /<script\b[^>]*?\bsrc\s*=\s*(?:"([^"]*)"|'([^']*)'|([^\s"'>]+))/gi;
44
+ for (const match of html.matchAll(tag)) {
45
+ const raw = (match[1] ?? match[2] ?? match[3] ?? "").trim();
46
+ if (!raw)
47
+ continue;
48
+ let origin;
49
+ try {
50
+ const url = new URL(raw, page);
51
+ if (url.protocol !== "http:" && url.protocol !== "https:")
52
+ continue;
53
+ origin = url.origin;
54
+ }
55
+ catch {
56
+ continue; // not a URL this check can reason about — see the conservative note
57
+ }
58
+ if (origin === page.origin)
59
+ continue;
60
+ if (seen.has(origin + raw))
61
+ continue;
62
+ seen.add(origin + raw);
63
+ found.push({ src: raw, origin });
64
+ }
65
+ return found;
66
+ }
67
+ /**
68
+ * Which of `scripts` the `content-security-policy` header would refuse.
69
+ *
70
+ * `null`/absent/empty header means no policy, which refuses nothing. The directive consulted
71
+ * is `script-src-elem`, then `script-src`, then `default-src` — the browser's own fallback
72
+ * order for a `<script src>` element.
73
+ */
74
+ export function scriptsRefusedByCsp(csp, scripts, pageUrl) {
75
+ if (!csp?.trim() || scripts.length === 0)
76
+ return [];
77
+ const directives = new Map();
78
+ for (const part of csp.split(";")) {
79
+ const tokens = part.trim().split(/\s+/).filter(Boolean);
80
+ const name = tokens.shift()?.toLowerCase();
81
+ if (name && !directives.has(name))
82
+ directives.set(name, tokens);
83
+ }
84
+ const sources = directives.get("script-src-elem") ??
85
+ directives.get("script-src") ??
86
+ directives.get("default-src");
87
+ if (!sources)
88
+ return [];
89
+ // `'strict-dynamic'` makes host allowlists inert in ways that depend on how the script was
90
+ // reached. Not decidable from the markup alone, so it is permitted — see the header.
91
+ if (sources.some((s) => s.toLowerCase() === "'strict-dynamic'"))
92
+ return [];
93
+ const self = new URL(pageUrl).origin;
94
+ return scripts.filter((script) => !sources.some((source) => sourceAdmits(source, script.origin, self)));
95
+ }
96
+ /** Does one CSP source expression admit `origin`? Unparseable ⇒ yes, deliberately. */
97
+ function sourceAdmits(source, origin, self) {
98
+ const value = source.trim();
99
+ if (!value)
100
+ return false;
101
+ if (value === "*")
102
+ return true;
103
+ if (value.startsWith("'")) {
104
+ const keyword = value.toLowerCase();
105
+ if (keyword === "'self'")
106
+ return origin === self;
107
+ // `'none'`, `'unsafe-inline'`, `'nonce-…'`, `'sha256-…'`, `'report-sample'`: none of
108
+ // them admits a host, and `'none'` in particular must not.
109
+ return false;
110
+ }
111
+ let target;
112
+ try {
113
+ target = new URL(origin);
114
+ }
115
+ catch {
116
+ return true;
117
+ }
118
+ // A bare scheme source — `https:`, `data:`.
119
+ if (/^[a-z][a-z0-9+.-]*:$/i.test(value))
120
+ return value.toLowerCase() === target.protocol;
121
+ const withScheme = /^[a-z][a-z0-9+.-]*:\/\//i.exec(value);
122
+ let rest = value;
123
+ if (withScheme) {
124
+ if (withScheme[0].toLowerCase() !== `${target.protocol}//`)
125
+ return false;
126
+ rest = value.slice(withScheme[0].length);
127
+ }
128
+ // Port and path narrow a source; ignoring them can only ever ADMIT more, which is the
129
+ // direction this file errs in.
130
+ const host = rest.split("/")[0]?.split(":")[0]?.toLowerCase() ?? "";
131
+ if (!host)
132
+ return true;
133
+ if (host === "*")
134
+ return true;
135
+ if (host.startsWith("*."))
136
+ return target.hostname.toLowerCase().endsWith(host.slice(1));
137
+ return target.hostname.toLowerCase() === host;
138
+ }
@@ -0,0 +1,73 @@
1
+ /**
2
+ * Cloudflare Web Analytics — ONE definition of the beacon, for every app on the fleet.
3
+ *
4
+ * ## 🔴 Why this is here and not in each app's `index.html` alone
5
+ *
6
+ * Until 2026-09-18 the zone carried **Automatic Setup**: Cloudflare rewrote every HTML
7
+ * response leaving `cursedalchemy.com` and appended the beacon tag. Nothing in any repo
8
+ * asked for it, nothing in any repo could see it locally, and it arrived on pages that
9
+ * deliberately refuse third-party script — including the master-lock wall, where a master
10
+ * password is typed. The owner read a console during an outage on 2026-09-17 and the FIRST
11
+ * line was that tag being refused; it broke nothing and cost an agent the first minutes of
12
+ * the incident.
13
+ *
14
+ * His decision, 2026-09-18, in his words: *"do your recommendation with option c but not on
15
+ * the wall. If we can stop the console error that would be good since we are purposely
16
+ * blocking on that page and it makes it look like there is a mistake when it shows."* — and,
17
+ * on why the wall's CSP may not simply be loosened: *"I don't want to trust cloudflare with
18
+ * master password data that is typed in."*
19
+ *
20
+ * So the install method moves from automatic to MANUAL. Same site, same dashboard, same
21
+ * data; the difference is that the fleet now decides *which pages* carry it. Public shells
22
+ * do. {@link "../master-lock/guard".MASTER_LOCK_CSP the wall} does not — and under manual
23
+ * setup it is not injected into at all, so there is no refused script and no console line.
24
+ *
25
+ * ## 🔴 `token` is the site TOKEN, not the site TAG
26
+ *
27
+ * They are both 32 hex characters and they are not interchangeable. `site_tag`
28
+ * ({@link WEB_ANALYTICS_SITE_TAG}) is the id the RUM API addresses the site by;
29
+ * `site_token` ({@link WEB_ANALYTICS_SITE_TOKEN}) is what goes in `data-cf-beacon`. Putting
30
+ * the tag in the snippet does not error anywhere — the page loads, the beacon loads, and the
31
+ * data goes nowhere. Measured 2026-09-18 against
32
+ * `GET /accounts/<acct>/rum/site_info/list`, which returns both fields **and** the ready-made
33
+ * `snippet`; {@link WEB_ANALYTICS_TAG} is that snippet byte-for-byte, and the token in it is
34
+ * the same one the edge was injecting.
35
+ *
36
+ * Neither value is a secret: the token is served to every visitor inside every page's HTML,
37
+ * which is the whole mechanism. It is an identifier, not a credential.
38
+ */
39
+ /** The host the beacon is fetched from — the origin a CSP would have to admit. */
40
+ export declare const WEB_ANALYTICS_BEACON_ORIGIN = "https://static.cloudflareinsights.com";
41
+ /** The beacon itself. Manual setup serves the unversioned path; the edge served a pinned one. */
42
+ export declare const WEB_ANALYTICS_BEACON_SRC = "https://static.cloudflareinsights.com/beacon.min.js";
43
+ /**
44
+ * The `cursedalchemy.com` site's id in the RUM API. **Not** what goes in the tag — see the
45
+ * module note. Kept beside the token so the next reader cannot pick the wrong one by accident.
46
+ */
47
+ export declare const WEB_ANALYTICS_SITE_TAG = "d1bea8562544421fbd2db5f11b74d1cc";
48
+ /** The `cursedalchemy.com` site's `data-cf-beacon` token. This is what the tag carries. */
49
+ export declare const WEB_ANALYTICS_SITE_TOKEN = "bd88d63678b64bc89f9702f48846bc88";
50
+ /**
51
+ * The snippet, exactly as Cloudflare's own `site_info` endpoint spells it.
52
+ *
53
+ * 🔴 The single-quoted attributes and the space after `"token":` are Cloudflare's, not a
54
+ * style choice, and they are kept because this string is COMPARED against what an app's
55
+ * shell contains. `data-cf-beacon`'s value is JSON containing double quotes, so the attribute
56
+ * quoting cannot be flipped without escaping; leaving it identical to the vendor's text means
57
+ * a future reader can diff it against the dashboard in one glance.
58
+ *
59
+ * `type='module'` rather than `defer`: that is what the endpoint returns today and what the
60
+ * edge was injecting, so switching install methods changes nothing about how the page loads.
61
+ */
62
+ export declare function webAnalyticsTag(token?: string): string;
63
+ /** The fleet's tag, for the one site this generation serves. */
64
+ export declare const WEB_ANALYTICS_TAG: string;
65
+ /**
66
+ * Does `html` carry the beacon for this site?
67
+ *
68
+ * Deliberately NOT a substring test against {@link WEB_ANALYTICS_TAG}: a shell is authored by
69
+ * hand and a formatter is allowed to re-wrap the tag across lines or re-order its attributes.
70
+ * What must be true is that the beacon's source is there AND that it is carrying the right
71
+ * token — the two halves that decide whether a pageview is recorded at all.
72
+ */
73
+ export declare function htmlCarriesWebAnalytics(html: string, token?: string): boolean;
@@ -0,0 +1,80 @@
1
+ /**
2
+ * Cloudflare Web Analytics — ONE definition of the beacon, for every app on the fleet.
3
+ *
4
+ * ## 🔴 Why this is here and not in each app's `index.html` alone
5
+ *
6
+ * Until 2026-09-18 the zone carried **Automatic Setup**: Cloudflare rewrote every HTML
7
+ * response leaving `cursedalchemy.com` and appended the beacon tag. Nothing in any repo
8
+ * asked for it, nothing in any repo could see it locally, and it arrived on pages that
9
+ * deliberately refuse third-party script — including the master-lock wall, where a master
10
+ * password is typed. The owner read a console during an outage on 2026-09-17 and the FIRST
11
+ * line was that tag being refused; it broke nothing and cost an agent the first minutes of
12
+ * the incident.
13
+ *
14
+ * His decision, 2026-09-18, in his words: *"do your recommendation with option c but not on
15
+ * the wall. If we can stop the console error that would be good since we are purposely
16
+ * blocking on that page and it makes it look like there is a mistake when it shows."* — and,
17
+ * on why the wall's CSP may not simply be loosened: *"I don't want to trust cloudflare with
18
+ * master password data that is typed in."*
19
+ *
20
+ * So the install method moves from automatic to MANUAL. Same site, same dashboard, same
21
+ * data; the difference is that the fleet now decides *which pages* carry it. Public shells
22
+ * do. {@link "../master-lock/guard".MASTER_LOCK_CSP the wall} does not — and under manual
23
+ * setup it is not injected into at all, so there is no refused script and no console line.
24
+ *
25
+ * ## 🔴 `token` is the site TOKEN, not the site TAG
26
+ *
27
+ * They are both 32 hex characters and they are not interchangeable. `site_tag`
28
+ * ({@link WEB_ANALYTICS_SITE_TAG}) is the id the RUM API addresses the site by;
29
+ * `site_token` ({@link WEB_ANALYTICS_SITE_TOKEN}) is what goes in `data-cf-beacon`. Putting
30
+ * the tag in the snippet does not error anywhere — the page loads, the beacon loads, and the
31
+ * data goes nowhere. Measured 2026-09-18 against
32
+ * `GET /accounts/<acct>/rum/site_info/list`, which returns both fields **and** the ready-made
33
+ * `snippet`; {@link WEB_ANALYTICS_TAG} is that snippet byte-for-byte, and the token in it is
34
+ * the same one the edge was injecting.
35
+ *
36
+ * Neither value is a secret: the token is served to every visitor inside every page's HTML,
37
+ * which is the whole mechanism. It is an identifier, not a credential.
38
+ */
39
+ /** The host the beacon is fetched from — the origin a CSP would have to admit. */
40
+ export const WEB_ANALYTICS_BEACON_ORIGIN = "https://static.cloudflareinsights.com";
41
+ /** The beacon itself. Manual setup serves the unversioned path; the edge served a pinned one. */
42
+ export const WEB_ANALYTICS_BEACON_SRC = `${WEB_ANALYTICS_BEACON_ORIGIN}/beacon.min.js`;
43
+ /**
44
+ * The `cursedalchemy.com` site's id in the RUM API. **Not** what goes in the tag — see the
45
+ * module note. Kept beside the token so the next reader cannot pick the wrong one by accident.
46
+ */
47
+ export const WEB_ANALYTICS_SITE_TAG = "d1bea8562544421fbd2db5f11b74d1cc";
48
+ /** The `cursedalchemy.com` site's `data-cf-beacon` token. This is what the tag carries. */
49
+ export const WEB_ANALYTICS_SITE_TOKEN = "bd88d63678b64bc89f9702f48846bc88";
50
+ /**
51
+ * The snippet, exactly as Cloudflare's own `site_info` endpoint spells it.
52
+ *
53
+ * 🔴 The single-quoted attributes and the space after `"token":` are Cloudflare's, not a
54
+ * style choice, and they are kept because this string is COMPARED against what an app's
55
+ * shell contains. `data-cf-beacon`'s value is JSON containing double quotes, so the attribute
56
+ * quoting cannot be flipped without escaping; leaving it identical to the vendor's text means
57
+ * a future reader can diff it against the dashboard in one glance.
58
+ *
59
+ * `type='module'` rather than `defer`: that is what the endpoint returns today and what the
60
+ * edge was injecting, so switching install methods changes nothing about how the page loads.
61
+ */
62
+ export function webAnalyticsTag(token = WEB_ANALYTICS_SITE_TOKEN) {
63
+ return ("<!-- Cloudflare Web Analytics -->" +
64
+ `<script type='module' src='${WEB_ANALYTICS_BEACON_SRC}' ` +
65
+ `data-cf-beacon='{"token": "${token}"}'></script>` +
66
+ "<!-- End Cloudflare Web Analytics -->");
67
+ }
68
+ /** The fleet's tag, for the one site this generation serves. */
69
+ export const WEB_ANALYTICS_TAG = webAnalyticsTag();
70
+ /**
71
+ * Does `html` carry the beacon for this site?
72
+ *
73
+ * Deliberately NOT a substring test against {@link WEB_ANALYTICS_TAG}: a shell is authored by
74
+ * hand and a formatter is allowed to re-wrap the tag across lines or re-order its attributes.
75
+ * What must be true is that the beacon's source is there AND that it is carrying the right
76
+ * token — the two halves that decide whether a pageview is recorded at all.
77
+ */
78
+ export function htmlCarriesWebAnalytics(html, token = WEB_ANALYTICS_SITE_TOKEN) {
79
+ return html.includes(WEB_ANALYTICS_BEACON_SRC) && html.includes(token);
80
+ }
@@ -15,7 +15,6 @@
15
15
  * ```
16
16
  */
17
17
  export { backupFor, type BackupPoint, checkpointWal, createLocalBackup, createTimeTravelBackup, type DatabaseBackup, type LocalBackupOpts, type TimeTravelOpts, } from './backup';
18
- export { createD1Kysely, D1LikeDialect, type D1PlumbingDb } from './kysely';
19
18
  export { type InvocationD1, perInvocation } from './invocation';
20
19
  export { assertBatchSize, assertWithinLimits, chunkForBind, LIMITS } from './limits';
21
20
  export { createLocalD1, refuseInteractiveTransaction } from './local';
@@ -15,7 +15,13 @@
15
15
  * ```
16
16
  */
17
17
  export { backupFor, checkpointWal, createLocalBackup, createTimeTravelBackup, } from './backup';
18
- export { createD1Kysely, D1LikeDialect } from './kysely';
18
+ // 🔴 `./kysely` is deliberately NOT re-exported here — import it from
19
+ // `cursedbelt-server/d1/kysely`. It statically imports `kysely` (real values: `Kysely`,
20
+ // `SqliteAdapter`, `SqliteQueryCompiler`), which is an OPTIONAL peer, so re-exporting it
21
+ // made `import 'cursedbelt-server/d1'` throw `Cannot find package 'kysely'` for every app
22
+ // that does not use the query builder — which per `../db/kysely.ts`'s own header is most
23
+ // of them, since business tables stay on raw statements. Measured 2026-09-18 against the
24
+ // published 4.3.0 tarball. `barrelsReachNoOptionalPeer.spec.ts` is what keeps it out.
19
25
  export { perInvocation } from './invocation';
20
26
  export { assertBatchSize, assertWithinLimits, chunkForBind, LIMITS } from './limits';
21
27
  export { createLocalD1, refuseInteractiveTransaction } from './local';
@@ -46,6 +46,20 @@ export interface MasterLockGuardOptions extends LockPageOptions {
46
46
  */
47
47
  secure?: (url: URL, req: Request) => boolean;
48
48
  }
49
+ /**
50
+ * The lock page's own CSP. Strict enough to stand in front of `apps/vault`'s wall without
51
+ * loosening it: nothing loads from anywhere but this origin, the form posts nowhere (the
52
+ * script does the POST), and the page cannot be framed.
53
+ *
54
+ * 🔴 **EXPORTED so that nothing has to copy it.** `apps/collections` had a hand-typed
55
+ * duplicate of this string in a test until 2026-09-18, which is a policy that can drift
56
+ * without anything going red. It is also what
57
+ * `beaconNeverReachesTheWall.spec.ts` measures the beacon against: the wall refuses
58
+ * `static.cloudflareinsights.com`, so the analytics tag and this response are mutually
59
+ * exclusive BY CONSTRUCTION rather than by anyone remembering. The owner's reason, 2026-09-18:
60
+ * *"I don't want to trust cloudflare with master password data that is typed in."*
61
+ */
62
+ export declare const MASTER_LOCK_CSP: string;
49
63
  export interface MasterLockGuard {
50
64
  /** `Response` when handled, `null` when the app should serve the request. */
51
65
  handle(req: Request): Promise<Response | null>;
@@ -19,8 +19,16 @@ const noStore = { "cache-control": "no-store, no-cache, must-revalidate", pragma
19
19
  * The lock page's own CSP. Strict enough to stand in front of `apps/vault`'s wall without
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
+ *
23
+ * 🔴 **EXPORTED so that nothing has to copy it.** `apps/collections` had a hand-typed
24
+ * duplicate of this string in a test until 2026-09-18, which is a policy that can drift
25
+ * without anything going red. It is also what
26
+ * `beaconNeverReachesTheWall.spec.ts` measures the beacon against: the wall refuses
27
+ * `static.cloudflareinsights.com`, so the analytics tag and this response are mutually
28
+ * exclusive BY CONSTRUCTION rather than by anyone remembering. The owner's reason, 2026-09-18:
29
+ * *"I don't want to trust cloudflare with master password data that is typed in."*
22
30
  */
23
- const LOCK_CSP = "default-src 'none'; style-src 'self'; script-src 'self'; connect-src 'self'; " +
31
+ export const MASTER_LOCK_CSP = "default-src 'none'; style-src 'self'; script-src 'self'; connect-src 'self'; " +
24
32
  "img-src data:; form-action 'none'; base-uri 'none'; frame-ancestors 'none'";
25
33
  export function createMasterLockGuard(options) {
26
34
  const resolve = typeof options.lock === "function" ? options.lock : () => options.lock;
@@ -256,7 +264,7 @@ function lockPage(options, lock) {
256
264
  status: 200,
257
265
  headers: {
258
266
  "content-type": "text/html; charset=utf-8",
259
- "content-security-policy": LOCK_CSP,
267
+ "content-security-policy": MASTER_LOCK_CSP,
260
268
  "x-content-type-options": "nosniff",
261
269
  [MASTER_LOCK_STATE_HEADER]: "locked",
262
270
  ...noStore,
@@ -275,7 +283,7 @@ function accountsPage(options) {
275
283
  status: 200,
276
284
  headers: {
277
285
  "content-type": "text/html; charset=utf-8",
278
- "content-security-policy": LOCK_CSP,
286
+ "content-security-policy": MASTER_LOCK_CSP,
279
287
  "x-content-type-options": "nosniff",
280
288
  ...noStore,
281
289
  },
@@ -21,7 +21,7 @@
21
21
  * bundle, no framework version to migrate, and no app-local copy to drift.
22
22
  */
23
23
  export { MASTER_LOCK_ACCOUNTS_PAGE_PATHS, MASTER_LOCK_ACCOUNTS_SCRIPT, MASTER_LOCK_ACCOUNTS_STYLE, MASTER_LOCK_MIN_PASSWORD_LENGTH, accountsPageHtml, } from "./accountsPage";
24
- export { type MasterLockGuard, type MasterLockGuardOptions, createMasterLockGuard } from "./guard";
24
+ export { MASTER_LOCK_CSP, type MasterLockGuard, type MasterLockGuardOptions, createMasterLockGuard, } from "./guard";
25
25
  export { LOCK_SCRIPT, LOCK_STYLE, MASTER_LOCK_DERIVE_SOURCE, type LockPageOptions, lockPageHtml, } from "./lockPage";
26
26
  export { MASTER_LOCK_PRINCIPAL_KEY_PREFIX, MasterLockDirectory, type MasterLockDirectoryOptions, masterLockPrincipalKey, } from "./principals";
27
27
  export { FIRST_ACCOUNT_ID, MasterLock, type MasterLockAccount, type MasterLockAddAccountResult, type MasterLockEnrollResult, type MasterLockOptions, type MasterLockRecord, type MasterLockStore, type MasterLockUnlockResult, readRecord, } from "./masterLock";
@@ -21,7 +21,7 @@
21
21
  * bundle, no framework version to migrate, and no app-local copy to drift.
22
22
  */
23
23
  export { MASTER_LOCK_ACCOUNTS_PAGE_PATHS, MASTER_LOCK_ACCOUNTS_SCRIPT, MASTER_LOCK_ACCOUNTS_STYLE, MASTER_LOCK_MIN_PASSWORD_LENGTH, accountsPageHtml, } from "./accountsPage";
24
- export { createMasterLockGuard } from "./guard";
24
+ export { MASTER_LOCK_CSP, createMasterLockGuard, } from "./guard";
25
25
  export { LOCK_SCRIPT, LOCK_STYLE, MASTER_LOCK_DERIVE_SOURCE, lockPageHtml, } from "./lockPage";
26
26
  export { MASTER_LOCK_PRINCIPAL_KEY_PREFIX, MasterLockDirectory, masterLockPrincipalKey, } from "./principals";
27
27
  export { FIRST_ACCOUNT_ID, MasterLock, readRecord, } from "./masterLock";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cursedbelt-server",
3
- "version": "4.3.0",
3
+ "version": "4.5.0",
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.",
@@ -48,6 +48,12 @@
48
48
  "source": "./src/server/context.ts",
49
49
  "import": "./dist/server/context.js"
50
50
  },
51
+ "./analytics": {
52
+ "types": "./dist/server/analytics/index.d.ts",
53
+ "bun": "./src/server/analytics/index.ts",
54
+ "source": "./src/server/analytics/index.ts",
55
+ "import": "./dist/server/analytics/index.js"
56
+ },
51
57
  "./bench": {
52
58
  "types": "./dist/server/bench/index.d.ts",
53
59
  "bun": "./src/server/bench/index.ts",
@@ -78,6 +84,12 @@
78
84
  "source": "./src/server/d1/index.ts",
79
85
  "import": "./dist/server/d1/index.js"
80
86
  },
87
+ "./d1/kysely": {
88
+ "types": "./dist/server/d1/kysely.d.ts",
89
+ "bun": "./src/server/d1/kysely.ts",
90
+ "source": "./src/server/d1/kysely.ts",
91
+ "import": "./dist/server/d1/kysely.js"
92
+ },
81
93
  "./d1/testing": {
82
94
  "types": "./dist/server/d1/fakeD1.d.ts",
83
95
  "bun": "./src/server/d1/fakeD1.ts",