cursedbelt-server 4.26.1 → 4.28.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 (68) hide show
  1. package/dist/server/activity/d1.d.ts +70 -0
  2. package/dist/server/activity/d1.js +68 -0
  3. package/dist/server/activity/migrations.d.ts +1 -3
  4. package/dist/server/activity/migrations.js +7 -32
  5. package/dist/server/activity/query.d.ts +123 -0
  6. package/dist/server/activity/query.js +171 -0
  7. package/dist/server/activity/schema.d.ts +13 -0
  8. package/dist/server/activity/schema.js +43 -0
  9. package/dist/server/activity/shareSource.d.ts +1 -1
  10. package/dist/server/activity/shareSource.js +1 -1
  11. package/dist/server/activity/store.d.ts +4 -62
  12. package/dist/server/activity/store.js +14 -145
  13. package/dist/server/engagement/d1.d.ts +67 -0
  14. package/dist/server/engagement/d1.js +81 -0
  15. package/dist/server/engagement/manifestPlaces.d.ts +9 -0
  16. package/dist/server/engagement/manifestPlaces.js +17 -0
  17. package/dist/server/engagement/places.js +2 -18
  18. package/dist/server/notifications/d1.d.ts +80 -0
  19. package/dist/server/notifications/d1.js +226 -0
  20. package/dist/server/notifications/service.d.ts +2 -20
  21. package/dist/server/notifications/types.d.ts +27 -0
  22. package/dist/server/notifications/types.js +1 -0
  23. package/dist/server/sharing/contract.d.ts +94 -0
  24. package/dist/server/sharing/contract.js +3 -0
  25. package/dist/server/sharing/d1.d.ts +77 -0
  26. package/dist/server/sharing/d1.js +309 -0
  27. package/dist/server/sharing/index.d.ts +1 -0
  28. package/dist/server/sharing/migrations.d.ts +1 -4
  29. package/dist/server/sharing/migrations.js +7 -72
  30. package/dist/server/sharing/router.d.ts +8 -4
  31. package/dist/server/sharing/router.js +7 -5
  32. package/dist/server/sharing/schema.d.ts +19 -0
  33. package/dist/server/sharing/schema.js +88 -0
  34. package/dist/server/sharing/sql.js +1 -1
  35. package/dist/server/sharing/statements.d.ts +116 -0
  36. package/dist/server/sharing/statements.js +139 -0
  37. package/dist/server/sharing/store.d.ts +3 -28
  38. package/dist/server/sharing/store.js +32 -105
  39. package/docs/activity.md +7 -0
  40. package/docs/engagement.md +6 -0
  41. package/docs/notifications.md +9 -0
  42. package/package.json +25 -1
  43. package/src/server/activity/d1.spec.ts +169 -0
  44. package/src/server/activity/d1.ts +172 -0
  45. package/src/server/activity/migrations.ts +6 -40
  46. package/src/server/activity/query.ts +283 -0
  47. package/src/server/activity/schema.ts +45 -0
  48. package/src/server/activity/shareSource.ts +2 -2
  49. package/src/server/activity/store.ts +45 -257
  50. package/src/server/engagement/d1.spec.ts +102 -0
  51. package/src/server/engagement/d1.ts +141 -0
  52. package/src/server/engagement/manifestPlaces.ts +24 -0
  53. package/src/server/engagement/places.ts +2 -16
  54. package/src/server/notifications/d1.spec.ts +179 -0
  55. package/src/server/notifications/d1.ts +334 -0
  56. package/src/server/notifications/service.ts +7 -23
  57. package/src/server/notifications/types.ts +29 -0
  58. package/src/server/sharing/contract.ts +109 -0
  59. package/src/server/sharing/d1.spec.ts +242 -0
  60. package/src/server/sharing/d1.ts +430 -0
  61. package/src/server/sharing/index.ts +3 -0
  62. package/src/server/sharing/migrations.ts +12 -85
  63. package/src/server/sharing/router.ts +21 -11
  64. package/src/server/sharing/schema.ts +90 -0
  65. package/src/server/sharing/sql.ts +1 -1
  66. package/src/server/sharing/statements.ts +213 -0
  67. package/src/server/sharing/store.ts +46 -221
  68. package/src/server/telemetryIsWorkerSafe.spec.ts +64 -0
