borgmcp-server 0.1.4 → 0.1.5
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.
- package/README.md +7 -6
- package/THIRD_PARTY_NOTICES.md +1 -0
- package/dist/coordination-api.d.ts +2 -1
- package/dist/coordination-api.js +269 -85
- package/dist/coordination-api.js.map +1 -1
- package/dist/credentials.d.ts +4 -6
- package/dist/credentials.js +43 -19
- package/dist/credentials.js.map +1 -1
- package/dist/debug-log.d.ts +2 -3
- package/dist/debug-log.js +2 -3
- package/dist/debug-log.js.map +1 -1
- package/dist/enrollment.d.ts +3 -11
- package/dist/enrollment.js +21 -60
- package/dist/enrollment.js.map +1 -1
- package/dist/https-server.d.ts +2 -20
- package/dist/https-server.js +33 -51
- package/dist/https-server.js.map +1 -1
- package/dist/message-taxonomy.d.ts +38 -0
- package/dist/message-taxonomy.js +218 -0
- package/dist/message-taxonomy.js.map +1 -0
- package/dist/migrations.js +28 -0
- package/dist/migrations.js.map +1 -1
- package/dist/service.d.ts +4 -1
- package/dist/service.js +25 -10
- package/dist/service.js.map +1 -1
- package/dist/store.d.ts +42 -8
- package/dist/store.js +380 -100
- package/dist/store.js.map +1 -1
- package/npm-shrinkwrap.json +6 -7
- package/package.json +2 -2
- package/src/coordination-api.ts +278 -82
- package/src/credentials.ts +44 -26
- package/src/debug-log.ts +5 -5
- package/src/enrollment.ts +32 -78
- package/src/https-server.ts +44 -82
- package/src/message-taxonomy.ts +284 -0
- package/src/migrations.ts +28 -0
- package/src/service.ts +29 -9
- package/src/store.ts +432 -121
- package/dist/protocol-draft.d.ts +0 -2
- package/dist/protocol-draft.js +0 -31
- package/dist/protocol-draft.js.map +0 -1
- 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,
|
|
@@ -60,11 +61,11 @@ export class CursorExpiredError extends Error {
|
|
|
60
61
|
this.name = "CursorExpiredError";
|
|
61
62
|
}
|
|
62
63
|
}
|
|
63
|
-
export class
|
|
64
|
-
code = "
|
|
64
|
+
export class AttachSessionRejectedError extends Error {
|
|
65
|
+
code = "SESSION_REJECTED";
|
|
65
66
|
constructor() {
|
|
66
|
-
super("The
|
|
67
|
-
this.name = "
|
|
67
|
+
super("The saved session is not accepted for this seat.");
|
|
68
|
+
this.name = "AttachSessionRejectedError";
|
|
68
69
|
}
|
|
69
70
|
}
|
|
70
71
|
export class StorageCapacityError extends Error {
|
|
@@ -150,9 +151,10 @@ export async function openStore(options) {
|
|
|
150
151
|
throw new Error("SQLite page size is unavailable.");
|
|
151
152
|
}
|
|
152
153
|
const capacityGuard = new StorageCapacityGuard(storageLimits, capacityProbe, requiredInteger(pageRow, "page_size"));
|
|
154
|
+
const activityHub = new ActivityHub();
|
|
153
155
|
const maintenance = new SqliteMaintenanceStore(database, clock);
|
|
156
|
+
const liveness = new SqliteLivenessStore(database, clock, activityHub, capacityGuard);
|
|
154
157
|
const credentials = new SqliteCredentialStore(database, clock, capacityGuard, options.mutationHook);
|
|
155
|
-
const activityHub = new ActivityHub();
|
|
156
158
|
return Object.freeze({
|
|
157
159
|
forPrincipal: (principal) => {
|
|
158
160
|
assertServerDerivedPrincipal(principal);
|
|
@@ -160,6 +162,7 @@ export async function openStore(options) {
|
|
|
160
162
|
},
|
|
161
163
|
maintenance,
|
|
162
164
|
credentials,
|
|
165
|
+
liveness,
|
|
163
166
|
diagnostics: () => diagnostics(database),
|
|
164
167
|
close: () => database.close(),
|
|
165
168
|
});
|
|
@@ -267,7 +270,8 @@ class SqliteScopedStore {
|
|
|
267
270
|
listCubes() {
|
|
268
271
|
const scope = this.#scope("read");
|
|
269
272
|
const rows = this.#database.prepare(`
|
|
270
|
-
SELECT c.id, c.owner_id, c.name, c.directive, c.
|
|
273
|
+
SELECT c.id, c.owner_id, c.name, c.directive, c.message_taxonomy,
|
|
274
|
+
c.created_at, c.updated_at
|
|
271
275
|
FROM cubes AS c
|
|
272
276
|
WHERE ${scope.sql}
|
|
273
277
|
ORDER BY c.id
|
|
@@ -278,25 +282,43 @@ class SqliteScopedStore {
|
|
|
278
282
|
assertCanonicalUuid(cubeId, "Cube id");
|
|
279
283
|
const scope = this.#scope("read");
|
|
280
284
|
const row = this.#database.prepare(`
|
|
281
|
-
SELECT c.id, c.owner_id, c.name, c.directive, c.
|
|
285
|
+
SELECT c.id, c.owner_id, c.name, c.directive, c.message_taxonomy,
|
|
286
|
+
c.created_at, c.updated_at
|
|
282
287
|
FROM cubes AS c
|
|
283
288
|
WHERE c.id = ? AND ${scope.sql}
|
|
284
289
|
`).get(cubeId, ...scope.parameters);
|
|
285
290
|
return row === undefined ? null : cubeRecord(cubeRow(row));
|
|
286
291
|
}
|
|
287
292
|
updateDirective(cubeId, directive) {
|
|
293
|
+
this.updateCube(cubeId, { directive });
|
|
294
|
+
}
|
|
295
|
+
updateCube(cubeId, input) {
|
|
288
296
|
assertCanonicalUuid(cubeId, "Cube id");
|
|
289
|
-
|
|
297
|
+
if (input.directive === undefined && input.messageTaxonomy === undefined) {
|
|
298
|
+
throw new TypeError("At least one cube field is required.");
|
|
299
|
+
}
|
|
300
|
+
if (input.directive !== undefined)
|
|
301
|
+
validateDirective(input.directive);
|
|
302
|
+
const taxonomy = input.messageTaxonomy === undefined
|
|
303
|
+
? undefined
|
|
304
|
+
: validateMessageTaxonomy(input.messageTaxonomy);
|
|
305
|
+
const serializedTaxonomy = taxonomy === undefined ? undefined : serializeTaxonomy(taxonomy);
|
|
290
306
|
const scope = this.#scope("manage");
|
|
291
307
|
this.#requireCube(cubeId, "manage");
|
|
292
|
-
this.#capacityGuard.assertCanGrow(Buffer.byteLength(directive));
|
|
308
|
+
this.#capacityGuard.assertCanGrow(Buffer.byteLength(input.directive ?? "") + Buffer.byteLength(serializedTaxonomy ?? ""));
|
|
293
309
|
const result = this.#database.prepare(`
|
|
294
310
|
UPDATE cubes AS c
|
|
295
|
-
SET directive = ?,
|
|
311
|
+
SET directive = COALESCE(?, directive),
|
|
312
|
+
message_taxonomy = CASE WHEN ? = 1 THEN ? ELSE message_taxonomy END,
|
|
313
|
+
updated_at = ?
|
|
296
314
|
WHERE c.id = ? AND ${scope.sql}
|
|
297
|
-
`).run(directive, this.#now(), cubeId, ...scope.parameters);
|
|
315
|
+
`).run(input.directive ?? null, serializedTaxonomy === undefined ? 0 : 1, serializedTaxonomy ?? null, this.#now(), cubeId, ...scope.parameters);
|
|
298
316
|
if (result.changes !== 1)
|
|
299
317
|
throw new ScopedStoreError();
|
|
318
|
+
const cube = this.getCube(cubeId);
|
|
319
|
+
if (cube === null)
|
|
320
|
+
throw new ScopedStoreError();
|
|
321
|
+
return cube;
|
|
300
322
|
}
|
|
301
323
|
appendActivity(cubeId, message) {
|
|
302
324
|
const entry = this.appendLog(cubeId, { message });
|
|
@@ -354,6 +376,7 @@ class SqliteScopedStore {
|
|
|
354
376
|
if (value !== undefined && typeof value !== "boolean")
|
|
355
377
|
throw new TypeError("Role flags must be boolean.");
|
|
356
378
|
}
|
|
379
|
+
validateRoleClass(input.roleClass);
|
|
357
380
|
const isDefault = input.isDefault ?? false;
|
|
358
381
|
const isMandatory = input.isMandatory ?? false;
|
|
359
382
|
const isHumanSeat = input.isHumanSeat ?? false;
|
|
@@ -379,8 +402,8 @@ class SqliteScopedStore {
|
|
|
379
402
|
id, cube_id, name, short_description, detailed_description,
|
|
380
403
|
is_default, is_mandatory, is_human_seat, can_broadcast,
|
|
381
404
|
receives_all_direct, role_class, created_at
|
|
382
|
-
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?,
|
|
383
|
-
`).run(id, cubeId, input.name, shortDescription, detailedDescription, booleanInteger(isDefault), booleanInteger(isMandatory), booleanInteger(isHumanSeat), booleanInteger(canBroadcast), booleanInteger(receivesAllDirect), this.#now());
|
|
405
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
406
|
+
`).run(id, cubeId, input.name, shortDescription, detailedDescription, booleanInteger(isDefault), booleanInteger(isMandatory), booleanInteger(isHumanSeat), booleanInteger(canBroadcast), booleanInteger(receivesAllDirect), input.roleClass ?? "worker", this.#now());
|
|
384
407
|
this.#mutationHook?.("role.insert");
|
|
385
408
|
const row = this.#database.prepare(`
|
|
386
409
|
SELECT id, cube_id, name, short_description, detailed_description,
|
|
@@ -425,6 +448,7 @@ class SqliteScopedStore {
|
|
|
425
448
|
if (value !== undefined && typeof value !== "boolean")
|
|
426
449
|
throw new TypeError("Role flags must be boolean.");
|
|
427
450
|
}
|
|
451
|
+
validateRoleClass(input.roleClass);
|
|
428
452
|
this.#requireCube(cubeId, "manage");
|
|
429
453
|
this.#capacityGuard.assertCanGrow(Buffer.byteLength(input.name ?? "") + Buffer.byteLength(input.shortDescription ?? "") +
|
|
430
454
|
Buffer.byteLength(input.detailedDescription ?? "") + 8_192);
|
|
@@ -458,9 +482,10 @@ class SqliteScopedStore {
|
|
|
458
482
|
this.#database.prepare(`
|
|
459
483
|
UPDATE roles SET
|
|
460
484
|
name = ?, short_description = ?, detailed_description = ?, is_default = ?,
|
|
461
|
-
is_mandatory = ?, is_human_seat = ?, can_broadcast = ?, receives_all_direct =
|
|
485
|
+
is_mandatory = ?, is_human_seat = ?, can_broadcast = ?, receives_all_direct = ?,
|
|
486
|
+
role_class = ?
|
|
462
487
|
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);
|
|
488
|
+
`).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
489
|
const row = this.#database.prepare(`
|
|
465
490
|
SELECT id, cube_id, name, short_description, detailed_description,
|
|
466
491
|
is_default, is_mandatory, is_human_seat, can_broadcast,
|
|
@@ -547,6 +572,49 @@ class SqliteScopedStore {
|
|
|
547
572
|
`).all(cubeId);
|
|
548
573
|
return rows.map(droneRecord);
|
|
549
574
|
}
|
|
575
|
+
listDronesSince(cubeId, since) {
|
|
576
|
+
this.#requireCube(cubeId, "read");
|
|
577
|
+
let createdAt;
|
|
578
|
+
let anchorId = null;
|
|
579
|
+
if (/^[0-9a-f]{8}-[0-9a-f-]{27}$/iu.test(since)) {
|
|
580
|
+
const anchor = this.#database.prepare("SELECT id, created_at FROM activity_log WHERE id = ? AND cube_id = ?").get(since, cubeId);
|
|
581
|
+
if (anchor === undefined)
|
|
582
|
+
throw new ScopedStoreError();
|
|
583
|
+
anchorId = requiredText(anchor, "id");
|
|
584
|
+
createdAt = requiredText(anchor, "created_at");
|
|
585
|
+
}
|
|
586
|
+
else {
|
|
587
|
+
validateTimestamp(since);
|
|
588
|
+
createdAt = since;
|
|
589
|
+
}
|
|
590
|
+
const rows = this.#database.prepare(`
|
|
591
|
+
SELECT drone.id, drone.cube_id, drone.role_id, drone.label,
|
|
592
|
+
COALESCE(drone.last_seen, drone.created_at) AS last_seen,
|
|
593
|
+
drone.hostname, drone.created_at,
|
|
594
|
+
CASE WHEN client.revoked_at IS NULL AND grant_row.access IN ('write', 'manage')
|
|
595
|
+
THEN 'participant' ELSE 'observer' END AS posture,
|
|
596
|
+
(SELECT MAX(post.created_at) FROM activity_log AS post
|
|
597
|
+
WHERE post.cube_id = drone.cube_id AND post.drone_id = drone.id) AS last_log_post_at,
|
|
598
|
+
EXISTS (SELECT 1 FROM activity_log AS response
|
|
599
|
+
WHERE response.cube_id = drone.cube_id AND response.drone_id = drone.id
|
|
600
|
+
AND (response.created_at > ? OR
|
|
601
|
+
(? IS NOT NULL AND response.created_at = ? AND response.id > ?))) AS seen_since
|
|
602
|
+
FROM drones AS drone
|
|
603
|
+
LEFT JOIN clients AS client ON client.id = drone.client_id
|
|
604
|
+
LEFT JOIN client_cube_grants AS grant_row
|
|
605
|
+
ON grant_row.client_id = drone.client_id AND grant_row.cube_id = drone.cube_id
|
|
606
|
+
WHERE drone.cube_id = ? AND drone.evicted_at IS NULL
|
|
607
|
+
ORDER BY drone.label, drone.id
|
|
608
|
+
`).all(createdAt, anchorId, createdAt, anchorId, cubeId);
|
|
609
|
+
return {
|
|
610
|
+
since: createdAt,
|
|
611
|
+
drones: rows.map((row) => ({
|
|
612
|
+
...droneRecord(row),
|
|
613
|
+
last_log_post_at: nullableText(row, "last_log_post_at"),
|
|
614
|
+
seen_since: requiredInteger(row, "seen_since") === 1,
|
|
615
|
+
})),
|
|
616
|
+
};
|
|
617
|
+
}
|
|
550
618
|
appendLog(cubeId, input) {
|
|
551
619
|
assertCanonicalUuid(cubeId, "Cube id");
|
|
552
620
|
if (input.message.length === 0 || Buffer.byteLength(input.message) > 10_240) {
|
|
@@ -592,6 +660,12 @@ class SqliteScopedStore {
|
|
|
592
660
|
`).run(id, droneId, this.#principal.kind, this.#principal.id, input.message, createdAt, visibility, cubeId, ...scope.parameters);
|
|
593
661
|
if (inserted.changes !== 1)
|
|
594
662
|
throw new ScopedStoreError();
|
|
663
|
+
if (droneId !== null) {
|
|
664
|
+
this.#database.prepare(`
|
|
665
|
+
UPDATE drones SET last_seen = ? WHERE id = ? AND cube_id = ? AND evicted_at IS NULL
|
|
666
|
+
`).run(createdAt, droneId, cubeId);
|
|
667
|
+
this.#database.prepare("DELETE FROM silent_seat_ping_state WHERE drone_id = ?").run(droneId);
|
|
668
|
+
}
|
|
595
669
|
if (recipients.length > 0) {
|
|
596
670
|
const valid = this.#database.prepare(`
|
|
597
671
|
SELECT COUNT(*) AS count
|
|
@@ -669,18 +743,58 @@ class SqliteScopedStore {
|
|
|
669
743
|
assertCanonicalUuid(entryId, "Activity entry id");
|
|
670
744
|
if (kind !== "ack" && kind !== "claim")
|
|
671
745
|
throw new Error("Unknown acknowledgement kind.");
|
|
672
|
-
const
|
|
673
|
-
|
|
746
|
+
const original = this.#database.prepare(`
|
|
747
|
+
SELECT drone_id, message, visibility FROM activity_log WHERE id = ? AND cube_id = ?
|
|
748
|
+
`).get(entryId, cubeId);
|
|
749
|
+
if (original === undefined)
|
|
674
750
|
throw new ScopedStoreError();
|
|
675
751
|
this.#capacityGuard.assertCanGrow(512);
|
|
676
752
|
const claimant = this.#principal.kind === "drone-session"
|
|
677
753
|
? this.#principal.droneId
|
|
678
754
|
: this.#principal.id;
|
|
679
|
-
this.#
|
|
755
|
+
const ackAt = this.#now();
|
|
756
|
+
const inserted = this.#database.prepare(`
|
|
680
757
|
INSERT OR IGNORE INTO activity_acks (
|
|
681
758
|
entry_id, principal_kind, principal_id, kind, created_at, claimant_drone_id
|
|
682
759
|
) VALUES (?, ?, ?, ?, ?, ?)
|
|
683
|
-
`).run(entryId, this.#principal.kind, this.#principal.id, kind,
|
|
760
|
+
`).run(entryId, this.#principal.kind, this.#principal.id, kind, ackAt, claimant);
|
|
761
|
+
if (inserted.changes !== 1 || this.#principal.kind !== "drone-session")
|
|
762
|
+
return;
|
|
763
|
+
const claimantDroneId = this.#principal.droneId;
|
|
764
|
+
const actor = this.#database.prepare(`
|
|
765
|
+
SELECT drone.label, role.name AS role_name
|
|
766
|
+
FROM drones AS drone JOIN roles AS role ON role.id = drone.role_id
|
|
767
|
+
WHERE drone.id = ? AND drone.cube_id = ? AND drone.evicted_at IS NULL
|
|
768
|
+
`).get(claimantDroneId, cubeId);
|
|
769
|
+
const preview = activityPreview(requiredText(original, "message"));
|
|
770
|
+
const originalAuthor = nullableText(original, "drone_id");
|
|
771
|
+
const recipients = kind === "ack"
|
|
772
|
+
? originalAuthor === null ? [] : [originalAuthor]
|
|
773
|
+
: this.#claimAudience(cubeId, entryId, requiredText(original, "visibility"))
|
|
774
|
+
.filter((droneId) => droneId !== claimantDroneId);
|
|
775
|
+
if (recipients.length === 0)
|
|
776
|
+
return;
|
|
777
|
+
const roleName = actor === undefined ? null : requiredText(actor, "role_name");
|
|
778
|
+
const notification = {
|
|
779
|
+
kind,
|
|
780
|
+
id: randomUUID(),
|
|
781
|
+
cube_id: cubeId,
|
|
782
|
+
drone_id: claimantDroneId,
|
|
783
|
+
drone_label: actor === undefined ? null : requiredText(actor, "label"),
|
|
784
|
+
role_name: roleName,
|
|
785
|
+
message: `[${kind.toUpperCase()}] ${preview}`,
|
|
786
|
+
created_at: ackAt,
|
|
787
|
+
visibility: "direct",
|
|
788
|
+
recipient_drone_ids: recipients,
|
|
789
|
+
log_entry_id: entryId,
|
|
790
|
+
ack_kind: kind,
|
|
791
|
+
ack_at: ackAt,
|
|
792
|
+
entry_preview: preview,
|
|
793
|
+
...(kind === "ack"
|
|
794
|
+
? { author_drone_id: recipients[0] }
|
|
795
|
+
: { claimant_drone_id: claimantDroneId, claimant_role: roleName }),
|
|
796
|
+
};
|
|
797
|
+
this.#activityHub.publish(cubeId, notification);
|
|
684
798
|
}
|
|
685
799
|
recordDecision(cubeId, input) {
|
|
686
800
|
this.#requireCube(cubeId, "manage");
|
|
@@ -730,7 +844,6 @@ class SqliteScopedStore {
|
|
|
730
844
|
throw new ScopedStoreError();
|
|
731
845
|
assertCanonicalUuid(input.cubeId, "Cube id");
|
|
732
846
|
assertCanonicalUuid(input.roleId, "Role id");
|
|
733
|
-
assertCanonicalUuid(input.retryKey, "Retry key");
|
|
734
847
|
if (input.priorDroneId !== undefined)
|
|
735
848
|
assertCanonicalUuid(input.priorDroneId, "Prior drone id");
|
|
736
849
|
assertCanonicalUuid(input.droneId, "Drone id");
|
|
@@ -758,82 +871,95 @@ class SqliteScopedStore {
|
|
|
758
871
|
`).get(input.cubeId, input.roleId, ...scope.parameters);
|
|
759
872
|
if (roleRow === undefined)
|
|
760
873
|
throw new ScopedStoreError();
|
|
761
|
-
const
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
JOIN drones AS drone ON drone.id =
|
|
769
|
-
|
|
770
|
-
|
|
874
|
+
const bound = this.#database.prepare(`
|
|
875
|
+
SELECT credential.verifier_digest, credential.revoked_at AS credential_revoked_at,
|
|
876
|
+
session.id AS session_id, session.client_id, session.cube_id, session.drone_id,
|
|
877
|
+
session.expires_at, session.revoked_at AS session_revoked_at,
|
|
878
|
+
drone.role_id, drone.label, drone.evicted_at
|
|
879
|
+
FROM drone_session_credentials AS credential
|
|
880
|
+
JOIN drone_sessions AS session ON session.id = credential.session_id
|
|
881
|
+
JOIN drones AS drone ON drone.id = session.drone_id
|
|
882
|
+
AND drone.client_id = session.client_id AND drone.cube_id = session.cube_id
|
|
883
|
+
WHERE credential.lookup_digest = ?
|
|
884
|
+
`).get(input.credentialDigest.lookup);
|
|
885
|
+
if (bound !== undefined) {
|
|
886
|
+
const verifier = requiredBuffer(bound, "verifier_digest");
|
|
887
|
+
if (!timingSafeEqual(verifier, input.credentialDigest.verifier) ||
|
|
888
|
+
optionalText(bound, "credential_revoked_at") !== null ||
|
|
889
|
+
optionalText(bound, "session_revoked_at") !== null ||
|
|
890
|
+
optionalText(bound, "evicted_at") !== null ||
|
|
891
|
+
requiredText(bound, "expires_at") <= now ||
|
|
892
|
+
requiredText(bound, "client_id") !== this.#principal.id ||
|
|
893
|
+
requiredText(bound, "cube_id") !== input.cubeId ||
|
|
894
|
+
requiredText(bound, "role_id") !== input.roleId ||
|
|
895
|
+
(input.priorDroneId !== undefined &&
|
|
896
|
+
requiredText(bound, "drone_id") !== input.priorDroneId)) {
|
|
897
|
+
throw new AttachSessionRejectedError();
|
|
898
|
+
}
|
|
899
|
+
const droneId = requiredText(bound, "drone_id");
|
|
900
|
+
this.#database.prepare("UPDATE drones SET last_seen = ? WHERE id = ?").run(now, droneId);
|
|
901
|
+
this.#database.prepare("DELETE FROM silent_seat_ping_state WHERE drone_id = ?").run(droneId);
|
|
902
|
+
const attachedRoleRow = this.#database.prepare(`
|
|
903
|
+
SELECT id AS role_id, name AS role_name, role_class, is_human_seat
|
|
904
|
+
FROM roles WHERE id = ? AND cube_id = ?
|
|
905
|
+
`).get(requiredText(bound, "role_id"), input.cubeId);
|
|
906
|
+
if (attachedRoleRow === undefined)
|
|
907
|
+
throw new Error("Attached drone role is unavailable.");
|
|
908
|
+
const roleClass = requiredText(attachedRoleRow, "role_class");
|
|
909
|
+
if (roleClass !== "queen" && roleClass !== "worker") {
|
|
910
|
+
throw new Error("Database contains invalid role class.");
|
|
911
|
+
}
|
|
912
|
+
this.#database.exec("COMMIT");
|
|
913
|
+
return {
|
|
914
|
+
cube: {
|
|
915
|
+
id: requiredText(roleRow, "cube_id"),
|
|
916
|
+
name: requiredText(roleRow, "cube_name"),
|
|
917
|
+
},
|
|
918
|
+
role: {
|
|
919
|
+
id: requiredText(attachedRoleRow, "role_id"),
|
|
920
|
+
name: requiredText(attachedRoleRow, "role_name"),
|
|
921
|
+
role_class: roleClass,
|
|
922
|
+
is_human_seat: requiredInteger(attachedRoleRow, "is_human_seat") === 1,
|
|
923
|
+
},
|
|
924
|
+
drone: { id: droneId, label: requiredText(bound, "label") },
|
|
925
|
+
sessionId: requiredText(bound, "session_id"),
|
|
926
|
+
expiresAt: requiredText(bound, "expires_at"),
|
|
927
|
+
result: "reused",
|
|
928
|
+
};
|
|
929
|
+
}
|
|
930
|
+
const priorSeat = input.priorDroneId === undefined ? undefined : this.#database.prepare(`
|
|
931
|
+
SELECT id, role_id, label FROM drones
|
|
932
|
+
WHERE id = ? AND client_id = ? AND cube_id = ? AND evicted_at IS NULL
|
|
933
|
+
`).get(input.priorDroneId, this.#principal.id, input.cubeId);
|
|
771
934
|
let droneId;
|
|
772
935
|
let droneLabel;
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
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();
|
|
936
|
+
if (priorSeat !== undefined) {
|
|
937
|
+
const priorSeatId = requiredText(priorSeat, "id");
|
|
938
|
+
if (requiredText(priorSeat, "role_id") !== input.roleId) {
|
|
939
|
+
throw new AttachSessionRejectedError();
|
|
782
940
|
}
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
941
|
+
const occupied = this.#database.prepare(`
|
|
942
|
+
SELECT 1 FROM drone_sessions AS session
|
|
943
|
+
JOIN drone_session_credentials AS credential ON credential.session_id = session.id
|
|
944
|
+
WHERE session.drone_id = ? AND session.client_id = ? AND session.cube_id = ?
|
|
945
|
+
AND session.revoked_at IS NULL AND credential.revoked_at IS NULL
|
|
946
|
+
AND session.expires_at > ?
|
|
947
|
+
`).get(priorSeatId, this.#principal.id, input.cubeId, now);
|
|
948
|
+
if (occupied !== undefined)
|
|
949
|
+
throw new AttachSessionRejectedError();
|
|
950
|
+
droneId = priorSeatId;
|
|
951
|
+
droneLabel = requiredText(priorSeat, "label");
|
|
952
|
+
this.#database.prepare("UPDATE drones SET last_seen = ? WHERE id = ?").run(now, droneId);
|
|
790
953
|
}
|
|
791
954
|
else {
|
|
792
|
-
|
|
793
|
-
|
|
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
|
-
}
|
|
818
|
-
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);
|
|
955
|
+
droneId = input.droneId;
|
|
956
|
+
droneLabel = seatLabel(requiredText(roleRow, "role_name"), droneId);
|
|
832
957
|
this.#database.prepare(`
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
`).run(now,
|
|
958
|
+
INSERT INTO drones (id, cube_id, role_id, client_id, label, created_at, last_seen)
|
|
959
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
960
|
+
`).run(droneId, input.cubeId, input.roleId, this.#principal.id, droneLabel, now, now);
|
|
836
961
|
}
|
|
962
|
+
this.#database.prepare("DELETE FROM silent_seat_ping_state WHERE drone_id = ?").run(droneId);
|
|
837
963
|
this.#database.prepare(`
|
|
838
964
|
INSERT INTO drone_sessions (
|
|
839
965
|
id, client_id, cube_id, drone_id, created_at, expires_at
|
|
@@ -869,10 +995,7 @@ class SqliteScopedStore {
|
|
|
869
995
|
drone: { id: droneId, label: droneLabel },
|
|
870
996
|
sessionId: input.sessionId,
|
|
871
997
|
expiresAt: input.expiresAt,
|
|
872
|
-
|
|
873
|
-
posture,
|
|
874
|
-
reattached,
|
|
875
|
-
revokedSessionIds: oldSessions,
|
|
998
|
+
result: "created",
|
|
876
999
|
};
|
|
877
1000
|
}
|
|
878
1001
|
catch (error) {
|
|
@@ -886,10 +1009,41 @@ class SqliteScopedStore {
|
|
|
886
1009
|
subscribeActivity(cubeId, listener) {
|
|
887
1010
|
this.#requireCube(cubeId, "read");
|
|
888
1011
|
return this.#activityHub.subscribe(cubeId, (entry) => {
|
|
1012
|
+
if ("kind" in entry) {
|
|
1013
|
+
if (this.#principal.kind === "drone-session" &&
|
|
1014
|
+
entry.recipient_drone_ids.includes(this.#principal.droneId))
|
|
1015
|
+
listener(entry);
|
|
1016
|
+
return;
|
|
1017
|
+
}
|
|
889
1018
|
if (entry.visibility === "broadcast" || this.#allowsDirectedWork(cubeId))
|
|
890
1019
|
listener(entry);
|
|
891
1020
|
});
|
|
892
1021
|
}
|
|
1022
|
+
#claimAudience(cubeId, entryId, visibility) {
|
|
1023
|
+
if (visibility === "direct") {
|
|
1024
|
+
return this.#database.prepare(`
|
|
1025
|
+
SELECT recipient.drone_id
|
|
1026
|
+
FROM activity_log_recipients AS recipient
|
|
1027
|
+
JOIN drones AS drone ON drone.id = recipient.drone_id
|
|
1028
|
+
JOIN clients AS client ON client.id = drone.client_id
|
|
1029
|
+
JOIN client_cube_grants AS grant_row
|
|
1030
|
+
ON grant_row.client_id = drone.client_id AND grant_row.cube_id = drone.cube_id
|
|
1031
|
+
WHERE recipient.entry_id = ? AND drone.cube_id = ? AND drone.evicted_at IS NULL
|
|
1032
|
+
AND client.revoked_at IS NULL AND grant_row.access IN ('write', 'manage')
|
|
1033
|
+
ORDER BY recipient.drone_id
|
|
1034
|
+
`).all(entryId, cubeId).map((row) => requiredText(row, "drone_id"));
|
|
1035
|
+
}
|
|
1036
|
+
return this.#database.prepare(`
|
|
1037
|
+
SELECT drone.id AS drone_id
|
|
1038
|
+
FROM drones AS drone
|
|
1039
|
+
JOIN clients AS client ON client.id = drone.client_id
|
|
1040
|
+
JOIN client_cube_grants AS grant_row
|
|
1041
|
+
ON grant_row.client_id = drone.client_id AND grant_row.cube_id = drone.cube_id
|
|
1042
|
+
WHERE drone.cube_id = ? AND drone.evicted_at IS NULL AND client.revoked_at IS NULL
|
|
1043
|
+
AND grant_row.access IN ('write', 'manage')
|
|
1044
|
+
ORDER BY drone.id
|
|
1045
|
+
`).all(cubeId).map((row) => requiredText(row, "drone_id"));
|
|
1046
|
+
}
|
|
893
1047
|
#allowsDirectedWork(cubeId) {
|
|
894
1048
|
const scope = this.#scope("write");
|
|
895
1049
|
return this.#database.prepare(`
|
|
@@ -902,8 +1056,17 @@ class SqliteScopedStore {
|
|
|
902
1056
|
const row = this.#database.prepare(`
|
|
903
1057
|
SELECT 1 AS present FROM cubes AS c WHERE c.id = ? AND ${scope.sql}
|
|
904
1058
|
`).get(cubeId, ...scope.parameters);
|
|
905
|
-
if (row
|
|
906
|
-
|
|
1059
|
+
if (row !== undefined)
|
|
1060
|
+
return;
|
|
1061
|
+
if (access === "manage") {
|
|
1062
|
+
const visibleScope = this.#scope("read");
|
|
1063
|
+
const visible = this.#database.prepare(`
|
|
1064
|
+
SELECT 1 AS present FROM cubes AS c WHERE c.id = ? AND ${visibleScope.sql}
|
|
1065
|
+
`).get(cubeId, ...visibleScope.parameters);
|
|
1066
|
+
if (visible !== undefined)
|
|
1067
|
+
throw new AccessDeniedError();
|
|
1068
|
+
}
|
|
1069
|
+
throw new ScopedStoreError();
|
|
907
1070
|
}
|
|
908
1071
|
#nextActivityTimestamp(cubeId) {
|
|
909
1072
|
const now = this.#clock().getTime();
|
|
@@ -1132,7 +1295,7 @@ class SqliteMaintenanceStore {
|
|
|
1132
1295
|
for (const table of [
|
|
1133
1296
|
"cube_create_bindings", "activity_acks", "activity_log_recipients", "activity_log",
|
|
1134
1297
|
"decisions", "expired_activity_cursors", "drone_session_credentials", "drone_sessions",
|
|
1135
|
-
"
|
|
1298
|
+
"drones", "roles", "client_cube_grants", "cubes", "client_server_capabilities",
|
|
1136
1299
|
"enrollment_claims", "client_credentials", "owner_enrollment_state", "clients",
|
|
1137
1300
|
"enrollment_invitations",
|
|
1138
1301
|
])
|
|
@@ -1275,6 +1438,70 @@ class ActivityHub {
|
|
|
1275
1438
|
}
|
|
1276
1439
|
}
|
|
1277
1440
|
}
|
|
1441
|
+
class SqliteLivenessStore {
|
|
1442
|
+
database;
|
|
1443
|
+
clock;
|
|
1444
|
+
hub;
|
|
1445
|
+
capacityGuard;
|
|
1446
|
+
constructor(database, clock, hub, capacityGuard) {
|
|
1447
|
+
this.database = database;
|
|
1448
|
+
this.clock = clock;
|
|
1449
|
+
this.hub = hub;
|
|
1450
|
+
this.capacityGuard = capacityGuard;
|
|
1451
|
+
}
|
|
1452
|
+
scan(options = {}) {
|
|
1453
|
+
const now = this.clock();
|
|
1454
|
+
const silentBefore = new Date(now.getTime() - (options.silentMs ?? 600_000)).toISOString();
|
|
1455
|
+
const cooldownBefore = new Date(now.getTime() - (options.cooldownMs ?? 600_000)).toISOString();
|
|
1456
|
+
const limit = Math.max(1, Math.min(Math.trunc(options.limit ?? 25), 25));
|
|
1457
|
+
const candidates = this.database.prepare(`
|
|
1458
|
+
SELECT drone.id AS drone_id, drone.cube_id, COALESCE(state.attempts, 0) AS attempts
|
|
1459
|
+
FROM drones AS drone
|
|
1460
|
+
JOIN clients AS client ON client.id = drone.client_id AND client.revoked_at IS NULL
|
|
1461
|
+
JOIN client_cube_grants AS grant_row ON grant_row.client_id = drone.client_id
|
|
1462
|
+
AND grant_row.cube_id = drone.cube_id AND grant_row.access IN ('write', 'manage')
|
|
1463
|
+
LEFT JOIN silent_seat_ping_state AS state ON state.drone_id = drone.id
|
|
1464
|
+
WHERE drone.evicted_at IS NULL AND COALESCE(drone.last_seen, drone.created_at) < ?
|
|
1465
|
+
AND COALESCE(state.attempts, 0) < 3
|
|
1466
|
+
AND (state.last_ping_at IS NULL OR state.last_ping_at < ?)
|
|
1467
|
+
AND EXISTS (SELECT 1 FROM drone_sessions AS session
|
|
1468
|
+
WHERE session.drone_id = drone.id AND session.revoked_at IS NULL AND session.expires_at > ?)
|
|
1469
|
+
ORDER BY COALESCE(drone.last_seen, drone.created_at), drone.id LIMIT ?
|
|
1470
|
+
`).all(silentBefore, cooldownBefore, now.toISOString(), limit);
|
|
1471
|
+
const published = [];
|
|
1472
|
+
for (const candidate of candidates) {
|
|
1473
|
+
const droneId = requiredText(candidate, "drone_id");
|
|
1474
|
+
const cubeId = requiredText(candidate, "cube_id");
|
|
1475
|
+
const message = "[HEARTBEAT-PING] No recent activity; confirm this seat is responsive.";
|
|
1476
|
+
this.capacityGuard.assertCanGrow(256);
|
|
1477
|
+
const id = randomUUID();
|
|
1478
|
+
const createdAt = now.toISOString();
|
|
1479
|
+
this.database.exec("BEGIN IMMEDIATE");
|
|
1480
|
+
try {
|
|
1481
|
+
this.database.prepare(`INSERT INTO silent_seat_ping_state (drone_id, attempts, last_ping_at)
|
|
1482
|
+
VALUES (?, 1, ?) ON CONFLICT(drone_id) DO UPDATE
|
|
1483
|
+
SET attempts = attempts + 1, last_ping_at = excluded.last_ping_at
|
|
1484
|
+
`).run(droneId, now.toISOString());
|
|
1485
|
+
this.database.exec("COMMIT");
|
|
1486
|
+
}
|
|
1487
|
+
catch (error) {
|
|
1488
|
+
try {
|
|
1489
|
+
this.database.exec("ROLLBACK");
|
|
1490
|
+
}
|
|
1491
|
+
catch { /* Preserve original failure. */ }
|
|
1492
|
+
throw error;
|
|
1493
|
+
}
|
|
1494
|
+
const entry = {
|
|
1495
|
+
kind: "heartbeat_ping",
|
|
1496
|
+
id, cube_id: cubeId, drone_id: null, message, visibility: "direct", created_at: createdAt,
|
|
1497
|
+
drone_label: null, role_name: null, recipient_drone_ids: [droneId],
|
|
1498
|
+
};
|
|
1499
|
+
this.hub.publish(cubeId, entry);
|
|
1500
|
+
published.push(entry);
|
|
1501
|
+
}
|
|
1502
|
+
return published;
|
|
1503
|
+
}
|
|
1504
|
+
}
|
|
1278
1505
|
class SqliteCredentialStore {
|
|
1279
1506
|
#database;
|
|
1280
1507
|
#clock;
|
|
@@ -1566,9 +1793,8 @@ class SqliteCredentialStore {
|
|
|
1566
1793
|
SELECT credential.id, credential.lookup_digest, credential.verifier_digest,
|
|
1567
1794
|
credential.session_id, session.client_id, session.cube_id, session.drone_id,
|
|
1568
1795
|
session.expires_at,
|
|
1569
|
-
COALESCE(
|
|
1570
|
-
|
|
1571
|
-
) AS revoked_at
|
|
1796
|
+
COALESCE(credential.revoked_at, session.revoked_at, client.revoked_at) AS revoked_at,
|
|
1797
|
+
drone.evicted_at
|
|
1572
1798
|
FROM drone_session_credentials AS credential
|
|
1573
1799
|
JOIN drone_sessions AS session ON session.id = credential.session_id
|
|
1574
1800
|
JOIN clients AS client ON client.id = session.client_id
|
|
@@ -1579,6 +1805,20 @@ class SqliteCredentialStore {
|
|
|
1579
1805
|
`).get(lookup);
|
|
1580
1806
|
return row === undefined ? null : storedDroneSessionDigest(row);
|
|
1581
1807
|
}
|
|
1808
|
+
findActiveDroneSessionExpiry(sessionId) {
|
|
1809
|
+
assertCanonicalUuid(sessionId, "Drone session id");
|
|
1810
|
+
const row = this.#database.prepare(`
|
|
1811
|
+
SELECT session.expires_at
|
|
1812
|
+
FROM drone_sessions AS session
|
|
1813
|
+
JOIN clients AS client ON client.id = session.client_id
|
|
1814
|
+
JOIN drones AS drone ON drone.id = session.drone_id
|
|
1815
|
+
AND drone.client_id = session.client_id
|
|
1816
|
+
AND drone.cube_id = session.cube_id
|
|
1817
|
+
WHERE session.id = ? AND session.revoked_at IS NULL
|
|
1818
|
+
AND client.revoked_at IS NULL AND drone.evicted_at IS NULL
|
|
1819
|
+
`).get(sessionId);
|
|
1820
|
+
return row === undefined ? null : requiredText(row, "expires_at");
|
|
1821
|
+
}
|
|
1582
1822
|
rotateClientCredential(input) {
|
|
1583
1823
|
assertCanonicalUuid(input.clientId, "Client id");
|
|
1584
1824
|
assertCanonicalUuid(input.credentialId, "Credential id");
|
|
@@ -1594,6 +1834,16 @@ class SqliteCredentialStore {
|
|
|
1594
1834
|
this.#database.prepare(`
|
|
1595
1835
|
UPDATE client_credentials SET revoked_at = ?
|
|
1596
1836
|
WHERE client_id = ? AND revoked_at IS NULL
|
|
1837
|
+
`).run(now, input.clientId);
|
|
1838
|
+
this.#database.prepare(`
|
|
1839
|
+
UPDATE drone_session_credentials SET revoked_at = ?
|
|
1840
|
+
WHERE revoked_at IS NULL AND session_id IN (
|
|
1841
|
+
SELECT id FROM drone_sessions WHERE client_id = ?
|
|
1842
|
+
)
|
|
1843
|
+
`).run(now, input.clientId);
|
|
1844
|
+
this.#database.prepare(`
|
|
1845
|
+
UPDATE drone_sessions SET revoked_at = ?
|
|
1846
|
+
WHERE client_id = ? AND revoked_at IS NULL
|
|
1597
1847
|
`).run(now, input.clientId);
|
|
1598
1848
|
this.#database.prepare(`
|
|
1599
1849
|
INSERT INTO client_credentials (
|
|
@@ -1752,6 +2002,7 @@ function cubeRecord(row) {
|
|
|
1752
2002
|
ownerId: row.owner_id,
|
|
1753
2003
|
name: row.name,
|
|
1754
2004
|
directive: row.directive,
|
|
2005
|
+
messageTaxonomy: parseTaxonomy(row.message_taxonomy),
|
|
1755
2006
|
createdAt: row.created_at,
|
|
1756
2007
|
updatedAt: row.updated_at,
|
|
1757
2008
|
};
|
|
@@ -1773,6 +2024,7 @@ function cubeRow(row) {
|
|
|
1773
2024
|
owner_id: requiredText(row, "owner_id"),
|
|
1774
2025
|
name: requiredText(row, "name"),
|
|
1775
2026
|
directive: requiredText(row, "directive"),
|
|
2027
|
+
message_taxonomy: nullableText(row, "message_taxonomy"),
|
|
1776
2028
|
created_at: requiredText(row, "created_at"),
|
|
1777
2029
|
updated_at: requiredText(row, "updated_at"),
|
|
1778
2030
|
};
|
|
@@ -1972,6 +2224,7 @@ function storedDroneSessionDigest(row) {
|
|
|
1972
2224
|
cubeId: requiredText(row, "cube_id"),
|
|
1973
2225
|
droneId: requiredText(row, "drone_id"),
|
|
1974
2226
|
expiresAt: requiredText(row, "expires_at"),
|
|
2227
|
+
evictedAt: nullableText(row, "evicted_at"),
|
|
1975
2228
|
};
|
|
1976
2229
|
}
|
|
1977
2230
|
function requiredBuffer(row, key) {
|
|
@@ -2050,6 +2303,33 @@ function validateDirective(value) {
|
|
|
2050
2303
|
throw new Error("Cube directive exceeds 100000 bytes.");
|
|
2051
2304
|
}
|
|
2052
2305
|
}
|
|
2306
|
+
function activityPreview(message) {
|
|
2307
|
+
return message.replace(/\s+/gu, " ").trim().slice(0, 200);
|
|
2308
|
+
}
|
|
2309
|
+
function validateRoleClass(value) {
|
|
2310
|
+
if (value !== undefined && value !== "queen" && value !== "worker") {
|
|
2311
|
+
throw new TypeError("Role class must be queen or worker.");
|
|
2312
|
+
}
|
|
2313
|
+
}
|
|
2314
|
+
function serializeTaxonomy(value) {
|
|
2315
|
+
if (value === null)
|
|
2316
|
+
return null;
|
|
2317
|
+
const serialized = JSON.stringify(value);
|
|
2318
|
+
if (Buffer.byteLength(serialized) > 100_000) {
|
|
2319
|
+
throw new RangeError("Message taxonomy exceeds 100000 bytes.");
|
|
2320
|
+
}
|
|
2321
|
+
return serialized;
|
|
2322
|
+
}
|
|
2323
|
+
function parseTaxonomy(value) {
|
|
2324
|
+
if (value === null)
|
|
2325
|
+
return null;
|
|
2326
|
+
try {
|
|
2327
|
+
return validateMessageTaxonomy(JSON.parse(value));
|
|
2328
|
+
}
|
|
2329
|
+
catch {
|
|
2330
|
+
throw new Error("Database contains invalid message taxonomy.");
|
|
2331
|
+
}
|
|
2332
|
+
}
|
|
2053
2333
|
function validateDigest(digest) {
|
|
2054
2334
|
validateLookup(digest.lookup);
|
|
2055
2335
|
if (digest.verifier.length !== 32)
|