cursedbelt-server 4.22.0 → 4.24.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.
Files changed (43) hide show
  1. package/README.md +8 -0
  2. package/dist/server/bench/budget.d.ts +50 -0
  3. package/dist/server/bench/budget.js +49 -1
  4. package/dist/server/bench/index.d.ts +1 -1
  5. package/dist/server/bench/index.js +1 -1
  6. package/dist/server/metrics/metricsBuffer.d.ts +10 -0
  7. package/dist/server/metrics/metricsBuffer.js +11 -3
  8. package/dist/server/metrics/requestPlace.d.ts +48 -0
  9. package/dist/server/metrics/requestPlace.js +77 -0
  10. package/dist/server/metrics/telemetrySink.d.ts +6 -1
  11. package/dist/server/metrics/telemetrySink.js +14 -2
  12. package/dist/server/middleware/requestLogger.d.ts +8 -0
  13. package/dist/server/middleware/requestLogger.js +4 -1
  14. package/dist/server/requestLog/requestMetrics.d.ts +11 -2
  15. package/dist/server/requestLog/requestMetrics.js +22 -4
  16. package/dist/server/telemetry.d.ts +1 -0
  17. package/dist/server/telemetry.js +1 -0
  18. package/dist/server/worker-argon2/argon2.d.ts +11 -0
  19. package/dist/server/worker-argon2/argon2.js +71 -0
  20. package/dist/server/worker-argon2/index.d.ts +9 -0
  21. package/dist/server/worker-argon2/index.js +9 -0
  22. package/dist/server/worker-argon2/password.d.ts +26 -0
  23. package/dist/server/worker-argon2/password.js +164 -0
  24. package/package.json +8 -1
  25. package/src/server/bench/budget.spec.ts +38 -0
  26. package/src/server/bench/budget.ts +58 -1
  27. package/src/server/bench/cpuBudget.spec.ts +15 -3
  28. package/src/server/bench/index.ts +1 -0
  29. package/src/server/metrics/metricsBuffer.ts +22 -2
  30. package/src/server/metrics/requestPlace.spec.ts +142 -0
  31. package/src/server/metrics/requestPlace.ts +92 -0
  32. package/src/server/metrics/telemetrySink.spec.ts +2 -1
  33. package/src/server/metrics/telemetrySink.ts +14 -2
  34. package/src/server/middleware/requestLogger.ts +12 -1
  35. package/src/server/requestLog/requestMetrics.spec.ts +73 -0
  36. package/src/server/requestLog/requestMetrics.ts +37 -4
  37. package/src/server/telemetry.ts +5 -0
  38. package/src/server/telemetryIsWorkerSafe.spec.ts +17 -0
  39. package/src/server/worker-argon2/argon2.ts +85 -0
  40. package/src/server/worker-argon2/index.spec.ts +162 -0
  41. package/src/server/worker-argon2/index.ts +9 -0
  42. package/src/server/worker-argon2/password.ts +185 -0
  43. package/src/server/worker-argon2/wasm.d.ts +9 -0