@@ -49,25 +49,25 @@ import {
49
49
  strongerAccess,
50
50
  } from "cursedbelt-core/sharing/model";
51
51
  import { runMigrations } from "../migration/runner.js";
52
- import {
53
- SHARE_EVENTS_TABLE,
54
- SHARE_GRANTS_TABLE,
55
- SHARE_GROUPS_TABLE,
56
- SHARE_GROUP_MEMBERS_TABLE,
57
- SHARE_MIGRATIONS,
58
- } from "./migrations.js";
52
+ import type { GrantInput, RevokeInput, ShareEventQuery, ShareTarget } from "./contract.js";
53
+ import { SHARE_MIGRATIONS } from "./migrations.js";
59
54
  import { sharedResourceSql } from "./sql.js";
60
-
61
- /** Shorthand for the common subject. `group(id)` is its sibling. */
62
- export const user = (id: string): ShareSubject => ({ type: "user", id });
63
- export const group = (id: string): ShareSubject => ({ type: "group", id });
64
-
65
- /** A resource within the store's own app — the `app` half is supplied by the
66
- * store, so a caller cannot mistakenly write a grant into another app's rows. */
67
- export interface ShareTarget {
68
- kind: string;
69
- id: string;
70
- }
55
+ import {
56
+ type EventRow,
57
+ eventLimit,
58
+ eventsWhere,
59
+ type GrantRow,
60
+ type GroupMemberRow,
61
+ type GroupRow,
62
+ SHARE_SQL,
63
+ toEvent,
64
+ toGrant,
65
+ } from "./statements.js";
66
+
67
+ // The contract has no driver in it, so `./d1.ts` (which runs on `workerd`) shares it without
68
+ // reaching this file. Re-exported so `cursedbelt-server/sharing` is unchanged.
69
+ export { group, user } from "./contract.js";
70
+ export type { GrantInput, RevokeInput, ShareEventQuery, ShareTarget } from "./contract.js";
71
71
 
