borgmcp-server 0.1.7 → 0.1.9
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 +61 -46
- package/SECURITY.md +12 -1
- package/THIRD_PARTY_NOTICES.md +1 -1
- package/dist/bootstrap.d.ts +8 -1
- package/dist/bootstrap.js +39 -6
- package/dist/bootstrap.js.map +1 -1
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +176 -1
- package/dist/cli.js.map +1 -1
- package/dist/coordination-api.js +10 -1
- package/dist/coordination-api.js.map +1 -1
- package/dist/credentials.d.ts +5 -1
- package/dist/credentials.js +35 -3
- package/dist/credentials.js.map +1 -1
- package/dist/debug-log.d.ts +2 -2
- package/dist/debug-log.js +2 -2
- package/dist/debug-log.js.map +1 -1
- package/dist/https-server.d.ts +5 -1
- package/dist/https-server.js +93 -27
- package/dist/https-server.js.map +1 -1
- package/dist/index.d.ts +14 -1
- package/dist/index.js +8 -0
- package/dist/index.js.map +1 -1
- package/dist/main.js +1 -0
- package/dist/main.js.map +1 -1
- package/dist/managed-service.d.ts +20 -0
- package/dist/managed-service.js +80 -0
- package/dist/managed-service.js.map +1 -0
- package/dist/migrations.js +23 -0
- package/dist/migrations.js.map +1 -1
- package/dist/portable-credential-store.d.ts +16 -0
- package/dist/portable-credential-store.js +305 -0
- package/dist/portable-credential-store.js.map +1 -0
- package/dist/registry-artifact.d.ts +11 -0
- package/dist/registry-artifact.js +99 -0
- package/dist/registry-artifact.js.map +1 -0
- package/dist/runtime-identity.d.ts +19 -0
- package/dist/runtime-identity.js +61 -0
- package/dist/runtime-identity.js.map +1 -0
- package/dist/runtime-lifecycle.d.ts +56 -0
- package/dist/runtime-lifecycle.js +486 -0
- package/dist/runtime-lifecycle.js.map +1 -0
- package/dist/runtime-operator.d.ts +24 -0
- package/dist/runtime-operator.js +95 -0
- package/dist/runtime-operator.js.map +1 -0
- package/dist/service.d.ts +53 -5
- package/dist/service.js +326 -15
- package/dist/service.js.map +1 -1
- package/dist/store.d.ts +16 -7
- package/dist/store.js +93 -83
- package/dist/store.js.map +1 -1
- package/npm-shrinkwrap.json +6 -6
- package/package.json +2 -2
- package/src/bootstrap.ts +46 -5
- package/src/cli.ts +171 -1
- package/src/coordination-api.ts +12 -0
- package/src/credentials.ts +36 -5
- package/src/debug-log.ts +4 -3
- package/src/https-server.ts +124 -27
- package/src/index.ts +46 -1
- package/src/main.ts +1 -0
- package/src/managed-service.ts +107 -0
- package/src/migrations.ts +23 -0
- package/src/portable-credential-store.ts +306 -0
- package/src/registry-artifact.ts +111 -0
- package/src/runtime-identity.ts +82 -0
- package/src/runtime-lifecycle.ts +567 -0
- package/src/runtime-operator.ts +124 -0
- package/src/service.ts +401 -20
- package/src/store.ts +100 -83
package/src/store.ts
CHANGED
|
@@ -65,7 +65,6 @@ export type ActivityNotificationRecord = EnrichedActivityRecord & (
|
|
|
65
65
|
readonly claimant_drone_id?: string;
|
|
66
66
|
readonly claimant_role?: string | null;
|
|
67
67
|
}
|
|
68
|
-
| { readonly kind: "heartbeat_ping" }
|
|
69
68
|
);
|
|
70
69
|
|
|
71
70
|
export type ActivityStreamRecord = EnrichedActivityRecord | ActivityNotificationRecord;
|
|
@@ -180,11 +179,7 @@ export interface StoreRuntime {
|
|
|
180
179
|
}
|
|
181
180
|
|
|
182
181
|
export interface LivenessStore {
|
|
183
|
-
readonly scan: (
|
|
184
|
-
readonly silentMs?: number;
|
|
185
|
-
readonly cooldownMs?: number;
|
|
186
|
-
readonly limit?: number;
|
|
187
|
-
}) => ActivityNotificationRecord[];
|
|
182
|
+
readonly scan: () => ActivityNotificationRecord[];
|
|
188
183
|
}
|
|
189
184
|
|
|
190
185
|
export interface DigestPair {
|
|
@@ -237,6 +232,7 @@ export interface StoredDroneSessionDigest extends StoredSecretDigest {
|
|
|
237
232
|
readonly droneId: string;
|
|
238
233
|
readonly expiresAt: string;
|
|
239
234
|
readonly evictedAt: string | null;
|
|
235
|
+
readonly takenOver: boolean;
|
|
240
236
|
}
|
|
241
237
|
|
|
242
238
|
export interface CredentialStore {
|
|
@@ -262,6 +258,7 @@ export interface CredentialStore {
|
|
|
262
258
|
readonly findClientCredential: (lookup: Buffer) => StoredSecretDigest | null;
|
|
263
259
|
readonly clientExists: (clientId: string) => boolean;
|
|
264
260
|
readonly clientIsActive: (clientId: string) => boolean;
|
|
261
|
+
readonly clientHasServerCapability: (clientId: string, capability: "create_cube") => boolean;
|
|
265
262
|
readonly findDroneSessionCredential: (lookup: Buffer) => StoredDroneSessionDigest | null;
|
|
266
263
|
readonly findActiveDroneSessionExpiry: (sessionId: string) => string | null;
|
|
267
264
|
readonly rotateClientCredential: (input: {
|
|
@@ -362,6 +359,7 @@ export interface SeatAttachInput {
|
|
|
362
359
|
readonly credentialId: string;
|
|
363
360
|
readonly credentialDigest: DigestPair;
|
|
364
361
|
readonly expiresAt: string;
|
|
362
|
+
readonly renewIfExpiresAtOrBefore: string;
|
|
365
363
|
}
|
|
366
364
|
|
|
367
365
|
export interface SeatAttachRecord {
|
|
@@ -527,6 +525,33 @@ export class AttachSessionRejectedError extends Error {
|
|
|
527
525
|
}
|
|
528
526
|
}
|
|
529
527
|
|
|
528
|
+
export class AttachSessionExpiredError extends Error {
|
|
529
|
+
readonly code = "AUTH_EXPIRED";
|
|
530
|
+
|
|
531
|
+
constructor() {
|
|
532
|
+
super("Authentication failed.");
|
|
533
|
+
this.name = "AttachSessionExpiredError";
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
export class AttachSessionRevokedError extends Error {
|
|
538
|
+
readonly code = "SESSION_REVOKED";
|
|
539
|
+
|
|
540
|
+
constructor() {
|
|
541
|
+
super("Authentication failed.");
|
|
542
|
+
this.name = "AttachSessionRevokedError";
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
export class AttachDroneEvictedError extends Error {
|
|
547
|
+
readonly code = "DRONE_EVICTED";
|
|
548
|
+
|
|
549
|
+
constructor() {
|
|
550
|
+
super("Authentication failed.");
|
|
551
|
+
this.name = "AttachDroneEvictedError";
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
|
|
530
555
|
export class StorageCapacityError extends Error {
|
|
531
556
|
readonly code = "CAPACITY_EXCEEDED";
|
|
532
557
|
|
|
@@ -645,7 +670,7 @@ export async function openStore(options: OpenStoreOptions): Promise<StoreRuntime
|
|
|
645
670
|
);
|
|
646
671
|
const activityHub = new ActivityHub();
|
|
647
672
|
const maintenance = new SqliteMaintenanceStore(database, clock);
|
|
648
|
-
const liveness = new
|
|
673
|
+
const liveness = new IdleLivenessStore();
|
|
649
674
|
const credentials = new SqliteCredentialStore(database, clock, capacityGuard, options.mutationHook);
|
|
650
675
|
return Object.freeze({
|
|
651
676
|
forPrincipal: (principal: Principal) => {
|
|
@@ -1283,7 +1308,6 @@ class SqliteScopedStore implements ScopedStore {
|
|
|
1283
1308
|
this.#database.prepare(`
|
|
1284
1309
|
UPDATE drones SET last_seen = ? WHERE id = ? AND cube_id = ? AND evicted_at IS NULL
|
|
1285
1310
|
`).run(createdAt, droneId, cubeId);
|
|
1286
|
-
this.#database.prepare("DELETE FROM silent_seat_ping_state WHERE drone_id = ?").run(droneId);
|
|
1287
1311
|
}
|
|
1288
1312
|
if (recipients.length > 0) {
|
|
1289
1313
|
const valid = this.#database.prepare(`
|
|
@@ -1470,6 +1494,7 @@ class SqliteScopedStore implements ScopedStore {
|
|
|
1470
1494
|
assertCanonicalUuid(input.credentialId, "Drone session credential id");
|
|
1471
1495
|
validateDigest(input.credentialDigest);
|
|
1472
1496
|
validateTimestamp(input.expiresAt);
|
|
1497
|
+
validateTimestamp(input.renewIfExpiresAtOrBefore);
|
|
1473
1498
|
const scope = this.#scope("read");
|
|
1474
1499
|
const authorizedRole = this.#database.prepare(`
|
|
1475
1500
|
SELECT 1 FROM cubes AS c JOIN roles AS role ON role.cube_id = c.id
|
|
@@ -1492,7 +1517,7 @@ class SqliteScopedStore implements ScopedStore {
|
|
|
1492
1517
|
SELECT credential.verifier_digest, credential.revoked_at AS credential_revoked_at,
|
|
1493
1518
|
session.id AS session_id, session.client_id, session.cube_id, session.drone_id,
|
|
1494
1519
|
session.expires_at, session.revoked_at AS session_revoked_at,
|
|
1495
|
-
drone.role_id, drone.label, drone.evicted_at
|
|
1520
|
+
session.superseded_at, drone.role_id, drone.label, drone.evicted_at
|
|
1496
1521
|
FROM drone_session_credentials AS credential
|
|
1497
1522
|
JOIN drone_sessions AS session ON session.id = credential.session_id
|
|
1498
1523
|
JOIN drones AS drone ON drone.id = session.drone_id
|
|
@@ -1502,10 +1527,6 @@ class SqliteScopedStore implements ScopedStore {
|
|
|
1502
1527
|
if (bound !== undefined) {
|
|
1503
1528
|
const verifier = requiredBuffer(bound, "verifier_digest");
|
|
1504
1529
|
if (!timingSafeEqual(verifier, input.credentialDigest.verifier) ||
|
|
1505
|
-
optionalText(bound, "credential_revoked_at") !== null ||
|
|
1506
|
-
optionalText(bound, "session_revoked_at") !== null ||
|
|
1507
|
-
optionalText(bound, "evicted_at") !== null ||
|
|
1508
|
-
requiredText(bound, "expires_at") <= now ||
|
|
1509
1530
|
requiredText(bound, "client_id") !== this.#principal.id ||
|
|
1510
1531
|
requiredText(bound, "cube_id") !== input.cubeId ||
|
|
1511
1532
|
requiredText(bound, "role_id") !== input.roleId ||
|
|
@@ -1513,9 +1534,27 @@ class SqliteScopedStore implements ScopedStore {
|
|
|
1513
1534
|
requiredText(bound, "drone_id") !== input.priorDroneId)) {
|
|
1514
1535
|
throw new AttachSessionRejectedError();
|
|
1515
1536
|
}
|
|
1537
|
+
if (optionalText(bound, "evicted_at") !== null) throw new AttachDroneEvictedError();
|
|
1538
|
+
if (optionalText(bound, "credential_revoked_at") !== null ||
|
|
1539
|
+
optionalText(bound, "session_revoked_at") !== null) {
|
|
1540
|
+
throw new AttachSessionRevokedError();
|
|
1541
|
+
}
|
|
1542
|
+
if (optionalText(bound, "superseded_at") !== null) {
|
|
1543
|
+
throw new AttachSessionRejectedError();
|
|
1544
|
+
}
|
|
1545
|
+
if (requiredText(bound, "expires_at") <= now) throw new AttachSessionExpiredError();
|
|
1516
1546
|
const droneId = requiredText(bound, "drone_id");
|
|
1547
|
+
const sessionId = requiredText(bound, "session_id");
|
|
1548
|
+
const previousExpiry = requiredText(bound, "expires_at");
|
|
1549
|
+
const committedExpiry = input.expiresAt > previousExpiry &&
|
|
1550
|
+
previousExpiry <= input.renewIfExpiresAtOrBefore
|
|
1551
|
+
? input.expiresAt
|
|
1552
|
+
: previousExpiry;
|
|
1553
|
+
if (committedExpiry !== previousExpiry) {
|
|
1554
|
+
this.#database.prepare("UPDATE drone_sessions SET expires_at = ? WHERE id = ?")
|
|
1555
|
+
.run(committedExpiry, sessionId);
|
|
1556
|
+
}
|
|
1517
1557
|
this.#database.prepare("UPDATE drones SET last_seen = ? WHERE id = ?").run(now, droneId);
|
|
1518
|
-
this.#database.prepare("DELETE FROM silent_seat_ping_state WHERE drone_id = ?").run(droneId);
|
|
1519
1558
|
const attachedRoleRow = this.#database.prepare(`
|
|
1520
1559
|
SELECT id AS role_id, name AS role_name, role_class, is_human_seat
|
|
1521
1560
|
FROM roles WHERE id = ? AND cube_id = ?
|
|
@@ -1538,33 +1577,53 @@ class SqliteScopedStore implements ScopedStore {
|
|
|
1538
1577
|
is_human_seat: requiredInteger(attachedRoleRow, "is_human_seat") === 1,
|
|
1539
1578
|
},
|
|
1540
1579
|
drone: { id: droneId, label: requiredText(bound, "label") },
|
|
1541
|
-
sessionId
|
|
1542
|
-
expiresAt:
|
|
1580
|
+
sessionId,
|
|
1581
|
+
expiresAt: committedExpiry,
|
|
1543
1582
|
result: "reused",
|
|
1544
1583
|
};
|
|
1545
1584
|
}
|
|
1546
1585
|
|
|
1547
1586
|
const priorSeat = input.priorDroneId === undefined ? undefined : this.#database.prepare(`
|
|
1548
|
-
SELECT id, role_id, label FROM drones
|
|
1549
|
-
WHERE id = ? AND client_id = ? AND cube_id = ?
|
|
1587
|
+
SELECT id, role_id, label, evicted_at FROM drones
|
|
1588
|
+
WHERE id = ? AND client_id = ? AND cube_id = ?
|
|
1550
1589
|
`).get(input.priorDroneId, this.#principal.id, input.cubeId);
|
|
1551
1590
|
let droneId: string;
|
|
1552
1591
|
let droneLabel: string;
|
|
1553
|
-
if (
|
|
1592
|
+
if (input.priorDroneId !== undefined) {
|
|
1593
|
+
if (priorSeat === undefined) throw new AttachSessionRejectedError();
|
|
1594
|
+
if (optionalText(priorSeat, "evicted_at") !== null) throw new AttachDroneEvictedError();
|
|
1554
1595
|
const priorSeatId = requiredText(priorSeat, "id");
|
|
1555
1596
|
if (requiredText(priorSeat, "role_id") !== input.roleId) {
|
|
1556
1597
|
throw new AttachSessionRejectedError();
|
|
1557
1598
|
}
|
|
1558
|
-
const
|
|
1559
|
-
SELECT
|
|
1599
|
+
const latestSession = this.#database.prepare(`
|
|
1600
|
+
SELECT session.id, session.expires_at, session.revoked_at AS session_revoked_at,
|
|
1601
|
+
MAX(CASE WHEN credential.revoked_at IS NOT NULL THEN 1 ELSE 0 END)
|
|
1602
|
+
AS credential_revoked
|
|
1603
|
+
FROM drone_sessions AS session
|
|
1560
1604
|
JOIN drone_session_credentials AS credential ON credential.session_id = session.id
|
|
1561
1605
|
WHERE session.drone_id = ? AND session.client_id = ? AND session.cube_id = ?
|
|
1562
|
-
AND session.
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
1606
|
+
AND session.superseded_at IS NULL
|
|
1607
|
+
GROUP BY session.id
|
|
1608
|
+
LIMIT 1
|
|
1609
|
+
`).get(priorSeatId, this.#principal.id, input.cubeId);
|
|
1610
|
+
if (latestSession === undefined) throw new AttachSessionRejectedError();
|
|
1611
|
+
if (optionalText(latestSession, "session_revoked_at") !== null ||
|
|
1612
|
+
requiredInteger(latestSession, "credential_revoked") === 1) {
|
|
1613
|
+
throw new AttachSessionRevokedError();
|
|
1614
|
+
}
|
|
1615
|
+
if (requiredText(latestSession, "expires_at") > now) {
|
|
1616
|
+
throw new AttachSessionRejectedError();
|
|
1617
|
+
}
|
|
1566
1618
|
droneId = priorSeatId;
|
|
1567
1619
|
droneLabel = requiredText(priorSeat, "label");
|
|
1620
|
+
const superseded = this.#database.prepare(`
|
|
1621
|
+
UPDATE drone_sessions SET superseded_at = ?
|
|
1622
|
+
WHERE id = ? AND superseded_at IS NULL
|
|
1623
|
+
`).run(now, requiredText(latestSession, "id"));
|
|
1624
|
+
if (Number(superseded.changes) !== 1) {
|
|
1625
|
+
throw new Error("Expired session supersession was not atomic.");
|
|
1626
|
+
}
|
|
1568
1627
|
this.#database.prepare("UPDATE drones SET last_seen = ? WHERE id = ?").run(now, droneId);
|
|
1569
1628
|
} else {
|
|
1570
1629
|
droneId = input.droneId;
|
|
@@ -1576,8 +1635,6 @@ class SqliteScopedStore implements ScopedStore {
|
|
|
1576
1635
|
droneId, input.cubeId, input.roleId, this.#principal.id, droneLabel, now, now,
|
|
1577
1636
|
);
|
|
1578
1637
|
}
|
|
1579
|
-
this.#database.prepare("DELETE FROM silent_seat_ping_state WHERE drone_id = ?").run(droneId);
|
|
1580
|
-
|
|
1581
1638
|
this.#database.prepare(`
|
|
1582
1639
|
INSERT INTO drone_sessions (
|
|
1583
1640
|
id, client_id, cube_id, drone_id, created_at, expires_at
|
|
@@ -2194,61 +2251,11 @@ class ActivityHub {
|
|
|
2194
2251
|
}
|
|
2195
2252
|
}
|
|
2196
2253
|
|
|
2197
|
-
class
|
|
2198
|
-
|
|
2199
|
-
|
|
2200
|
-
|
|
2201
|
-
|
|
2202
|
-
readonly capacityGuard: StorageCapacityGuard,
|
|
2203
|
-
) {}
|
|
2204
|
-
|
|
2205
|
-
scan(options: { silentMs?: number; cooldownMs?: number; limit?: number } = {}): ActivityNotificationRecord[] {
|
|
2206
|
-
const now = this.clock();
|
|
2207
|
-
const silentBefore = new Date(now.getTime() - (options.silentMs ?? 600_000)).toISOString();
|
|
2208
|
-
const cooldownBefore = new Date(now.getTime() - (options.cooldownMs ?? 600_000)).toISOString();
|
|
2209
|
-
const limit = Math.max(1, Math.min(Math.trunc(options.limit ?? 25), 25));
|
|
2210
|
-
const candidates = this.database.prepare(`
|
|
2211
|
-
SELECT drone.id AS drone_id, drone.cube_id, COALESCE(state.attempts, 0) AS attempts
|
|
2212
|
-
FROM drones AS drone
|
|
2213
|
-
JOIN clients AS client ON client.id = drone.client_id AND client.revoked_at IS NULL
|
|
2214
|
-
JOIN client_cube_grants AS grant_row ON grant_row.client_id = drone.client_id
|
|
2215
|
-
AND grant_row.cube_id = drone.cube_id AND grant_row.access IN ('write', 'manage')
|
|
2216
|
-
LEFT JOIN silent_seat_ping_state AS state ON state.drone_id = drone.id
|
|
2217
|
-
WHERE drone.evicted_at IS NULL AND COALESCE(drone.last_seen, drone.created_at) < ?
|
|
2218
|
-
AND COALESCE(state.attempts, 0) < 3
|
|
2219
|
-
AND (state.last_ping_at IS NULL OR state.last_ping_at < ?)
|
|
2220
|
-
AND EXISTS (SELECT 1 FROM drone_sessions AS session
|
|
2221
|
-
WHERE session.drone_id = drone.id AND session.revoked_at IS NULL AND session.expires_at > ?)
|
|
2222
|
-
ORDER BY COALESCE(drone.last_seen, drone.created_at), drone.id LIMIT ?
|
|
2223
|
-
`).all(silentBefore, cooldownBefore, now.toISOString(), limit);
|
|
2224
|
-
const published: ActivityNotificationRecord[] = [];
|
|
2225
|
-
for (const candidate of candidates) {
|
|
2226
|
-
const droneId = requiredText(candidate, "drone_id");
|
|
2227
|
-
const cubeId = requiredText(candidate, "cube_id");
|
|
2228
|
-
const message = "[HEARTBEAT-PING] No recent activity; confirm this seat is responsive.";
|
|
2229
|
-
this.capacityGuard.assertCanGrow(256);
|
|
2230
|
-
const id = randomUUID();
|
|
2231
|
-
const createdAt = now.toISOString();
|
|
2232
|
-
this.database.exec("BEGIN IMMEDIATE");
|
|
2233
|
-
try {
|
|
2234
|
-
this.database.prepare(`INSERT INTO silent_seat_ping_state (drone_id, attempts, last_ping_at)
|
|
2235
|
-
VALUES (?, 1, ?) ON CONFLICT(drone_id) DO UPDATE
|
|
2236
|
-
SET attempts = attempts + 1, last_ping_at = excluded.last_ping_at
|
|
2237
|
-
`).run(droneId, now.toISOString());
|
|
2238
|
-
this.database.exec("COMMIT");
|
|
2239
|
-
} catch (error) {
|
|
2240
|
-
try { this.database.exec("ROLLBACK"); } catch { /* Preserve original failure. */ }
|
|
2241
|
-
throw error;
|
|
2242
|
-
}
|
|
2243
|
-
const entry: ActivityNotificationRecord = {
|
|
2244
|
-
kind: "heartbeat_ping",
|
|
2245
|
-
id, cube_id: cubeId, drone_id: null, message, visibility: "direct", created_at: createdAt,
|
|
2246
|
-
drone_label: null, role_name: null, recipient_drone_ids: [droneId],
|
|
2247
|
-
};
|
|
2248
|
-
this.hub.publish(cubeId, entry);
|
|
2249
|
-
published.push(entry);
|
|
2250
|
-
}
|
|
2251
|
-
return published;
|
|
2254
|
+
class IdleLivenessStore implements LivenessStore {
|
|
2255
|
+
scan(): ActivityNotificationRecord[] {
|
|
2256
|
+
// Silence is not a delivery failure. Keep the scheduler's lifecycle seam without
|
|
2257
|
+
// creating a directed event that crosses the client/model wake boundary.
|
|
2258
|
+
return [];
|
|
2252
2259
|
}
|
|
2253
2260
|
}
|
|
2254
2261
|
|
|
@@ -2570,6 +2577,15 @@ class SqliteCredentialStore implements CredentialStore {
|
|
|
2570
2577
|
).get(clientId) !== undefined;
|
|
2571
2578
|
}
|
|
2572
2579
|
|
|
2580
|
+
clientHasServerCapability(clientId: string, capability: "create_cube"): boolean {
|
|
2581
|
+
assertCanonicalUuid(clientId, "Client id");
|
|
2582
|
+
return this.#database.prepare(`
|
|
2583
|
+
SELECT 1 FROM client_server_capabilities AS capability
|
|
2584
|
+
JOIN clients AS client ON client.id = capability.client_id
|
|
2585
|
+
WHERE capability.client_id = ? AND capability.capability = ? AND client.revoked_at IS NULL
|
|
2586
|
+
`).get(clientId, capability) !== undefined;
|
|
2587
|
+
}
|
|
2588
|
+
|
|
2573
2589
|
findDroneSessionCredential(lookup: Buffer): StoredDroneSessionDigest | null {
|
|
2574
2590
|
validateLookup(lookup);
|
|
2575
2591
|
const row = this.#database.prepare(`
|
|
@@ -2577,7 +2593,7 @@ class SqliteCredentialStore implements CredentialStore {
|
|
|
2577
2593
|
credential.session_id, session.client_id, session.cube_id, session.drone_id,
|
|
2578
2594
|
session.expires_at,
|
|
2579
2595
|
COALESCE(credential.revoked_at, session.revoked_at, client.revoked_at) AS revoked_at,
|
|
2580
|
-
drone.evicted_at
|
|
2596
|
+
drone.evicted_at, session.superseded_at
|
|
2581
2597
|
FROM drone_session_credentials AS credential
|
|
2582
2598
|
JOIN drone_sessions AS session ON session.id = credential.session_id
|
|
2583
2599
|
JOIN clients AS client ON client.id = session.client_id
|
|
@@ -3055,6 +3071,7 @@ function storedDroneSessionDigest(row: Record<string, unknown>): StoredDroneSess
|
|
|
3055
3071
|
droneId: requiredText(row, "drone_id"),
|
|
3056
3072
|
expiresAt: requiredText(row, "expires_at"),
|
|
3057
3073
|
evictedAt: nullableText(row, "evicted_at"),
|
|
3074
|
+
takenOver: nullableText(row, "superseded_at") !== null,
|
|
3058
3075
|
};
|
|
3059
3076
|
}
|
|
3060
3077
|
|