@byok-sdk/server 0.10.2 → 0.12.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/dist/index.js CHANGED
@@ -1,16 +1,16 @@
1
+ import { CAPABILITY_FLAGS, byokBlobContentPath, canTransition, RESULT_DOCUMENT_MAX_BYTES, PROTOCOL_VERSION, encodeEnvelope, DAEMON_TO_SERVER_TYPES, AGENT_EGRESS_POLICY_CAPABILITY, AGENT_EGRESS_RELIABLE_ACK_CAPABILITY, AGENT_MESSAGE_EGRESS_CAPABILITY, AgentRefSchema, AgentEgressPolicySchema, AgentMessageEgressRequirementSchema, TerminalProjectionSelectionSchema, AgentMessageServerContextSchema, DispatchSelectionSchema, RequiredToolsetsSchema, STRICT_AGENT_ONLY_CAPABILITY, AGENT_EGRESS_FRESH_SESSION_CAPABILITY, TERMINAL_PROJECTION_SELECTION_CAPABILITY, AgentContentReadPayloadSchema, AgentHomeProjectionPayloadSchema, AGENT_HOME_PROJECTION_CAPABILITY, AgentHomeProjectionReadbackSchema, AgentHomeProjectionCompletionRequestSchema, createEnvelope, TASK_STATES, BYOK_PAIR_PATH, PairRequestSchema, PairResponseSchema, 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, BYOK_AGENT_HOME_PROJECTION_COMPLETION_ROUTE, AGENT_CONTENT_ARTIFACT_READ_CAPABILITY, AGENT_CONTENT_TRANSCRIPT_READ_CAPABILITY, AGENT_CONTENT_WORKSPACE_READ_CAPABILITY, PairResponseTenantIdSchema, decodeEnvelope, BYOK_WS_PATH } from '@byok-sdk/protocol';
1
2
  import { randomBytes, randomUUID, timingSafeEqual, createHmac, createHash, createPublicKey, verify } from 'crypto';
2
3
  import { nonceSigningBytes } from '@byok-sdk/core';
3
4
  import { jwtVerify, SignJWT } from 'jose';
4
- import { mkdir, writeFile, readFile } from 'fs/promises';
5
+ import { mkdir, writeFile, readFile, rename } from 'fs/promises';
5
6
  import { mkdtempSync, mkdirSync, existsSync, chmodSync } from 'fs';
6
7
  import { tmpdir } from 'os';
7
8
  import path, { dirname } from 'path';
8
- import { CAPABILITY_FLAGS, byokBlobContentPath, canTransition, PROTOCOL_VERSION, encodeEnvelope, DAEMON_TO_SERVER_TYPES, AGENT_EGRESS_POLICY_CAPABILITY, AGENT_EGRESS_RELIABLE_ACK_CAPABILITY, AGENT_MESSAGE_EGRESS_CAPABILITY, AgentRefSchema, AgentEgressPolicySchema, AgentMessageEgressRequirementSchema, TerminalProjectionSelectionSchema, AgentMessageServerContextSchema, DispatchSelectionSchema, RequiredToolsetsSchema, STRICT_AGENT_ONLY_CAPABILITY, AGENT_EGRESS_FRESH_SESSION_CAPABILITY, TERMINAL_PROJECTION_SELECTION_CAPABILITY, AgentContentReadPayloadSchema, AgentHomeProjectionPayloadSchema, AGENT_HOME_PROJECTION_CAPABILITY, AgentHomeProjectionReadbackSchema, AgentHomeProjectionCompletionRequestSchema, createEnvelope, TASK_STATES, BYOK_PAIR_PATH, PairRequestSchema, PairResponseSchema, 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, BYOK_AGENT_HOME_PROJECTION_COMPLETION_ROUTE, AGENT_CONTENT_ARTIFACT_READ_CAPABILITY, AGENT_CONTENT_TRANSCRIPT_READ_CAPABILITY, AGENT_CONTENT_WORKSPACE_READ_CAPABILITY, PairResponseTenantIdSchema, decodeEnvelope, BYOK_WS_PATH } from '@byok-sdk/protocol';
9
9
  import { Hono } from 'hono';
10
10
  import { WebSocketServer } from 'ws';
11
11
  import { createRequire } from 'module';
12
12
 
13
- // src/auth.ts
13
+ // src/index.ts
14
14
  var ACCESS_TOKEN_TTL_SECONDS = 60 * 60;
15
15
  var NONCE_TTL_MS = 5 * 60 * 1e3;