72
72
  export interface ShareStoreOptions {
73
73
  db: Database;
@@ -93,79 +93,6 @@ export interface ShareStoreOptions {
93
93
  now?: () => string;
94
94
  }
95
95
 
96
- interface GrantRow {
97
- app: string;
98
- kind: string;
99
- resource_id: string;
100
- subject_type: "user" | "group";
101
- subject_id: string;
102
- level: ShareLevel;
103
- granted_by: string;
104
- granted_at: string;
105
- note: string;
106
- }
107
-
108
- interface EventRow {
109
- event_id: number;
110
- at: string;
111
- action: ShareAction;
112
- app: string | null;
113
- kind: string | null;
114
- resource_id: string | null;
115
- subject_type: "user" | "group";
116
- subject_id: string;
117
- level: ShareLevel | null;
118
- previous_level: ShareLevel | null;
119
- actor: string;
120
- note: string;
121
- }
122
-
123
- const toGrant = (row: GrantRow): ShareGrant => ({
124
- resource: { app: row.app, kind: row.kind, id: row.resource_id },
125
- subject: { type: row.subject_type, id: row.subject_id },
126
- level: row.level,
127
- grantedBy: row.granted_by,
128
- grantedAt: row.granted_at,
129
- note: row.note,
130
- });
131
-
132
- const toEvent = (row: EventRow): ShareEvent => ({
133
- id: row.event_id,
134
- at: row.at,
135
- action: row.action,
136
- resource:
137
- row.app === null || row.kind === null || row.resource_id === null
138
- ? null
139
- : { app: row.app, kind: row.kind, id: row.resource_id },
140
- subject: { type: row.subject_type, id: row.subject_id },
141
- level: row.level,
142
- previousLevel: row.previous_level,
143
- actor: row.actor,
144
- note: row.note,
145
- });
146
-
147
- export interface GrantInput extends ShareTarget {
148
- subject: ShareSubject;
149
- level: ShareLevel;
150
- grantedBy: string;
151
- note?: string;
152
- }
153
-
154
- export interface RevokeInput extends ShareTarget {
155
- subject: ShareSubject;
156
- revokedBy: string;
157
- note?: string;
158
- }
159
-
160
- export interface ShareEventQuery {
161
- kind?: string;
162
- id?: string;
163
- subject?: ShareSubject;
164
- /** Newest first, capped. Default 200 — a panel shows a page, and an unbounded
165
- * audit read on a busy resource is the query that times out in production. */
166
- limit?: number;
167
- }
168
-
169
96
  export type ShareStore = ReturnType<typeof createShareStore>;
170
97
 
171
98
  /**
@@ -204,11 +131,7 @@ export function createShareStore(options: ShareStoreOptions) {
204
131
  note?: string;
205
132
  at: string;
206
133
  }): ShareEvent => {
207
- db.query(
208
- `INSERT INTO ${SHARE_EVENTS_TABLE}
209
- (at, action, app, kind, resource_id, subject_type, subject_id, level, previous_level, actor, note)
210
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
211
- ).run(
134
+ db.query(SHARE_SQL.insertEvent).run(
212
135
  input.at,
213
136
  input.action,
214
137
  input.resource?.app ?? null,
@@ -242,10 +165,7 @@ export function createShareStore(options: ShareStoreOptions) {
242
165
 
243
166
  const readGrant = (target: ShareTarget, subject: ShareSubject): ShareGrant | null => {
244
167
  const row = db
245
- .query(
246
- `SELECT * FROM ${SHARE_GRANTS_TABLE}
247
- WHERE app = ? AND kind = ? AND resource_id = ? AND subject_type = ? AND subject_id = ?`,
248
- )
168
+ .query(SHARE_SQL.selectGrant)
249
169
  .get(app, target.kind, target.id, subject.type, subject.id) as GrantRow | null;
250
170
  return row ? toGrant(row) : null;
251
171
  };
@@ -270,14 +190,7 @@ export function createShareStore(options: ShareStoreOptions) {
270
190
  const events: ShareEvent[] = [];
271
191
  const grant = db.transaction((): ShareGrant => {
272
192
  const existing = readGrant(input, subject);
273
- db.query(
274
- `INSERT INTO ${SHARE_GRANTS_TABLE}
275
- (app, kind, resource_id, subject_type, subject_id, level, granted_by, granted_at, note)
276
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
277
- ON CONFLICT (app, kind, resource_id, subject_type, subject_id)
278
- DO UPDATE SET level = excluded.level, granted_by = excluded.granted_by,
279
- granted_at = excluded.granted_at, note = excluded.note`,
280
- ).run(
193
+ db.query(SHARE_SQL.upsertGrant).run(
281
194
  app,
282
195
  input.kind,
283
196
  input.id,
@@ -324,10 +237,7 @@ export function createShareStore(options: ShareStoreOptions) {
324
237
  const removed = db.transaction((): ShareLevel | null => {
325
238
  const existing = readGrant(input, subject);
326
239
  if (!existing) return null;
327
- db.query(
328
- `DELETE FROM ${SHARE_GRANTS_TABLE}
329
- WHERE app = ? AND kind = ? AND resource_id = ? AND subject_type = ? AND subject_id = ?`,
330
- ).run(app, input.kind, input.id, subject.type, subject.id);
240
+ db.query(SHARE_SQL.deleteGrant).run(app, input.kind, input.id, subject.type, subject.id);
331
241
  events.push(
332
242
  recordEvent({
333
243
  action: "revoke",
@@ -359,12 +269,10 @@ export function createShareStore(options: ShareStoreOptions) {
359
269
  const events: ShareEvent[] = [];
360
270
  const count = db.transaction((): number => {
361
271
  const rows = db
362
- .query(`SELECT * FROM ${SHARE_GRANTS_TABLE} WHERE app = ? AND kind = ? AND resource_id = ?`)
272
+ .query(SHARE_SQL.selectResourceGrants)
363
273
  .all(app, target.kind, target.id) as GrantRow[];
364
274
  if (rows.length === 0) return 0;
365
- db.query(
366
- `DELETE FROM ${SHARE_GRANTS_TABLE} WHERE app = ? AND kind = ? AND resource_id = ?`,
367
- ).run(app, target.kind, target.id);
275
+ db.query(SHARE_SQL.deleteResourceGrants).run(app, target.kind, target.id);
368
276
  for (const row of rows) {
369
277
  events.push(
370
278
  recordEvent({
@@ -392,14 +300,10 @@ export function createShareStore(options: ShareStoreOptions) {
392
300
  const events: ShareEvent[] = [];
393
301
  const count = db.transaction((): number => {
394
302
  const rows = db
395
- .query(
396
- `SELECT * FROM ${SHARE_GRANTS_TABLE} WHERE app = ? AND subject_type = ? AND subject_id = ?`,
397
- )
303
+ .query(SHARE_SQL.selectSubjectGrants)
398
304
  .all(app, normal.type, normal.id) as GrantRow[];
399
305
  if (rows.length === 0) return 0;
400
- db.query(
401
- `DELETE FROM ${SHARE_GRANTS_TABLE} WHERE app = ? AND subject_type = ? AND subject_id = ?`,
402
- ).run(app, normal.type, normal.id);
306
+ db.query(SHARE_SQL.deleteSubjectGrants).run(app, normal.type, normal.id);
403
307
  for (const row of rows) {
404
308
  events.push(
405
309
  recordEvent({
@@ -421,16 +325,7 @@ export function createShareStore(options: ShareStoreOptions) {
421
325
 
422
326
  /** Who can see this record, strongest first then oldest — the panel's read. */
423
327
  listForResource(target: ShareTarget): ShareGrant[] {
424
- const rows = db
425
- .query(
426
- `SELECT * FROM ${SHARE_GRANTS_TABLE}
427
- WHERE app = ? AND kind = ? AND resource_id = ?
428
- -- Strongest first, then oldest. Spelled as a CASE rather than an
429
- -- ORDER BY on level, which happens to work only because 'write'
430
- -- sorts after 'read' and would silently mis-order a third level.
431
- ORDER BY CASE level WHEN 'write' THEN 0 ELSE 1 END, granted_at ASC`,
432
- )
433
- .all(app, target.kind, target.id) as GrantRow[];
328
+ const rows = db.query(SHARE_SQL.listForResource).all(app, target.kind, target.id) as GrantRow[];
434
329
  return rows.map(toGrant);
435
330
  },
436
331
 
@@ -438,13 +333,7 @@ export function createShareStore(options: ShareStoreOptions) {
438
333
  * NOT expanded here; use {@link resourceIdsFor} for the resolved answer. */
439
334
  listForSubject(subject: ShareSubject): ShareGrant[] {
440
335
  const normal = normalized(subject);
441
- const rows = db
442
- .query(
443
- `SELECT * FROM ${SHARE_GRANTS_TABLE}
444
- WHERE app = ? AND subject_type = ? AND subject_id = ?
445
- ORDER BY granted_at DESC`,
446
- )
447
- .all(app, normal.type, normal.id) as GrantRow[];
336
+ const rows = db.query(SHARE_SQL.listForSubject).all(app, normal.type, normal.id) as GrantRow[];
448
337
  return rows.map(toGrant);
449
338
  },
450
339
 
@@ -459,13 +348,7 @@ export function createShareStore(options: ShareStoreOptions) {
459
348
  const subjectId = normalizeSubjectId("user", viewer.subjectId);
460
349
  if (!subjectId) return "none";
461
350
  const rows = db
462
- .query(
463
- `SELECT level FROM ${SHARE_GRANTS_TABLE}
464
- WHERE app = ? AND kind = ? AND resource_id = ?
465
- AND ( (subject_type = 'user' AND subject_id = ?)
466
- OR (subject_type = 'group' AND subject_id IN
467
- (SELECT group_id FROM ${SHARE_GROUP_MEMBERS_TABLE} WHERE subject_id = ?)) )`,
468
- )
351
+ .query(SHARE_SQL.accessFor)
469
352
  .all(app, target.kind, target.id, subjectId, subjectId) as { level: ShareLevel }[];
470
353
  let access: ShareAccess = "none";
471
354
  for (const row of rows) access = strongerAccess(access, row.level);
@@ -479,14 +362,7 @@ export function createShareStore(options: ShareStoreOptions) {
479
362
  if (!subjectId) return [];
480
363
  const levels = levelsMeeting(atLeast);
481
364
  const rows = db
482
- .query(
483
- `SELECT DISTINCT resource_id FROM ${SHARE_GRANTS_TABLE}
484
- WHERE app = ? AND kind = ? AND level IN (${levels.map(() => "?").join(", ")})
485
- AND ( (subject_type = 'user' AND subject_id = ?)
486
- OR (subject_type = 'group' AND subject_id IN
487
- (SELECT group_id FROM ${SHARE_GROUP_MEMBERS_TABLE} WHERE subject_id = ?)) )
488
- ORDER BY resource_id`,
489
- )
365
+ .query(SHARE_SQL.resourceIdsFor(levels.length))
490
366
  .all(app, kind, ...levels, subjectId, subjectId) as { resource_id: string }[];
491
367
  return rows.map((row) => row.resource_id);
492
368
  },
@@ -505,11 +381,7 @@ export function createShareStore(options: ShareStoreOptions) {
505
381
  const at = clock();
506
382
  const events: ShareEvent[] = [];
507
383
  const created = db.transaction((): ShareGroup => {
508
- db.query(
509
- `INSERT INTO ${SHARE_GROUPS_TABLE} (group_id, name, created_by, created_at, note)
510
- VALUES (?, ?, ?, ?, ?)
511
- ON CONFLICT (group_id) DO UPDATE SET name = excluded.name, note = excluded.note`,
512
- ).run(id, input.name, input.createdBy, at, input.note ?? "");
384
+ db.query(SHARE_SQL.upsertGroup).run(id, input.name, input.createdBy, at, input.note ?? "");
513
385
  events.push(
514
386
  recordEvent({
515
387
  action: "group.create",
@@ -538,7 +410,7 @@ export function createShareStore(options: ShareStoreOptions) {
538
410
  const at = clock();
539
411
  const events: ShareEvent[] = [];
540
412
  db.transaction(() => {
541
- db.query(`UPDATE ${SHARE_GROUPS_TABLE} SET name = ? WHERE group_id = ?`).run(name, groupId);
413
+ db.query(SHARE_SQL.renameGroup).run(name, groupId);
542
414
  events.push(
543
415
  recordEvent({
544
416
  action: "group.rename",
@@ -568,14 +440,8 @@ export function createShareStore(options: ShareStoreOptions) {
568
440
  const at = clock();
569
441
  const events: ShareEvent[] = [];
570
442
  db.transaction(() => {
571
- const grants = db
572
- .query(
573
- `SELECT * FROM ${SHARE_GRANTS_TABLE} WHERE app = ? AND subject_type = 'group' AND subject_id = ?`,
574
- )
575
- .all(app, groupId) as GrantRow[];
576
- db.query(
577
- `DELETE FROM ${SHARE_GRANTS_TABLE} WHERE app = ? AND subject_type = 'group' AND subject_id = ?`,
578
- ).run(app, groupId);
443
+ const grants = db.query(SHARE_SQL.selectGroupGrants).all(app, groupId) as GrantRow[];
444
+ db.query(SHARE_SQL.deleteGroupGrants).run(app, groupId);
579
445
  for (const row of grants) {
580
446
  events.push(
581
447
  recordEvent({
@@ -589,7 +455,7 @@ export function createShareStore(options: ShareStoreOptions) {
589
455
  }),
590
456
  );
591
457
  }
592
- db.query(`DELETE FROM ${SHARE_GROUPS_TABLE} WHERE group_id = ?`).run(groupId);
458
+ db.query(SHARE_SQL.deleteGroup).run(groupId);
593
459
  events.push(
594
460
  recordEvent({
595
461
  action: "group.delete",
@@ -606,15 +472,7 @@ export function createShareStore(options: ShareStoreOptions) {
606
472
  },
607
473
 
608
474
  listGroups(): ShareGroup[] {
609
- const rows = db
610
- .query(`SELECT * FROM ${SHARE_GROUPS_TABLE} ORDER BY name`)
611
- .all() as {
612
- group_id: string;
613
- name: string;
614
- created_by: string;
615
- created_at: string;
616
- note: string;
617
- }[];
475
+ const rows = db.query(SHARE_SQL.listGroups).all() as GroupRow[];
618
476
  return rows.map((row) => ({
619
477
  id: row.group_id,
620
478
  name: row.name,
@@ -631,10 +489,7 @@ export function createShareStore(options: ShareStoreOptions) {
631
489
  const at = clock();
632
490
  const events: ShareEvent[] = [];
633
491
  db.transaction(() => {
634
- db.query(
635
- `INSERT INTO ${SHARE_GROUP_MEMBERS_TABLE} (group_id, subject_id, added_by, added_at)
636
- VALUES (?, ?, ?, ?) ON CONFLICT (group_id, subject_id) DO NOTHING`,
637
- ).run(id, member, actor, at);
492
+ db.query(SHARE_SQL.insertMember).run(id, member, actor, at);
638
493
  events.push(
639
494
  recordEvent({
640
495
  action: "group.join",
@@ -656,9 +511,7 @@ export function createShareStore(options: ShareStoreOptions) {
656
511
  const at = clock();
657
512
  const events: ShareEvent[] = [];
658
513
  db.transaction(() => {
659
- db.query(
660
- `DELETE FROM ${SHARE_GROUP_MEMBERS_TABLE} WHERE group_id = ? AND subject_id = ?`,
661
- ).run(id, member);
514
+ db.query(SHARE_SQL.deleteMember).run(id, member);
662
515
  events.push(
663
516
  recordEvent({
664
517
  action: "group.leave",
@@ -676,15 +529,8 @@ export function createShareStore(options: ShareStoreOptions) {
676
529
 
677
530
  listGroupMembers(groupId: string): ShareGroupMember[] {
678
531
  const rows = db
679
- .query(
680
- `SELECT * FROM ${SHARE_GROUP_MEMBERS_TABLE} WHERE group_id = ? ORDER BY subject_id`,
681
- )
682
- .all(normalizeSubjectId("group", groupId)) as {
683
- group_id: string;
684
- subject_id: string;
685
- added_by: string;
686
- added_at: string;
687
- }[];
532
+ .query(SHARE_SQL.listMembers)
533
+ .all(normalizeSubjectId("group", groupId)) as GroupMemberRow[];
688
534
  return rows.map((row) => ({
689
535
  groupId: row.group_id,
690
536
  subjectId: row.subject_id,
@@ -697,11 +543,7 @@ export function createShareStore(options: ShareStoreOptions) {
697
543
  groupIdsFor(subjectId: string): string[] {
698
544
  const member = normalizeSubjectId("user", subjectId);
699
545
  if (!member) return [];
700
- const rows = db
701
- .query(
702
- `SELECT group_id FROM ${SHARE_GROUP_MEMBERS_TABLE} WHERE subject_id = ? ORDER BY group_id`,
703
- )
704
- .all(member) as { group_id: string }[];
546
+ const rows = db.query(SHARE_SQL.groupIdsFor).all(member) as { group_id: string }[];
705
547
  return rows.map((row) => row.group_id);
706
548
  },
707
549
 
@@ -709,28 +551,11 @@ export function createShareStore(options: ShareStoreOptions) {
709
551
 
710
552
  /** The trail, newest first. Filter by resource, by subject, or neither. */
711
553
  events(query: ShareEventQuery = {}): ShareEvent[] {
712
- const clauses = ["app IS NULL OR app = ?"];
713
- const params: (string | number)[] = [app];
714
- if (query.kind !== undefined) {
715
- clauses.push("kind = ?");
716
- params.push(query.kind);
717
- }
718
- if (query.id !== undefined) {
719
- clauses.push("resource_id = ?");
720
- params.push(query.id);
721
- }
722
- if (query.subject) {
723
- const normal = normalized(query.subject);
724
- clauses.push("subject_type = ? AND subject_id = ?");
725
- params.push(normal.type, normal.id);
726
- }
727
- const where = clauses.map((clause) => `(${clause})`).join(" AND ");
728
- const limit = Math.max(1, Math.min(query.limit ?? 200, 1000));
729
- const rows = db
730
- .query(
731
- `SELECT * FROM ${SHARE_EVENTS_TABLE} WHERE ${where} ORDER BY event_id DESC LIMIT ?`,
732
- )
733
- .all(...params, limit) as EventRow[];
554
+ const { where, params } = eventsWhere(app, {
555
+ ...query,
556
+ subject: query.subject ? normalized(query.subject) : undefined,
557
+ });
558
+ const rows = db.query(SHARE_SQL.events(where)).all(...params, eventLimit(query.limit)) as EventRow[];
734
559
  return rows.map(toEvent);
735
560
  },
736
561
  };
@@ -89,3 +89,67 @@ describe('cursedbelt-server/telemetry', () => {
89
89
  expect(found).toEqual(['bun:sqlite']);
90
90
  });
91
91
  });
92
+
93
+ describe('🔴 cursedbelt-server/engagement/d1 — a Worker mounts it (task 2096)', () => {
94
+ test('reaches no `bun:` module at runtime — not the store, not the Bun-only barrel', () => {
95
+ const { files, bunImports } = runtimeGraph(join(import.meta.dir, 'engagement', 'd1.ts'));
96
+ expect(bunImports).toEqual([]);
97
+ // Not vacuous: the policy and the manifest rule are both in the graph, and the store is not.
98
+ expect(files.some((f) => f.endsWith('engagement/policy.ts'))).toBe(true);
99
+ expect(files.some((f) => f.endsWith('engagement/manifestPlaces.ts'))).toBe(true);
100
+ expect(files.some((f) => f.endsWith('engagement/store.ts'))).toBe(false);
101
+ });
102
+
103
+ test("its export map's `import` is the real module, not the Bun-only refusal", () => {
104
+ const pkg = JSON.parse(readFileSync(join(import.meta.dir, '..', '..', 'package.json'), 'utf8')) as {
105
+ exports: Record<string, { import?: string }>;
106
+ };
107
+ expect(pkg.exports['./engagement/d1']?.import).toBe('./dist/server/engagement/d1.js');
108
+ });
109
+ });
110
+
111
+ /**
112
+ * The three D1 halves `apps/family`'s Worker port needs (task 393). Each sits beside a sync
113
+ * store that is `bun:sqlite` by construction, so the walk also proves the split held: the
114
+ * driver-free modules they share are IN the graph, and the Bun-only siblings are NOT.
115
+ */
116
+ describe('🔴 cursedbelt-server/{sharing,activity,notifications}/d1 — a Worker imports them (task 393)', () => {
117
+ const pkg = JSON.parse(readFileSync(join(import.meta.dir, '..', '..', 'package.json'), 'utf8')) as {
118
+ exports: Record<string, { import?: string; source?: string }>;
119
+ };
120
+ const CASES = [
121
+ {
122
+ subpath: './sharing/d1',
123
+ entry: join(import.meta.dir, 'sharing', 'd1.ts'),
124
+ reaches: ['sharing/statements.ts', 'sharing/schema.ts', 'sharing/sql.ts', 'sharing/contract.ts'],
125
+ never: ['sharing/store.ts', 'sharing/migrations.ts', 'migration/runner.ts'],
126
+ },
127
+ {
128
+ subpath: './activity/d1',
129
+ entry: join(import.meta.dir, 'activity', 'd1.ts'),
130
+ reaches: ['activity/query.ts', 'activity/schema.ts', 'activity/shareSource.ts', 'sharing/schema.ts'],
131
+ never: ['activity/store.ts', 'activity/migrations.ts', 'sharing/migrations.ts', 'migration/runner.ts'],
132
+ },
133
+ {
134
+ subpath: './notifications/d1',
135
+ entry: join(import.meta.dir, 'notifications', 'd1.ts'),
136
+ reaches: ['notifications/routes.ts'],
137
+ never: ['notifications/service.ts', 'notifications/migrations.ts', 'notifications/hub.ts'],
138
+ },
139
+ ] as const;
140
+
141
+ for (const { subpath, entry, reaches, never } of CASES) {
142
+ test(`${subpath} reaches no \`bun:\` module at runtime, and none of its Bun-only siblings`, () => {
143
+ const { files, bunImports } = runtimeGraph(entry);
144
+ expect(bunImports).toEqual([]);
145
+ for (const file of reaches) expect({ file, reached: files.some((f) => f.endsWith(file)) }).toEqual({ file, reached: true });
146
+ for (const file of never) expect({ file, reached: files.some((f) => f.endsWith(file)) }).toEqual({ file, reached: false });
147
+ });
148
+
149
+ test(`${subpath}'s export map points at the real module, not the Bun-only refusal`, () => {
150
+ const rel = entry.split('/src/server/')[1]?.replace(/\.ts$/, '');
151
+ expect(pkg.exports[subpath]?.import).toBe(`./dist/server/${rel}.js`);
152
+ expect(pkg.exports[subpath]?.source).toBe(`./src/server/${rel}.ts`);
153
+ });
154
+ }
155
+ });