borgmcp-server 0.1.4 → 0.1.7

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 +32 -11
  2. package/THIRD_PARTY_NOTICES.md +1 -0
  3. package/dist/coordination-api.d.ts +2 -1
  4. package/dist/coordination-api.js +298 -89
  5. package/dist/coordination-api.js.map +1 -1
  6. package/dist/credentials.d.ts +4 -6
  7. package/dist/credentials.js +43 -19
  8. package/dist/credentials.js.map +1 -1
  9. package/dist/debug-log.d.ts +2 -3
  10. package/dist/debug-log.js +2 -3
  11. package/dist/debug-log.js.map +1 -1
  12. package/dist/enrollment.d.ts +3 -11
  13. package/dist/enrollment.js +21 -60
  14. package/dist/enrollment.js.map +1 -1
  15. package/dist/https-server.d.ts +2 -20
  16. package/dist/https-server.js +33 -51
  17. package/dist/https-server.js.map +1 -1
  18. package/dist/message-taxonomy.d.ts +38 -0
  19. package/dist/message-taxonomy.js +218 -0
  20. package/dist/message-taxonomy.js.map +1 -0
  21. package/dist/migrations.js +28 -0
  22. package/dist/migrations.js.map +1 -1
  23. package/dist/service.d.ts +4 -1
  24. package/dist/service.js +25 -10
  25. package/dist/service.js.map +1 -1
  26. package/dist/store.d.ts +66 -8
  27. package/dist/store.js +525 -100
  28. package/dist/store.js.map +1 -1
  29. package/npm-shrinkwrap.json +6 -7
  30. package/package.json +2 -2
  31. package/src/coordination-api.ts +312 -86
  32. package/src/credentials.ts +44 -26
  33. package/src/debug-log.ts +5 -5
  34. package/src/enrollment.ts +32 -78
  35. package/src/https-server.ts +44 -82
  36. package/src/message-taxonomy.ts +284 -0
  37. package/src/migrations.ts +28 -0
  38. package/src/service.ts +29 -9
  39. package/src/store.ts +593 -121
  40. package/dist/protocol-draft.d.ts +0 -2
  41. package/dist/protocol-draft.js +0 -31
  42. package/dist/protocol-draft.js.map +0 -1
  43. package/src/protocol-draft.ts +0 -32
package/dist/store.js CHANGED
@@ -1,4 +1,4 @@
1
- import { randomUUID } from "node:crypto";
1
+ import { randomUUID, timingSafeEqual } from "node:crypto";
2
2
  import { statfsSync, statSync } from "node:fs";
3
3
  import { chmod, lstat, mkdir, open } from "node:fs/promises";
4
4
  import { dirname, join, parse, relative, resolve, sep } from "node:path";
@@ -7,6 +7,7 @@ import { applyMigrations, assertMigrationsCurrent } from "./migrations.js";
7
7
  import { operatorErrors } from "./operator-error.js";
8
8
  import { assertCanonicalUuid, assertServerDerivedPrincipal, } from "./principal.js";
9
9
  import { patchRoleSectionText } from "./role-section.js";
