@byok-sdk/server 0.2.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +19 -1
- package/dist/auth.d.ts +32 -10
- package/dist/hub.d.ts +3 -3
- package/dist/index.js +205 -75
- package/dist/index.js.map +1 -1
- package/dist/sqlite-blob-store.d.ts +1 -0
- package/dist/sqlite-support.d.ts +14 -1
- package/dist/sqlite-task-store.d.ts +1 -0
- package/dist/task-store.d.ts +2 -1
- package/dist/types.d.ts +31 -1
- package/package.json +3 -2
package/dist/index.js
CHANGED
|
@@ -1,18 +1,18 @@
|
|
|
1
1
|
import { randomBytes, randomUUID, timingSafeEqual, createHmac, createHash, createPublicKey, verify } from 'crypto';
|
|
2
|
+
import { nonceSigningBytes } from '@byok-sdk/core';
|
|
2
3
|
import { jwtVerify, SignJWT } from 'jose';
|
|
3
4
|
import { mkdir, writeFile, readFile } from 'fs/promises';
|
|
4
5
|
import { mkdtempSync, mkdirSync, existsSync, chmodSync } from 'fs';
|
|
5
6
|
import { tmpdir } from 'os';
|
|
6
7
|
import path, { dirname } from 'path';
|
|
8
|
+
import { CAPABILITY_FLAGS, byokBlobContentPath, canTransition, PROTOCOL_VERSION, encodeEnvelope, DAEMON_TO_SERVER_TYPES, DispatchSelectionSchema, RequiredToolsetsSchema, createEnvelope, TASK_STATES, BYOK_PAIR_PATH, PairRequestSchema, BYOK_CHALLENGE_PATH, ChallengeRequestSchema, BYOK_TOKEN_PATH, TokenRequestSchema, BYOK_BLOBS_PATH, CreateBlobRequestSchema, BYOK_BLOB_FINALIZE_ROUTE, BYOK_BLOB_URL_ROUTE, BYOK_BLOB_CONTENT_ROUTE, BYOK_EVENTS_PATH, BYOK_MESSAGES_PATH, MessagesSendRequestSchema, decodeEnvelope, BYOK_WS_PATH } from '@byok-sdk/protocol';
|
|
7
9
|
import { Hono } from 'hono';
|
|
8
|
-
import { CAPABILITY_FLAGS, canTransition, PROTOCOL_VERSION, encodeEnvelope, DAEMON_TO_SERVER_TYPES, createEnvelope, TASK_STATES, PairRequestSchema, ChallengeRequestSchema, TokenRequestSchema, CreateBlobRequestSchema, MessagesSendRequestSchema, decodeEnvelope } from '@byok-sdk/protocol';
|
|
9
10
|
import { WebSocketServer } from 'ws';
|
|
10
11
|
import { createRequire } from 'module';
|
|
11
12
|
|
|
12
13
|
// src/auth.ts
|
|
13
14
|
var ACCESS_TOKEN_TTL_SECONDS = 60 * 60;
|
|
14
15
|
var NONCE_TTL_MS = 5 * 60 * 1e3;
|
|
15
|
-
var NONCE_SIGNING_DOMAIN = "byok-nonce-v1\n";
|
|
16
16
|
function createHmacTokenSigner(secret = randomBytes(32)) {
|
|
17
17
|
return {
|
|
18
18
|
async sign(claims, expiresInSeconds) {
|
|
@@ -143,13 +143,13 @@ function verifyEd25519Signature(devicePublicKey, message, signature) {
|
|
|
143
143
|
key: { kty: "OKP", crv: "Ed25519", x: devicePublicKey },
|
|
144
144
|
format: "jwk"
|
|
145
145
|
});
|
|
146
|
-
return verify(null,
|
|
146
|
+
return verify(null, message, keyObject, Buffer.from(signature, "base64url"));
|
|
147
147
|
} catch {
|
|
148
148
|
return false;
|
|
149
149
|
}
|
|
150
150
|
}
|
|
151
151
|
function verifyNonceSignature(devicePublicKey, nonce, signature) {
|
|
152
|
-
return verifyEd25519Signature(devicePublicKey,
|
|
152
|
+
return verifyEd25519Signature(devicePublicKey, nonceSigningBytes(nonce), signature);
|
|
153
153
|
}
|
|
154
154
|
function extractBearerToken(header) {
|
|
155
155
|
if (!header) return void 0;
|
|
@@ -164,6 +164,7 @@ async function authenticateBearer(header, deps) {
|
|
|
164
164
|
const device = deps.devices.get(claims.tenantId, claims.deviceId);
|
|
165
165
|
if (!device || device.revoked) return void 0;
|
|
166
166
|
if (device.productId !== claims.productId) return void 0;
|
|
167
|
+
if (device.productId !== deps.productId) return void 0;
|
|
167
168
|
return { deviceId: device.deviceId, tenantId: device.tenantId, productId: device.productId };
|
|
168
169
|
}
|
|
169
170
|
var BlobDeclarationConflictError = class extends Error {
|
|
@@ -247,7 +248,7 @@ var LocalDiskBlobStore = class {
|
|
|
247
248
|
signUrl(blobId, action) {
|
|
248
249
|
const exp = Date.now() + this.urlTtlMs;
|
|
249
250
|
const sig = this.computeSig(blobId, action, exp);
|
|
250
|
-
return
|
|
251
|
+
return `${byokBlobContentPath(blobId)}?sig=${sig}&exp=${exp}`;
|
|
251
252
|
}
|
|
252
253
|
};
|
|
253
254
|
var PAIRING_CODE_ALPHABET = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789";
|
|
@@ -342,7 +343,7 @@ function buildHonoApp(deps) {
|
|
|
342
343
|
if (deps.healthzRoute) {
|
|
343
344
|
app.get("/healthz", (c) => c.json({ ok: true, uptimeMs: Date.now() - serverStartedAtMs }, 200));
|
|
344
345
|
}
|
|
345
|
-
app.post(
|
|
346
|
+
app.post(BYOK_PAIR_PATH, async (c) => {
|
|
346
347
|
const parsed = PairRequestSchema.safeParse(await readJsonBody(c));
|
|
347
348
|
if (!parsed.success) {
|
|
348
349
|
return c.json({ error: "pairingCode, deviceName, and devicePublicKey are required strings" }, 400);
|
|
@@ -373,7 +374,7 @@ function buildHonoApp(deps) {
|
|
|
373
374
|
const response = { deviceId, accessToken, refreshHint: expiresAt };
|
|
374
375
|
return c.json(response, 200);
|
|
375
376
|
});
|
|
376
|
-
app.post(
|
|
377
|
+
app.post(BYOK_CHALLENGE_PATH, async (c) => {
|
|
377
378
|
const parsed = ChallengeRequestSchema.safeParse(await readJsonBody(c));
|
|
378
379
|
if (!parsed.success) return c.json({ error: "deviceId is required" }, 400);
|
|
379
380
|
const { deviceId } = parsed.data;
|
|
@@ -385,7 +386,7 @@ function buildHonoApp(deps) {
|
|
|
385
386
|
const response = { nonce };
|
|
386
387
|
return c.json(response, 200);
|
|
387
388
|
});
|
|
388
|
-
app.post(
|
|
389
|
+
app.post(BYOK_TOKEN_PATH, async (c) => {
|
|
389
390
|
const parsed = TokenRequestSchema.safeParse(await readJsonBody(c));
|
|
390
391
|
if (!parsed.success) return c.json({ error: "deviceId, nonce, and signature are required" }, 400);
|
|
391
392
|
const { deviceId, nonce, signature } = parsed.data;
|
|
@@ -408,7 +409,7 @@ function buildHonoApp(deps) {
|
|
|
408
409
|
const response = { accessToken, expiresAt };
|
|
409
410
|
return c.json(response, 200);
|
|
410
411
|
});
|
|
411
|
-
app.post(
|
|
412
|
+
app.post(BYOK_BLOBS_PATH, async (c) => {
|
|
412
413
|
const principal = await authenticateBearer(c.req.header("authorization"), deps);
|
|
413
414
|
if (!principal) return c.json({ error: "unauthorized" }, 401);
|
|
414
415
|
const parsed = CreateBlobRequestSchema.safeParse(await readJsonBody(c));
|
|
@@ -432,7 +433,7 @@ function buildHonoApp(deps) {
|
|
|
432
433
|
throw error;
|
|
433
434
|
}
|
|
434
435
|
});
|
|
435
|
-
app.post(
|
|
436
|
+
app.post(BYOK_BLOB_FINALIZE_ROUTE, async (c) => {
|
|
436
437
|
const principal = await authenticateBearer(c.req.header("authorization"), deps);
|
|
437
438
|
if (!principal) return c.json({ error: "unauthorized" }, 401);
|
|
438
439
|
const reservationId = c.req.header("idempotency-key");
|
|
@@ -448,7 +449,7 @@ function buildHonoApp(deps) {
|
|
|
448
449
|
}
|
|
449
450
|
return c.body(null, 204);
|
|
450
451
|
});
|
|
451
|
-
app.get(
|
|
452
|
+
app.get(BYOK_BLOB_URL_ROUTE, async (c) => {
|
|
452
453
|
const principal = await authenticateBearer(c.req.header("authorization"), deps);
|
|
453
454
|
if (!principal) return c.json({ error: "unauthorized" }, 401);
|
|
454
455
|
const downloadUrl = await deps.blobStore.getDownloadUrl(c.req.param("id"));
|
|
@@ -456,7 +457,7 @@ function buildHonoApp(deps) {
|
|
|
456
457
|
const response = { downloadUrl };
|
|
457
458
|
return c.json(response, 200);
|
|
458
459
|
});
|
|
459
|
-
app.put(
|
|
460
|
+
app.put(BYOK_BLOB_CONTENT_ROUTE, async (c) => {
|
|
460
461
|
const blobId = c.req.param("id");
|
|
461
462
|
const { sig, exp } = signedUrlParams(c.req.query("sig"), c.req.query("exp"));
|
|
462
463
|
if (!sig || exp === void 0 || !deps.blobStore.verifySignedUrl(blobId, "put", sig, exp)) {
|
|
@@ -467,7 +468,7 @@ function buildHonoApp(deps) {
|
|
|
467
468
|
if (!result.ok) return c.json({ error: result.reason }, 422);
|
|
468
469
|
return c.body(null, 204);
|
|
469
470
|
});
|
|
470
|
-
app.get(
|
|
471
|
+
app.get(BYOK_BLOB_CONTENT_ROUTE, async (c) => {
|
|
471
472
|
const blobId = c.req.param("id");
|
|
472
473
|
const { sig, exp } = signedUrlParams(c.req.query("sig"), c.req.query("exp"));
|
|
473
474
|
if (!sig || exp === void 0 || !deps.blobStore.verifySignedUrl(blobId, "get", sig, exp)) {
|
|
@@ -477,7 +478,7 @@ function buildHonoApp(deps) {
|
|
|
477
478
|
if (!content) return c.json({ error: "blob not found" }, 404);
|
|
478
479
|
return c.body(new Uint8Array(content.data), 200, { "content-type": content.contentType });
|
|
479
480
|
});
|
|
480
|
-
app.get(
|
|
481
|
+
app.get(BYOK_EVENTS_PATH, async (c) => {
|
|
481
482
|
const principal = await authenticateBearer(c.req.header("authorization"), deps);
|
|
482
483
|
if (!principal) return c.json({ error: "unauthorized" }, 401);
|
|
483
484
|
const cursorRaw = c.req.query("cursor");
|
|
@@ -491,7 +492,7 @@ function buildHonoApp(deps) {
|
|
|
491
492
|
const response = result;
|
|
492
493
|
return c.json(response, 200);
|
|
493
494
|
});
|
|
494
|
-
app.post(
|
|
495
|
+
app.post(BYOK_MESSAGES_PATH, async (c) => {
|
|
495
496
|
const principal = await authenticateBearer(c.req.header("authorization"), deps);
|
|
496
497
|
if (!principal) return c.json({ error: "unauthorized" }, 401);
|
|
497
498
|
const parsed = MessagesSendRequestSchema.safeParse(await readJsonBody(c));
|
|
@@ -843,9 +844,16 @@ var ConnectionHub = class {
|
|
|
843
844
|
* connection this hub never learns capabilities for simply reads back
|
|
844
845
|
* `undefined` from {@link getDeviceCapabilities}.
|
|
845
846
|
*/
|
|
846
|
-
registerConnection(deviceId, ws, runtimes, capabilities) {
|
|
847
|
+
registerConnection(deviceId, ws, runtimes, capabilities, configuredToolsets) {
|
|
847
848
|
const at = (/* @__PURE__ */ new Date()).toISOString();
|
|
848
|
-
this.connections.set(deviceId, {
|
|
849
|
+
this.connections.set(deviceId, {
|
|
850
|
+
ws,
|
|
851
|
+
connected: true,
|
|
852
|
+
lastSeen: at,
|
|
853
|
+
runtimes,
|
|
854
|
+
capabilities,
|
|
855
|
+
configuredToolsets
|
|
856
|
+
});
|
|
849
857
|
this.serverEvents.push({ kind: "device.connected", deviceId, at });
|
|
850
858
|
this.settleLongPollWaiter(deviceId);
|
|
851
859
|
}
|
|
@@ -940,7 +948,13 @@ var ConnectionHub = class {
|
|
|
940
948
|
const wasFreshlyConnected = !conn || conn.ws !== void 0 || !conn.connected;
|
|
941
949
|
if (conn?.ws) {
|
|
942
950
|
const ws = conn.ws;
|
|
943
|
-
this.connections.set(deviceId, {
|
|
951
|
+
this.connections.set(deviceId, {
|
|
952
|
+
connected: true,
|
|
953
|
+
lastSeen: at,
|
|
954
|
+
runtimes: conn.runtimes,
|
|
955
|
+
capabilities: conn.capabilities,
|
|
956
|
+
configuredToolsets: conn.configuredToolsets
|
|
957
|
+
});
|
|
944
958
|
ws.close(1e3, "superseded by long-poll connection");
|
|
945
959
|
} else if (!conn) {
|
|
946
960
|
this.connections.set(deviceId, { connected: true, lastSeen: at });
|
|
@@ -1274,7 +1288,17 @@ var ConnectionHub = class {
|
|
|
1274
1288
|
state: "Complete",
|
|
1275
1289
|
summary: payload.summary,
|
|
1276
1290
|
sessionRef: payload.sessionRef,
|
|
1277
|
-
artifactRefs: payload.artifactRefs
|
|
1291
|
+
artifactRefs: payload.artifactRefs,
|
|
1292
|
+
// additive-minor (`task.complete.document`): projected verbatim, the
|
|
1293
|
+
// same way `summary`/`artifactRefs` are. Nothing to validate or
|
|
1294
|
+
// measure here — the payload only got this far because
|
|
1295
|
+
// `TaskCompletePayloadSchema`'s own refinement already enforced
|
|
1296
|
+
// JSON-serializability and `RESULT_DOCUMENT_MAX_BYTES` at the inbound
|
|
1297
|
+
// boundary, and re-checking would make this a second authority for a
|
|
1298
|
+
// rule the wire already owns. Stays `undefined` for the two cases that
|
|
1299
|
+
// never carry one: a daemon with no extractor configured, and a
|
|
1300
|
+
// pre-`result-document` daemon build.
|
|
1301
|
+
document: payload.document
|
|
1278
1302
|
};
|
|
1279
1303
|
this.applyOrFail(taskId, "Complete", { result, sessionRef: payload.sessionRef });
|
|
1280
1304
|
}
|
|
@@ -1658,19 +1682,53 @@ var ConnectionHub = class {
|
|
|
1658
1682
|
// dispatch() and the TaskHandle it returns
|
|
1659
1683
|
// ---------------------------------------------------------------------
|
|
1660
1684
|
async dispatch(input) {
|
|
1661
|
-
const
|
|
1685
|
+
const dispatchSelection = input.dispatchSelection === void 0 ? void 0 : DispatchSelectionSchema.parse(input.dispatchSelection);
|
|
1686
|
+
const requiredToolsets = input.requiredToolsets === void 0 ? void 0 : RequiredToolsetsSchema.parse(input.requiredToolsets);
|
|
1687
|
+
const deviceId = input.deviceId ?? this.pickFirstConnectedDevice(requiredToolsets);
|
|
1662
1688
|
if (!deviceId || !this.connections.get(deviceId)?.connected) {
|
|
1663
1689
|
throw new Error(
|
|
1664
1690
|
deviceId ? `device ${deviceId} is not connected` : "no connected device to dispatch to (M0 does not queue tasks until a device connects)"
|
|
1665
1691
|
);
|
|
1666
1692
|
}
|
|
1693
|
+
if (dispatchSelection !== void 0 && !(this.getDeviceCapabilities(deviceId)?.includes("dispatch-selection") ?? false)) {
|
|
1694
|
+
throw new Error(
|
|
1695
|
+
`device ${deviceId} did not advertise dispatch-selection capability; refusing authoritative provider/model dispatch`
|
|
1696
|
+
);
|
|
1697
|
+
}
|
|
1698
|
+
if (requiredToolsets !== void 0 && !(this.getDeviceCapabilities(deviceId)?.includes("toolset-selection") ?? false)) {
|
|
1699
|
+
throw new Error(
|
|
1700
|
+
`device ${deviceId} did not advertise toolset-selection capability; refusing a task whose semantics require local MCP tools`
|
|
1701
|
+
);
|
|
1702
|
+
}
|
|
1703
|
+
if (requiredToolsets !== void 0) {
|
|
1704
|
+
const configuredToolsets = this.connections.get(deviceId)?.configuredToolsets;
|
|
1705
|
+
if (configuredToolsets === void 0) {
|
|
1706
|
+
throw new Error(
|
|
1707
|
+
`device ${deviceId} did not advertise its configured toolset inventory; refusing to guess from runtime capability`
|
|
1708
|
+
);
|
|
1709
|
+
}
|
|
1710
|
+
const configured = new Set(configuredToolsets);
|
|
1711
|
+
const missing = requiredToolsets.filter((toolsetId) => !configured.has(toolsetId));
|
|
1712
|
+
if (missing.length > 0) {
|
|
1713
|
+
throw new Error(
|
|
1714
|
+
`device ${deviceId} is missing required MCP toolset(s): ${missing.join(", ")}`
|
|
1715
|
+
);
|
|
1716
|
+
}
|
|
1717
|
+
}
|
|
1718
|
+
if (dispatchSelection !== void 0 && input.runtime !== void 0 && input.runtime !== dispatchSelection.runtimeId) {
|
|
1719
|
+
throw new Error(
|
|
1720
|
+
`dispatch runtime ${input.runtime} does not match dispatchSelection.runtimeId ${dispatchSelection.runtimeId}`
|
|
1721
|
+
);
|
|
1722
|
+
}
|
|
1667
1723
|
const taskId = generateTaskId();
|
|
1668
1724
|
const policy = input.policy ?? DEFAULT_POLICY;
|
|
1725
|
+
const runtime = dispatchSelection?.runtimeId ?? input.runtime;
|
|
1669
1726
|
const record = this.taskStore.create({
|
|
1670
1727
|
taskId,
|
|
1671
1728
|
instruction: input.instruction,
|
|
1672
|
-
runtime
|
|
1729
|
+
runtime,
|
|
1673
1730
|
policy,
|
|
1731
|
+
requiredToolsets,
|
|
1674
1732
|
deviceId,
|
|
1675
1733
|
sessionRef: input.sessionRef
|
|
1676
1734
|
});
|
|
@@ -1682,17 +1740,23 @@ var ConnectionHub = class {
|
|
|
1682
1740
|
this.runtimes.set(taskId, { queue, resolveResult, result });
|
|
1683
1741
|
queue.push({ kind: "state", state: record.state, at: record.createdAt });
|
|
1684
1742
|
this.serverEvents.push({ kind: "task.created", taskId, at: record.createdAt });
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
}
|
|
1694
|
-
|
|
1695
|
-
|
|
1743
|
+
const commonOffer = {
|
|
1744
|
+
instruction: input.instruction,
|
|
1745
|
+
policy,
|
|
1746
|
+
runtime,
|
|
1747
|
+
dispatchSelection,
|
|
1748
|
+
sessionRef: input.sessionRef
|
|
1749
|
+
};
|
|
1750
|
+
if (requiredToolsets === void 0) {
|
|
1751
|
+
this.sendToDevice(deviceId, "task.offer", commonOffer, { taskId, sessionRef: input.sessionRef });
|
|
1752
|
+
} else {
|
|
1753
|
+
this.sendToDevice(
|
|
1754
|
+
deviceId,
|
|
1755
|
+
"task.offer_with_toolsets",
|
|
1756
|
+
{ ...commonOffer, requiredToolsets },
|
|
1757
|
+
{ taskId, sessionRef: input.sessionRef }
|
|
1758
|
+
);
|
|
1759
|
+
}
|
|
1696
1760
|
return this.buildTaskHandle(taskId);
|
|
1697
1761
|
}
|
|
1698
1762
|
buildTaskHandle(taskId) {
|
|
@@ -1843,9 +1907,14 @@ var ConnectionHub = class {
|
|
|
1843
1907
|
}
|
|
1844
1908
|
this.sendToDevice(record.deviceId, "task.steer", { text }, { taskId });
|
|
1845
1909
|
}
|
|
1846
|
-
pickFirstConnectedDevice() {
|
|
1910
|
+
pickFirstConnectedDevice(requiredToolsets) {
|
|
1847
1911
|
for (const [deviceId, conn] of this.connections) {
|
|
1848
|
-
if (conn.connected)
|
|
1912
|
+
if (!conn.connected) continue;
|
|
1913
|
+
if (requiredToolsets === void 0) return deviceId;
|
|
1914
|
+
if (!conn.capabilities?.includes("toolset-selection")) continue;
|
|
1915
|
+
if (conn.configuredToolsets === void 0) continue;
|
|
1916
|
+
const configured = new Set(conn.configuredToolsets);
|
|
1917
|
+
if (requiredToolsets.every((toolsetId) => configured.has(toolsetId))) return deviceId;
|
|
1849
1918
|
}
|
|
1850
1919
|
return void 0;
|
|
1851
1920
|
}
|
|
@@ -1928,7 +1997,8 @@ var ConnectionHub = class {
|
|
|
1928
1997
|
deviceName,
|
|
1929
1998
|
connected: conn?.connected ?? false,
|
|
1930
1999
|
lastSeen: conn?.lastSeen,
|
|
1931
|
-
runtimes: conn?.runtimes
|
|
2000
|
+
runtimes: conn?.runtimes,
|
|
2001
|
+
configuredToolsets: conn?.configuredToolsets ? [...conn.configuredToolsets] : void 0
|
|
1932
2002
|
};
|
|
1933
2003
|
});
|
|
1934
2004
|
}
|
|
@@ -2008,6 +2078,7 @@ var InMemoryTaskStore = class {
|
|
|
2008
2078
|
instruction: input.instruction,
|
|
2009
2079
|
runtime: input.runtime,
|
|
2010
2080
|
policy: input.policy,
|
|
2081
|
+
requiredToolsets: input.requiredToolsets,
|
|
2011
2082
|
deviceId: input.deviceId,
|
|
2012
2083
|
sessionRef: input.sessionRef,
|
|
2013
2084
|
createdAt: now,
|
|
@@ -2101,7 +2172,7 @@ function startHeartbeat(ws, opts = {}) {
|
|
|
2101
2172
|
}
|
|
2102
2173
|
|
|
2103
2174
|
// src/ws-server.ts
|
|
2104
|
-
var WS_PATH =
|
|
2175
|
+
var WS_PATH = BYOK_WS_PATH;
|
|
2105
2176
|
var SUPPORTED_CAPABILITIES = [...CAPABILITY_FLAGS];
|
|
2106
2177
|
function matchesWsPath(url) {
|
|
2107
2178
|
return url.split("?")[0] === WS_PATH;
|
|
@@ -2175,7 +2246,13 @@ function handleConnection(ws, principal, deps) {
|
|
|
2175
2246
|
return;
|
|
2176
2247
|
}
|
|
2177
2248
|
helloReceived = true;
|
|
2178
|
-
deps.hub.registerConnection(
|
|
2249
|
+
deps.hub.registerConnection(
|
|
2250
|
+
deviceId,
|
|
2251
|
+
ws,
|
|
2252
|
+
payload.runtimes,
|
|
2253
|
+
payload.capabilities,
|
|
2254
|
+
payload.configuredToolsets
|
|
2255
|
+
);
|
|
2179
2256
|
deps.hub.sendConnAck(deviceId, SUPPORTED_CAPABILITIES);
|
|
2180
2257
|
if (payload.cursor !== void 0) {
|
|
2181
2258
|
deps.hub.redeliverAfterReconnect(deviceId, payload.cursor);
|
|
@@ -2199,6 +2276,14 @@ function handleConnection(ws, principal, deps) {
|
|
|
2199
2276
|
}
|
|
2200
2277
|
});
|
|
2201
2278
|
}
|
|
2279
|
+
function closeSqliteDatabaseAfterInitializationFailure(db, initializationError, message, close = (database) => database.close()) {
|
|
2280
|
+
try {
|
|
2281
|
+
close(db);
|
|
2282
|
+
} catch (closeError) {
|
|
2283
|
+
throw new AggregateError([initializationError, closeError], message);
|
|
2284
|
+
}
|
|
2285
|
+
throw initializationError;
|
|
2286
|
+
}
|
|
2202
2287
|
var SqliteUnavailableError = class extends Error {
|
|
2203
2288
|
constructor(cause) {
|
|
2204
2289
|
super(
|
|
@@ -2238,16 +2323,27 @@ function loadSqliteModule() {
|
|
|
2238
2323
|
var DEFAULT_BUSY_TIMEOUT_MS = 5e3;
|
|
2239
2324
|
var SECURE_FILE_MODE = 384;
|
|
2240
2325
|
var SECURE_DIR_MODE = 448;
|
|
2241
|
-
function openSqliteDatabase(path2, options) {
|
|
2326
|
+
function openSqliteDatabase(path2, options, faults) {
|
|
2242
2327
|
const { DatabaseSync } = loadSqliteModule();
|
|
2243
2328
|
if (path2 !== ":memory:") {
|
|
2244
2329
|
mkdirSync(dirname(path2), { recursive: true, mode: SECURE_DIR_MODE });
|
|
2245
2330
|
}
|
|
2246
2331
|
const db = new DatabaseSync(path2, { timeout: DEFAULT_BUSY_TIMEOUT_MS, ...options });
|
|
2247
|
-
|
|
2248
|
-
|
|
2332
|
+
try {
|
|
2333
|
+
faults?.onStep?.("after-open");
|
|
2334
|
+
if (path2 !== ":memory:") {
|
|
2335
|
+
db.exec("PRAGMA journal_mode = WAL;");
|
|
2336
|
+
faults?.onStep?.("after-wal");
|
|
2337
|
+
}
|
|
2338
|
+
return db;
|
|
2339
|
+
} catch (error) {
|
|
2340
|
+
closeSqliteDatabaseAfterInitializationFailure(
|
|
2341
|
+
db,
|
|
2342
|
+
error,
|
|
2343
|
+
"SQLite open initialization failed and its native handle could not be closed",
|
|
2344
|
+
faults?.close
|
|
2345
|
+
);
|
|
2249
2346
|
}
|
|
2250
|
-
return db;
|
|
2251
2347
|
}
|
|
2252
2348
|
function secureSqliteFilePermissions(dbPath) {
|
|
2253
2349
|
if (dbPath === ":memory:") return;
|
|
@@ -2265,6 +2361,7 @@ CREATE TABLE IF NOT EXISTS tasks (
|
|
|
2265
2361
|
state TEXT NOT NULL,
|
|
2266
2362
|
instruction TEXT NOT NULL,
|
|
2267
2363
|
runtime TEXT,
|
|
2364
|
+
required_toolsets_json TEXT,
|
|
2268
2365
|
policy_json TEXT NOT NULL,
|
|
2269
2366
|
device_id TEXT,
|
|
2270
2367
|
session_ref TEXT,
|
|
@@ -2279,6 +2376,7 @@ CREATE TABLE IF NOT EXISTS tasks (
|
|
|
2279
2376
|
var ADDITIVE_COLUMNS = [
|
|
2280
2377
|
{ name: "pending_approval_id", ddl: "ALTER TABLE tasks ADD COLUMN pending_approval_id TEXT" },
|
|
2281
2378
|
{ name: "claimed_runtime", ddl: "ALTER TABLE tasks ADD COLUMN claimed_runtime TEXT" },
|
|
2379
|
+
{ name: "required_toolsets_json", ddl: "ALTER TABLE tasks ADD COLUMN required_toolsets_json TEXT" },
|
|
2282
2380
|
// S0 (GAP-002): the claim-time `RuntimeCapabilities` snapshot, stored as
|
|
2283
2381
|
// the JSON text of that closed, protocol-validated object (same idiom as
|
|
2284
2382
|
// `policy_json`/`result_json` above — no column-per-flag, so a future
|
|
@@ -2311,12 +2409,14 @@ function ensureAdditiveColumns(db) {
|
|
|
2311
2409
|
}
|
|
2312
2410
|
function rowToRecord(row) {
|
|
2313
2411
|
const resultJson = row.result_json;
|
|
2412
|
+
const requiredToolsetsJson = row.required_toolsets_json;
|
|
2314
2413
|
const claimedRuntimeCapabilitiesJson = row.claimed_runtime_capabilities_json;
|
|
2315
2414
|
return {
|
|
2316
2415
|
taskId: row.task_id,
|
|
2317
2416
|
state: row.state,
|
|
2318
2417
|
instruction: row.instruction,
|
|
2319
2418
|
runtime: row.runtime ?? void 0,
|
|
2419
|
+
requiredToolsets: requiredToolsetsJson ? JSON.parse(requiredToolsetsJson) : void 0,
|
|
2320
2420
|
policy: JSON.parse(row.policy_json),
|
|
2321
2421
|
deviceId: row.device_id ?? void 0,
|
|
2322
2422
|
sessionRef: row.session_ref ?? void 0,
|
|
@@ -2335,28 +2435,37 @@ var SqliteTaskStore = class {
|
|
|
2335
2435
|
selectStmt;
|
|
2336
2436
|
selectAllStmt;
|
|
2337
2437
|
updatePendingApprovalIdStmt;
|
|
2438
|
+
closed = false;
|
|
2338
2439
|
constructor(opts) {
|
|
2339
2440
|
this.db = openSqliteDatabase(opts.path);
|
|
2340
|
-
|
|
2341
|
-
|
|
2342
|
-
|
|
2343
|
-
|
|
2344
|
-
|
|
2345
|
-
|
|
2346
|
-
|
|
2347
|
-
|
|
2348
|
-
|
|
2349
|
-
|
|
2350
|
-
|
|
2351
|
-
|
|
2352
|
-
|
|
2353
|
-
|
|
2354
|
-
|
|
2355
|
-
|
|
2356
|
-
|
|
2357
|
-
|
|
2358
|
-
|
|
2359
|
-
|
|
2441
|
+
try {
|
|
2442
|
+
this.db.exec(SCHEMA);
|
|
2443
|
+
ensureAdditiveColumns(this.db);
|
|
2444
|
+
secureSqliteFilePermissions(opts.path);
|
|
2445
|
+
this.insertStmt = this.db.prepare(
|
|
2446
|
+
`INSERT INTO tasks
|
|
2447
|
+
(task_id, state, instruction, runtime, required_toolsets_json, policy_json, device_id, session_ref, created_at, updated_at, result_json)
|
|
2448
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
|
|
2449
|
+
);
|
|
2450
|
+
this.updateStmt = this.db.prepare(
|
|
2451
|
+
`UPDATE tasks SET
|
|
2452
|
+
state = ?, instruction = ?, runtime = ?, required_toolsets_json = ?, policy_json = ?, device_id = ?,
|
|
2453
|
+
session_ref = ?, created_at = ?, updated_at = ?, result_json = ?, pending_approval_id = ?,
|
|
2454
|
+
claimed_runtime = ?, claimed_runtime_capabilities_json = ?
|
|
2455
|
+
WHERE task_id = ? AND state = ?`
|
|
2456
|
+
);
|
|
2457
|
+
this.selectStmt = this.db.prepare("SELECT * FROM tasks WHERE task_id = ?");
|
|
2458
|
+
this.selectAllStmt = this.db.prepare("SELECT * FROM tasks ORDER BY rowid ASC");
|
|
2459
|
+
this.updatePendingApprovalIdStmt = this.db.prepare(
|
|
2460
|
+
"UPDATE tasks SET pending_approval_id = ?, updated_at = ? WHERE task_id = ? AND state = 'AwaitApproval'"
|
|
2461
|
+
);
|
|
2462
|
+
} catch (error) {
|
|
2463
|
+
closeSqliteDatabaseAfterInitializationFailure(
|
|
2464
|
+
this.db,
|
|
2465
|
+
error,
|
|
2466
|
+
"SqliteTaskStore initialization failed and its native handle could not be closed"
|
|
2467
|
+
);
|
|
2468
|
+
}
|
|
2360
2469
|
}
|
|
2361
2470
|
create(input) {
|
|
2362
2471
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
@@ -2365,6 +2474,7 @@ var SqliteTaskStore = class {
|
|
|
2365
2474
|
state: "Offered",
|
|
2366
2475
|
instruction: input.instruction,
|
|
2367
2476
|
runtime: input.runtime,
|
|
2477
|
+
requiredToolsets: input.requiredToolsets,
|
|
2368
2478
|
policy: input.policy,
|
|
2369
2479
|
deviceId: input.deviceId,
|
|
2370
2480
|
sessionRef: input.sessionRef,
|
|
@@ -2376,6 +2486,7 @@ var SqliteTaskStore = class {
|
|
|
2376
2486
|
record.state,
|
|
2377
2487
|
record.instruction,
|
|
2378
2488
|
record.runtime ?? null,
|
|
2489
|
+
record.requiredToolsets ? JSON.stringify(record.requiredToolsets) : null,
|
|
2379
2490
|
JSON.stringify(record.policy),
|
|
2380
2491
|
record.deviceId ?? null,
|
|
2381
2492
|
record.sessionRef ?? null,
|
|
@@ -2433,6 +2544,7 @@ var SqliteTaskStore = class {
|
|
|
2433
2544
|
updated.state,
|
|
2434
2545
|
updated.instruction,
|
|
2435
2546
|
updated.runtime ?? null,
|
|
2547
|
+
updated.requiredToolsets ? JSON.stringify(updated.requiredToolsets) : null,
|
|
2436
2548
|
JSON.stringify(updated.policy),
|
|
2437
2549
|
updated.deviceId ?? null,
|
|
2438
2550
|
updated.sessionRef ?? null,
|
|
@@ -2481,7 +2593,9 @@ var SqliteTaskStore = class {
|
|
|
2481
2593
|
* instance against the same file, or on process shutdown.
|
|
2482
2594
|
*/
|
|
2483
2595
|
close() {
|
|
2596
|
+
if (this.closed) return;
|
|
2484
2597
|
this.db.close();
|
|
2598
|
+
this.closed = true;
|
|
2485
2599
|
}
|
|
2486
2600
|
};
|
|
2487
2601
|
var SCHEMA2 = `
|
|
@@ -2530,21 +2644,30 @@ var SqliteBlobStore = class {
|
|
|
2530
2644
|
selectBlobStmt;
|
|
2531
2645
|
selectUploadedStmt;
|
|
2532
2646
|
writeContentStmt;
|
|
2647
|
+
closed = false;
|
|
2533
2648
|
constructor(opts) {
|
|
2534
2649
|
this.db = openSqliteDatabase(opts.path);
|
|
2535
|
-
|
|
2536
|
-
|
|
2537
|
-
|
|
2538
|
-
|
|
2539
|
-
|
|
2540
|
-
|
|
2541
|
-
|
|
2542
|
-
|
|
2543
|
-
|
|
2544
|
-
|
|
2545
|
-
|
|
2546
|
-
|
|
2547
|
-
|
|
2650
|
+
try {
|
|
2651
|
+
this.db.exec(SCHEMA2);
|
|
2652
|
+
secureSqliteFilePermissions(opts.path);
|
|
2653
|
+
this.urlTtlMs = opts.urlTtlMs ?? DEFAULT_URL_TTL_MS2;
|
|
2654
|
+
this.secret = opts.signingKey ?? loadOrCreateSigningSecret(this.db);
|
|
2655
|
+
this.insertBlobStmt = this.db.prepare(
|
|
2656
|
+
`INSERT INTO blobs (blob_id, size, content_type, content_hash, uploaded, data)
|
|
2657
|
+
VALUES (?, ?, ?, ?, 0, NULL)`
|
|
2658
|
+
);
|
|
2659
|
+
this.selectBlobStmt = this.db.prepare(
|
|
2660
|
+
"SELECT size, content_type, content_hash, uploaded, data FROM blobs WHERE blob_id = ?"
|
|
2661
|
+
);
|
|
2662
|
+
this.selectUploadedStmt = this.db.prepare("SELECT uploaded FROM blobs WHERE blob_id = ?");
|
|
2663
|
+
this.writeContentStmt = this.db.prepare("UPDATE blobs SET data = ?, uploaded = 1 WHERE blob_id = ?");
|
|
2664
|
+
} catch (error) {
|
|
2665
|
+
closeSqliteDatabaseAfterInitializationFailure(
|
|
2666
|
+
this.db,
|
|
2667
|
+
error,
|
|
2668
|
+
"SqliteBlobStore initialization failed and its native handle could not be closed"
|
|
2669
|
+
);
|
|
2670
|
+
}
|
|
2548
2671
|
}
|
|
2549
2672
|
async createUpload(input, requestedBlobId) {
|
|
2550
2673
|
const blobId = requestedBlobId ?? `blob_${randomUUID()}`;
|
|
@@ -2596,7 +2719,9 @@ var SqliteBlobStore = class {
|
|
|
2596
2719
|
}
|
|
2597
2720
|
/** Close the underlying database connection — see `SqliteTaskStore.close`'s doc comment; same rationale. */
|
|
2598
2721
|
close() {
|
|
2722
|
+
if (this.closed) return;
|
|
2599
2723
|
this.db.close();
|
|
2724
|
+
this.closed = true;
|
|
2600
2725
|
}
|
|
2601
2726
|
computeSig(blobId, action, exp) {
|
|
2602
2727
|
return createHmac("sha256", this.secret).update(`${blobId}:${action}:${exp}`).digest("base64url");
|
|
@@ -2604,7 +2729,7 @@ var SqliteBlobStore = class {
|
|
|
2604
2729
|
signUrl(blobId, action) {
|
|
2605
2730
|
const exp = Date.now() + this.urlTtlMs;
|
|
2606
2731
|
const sig = this.computeSig(blobId, action, exp);
|
|
2607
|
-
return
|
|
2732
|
+
return `${byokBlobContentPath(blobId)}?sig=${sig}&exp=${exp}`;
|
|
2608
2733
|
}
|
|
2609
2734
|
};
|
|
2610
2735
|
|
|
@@ -2629,6 +2754,11 @@ function createByokServer(opts) {
|
|
|
2629
2754
|
devices,
|
|
2630
2755
|
nonces,
|
|
2631
2756
|
tokenSigner,
|
|
2757
|
+
// S1: the product this instance serves is part of `authenticateBearer`'s
|
|
2758
|
+
// decision (`auth.ts`), not just of the WS hello gate — so every
|
|
2759
|
+
// bearer-authed route gets the same instance-equality guarantee the WS
|
|
2760
|
+
// upgrade already had transitively.
|
|
2761
|
+
productId: opts.productId,
|
|
2632
2762
|
blobStore,
|
|
2633
2763
|
maxBlobSizeBytes,
|
|
2634
2764
|
longPollHoldMs,
|