cursedbelt-server 4.17.0 → 4.18.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.
@@ -139,6 +139,16 @@ export async function pullD1ToSqlite(options) {
139
139
  // `exec`, not `run`: each is many statements and `run` would execute only the first,
140
140
  // leaving a file with a schema and no rows that looks perfectly valid.
141
141
  db.exec(options.schemaSql);
142
+ // 🔴 A table D1 holds that the schema does not declare is created from D1's OWN DDL, never
143
+ // dropped: measured on music's first post-cutover pull (2026-09-23), `art_lookups` — a
144
+ // cache its art job creates where it first needs it — made the whole pull refuse with
145
+ // `no such table`, because its rows were exported and its table was not in the schema.
146
+ const have = new Set(db.query("SELECT name FROM sqlite_master WHERE type = 'table'").all().map((r) => r.name));
147
+ for (const row of rows) {
148
+ if (!tables.includes(row.name) || have.has(row.name) || !row.sql)
149
+ continue;
150
+ db.exec(row.sql.replace(/^\s*CREATE\s+TABLE\s+(?!IF\s+NOT\s+EXISTS)/i, 'CREATE TABLE IF NOT EXISTS '));
151
+ }
142
152
  db.exec(sql);
143
153
  if (options.afterLoadSql)
144
154
  db.exec(options.afterLoadSql);
@@ -0,0 +1,19 @@
1
+ /**
2
+ * `cursedbelt-server/telemetry` — the telemetry seam, from a WORKER.
3
+ *
4
+ * `requestLogger` and `createTelemetrySink` are built for exactly one host that most needs them:
5
+ * a Worker, where a row per request in D1 is the one line on Cloudflare's price list with a
6
+ * dollar-scale overage and Workers Analytics Engine is the purpose-built sink
7
+ * (`./metrics/telemetrySink.ts`). Until 2026-09-23 the only door to them was
8
+ * `cursedbelt-server/middleware`, whose export map sends every non-Bun importer to
9
+ * `_bunOnly.js` because the barrel carries Bun-only middleware beside them — so `wrangler`
10
+ * refused the import (`No matching export … for import "requestLogger"`, measured on `music`'s
11
+ * first attempt to mount it) and the seam could reach every host except the one it was for.
12
+ *
13
+ * Everything here is runtime-safe on workerd: `bun:sqlite` appears only as a TYPE (a Mac host
14
+ * passes its `Database`; a Worker passes `null` and an `analytics` binding).
15
+ * `telemetryIsWorkerSafe.spec.ts` walks this file's runtime import graph and fails on any
16
+ * `bun:` import, so it stays that way.
17
+ */
18
+ export { type RequestLoggerOpts, requestLogger } from './middleware/requestLogger.js';
19
+ export { type AnalyticsEngineDataset, createTelemetrySink, TELEMETRY_POINT_VERSION, type TelemetryEvent, type TelemetrySink, type TelemetrySinkOptions, toDataPoint, } from './metrics/telemetrySink.js';
@@ -0,0 +1,19 @@
1
+ /**
2
+ * `cursedbelt-server/telemetry` — the telemetry seam, from a WORKER.
3
+ *
4
+ * `requestLogger` and `createTelemetrySink` are built for exactly one host that most needs them:
5
+ * a Worker, where a row per request in D1 is the one line on Cloudflare's price list with a
6
+ * dollar-scale overage and Workers Analytics Engine is the purpose-built sink
7
+ * (`./metrics/telemetrySink.ts`). Until 2026-09-23 the only door to them was
8
+ * `cursedbelt-server/middleware`, whose export map sends every non-Bun importer to
9
+ * `_bunOnly.js` because the barrel carries Bun-only middleware beside them — so `wrangler`
10
+ * refused the import (`No matching export … for import "requestLogger"`, measured on `music`'s
11
+ * first attempt to mount it) and the seam could reach every host except the one it was for.
12
+ *
13
+ * Everything here is runtime-safe on workerd: `bun:sqlite` appears only as a TYPE (a Mac host
14
+ * passes its `Database`; a Worker passes `null` and an `analytics` binding).
15
+ * `telemetryIsWorkerSafe.spec.ts` walks this file's runtime import graph and fails on any
16
+ * `bun:` import, so it stays that way.
17
+ */
18
+ export { requestLogger } from './middleware/requestLogger.js';
19
+ export { createTelemetrySink, TELEMETRY_POINT_VERSION, toDataPoint, } from './metrics/telemetrySink.js';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cursedbelt-server",
3
- "version": "4.17.0",
3
+ "version": "4.18.0",
4
4
  "license": "ISC",
5
5
  "type": "module",
6
6
  "description": "The app-facing Bun/Hono server tier of the cursedbelt split — storage, sharing, activity, guard, sync. React-free; cursedbelt-core below it.",
@@ -163,6 +163,12 @@
163
163
  "source": "./src/server/master-lock/index.ts",
164
164
  "import": "./dist/server/master-lock/index.js"
165
165
  },