16
16
  function createHmacTokenSigner(secret = randomBytes(32)) {
@@ -208,34 +208,46 @@ function sha256Hex(data) {
208
208
  var LocalDiskBlobStore = class {
209
209
  secret = randomBytes(32);
210
210
  directory;
211
+ metadataPath;
211
212
  urlTtlMs;
212
213
  blobs = /* @__PURE__ */ new Map();
213
214
  ready;
215
+ metadataWriteTail = Promise.resolve();
214
216
  constructor(opts = {}) {
215
217
  this.directory = opts.directory ?? mkdtempSync(path.join(tmpdir(), "byok-blobs-"));
218
+ this.metadataPath = path.join(this.directory, "metadata.json");
216
219
  this.urlTtlMs = opts.urlTtlMs ?? DEFAULT_URL_TTL_MS;
217
- this.ready = mkdir(this.directory, { recursive: true }).then(() => void 0);
220
+ this.ready = mkdir(this.directory, { recursive: true }).then(() => this.loadMetadata());
218
221
  }
219
- async createUpload(input, requestedBlobId) {
222
+ async createUpload(tenantId, input, requestedBlobId) {
220
223
  await this.ready;
221
224
  const blobId = requestedBlobId ?? `blob_${randomUUID()}`;
222
225
  const existing = this.blobs.get(blobId);
223
226
  if (existing !== void 0) {
224
- if (existing.meta.size !== input.size || existing.meta.contentType !== input.contentType || existing.meta.contentHash !== input.contentHash) {
227
+ if (existing.tenantId !== tenantId || existing.meta.size !== input.size || existing.meta.contentType !== input.contentType || existing.meta.contentHash !== input.contentHash) {
225
228
  throw new BlobDeclarationConflictError(blobId);
226
229
  }
227
230
  return { blobId, uploadUrl: this.signUrl(blobId, "put") };
228
231
  }
229
- this.blobs.set(blobId, { meta: input, uploaded: false });
232
+ this.blobs.set(blobId, { tenantId, meta: input, uploaded: false });
233
+ await this.persistMetadata();
230
234
  return { blobId, uploadUrl: this.signUrl(blobId, "put") };
231
235
  }
232
- async getDownloadUrl(blobId) {
236
+ async getDownloadUrl(tenantId, blobId) {
237
+ await this.ready;
233
238
  const record = this.blobs.get(blobId);
234
- if (!record?.uploaded) return void 0;
239
+ if (!record?.uploaded || record.tenantId !== tenantId) return void 0;
235
240
  return this.signUrl(blobId, "get");
236
241
  }
237
- async exists(blobId) {
238
- return this.blobs.get(blobId)?.uploaded ?? false;
242
+ async exists(tenantId, blobId) {
243
+ await this.ready;
244
+ const record = this.blobs.get(blobId);
245
+ return record?.tenantId === tenantId && record.uploaded;
246
+ }
247
+ async getUploadReservation(blobId) {
248
+ await this.ready;
249
+ const record = this.blobs.get(blobId);
250
+ return record === void 0 ? void 0 : { size: record.meta.size };
239
251
  }
240
252
  verifySignedUrl(blobId, action, sig, exp) {
241
253
  if (!Number.isFinite(exp) || Date.now() > exp) return false;
@@ -259,9 +271,11 @@ var LocalDiskBlobStore = class {
259
271
  }
260
272
  await writeFile(this.pathFor(blobId), data);
261
273
  record.uploaded = true;
274
+ await this.persistMetadata();
262
275
  return { ok: true };
263
276
  }
264
277
  async readContent(blobId) {
278
+ await this.ready;
265
279
  const record = this.blobs.get(blobId);
266
280
  if (!record?.uploaded) return void 0;
267
281
  const data = await readFile(this.pathFor(blobId));
@@ -270,6 +284,41 @@ var LocalDiskBlobStore = class {
270
284
  pathFor(blobId) {
271
285
  return path.join(this.directory, blobId);
272
286
  }
287
+ async loadMetadata() {
288
+ let raw;
289
+ try {
290
+ raw = await readFile(this.metadataPath, "utf8");
291
+ } catch (error) {
292
+ if (error.code === "ENOENT") return;
293
+ throw error;
294
+ }
295
+ const parsed = JSON.parse(raw);
296
+ if (!isBlobMetadataDocument(parsed)) {
297
+ throw new Error("invalid LocalDiskBlobStore metadata document");
298
+ }
299
+ for (const entry of parsed.blobs) {
300
+ this.blobs.set(entry.blobId, {
301
+ tenantId: entry.tenantId,
302
+ meta: entry.meta,
303
+ uploaded: entry.uploaded
304
+ });
305
+ }
306
+ this.secret = Buffer.from(parsed.signingSecret, "base64url");
307
+ }
308
+ async persistMetadata() {
309
+ const write = this.metadataWriteTail.then(async () => {
310
+ const metadata = {
311
+ version: 1,
312
+ signingSecret: this.secret.toString("base64url"),
313
+ blobs: [...this.blobs.entries()].map(([blobId, record]) => ({ blobId, ...record }))
314
+ };
315
+ const temporaryPath = `${this.metadataPath}.${randomUUID()}.tmp`;
316
+ await writeFile(temporaryPath, JSON.stringify(metadata), "utf8");
317
+ await rename(temporaryPath, this.metadataPath);
318
+ });
319
+ this.metadataWriteTail = write.catch(() => void 0);
320
+ await write;
321
+ }
273
322
  computeSig(blobId, action, exp) {
274
323
  return createHmac("sha256", this.secret).update(`${blobId}:${action}:${exp}`).digest("base64url");
275
324
  }
@@ -279,6 +328,21 @@ var LocalDiskBlobStore = class {
279
328
  return `${byokBlobContentPath(blobId)}?sig=${sig}&exp=${exp}`;
280
329
  }
281
330
  };
331
+ function isBlobMetadataDocument(value) {
332
+ if (typeof value !== "object" || value === null) return false;
333
+ const document = value;
334
+ if (document.version !== 1 || typeof document.signingSecret !== "string" || !Array.isArray(document.blobs)) return false;
335
+ const signingSecret = Buffer.from(document.signingSecret, "base64url");
336
+ if (signingSecret.length !== 32) return false;
337
+ return document.blobs.every((entry) => {
338
+ if (typeof entry !== "object" || entry === null) return false;
339
+ const candidate = entry;
340
+ if (typeof candidate.blobId !== "string" || typeof candidate.tenantId !== "string" || typeof candidate.uploaded !== "boolean") return false;
341
+ if (typeof candidate.meta !== "object" || candidate.meta === null) return false;
342
+ const meta = candidate.meta;
343
+ return typeof meta.size === "number" && Number.isSafeInteger(meta.size) && meta.size >= 0 && typeof meta.contentType === "string" && typeof meta.contentHash === "string";
344
+ });
345
+ }
282
346
 
283
347
  // src/event-queue.ts
284
348
  var AsyncEventQueue = class {
@@ -567,6 +631,16 @@ var AgentHomeProjectionCompletionError = class extends Error {
567
631
  }
568
632
  code;
569
633
  };
634
+ var ReplayGapError = class extends Error {
635
+ constructor(cursor, recoverableFrom) {
636
+ super(`cursor_too_old: cursor ${String(cursor)} is older than recoverable sequence ${String(recoverableFrom)}`);
637
+ this.cursor = cursor;
638
+ this.recoverableFrom = recoverableFrom;
639
+ this.name = "ReplayGapError";
640
+ }
641
+ cursor;
642
+ recoverableFrom;
643
+ };
570
644
  var ConnectionHub = class {
571
645
  constructor(taskStore, devices, taskLeaseMs, rateLimiter = new RateLimiter(), agentMessage) {
572
646
  this.taskStore = taskStore;
@@ -584,6 +658,7 @@ var ConnectionHub = class {
584
658
  rateLimiter;
585
659
  agentMessage;
586
660
  connections = /* @__PURE__ */ new Map();
661
+ connectionEpochs = /* @__PURE__ */ new Map();
587
662
  outboxes = /* @__PURE__ */ new Map();
588
663
  /** Idempotency window per device (N3) — recent inbound envelope ids, capped at {@link DEDUP_RING_CAPACITY}. */
589
664
  dedupRings = /* @__PURE__ */ new Map();
@@ -661,8 +736,11 @@ var ConnectionHub = class {
661
736
  */
662
737
  registerConnection(deviceId, ws, runtimes, capabilities, configuredToolsets, clientVersion) {
663
738
  const at = (/* @__PURE__ */ new Date()).toISOString();
739
+ const prior = this.connections.get(deviceId);
740
+ const epoch = this.nextConnectionEpoch(deviceId);
664
741
  this.connections.set(deviceId, {
665
742
  ws,
743
+ epoch,
666
744
  connected: true,
667
745
  lastSeen: at,
668
746
  clientVersion,
@@ -672,6 +750,15 @@ var ConnectionHub = class {
672
750
  });
673
751
  this.serverEvents.push({ kind: "device.connected", deviceId, at });
674
752
  this.settleLongPollWaiter(deviceId);
753
+ if (prior?.ws && prior.ws !== ws) {
754
+ prior.ws.close(1e3, "superseded by newer websocket connection");
755
+ }
756
+ return epoch;
757
+ }
758
+ /** True only while this exact WS registration remains the device authority. */
759
+ isCurrentConnection(deviceId, ws, epoch) {
760
+ const connection = this.connections.get(deviceId);
761
+ return connection?.connected === true && connection.ws === ws && connection.epoch === epoch;
675
762
  }
676
763
  sendConnAck(deviceId, capabilities) {
677
764
  this.sendToDevice(
@@ -692,6 +779,7 @@ var ConnectionHub = class {
692
779
  * non-terminal task. Called after `conn.ack` (step 2), per the spec.
693
780
  */
694
781
  redeliverAfterReconnect(deviceId, cursor) {
782
+ this.assertReplayAvailable(deviceId, cursor);
695
783
  const conn = this.connections.get(deviceId);
696
784
  if (!conn?.ws || !conn.connected) return;
697
785
  for (const envelope of this.collectRelevant(deviceId, cursor)) {
@@ -752,6 +840,7 @@ var ConnectionHub = class {
752
840
  forgetDevice(deviceId) {
753
841
  const conn = this.connections.get(deviceId);
754
842
  this.connections.delete(deviceId);
843
+ this.connectionEpochs.delete(deviceId);
755
844
  this.outboxes.delete(deviceId);
756
845
  this.dedupRings.delete(deviceId);
757
846
  this.rateLimitEventEmittedFor.delete(deviceId);
@@ -770,19 +859,25 @@ var ConnectionHub = class {
770
859
  * {@link ConnectionState}).
771
860
  */
772
861
  async pollEvents(deviceId, cursor, holdMs) {
862
+ this.assertReplayAvailable(deviceId, cursor);
773
863
  this.takeOverAsLongPoll(deviceId);
774
864
  this.settleLongPollWaiter(deviceId);
775
865
  const immediate = this.collectRelevant(deviceId, cursor);
776
866
  if (immediate.length > 0) {
777
867
  return { events: immediate, cursor: this.currentCursor(deviceId) };
778
868
  }
779
- return new Promise((resolve) => {
869
+ return new Promise((resolve, reject) => {
780
870
  const timer = setTimeout(() => {
781
871
  this.longPollWaiters.delete(deviceId);
782
- resolve({ events: [], cursor: this.currentCursor(deviceId) });
872
+ try {
873
+ this.assertReplayAvailable(deviceId, cursor);
874
+ resolve({ events: [], cursor: this.currentCursor(deviceId) });
875
+ } catch (error) {
876
+ reject(error);
877
+ }
783
878
  }, holdMs);
784
879
  timer.unref?.();
785
- this.longPollWaiters.set(deviceId, { cursor, resolve, timer });
880
+ this.longPollWaiters.set(deviceId, { cursor, resolve, reject, timer });
786
881
  });
787
882
  }
788
883
  /** Make long-poll this device's active transport, closing any live WS ("last one wins", §8). */
@@ -793,6 +888,7 @@ var ConnectionHub = class {
793
888
  if (conn?.ws) {
794
889
  const ws = conn.ws;
795
890
  this.connections.set(deviceId, {
891
+ epoch: this.nextConnectionEpoch(deviceId),
796
892
  connected: true,
797
893
  lastSeen: at,
798
894
  runtimes: conn.runtimes,
@@ -801,7 +897,7 @@ var ConnectionHub = class {
801
897
  });
802
898
  ws.close(1e3, "superseded by long-poll connection");
803
899
  } else if (!conn) {
804
- this.connections.set(deviceId, { connected: true, lastSeen: at });
900
+ this.connections.set(deviceId, { epoch: this.nextConnectionEpoch(deviceId), connected: true, lastSeen: at });
805
901
  } else {
806
902
  conn.connected = true;
807
903
  conn.lastSeen = at;
@@ -817,7 +913,12 @@ var ConnectionHub = class {
817
913
  if (!waiter) return;
818
914
  this.longPollWaiters.delete(deviceId);
819
915
  clearTimeout(waiter.timer);
820
- waiter.resolve({ events: this.collectRelevant(deviceId, waiter.cursor), cursor: this.currentCursor(deviceId) });
916
+ try {
917
+ this.assertReplayAvailable(deviceId, waiter.cursor);
918
+ waiter.resolve({ events: this.collectRelevant(deviceId, waiter.cursor), cursor: this.currentCursor(deviceId) });
919
+ } catch (error) {
920
+ waiter.reject(error);
921
+ }
821
922
  }
822
923
  // ---------------------------------------------------------------------
823
924
  // inbound envelopes from a connected daemon
@@ -939,7 +1040,6 @@ var ConnectionHub = class {
939
1040
  if (binding === void 0 || record === void 0 || record.deviceId !== deviceId || binding.deviceId !== deviceId || !sameAgentRef(binding.agentRef, payload.agentRef) || binding.sessionRef !== void 0 && binding.sessionRef !== payload.sessionRef || binding.requirement.contract !== payload.contract || binding.requirement.contentType !== payload.contentType || payload.byteCount > binding.requirement.maxBytes) return "rejected";
940
1041
  const taskPayload = this.agentMessageTaskPayloads.get(taskId);
941
1042
  if (taskPayload !== void 0 && JSON.stringify(taskPayload) !== JSON.stringify(payload)) return "rejected";
942
- if (taskPayload === void 0) this.agentMessageTaskPayloads.set(taskId, payload);
943
1043
  const key = `${deviceId}\0${payload.messageId}`;
944
1044
  const previous = this.agentMessageReceipts.get(key);
945
1045
  if (previous !== void 0) {
@@ -947,6 +1047,9 @@ var ConnectionHub = class {
947
1047
  this.sendAgentMessageDisposition(deviceId, taskId, previous.disposition);
948
1048
  return "duplicate";
949
1049
  }
1050
+ if (isTerminal(record.state)) return "rejected";
1051
+ if (taskPayload !== void 0) return "rejected";
1052
+ this.agentMessageTaskPayloads.set(taskId, payload);
950
1053
  const decision = this.agentMessage?.consume({ deviceId, taskId, context: binding.context, payload }) ?? { outcome: "held", reasonCode: "consumer_unavailable" };
951
1054
  const disposition = {
952
1055
  agentRef: payload.agentRef,
@@ -2172,7 +2275,12 @@ var ConnectionHub = class {
2172
2275
  const redeliverThroughTerminal = type === "task.cancel" || type === "task.reject";
2173
2276
  const redeliverWithoutTask = type === "agent.egress.ack" || type === "agent.content.read" || type === "agent.home.projection" || type === "agent.message.disposition";
2174
2277
  outbox.ring.push({ seq, taskId, envelope, redeliverThroughTerminal, redeliverWithoutTask });
2175
- if (outbox.ring.length > OUTBOX_RING_CAPACITY) outbox.ring.shift();
2278
+ if (outbox.ring.length > OUTBOX_RING_CAPACITY) {
2279
+ const evicted = outbox.ring.shift();
2280
+ if (evicted !== void 0 && this.isRecoverableEntry(evicted)) {
2281
+ outbox.recoverableFrom = Math.max(outbox.recoverableFrom, evicted.seq + 1);
2282
+ }
2283
+ }
2176
2284
  this.deliverToDevice(deviceId, envelope);
2177
2285
  return envelope;
2178
2286
  }
@@ -2199,6 +2307,26 @@ var ConnectionHub = class {
2199
2307
  (entry) => entry.seq > cursor && (entry.taskId === void 0 ? entry.redeliverWithoutTask === true : !this.isTaskTerminal(entry.taskId) || entry.redeliverThroughTerminal)
2200
2308
  ).map((entry) => entry.envelope);
2201
2309
  }
2310
+ /**
2311
+ * A caller at `recoverableFrom - 1` can still receive the first retained
2312
+ * control. Anything earlier would be a partial tail and must fail closed.
2313
+ */
2314
+ assertReplayAvailable(deviceId, cursor, pendingOutbound = 0) {
2315
+ const outbox = this.outboxes.get(deviceId);
2316
+ if (outbox === void 0) return;
2317
+ let recoverableFrom = outbox.recoverableFrom;
2318
+ const evictions = Math.max(0, outbox.ring.length + pendingOutbound - OUTBOX_RING_CAPACITY);
2319
+ for (let index = 0; index < evictions; index++) {
2320
+ const evicted = outbox.ring[index];
2321
+ if (evicted !== void 0 && this.isRecoverableEntry(evicted)) {
2322
+ recoverableFrom = Math.max(recoverableFrom, evicted.seq + 1);
2323
+ }
2324
+ }
2325
+ if (cursor < recoverableFrom - 1) throw new ReplayGapError(cursor, recoverableFrom);
2326
+ }
2327
+ isRecoverableEntry(entry) {
2328
+ return entry.redeliverWithoutTask === true || entry.taskId !== void 0 && (!this.isTaskTerminal(entry.taskId) || entry.redeliverThroughTerminal === true);
2329
+ }
2202
2330
  isTaskTerminal(taskId) {
2203
2331
  const record = this.taskStore.get(taskId);
2204
2332
  return !record || isTerminal(record.state);
@@ -2211,11 +2339,16 @@ var ConnectionHub = class {
2211
2339
  getOrCreateOutbox(deviceId) {
2212
2340
  let outbox = this.outboxes.get(deviceId);
2213
2341
  if (!outbox) {
2214
- outbox = { nextSeq: 1, ring: [] };
2342
+ outbox = { nextSeq: 1, recoverableFrom: 1, ring: [] };
2215
2343
  this.outboxes.set(deviceId, outbox);
2216
2344
  }
2217
2345
  return outbox;
2218
2346
  }
2347
+ nextConnectionEpoch(deviceId) {
2348
+ const epoch = (this.connectionEpochs.get(deviceId) ?? 0) + 1;
2349
+ this.connectionEpochs.set(deviceId, epoch);
2350
+ return epoch;
2351
+ }
2219
2352
  // ---------------------------------------------------------------------
2220
2353
  // read-only accessors backing the public `machines` / `tasks` API
2221
2354
  // ---------------------------------------------------------------------
@@ -2301,6 +2434,12 @@ var PairingCodeInvalidError = class extends Error {
2301
2434
  this.name = "PairingCodeInvalidError";
2302
2435
  }
2303
2436
  };
2437
+ var PairingAttemptConflictError = class extends Error {
2438
+ constructor() {
2439
+ super("pairing code is already bound to a different pairing attempt");
2440
+ this.name = "PairingAttemptConflictError";
2441
+ }
2442
+ };
2304
2443
  var PairingManager = class {
2305
2444
  codes = /* @__PURE__ */ new Map();
2306
2445
  /**
@@ -2340,6 +2479,46 @@ var PairingManager = class {
2340
2479
  record.used = true;
2341
2480
  return record.claims;
2342
2481
  }
2482
+ /**
2483
+ * Bind a code to one exact pairing request, returning any completion that a
2484
+ * prior response/token failure left recoverable. This mutation is entirely
2485
+ * synchronous: the HTTP route cannot yield between binding, registration,
2486
+ * and completion recording, so concurrent requests have one winner.
2487
+ */
2488
+ beginPairingAttempt(code, binding) {
2489
+ const record = this.codes.get(code);
2490
+ if (!record) throw new PairingCodeInvalidError("unknown code");
2491
+ if (Date.now() > record.expiresAt) throw new PairingCodeInvalidError("code expired");
2492
+ validatePairingAttemptBinding(binding);
2493
+ if (record.attempt !== void 0) {
2494
+ if (!samePairingAttemptBinding(record.attempt.binding, binding)) throw new PairingAttemptConflictError();
2495
+ return { claims: record.claims, completion: record.attempt.completion };
2496
+ }
2497
+ if (record.used) throw new PairingCodeInvalidError("code already used");
2498
+ record.used = true;
2499
+ record.attempt = { binding: { ...binding } };
2500
+ return { claims: record.claims };
2501
+ }
2502
+ /** Record the one device identity a bound pairing attempt completed as. */
2503
+ completePairingAttempt(code, binding, completion) {
2504
+ const record = this.codes.get(code);
2505
+ if (!record?.attempt || !samePairingAttemptBinding(record.attempt.binding, binding)) {
2506
+ throw new PairingAttemptConflictError();
2507
+ }
2508
+ const expected = {
2509
+ deviceId: completion.deviceId,
2510
+ tenantId: record.claims.tenantId,
2511
+ productId: record.claims.productId,
2512
+ deviceName: binding.deviceName,
2513
+ devicePublicKey: binding.devicePublicKey
2514
+ };
2515
+ if (record.attempt.completion !== void 0) {
2516
+ if (!samePairingCompletion(record.attempt.completion, expected)) throw new PairingAttemptConflictError();
2517
+ return record.attempt.completion;
2518
+ }
2519
+ record.attempt.completion = expected;
2520
+ return expected;
2521
+ }
2343
2522
  };
2344
2523
  function validatePairingCodeClaims(claims) {
2345
2524
  if (typeof claims !== "object" || claims === null) {
@@ -2355,6 +2534,17 @@ function validatePairingCodeClaims(claims) {
2355
2534
  }
2356
2535
  return { tenantId: tenantResult.data, productId };
2357
2536
  }
2537
+ function validatePairingAttemptBinding(binding) {
2538
+ if (typeof binding !== "object" || binding === null || typeof binding.deviceName !== "string" || binding.deviceName.length === 0 || typeof binding.devicePublicKey !== "string" || binding.devicePublicKey.length === 0) {
2539
+ throw new TypeError("pairing attempt requires a non-empty deviceName and devicePublicKey");
2540
+ }
2541
+ }
2542
+ function samePairingAttemptBinding(left, right) {
2543
+ return left.deviceName === right.deviceName && left.devicePublicKey === right.devicePublicKey;
2544
+ }
2545
+ function samePairingCompletion(left, right) {
2546
+ return left.deviceId === right.deviceId && left.tenantId === right.tenantId && left.productId === right.productId && left.deviceName === right.deviceName && left.devicePublicKey === right.devicePublicKey;
2547
+ }
2358
2548
 
2359
2549
  // src/http.ts
2360
2550
  async function readJsonBody(c) {
@@ -2364,6 +2554,34 @@ async function readJsonBody(c) {
2364
2554
  return void 0;
2365
2555
  }
2366
2556
  }
2557
+ function declaredLengthExceeds(value, limit) {
2558
+ if (value === void 0 || !/^(?:0|[1-9]\d*)$/.test(value)) return false;
2559
+ const parsed = Number(value);
2560
+ return Number.isSafeInteger(parsed) && parsed > limit;
2561
+ }
2562
+ async function readBoundedUpload(c, limit) {
2563
+ const body = c.req.raw.body;
2564
+ if (body === null) return { kind: "ok", data: Buffer.alloc(0) };
2565
+ const reader = body.getReader();
2566
+ const target = Buffer.allocUnsafe(limit);
2567
+ let received = 0;
2568
+ try {
2569
+ while (true) {
2570
+ const { done, value } = await reader.read();
2571
+ if (done) return { kind: "ok", data: target.subarray(0, received) };
2572
+ if (received + value.byteLength > limit) {
2573
+ void reader.cancel().catch(() => void 0);
2574
+ return { kind: "too_large" };
2575
+ }
2576
+ target.set(value, received);
2577
+ received += value.byteLength;
2578
+ }
2579
+ } catch {
2580
+ return { kind: "read_error" };
2581
+ } finally {
2582
+ reader.releaseLock();
2583
+ }
2584
+ }
2367
2585
  function buildHonoApp(deps) {
2368
2586
  const app = new Hono();
2369
2587
  const serverStartedAtMs = Date.now();
@@ -2376,24 +2594,32 @@ function buildHonoApp(deps) {
2376
2594
  return c.json({ error: "pairingCode, deviceName, and devicePublicKey are required strings" }, 400);
2377
2595
  }
2378
2596
  const { pairingCode, deviceName, devicePublicKey } = parsed.data;
2379
- let claims;
2597
+ const binding = { deviceName, devicePublicKey };
2598
+ let attempt;
2380
2599
  try {
2381
- claims = deps.pairing.redeemPairingCode(pairingCode);
2600
+ attempt = deps.pairing.beginPairingAttempt(pairingCode, binding);
2382
2601
  } catch (err) {
2383
2602
  if (err instanceof PairingCodeInvalidError) {
2384
2603
  return c.json({ error: err.message }, 401);
2385
2604
  }
2605
+ if (err instanceof PairingAttemptConflictError) {
2606
+ return c.json({ error: "pairing_attempt_conflict" }, 409);
2607
+ }
2386
2608
  throw err;
2387
2609
  }
2388
- const deviceId = generateDeviceId();
2389
- deps.devices.register({
2390
- tenantId: claims.tenantId,
2391
- productId: claims.productId,
2392
- deviceId,
2393
- deviceName,
2394
- devicePublicKey
2395
- });
2396
- const device = deps.devices.get(claims.tenantId, deviceId);
2610
+ let completion = attempt.completion;
2611
+ if (completion === void 0) {
2612
+ const candidate = {
2613
+ tenantId: attempt.claims.tenantId,
2614
+ productId: attempt.claims.productId,
2615
+ deviceId: generateDeviceId(),
2616
+ deviceName,
2617
+ devicePublicKey
2618
+ };
2619
+ deps.devices.register(candidate);
2620
+ completion = deps.pairing.completePairingAttempt(pairingCode, binding, candidate);
2621
+ }
2622
+ const device = deps.devices.get(completion.tenantId, completion.deviceId);
2397
2623
  if (device === void 0) {
2398
2624
  throw new Error("paired device row was not persisted");
2399
2625
  }
@@ -2459,7 +2685,7 @@ function buildHonoApp(deps) {
2459
2685
  }
2460
2686
  const blobId = reservationBlobId(principal.tenantId, reservationId);
2461
2687
  try {
2462
- const created = await deps.blobStore.createUpload(parsed.data, blobId);
2688
+ const created = await deps.blobStore.createUpload(principal.tenantId, parsed.data, blobId);
2463
2689
  const response = created;
2464
2690
  return c.json(response, 200);
2465
2691
  } catch (error) {
@@ -2480,7 +2706,7 @@ function buildHonoApp(deps) {
2480
2706
  if (reservationBlobId(principal.tenantId, reservationId) !== blobId) {
2481
2707
  return c.json({ error: "storage_integrity_mismatch" }, 422);
2482
2708
  }
2483
- if (!await deps.blobStore.exists(blobId)) {
2709
+ if (!await deps.blobStore.exists(principal.tenantId, blobId)) {
2484
2710
  return c.json({ error: "storage_reservation_not_found" }, 404);
2485
2711
  }
2486
2712
  return c.body(null, 204);
@@ -2488,7 +2714,7 @@ function buildHonoApp(deps) {
2488
2714
  app.get(BYOK_BLOB_URL_ROUTE, async (c) => {
2489
2715
  const principal = await authenticateBearer(c.req.header("authorization"), deps);
2490
2716
  if (!principal) return c.json({ error: "unauthorized" }, 401);
2491
- const downloadUrl = await deps.blobStore.getDownloadUrl(c.req.param("id"));
2717
+ const downloadUrl = await deps.blobStore.getDownloadUrl(principal.tenantId, c.req.param("id"));
2492
2718
  if (!downloadUrl) return c.json({ error: "blob not found" }, 404);
2493
2719
  const response = { downloadUrl };
2494
2720
  return c.json(response, 200);
@@ -2499,8 +2725,18 @@ function buildHonoApp(deps) {
2499
2725
  if (!sig || exp === void 0 || !deps.blobStore.verifySignedUrl(blobId, "put", sig, exp)) {
2500
2726
  return c.json({ error: "invalid or expired signature" }, 401);
2501
2727
  }
2502
- const data = Buffer.from(await c.req.arrayBuffer());
2503
- const result = await deps.blobStore.writeContent(blobId, data);
2728
+ const reservation = await deps.blobStore.getUploadReservation(blobId);
2729
+ if (reservation === void 0) return c.json({ error: "blob not found" }, 404);
2730
+ if (reservation.size > deps.maxBlobSizeBytes) {
2731
+ return c.json({ error: `blob exceeds max size of ${deps.maxBlobSizeBytes} bytes` }, 413);
2732
+ }
2733
+ if (declaredLengthExceeds(c.req.header("content-length"), reservation.size)) {
2734
+ return c.json({ error: "upload body exceeds reservation" }, 413);
2735
+ }
2736
+ const body = await readBoundedUpload(c, reservation.size);
2737
+ if (body.kind === "too_large") return c.json({ error: "upload body exceeds reservation" }, 413);
2738
+ if (body.kind === "read_error") return c.json({ error: "failed to read upload body" }, 400);
2739
+ const result = await deps.blobStore.writeContent(blobId, body.data);
2504
2740
  if (!result.ok) return c.json({ error: result.reason }, 422);
2505
2741
  return c.body(null, 204);
2506
2742
  });
@@ -2524,9 +2760,16 @@ function buildHonoApp(deps) {
2524
2760
  if (!Number.isInteger(parsedCursor) || parsedCursor < 0) return c.json({ error: "invalid cursor" }, 400);
2525
2761
  cursor = parsedCursor;
2526
2762
  }
2527
- const result = await deps.hub.pollEvents(principal.deviceId, cursor, deps.longPollHoldMs);
2528
- const response = { ...result, capabilities: [...CAPABILITY_FLAGS] };
2529
- return c.json(response, 200);
2763
+ try {
2764
+ const result = await deps.hub.pollEvents(principal.deviceId, cursor, deps.longPollHoldMs);
2765
+ const response = { ...result, capabilities: [...CAPABILITY_FLAGS] };
2766
+ return c.json(response, 200);
2767
+ } catch (error) {
2768
+ if (error instanceof ReplayGapError) {
2769
+ return c.json({ error: "cursor_too_old", recoverableFrom: error.recoverableFrom }, 409);
2770
+ }
2771
+ throw error;
2772
+ }
2530
2773
  });
2531
2774
  app.post(BYOK_MESSAGES_PATH, async (c) => {
2532
2775
  const principal = await authenticateBearer(c.req.header("authorization"), deps);
@@ -2716,8 +2959,14 @@ function toDecodable(data) {
2716
2959
  if (data instanceof ArrayBuffer) return new Uint8Array(data);
2717
2960
  return data;
2718
2961
  }
2962
+ function rawDataByteLength(data) {
2963
+ if (typeof data === "string") return Buffer.byteLength(data);
2964
+ if (Array.isArray(data)) return data.reduce((total, part) => total + part.byteLength, 0);
2965
+ return data.byteLength;
2966
+ }
2719
2967
  function attachWebSocket(server, deps) {
2720
- const wss = new WebSocketServer({ noServer: true });
2968
+ const wss = new WebSocketServer({ noServer: true, maxPayload: deps.maxPayloadBytes });
2969
+ let pendingHelloConnections = 0;
2721
2970
  server.on("upgrade", (req, socket, head) => {
2722
2971
  if (!matchesWsPath(req.url ?? "")) return;
2723
2972
  void (async () => {
@@ -2726,17 +2975,44 @@ function attachWebSocket(server, deps) {
2726
2975
  rejectUpgrade(socket, 401, "Unauthorized");
2727
2976
  return;
2728
2977
  }
2729
- wss.handleUpgrade(req, socket, head, (ws) => {
2730
- handleConnection(ws, principal, deps);
2731
- });
2978
+ if (pendingHelloConnections >= deps.maxPendingWebSockets) {
2979
+ rejectUpgrade(socket, 503, "WebSocket hello admission limit reached");
2980
+ return;
2981
+ }
2982
+ pendingHelloConnections++;
2983
+ let released = false;
2984
+ const releasePendingHello = () => {
2985
+ if (released) return;
2986
+ released = true;
2987
+ pendingHelloConnections--;
2988
+ };
2989
+ try {
2990
+ wss.handleUpgrade(req, socket, head, (ws) => {
2991
+ handleConnection(ws, principal, deps, releasePendingHello);
2992
+ });
2993
+ } catch {
2994
+ releasePendingHello();
2995
+ socket.destroy();
2996
+ }
2732
2997
  })();
2733
2998
  });
2734
2999
  }
2735
- function handleConnection(ws, principal, deps) {
3000
+ function handleConnection(ws, principal, deps, releasePendingHello) {
2736
3001
  const { deviceId } = principal;
2737
3002
  let helloReceived = false;
3003
+ let epoch;
2738
3004
  let heartbeat;
3005
+ const helloDeadline = setTimeout(() => {
3006
+ if (!helloReceived) ws.close(1008, "hello_timeout");
3007
+ }, deps.helloTimeoutMs);
3008
+ helloDeadline.unref?.();
3009
+ const clearHelloDeadline = () => clearTimeout(helloDeadline);
3010
+ ws.on("error", () => void 0);
2739
3011
  ws.once("message", (data) => {
3012
+ if (rawDataByteLength(data) > deps.maxPayloadBytes) {
3013
+ ws.close(1009, "payload_too_large");
3014
+ return;
3015
+ }
2740
3016
  let envelope;
2741
3017
  try {
2742
3018
  envelope = decodeEnvelope(toDecodable(data));
@@ -2765,8 +3041,16 @@ function handleConnection(ws, principal, deps) {
2765
3041
  ws.close(1002, "deviceId does not match authenticated token");
2766
3042
  return;
2767
3043
  }
3044
+ if (payload.cursor !== void 0) {
3045
+ try {
3046
+ deps.hub.assertReplayAvailable(deviceId, payload.cursor, 1);
3047
+ } catch {
3048
+ ws.close(1008, "cursor_too_old");
3049
+ return;
3050
+ }
3051
+ }
2768
3052
  helloReceived = true;
2769
- deps.hub.registerConnection(
3053
+ epoch = deps.hub.registerConnection(
2770
3054
  deviceId,
2771
3055
  ws,
2772
3056
  payload.runtimes,
@@ -2774,12 +3058,19 @@ function handleConnection(ws, principal, deps) {
2774
3058
  payload.configuredToolsets,
2775
3059
  payload.clientVersion
2776
3060
  );
3061
+ clearHelloDeadline();
3062
+ releasePendingHello();
2777
3063
  deps.hub.sendConnAck(deviceId, SUPPORTED_CAPABILITIES);
2778
3064
  if (payload.cursor !== void 0) {
2779
3065
  deps.hub.redeliverAfterReconnect(deviceId, payload.cursor);
2780
3066
  }
2781
3067
  heartbeat = startHeartbeat(ws, { intervalMs: deps.heartbeatIntervalMs });
2782
3068
  ws.on("message", (msgData) => {
3069
+ if (epoch === void 0 || !deps.hub.isCurrentConnection(deviceId, ws, epoch)) return;
3070
+ if (rawDataByteLength(msgData) > deps.maxPayloadBytes) {
3071
+ ws.close(1009, "payload_too_large");
3072
+ return;
3073
+ }
2783
3074
  let msg;
2784
3075
  try {
2785
3076
  msg = decodeEnvelope(toDecodable(msgData));
@@ -2787,12 +3078,16 @@ function handleConnection(ws, principal, deps) {
2787
3078
  console.warn(`[byok/server] dropping unparsable frame from device ${deviceId}:`, err);
2788
3079
  return;
2789
3080
  }
2790
- deps.hub.handleInbound(deviceId, msg);
3081
+ if (deps.hub.isCurrentConnection(deviceId, ws, epoch)) {
3082
+ deps.hub.handleInbound(deviceId, msg);
3083
+ }
2791
3084
  });
2792
3085
  });
2793
3086
  ws.on("close", () => {
3087
+ clearHelloDeadline();
3088
+ releasePendingHello();
2794
3089
  heartbeat?.stop();
2795
- if (helloReceived) {
3090
+ if (helloReceived && epoch !== void 0) {
2796
3091
  deps.hub.handleDisconnect(deviceId, ws);
2797
3092
  }
2798
3093
  });
@@ -3133,6 +3428,7 @@ CREATE TABLE IF NOT EXISTS meta (
3133
3428
  );
3134
3429
  CREATE TABLE IF NOT EXISTS blobs (
3135
3430
  blob_id TEXT PRIMARY KEY,
3431
+ tenant_id TEXT NOT NULL,
3136
3432
  size INTEGER NOT NULL,
3137
3433
  content_type TEXT NOT NULL,
3138
3434
  content_hash TEXT NOT NULL,
@@ -3149,6 +3445,12 @@ function toBuffer(value) {
3149
3445
  const view = value;
3150
3446
  return Buffer.from(view.buffer, view.byteOffset, view.byteLength);
3151
3447
  }
3448
+ function ensureTenantOwnershipColumn(db) {
3449
+ const columns = db.prepare("PRAGMA table_info(blobs)").all();
3450
+ if (!columns.some((column) => column.name === "tenant_id")) {
3451
+ db.exec("ALTER TABLE blobs ADD COLUMN tenant_id TEXT");
3452
+ }
3453
+ }
3152
3454
  function loadOrCreateSigningSecret(db, generateCandidate = () => randomBytes(32)) {
3153
3455
  const selectStmt = db.prepare("SELECT value FROM meta WHERE key = ?");
3154
3456
  const existing = selectStmt.get(SIGNING_SECRET_META_KEY);
@@ -3177,17 +3479,18 @@ var SqliteBlobStore = class {
3177
3479
  this.db = openSqliteDatabase(opts.path);
3178
3480
  try {
3179
3481
  this.db.exec(SCHEMA2);
3482
+ ensureTenantOwnershipColumn(this.db);
3180
3483
  secureSqliteFilePermissions(opts.path);
3181
3484
  this.urlTtlMs = opts.urlTtlMs ?? DEFAULT_URL_TTL_MS2;
3182
3485
  this.secret = opts.signingKey ?? loadOrCreateSigningSecret(this.db);
3183
3486
  this.insertBlobStmt = this.db.prepare(
3184
- `INSERT INTO blobs (blob_id, size, content_type, content_hash, uploaded, data)
3185
- VALUES (?, ?, ?, ?, 0, NULL)`
3487
+ `INSERT INTO blobs (blob_id, tenant_id, size, content_type, content_hash, uploaded, data)
3488
+ VALUES (?, ?, ?, ?, ?, 0, NULL)`
3186
3489
  );
3187
3490
  this.selectBlobStmt = this.db.prepare(
3188
- "SELECT size, content_type, content_hash, uploaded, data FROM blobs WHERE blob_id = ?"
3491
+ "SELECT tenant_id, size, content_type, content_hash, uploaded, data FROM blobs WHERE blob_id = ?"
3189
3492
  );
3190
- this.selectUploadedStmt = this.db.prepare("SELECT uploaded FROM blobs WHERE blob_id = ?");
3493
+ this.selectUploadedStmt = this.db.prepare("SELECT tenant_id, uploaded FROM blobs WHERE blob_id = ?");
3191
3494
  this.writeContentStmt = this.db.prepare("UPDATE blobs SET data = ?, uploaded = 1 WHERE blob_id = ?");
3192
3495
  } catch (error) {
3193
3496
  closeSqliteDatabaseAfterInitializationFailure(
@@ -3197,26 +3500,30 @@ var SqliteBlobStore = class {
3197
3500
  );
3198
3501
  }
3199
3502
  }
3200
- async createUpload(input, requestedBlobId) {
3503
+ async createUpload(tenantId, input, requestedBlobId) {
3201
3504
  const blobId = requestedBlobId ?? `blob_${randomUUID()}`;
3202
3505
  const existing = this.selectBlobStmt.get(blobId);
3203
3506
  if (existing !== void 0) {
3204
- if (existing.size !== input.size || existing.content_type !== input.contentType || existing.content_hash !== input.contentHash) {
3507
+ if (existing.tenant_id !== tenantId || existing.size !== input.size || existing.content_type !== input.contentType || existing.content_hash !== input.contentHash) {
3205
3508
  throw new BlobDeclarationConflictError(blobId);
3206
3509
  }
3207
3510
  return { blobId, uploadUrl: this.signUrl(blobId, "put") };
3208
3511
  }
3209
- this.insertBlobStmt.run(blobId, input.size, input.contentType, input.contentHash);
3512
+ this.insertBlobStmt.run(blobId, tenantId, input.size, input.contentType, input.contentHash);
3210
3513
  return { blobId, uploadUrl: this.signUrl(blobId, "put") };
3211
3514
  }
3212
- async getDownloadUrl(blobId) {
3515
+ async getDownloadUrl(tenantId, blobId) {
3213
3516
  const row = this.selectUploadedStmt.get(blobId);
3214
- if (!row?.uploaded) return void 0;
3517
+ if (!row?.uploaded || row.tenant_id !== tenantId) return void 0;
3215
3518
  return this.signUrl(blobId, "get");
3216
3519
  }
3217
- async exists(blobId) {
3520
+ async exists(tenantId, blobId) {
3218
3521
  const row = this.selectUploadedStmt.get(blobId);
3219
- return Boolean(row?.uploaded);
3522
+ return row?.tenant_id === tenantId && Boolean(row.uploaded);
3523
+ }
3524
+ async getUploadReservation(blobId) {
3525
+ const row = this.selectBlobStmt.get(blobId);
3526
+ return row === void 0 ? void 0 : { size: row.size };
3220
3527
  }
3221
3528
  verifySignedUrl(blobId, action, sig, exp) {
3222
3529
  if (!Number.isFinite(exp) || Date.now() > exp) return false;
@@ -3265,6 +3572,9 @@ var SqliteBlobStore = class {
3265
3572
  var DEFAULT_MAX_BLOB_SIZE_BYTES = 100 * 1024 * 1024;
3266
3573
  var DEFAULT_LONG_POLL_HOLD_MS = 5e4;
3267
3574
  var DEFAULT_TASK_LEASE_MS = 30 * 6e4;
3575
+ var DEFAULT_WEBSOCKET_HELLO_TIMEOUT_MS = 5e3;
3576
+ var DEFAULT_MAX_PENDING_WEBSOCKETS = 32;
3577
+ var DEFAULT_MAX_WEBSOCKET_PAYLOAD_BYTES = RESULT_DOCUMENT_MAX_BYTES + 64 * 1024;
3268
3578
  function createByokServer(opts) {
3269
3579
  const pairing = new PairingManager();
3270
3580
  const devices = new DeviceRegistry();
@@ -3274,6 +3584,18 @@ function createByokServer(opts) {
3274
3584
  const maxBlobSizeBytes = opts.maxBlobSizeBytes ?? DEFAULT_MAX_BLOB_SIZE_BYTES;
3275
3585
  const longPollHoldMs = opts.longPollHoldMs ?? DEFAULT_LONG_POLL_HOLD_MS;
3276
3586
  const taskLeaseMs = opts.taskLeaseMs ?? DEFAULT_TASK_LEASE_MS;
3587
+ const webSocketHelloTimeoutMs = positiveSafeInteger(
3588
+ opts.webSocketHelloTimeoutMs ?? DEFAULT_WEBSOCKET_HELLO_TIMEOUT_MS,
3589
+ "webSocketHelloTimeoutMs"
3590
+ );
3591
+ const maxPendingWebSockets = positiveSafeInteger(
3592
+ opts.maxPendingWebSockets ?? DEFAULT_MAX_PENDING_WEBSOCKETS,
3593
+ "maxPendingWebSockets"
3594
+ );
3595
+ const maxWebSocketPayloadBytes = positiveSafeInteger(
3596
+ opts.maxWebSocketPayloadBytes ?? DEFAULT_MAX_WEBSOCKET_PAYLOAD_BYTES,
3597
+ "maxWebSocketPayloadBytes"
3598
+ );
3277
3599
  const rateLimiter = new RateLimiter(opts.rateLimit);
3278
3600
  const taskStore = opts.taskStore ?? new InMemoryTaskStore();
3279
3601
  const hub = new ConnectionHub(taskStore, devices, taskLeaseMs, rateLimiter, opts.agentMessage);
@@ -3301,7 +3623,10 @@ function createByokServer(opts) {
3301
3623
  tokenSigner,
3302
3624
  hub,
3303
3625
  productId: opts.productId,
3304
- heartbeatIntervalMs: opts.heartbeatIntervalMs
3626
+ heartbeatIntervalMs: opts.heartbeatIntervalMs,
3627
+ helloTimeoutMs: webSocketHelloTimeoutMs,
3628
+ maxPendingWebSockets,
3629
+ maxPayloadBytes: maxWebSocketPayloadBytes
3305
3630
  });
3306
3631
  },
3307
3632
  pairing: {
@@ -3338,7 +3663,11 @@ function createByokServer(opts) {
3338
3663
  stats: () => hub.stats()
3339
3664
  };
3340
3665
  }
3666
+ function positiveSafeInteger(value, name) {
3667
+ if (!Number.isSafeInteger(value) || value <= 0) throw new TypeError(`${name} must be a positive safe integer`);
3668
+ return value;
3669
+ }
3341
3670
 
3342
- export { AgentHomeProjectionCompletionError, IllegalTaskTransitionError, InMemoryTaskStore, LocalDiskBlobStore, PairingCodeInvalidError, SqliteBlobStore, SqliteTaskStore, SqliteUnavailableError, StaleApprovalError, SteerRejectedError, createByokServer, createHmacTokenSigner };
3671
+ export { AgentHomeProjectionCompletionError, IllegalTaskTransitionError, InMemoryTaskStore, LocalDiskBlobStore, PairingAttemptConflictError, PairingCodeInvalidError, SqliteBlobStore, SqliteTaskStore, SqliteUnavailableError, StaleApprovalError, SteerRejectedError, createByokServer, createHmacTokenSigner };
3343
3672
  //# sourceMappingURL=index.js.map
3344
3673
  //# sourceMappingURL=index.js.map