@@ -0,0 +1,142 @@
1
+ import { Database } from 'bun:sqlite';
2
+ import { describe, expect, it } from 'bun:test';
3
+ import { Hono } from 'hono';
4
+ import { requestLogger } from '../middleware/requestLogger.js';
5
+ import { createMetricsBuffer } from './metricsBuffer.js';
6
+ import { locateFromCloudflare, normalizePlace, PLACE_FIELD_MAX_CHARS, type RequestLocator } from './requestPlace.js';
7
+ import { type AnalyticsEngineDataset, TELEMETRY_POINT_VERSION, toDataPoint } from './telemetrySink.js';
8
+
9
+ /**
10
+ * Task 2123: a request is recorded with WHERE it came from — as a place, never as an address.
11
+ * Each claim here has its failure path beside it.
12
+ */
13
+
14
+ type Point = Parameters<AnalyticsEngineDataset['writeDataPoint']>[0];
15
+ const fakeDataset = () => {
16
+ const points: Point[] = [];
17
+ return { points, writeDataPoint: (p: Point) => void points.push(p) };
18
+ };
19
+
20
+ /** A Worker request: workerd hangs Cloudflare's geolocation off `request.cf`. */
21
+ const workerRequest = (url: string, cf: Record<string, unknown>): Request => {
22
+ const request = new Request(url);
23
+ Object.defineProperty(request, 'cf', { value: cf });
24
+ return request;
25
+ };
26
+
27
+ const LEGACY_DDL = `CREATE TABLE request_metrics (
28
+ id TEXT PRIMARY KEY, ts INTEGER NOT NULL, method TEXT, route TEXT,
29
+ status INTEGER, duration_ms REAL, bytes_out INTEGER, user_id TEXT)`;
30
+ const PLACED_DDL = LEGACY_DDL.replace('user_id TEXT)', 'user_id TEXT, country TEXT, region TEXT, city TEXT)');
31
+
32
+ describe('normalizePlace — a place and nothing else', () => {
33
+ it('🔴 copies only country/region/city: an ip a locator returns can never be written', () => {
34
+ const leaky = { country: 'IE', city: 'Dublin', ip: '203.0.113.9' } as never;
35
+ const place = normalizePlace(leaky);
36
+ expect(place).toEqual({ country: 'IE', region: null, city: 'Dublin' });
37
+ expect(JSON.stringify(place)).not.toContain('203.0.113.9');
38
+ });
39
+
40
+ it("an unknown country ('XX', '', whitespace) is no place at all — region/city are dropped with it", () => {
41
+ for (const country of ['XX', 'xx', '', ' ']) {
42
+ expect(normalizePlace({ country, city: 'Nowhere' })).toEqual({ country: null, region: null, city: null });
43
+ }
44
+ expect(normalizePlace(null)).toEqual({ country: null, region: null, city: null });
45
+ });
46
+
47
+ it('caps each field so three of them cannot crowd the Analytics Engine blob budget', () => {
48
+ const place = normalizePlace({ country: 'US', city: 'x'.repeat(5000) });
49
+ expect(place.city?.length).toBe(PLACE_FIELD_MAX_CHARS);
50
+ });
51
+ });
52
+
53
+ describe('locateFromCloudflare — the default reads the edge', () => {
54
+ it('on a Worker: request.cf.country/region/city', () => {
55
+ const request = workerRequest('https://app.test/', { country: 'PT', region: 'Lisbon', city: 'Lisbon' });
56
+ expect(locateFromCloudflare(request)).toEqual({ country: 'PT', region: 'Lisbon', city: 'Lisbon' });
57
+ });
58
+
59
+ it('behind the tunnel: the cf-ipcountry header, and the finer headers when the zone sends them (percent-decoded)', () => {
60
+ const coarse = new Request('https://app.test/', { headers: { 'cf-ipcountry': 'BR' } });
61
+ expect(normalizePlace(locateFromCloudflare(coarse))).toEqual({ country: 'BR', region: null, city: null });
62
+ const fine = new Request('https://app.test/', {
63
+ headers: { 'cf-ipcountry': 'BR', 'cf-region': 'S%C3%A3o%20Paulo', 'cf-ipcity': 'S%C3%A3o%20Paulo' },
64
+ });
65
+ expect(locateFromCloudflare(fine)).toEqual({ country: 'BR', region: 'São Paulo', city: 'São Paulo' });
66
+ });
67
+
68
+ it('FAILURE PATH: a direct request (dev, tests) is unplaced, not invented', () => {
69
+ expect(locateFromCloudflare(new Request('http://localhost/'))).toBeNull();
70
+ });
71
+ });
72
+
73
+ describe('the Analytics Engine wire format — v2 appends the place at blob4–6', () => {
74
+ it('toDataPoint puts country/region/city in blob4/5/6 and leaves blob1–3 where they were', () => {
75
+ expect(TELEMETRY_POINT_VERSION).toBe(2);
76
+ const { blobs } = toDataPoint({
77
+ ts: 1, method: 'GET', route: '/', status: 200, durationMs: 1, bytesOut: 0, userId: 'u',
78
+ country: 'JP', region: 'Tokyo', city: 'Shibuya',
79
+ });
80
+ expect(blobs).toEqual(['GET', '/', 'u', 'JP', 'Tokyo', 'Shibuya']);
81
+ });
82
+
83
+ it('a Worker request is placed from request.cf with no app code at all', async () => {
84
+ const ae = fakeDataset();
85
+ const app = new Hono();
86
+ app.use('*', requestLogger(null, { analytics: ae }));
87
+ app.get('/', (c) => c.text('ok'));
88
+ await app.fetch(workerRequest('https://app.test/', { country: 'NZ', city: 'Wellington' }));
89
+ expect(ae.points[0].blobs?.slice(3)).toEqual(['NZ', '', 'Wellington']);
90
+ });
91
+
92
+ it('FAILURE PATH: a request the edge could not place writes empty place blobs, not a missing position', async () => {
93
+ const ae = fakeDataset();
94
+ const app = new Hono();
95
+ app.use('*', requestLogger(null, { analytics: ae }));
96
+ app.get('/', (c) => c.text('ok'));
97
+ await app.fetch(new Request('https://app.test/'));
98
+ expect(ae.points[0].blobs).toHaveLength(6);
99
+ expect(ae.points[0].blobs?.slice(3)).toEqual(['', '', '']);
100
+ });
101
+ });
102
+
103
+ describe('the SQLite sink — place columns where the table has them', () => {
104
+ const serve = async (db: Database, locate?: RequestLocator, request = new Request('http://app.test/x')) => {
105
+ const buffer = createMetricsBuffer({ db });
106
+ const app = new Hono();
107
+ app.use('*', requestLogger(null, { sink: buffer, ...(locate ? { locate } : {}) }));
108
+ app.get('/x', (c) => c.text('ok'));
109
+ expect((await app.fetch(request)).status).toBe(200);
110
+ buffer.stop();
111
+ return db.query('SELECT * FROM request_metrics').all() as Record<string, unknown>[];
112
+ };
113
+
114
+ it("an app's locate hook gets the Request and its place lands in country/region/city", async () => {
115
+ const db = new Database(':memory:');
116
+ db.run(PLACED_DDL);
117
+ let seen: Request | null = null;
118
+ const rows = await serve(db, (request) => {
119
+ seen = request;
120
+ return { country: 'IE', region: 'Leinster', city: 'Dublin' };
121
+ });
122
+ expect(seen).toBeInstanceOf(Request);
123
+ expect(rows).toEqual([expect.objectContaining({ route: '/x', country: 'IE', region: 'Leinster', city: 'Dublin' })]);
124
+ });
125
+
126
+ it('a locate that THROWS records the request unplaced — it never fails the request or loses the row', async () => {
127
+ const db = new Database(':memory:');
128
+ db.run(PLACED_DDL);
129
+ const rows = await serve(db, () => {
130
+ throw new Error('snapshot missing');
131
+ });
132
+ expect(rows).toEqual([expect.objectContaining({ route: '/x', status: 200, country: null, region: null, city: null })]);
133
+ });
134
+
135
+ it("🔴 a table created by an app's OWN older DDL (no place columns) keeps working after the bump", async () => {
136
+ const db = new Database(':memory:');
137
+ db.run(LEGACY_DDL);
138
+ const rows = await serve(db, () => ({ country: 'IE' }));
139
+ expect(rows).toHaveLength(1);
140
+ expect(Object.keys(rows[0])).not.toContain('country');
141
+ });
142
+ });
@@ -0,0 +1,92 @@
1
+ /**
2
+ * Where a request came from — as a PLACE, never as an address (4.23.0, task 2123).
3
+ *
4
+ * ── 🔴 The raw IP is never stored ───────────────────────────────────────────
5
+ * The retired instances' logger wrote `ip` on every row. This one does not, anywhere: not in
6
+ * `request_metrics`, not in an Analytics Engine blob. A {@link RequestLocator} receives the
7
+ * `Request`, may read whatever address it needs from it (`cf-connecting-ip` behind the tunnel),
8
+ * and returns only `{ country, region?, city? }`. The address dies with the request. That is
9
+ * the owner's privacy choice, fixed in the type rather than in a comment: {@link RequestPlace}
10
+ * has no field an address could go in, and {@link normalizePlace} copies only those three.
11
+ *
12
+ * ── The default reads the edge ──────────────────────────────────────────────
13
+ * {@link locateFromCloudflare} is what `requestLogger` uses when an app passes no `locate`:
14
+ *
15
+ * - on a Worker, `request.cf.country/region/city` — Cloudflare's own geolocation;
16
+ * - behind the tunnel (a Mac app), the `cf-ipcountry` header every proxied request carries,
17
+ * plus `cf-region`/`cf-ipcity` when the zone's "visitor location headers" transform is on.
18
+ *
19
+ * A Bun process reached directly (dev, tests) has neither, and gets `null` — an unplaced row,
20
+ * never an invented one. An app that can do better (station's DB-IP snapshot) passes its own
21
+ * `locate`, and should merge per field with this one so the edge wins wherever it speaks.
22
+ *
23
+ * Worker-safe: no `bun:` import (`telemetryIsWorkerSafe.spec.ts` walks this graph).
24
+ */
25
+
26
+ /** A place. `country` is an ISO-3166 alpha-2 code as the edge spells it (`US`, `IE`, `T1` for Tor). */
27
+ export interface RequestPlace {
28
+ country: string;
29
+ region?: string | null;
30
+ city?: string | null;
31
+ }
32
+
33
+ /** Place one request, or `null` when it cannot. Must not throw — but a throw is caught and read as `null`. */
34
+ export type RequestLocator = (request: Request) => RequestPlace | null;
35
+
36
+ /** Each place field is capped so three of them can never crowd the 5120-byte Analytics Engine blob budget. */
37
+ export const PLACE_FIELD_MAX_CHARS = 96;
38
+
39
+ /** Cloudflare's "no country could be determined" code — an unknown, not a place. */
40
+ const UNKNOWN_COUNTRY = 'XX';
41
+
42
+ const clean = (value: unknown): string | null => {
43
+ if (typeof value !== 'string') return null;
44
+ const trimmed = value.trim().slice(0, PLACE_FIELD_MAX_CHARS);
45
+ return trimmed === '' ? null : trimmed;
46
+ };
47
+
48
+ /**
49
+ * The three columns a row stores, from whatever a locator returned. Copies ONLY
50
+ * `country/region/city` — so a locator that returns extra fields (an `ip`, say) cannot get them
51
+ * written — and treats an empty or unknown country as no place at all.
52
+ */
53
+ export function normalizePlace(place: RequestPlace | null | undefined): {
54
+ country: string | null;
55
+ region: string | null;
56
+ city: string | null;
57
+ } {
58
+ const country = clean(place?.country);
59
+ if (!country || country.toUpperCase() === UNKNOWN_COUNTRY) return { country: null, region: null, city: null };
60
+ return { country, region: clean(place?.region), city: clean(place?.city) };
61
+ }
62
+
63
+ /** Header values arrive percent-encoded when the edge had to (`cf-ipcity: S%C3%A3o%20Paulo`). */
64
+ const header = (request: Request, name: string): string | null => {
65
+ const raw = request.headers.get(name);
66
+ if (raw == null) return null;
67
+ try {
68
+ return decodeURIComponent(raw);
69
+ } catch {
70
+ return raw;
71
+ }
72
+ };
73
+
74
+ /** The edge's own answer: `request.cf` on a Worker, the `cf-*` headers behind the tunnel, else `null`. */
75
+ export const locateFromCloudflare: RequestLocator = (request) => {
76
+ const cf = (request as Request & { cf?: Record<string, unknown> }).cf;
77
+ if (cf && typeof cf.country === 'string') {
78
+ return { country: cf.country, region: clean(cf.region), city: clean(cf.city) };
79
+ }
80
+ const country = header(request, 'cf-ipcountry');
81
+ if (!country) return null;
82
+ return { country, region: header(request, 'cf-region'), city: header(request, 'cf-ipcity') };
83
+ };
84
+
85
+ /** Run a locator the way the logger must: a throw is an unplaced request, never a failed one. */
86
+ export function placeOf(locate: RequestLocator, request: Request): ReturnType<typeof normalizePlace> {
87
+ try {
88
+ return normalizePlace(locate(request));
89
+ } catch {
90
+ return normalizePlace(null);
91
+ }
92
+ }
@@ -145,7 +145,8 @@ describe('toDataPoint — the layout is a wire format', () => {
145
145
  }),
