@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/auth/plane.d.ts +3 -4
- package/dist/handlers/blobs.d.ts +2 -0
- package/dist/handlers/events.d.ts +3 -3
- package/dist/inbound.d.ts +4 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +308 -104
- package/dist/index.js.map +1 -1
- package/dist/stores/in-memory/blobs.d.ts +1 -0
- package/dist/stores/in-memory/nonces.d.ts +1 -2
- package/dist/stores/in-memory/pairing-codes.d.ts +9 -5
- package/dist/stores/in-memory/task-attempts.d.ts +21 -1
- package/dist/stores/ports.d.ts +73 -22
- package/dist/tenant-stores.d.ts +21 -1
- package/package.json +3 -3
package/dist/index.js
CHANGED
|
@@ -182,10 +182,8 @@ function createAuthPlane(deps) {
|
|
|
182
182
|
});
|
|
183
183
|
},
|
|
184
184
|
async redeemAndRegister(input) {
|
|
185
|
-
|
|
186
|
-
|
|
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
|
-
|
|
206
|
-
return stores.nonces.
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
const
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
}
|
|
1068
|
-
|
|
1069
|
-
|
|
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
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
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
|
|
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
|
|
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"
|
|
1351
|
-
await
|
|
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
|
|
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
|
|
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 = {
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
3503
|
-
const
|
|
3504
|
-
|
|
3505
|
-
|
|
3506
|
-
|
|
3507
|
-
|
|
3508
|
-
|
|
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
|
|
3874
|
-
|
|
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"
|
|
4319
|
-
|
|
4510
|
+
pairingCodes: ["issue"],
|
|
4511
|
+
pairing: ["redeemAndRegister"],
|
|
4512
|
+
nonces: ["issue", "consumeIfValid"],
|
|
4320
4513
|
dedup: ["checkAndRecord"],
|
|
4321
|
-
tasks: [
|
|
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",
|