@byok-sdk/cloud 0.11.0 → 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
@@ -182,10 +182,8 @@ function createAuthPlane(deps) {
182
182
  });
183
183
  },
184
184
  async redeemAndRegister(input) {
185
- const claims = await stores.pairingCodes.redeem(input.pairingCode);
186
- if (claims === void 0) return void 0;
187
- return stores.devices.register(claims.tenantId, {
188
- productId: claims.productId,
185
+ return stores.pairing.redeemAndRegister({
186
+ pairingCode: input.pairingCode,
189
187
  deviceId: `dev_${crypto2.randomUuid()}`,
190
188
  deviceName: input.deviceName,
191
189
  devicePublicKey: input.devicePublicKey,
@@ -202,11 +200,8 @@ function createAuthPlane(deps) {
202
200
  issueNonce(device) {
203
201
  return stores.nonces.issue(device.tenantId, device.deviceId);
204
202
  },
205
- validateNonce(device, nonce) {
206
- return stores.nonces.validate(device.tenantId, device.deviceId, nonce);
207
- },
208
- consumeNonce(device, nonce) {
209
- return stores.nonces.markUsed(device.tenantId, nonce);
203
+ consumeNonceIfValid(device, nonce) {
204
+ return stores.nonces.consumeIfValid(device.tenantId, device.deviceId, nonce);
210
205
  },
211
206
  verifySignature(device, nonce, signature) {
212
207
  return verifyNonceSignature(crypto2, device.devicePublicKey, nonce, signature);
@@ -541,6 +536,7 @@ function tenantStoresFor(principal, root) {
541
536
  mailbox: {
542
537
  append: (input) => core.mailbox.append(tenant, input),
543
538
  readAfter: (query) => core.mailbox.readAfter(tenant, query),
539
+ recordDelivery: (input) => core.mailbox.recordDelivery(tenant, input),
544
540
  advanceCursor: (input) => core.mailbox.advanceCursor(tenant, input),
545
541
  readCursor: (deviceId) => core.mailbox.readCursor(tenant, deviceId)
546
542
  },
@@ -578,6 +574,9 @@ function tenantStoresFor(principal, root) {
578
574
  tasks: {
579
575
  open: (input) => cloud.tasks.open(tenant, input),
580
576
  reserveAgentOffer: (input) => cloud.tasks.reserveAgentOffer(tenant, input),
577
+ reserveAgentMessage: (input) => cloud.tasks.reserveAgentMessage(tenant, input),
578
+ readAgentMessage: (input) => cloud.tasks.readAgentMessage(tenant, input),
579
+ finalizeAgentMessage: (input) => cloud.tasks.finalizeAgentMessage(tenant, input),
581
580
  get: (taskId) => cloud.tasks.get(tenant, taskId),
582
581
  getMany: (taskIds) => cloud.tasks.getMany(tenant, taskIds),
583
582
  claim: (input) => cloud.tasks.claim(tenant, input),
@@ -627,6 +626,7 @@ async function readBoundedJsonBody(c, maximum) {
627
626
  if (declaredLength !== void 0) {
628
627
  const parsedLength = parseContentLength(declaredLength);
629
628
  if (parsedLength !== void 0 && parsedLength > BigInt(maximum)) {
629
+ cancelBody(c.req.raw.body);
630
630
  return { body: void 0, tooLarge: true };
631
631
  }
632
632
  }
@@ -640,10 +640,7 @@ async function readBoundedJsonBody(c, maximum) {
640
640
  const { done, value } = await reader.read();
641
641
  if (done) break;
642
642
  if (value.byteLength > maximum - total) {
643
- try {
644
- await reader.cancel();
645
- } catch {
646
- }
643
+ cancelBody(reader);
647
644
  return { body: void 0, tooLarge: true };
648
645
  }
649
646
  if (value.byteLength === 0) continue;
@@ -663,6 +660,14 @@ async function readBoundedJsonBody(c, maximum) {
663
660
  }
664
661
  return { body: parseJsonBytes(bytes), tooLarge: false };
665
662
  }
663
+ function cancelBody(body) {
664
+ if (body === null) return;
665
+ try {
666
+ void Promise.resolve(body.cancel()).catch(() => {
667
+ });
668
+ } catch {
669
+ }
670
+ }
666
671
  function parseContentLength(value) {
667
672
  const normalized = value.trim();
668
673
  if (!/^\d+$/u.test(normalized)) return void 0;
@@ -781,11 +786,43 @@ function blobUploadContentHandler(deps) {
781
786
  if (!query.success || !await deps.contentProxy.verifySignedUrl(blobId, "put", query.data.sig, query.data.exp)) {
782
787
  return c.json({ error: "invalid or expired signature" }, 401);
783
788
  }
784
- const result = await deps.contentProxy.writeContent(blobId, new Uint8Array(await c.req.arrayBuffer()));
789
+ const expectedBytes = await deps.contentProxy.expectedUploadBytes(blobId);
790
+ if (expectedBytes === void 0) return c.json({ error: "blob not found" }, 404);
791
+ const absoluteCeiling = BigInt(deps.maxUploadBytes);
792
+ if (expectedBytes > absoluteCeiling) return c.json({ error: "upload exceeds size limit" }, 413);
793
+ const contentLength = c.req.header("content-length");
794
+ if (contentLength !== void 0 && /^\d+$/u.test(contentLength) && BigInt(contentLength) > expectedBytes) {
795
+ return c.json({ error: "upload exceeds declared size" }, 413);
796
+ }
797
+ const body = await readBoundedBody(c.req.raw.body, Number(expectedBytes));
798
+ if (body.tooLarge) return c.json({ error: "upload exceeds declared size" }, 413);
799
+ const result = await deps.contentProxy.writeContent(blobId, body.data);
785
800
  if (!result.ok) return c.json({ error: result.reason }, 422);
786
801
  return c.body(null, 204);
787
802
  };
788
803
  }
804
+ async function readBoundedBody(body, ceiling) {
805
+ if (body === null) return { tooLarge: false, data: new Uint8Array() };
806
+ const reader = body.getReader();
807
+ const data = new Uint8Array(ceiling);
808
+ let offset = 0;
809
+ try {
810
+ for (; ; ) {
811
+ const next = await reader.read();
812
+ if (next.done) break;
813
+ const chunk = next.value;
814
+ if (chunk.byteLength > ceiling - offset) {
815
+ void reader.cancel().catch(() => void 0);
816
+ return { tooLarge: true };
817
+ }
818
+ data.set(chunk, offset);
819
+ offset += chunk.byteLength;
820
+ }
821
+ } finally {
822
+ reader.releaseLock();
823
+ }
824
+ return { tooLarge: false, data: data.subarray(0, offset) };
825
+ }
789
826
  function blobDownloadContentHandler(deps) {
790
827
  return async (c) => {
791
828
  const blobId = c.req.param("id") ?? "";
@@ -830,9 +867,12 @@ function renderStorageError(c, error) {
830
867
  function capabilitiesHandler(deps) {
831
868
  return async (c) => c.json(deps.declaration, 200);
832
869
  }
870
+ var AUTH_JSON_BODY_MAX_BYTES = 16 * 1024;
833
871
  function pairHandler(deps) {
834
872
  return async (c) => {
835
- const parsed = PairRequestSchema.safeParse(await readJsonBody(c));
873
+ const body = await readBoundedJsonBody(c, AUTH_JSON_BODY_MAX_BYTES);
874
+ if (body.tooLarge) return c.json({ error: "request body too large" }, 413);
875
+ const parsed = PairRequestSchema.safeParse(body.body);
836
876
  if (!parsed.success) {
837
877
  return c.json(
838
878
  {
@@ -855,7 +895,9 @@ function pairHandler(deps) {
855
895
  }
856
896
  function challengeHandler(deps) {
857
897
  return async (c) => {
858
- const parsed = ChallengeRequestSchema.safeParse(await readJsonBody(c));
898
+ const body = await readBoundedJsonBody(c, AUTH_JSON_BODY_MAX_BYTES);
899
+ if (body.tooLarge) return c.json({ error: "request body too large" }, 413);
900
+ const parsed = ChallengeRequestSchema.safeParse(body.body);
859
901
  if (!parsed.success) return c.json({ error: "deviceId is required" }, 400);
860
902
  const device = await deps.auth.resolveDevice(parsed.data.deviceId);
861
903
  if (device === void 0) return c.json({ error: "unknown or revoked device" }, 401);
@@ -865,18 +907,19 @@ function challengeHandler(deps) {
865
907
  }
866
908
  function tokenHandler(deps) {
867
909
  return async (c) => {
868
- const parsed = TokenRequestSchema.safeParse(await readJsonBody(c));
910
+ const body = await readBoundedJsonBody(c, AUTH_JSON_BODY_MAX_BYTES);
911
+ if (body.tooLarge) return c.json({ error: "request body too large" }, 413);
912
+ const parsed = TokenRequestSchema.safeParse(body.body);
869
913
  if (!parsed.success) return c.json({ error: "deviceId, nonce, and signature are required" }, 400);
870
914
  const { deviceId, nonce, signature } = parsed.data;
871
915
  const device = await deps.auth.resolveDevice(deviceId);
872
916
  if (device === void 0) return c.json({ error: "unknown or revoked device" }, 401);
873
- if (!await deps.auth.validateNonce(device, nonce)) {
874
- return c.json({ error: "invalid, expired, or already-used nonce" }, 401);
875
- }
876
917
  if (!await deps.auth.verifySignature(device, nonce, signature)) {
877
918
  return c.json({ error: "invalid signature" }, 401);
878
919
  }
879
- await deps.auth.consumeNonce(device, nonce);
920
+ if (!await deps.auth.consumeNonceIfValid(device, nonce)) {
921
+ return c.json({ error: "invalid, expired, or already-used nonce" }, 401);
922
+ }
880
923
  const { accessToken, expiresAt } = await deps.auth.mintAccessToken(device);
881
924
  const response = { accessToken, expiresAt };
882
925
  return c.json(response, 200);
@@ -905,12 +948,21 @@ function eventsHandler(deps) {
905
948
  let cursor = 0;
906
949
  if (cursorRaw !== void 0) {
907
950
  const parsedCursor = Number(cursorRaw);
908
- if (!Number.isInteger(parsedCursor) || parsedCursor < 0) return c.json({ error: "invalid cursor" }, 400);
951
+ if (!Number.isSafeInteger(parsedCursor) || parsedCursor < 0) {
952
+ return c.json({ error: "invalid cursor" }, 400);
953
+ }
909
954
  cursor = parsedCursor;
910
955
  }
911
956
  const acked = await stores.mailbox.readCursor(device.deviceId);
912
957
  if (cursor > acked.ackedSeq) {
913
- await stores.mailbox.advanceCursor({ deviceId: device.deviceId, ackedSeq: cursor });
958
+ try {
959
+ await stores.mailbox.advanceCursor({ deviceId: device.deviceId, ackedSeq: cursor });
960
+ } catch (caught) {
961
+ if (isCoreConflictError(caught, "mailbox_cursor_ahead_of_delivery")) {
962
+ return c.json({ error: "cursor exceeds delivered watermark" }, 409);
963
+ }
964
+ throw caught;
965
+ }
914
966
  }
915
967
  const attempts = Math.max(1, Math.ceil(deps.longPollHoldMs / deps.longPollIntervalMs));
916
968
  for (let attempt = 0; attempt < attempts; attempt += 1) {
@@ -945,6 +997,10 @@ function eventsHandler(deps) {
945
997
  cursor: page.nextSeq,
946
998
  capabilities: CLOUD_PROTOCOL_CAPABILITIES
947
999
  };
1000
+ await stores.mailbox.recordDelivery({
1001
+ deviceId: device.deviceId,
1002
+ deliveredSeq: response2.cursor
1003
+ });
948
1004
  return c.json(response2, 200);
949
1005
  }
950
1006
  if (attempt < attempts - 1) await sleep(deps.longPollIntervalMs);
@@ -1042,11 +1098,11 @@ async function projectTerminalToReview(board, taskId) {
1042
1098
  }
1043
1099
 
1044
1100
  // src/inbound.ts
1045
- async function handleAgentMessagePublish(stores, deviceId, taskId, payload, consume) {
1101
+ async function handleAgentMessagePublish(stores, deviceId, taskId, payload, consume, alreadyDeduplicated) {
1046
1102
  const device = await stores.devices.get(deviceId);
1047
1103
  const attempt = await stores.tasks.get(taskId);
1048
1104
  const binding = await stores.receipts.get(`agent-message-offer:${deviceId}:${taskId}`);
1049
- if (device === void 0 || device.revoked || !hasCapabilities(device.capabilities, [AGENT_MESSAGE_EGRESS_CAPABILITY]) || attempt === void 0 || attempt.deviceId !== deviceId || binding === void 0 || !sameAgentRef(attempt.agentRef, payload.agentRef)) return { outcome: "rejected" };
1105
+ if (device === void 0 || device.revoked || !hasCapabilities(device.capabilities, [AGENT_MESSAGE_EGRESS_CAPABILITY]) || attempt === void 0 || attempt.deviceId !== deviceId || binding === void 0 || attempt.agentRef === void 0 || !sameAgentRef(attempt.agentRef, payload.agentRef)) return { outcome: "rejected" };
1050
1106
  let frozen;
1051
1107
  try {
1052
1108
  frozen = JSON.parse(binding.body);
@@ -1054,19 +1110,41 @@ async function handleAgentMessagePublish(stores, deviceId, taskId, payload, cons
1054
1110
  return { outcome: "rejected" };
1055
1111
  }
1056
1112
  if (!sameAgentRef(frozen.agentRef, payload.agentRef) || frozen.context === void 0 || frozen.requirement?.contract !== payload.contract || frozen.sessionRef !== void 0 && frozen.sessionRef !== payload.sessionRef || frozen.requirement.contentType !== payload.contentType || typeof frozen.requirement.maxBytes !== "number" || payload.byteCount > frozen.requirement.maxBytes) return { outcome: "rejected" };
1057
- const taskMessageKey = `agent-message-task:${deviceId}:${taskId}`;
1058
- const taskMessageBody = JSON.stringify(payload);
1059
- const lockedTaskMessage = await stores.receipts.record({ key: taskMessageKey, body: taskMessageBody });
1060
- if (!lockedTaskMessage.created && lockedTaskMessage.receipt.body !== taskMessageBody) return { outcome: "rejected" };
1061
- const receiptKey2 = `agent-message:${deviceId}:${payload.messageId}`;
1062
- const previous = await stores.receipts.get(receiptKey2);
1063
- if (previous !== void 0) {
1064
- const decoded = JSON.parse(previous.body);
1065
- if (JSON.stringify(decoded.payload) !== JSON.stringify(payload)) return { outcome: "rejected" };
1066
- return { outcome: "duplicate", disposition: decoded.disposition };
1067
- }
1068
- const decision = consume === void 0 ? { outcome: "held", reasonCode: "consumer_unavailable" } : await consume({ tenant: stores.tenant, deviceId, taskId, context: frozen.context, payload });
1069
- const disposition = {
1113
+ const priorDisposition = await readAgentMessageDisposition(stores, deviceId, taskId, payload);
1114
+ if (priorDisposition !== void 0) return { outcome: "duplicate", disposition: priorDisposition };
1115
+ if (alreadyDeduplicated) return { outcome: "rejected" };
1116
+ const payloadBody = JSON.stringify(payload);
1117
+ const reservation = await stores.tasks.reserveAgentMessage({
1118
+ taskId,
1119
+ deviceId,
1120
+ messageId: payload.messageId,
1121
+ payloadBody
1122
+ });
1123
+ if (reservation === "pending") return { outcome: "rejected" };
1124
+ if (reservation !== "reserved") return { outcome: "rejected" };
1125
+ let decision;
1126
+ if (consume === void 0) {
1127
+ decision = { outcome: "held", reasonCode: "consumer_unavailable" };
1128
+ } else {
1129
+ try {
1130
+ decision = await consume({ tenant: stores.tenant, deviceId, taskId, context: frozen.context, payload });
1131
+ } catch {
1132
+ decision = { outcome: "held", reasonCode: "consumer_failed" };
1133
+ }
1134
+ }
1135
+ const disposition = agentMessageDisposition(payload, decision);
1136
+ const admission = await stores.tasks.finalizeAgentMessage({
1137
+ taskId,
1138
+ deviceId,
1139
+ messageId: payload.messageId,
1140
+ payloadBody,
1141
+ terminalBody: JSON.stringify({ payload, disposition })
1142
+ });
1143
+ const terminal = admission?.terminalBody === void 0 ? void 0 : parseAgentMessageDisposition(payload, admission.terminalBody);
1144
+ return terminal === void 0 ? { outcome: "rejected" } : { outcome: "accepted", disposition: terminal };
1145
+ }
1146
+ function agentMessageDisposition(payload, decision) {
1147
+ return {
1070
1148
  agentRef: payload.agentRef,
1071
1149
  sessionRef: payload.sessionRef,
1072
1150
  contract: payload.contract,
@@ -1077,9 +1155,23 @@ async function handleAgentMessagePublish(stores, deviceId, taskId, payload, cons
1077
1155
  receiptId: crypto.randomUUID(),
1078
1156
  ...decision.reasonCode === void 0 ? {} : { reasonCode: decision.reasonCode }
1079
1157
  };
1080
- const recorded = await stores.receipts.record({ key: receiptKey2, body: JSON.stringify({ payload, disposition }) });
1081
- if (!recorded.created && recorded.receipt.body !== JSON.stringify({ payload, disposition })) return { outcome: "rejected" };
1082
- return { outcome: "accepted", disposition };
1158
+ }
1159
+ async function readAgentMessageDisposition(stores, deviceId, taskId, payload) {
1160
+ const admission = await stores.tasks.readAgentMessage({
1161
+ taskId,
1162
+ deviceId,
1163
+ messageId: payload.messageId,
1164
+ payloadBody: JSON.stringify(payload)
1165
+ });
1166
+ return admission?.terminalBody === void 0 ? void 0 : parseAgentMessageDisposition(payload, admission.terminalBody);
1167
+ }
1168
+ function parseAgentMessageDisposition(payload, terminalBody) {
1169
+ try {
1170
+ const decoded = JSON.parse(terminalBody);
1171
+ return decoded.disposition !== void 0 && JSON.stringify(decoded.payload) === JSON.stringify(payload) ? decoded.disposition : void 0;
1172
+ } catch {
1173
+ return void 0;
1174
+ }
1083
1175
  }
1084
1176
  function terminalReceiptKey(taskId) {
1085
1177
  return `task:${taskId}:terminal`;
@@ -1136,7 +1228,7 @@ function inboundAgentRef(envelope) {
1136
1228
  function agentRefEchoRequired(envelope) {
1137
1229
  return envelope.type === "task.claim" || envelope.type === "task.decline" || envelope.type === "task.complete" || envelope.type === "task.fail" || envelope.type === "task.cancelled";
1138
1230
  }
1139
- async function handleInboundEnvelope(stores, deviceId, envelope, activityBounds = DEFAULT_ACTIVITY_BOUNDS) {
1231
+ async function handleInboundEnvelope(stores, deviceId, envelope, activityBounds = DEFAULT_ACTIVITY_BOUNDS, agentMessageConsume) {
1140
1232
  if (!await stores.rateLimiter.consume(deviceId)) return "rate_limited";
1141
1233
  if (envelope.type === "conn.hello") {
1142
1234
  if (envelope.payload.deviceId !== deviceId) return "rejected";
@@ -1149,6 +1241,17 @@ async function handleInboundEnvelope(stores, deviceId, envelope, activityBounds
1149
1241
  return await stores.devices.recordCapabilities({ capabilities: envelope.payload.capabilities }) === void 0 ? "rejected" : "accepted";
1150
1242
  }
1151
1243
  if (!DAEMON_TO_SERVER_TYPES.includes(envelope.type)) return "rejected";
1244
+ if (envelope.type === "agent.message.publish") {
1245
+ const alreadyDeduplicated = await stores.dedup.checkAndRecord(deviceId, envelope.id);
1246
+ return (await handleAgentMessagePublish(
1247
+ stores,
1248
+ deviceId,
1249
+ envelope.task_id,
1250
+ envelope.payload,
1251
+ agentMessageConsume,
1252
+ alreadyDeduplicated
1253
+ )).outcome;
1254
+ }
1152
1255
  if (envelope.type === "agent.egress.reliable") {
1153
1256
  const device = await stores.devices.get(deviceId);
1154
1257
  if (device === void 0 || device.revoked || !hasCapabilities(device.capabilities, [AGENT_EGRESS_POLICY_CAPABILITY, AGENT_EGRESS_RELIABLE_ACK_CAPABILITY])) {
@@ -1323,32 +1426,32 @@ async function recordTerminal(stores, taskId, envelope, status) {
1323
1426
  }
1324
1427
 
1325
1428
  // src/handlers/messages.ts
1429
+ var MESSAGES_JSON_BODY_MAX_BYTES = 2 * 1024 * 1024;
1326
1430
  function messagesHandler(deps) {
1327
1431
  return async (c) => {
1328
1432
  const authenticated = await authenticateDevice(c, deps);
1329
1433
  if (authenticated === void 0) return c.json({ error: "unauthorized" }, 401);
1330
1434
  const { device, stores } = authenticated;
1331
- const parsed = MessagesSendRequestSchema.safeParse(await readJsonBody(c));
1435
+ const body = await readBoundedJsonBody(c, MESSAGES_JSON_BODY_MAX_BYTES);
1436
+ if (body.tooLarge) return c.json({ error: "request body too large" }, 413);
1437
+ const parsed = MessagesSendRequestSchema.safeParse(body.body);
1332
1438
  if (!parsed.success) return c.json({ error: "messages must be an array of envelopes" }, 400);
1333
1439
  let accepted = 0;
1334
1440
  let rejected = 0;
1335
1441
  for (const envelope of parsed.data.messages) {
1336
- const messageResult = envelope.type === "agent.message.publish" ? await handleAgentMessagePublish(
1337
- stores,
1338
- device.deviceId,
1339
- envelope.task_id,
1340
- envelope.payload,
1341
- deps.agentMessage?.consume
1342
- ) : void 0;
1343
- const outcome = messageResult?.outcome ?? await handleInboundEnvelope(
1442
+ const outcome = await handleInboundEnvelope(
1344
1443
  stores,
1345
1444
  device.deviceId,
1346
1445
  envelope,
1347
- deps.activityBounds
1446
+ deps.activityBounds,
1447
+ envelope.type === "agent.message.publish" ? deps.agentMessage?.consume : void 0
1348
1448
  );
1349
1449
  if (outcome === "rate_limited") return c.json({ error: "rate limit exceeded" }, 429);
1350
- if (envelope.type === "agent.message.publish" && messageResult?.disposition !== void 0) {
1351
- await deps.appendAgentMessageDisposition(stores, device.deviceId, envelope.task_id, messageResult.disposition);
1450
+ if (envelope.type === "agent.message.publish") {
1451
+ const disposition = await readAgentMessageDisposition(stores, device.deviceId, envelope.task_id, envelope.payload);
1452
+ if (disposition !== void 0) {
1453
+ await deps.appendAgentMessageDisposition(stores, device.deviceId, envelope.task_id, disposition);
1454
+ }
1352
1455
  }
1353
1456
  if (envelope.type === "agent.egress.reliable" && (outcome === "accepted" || outcome === "duplicate")) {
1354
1457
  const record = await stores.egress.get(device.deviceId, envelope.payload.eventId);
@@ -1961,7 +2064,7 @@ function truthGetHandler(deps) {
1961
2064
  }
1962
2065
  function truthPutHandler(deps) {
1963
2066
  return async (c) => {
1964
- const body = await readBoundedBody(c, deps.maxRequestBytes);
2067
+ const body = await readBoundedBody2(c, deps.maxRequestBytes);
1965
2068
  if (body === "too_large") return c.json({ error: "truth request body too large" }, 413);
1966
2069
  if (body === void 0) return c.json({ error: "invalid truth request body" }, 400);
1967
2070
  const authenticated = await authenticateTruthRequest(c, deps, {
@@ -2093,7 +2196,7 @@ function decodeProofHeader(value) {
2093
2196
  return void 0;
2094
2197
  }
2095
2198
  }
2096
- async function readBoundedBody(c, maximum) {
2199
+ async function readBoundedBody2(c, maximum) {
2097
2200
  const length = c.req.header("content-length");
2098
2201
  if (length !== void 0) {
2099
2202
  const parsed = Number(length);
@@ -2522,7 +2625,10 @@ function createByokCloud(options) {
2522
2625
  }
2523
2626
  const contentProxy = options.blobContentProxy;
2524
2627
  if (contentProxy !== void 0 && declares(declaration, CLOUD_CAPABILITIES.blobsContentProxy)) {
2525
- const contentDeps = { contentProxy };
2628
+ const contentDeps = {
2629
+ contentProxy,
2630
+ maxUploadBytes: options.maxBlobSizeBytes ?? DEFAULT_MAX_BLOB_SIZE_BYTES
2631
+ };
2526
2632
  registry.register(
2527
2633
  { method: "PUT", path: BYOK_BLOB_CONTENT_ROUTE, class: "presigned" },
2528
2634
  blobUploadContentHandler(contentDeps)
@@ -3256,6 +3362,9 @@ var InMemoryBlobContentProxy = class {
3256
3362
  const expected = await this.#registry.computeSig(blobId, action, exp);
3257
3363
  return this.#registry.crypto.timingSafeEqual(expected, sig);
3258
3364
  }
3365
+ async expectedUploadBytes(blobId) {
3366
+ return this.#registry.blobs.get(blobId)?.byteSize;
3367
+ }
3259
3368
  async writeContent(blobId, data) {
3260
3369
  const record = this.#registry.blobs.get(blobId);
3261
3370
  if (record === void 0) return { ok: false, reason: "unknown blobId" };
@@ -3469,27 +3578,38 @@ var InMemoryNonceStore = class {
3469
3578
  });
3470
3579
  return nonce;
3471
3580
  }
3472
- async validate(tenant, deviceId, nonce) {
3581
+ async consumeIfValid(tenant, deviceId, nonce) {
3473
3582
  const record = this.#nonces.get(nonce);
3474
3583
  if (record === void 0) return false;
3475
3584
  if (record.used) return false;
3476
3585
  if (record.owner !== tenantKey(tenant, deviceId)) return false;
3477
- return this.#clock.now().getTime() <= record.expiresAtMs;
3478
- }
3479
- async markUsed(tenant, nonce) {
3480
- const record = this.#nonces.get(nonce);
3481
- if (record === void 0) return;
3482
- if (!record.owner.startsWith(tenantKey(tenant, ""))) return;
3586
+ if (this.#clock.now().getTime() > record.expiresAtMs) return false;
3483
3587
  record.used = true;
3588
+ return true;
3484
3589
  }
3485
3590
  };
3486
3591
 
3487
3592
  // src/stores/in-memory/pairing-codes.ts
3593
+ function bindingOf(input) {
3594
+ return {
3595
+ deviceName: input.deviceName,
3596
+ devicePublicKey: input.devicePublicKey,
3597
+ proofKeyId: input.proofKeyId,
3598
+ proofKeyEpoch: input.proofKeyEpoch,
3599
+ ...input.machineId === void 0 ? {} : { machineId: input.machineId }
3600
+ };
3601
+ }
3602
+ function sameBinding(left, right) {
3603
+ return left.deviceName === right.deviceName && left.devicePublicKey === right.devicePublicKey && left.proofKeyId === right.proofKeyId && left.proofKeyEpoch === right.proofKeyEpoch && left.machineId === right.machineId;
3604
+ }
3488
3605
  var InMemoryPairingCodeStore = class {
3489
3606
  #codes = /* @__PURE__ */ new Map();
3607
+ #enrollmentTails = /* @__PURE__ */ new Map();
3490
3608
  #clock;
3491
- constructor(clock) {
3609
+ #devices;
3610
+ constructor(clock, devices) {
3492
3611
  this.#clock = clock;
3612
+ this.#devices = devices;
3493
3613
  }
3494
3614
  async issue(tenant, input) {
3495
3615
  this.#codes.set(input.code, {
@@ -3499,13 +3619,42 @@ var InMemoryPairingCodeStore = class {
3499
3619
  });
3500
3620
  return { code: input.code, expiresAt: input.expiresAt };
3501
3621
  }
3502
- async redeem(code) {
3503
- const record = this.#codes.get(code);
3504
- if (record === void 0) return void 0;
3505
- if (record.used) return void 0;
3506
- if (this.#clock.now().getTime() > record.expiresAtMs) return void 0;
3507
- record.used = true;
3508
- return record.claims;
3622
+ async redeemAndRegister(input) {
3623
+ const previous = this.#enrollmentTails.get(input.pairingCode) ?? Promise.resolve();
3624
+ let release;
3625
+ const current = new Promise((resolve) => {
3626
+ release = resolve;
3627
+ });
3628
+ this.#enrollmentTails.set(input.pairingCode, current);
3629
+ await previous;
3630
+ try {
3631
+ const record = this.#codes.get(input.pairingCode);
3632
+ if (record === void 0) return void 0;
3633
+ if (record.used) {
3634
+ const completion = record.completion;
3635
+ if (completion === void 0 || !sameBinding(completion.binding, bindingOf(input))) return void 0;
3636
+ const active = await this.#devices.get(completion.device.tenantId, completion.device.deviceId);
3637
+ return active === void 0 ? void 0 : completion.device;
3638
+ }
3639
+ if (this.#clock.now().getTime() > record.expiresAtMs) return void 0;
3640
+ const device = await this.#devices.register(record.claims.tenantId, {
3641
+ productId: record.claims.productId,
3642
+ deviceId: input.deviceId,
3643
+ deviceName: input.deviceName,
3644
+ devicePublicKey: input.devicePublicKey,
3645
+ proofKeyId: input.proofKeyId,
3646
+ proofKeyEpoch: input.proofKeyEpoch,
3647
+ ...input.machineId === void 0 ? {} : { machineId: input.machineId }
3648
+ });
3649
+ record.used = true;
3650
+ record.completion = { device, binding: bindingOf(input) };
3651
+ return device;
3652
+ } finally {
3653
+ release();
3654
+ if (this.#enrollmentTails.get(input.pairingCode) === current) {
3655
+ this.#enrollmentTails.delete(input.pairingCode);
3656
+ }
3657
+ }
3509
3658
  }
3510
3659
  };
3511
3660
  var InMemoryRequestReceiptStore = class {
@@ -3553,6 +3702,36 @@ var InMemoryProofRequestReceiptStore = class {
3553
3702
  return this.#receipts.get(tenantKey(tenant, deviceId, requestId));
3554
3703
  }
3555
3704
  };
3705
+
3706
+ // src/stores/ports.ts
3707
+ var TASK_ATTEMPT_STATUSES = [
3708
+ "offered",
3709
+ "claimed",
3710
+ "running",
3711
+ "cancel_requested",
3712
+ "complete",
3713
+ "failed",
3714
+ "cancelled"
3715
+ ];
3716
+ var BLOB_READ_ERROR_CODES = ["blob_upstream_unavailable", "blob_upstream_stream_interrupted"];
3717
+ var CLOUD_STORE_NAMES = [
3718
+ "activity",
3719
+ "approvals",
3720
+ "devices",
3721
+ "pairingCodes",
3722
+ "pairing",
3723
+ "nonces",
3724
+ "dedup",
3725
+ "tasks",
3726
+ "cancellations",
3727
+ "receipts",
3728
+ "egress",
3729
+ "proofReceipts",
3730
+ "blobs",
3731
+ "rateLimiter"
3732
+ ];
3733
+
3734
+ // src/stores/in-memory/task-attempts.ts
3556
3735
  function sameAgentRef3(left, right) {
3557
3736
  return left?.agentId === right?.agentId && left?.profileRevision === right?.profileRevision;
3558
3737
  }
@@ -3595,6 +3774,40 @@ var InMemoryTaskAttemptStore = class {
3595
3774
  return { attempt, created: true };
3596
3775
  });
3597
3776
  }
3777
+ async reserveAgentMessage(tenant, input) {
3778
+ const key = tenantKey(tenant, input.taskId);
3779
+ return this.#state.mutate(key, () => {
3780
+ const attempt = this.#state.attempts.get(key);
3781
+ if (attempt === void 0 || attempt.deviceId !== input.deviceId || attempt.cancellation !== void 0 || !["offered", "claimed", "running"].includes(attempt.status)) return "rejected";
3782
+ const existing = this.#state.messageAdmissions.get(key);
3783
+ if (existing === void 0) {
3784
+ this.#state.messageAdmissions.set(key, {
3785
+ messageId: input.messageId,
3786
+ payloadBody: input.payloadBody
3787
+ });
3788
+ return "reserved";
3789
+ }
3790
+ return existing.messageId === input.messageId && existing.payloadBody === input.payloadBody ? "pending" : "rejected";
3791
+ });
3792
+ }
3793
+ async readAgentMessage(tenant, input) {
3794
+ const admission = this.#state.messageAdmissions.get(tenantKey(tenant, input.taskId));
3795
+ return admission !== void 0 && admission.messageId === input.messageId && admission.payloadBody === input.payloadBody ? admission : void 0;
3796
+ }
3797
+ async finalizeAgentMessage(tenant, input) {
3798
+ const key = tenantKey(tenant, input.taskId);
3799
+ return this.#state.mutate(key, () => this.#terminalize(key, input));
3800
+ }
3801
+ #terminalize(key, input) {
3802
+ const admission = this.#state.messageAdmissions.get(key);
3803
+ if (admission === void 0 || admission.messageId !== input.messageId || admission.payloadBody !== input.payloadBody) {
3804
+ return void 0;
3805
+ }
3806
+ if (admission.terminalBody !== void 0) return admission;
3807
+ const terminal = { ...admission, terminalBody: input.terminalBody };
3808
+ this.#state.messageAdmissions.set(key, terminal);
3809
+ return terminal;
3810
+ }
3598
3811
  async get(tenant, taskId) {
3599
3812
  return this.#state.attempts.get(tenantKey(tenant, taskId));
3600
3813
  }
@@ -3647,6 +3860,7 @@ var InMemoryTaskAttemptStore = class {
3647
3860
  };
3648
3861
  var InMemoryTaskAttemptState = class {
3649
3862
  attempts = /* @__PURE__ */ new Map();
3863
+ messageAdmissions = /* @__PURE__ */ new Map();
3650
3864
  #clock;
3651
3865
  #mutationTails = /* @__PURE__ */ new Map();
3652
3866
  constructor(clock) {
@@ -3866,12 +4080,17 @@ function createInMemoryCloudStores(clock, crypto2, objects, mailbox) {
3866
4080
  const blobs = createInMemoryBlobs(clock, crypto2, objects);
3867
4081
  const taskState = new InMemoryTaskAttemptState(clock);
3868
4082
  const tasks = new InMemoryTaskAttemptStore(clock, taskState);
4083
+ const devices = new InMemoryDeviceDirectory();
4084
+ const pairing = new InMemoryPairingCodeStore(clock, devices);
3869
4085
  return {
3870
4086
  stores: {
3871
4087
  activity: new InMemoryActivityStore(clock),
3872
4088
  approvals: new InMemoryApprovalTimelineStore(clock),
3873
- devices: new InMemoryDeviceDirectory(),
3874
- pairingCodes: new InMemoryPairingCodeStore(clock),
4089
+ devices,
4090
+ // One code/device authority, projected into issuance and consumption
4091
+ // ports so neither side exposes the other's methods.
4092
+ pairingCodes: { issue: pairing.issue.bind(pairing) },
4093
+ pairing: { redeemAndRegister: pairing.redeemAndRegister.bind(pairing) },
3875
4094
  nonces: new InMemoryNonceStore(clock, crypto2),
3876
4095
  dedup: new InMemoryInboundDedupStore(),
3877
4096
  tasks,
@@ -4283,42 +4502,26 @@ function parseSseFrame(frame) {
4283
4502
  return { type: "board", item: BoardFeedItemSchema.parse(JSON.parse(data.join("\n"))) };
4284
4503
  }
4285
4504
 
4286
- // src/stores/ports.ts
4287
- var TASK_ATTEMPT_STATUSES = [
4288
- "offered",
4289
- "claimed",
4290
- "running",
4291
- "cancel_requested",
4292
- "complete",
4293
- "failed",
4294
- "cancelled"
4295
- ];
4296
- var BLOB_READ_ERROR_CODES = ["blob_upstream_unavailable", "blob_upstream_stream_interrupted"];
4297
- var CLOUD_STORE_NAMES = [
4298
- "activity",
4299
- "approvals",
4300
- "devices",
4301
- "pairingCodes",
4302
- "nonces",
4303
- "dedup",
4304
- "tasks",
4305
- "cancellations",
4306
- "receipts",
4307
- "egress",
4308
- "proofReceipts",
4309
- "blobs",
4310
- "rateLimiter"
4311
- ];
4312
-
4313
4505
  // src/stores/ports-contract.ts
4314
4506
  var CLOUD_PORT_METHODS = {
4315
4507
  activity: ["append", "read"],
4316
4508
  approvals: ["append", "read"],
4317
4509
  devices: ["register", "get", "revoke", "list", "readiness", "recordCapabilities", "resolveByDeviceId"],
4318
- pairingCodes: ["issue", "redeem"],
4319
- nonces: ["issue", "validate", "markUsed"],
4510
+ pairingCodes: ["issue"],
4511
+ pairing: ["redeemAndRegister"],
4512
+ nonces: ["issue", "consumeIfValid"],
4320
4513
  dedup: ["checkAndRecord"],
4321
- tasks: ["open", "reserveAgentOffer", "get", "getMany", "claim", "recordStatus"],
4514
+ tasks: [
4515
+ "open",
4516
+ "reserveAgentOffer",
4517
+ "reserveAgentMessage",
4518
+ "readAgentMessage",
4519
+ "finalizeAgentMessage",
4520
+ "get",
4521
+ "getMany",
4522
+ "claim",
4523
+ "recordStatus"
4524
+ ],
4322
4525
  cancellations: ["request"],
4323
4526
  receipts: ["record", "get"],
4324
4527
  egress: ["record", "get"],
@@ -4334,6 +4537,7 @@ var CLOUD_PORT_INTERFACES = {
4334
4537
  approvals: "ApprovalTimelineStore",
4335
4538
  devices: "DeviceDirectory",
4336
4539
  pairingCodes: "PairingCodeStore",
4540
+ pairing: "PairingEnrollment",
4337
4541
  nonces: "NonceStore",
4338
4542
  dedup: "InboundDedupStore",
4339
4543
  tasks: "TaskAttemptStore",