146
146
  ).toEqual({
147
147
  indexes: ['/api/v1/sync/requested'],
148
- blobs: ['POST', '/api/v1/sync/requested', 'u-1'],
148
+ // blob4–6 (v2) are the place; '' when unplaced, never a null hole.
149
+ blobs: ['POST', '/api/v1/sync/requested', 'u-1', '', '', ''],
149
150
  // bytesOut null → -1. `0` is a real value for a 204, so it cannot be the marker.
150
151
  doubles: [204, 12.5, -1],
151
152
  });
@@ -97,8 +97,13 @@ export interface TelemetrySinkOptions {
97
97
  * index1 = route, truncated to 96 bytes — the sampling key, so sampling is per route
98
98
  * blob1 = method blob2 = route (untruncated) blob3 = userId ('' when anonymous)
99
99
  * double1 = status double2 = durationMs double3 = bytesOut
100
+ * blob4 = country blob5 = region blob6 = city (v2, 4.23.0)
100
101
  * ```
101
102
  *
103
+ * v2 APPENDED blob4–6: where the request came from, as a place and never an address
104
+ * (`./requestPlace.ts`). `''` when unplaced, like `userId`. A v1 query never reads past blob3,
105
+ * so it is unaffected; a query written against v2 reads `''` on points written before it.
106
+ *
102
107
  * `ts` is deliberately absent: Analytics Engine stamps its own `timestamp`
103
108
  * column, so sending ours would store the same instant twice.
104
109
  *
@@ -106,7 +111,7 @@ export interface TelemetrySinkOptions {
106
111
  * doubles array cannot hold a null and `0` is a real value for every one of them
107
112
  * (a 0-byte 204, notably). Read it as `NULL`, not as a measurement.
108
113
  */
109
- export const TELEMETRY_POINT_VERSION = 1;
114
+ export const TELEMETRY_POINT_VERSION = 2;
110
115
 
111
116
  const INDEX_MAX_BYTES = 96;
112
117
 
@@ -137,7 +142,14 @@ export function toDataPoint(event: TelemetryEvent): {
137
142
  const route = event.route ?? '';
138
143
  return {
139
144
  indexes: [truncateUtf8(route, INDEX_MAX_BYTES)],
140
- blobs: [event.method ?? '', route, event.userId ?? ''],
145
+ blobs: [
146
+ event.method ?? '',
147
+ route,
148
+ event.userId ?? '',
149
+ event.country ?? '',
150
+ event.region ?? '',
151
+ event.city ?? '',
152
+ ],
141
153
  doubles: [num(event.status), num(event.durationMs), num(event.bytesOut)],
142
154
  };
143
155
  }
@@ -10,6 +10,7 @@ import {
10
10
  createTelemetrySink,
11
11
  type TelemetrySink,
12
12
  } from '../metrics/telemetrySink.js';
13
+ import { locateFromCloudflare, placeOf, type RequestLocator } from '../metrics/requestPlace.js';
13
14
 
14
15
  /**
15
16
  * Structured request/response logging. After each request it records one
@@ -42,6 +43,13 @@ export interface RequestLoggerOpts {
42
43
  sink?: TelemetrySink;
43
44
  /** @deprecated Use {@link RequestLoggerOpts.sink} — a `MetricsBuffer` is one. Kept for callers that predate the sink. */
44
45
  buffer?: MetricsBuffer;
46
+ /**
47
+ * Where the request came from — returns a PLACE (`{ country, region?, city? }`) or `null`,
48
+ * never an address; the raw IP is not stored anywhere (`../metrics/requestPlace.ts`).
49
+ * Default: {@link locateFromCloudflare} — `request.cf` on a Worker, the `cf-ipcountry`
50
+ * header behind the tunnel. A throw is caught and the request recorded unplaced.
51
+ */
52
+ locate?: RequestLocator;
45
53
  /** Duration (ms) at/above which a 2xx request also logs an `event_logs` row. Default: 2000. */
46
54
  slowMs?: number;
47
55
  /** Also write `event_logs` rows for status ≥ 400. Default: true. */
@@ -69,6 +77,7 @@ export function requestLogger(
69
77
  opts.sink ?? opts.buffer ?? createTelemetrySink({ db, analytics: opts.analytics ?? null });
70
78
  const slowMs = opts.slowMs ?? 2000;
71
79
  const logErrors = opts.logErrors ?? true;
80
+ const locate = opts.locate ?? locateFromCloudflare;
72
81
 
73
82
  const insertEvent = db
74
83
  ? db.prepare(
@@ -96,7 +105,9 @@ export function requestLogger(
96
105
  const bytesOut = lenHeader ? Number(lenHeader) : null;
97
106
  const userId = c.get('userId') ?? null;
98
107
 
99
- sink.record({ ts: Date.now(), method, route, status, durationMs, bytesOut, userId });
108
+ const place = placeOf(locate, c.req.raw);
109
+
110
+ sink.record({ ts: Date.now(), method, route, status, durationMs, bytesOut, userId, ...place });
100
111
 
101
112
  const isError = status >= 400;
102
113
  const isSlow = durationMs >= slowMs;
@@ -204,3 +204,76 @@ describe("🔴 the inherited corpus is archived, never written", () => {
204
204
  expect(readFileSync(join(dir, `${RETIRED_METRICS_DB}-wal`), "utf8")).toBe("moved already");
205
205
  });
206
206
  });
207
+
208
+ describe("0002_request_place — where a request came from, never its address (task 2123)", () => {
209
+ const placed = (path = join(dir, METRICS_DB)) => {
210
+ const db = new Database(path, { readonly: true });
211
+ try {
212
+ return db.query("SELECT route, country, region, city FROM request_metrics ORDER BY ts").all();
213
+ } finally {
214
+ db.close();
215
+ }
216
+ };
217
+
218
+ test("a fresh corpus has nullable country/region/city and 🔴 no ip column", () => {
219
+ const m = track(openRequestMetrics(dir));
220
+ const columns = (m.db.query("PRAGMA table_info(request_metrics)").all() as { name: string; notnull: number }[]);
221
+ const byName = new Map(columns.map((c) => [c.name, c]));
222
+ for (const c of ["country", "region", "city"]) expect(byName.get(c)?.notnull).toBe(0);
223
+ expect(byName.has("ip")).toBe(false);
224
+ expect(JSON.stringify(m.db.query("SELECT name FROM _migrations").all())).toContain("0002_request_place");
225
+ });
226
+
227
+ test("a live corpus from before 0002 is UPGRADED in place — not archived, its rows kept and read as unplaced", async () => {
228
+ // Exactly what 4.21/4.22 left on disk: application_id stamped, 0001 applied, rows in it.
229
+ const first = track(openRequestMetrics(dir));
230
+ first.db.run("ALTER TABLE request_metrics DROP COLUMN country");
231
+ first.db.run("ALTER TABLE request_metrics DROP COLUMN region");
232
+ first.db.run("ALTER TABLE request_metrics DROP COLUMN city");
233
+ first.db.run("DELETE FROM _migrations WHERE name = '0002_request_place'");
234
+ first.db.run("INSERT INTO request_metrics (id, ts, method, route, status) VALUES ('old', 1, 'GET', '/before', 200)");
235
+ first.sink.stop();
236
+ first.db.close();
237
+ opened.splice(opened.indexOf(first), 1);
238
+
239
+ const second = track(openRequestMetrics(dir, { locate: () => ({ country: "IE", city: "Dublin" }) }));
240
+ expect(second.archived).toBeNull();
241
+ await appOver(second).fetch(new Request("http://app.test/healthz"));
242
+ second.sink.flush();
243
+ expect(placed()).toEqual([
244
+ { route: "/before", country: null, region: null, city: null },
245
+ { route: "/healthz", country: "IE", region: null, city: "Dublin" },
246
+ ]);
247
+ });
248
+
249
+ test("idempotent: a column already there (hand-added) is left alone and the step still records", () => {
250
+ const db = new Database(join(dir, METRICS_DB), { create: true });
251
+ db.run(`CREATE TABLE request_metrics (id TEXT PRIMARY KEY, ts INTEGER NOT NULL, method TEXT, route TEXT,
252
+ status INTEGER, duration_ms REAL, bytes_out INTEGER, user_id TEXT, country TEXT)`);
253
+ db.run(`PRAGMA application_id = ${0x4346524d}`); // the writer's marker, stamped after page 1 exists
254
+ db.run("CREATE TABLE _migrations (name TEXT PRIMARY KEY, applied_at TEXT NOT NULL)");
255
+ db.run("INSERT INTO _migrations VALUES ('0001_request_metrics', 'x')");
256
+ db.close();
257
+ const m = track(openRequestMetrics(dir));
258
+ expect(m.archived).toBeNull();
259
+ const names = (m.db.query("PRAGMA table_info(request_metrics)").all() as { name: string }[]).map((c) => c.name);
260
+ expect(names.filter((n) => n === "country")).toHaveLength(1);
261
+ expect(names).toEqual(expect.arrayContaining(["region", "city"]));
262
+ });
263
+
264
+ test("the default places a tunnelled request by its cf-ipcountry header; a direct one is unplaced", async () => {
265
+ const m = track(openRequestMetrics(dir));
266
+ const app = appOver(m);
267
+ await app.fetch(new Request("http://app.test/healthz", { headers: { "cf-ipcountry": "CA" } }));
268
+ await app.fetch(new Request("http://app.test/healthz"));
269
+ m.sink.flush();
270
+ expect(placed().map((r) => (r as { country: string | null }).country)).toEqual(["CA", null]);
271
+ });
272
+
273
+ test("openRequestMetricsFromEnv passes the locator through", async () => {
274
+ const m = track(openRequestMetricsFromEnv("app", { APP_DATA_DIR: dir }, { locate: () => ({ country: "FR" }) }));
275
+ await appOver(m).fetch(new Request("http://app.test/healthz"));
276
+ m.sink.flush();
277
+ expect(placed()).toEqual([{ route: "/healthz", country: "FR", region: null, city: null }]);
278
+ });
279
+ });
@@ -63,6 +63,8 @@ import { closeSync, existsSync, mkdirSync, openSync, readSync, renameSync, statS
63
63
  import { join } from "node:path";
64
64
  import { applyCcPragmas } from "cwip/sqlite";
65
65
  import type { MiddlewareHandler } from "hono";
66
+ import { PLACE_COLUMNS } from "../metrics/metricsBuffer.js";
67
+ import type { RequestLocator } from "../metrics/requestPlace.js";
66
68
  import { createTelemetrySink, type TelemetrySink } from "../metrics/telemetrySink.js";
67
69
  import { requestLogger } from "../middleware/requestLogger.js";
68
70
  import { runMigrations } from "../migration/runner.js";
@@ -97,6 +99,21 @@ const REQUEST_METRICS_MIGRATIONS: readonly Migration[] = [
97
99
  db.run("CREATE INDEX idx_request_metrics_ts ON request_metrics (ts)");
98
100
  },
99
101
  },
102
+ {
103
+ // 4.23.0, task 2123: where a request came from — a PLACE, never the address (no `ip`
104
+ // column, ever; `../metrics/requestPlace.ts`). Nullable, so every row written before
105
+ // this reads as unplaced. Idempotent beyond the runner's once-only record: a column
106
+ // already present (hand-added, or a half-applied copy of this step) is left alone.
107
+ name: "0002_request_place",
108
+ up(db) {
109
+ const have = new Set(
110
+ (db.query("PRAGMA table_info(request_metrics)").all() as { name: string }[]).map((c) => c.name),
111
+ );
112
+ for (const column of PLACE_COLUMNS) {
113
+ if (!have.has(column)) db.run(`ALTER TABLE request_metrics ADD COLUMN ${column} TEXT`);
114
+ }
115
+ },
116
+ },
100
117
  ];
101
118
 
102
119
  /** The `application_id` stamped in a SQLite file's header, or null when it has no header yet. */
@@ -161,8 +178,17 @@ export interface RequestMetrics {
161
178
  archived: string | null;
162
179
  }
163
180
 
181
+ export interface RequestMetricsOptions {
182
+ /**
183
+ * Place each request (`{ country, region?, city? }` or `null`) — never an address; the raw IP
184
+ * is not stored. Default: the edge's own answer, `locateFromCloudflare` (the `cf-ipcountry`
185
+ * header every tunnelled request carries). station passes its DB-IP reader here.
186
+ */
187
+ locate?: RequestLocator;
188
+ }
189
+
164
190
  /** Open the corpus in `dir` and build the logger that writes into it. */
165
- export function openRequestMetrics(dir: string): RequestMetrics {
191
+ export function openRequestMetrics(dir: string, options: RequestMetricsOptions = {}): RequestMetrics {
166
192
  const { db, archived } = openMetricsDb(dir);
167
193
  const inner = createTelemetrySink({ db });
168
194
  // 🔴 No user id — see the header. Nulled here so no context value can reach the row.
@@ -175,7 +201,10 @@ export function openRequestMetrics(dir: string): RequestMetrics {
175
201
  },
176
202
  };
177
203
  // `null` database: the logger writes through `sink` only, and no `event_logs`.
178
- const middleware = requestLogger(null, { sink }) as unknown as MiddlewareHandler;
204
+ const middleware = requestLogger(null, {
205
+ sink,
206
+ ...(options.locate ? { locate: options.locate } : {}),
207
+ }) as unknown as MiddlewareHandler;
179
208
  return { db, sink, middleware, archived };
180
209
  }
181
210
 
@@ -185,11 +214,15 @@ export function openRequestMetrics(dir: string): RequestMetrics {
185
214
  * records nothing rather than appending a developer's clicks to the live corpus.
186
215
  * Never throws: a failure is one stderr line and an app that serves unmeasured.
187
216
  */
188
- export function openRequestMetricsFromEnv(name: string, env: NodeJS.ProcessEnv): RequestMetrics | null {
217
+ export function openRequestMetricsFromEnv(
218
+ name: string,
219
+ env: NodeJS.ProcessEnv,
220
+ options: RequestMetricsOptions = {},
221
+ ): RequestMetrics | null {
189
222
  const dir = env.APP_DATA_DIR?.trim();
190
223
  if (!dir || isTestRuntime(env)) return null;
191
224
  try {
192
- const metrics = openRequestMetrics(dir);
225
+ const metrics = openRequestMetrics(dir, options);
193
226
  if (metrics.archived) {
194
227
  console.log(`[${name}] request metrics: archived the inherited corpus to ${metrics.archived}`);
195
228
  }
@@ -25,3 +25,8 @@ export {
25
25
  type TelemetrySinkOptions,
26
26
  toDataPoint,
27
27
  } from './metrics/telemetrySink.js';
28
+ export {
29
+ locateFromCloudflare,
30
+ type RequestLocator,
31
+ type RequestPlace,
32
+ } from './metrics/requestPlace.js';
@@ -43,6 +43,23 @@ describe('🔴 cursedbelt-server/password — a Worker can import it (task 2129)
43
43
  });
44
44
  });
45
45
 
46
+ describe('🔴 cursedbelt-server/worker-argon2 — it exists to be imported by a Worker', () => {
47
+ test('reaches no `bun:` module at runtime; `Bun.file` is touched only on the Bun-only path branch', () => {
48
+ const { files, bunImports } = runtimeGraph(join(import.meta.dir, 'worker-argon2', 'index.ts'));
49
+ expect(bunImports).toEqual([]);
50
+ // Not vacuous: the loader and the shim are both in the graph.
51
+ expect(files.some((f) => f.endsWith('worker-argon2/argon2.ts'))).toBe(true);
52
+ expect(files.some((f) => f.endsWith('worker-argon2/password.ts'))).toBe(true);
53
+ });
54
+
55
+ test("its export map's `import` is the real module, not the Bun-only refusal", () => {
56
+ const pkg = JSON.parse(readFileSync(join(import.meta.dir, '..', '..', 'package.json'), 'utf8')) as {
57
+ exports: Record<string, { import?: string }>;
58
+ };
59
+ expect(pkg.exports['./worker-argon2']?.import).toBe('./dist/server/worker-argon2/index.js');
60
+ });
61
+ });
62
+
46
63
  describe('cursedbelt-server/telemetry', () => {
47
64
  test('🔴 reaches no `bun:` module at runtime — a Worker can import it', () => {
48
65
  const { files, bunImports } = runtimeGraph(join(import.meta.dir, 'telemetry.ts'));
@@ -0,0 +1,85 @@
1
+ /**
2
+ * argon2id as WebAssembly the Worker was HANDED, never WebAssembly it compiles.
3
+ *
4
+ * ## 🔴 Why this file exists: one unlock was 8.5–15 seconds of billed CPU
5
+ *
6
+ * `collections` averaged **489 CPU-ms per request on 2026-09-22 with a 15.2 s p99** (Cloudflare
7
+ * GraphQL, `workersInvocationsAdaptive`). Every second of it was `POST /__lock/unlock`: a real
8
+ * `wrangler tail` of the stage walk on 2026-09-23 read **8,519 CPU-ms and 10.4 s wall** for that
9
+ * one request and 10–38 ms for each of the other eleven. The lock verifies every account's
10
+ * argon2id hash (no early exit — `cursedbelt-server/master-lock` explains the timing oracle) plus
11
+ * a timing equalizer, so production's two accounts are three to four derivations per unlock, and
12
+ * `@noble/hashes`' pure-JS argon2id costs ~2.8 s each on workerd — five times the 562 ms it
13
+ * costs on this Mac. The owner waited ten seconds at the lock page, and a third account would
14
+ * have put the unlock within reach of the Worker's 30 s CPU ceiling, where it is killed and
15
+ * reads as a wrong password.
16
+ *
17
+ * ## 🔴 Why this shape: workerd refuses to COMPILE WebAssembly, not to RUN it
18
+ *
19
+ * `hash-wasm` failed here on 2026-09-18 with `Wasm code generation disallowed by embedder`
20
+ * because it compiles bytes it carries inline (`./password.ts` has the whole story). A
21
+ * `.wasm` file that is a STATIC import is different: wrangler bundles it as a
22
+ * `CompiledWasm` module and the runtime hands this code a ready `WebAssembly.Module`, which
23
+ * `WebAssembly.instantiate(module, imports)` may instantiate. `argon2id` (openpgpjs, MIT) ships
24
+ * its two binaries as plain files and lets the caller do the instantiating, which is exactly the
25
+ * seam a Worker needs. Its default entry inlines base64 and compiles it — so it is never imported.
26
+ *
27
+ * Under Bun (every test, and the Mac) the same import is a PATH STRING, so the bytes are read
28
+ * and compiled here — which Bun allows. `index.spec.ts` drives the Worker's branch too, with
29
+ * `WebAssembly.compile` made to throw exactly as workerd throws.
30
+ *
31
+ * ## Why it lives in `cursedbelt-server` (4.24.0)
32
+ *
33
+ * It was born in `apps/collections/worker/argon2.ts` (commit 7996ded) while `apps/vault` and
34
+ * `apps/patterns` still carried the pure-JS copy, so every sign-in there billed seconds. One copy
35
+ * here, `argon2id` a pinned dependency of this package: its two `.wasm` files ship in its own
36
+ * `files`, and wrangler — which resolves this subpath's `import` condition, `dist/…/argon2.js`,
37
+ * where tsc leaves the bare `argon2id/dist/*.wasm` specifiers untouched — bundles them from
38
+ * `node_modules` as `CompiledWasm`. `index.spec.ts` asserts `dist` still carries them that way.
39
+ */
40
+ import noSimdWasm from "argon2id/dist/no-simd.wasm";
41
+ import simdWasm from "argon2id/dist/simd.wasm";
42
+ import setupWasm, { type computeHash } from "argon2id/lib/setup.js";
43
+
44
+ /** What a `.wasm` import is: a compiled module under wrangler, a file path under Bun. */
45
+ export type WasmImport = WebAssembly.Module | string;
46
+
47
+ /**
48
+ * Turn one `.wasm` import into an instance. 🔴 A `Module` is instantiated and NEVER compiled —
49
+ * that is the whole workerd rule — and only a path (Bun) is read and compiled.
50
+ */
51
+ export async function instantiateWasm(
52
+ source: WasmImport,
53
+ imports: WebAssembly.Imports,
54
+ ): Promise<WebAssembly.WebAssemblyInstantiatedSource> {
55
+ const module =
56
+ typeof source === "string" ? await WebAssembly.compile(await Bun.file(source).arrayBuffer()) : source;
57
+ const instance = await WebAssembly.instantiate(module, imports);
58
+ return { module, instance };
59
+ }
60
+
61
+ /** Build a hasher from the two binaries — SIMD first, the plain one if SIMD will not load. */
62
+ export function loadArgon2(simd: WasmImport = simdWasm, noSimd: WasmImport = noSimdWasm): Promise<computeHash> {
63
+ return setupWasm(
64
+ (imports) => instantiateWasm(simd, imports),
65
+ (imports) => instantiateWasm(noSimd, imports),
66
+ );
67
+ }
68
+
69
+ /*
70
+ * 🔴 ONE hasher per isolate, built on first use. Not request state — it holds no password and no
71
+ * result between calls (the library clears its memory after each) — so this is the one kind of
72
+ * module-scope value a Worker may keep. The reason to keep it is MEMORY: each
73
+ * instance owns a 65 MB `WebAssembly.Memory` that can never shrink, and an unlock runs three or
74
+ * four derivations back to back; one per call would stack up to four of them against a 128 MB
75
+ * isolate before a collector ran. A load that FAILS is not cached, so the next unlock retries.
76
+ */
77
+ let shared: Promise<computeHash> | null = null;
78
+
79
+ export function argon2(): Promise<computeHash> {
80
+ shared ??= loadArgon2().catch((error: unknown) => {
81
+ shared = null;
82
+ throw error;
83
+ });
84
+ return shared;
85
+ }