10
+ import { validateMessageTaxonomy } from "./message-taxonomy.js";
10
11
  export const DEFAULT_CUBE_LIMITS = Object.freeze({
11
12
  maxCubesPerClient: 100,
12
13
  maxCubesTotal: 1_000,
@@ -53,6 +54,13 @@ export class RoleSectionConflictError extends Error {
53
54
  this.name = "RoleSectionConflictError";
54
55
  }
55
56
  }
57
+ export class RoleInUseError extends Error {
58
+ code = "ROLE_IN_USE";
59
+ constructor() {
60
+ super("The human-seat role is already occupied.");
61
+ this.name = "RoleInUseError";
62
+ }
63
+ }
56
64
  export class CursorExpiredError extends Error {
57
65
  code = "CURSOR_EXPIRED";
58
66
  constructor() {
@@ -60,11 +68,11 @@ export class CursorExpiredError extends Error {
60
68
  this.name = "CursorExpiredError";
61
69
  }
62
70
  }
63
- export class AttachConflictError extends Error {
64
- code = "ATTACH_CONFLICT";
71
+ export class AttachSessionRejectedError extends Error {
72
+ code = "SESSION_REJECTED";
65
73
  constructor() {
66
- super("The attach request conflicts with an existing attachment.");
67
- this.name = "AttachConflictError";
74
+ super("The saved session is not accepted for this seat.");
75
+ this.name = "AttachSessionRejectedError";
68
76
  }
69
77
  }
70
78
  export class StorageCapacityError extends Error {
@@ -150,9 +158,10 @@ export async function openStore(options) {
150
158
  throw new Error("SQLite page size is unavailable.");
151
159
  }
152
160
  const capacityGuard = new StorageCapacityGuard(storageLimits, capacityProbe, requiredInteger(pageRow, "page_size"));
161
+ const activityHub = new ActivityHub();
153
162
  const maintenance = new SqliteMaintenanceStore(database, clock);
163
+ const liveness = new SqliteLivenessStore(database, clock, activityHub, capacityGuard);
154
164
  const credentials = new SqliteCredentialStore(database, clock, capacityGuard, options.mutationHook);
155
- const activityHub = new ActivityHub();
156
165
  return Object.freeze({
157
166
  forPrincipal: (principal) => {
158
167
  assertServerDerivedPrincipal(principal);
@@ -160,6 +169,7 @@ export async function openStore(options) {
160
169
  },
161
170
  maintenance,
162
171
  credentials,
172
+ liveness,
163
173
  diagnostics: () => diagnostics(database),
164
174
  close: () => database.close(),
165
175
  });
@@ -267,7 +277,8 @@ class SqliteScopedStore {
267
277
  listCubes() {
268
278
  const scope = this.#scope("read");
269
279
  const rows = this.#database.prepare(`
270
- SELECT c.id, c.owner_id, c.name, c.directive, c.created_at, c.updated_at
280
+ SELECT c.id, c.owner_id, c.name, c.directive, c.message_taxonomy,
281
+ c.created_at, c.updated_at
271
282
  FROM cubes AS c
272
283
  WHERE ${scope.sql}
273
284
  ORDER BY c.id
@@ -278,25 +289,43 @@ class SqliteScopedStore {
278
289
  assertCanonicalUuid(cubeId, "Cube id");
279
290
  const scope = this.#scope("read");
280
291
  const row = this.#database.prepare(`
281
- SELECT c.id, c.owner_id, c.name, c.directive, c.created_at, c.updated_at
292
+ SELECT c.id, c.owner_id, c.name, c.directive, c.message_taxonomy,
293
+ c.created_at, c.updated_at
282
294
  FROM cubes AS c
283
295
  WHERE c.id = ? AND ${scope.sql}
284
296
  `).get(cubeId, ...scope.parameters);
285
297
  return row === undefined ? null : cubeRecord(cubeRow(row));
286
298
  }
287
299
  updateDirective(cubeId, directive) {
300
+ this.updateCube(cubeId, { directive });
301
+ }
302
+ updateCube(cubeId, input) {
288
303
  assertCanonicalUuid(cubeId, "Cube id");
289
- validateDirective(directive);
304
+ if (input.directive === undefined && input.messageTaxonomy === undefined) {
305
+ throw new TypeError("At least one cube field is required.");
306
+ }
307
+ if (input.directive !== undefined)
308
+ validateDirective(input.directive);
309
+ const taxonomy = input.messageTaxonomy === undefined
310
+ ? undefined
311
+ : validateMessageTaxonomy(input.messageTaxonomy);
312
+ const serializedTaxonomy = taxonomy === undefined ? undefined : serializeTaxonomy(taxonomy);
290
313
  const scope = this.#scope("manage");
291
314
  this.#requireCube(cubeId, "manage");
292
- this.#capacityGuard.assertCanGrow(Buffer.byteLength(directive));
315
+ this.#capacityGuard.assertCanGrow(Buffer.byteLength(input.directive ?? "") + Buffer.byteLength(serializedTaxonomy ?? ""));
293
316
  const result = this.#database.prepare(`
294
317
  UPDATE cubes AS c
295
- SET directive = ?, updated_at = ?
318
+ SET directive = COALESCE(?, directive),
319
+ message_taxonomy = CASE WHEN ? = 1 THEN ? ELSE message_taxonomy END,
320
+ updated_at = ?
296
321
  WHERE c.id = ? AND ${scope.sql}
297
- `).run(directive, this.#now(), cubeId, ...scope.parameters);
322
+ `).run(input.directive ?? null, serializedTaxonomy === undefined ? 0 : 1, serializedTaxonomy ?? null, this.#now(), cubeId, ...scope.parameters);
298
323
  if (result.changes !== 1)
299
324
  throw new ScopedStoreError();
325
+ const cube = this.getCube(cubeId);
326
+ if (cube === null)
327
+ throw new ScopedStoreError();
328
+ return cube;
300
329
  }
301
330
  appendActivity(cubeId, message) {
302
331
  const entry = this.appendLog(cubeId, { message });
@@ -354,6 +383,7 @@ class SqliteScopedStore {
354
383
  if (value !== undefined && typeof value !== "boolean")
355
384
  throw new TypeError("Role flags must be boolean.");
356
385
  }
386
+ validateRoleClass(input.roleClass);
357
387
  const isDefault = input.isDefault ?? false;
358
388
  const isMandatory = input.isMandatory ?? false;
359
389
  const isHumanSeat = input.isHumanSeat ?? false;
@@ -379,8 +409,8 @@ class SqliteScopedStore {
379
409
  id, cube_id, name, short_description, detailed_description,
380
410
  is_default, is_mandatory, is_human_seat, can_broadcast,
381
411
  receives_all_direct, role_class, created_at
382
- ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 'worker', ?)
383
- `).run(id, cubeId, input.name, shortDescription, detailedDescription, booleanInteger(isDefault), booleanInteger(isMandatory), booleanInteger(isHumanSeat), booleanInteger(canBroadcast), booleanInteger(receivesAllDirect), this.#now());
412
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
413
+ `).run(id, cubeId, input.name, shortDescription, detailedDescription, booleanInteger(isDefault), booleanInteger(isMandatory), booleanInteger(isHumanSeat), booleanInteger(canBroadcast), booleanInteger(receivesAllDirect), input.roleClass ?? "worker", this.#now());
384
414
  this.#mutationHook?.("role.insert");
385
415
  const row = this.#database.prepare(`
386
416
  SELECT id, cube_id, name, short_description, detailed_description,
@@ -425,6 +455,7 @@ class SqliteScopedStore {
425
455
  if (value !== undefined && typeof value !== "boolean")
426
456
  throw new TypeError("Role flags must be boolean.");
427
457
  }
458
+ validateRoleClass(input.roleClass);
428
459
  this.#requireCube(cubeId, "manage");
429
460
  this.#capacityGuard.assertCanGrow(Buffer.byteLength(input.name ?? "") + Buffer.byteLength(input.shortDescription ?? "") +
430
461
  Buffer.byteLength(input.detailedDescription ?? "") + 8_192);
@@ -458,9 +489,10 @@ class SqliteScopedStore {
458
489
  this.#database.prepare(`
459
490
  UPDATE roles SET
460
491
  name = ?, short_description = ?, detailed_description = ?, is_default = ?,
461
- is_mandatory = ?, is_human_seat = ?, can_broadcast = ?, receives_all_direct = ?
492
+ is_mandatory = ?, is_human_seat = ?, can_broadcast = ?, receives_all_direct = ?,
493
+ role_class = ?
462
494
  WHERE id = ? AND cube_id = ?
463
- `).run(input.name ?? existing.name, input.shortDescription ?? existing.short_description, nextDetailedDescription, booleanInteger(input.isDefault ?? existing.is_default), booleanInteger(input.isMandatory ?? existing.is_mandatory), booleanInteger(input.isHumanSeat ?? existing.is_human_seat), booleanInteger(input.canBroadcast ?? existing.can_broadcast), booleanInteger(input.receivesAllDirect ?? existing.receives_all_direct), roleId, cubeId);
495
+ `).run(input.name ?? existing.name, input.shortDescription ?? existing.short_description, nextDetailedDescription, booleanInteger(input.isDefault ?? existing.is_default), booleanInteger(input.isMandatory ?? existing.is_mandatory), booleanInteger(input.isHumanSeat ?? existing.is_human_seat), booleanInteger(input.canBroadcast ?? existing.can_broadcast), booleanInteger(input.receivesAllDirect ?? existing.receives_all_direct), input.roleClass ?? existing.role_class, roleId, cubeId);
464
496
  const row = this.#database.prepare(`
465
497
  SELECT id, cube_id, name, short_description, detailed_description,
466
498
  is_default, is_mandatory, is_human_seat, can_broadcast,
@@ -547,6 +579,139 @@ class SqliteScopedStore {
547
579
  `).all(cubeId);
548
580
  return rows.map(droneRecord);
549
581
  }
582
+ listDronesSince(cubeId, since) {
583
+ this.#requireCube(cubeId, "read");
584
+ let createdAt;
585
+ let anchorId = null;
586
+ if (/^[0-9a-f]{8}-[0-9a-f-]{27}$/iu.test(since)) {
587
+ const anchor = this.#database.prepare("SELECT id, created_at FROM activity_log WHERE id = ? AND cube_id = ?").get(since, cubeId);
588
+ if (anchor === undefined)
589
+ throw new ScopedStoreError();
590
+ anchorId = requiredText(anchor, "id");
591
+ createdAt = requiredText(anchor, "created_at");
592
+ }
593
+ else {
594
+ validateTimestamp(since);
595
+ createdAt = since;
596
+ }
597
+ const rows = this.#database.prepare(`
598
+ SELECT drone.id, drone.cube_id, drone.role_id, drone.label,
599
+ COALESCE(drone.last_seen, drone.created_at) AS last_seen,
600
+ drone.hostname, drone.created_at,
601
+ CASE WHEN client.revoked_at IS NULL AND grant_row.access IN ('write', 'manage')
602
+ THEN 'participant' ELSE 'observer' END AS posture,
603
+ (SELECT MAX(post.created_at) FROM activity_log AS post
604
+ WHERE post.cube_id = drone.cube_id AND post.drone_id = drone.id) AS last_log_post_at,
605
+ EXISTS (SELECT 1 FROM activity_log AS response
606
+ WHERE response.cube_id = drone.cube_id AND response.drone_id = drone.id
607
+ AND (response.created_at > ? OR
608
+ (? IS NOT NULL AND response.created_at = ? AND response.id > ?))) AS seen_since
609
+ FROM drones AS drone
610
+ LEFT JOIN clients AS client ON client.id = drone.client_id
611
+ LEFT JOIN client_cube_grants AS grant_row
612
+ ON grant_row.client_id = drone.client_id AND grant_row.cube_id = drone.cube_id
613
+ WHERE drone.cube_id = ? AND drone.evicted_at IS NULL
614
+ ORDER BY drone.label, drone.id
615
+ `).all(createdAt, anchorId, createdAt, anchorId, cubeId);
616
+ return {
617
+ since: createdAt,
618
+ drones: rows.map((row) => ({
619
+ ...droneRecord(row),
620
+ last_log_post_at: nullableText(row, "last_log_post_at"),
621
+ seen_since: requiredInteger(row, "seen_since") === 1,
622
+ })),
623
+ };
624
+ }
625
+ reassignDrone(cubeId, droneId, roleId) {
626
+ assertCanonicalUuid(cubeId, "Cube id");
627
+ assertCanonicalUuid(droneId, "Drone id");
628
+ assertCanonicalUuid(roleId, "Role id");
629
+ this.#requireCube(cubeId, "manage");
630
+ this.#database.exec("BEGIN IMMEDIATE");
631
+ try {
632
+ this.#requireCube(cubeId, "manage");
633
+ const droneRow = this.#database.prepare(`
634
+ SELECT id, cube_id, role_id, label FROM drones
635
+ WHERE id = ? AND cube_id = ? AND evicted_at IS NULL
636
+ `).get(droneId, cubeId);
637
+ const targetRoleRow = this.#database.prepare(`
638
+ SELECT id, is_human_seat, role_class FROM roles WHERE id = ? AND cube_id = ?
639
+ `).get(roleId, cubeId);
640
+ if (droneRow === undefined || targetRoleRow === undefined)
641
+ throw new ScopedStoreError();
642
+ const sourceRoleRow = this.#database.prepare(`
643
+ SELECT is_human_seat FROM roles WHERE id = ? AND cube_id = ?
644
+ `).get(requiredText(droneRow, "role_id"), cubeId);
645
+ if (sourceRoleRow === undefined)
646
+ throw new ScopedStoreError();
647
+ if (requiredText(targetRoleRow, "role_class") === "queen" &&
648
+ requiredInteger(sourceRoleRow, "is_human_seat") !== 1) {
649
+ throw new AccessDeniedError();
650
+ }
651
+ if (requiredInteger(targetRoleRow, "is_human_seat") === 1) {
652
+ const occupied = this.#database.prepare(`
653
+ SELECT 1 AS present FROM drones
654
+ WHERE cube_id = ? AND role_id = ? AND id <> ? AND evicted_at IS NULL
655
+ `).get(cubeId, roleId, droneId);
656
+ if (occupied !== undefined)
657
+ throw new RoleInUseError();
658
+ }
659
+ this.#database.prepare("UPDATE drones SET role_id = ? WHERE id = ? AND cube_id = ?")
660
+ .run(roleId, droneId, cubeId);
661
+ const row = this.#database.prepare(`
662
+ SELECT drone.id, drone.cube_id, drone.role_id, drone.label,
663
+ COALESCE(drone.last_seen, drone.created_at) AS last_seen,
664
+ drone.hostname, drone.created_at,
665
+ CASE WHEN client.revoked_at IS NULL AND grant_row.access IN ('write', 'manage')
666
+ THEN 'participant' ELSE 'observer' END AS posture
667
+ FROM drones AS drone
668
+ LEFT JOIN clients AS client ON client.id = drone.client_id
669
+ LEFT JOIN client_cube_grants AS grant_row
670
+ ON grant_row.client_id = drone.client_id AND grant_row.cube_id = drone.cube_id
671
+ WHERE drone.id = ? AND drone.cube_id = ? AND drone.evicted_at IS NULL
672
+ `).get(droneId, cubeId);
673
+ if (row === undefined)
674
+ throw new ScopedStoreError();
675
+ this.#database.exec("COMMIT");
676
+ return droneRecord(row);
677
+ }
678
+ catch (error) {
679
+ try {
680
+ this.#database.exec("ROLLBACK");
681
+ }
682
+ catch { /* Preserve the original failure. */ }
683
+ throw error;
684
+ }
685
+ }
686
+ evictDrone(cubeId, droneId) {
687
+ assertCanonicalUuid(cubeId, "Cube id");
688
+ assertCanonicalUuid(droneId, "Drone id");
689
+ this.#requireCube(cubeId, "manage");
690
+ this.#database.exec("BEGIN IMMEDIATE");
691
+ try {
692
+ this.#requireCube(cubeId, "manage");
693
+ const active = this.#database.prepare(`
694
+ SELECT 1 AS present FROM drones WHERE id = ? AND cube_id = ? AND evicted_at IS NULL
695
+ `).get(droneId, cubeId);
696
+ if (active === undefined)
697
+ throw new ScopedStoreError();
698
+ const now = this.#now();
699
+ this.#database.prepare("UPDATE drones SET evicted_at = ? WHERE id = ? AND cube_id = ?")
700
+ .run(now, droneId, cubeId);
701
+ this.#database.prepare(`
702
+ UPDATE drone_sessions SET revoked_at = ?
703
+ WHERE drone_id = ? AND cube_id = ? AND revoked_at IS NULL
704
+ `).run(now, droneId, cubeId);
705
+ this.#database.exec("COMMIT");
706
+ }
707
+ catch (error) {
708
+ try {
709
+ this.#database.exec("ROLLBACK");
710
+ }
711
+ catch { /* Preserve the original failure. */ }
712
+ throw error;
713
+ }
714
+ }
550
715
  appendLog(cubeId, input) {
551
716
  assertCanonicalUuid(cubeId, "Cube id");
552
717
  if (input.message.length === 0 || Buffer.byteLength(input.message) > 10_240) {
@@ -592,6 +757,12 @@ class SqliteScopedStore {
592
757
  `).run(id, droneId, this.#principal.kind, this.#principal.id, input.message, createdAt, visibility, cubeId, ...scope.parameters);
593
758
  if (inserted.changes !== 1)
594
759
  throw new ScopedStoreError();
760
+ if (droneId !== null) {
761
+ this.#database.prepare(`
762
+ UPDATE drones SET last_seen = ? WHERE id = ? AND cube_id = ? AND evicted_at IS NULL
763
+ `).run(createdAt, droneId, cubeId);
764
+ this.#database.prepare("DELETE FROM silent_seat_ping_state WHERE drone_id = ?").run(droneId);
765
+ }
595
766
  if (recipients.length > 0) {
596
767
  const valid = this.#database.prepare(`
597
768
  SELECT COUNT(*) AS count
@@ -669,18 +840,58 @@ class SqliteScopedStore {
669
840
  assertCanonicalUuid(entryId, "Activity entry id");
670
841
  if (kind !== "ack" && kind !== "claim")
671
842
  throw new Error("Unknown acknowledgement kind.");
672
- const exists = this.#database.prepare("SELECT 1 AS present FROM activity_log WHERE id = ? AND cube_id = ?").get(entryId, cubeId);
673
- if (exists === undefined)
843
+ const original = this.#database.prepare(`
844
+ SELECT drone_id, message, visibility FROM activity_log WHERE id = ? AND cube_id = ?
845
+ `).get(entryId, cubeId);
846
+ if (original === undefined)
674
847
  throw new ScopedStoreError();
675
848
  this.#capacityGuard.assertCanGrow(512);
676
849
  const claimant = this.#principal.kind === "drone-session"
677
850
  ? this.#principal.droneId
678
851
  : this.#principal.id;
679
- this.#database.prepare(`
852
+ const ackAt = this.#now();
853
+ const inserted = this.#database.prepare(`
680
854
  INSERT OR IGNORE INTO activity_acks (
681
855
  entry_id, principal_kind, principal_id, kind, created_at, claimant_drone_id
682
856
  ) VALUES (?, ?, ?, ?, ?, ?)
683
- `).run(entryId, this.#principal.kind, this.#principal.id, kind, this.#now(), claimant);
857
+ `).run(entryId, this.#principal.kind, this.#principal.id, kind, ackAt, claimant);
858
+ if (inserted.changes !== 1 || this.#principal.kind !== "drone-session")
859
+ return;
860
+ const claimantDroneId = this.#principal.droneId;
861
+ const actor = this.#database.prepare(`
862
+ SELECT drone.label, role.name AS role_name
863
+ FROM drones AS drone JOIN roles AS role ON role.id = drone.role_id
864
+ WHERE drone.id = ? AND drone.cube_id = ? AND drone.evicted_at IS NULL
865
+ `).get(claimantDroneId, cubeId);
866
+ const preview = activityPreview(requiredText(original, "message"));
867
+ const originalAuthor = nullableText(original, "drone_id");
868
+ const recipients = kind === "ack"
869
+ ? originalAuthor === null ? [] : [originalAuthor]
870
+ : this.#claimAudience(cubeId, entryId, requiredText(original, "visibility"))
871
+ .filter((droneId) => droneId !== claimantDroneId);
872
+ if (recipients.length === 0)
873
+ return;
874
+ const roleName = actor === undefined ? null : requiredText(actor, "role_name");
875
+ const notification = {
876
+ kind,
877
+ id: randomUUID(),
878
+ cube_id: cubeId,
879
+ drone_id: claimantDroneId,
880
+ drone_label: actor === undefined ? null : requiredText(actor, "label"),
881
+ role_name: roleName,
882
+ message: `[${kind.toUpperCase()}] ${preview}`,
883
+ created_at: ackAt,
884
+ visibility: "direct",
885
+ recipient_drone_ids: recipients,
886
+ log_entry_id: entryId,
887
+ ack_kind: kind,
888
+ ack_at: ackAt,
889
+ entry_preview: preview,
890
+ ...(kind === "ack"
891
+ ? { author_drone_id: recipients[0] }
892
+ : { claimant_drone_id: claimantDroneId, claimant_role: roleName }),
893
+ };
894
+ this.#activityHub.publish(cubeId, notification);
684
895
  }
685
896
  recordDecision(cubeId, input) {
686
897
  this.#requireCube(cubeId, "manage");
@@ -730,7 +941,6 @@ class SqliteScopedStore {
730
941
  throw new ScopedStoreError();
731
942
  assertCanonicalUuid(input.cubeId, "Cube id");
732
943
  assertCanonicalUuid(input.roleId, "Role id");
733
- assertCanonicalUuid(input.retryKey, "Retry key");
734
944
  if (input.priorDroneId !== undefined)
735
945
  assertCanonicalUuid(input.priorDroneId, "Prior drone id");
736
946
  assertCanonicalUuid(input.droneId, "Drone id");
@@ -758,82 +968,95 @@ class SqliteScopedStore {
758
968
  `).get(input.cubeId, input.roleId, ...scope.parameters);
759
969
  if (roleRow === undefined)
760
970
  throw new ScopedStoreError();
761
- const posture = this.#allowsDirectedWork(input.cubeId) ? "participant" : "observer";
762
- const retryBinding = this.#database.prepare(`
763
- SELECT binding.cube_id AS binding_cube_id,
764
- binding.requested_role_id AS binding_requested_role_id,
765
- binding.prior_drone_id, drone.id, drone.label,
766
- drone.attach_generation, drone.evicted_at
767
- FROM seat_attach_bindings AS binding
768
- JOIN drones AS drone ON drone.id = binding.drone_id
769
- WHERE binding.client_id = ? AND binding.retry_key = ?
770
- `).get(this.#principal.id, input.retryKey);
971
+ const bound = this.#database.prepare(`
972
+ SELECT credential.verifier_digest, credential.revoked_at AS credential_revoked_at,
973
+ session.id AS session_id, session.client_id, session.cube_id, session.drone_id,
974
+ session.expires_at, session.revoked_at AS session_revoked_at,
975
+ drone.role_id, drone.label, drone.evicted_at
976
+ FROM drone_session_credentials AS credential
977
+ JOIN drone_sessions AS session ON session.id = credential.session_id
978
+ JOIN drones AS drone ON drone.id = session.drone_id
979
+ AND drone.client_id = session.client_id AND drone.cube_id = session.cube_id
980
+ WHERE credential.lookup_digest = ?
981
+ `).get(input.credentialDigest.lookup);
982
+ if (bound !== undefined) {
983
+ const verifier = requiredBuffer(bound, "verifier_digest");
984
+ if (!timingSafeEqual(verifier, input.credentialDigest.verifier) ||
985
+ optionalText(bound, "credential_revoked_at") !== null ||
986
+ optionalText(bound, "session_revoked_at") !== null ||
987
+ optionalText(bound, "evicted_at") !== null ||
988
+ requiredText(bound, "expires_at") <= now ||
989
+ requiredText(bound, "client_id") !== this.#principal.id ||
990
+ requiredText(bound, "cube_id") !== input.cubeId ||
991
+ requiredText(bound, "role_id") !== input.roleId ||
992
+ (input.priorDroneId !== undefined &&
993
+ requiredText(bound, "drone_id") !== input.priorDroneId)) {
994
+ throw new AttachSessionRejectedError();
995
+ }
996
+ const droneId = requiredText(bound, "drone_id");
997
+ this.#database.prepare("UPDATE drones SET last_seen = ? WHERE id = ?").run(now, droneId);
998
+ this.#database.prepare("DELETE FROM silent_seat_ping_state WHERE drone_id = ?").run(droneId);
999
+ const attachedRoleRow = this.#database.prepare(`
1000
+ SELECT id AS role_id, name AS role_name, role_class, is_human_seat
1001
+ FROM roles WHERE id = ? AND cube_id = ?
1002
+ `).get(requiredText(bound, "role_id"), input.cubeId);
1003
+ if (attachedRoleRow === undefined)
1004
+ throw new Error("Attached drone role is unavailable.");
1005
+ const roleClass = requiredText(attachedRoleRow, "role_class");
1006
+ if (roleClass !== "queen" && roleClass !== "worker") {
1007
+ throw new Error("Database contains invalid role class.");
1008
+ }
1009
+ this.#database.exec("COMMIT");
1010
+ return {
1011
+ cube: {
1012
+ id: requiredText(roleRow, "cube_id"),
1013
+ name: requiredText(roleRow, "cube_name"),
1014
+ },
1015
+ role: {
1016
+ id: requiredText(attachedRoleRow, "role_id"),
1017
+ name: requiredText(attachedRoleRow, "role_name"),
1018
+ role_class: roleClass,
1019
+ is_human_seat: requiredInteger(attachedRoleRow, "is_human_seat") === 1,
1020
+ },
1021
+ drone: { id: droneId, label: requiredText(bound, "label") },
1022
+ sessionId: requiredText(bound, "session_id"),
1023
+ expiresAt: requiredText(bound, "expires_at"),
1024
+ result: "reused",
1025
+ };
1026
+ }
1027
+ const priorSeat = input.priorDroneId === undefined ? undefined : this.#database.prepare(`
1028
+ SELECT id, role_id, label FROM drones
1029
+ WHERE id = ? AND client_id = ? AND cube_id = ? AND evicted_at IS NULL
1030
+ `).get(input.priorDroneId, this.#principal.id, input.cubeId);
771
1031
  let droneId;
772
1032
  let droneLabel;
773
- let reattached;
774
- let generation;
775
- if (retryBinding !== undefined) {
776
- if (optionalText(retryBinding, "evicted_at") != null)
777
- throw new ScopedStoreError();
778
- if (requiredText(retryBinding, "binding_cube_id") !== input.cubeId ||
779
- requiredText(retryBinding, "binding_requested_role_id") !== input.roleId ||
780
- nullableText(retryBinding, "prior_drone_id") !== (input.priorDroneId ?? null)) {
781
- throw new AttachConflictError();
1033
+ if (priorSeat !== undefined) {
1034
+ const priorSeatId = requiredText(priorSeat, "id");
1035
+ if (requiredText(priorSeat, "role_id") !== input.roleId) {
1036
+ throw new AttachSessionRejectedError();
782
1037
  }
783
- droneId = requiredText(retryBinding, "id");
784
- droneLabel = requiredText(retryBinding, "label");
785
- generation = requiredInteger(retryBinding, "attach_generation") + 1;
786
- this.#database.prepare(`
787
- UPDATE drones SET last_seen = ?, attach_generation = ? WHERE id = ?
788
- `).run(now, generation, droneId);
789
- reattached = true;
1038
+ const occupied = this.#database.prepare(`
1039
+ SELECT 1 FROM drone_sessions AS session
1040
+ JOIN drone_session_credentials AS credential ON credential.session_id = session.id
1041
+ WHERE session.drone_id = ? AND session.client_id = ? AND session.cube_id = ?
1042
+ AND session.revoked_at IS NULL AND credential.revoked_at IS NULL
1043
+ AND session.expires_at > ?
1044
+ `).get(priorSeatId, this.#principal.id, input.cubeId, now);
1045
+ if (occupied !== undefined)
1046
+ throw new AttachSessionRejectedError();
1047
+ droneId = priorSeatId;
1048
+ droneLabel = requiredText(priorSeat, "label");
1049
+ this.#database.prepare("UPDATE drones SET last_seen = ? WHERE id = ?").run(now, droneId);
790
1050
  }
791
1051
  else {
792
- const priorSeat = input.priorDroneId === undefined ? undefined : this.#database.prepare(`
793
- SELECT id, label, attach_generation
794
- FROM drones
795
- WHERE id = ? AND client_id = ? AND cube_id = ? AND evicted_at IS NULL
796
- `).get(input.priorDroneId, this.#principal.id, input.cubeId);
797
- if (priorSeat !== undefined) {
798
- droneId = requiredText(priorSeat, "id");
799
- droneLabel = requiredText(priorSeat, "label");
800
- generation = requiredInteger(priorSeat, "attach_generation") + 1;
801
- this.#database.prepare(`
802
- UPDATE drones SET last_seen = ?, attach_generation = ? WHERE id = ?
803
- `).run(now, generation, droneId);
804
- reattached = true;
805
- }
806
- else {
807
- droneId = input.droneId;
808
- droneLabel = seatLabel(requiredText(roleRow, "role_name"), droneId);
809
- this.#database.prepare(`
810
- INSERT INTO drones (
811
- id, cube_id, role_id, client_id, label, created_at, last_seen, retry_key,
812
- attach_generation
813
- ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, 1)
814
- `).run(droneId, input.cubeId, input.roleId, this.#principal.id, droneLabel, now, now, input.retryKey);
815
- reattached = false;
816
- generation = 1;
817
- }
1052
+ droneId = input.droneId;
1053
+ droneLabel = seatLabel(requiredText(roleRow, "role_name"), droneId);
818
1054
  this.#database.prepare(`
819
- INSERT INTO seat_attach_bindings (
820
- client_id, retry_key, cube_id, requested_role_id, drone_id, prior_drone_id, created_at
821
- ) VALUES (?, ?, ?, ?, ?, ?, ?)
822
- `).run(this.#principal.id, input.retryKey, input.cubeId, input.roleId, droneId, input.priorDroneId ?? null, now);
823
- }
824
- const oldSessions = this.#database.prepare("SELECT id FROM drone_sessions WHERE drone_id = ? AND client_id = ? AND cube_id = ?").all(droneId, this.#principal.id, input.cubeId)
825
- .map((row) => requiredText(row, "id"));
826
- if (oldSessions.length > 0) {
827
- const placeholders = oldSessions.map(() => "?").join(", ");
828
- this.#database.prepare(`
829
- UPDATE drone_session_credentials SET revoked_at = ?
830
- WHERE session_id IN (${placeholders}) AND revoked_at IS NULL
831
- `).run(now, ...oldSessions);
832
- this.#database.prepare(`
833
- UPDATE drone_sessions SET revoked_at = ?
834
- WHERE id IN (${placeholders}) AND revoked_at IS NULL
835
- `).run(now, ...oldSessions);
1055
+ INSERT INTO drones (id, cube_id, role_id, client_id, label, created_at, last_seen)
1056
+ VALUES (?, ?, ?, ?, ?, ?, ?)
1057
+ `).run(droneId, input.cubeId, input.roleId, this.#principal.id, droneLabel, now, now);
836
1058
  }
1059
+ this.#database.prepare("DELETE FROM silent_seat_ping_state WHERE drone_id = ?").run(droneId);
837
1060
  this.#database.prepare(`
838
1061
  INSERT INTO drone_sessions (
839
1062
  id, client_id, cube_id, drone_id, created_at, expires_at
@@ -869,10 +1092,7 @@ class SqliteScopedStore {
869
1092
  drone: { id: droneId, label: droneLabel },
870
1093
  sessionId: input.sessionId,
871
1094
  expiresAt: input.expiresAt,
872
- generation,
873
- posture,
874
- reattached,
875
- revokedSessionIds: oldSessions,
1095
+ result: "created",
876
1096
  };
877
1097
  }
878
1098
  catch (error) {
@@ -886,10 +1106,41 @@ class SqliteScopedStore {
886
1106
  subscribeActivity(cubeId, listener) {
887
1107
  this.#requireCube(cubeId, "read");
888
1108
  return this.#activityHub.subscribe(cubeId, (entry) => {
1109
+ if ("kind" in entry) {
1110
+ if (this.#principal.kind === "drone-session" &&
1111
+ entry.recipient_drone_ids.includes(this.#principal.droneId))
1112
+ listener(entry);
1113
+ return;
1114
+ }
889
1115
  if (entry.visibility === "broadcast" || this.#allowsDirectedWork(cubeId))
890
1116
  listener(entry);
891
1117
  });
892
1118
  }
1119
+ #claimAudience(cubeId, entryId, visibility) {
1120
+ if (visibility === "direct") {
1121
+ return this.#database.prepare(`
1122
+ SELECT recipient.drone_id
1123
+ FROM activity_log_recipients AS recipient
1124
+ JOIN drones AS drone ON drone.id = recipient.drone_id
1125
+ JOIN clients AS client ON client.id = drone.client_id
1126
+ JOIN client_cube_grants AS grant_row
1127
+ ON grant_row.client_id = drone.client_id AND grant_row.cube_id = drone.cube_id
1128
+ WHERE recipient.entry_id = ? AND drone.cube_id = ? AND drone.evicted_at IS NULL
1129
+ AND client.revoked_at IS NULL AND grant_row.access IN ('write', 'manage')
1130
+ ORDER BY recipient.drone_id
1131
+ `).all(entryId, cubeId).map((row) => requiredText(row, "drone_id"));
1132
+ }
1133
+ return this.#database.prepare(`
1134
+ SELECT drone.id AS drone_id
1135
+ FROM drones AS drone
1136
+ JOIN clients AS client ON client.id = drone.client_id
1137
+ JOIN client_cube_grants AS grant_row
1138
+ ON grant_row.client_id = drone.client_id AND grant_row.cube_id = drone.cube_id
1139
+ WHERE drone.cube_id = ? AND drone.evicted_at IS NULL AND client.revoked_at IS NULL
1140
+ AND grant_row.access IN ('write', 'manage')
1141
+ ORDER BY drone.id
1142
+ `).all(cubeId).map((row) => requiredText(row, "drone_id"));
1143
+ }
893
1144
  #allowsDirectedWork(cubeId) {
894
1145
  const scope = this.#scope("write");
895
1146
  return this.#database.prepare(`
@@ -902,8 +1153,17 @@ class SqliteScopedStore {
902
1153
  const row = this.#database.prepare(`
903
1154
  SELECT 1 AS present FROM cubes AS c WHERE c.id = ? AND ${scope.sql}
904
1155
  `).get(cubeId, ...scope.parameters);
905
- if (row === undefined)
906
- throw new ScopedStoreError();
1156
+ if (row !== undefined)
1157
+ return;
1158
+ if (access === "manage") {
1159
+ const visibleScope = this.#scope("read");
1160
+ const visible = this.#database.prepare(`
1161
+ SELECT 1 AS present FROM cubes AS c WHERE c.id = ? AND ${visibleScope.sql}
1162
+ `).get(cubeId, ...visibleScope.parameters);
1163
+ if (visible !== undefined)
1164
+ throw new AccessDeniedError();
1165
+ }
1166
+ throw new ScopedStoreError();
907
1167
  }
908
1168
  #nextActivityTimestamp(cubeId) {
909
1169
  const now = this.#clock().getTime();
@@ -1132,7 +1392,7 @@ class SqliteMaintenanceStore {
1132
1392
  for (const table of [
1133
1393
  "cube_create_bindings", "activity_acks", "activity_log_recipients", "activity_log",
1134
1394
  "decisions", "expired_activity_cursors", "drone_session_credentials", "drone_sessions",
1135
- "seat_attach_bindings", "drones", "roles", "client_cube_grants", "cubes", "client_server_capabilities",
1395
+ "drones", "roles", "client_cube_grants", "cubes", "client_server_capabilities",
1136
1396
  "enrollment_claims", "client_credentials", "owner_enrollment_state", "clients",
1137
1397
  "enrollment_invitations",
1138
1398
  ])
@@ -1234,6 +1494,54 @@ class SqliteMaintenanceStore {
1234
1494
  assertCanonicalUuid(sessionId, "Drone session id");
1235
1495
  this.#database.prepare("UPDATE drone_sessions SET revoked_at = ? WHERE id = ? AND revoked_at IS NULL").run(this.#now(), sessionId);
1236
1496
  }
1497
+ expireDroneSession(sessionId) {
1498
+ assertCanonicalUuid(sessionId, "Drone session id");
1499
+ this.#database.prepare("UPDATE drone_sessions SET expires_at = ? WHERE id = ?")
1500
+ .run(new Date(0).toISOString(), sessionId);
1501
+ }
1502
+ inspectManagedDrone(droneId) {
1503
+ assertCanonicalUuid(droneId, "Drone id");
1504
+ const row = this.#database.prepare(`
1505
+ SELECT drone.role_id, drone.evicted_at,
1506
+ EXISTS(SELECT 1 FROM drone_sessions AS session
1507
+ WHERE session.drone_id = drone.id AND session.revoked_at IS NOT NULL) AS session_revoked
1508
+ FROM drones AS drone WHERE drone.id = ?
1509
+ `).get(droneId);
1510
+ if (row === undefined)
1511
+ throw new ScopedStoreError();
1512
+ return {
1513
+ role_id: requiredText(row, "role_id"),
1514
+ evicted: nullableText(row, "evicted_at") !== null,
1515
+ session_revoked: requiredInteger(row, "session_revoked") === 1,
1516
+ };
1517
+ }
1518
+ inspectCubeManagementState(cubeId) {
1519
+ assertCanonicalUuid(cubeId, "Cube id");
1520
+ const cube = this.#database.prepare("SELECT directive, message_taxonomy FROM cubes WHERE id = ?").get(cubeId);
1521
+ if (cube === undefined)
1522
+ throw new ScopedStoreError();
1523
+ const taxonomy = nullableText(cube, "message_taxonomy");
1524
+ const parsed = taxonomy === null ? null : JSON.parse(taxonomy);
1525
+ return {
1526
+ directive: requiredText(cube, "directive"),
1527
+ taxonomy_marker: typeof parsed?.[0]?.class === "string" ? parsed[0].class : null,
1528
+ role_ids: this.#database.prepare("SELECT id FROM roles WHERE cube_id = ? ORDER BY id").all(cubeId).map((row) => requiredText(row, "id")),
1529
+ active_decision_ids: this.#database.prepare(`
1530
+ SELECT id FROM decisions WHERE cube_id = ? AND status = 'active' ORDER BY id
1531
+ `).all(cubeId).map((row) => requiredText(row, "id")),
1532
+ drones: this.#database.prepare(`
1533
+ SELECT drone.id, drone.role_id, drone.evicted_at,
1534
+ EXISTS(SELECT 1 FROM drone_sessions AS session
1535
+ WHERE session.drone_id = drone.id AND session.revoked_at IS NOT NULL) AS session_revoked
1536
+ FROM drones AS drone WHERE drone.cube_id = ? ORDER BY drone.id
1537
+ `).all(cubeId).map((row) => ({
1538
+ id: requiredText(row, "id"),
1539
+ role_id: requiredText(row, "role_id"),
1540
+ evicted: nullableText(row, "evicted_at") !== null,
1541
+ session_revoked: requiredInteger(row, "session_revoked") === 1,
1542
+ })),
1543
+ };
1544
+ }
1237
1545
  expireActivityCursor(cubeId, cursor) {
1238
1546
  assertCanonicalUuid(cubeId, "Cube id");
1239
1547
  assertCanonicalUuid(cursor.id, "Activity cursor id");
@@ -1275,6 +1583,70 @@ class ActivityHub {
1275
1583
  }
1276
1584
  }
1277
1585
  }
1586
+ class SqliteLivenessStore {
1587
+ database;
1588
+ clock;
1589
+ hub;
1590
+ capacityGuard;
1591
+ constructor(database, clock, hub, capacityGuard) {
1592
+ this.database = database;
1593
+ this.clock = clock;
1594
+ this.hub = hub;
1595
+ this.capacityGuard = capacityGuard;
1596
+ }
1597
+ scan(options = {}) {
1598
+ const now = this.clock();
1599
+ const silentBefore = new Date(now.getTime() - (options.silentMs ?? 600_000)).toISOString();
1600
+ const cooldownBefore = new Date(now.getTime() - (options.cooldownMs ?? 600_000)).toISOString();
1601
+ const limit = Math.max(1, Math.min(Math.trunc(options.limit ?? 25), 25));
1602
+ const candidates = this.database.prepare(`
1603
+ SELECT drone.id AS drone_id, drone.cube_id, COALESCE(state.attempts, 0) AS attempts
1604
+ FROM drones AS drone
1605
+ JOIN clients AS client ON client.id = drone.client_id AND client.revoked_at IS NULL
1606
+ JOIN client_cube_grants AS grant_row ON grant_row.client_id = drone.client_id
1607
+ AND grant_row.cube_id = drone.cube_id AND grant_row.access IN ('write', 'manage')
1608
+ LEFT JOIN silent_seat_ping_state AS state ON state.drone_id = drone.id
1609
+ WHERE drone.evicted_at IS NULL AND COALESCE(drone.last_seen, drone.created_at) < ?
1610
+ AND COALESCE(state.attempts, 0) < 3
1611
+ AND (state.last_ping_at IS NULL OR state.last_ping_at < ?)
1612
+ AND EXISTS (SELECT 1 FROM drone_sessions AS session
1613
+ WHERE session.drone_id = drone.id AND session.revoked_at IS NULL AND session.expires_at > ?)
1614
+ ORDER BY COALESCE(drone.last_seen, drone.created_at), drone.id LIMIT ?
1615
+ `).all(silentBefore, cooldownBefore, now.toISOString(), limit);
1616
+ const published = [];
1617
+ for (const candidate of candidates) {
1618
+ const droneId = requiredText(candidate, "drone_id");
1619
+ const cubeId = requiredText(candidate, "cube_id");
1620
+ const message = "[HEARTBEAT-PING] No recent activity; confirm this seat is responsive.";
1621
+ this.capacityGuard.assertCanGrow(256);
1622
+ const id = randomUUID();
1623
+ const createdAt = now.toISOString();
1624
+ this.database.exec("BEGIN IMMEDIATE");
1625
+ try {
1626
+ this.database.prepare(`INSERT INTO silent_seat_ping_state (drone_id, attempts, last_ping_at)
1627
+ VALUES (?, 1, ?) ON CONFLICT(drone_id) DO UPDATE
1628
+ SET attempts = attempts + 1, last_ping_at = excluded.last_ping_at
1629
+ `).run(droneId, now.toISOString());
1630
+ this.database.exec("COMMIT");
1631
+ }
1632
+ catch (error) {
1633
+ try {
1634
+ this.database.exec("ROLLBACK");
1635
+ }
1636
+ catch { /* Preserve original failure. */ }
1637
+ throw error;
1638
+ }
1639
+ const entry = {
1640
+ kind: "heartbeat_ping",
1641
+ id, cube_id: cubeId, drone_id: null, message, visibility: "direct", created_at: createdAt,
1642
+ drone_label: null, role_name: null, recipient_drone_ids: [droneId],
1643
+ };
1644
+ this.hub.publish(cubeId, entry);
1645
+ published.push(entry);
1646
+ }
1647
+ return published;
1648
+ }
1649
+ }
1278
1650
  class SqliteCredentialStore {
1279
1651
  #database;
1280
1652
  #clock;
@@ -1566,9 +1938,8 @@ class SqliteCredentialStore {
1566
1938
  SELECT credential.id, credential.lookup_digest, credential.verifier_digest,
1567
1939
  credential.session_id, session.client_id, session.cube_id, session.drone_id,
1568
1940
  session.expires_at,
1569
- COALESCE(
1570
- credential.revoked_at, session.revoked_at, client.revoked_at, drone.evicted_at
1571
- ) AS revoked_at
1941
+ COALESCE(credential.revoked_at, session.revoked_at, client.revoked_at) AS revoked_at,
1942
+ drone.evicted_at
1572
1943
  FROM drone_session_credentials AS credential
1573
1944
  JOIN drone_sessions AS session ON session.id = credential.session_id
1574
1945
  JOIN clients AS client ON client.id = session.client_id
@@ -1579,6 +1950,20 @@ class SqliteCredentialStore {
1579
1950
  `).get(lookup);
1580
1951
  return row === undefined ? null : storedDroneSessionDigest(row);
1581
1952
  }
1953
+ findActiveDroneSessionExpiry(sessionId) {
1954
+ assertCanonicalUuid(sessionId, "Drone session id");
1955
+ const row = this.#database.prepare(`
1956
+ SELECT session.expires_at
1957
+ FROM drone_sessions AS session
1958
+ JOIN clients AS client ON client.id = session.client_id
1959
+ JOIN drones AS drone ON drone.id = session.drone_id
1960
+ AND drone.client_id = session.client_id
1961
+ AND drone.cube_id = session.cube_id
1962
+ WHERE session.id = ? AND session.revoked_at IS NULL
1963
+ AND client.revoked_at IS NULL AND drone.evicted_at IS NULL
1964
+ `).get(sessionId);
1965
+ return row === undefined ? null : requiredText(row, "expires_at");
1966
+ }
1582
1967
  rotateClientCredential(input) {
1583
1968
  assertCanonicalUuid(input.clientId, "Client id");
1584
1969
  assertCanonicalUuid(input.credentialId, "Credential id");
@@ -1594,6 +1979,16 @@ class SqliteCredentialStore {
1594
1979
  this.#database.prepare(`
1595
1980
  UPDATE client_credentials SET revoked_at = ?
1596
1981
  WHERE client_id = ? AND revoked_at IS NULL
1982
+ `).run(now, input.clientId);
1983
+ this.#database.prepare(`
1984
+ UPDATE drone_session_credentials SET revoked_at = ?
1985
+ WHERE revoked_at IS NULL AND session_id IN (
1986
+ SELECT id FROM drone_sessions WHERE client_id = ?
1987
+ )
1988
+ `).run(now, input.clientId);
1989
+ this.#database.prepare(`
1990
+ UPDATE drone_sessions SET revoked_at = ?
1991
+ WHERE client_id = ? AND revoked_at IS NULL
1597
1992
  `).run(now, input.clientId);
1598
1993
  this.#database.prepare(`
1599
1994
  INSERT INTO client_credentials (
@@ -1752,6 +2147,7 @@ function cubeRecord(row) {
1752
2147
  ownerId: row.owner_id,
1753
2148
  name: row.name,
1754
2149
  directive: row.directive,
2150
+ messageTaxonomy: parseTaxonomy(row.message_taxonomy),
1755
2151
  createdAt: row.created_at,
1756
2152
  updatedAt: row.updated_at,
1757
2153
  };
@@ -1773,6 +2169,7 @@ function cubeRow(row) {
1773
2169
  owner_id: requiredText(row, "owner_id"),
1774
2170
  name: requiredText(row, "name"),
1775
2171
  directive: requiredText(row, "directive"),
2172
+ message_taxonomy: nullableText(row, "message_taxonomy"),
1776
2173
  created_at: requiredText(row, "created_at"),
1777
2174
  updated_at: requiredText(row, "updated_at"),
1778
2175
  };
@@ -1972,6 +2369,7 @@ function storedDroneSessionDigest(row) {
1972
2369
  cubeId: requiredText(row, "cube_id"),
1973
2370
  droneId: requiredText(row, "drone_id"),
1974
2371
  expiresAt: requiredText(row, "expires_at"),
2372
+ evictedAt: nullableText(row, "evicted_at"),
1975
2373
  };
1976
2374
  }
1977
2375
  function requiredBuffer(row, key) {
@@ -2050,6 +2448,33 @@ function validateDirective(value) {
2050
2448
  throw new Error("Cube directive exceeds 100000 bytes.");
2051
2449
  }
2052
2450
  }
2451
+ function activityPreview(message) {
2452
+ return message.replace(/\s+/gu, " ").trim().slice(0, 200);
2453
+ }
2454
+ function validateRoleClass(value) {
2455
+ if (value !== undefined && value !== "queen" && value !== "worker") {
2456
+ throw new TypeError("Role class must be queen or worker.");
2457
+ }
2458
+ }
2459
+ function serializeTaxonomy(value) {
2460
+ if (value === null)
2461
+ return null;
2462
+ const serialized = JSON.stringify(value);
2463
+ if (Buffer.byteLength(serialized) > 100_000) {
2464
+ throw new RangeError("Message taxonomy exceeds 100000 bytes.");
2465
+ }
2466
+ return serialized;
2467
+ }
2468
+ function parseTaxonomy(value) {
2469
+ if (value === null)
2470
+ return null;
2471
+ try {
2472
+ return validateMessageTaxonomy(JSON.parse(value));
2473
+ }
2474
+ catch {
2475
+ throw new Error("Database contains invalid message taxonomy.");
2476
+ }
2477
+ }
2053
2478
  function validateDigest(digest) {
2054
2479
  validateLookup(digest.lookup);
2055
2480
  if (digest.verifier.length !== 32)