166
+ "./telemetry": {
167
+ "types": "./dist/server/telemetry.d.ts",
168
+ "bun": "./src/server/telemetry.ts",
169
+ "source": "./src/server/telemetry.ts",
170
+ "import": "./dist/server/telemetry.js"
171
+ },
166
172
  "./middleware": {
167
173
  "types": "./dist/server/middleware/index.d.ts",
168
174
  "bun": "./src/server/middleware/index.ts",
@@ -144,6 +144,26 @@ describe('pulling D1 down — table by table, because a whole-database export re
144
144
  expect(hits.n).toBe(1);
145
145
  });
146
146
 
147
+ test('🔴 a table D1 holds that the schema does not declare is created from D1\'s own DDL', async () => {
148
+ const dir = scratch();
149
+ const master = [...MASTER, { name: 'art_lookups', sql: 'CREATE TABLE art_lookups (key TEXT PRIMARY KEY, art_key TEXT)' }];
150
+ const run: Wrangler = (args) => {
151
+ if (args[1] === 'execute') return { code: 0, stdout: JSON.stringify([{ results: master }]), stderr: '' };
152
+ const table = args[args.indexOf('--table') + 1] as string;
153
+ const out = args[args.indexOf('--output') + 1] as string;
154
+ writeFileSync(
155
+ out,
156
+ table === 'items' ? "INSERT INTO items (id) VALUES ('i1');" : table === 'art_lookups' ? "INSERT INTO art_lookups VALUES ('k', 'art/1');" : '',
157
+ );
158
+ return { code: 0, stdout: '', stderr: '' };
159
+ };
160
+ await pull(dir, run);
161
+ const db = new Database(join(dir, 'pulled.sqlite'), { readonly: true });
162
+ const row = db.query('select art_key from art_lookups').get() as { art_key: string } | null;
163
+ db.close();
164
+ expect(row?.art_key).toBe('art/1');
165
+ });
166
+
147
167
  test('🔴 a database without the required table is refused as the wrong one', async () => {
148
168
  const dir = scratch();
149
169
  const { run } = fake(() => '');
@@ -176,6 +176,17 @@ export async function pullD1ToSqlite(options: PullD1Options): Promise<void> {
176
176
  // `exec`, not `run`: each is many statements and `run` would execute only the first,
177
177
  // leaving a file with a schema and no rows that looks perfectly valid.
178
178
  db.exec(options.schemaSql);
179
+ // 🔴 A table D1 holds that the schema does not declare is created from D1's OWN DDL, never
180
+ // dropped: measured on music's first post-cutover pull (2026-09-23), `art_lookups` — a
181
+ // cache its art job creates where it first needs it — made the whole pull refuse with
182
+ // `no such table`, because its rows were exported and its table was not in the schema.
183
+ const have = new Set(
184
+ (db.query("SELECT name FROM sqlite_master WHERE type = 'table'").all() as { name: string }[]).map((r) => r.name),
185
+ );
186
+ for (const row of rows) {
187
+ if (!tables.includes(row.name) || have.has(row.name) || !row.sql) continue;
188
+ db.exec(row.sql.replace(/^\s*CREATE\s+TABLE\s+(?!IF\s+NOT\s+EXISTS)/i, 'CREATE TABLE IF NOT EXISTS '));
189
+ }
179
190
  db.exec(sql);
180
191
  if (options.afterLoadSql) db.exec(options.afterLoadSql);
181
192
  } finally {
@@ -0,0 +1,27 @@
1
+ /**
2
+ * `cursedbelt-server/telemetry` — the telemetry seam, from a WORKER.
3
+ *
4
+ * `requestLogger` and `createTelemetrySink` are built for exactly one host that most needs them:
5
+ * a Worker, where a row per request in D1 is the one line on Cloudflare's price list with a
6
+ * dollar-scale overage and Workers Analytics Engine is the purpose-built sink
7
+ * (`./metrics/telemetrySink.ts`). Until 2026-09-23 the only door to them was
8
+ * `cursedbelt-server/middleware`, whose export map sends every non-Bun importer to
9
+ * `_bunOnly.js` because the barrel carries Bun-only middleware beside them — so `wrangler`
10
+ * refused the import (`No matching export … for import "requestLogger"`, measured on `music`'s
11
+ * first attempt to mount it) and the seam could reach every host except the one it was for.
12
+ *
13
+ * Everything here is runtime-safe on workerd: `bun:sqlite` appears only as a TYPE (a Mac host
14
+ * passes its `Database`; a Worker passes `null` and an `analytics` binding).
15
+ * `telemetryIsWorkerSafe.spec.ts` walks this file's runtime import graph and fails on any
16
+ * `bun:` import, so it stays that way.
17
+ */
18
+ export { type RequestLoggerOpts, requestLogger } from './middleware/requestLogger.js';
19
+ export {
20
+ type AnalyticsEngineDataset,
21
+ createTelemetrySink,
22
+ TELEMETRY_POINT_VERSION,
23
+ type TelemetryEvent,
24
+ type TelemetrySink,
25
+ type TelemetrySinkOptions,
26
+ toDataPoint,
27
+ } from './metrics/telemetrySink.js';
@@ -0,0 +1,42 @@
1
+ import { describe, expect, test } from 'bun:test';
2
+ import { readFileSync } from 'node:fs';
3
+ import { dirname, join, resolve } from 'node:path';
4
+
5
+ /**
6
+ * `cursedbelt-server/telemetry` must be importable by a Worker: nothing reachable from it at
7
+ * RUNTIME may import `bun:*`. A `import type` / `export type` is erased and does not count.
8
+ */
9
+ const RUNTIME_IMPORT = /^\s*(?:import|export)\s+(?!type\b)(?:[^'"]*?from\s+)?['"]([^'"]+)['"]/gm;
10
+
11
+ function runtimeGraph(entry: string): { files: string[]; bunImports: string[] } {
12
+ const seen = new Set<string>();
13
+ const bunImports: string[] = [];
14
+ const walk = (file: string): void => {
15
+ if (seen.has(file)) return;
16
+ seen.add(file);
17
+ const source = readFileSync(file, 'utf8');
18
+ for (const match of source.matchAll(RUNTIME_IMPORT)) {
19
+ const spec = match[1] as string;
20
+ if (spec.startsWith('bun:')) bunImports.push(`${file.split('/src/')[1]} → ${spec}`);
21
+ if (spec.startsWith('.')) walk(resolve(dirname(file), spec.replace(/\.js$/, '.ts')));
22
+ }
23
+ };
24
+ walk(entry);
25
+ return { files: [...seen], bunImports };
26
+ }
27
+
28
+ describe('cursedbelt-server/telemetry', () => {
29
+ test('🔴 reaches no `bun:` module at runtime — a Worker can import it', () => {
30
+ const { files, bunImports } = runtimeGraph(join(import.meta.dir, 'telemetry.ts'));
31
+ expect(bunImports).toEqual([]);
32
+ // Not vacuous: the logger and the sink are both in the graph.
33
+ expect(files.some((f) => f.endsWith('requestLogger.ts'))).toBe(true);
34
+ expect(files.some((f) => f.endsWith('telemetrySink.ts'))).toBe(true);
35
+ });
36
+
37
+ test('the walk goes red on the shape it exists for', () => {
38
+ const regex = new RegExp(RUNTIME_IMPORT.source, RUNTIME_IMPORT.flags);
39
+ const found = [..."import { Database } from 'bun:sqlite';\nimport type { X } from 'bun:sqlite';".matchAll(regex)].map((m) => m[1]);
40
+ expect(found).toEqual(['bun:sqlite']);
41
+ });
42
+ });