@byok-sdk/cloud-dataplane 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 +343 -98
- package/dist/index.js.map +1 -1
- package/dist/runtime.js +338 -98
- package/dist/runtime.js.map +1 -1
- package/dist/sql/0016_mailbox_delivery_watermark.sql +20 -0
- package/dist/sql/0017_agent_message_admission.sql +23 -0
- package/dist/stores/core/mailbox.d.ts +5 -4
- package/dist/stores/devices.d.ts +10 -1
- package/dist/stores/nonces.d.ts +1 -2
- package/dist/stores/pairing-codes.d.ts +15 -8
- package/dist/stores/task-attempts.d.ts +20 -1
- package/dist/tenant-erasure.d.ts +2 -2
- package/package.json +4 -4
package/dist/runtime.js
CHANGED
|
@@ -822,6 +822,57 @@ async function rollback(client) {
|
|
|
822
822
|
} catch {
|
|
823
823
|
}
|
|
824
824
|
}
|
|
825
|
+
async function registerDeviceOnClient(client, tenant, input) {
|
|
826
|
+
if (input.machineId !== void 0) {
|
|
827
|
+
const superseded = await client.query(
|
|
828
|
+
`DELETE FROM device
|
|
829
|
+
WHERE tenant_id = $1 AND product_id = $2 AND machine_id = $3 AND device_id <> $4
|
|
830
|
+
RETURNING device_id`,
|
|
831
|
+
[tenant, input.productId, input.machineId, input.deviceId]
|
|
832
|
+
);
|
|
833
|
+
await deleteDeviceState(
|
|
834
|
+
client,
|
|
835
|
+
tenant,
|
|
836
|
+
superseded.rows.map((row) => row.device_id)
|
|
837
|
+
);
|
|
838
|
+
}
|
|
839
|
+
const result = await client.query(
|
|
840
|
+
`INSERT INTO device (
|
|
841
|
+
tenant_id, device_id, product_id, device_name, device_public_key,
|
|
842
|
+
proof_key_id, proof_key_epoch, revoked, machine_id
|
|
843
|
+
)
|
|
844
|
+
VALUES ($1, $2, $3, $4, $5, $6, $7, false, $8)
|
|
845
|
+
ON CONFLICT (tenant_id, device_id) DO UPDATE
|
|
846
|
+
SET product_id = EXCLUDED.product_id,
|
|
847
|
+
device_name = EXCLUDED.device_name,
|
|
848
|
+
device_public_key = EXCLUDED.device_public_key,
|
|
849
|
+
proof_key_id = EXCLUDED.proof_key_id,
|
|
850
|
+
proof_key_epoch = EXCLUDED.proof_key_epoch,
|
|
851
|
+
revoked = false,
|
|
852
|
+
machine_id = EXCLUDED.machine_id,
|
|
853
|
+
capabilities = NULL
|
|
854
|
+
RETURNING ${SELECT_COLUMNS}`,
|
|
855
|
+
[
|
|
856
|
+
tenant,
|
|
857
|
+
input.deviceId,
|
|
858
|
+
input.productId,
|
|
859
|
+
input.deviceName,
|
|
860
|
+
input.devicePublicKey,
|
|
861
|
+
input.proofKeyId,
|
|
862
|
+
input.proofKeyEpoch,
|
|
863
|
+
input.machineId ?? null
|
|
864
|
+
]
|
|
865
|
+
);
|
|
866
|
+
return toRecord(result.rows[0]);
|
|
867
|
+
}
|
|
868
|
+
async function getDeviceOnClient(client, tenant, deviceId) {
|
|
869
|
+
const result = await client.query(
|
|
870
|
+
`SELECT ${SELECT_COLUMNS} FROM device WHERE tenant_id = $1 AND device_id = $2`,
|
|
871
|
+
[tenant, deviceId]
|
|
872
|
+
);
|
|
873
|
+
const row = result.rows[0];
|
|
874
|
+
return row === void 0 ? void 0 : toRecord(row);
|
|
875
|
+
}
|
|
825
876
|
var PostgresDeviceDirectory = class {
|
|
826
877
|
#pool;
|
|
827
878
|
#clock;
|
|
@@ -833,48 +884,9 @@ var PostgresDeviceDirectory = class {
|
|
|
833
884
|
const client = await this.#pool.connect();
|
|
834
885
|
try {
|
|
835
886
|
await client.query("BEGIN");
|
|
836
|
-
|
|
837
|
-
const superseded = await client.query(
|
|
838
|
-
`DELETE FROM device
|
|
839
|
-
WHERE tenant_id = $1 AND product_id = $2 AND machine_id = $3 AND device_id <> $4
|
|
840
|
-
RETURNING device_id`,
|
|
841
|
-
[tenant, input.productId, input.machineId, input.deviceId]
|
|
842
|
-
);
|
|
843
|
-
await deleteDeviceState(
|
|
844
|
-
client,
|
|
845
|
-
tenant,
|
|
846
|
-
superseded.rows.map((row) => row.device_id)
|
|
847
|
-
);
|
|
848
|
-
}
|
|
849
|
-
const result = await client.query(
|
|
850
|
-
`INSERT INTO device (
|
|
851
|
-
tenant_id, device_id, product_id, device_name, device_public_key,
|
|
852
|
-
proof_key_id, proof_key_epoch, revoked, machine_id
|
|
853
|
-
)
|
|
854
|
-
VALUES ($1, $2, $3, $4, $5, $6, $7, false, $8)
|
|
855
|
-
ON CONFLICT (tenant_id, device_id) DO UPDATE
|
|
856
|
-
SET product_id = EXCLUDED.product_id,
|
|
857
|
-
device_name = EXCLUDED.device_name,
|
|
858
|
-
device_public_key = EXCLUDED.device_public_key,
|
|
859
|
-
proof_key_id = EXCLUDED.proof_key_id,
|
|
860
|
-
proof_key_epoch = EXCLUDED.proof_key_epoch,
|
|
861
|
-
revoked = false,
|
|
862
|
-
machine_id = EXCLUDED.machine_id,
|
|
863
|
-
capabilities = NULL
|
|
864
|
-
RETURNING ${SELECT_COLUMNS}`,
|
|
865
|
-
[
|
|
866
|
-
tenant,
|
|
867
|
-
input.deviceId,
|
|
868
|
-
input.productId,
|
|
869
|
-
input.deviceName,
|
|
870
|
-
input.devicePublicKey,
|
|
871
|
-
input.proofKeyId,
|
|
872
|
-
input.proofKeyEpoch,
|
|
873
|
-
input.machineId ?? null
|
|
874
|
-
]
|
|
875
|
-
);
|
|
887
|
+
const record = await registerDeviceOnClient(client, tenant, input);
|
|
876
888
|
await client.query("COMMIT");
|
|
877
|
-
return
|
|
889
|
+
return record;
|
|
878
890
|
} catch (cause) {
|
|
879
891
|
await rollback(client);
|
|
880
892
|
throw cause;
|
|
@@ -883,12 +895,12 @@ var PostgresDeviceDirectory = class {
|
|
|
883
895
|
}
|
|
884
896
|
}
|
|
885
897
|
async get(tenant, deviceId) {
|
|
886
|
-
const
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
898
|
+
const client = await this.#pool.connect();
|
|
899
|
+
try {
|
|
900
|
+
return await getDeviceOnClient(client, tenant, deviceId);
|
|
901
|
+
} finally {
|
|
902
|
+
client.release();
|
|
903
|
+
}
|
|
892
904
|
}
|
|
893
905
|
/**
|
|
894
906
|
* Revocation removes the registration and the device-scoped state that only
|
|
@@ -1070,24 +1082,58 @@ var PostgresNonceStore = class {
|
|
|
1070
1082
|
);
|
|
1071
1083
|
return nonce;
|
|
1072
1084
|
}
|
|
1073
|
-
async
|
|
1085
|
+
async consumeIfValid(tenant, deviceId, nonce) {
|
|
1074
1086
|
const result = await this.#pool.query(
|
|
1075
|
-
`
|
|
1076
|
-
|
|
1077
|
-
|
|
1087
|
+
`UPDATE auth_nonce
|
|
1088
|
+
SET used = true
|
|
1089
|
+
WHERE tenant_id = $1 AND device_id = $2 AND nonce = $3
|
|
1090
|
+
AND used = false AND expires_at >= $4
|
|
1091
|
+
RETURNING nonce`,
|
|
1078
1092
|
[tenant, deviceId, nonce, this.#clock.now().toISOString()]
|
|
1079
1093
|
);
|
|
1080
1094
|
return result.rowCount === 1;
|
|
1081
1095
|
}
|
|
1082
|
-
async markUsed(tenant, nonce) {
|
|
1083
|
-
await this.#pool.query("UPDATE auth_nonce SET used = true WHERE tenant_id = $1 AND nonce = $2", [
|
|
1084
|
-
tenant,
|
|
1085
|
-
nonce
|
|
1086
|
-
]);
|
|
1087
|
-
}
|
|
1088
1096
|
};
|
|
1089
1097
|
|
|
1090
1098
|
// src/stores/pairing-codes.ts
|
|
1099
|
+
function bindingOf(input) {
|
|
1100
|
+
return {
|
|
1101
|
+
deviceName: input.deviceName,
|
|
1102
|
+
devicePublicKey: input.devicePublicKey,
|
|
1103
|
+
proofKeyId: input.proofKeyId,
|
|
1104
|
+
proofKeyEpoch: input.proofKeyEpoch,
|
|
1105
|
+
...input.machineId === void 0 ? {} : { machineId: input.machineId }
|
|
1106
|
+
};
|
|
1107
|
+
}
|
|
1108
|
+
async function completionKey(pairingCode) {
|
|
1109
|
+
const digest = new Uint8Array(await globalThis.crypto.subtle.digest(
|
|
1110
|
+
"SHA-256",
|
|
1111
|
+
new TextEncoder().encode(`byok-pairing-completion-v1\0${pairingCode}`)
|
|
1112
|
+
));
|
|
1113
|
+
const hex = [...digest].map((byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
1114
|
+
return `pairing-completion:v1:sha256:${hex}`;
|
|
1115
|
+
}
|
|
1116
|
+
function sameBinding(left, right) {
|
|
1117
|
+
return left.deviceName === right.deviceName && left.devicePublicKey === right.devicePublicKey && left.proofKeyId === right.proofKeyId && left.proofKeyEpoch === right.proofKeyEpoch && left.machineId === right.machineId;
|
|
1118
|
+
}
|
|
1119
|
+
function sameDevice(device, claims, binding) {
|
|
1120
|
+
return device.tenantId === claims.tenant_id && device.productId === claims.product_id && device.deviceName === binding.deviceName && device.devicePublicKey === binding.devicePublicKey && device.proofKeyId === binding.proofKeyId && device.proofKeyEpoch === binding.proofKeyEpoch && device.machineId === binding.machineId;
|
|
1121
|
+
}
|
|
1122
|
+
function parseCompletion(body) {
|
|
1123
|
+
try {
|
|
1124
|
+
const parsed = JSON.parse(body);
|
|
1125
|
+
if (typeof parsed.deviceId !== "string" || parsed.binding === void 0 || typeof parsed.binding.deviceName !== "string" || typeof parsed.binding.devicePublicKey !== "string" || typeof parsed.binding.proofKeyId !== "string" || typeof parsed.binding.proofKeyEpoch !== "number" || parsed.binding.machineId !== void 0 && typeof parsed.binding.machineId !== "string") return void 0;
|
|
1126
|
+
return parsed;
|
|
1127
|
+
} catch {
|
|
1128
|
+
return void 0;
|
|
1129
|
+
}
|
|
1130
|
+
}
|
|
1131
|
+
async function rollback2(client) {
|
|
1132
|
+
try {
|
|
1133
|
+
await client.query("ROLLBACK");
|
|
1134
|
+
} catch {
|
|
1135
|
+
}
|
|
1136
|
+
}
|
|
1091
1137
|
var PostgresPairingCodeStore = class {
|
|
1092
1138
|
#pool;
|
|
1093
1139
|
#clock;
|
|
@@ -1096,30 +1142,102 @@ var PostgresPairingCodeStore = class {
|
|
|
1096
1142
|
this.#clock = clock;
|
|
1097
1143
|
}
|
|
1098
1144
|
async issue(tenant, input) {
|
|
1099
|
-
await this.#pool.
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1145
|
+
const client = await this.#pool.connect();
|
|
1146
|
+
try {
|
|
1147
|
+
await client.query("BEGIN");
|
|
1148
|
+
const previous = await client.query(
|
|
1149
|
+
"SELECT tenant_id FROM pairing_code WHERE code = $1 FOR UPDATE",
|
|
1150
|
+
[input.code]
|
|
1151
|
+
);
|
|
1152
|
+
const key = await completionKey(input.code);
|
|
1153
|
+
const previousTenant = previous.rows[0]?.tenant_id;
|
|
1154
|
+
if (previousTenant !== void 0) {
|
|
1155
|
+
await client.query(
|
|
1156
|
+
"DELETE FROM device_request_receipts WHERE tenant_id = $1 AND key = $2",
|
|
1157
|
+
[previousTenant, key]
|
|
1158
|
+
);
|
|
1159
|
+
}
|
|
1160
|
+
await client.query(
|
|
1161
|
+
`INSERT INTO pairing_code (code, tenant_id, product_id, expires_at, redeemed_at)
|
|
1162
|
+
VALUES ($1, $2, $3, $4, NULL)
|
|
1163
|
+
ON CONFLICT (code) DO UPDATE
|
|
1164
|
+
SET tenant_id = EXCLUDED.tenant_id,
|
|
1165
|
+
product_id = EXCLUDED.product_id,
|
|
1166
|
+
expires_at = EXCLUDED.expires_at,
|
|
1167
|
+
redeemed_at = NULL`,
|
|
1168
|
+
[input.code, tenant, input.productId, input.expiresAt]
|
|
1169
|
+
);
|
|
1170
|
+
await client.query("COMMIT");
|
|
1171
|
+
} catch (cause) {
|
|
1172
|
+
await rollback2(client);
|
|
1173
|
+
throw cause;
|
|
1174
|
+
} finally {
|
|
1175
|
+
client.release();
|
|
1176
|
+
}
|
|
1109
1177
|
return { code: input.code, expiresAt: input.expiresAt };
|
|
1110
1178
|
}
|
|
1111
|
-
async
|
|
1112
|
-
const now = this.#clock.now()
|
|
1113
|
-
const
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1179
|
+
async redeemAndRegister(input) {
|
|
1180
|
+
const now = this.#clock.now();
|
|
1181
|
+
const client = await this.#pool.connect();
|
|
1182
|
+
try {
|
|
1183
|
+
await client.query("BEGIN");
|
|
1184
|
+
const selected = await client.query(
|
|
1185
|
+
`SELECT tenant_id, product_id, expires_at, redeemed_at
|
|
1186
|
+
FROM pairing_code WHERE code = $1 FOR UPDATE`,
|
|
1187
|
+
[input.pairingCode]
|
|
1188
|
+
);
|
|
1189
|
+
const claims = selected.rows[0];
|
|
1190
|
+
if (claims === void 0) {
|
|
1191
|
+
await client.query("COMMIT");
|
|
1192
|
+
return void 0;
|
|
1193
|
+
}
|
|
1194
|
+
const tenant = claims.tenant_id;
|
|
1195
|
+
const binding = bindingOf(input);
|
|
1196
|
+
if (claims.redeemed_at !== null) {
|
|
1197
|
+
const stored = await client.query(
|
|
1198
|
+
"SELECT body FROM device_request_receipts WHERE tenant_id = $1 AND key = $2",
|
|
1199
|
+
[tenant, await completionKey(input.pairingCode)]
|
|
1200
|
+
);
|
|
1201
|
+
const completion2 = stored.rows[0] === void 0 ? void 0 : parseCompletion(stored.rows[0].body);
|
|
1202
|
+
if (completion2 === void 0 || !sameBinding(completion2.binding, binding)) {
|
|
1203
|
+
await client.query("COMMIT");
|
|
1204
|
+
return void 0;
|
|
1205
|
+
}
|
|
1206
|
+
const device2 = await getDeviceOnClient(client, tenant, completion2.deviceId);
|
|
1207
|
+
await client.query("COMMIT");
|
|
1208
|
+
return device2 === void 0 || !sameDevice(device2, claims, binding) ? void 0 : device2;
|
|
1209
|
+
}
|
|
1210
|
+
if (claims.expires_at.getTime() < now.getTime()) {
|
|
1211
|
+
await client.query("COMMIT");
|
|
1212
|
+
return void 0;
|
|
1213
|
+
}
|
|
1214
|
+
await client.query(
|
|
1215
|
+
"UPDATE pairing_code SET redeemed_at = $2 WHERE code = $1 AND redeemed_at IS NULL",
|
|
1216
|
+
[input.pairingCode, now.toISOString()]
|
|
1217
|
+
);
|
|
1218
|
+
const device = await registerDeviceOnClient(client, tenant, {
|
|
1219
|
+
productId: claims.product_id,
|
|
1220
|
+
deviceId: input.deviceId,
|
|
1221
|
+
deviceName: input.deviceName,
|
|
1222
|
+
devicePublicKey: input.devicePublicKey,
|
|
1223
|
+
proofKeyId: input.proofKeyId,
|
|
1224
|
+
proofKeyEpoch: input.proofKeyEpoch,
|
|
1225
|
+
...input.machineId === void 0 ? {} : { machineId: input.machineId }
|
|
1226
|
+
});
|
|
1227
|
+
const completion = { deviceId: device.deviceId, binding };
|
|
1228
|
+
await client.query(
|
|
1229
|
+
`INSERT INTO device_request_receipts (tenant_id, key, body, recorded_at)
|
|
1230
|
+
VALUES ($1, $2, $3, $4)`,
|
|
1231
|
+
[tenant, await completionKey(input.pairingCode), JSON.stringify(completion), now.toISOString()]
|
|
1232
|
+
);
|
|
1233
|
+
await client.query("COMMIT");
|
|
1234
|
+
return device;
|
|
1235
|
+
} catch (cause) {
|
|
1236
|
+
await rollback2(client);
|
|
1237
|
+
throw cause;
|
|
1238
|
+
} finally {
|
|
1239
|
+
client.release();
|
|
1240
|
+
}
|
|
1123
1241
|
}
|
|
1124
1242
|
};
|
|
1125
1243
|
|
|
@@ -1226,9 +1344,15 @@ var PostgresProofRequestReceiptStore = class {
|
|
|
1226
1344
|
return row === void 0 ? void 0 : toReceipt2(row);
|
|
1227
1345
|
}
|
|
1228
1346
|
};
|
|
1229
|
-
|
|
1230
|
-
// src/stores/task-attempts.ts
|
|
1231
1347
|
var TASK_SELECT_COLUMNS = "tenant_id, task_id, device_id, agent_id, agent_profile_revision, owner_device_id, status, terminal_cause, cancel_requested_at, cancel_reason, cancel_message_id, updated_at";
|
|
1348
|
+
var AGENT_MESSAGE_ADMISSION_SELECT_COLUMNS = "message_id, payload_body, terminal_body";
|
|
1349
|
+
function admissionRowToRecord(row) {
|
|
1350
|
+
return {
|
|
1351
|
+
messageId: row.message_id,
|
|
1352
|
+
payloadBody: row.payload_body,
|
|
1353
|
+
...row.terminal_body === null ? {} : { terminalBody: row.terminal_body }
|
|
1354
|
+
};
|
|
1355
|
+
}
|
|
1232
1356
|
function taskRowToAttempt(row) {
|
|
1233
1357
|
return {
|
|
1234
1358
|
tenantId: row.tenant_id,
|
|
@@ -1304,6 +1428,70 @@ var PostgresTaskAttemptStore = class {
|
|
|
1304
1428
|
if (existing === void 0) throw new Error(`task ${input.taskId} vanished during Agent offer reservation`);
|
|
1305
1429
|
return { attempt: existing, created: false };
|
|
1306
1430
|
}
|
|
1431
|
+
async reserveAgentMessage(tenant, input) {
|
|
1432
|
+
const client = await this.#pool.connect();
|
|
1433
|
+
try {
|
|
1434
|
+
await client.query("BEGIN");
|
|
1435
|
+
const selected = await client.query(
|
|
1436
|
+
`SELECT ${TASK_SELECT_COLUMNS} FROM task
|
|
1437
|
+
WHERE tenant_id = $1 AND task_id = $2 FOR UPDATE`,
|
|
1438
|
+
[tenant, input.taskId]
|
|
1439
|
+
);
|
|
1440
|
+
const attempt = selected.rows[0];
|
|
1441
|
+
if (attempt === void 0 || attempt.device_id !== input.deviceId || attempt.cancel_requested_at !== null || !["offered", "claimed", "running"].includes(attempt.status)) {
|
|
1442
|
+
await client.query("COMMIT");
|
|
1443
|
+
return "rejected";
|
|
1444
|
+
}
|
|
1445
|
+
const inserted = await client.query(
|
|
1446
|
+
`INSERT INTO agent_message_admission (
|
|
1447
|
+
tenant_id, device_id, task_id, message_id, payload_body
|
|
1448
|
+
) VALUES ($1, $2, $3, $4, $5)
|
|
1449
|
+
ON CONFLICT (tenant_id, device_id, task_id) DO NOTHING
|
|
1450
|
+
RETURNING payload_body`,
|
|
1451
|
+
[tenant, input.deviceId, input.taskId, input.messageId, input.payloadBody]
|
|
1452
|
+
);
|
|
1453
|
+
if (inserted.rows[0] !== void 0) {
|
|
1454
|
+
await client.query("COMMIT");
|
|
1455
|
+
return "reserved";
|
|
1456
|
+
}
|
|
1457
|
+
const existing = await client.query(
|
|
1458
|
+
`SELECT message_id, payload_body FROM agent_message_admission
|
|
1459
|
+
WHERE tenant_id = $1 AND device_id = $2 AND task_id = $3`,
|
|
1460
|
+
[tenant, input.deviceId, input.taskId]
|
|
1461
|
+
);
|
|
1462
|
+
const admission = existing.rows[0];
|
|
1463
|
+
await client.query("COMMIT");
|
|
1464
|
+
return admission?.message_id === input.messageId && admission.payload_body === input.payloadBody ? "pending" : "rejected";
|
|
1465
|
+
} catch (cause) {
|
|
1466
|
+
await client.query("ROLLBACK").catch(() => {
|
|
1467
|
+
});
|
|
1468
|
+
throw cause;
|
|
1469
|
+
} finally {
|
|
1470
|
+
client.release();
|
|
1471
|
+
}
|
|
1472
|
+
}
|
|
1473
|
+
async readAgentMessage(tenant, input) {
|
|
1474
|
+
const result = await this.#pool.query(
|
|
1475
|
+
`SELECT ${AGENT_MESSAGE_ADMISSION_SELECT_COLUMNS}
|
|
1476
|
+
FROM agent_message_admission
|
|
1477
|
+
WHERE tenant_id = $1 AND device_id = $2 AND task_id = $3`,
|
|
1478
|
+
[tenant, input.deviceId, input.taskId]
|
|
1479
|
+
);
|
|
1480
|
+
const row = result.rows[0];
|
|
1481
|
+
return row !== void 0 && row.message_id === input.messageId && row.payload_body === input.payloadBody ? admissionRowToRecord(row) : void 0;
|
|
1482
|
+
}
|
|
1483
|
+
async finalizeAgentMessage(tenant, input) {
|
|
1484
|
+
const terminalized = await this.#pool.query(
|
|
1485
|
+
`UPDATE agent_message_admission
|
|
1486
|
+
SET terminal_body = $6
|
|
1487
|
+
WHERE tenant_id = $1 AND device_id = $2 AND task_id = $3
|
|
1488
|
+
AND message_id = $4 AND payload_body = $5 AND terminal_body IS NULL
|
|
1489
|
+
RETURNING ${AGENT_MESSAGE_ADMISSION_SELECT_COLUMNS}`,
|
|
1490
|
+
[tenant, input.deviceId, input.taskId, input.messageId, input.payloadBody, input.terminalBody]
|
|
1491
|
+
);
|
|
1492
|
+
const row = terminalized.rows[0];
|
|
1493
|
+
return row === void 0 ? this.readAgentMessage(tenant, input) : admissionRowToRecord(row);
|
|
1494
|
+
}
|
|
1307
1495
|
async get(tenant, taskId) {
|
|
1308
1496
|
const result = await this.#pool.query(
|
|
1309
1497
|
`SELECT ${TASK_SELECT_COLUMNS} FROM task WHERE tenant_id = $1 AND task_id = $2`,
|
|
@@ -1499,13 +1687,29 @@ var PostgresMailboxStore = class {
|
|
|
1499
1687
|
this.#requireDeviceId(input.deviceId);
|
|
1500
1688
|
const now = this.#now();
|
|
1501
1689
|
const moved = await this.#pool.query(
|
|
1502
|
-
`WITH
|
|
1503
|
-
INSERT INTO device_stream (
|
|
1504
|
-
|
|
1690
|
+
`WITH moved_zero AS (
|
|
1691
|
+
INSERT INTO device_stream (
|
|
1692
|
+
tenant_id, device_id, next_seq, acked_seq, acked_at, delivered_seq
|
|
1693
|
+
)
|
|
1694
|
+
SELECT $1, $2, 1, 0, $4, 0
|
|
1695
|
+
WHERE $3::bigint = 0
|
|
1505
1696
|
ON CONFLICT (tenant_id, device_id) DO UPDATE
|
|
1506
|
-
SET acked_seq =
|
|
1507
|
-
WHERE device_stream.acked_seq <=
|
|
1508
|
-
|
|
1697
|
+
SET acked_seq = 0, acked_at = EXCLUDED.acked_at
|
|
1698
|
+
WHERE device_stream.acked_seq <= 0
|
|
1699
|
+
AND device_stream.delivered_seq >= 0
|
|
1700
|
+
RETURNING acked_seq, acked_at, delivered_seq
|
|
1701
|
+
), moved_positive AS (
|
|
1702
|
+
UPDATE device_stream
|
|
1703
|
+
SET acked_seq = $3::bigint, acked_at = $4
|
|
1704
|
+
WHERE tenant_id = $1 AND device_id = $2
|
|
1705
|
+
AND $3::bigint > 0
|
|
1706
|
+
AND acked_seq <= $3::bigint
|
|
1707
|
+
AND delivered_seq >= $3::bigint
|
|
1708
|
+
RETURNING acked_seq, acked_at, delivered_seq
|
|
1709
|
+
), moved AS (
|
|
1710
|
+
SELECT acked_seq, acked_at, delivered_seq FROM moved_zero
|
|
1711
|
+
UNION ALL
|
|
1712
|
+
SELECT acked_seq, acked_at, delivered_seq FROM moved_positive
|
|
1509
1713
|
), marked AS (
|
|
1510
1714
|
UPDATE outbox
|
|
1511
1715
|
SET state = 'acked'
|
|
@@ -1513,7 +1717,7 @@ var PostgresMailboxStore = class {
|
|
|
1513
1717
|
AND seq <= (SELECT acked_seq FROM moved)
|
|
1514
1718
|
RETURNING 1
|
|
1515
1719
|
)
|
|
1516
|
-
SELECT acked_seq, acked_at FROM moved`,
|
|
1720
|
+
SELECT acked_seq, acked_at, delivered_seq FROM moved`,
|
|
1517
1721
|
[tenant, input.deviceId, input.ackedSeq, now]
|
|
1518
1722
|
);
|
|
1519
1723
|
const row = moved.rows[0];
|
|
@@ -1521,21 +1725,51 @@ var PostgresMailboxStore = class {
|
|
|
1521
1725
|
return {
|
|
1522
1726
|
tenantId: tenant,
|
|
1523
1727
|
deviceId: input.deviceId,
|
|
1728
|
+
deliveredSeq: Number(row.delivered_seq),
|
|
1524
1729
|
ackedSeq: Number(row.acked_seq),
|
|
1525
1730
|
updatedAt: row.acked_at ?? now
|
|
1526
1731
|
};
|
|
1527
1732
|
}
|
|
1528
1733
|
const current = await this.readCursor(tenant, input.deviceId);
|
|
1734
|
+
if (input.ackedSeq < current.ackedSeq) {
|
|
1735
|
+
throw new CoreConflictError(
|
|
1736
|
+
"mailbox_cursor_regression",
|
|
1737
|
+
`Cursor for device ${input.deviceId} is at ${current.ackedSeq}; refusing to move it back to ${input.ackedSeq}.`,
|
|
1738
|
+
current,
|
|
1739
|
+
this.#now()
|
|
1740
|
+
);
|
|
1741
|
+
}
|
|
1529
1742
|
throw new CoreConflictError(
|
|
1530
|
-
"
|
|
1531
|
-
`Cursor for device ${input.deviceId}
|
|
1743
|
+
"mailbox_cursor_ahead_of_delivery",
|
|
1744
|
+
`Cursor for device ${input.deviceId} was delivered through ${current.deliveredSeq}; refusing to acknowledge future cursor ${input.ackedSeq}.`,
|
|
1532
1745
|
current,
|
|
1533
1746
|
this.#now()
|
|
1534
1747
|
);
|
|
1535
1748
|
}
|
|
1749
|
+
async recordDelivery(tenant, input) {
|
|
1750
|
+
this.#requireDeviceId(input.deviceId);
|
|
1751
|
+
const recorded = await this.#pool.query(
|
|
1752
|
+
`INSERT INTO device_stream (
|
|
1753
|
+
tenant_id, device_id, next_seq, acked_seq, acked_at, delivered_seq
|
|
1754
|
+
)
|
|
1755
|
+
VALUES ($1, $2, 1, 0, NULL, $3::bigint)
|
|
1756
|
+
ON CONFLICT (tenant_id, device_id) DO UPDATE
|
|
1757
|
+
SET delivered_seq = GREATEST(device_stream.delivered_seq, EXCLUDED.delivered_seq)
|
|
1758
|
+
RETURNING acked_seq, acked_at, delivered_seq`,
|
|
1759
|
+
[tenant, input.deviceId, input.deliveredSeq]
|
|
1760
|
+
);
|
|
1761
|
+
const row = recorded.rows[0];
|
|
1762
|
+
return {
|
|
1763
|
+
tenantId: tenant,
|
|
1764
|
+
deviceId: input.deviceId,
|
|
1765
|
+
deliveredSeq: Number(row.delivered_seq),
|
|
1766
|
+
ackedSeq: Number(row.acked_seq),
|
|
1767
|
+
updatedAt: row.acked_at ?? this.#now()
|
|
1768
|
+
};
|
|
1769
|
+
}
|
|
1536
1770
|
async readCursor(tenant, deviceId) {
|
|
1537
1771
|
const result = await this.#pool.query(
|
|
1538
|
-
`SELECT acked_seq, acked_at FROM device_stream
|
|
1772
|
+
`SELECT acked_seq, acked_at, delivered_seq FROM device_stream
|
|
1539
1773
|
WHERE tenant_id = $1 AND device_id = $2`,
|
|
1540
1774
|
[tenant, deviceId]
|
|
1541
1775
|
);
|
|
@@ -1543,6 +1777,7 @@ var PostgresMailboxStore = class {
|
|
|
1543
1777
|
return {
|
|
1544
1778
|
tenantId: tenant,
|
|
1545
1779
|
deviceId,
|
|
1780
|
+
deliveredSeq: row === void 0 ? 0 : Number(row.delivered_seq),
|
|
1546
1781
|
ackedSeq: row === void 0 ? 0 : Number(row.acked_seq),
|
|
1547
1782
|
updatedAt: row?.acked_at ?? this.#now()
|
|
1548
1783
|
};
|
|
@@ -2147,7 +2382,7 @@ var PostgresAgentMemoryProjectionStore = class {
|
|
|
2147
2382
|
}
|
|
2148
2383
|
});
|
|
2149
2384
|
} catch (cause) {
|
|
2150
|
-
await
|
|
2385
|
+
await rollback3(client);
|
|
2151
2386
|
throw cause;
|
|
2152
2387
|
} finally {
|
|
2153
2388
|
client.release();
|
|
@@ -2184,7 +2419,7 @@ var PostgresAgentMemoryProjectionStore = class {
|
|
|
2184
2419
|
await client.query("COMMIT");
|
|
2185
2420
|
return AgentMemoryProjectionEraseResultSchema.parse({ nextWriterEpoch });
|
|
2186
2421
|
} catch (cause) {
|
|
2187
|
-
await
|
|
2422
|
+
await rollback3(client);
|
|
2188
2423
|
throw cause;
|
|
2189
2424
|
} finally {
|
|
2190
2425
|
client.release();
|
|
@@ -2321,7 +2556,7 @@ function toBase64(bytes) {
|
|
|
2321
2556
|
function toBase64Url(bytes) {
|
|
2322
2557
|
return toBase64(bytes).replaceAll("+", "-").replaceAll("/", "_").replace(/=+$/u, "");
|
|
2323
2558
|
}
|
|
2324
|
-
async function
|
|
2559
|
+
async function rollback3(client) {
|
|
2325
2560
|
try {
|
|
2326
2561
|
await client.query("ROLLBACK");
|
|
2327
2562
|
} catch {
|
|
@@ -2380,11 +2615,16 @@ var PostgresDeviceAssertionReplayAuthority = class {
|
|
|
2380
2615
|
// src/stores/index.ts
|
|
2381
2616
|
function createPostgresCloudStores(options) {
|
|
2382
2617
|
const { pool, clock, crypto } = options;
|
|
2618
|
+
const devices = new PostgresDeviceDirectory(pool, clock);
|
|
2619
|
+
const pairing = new PostgresPairingCodeStore(pool, clock);
|
|
2383
2620
|
return {
|
|
2384
2621
|
activity: new PostgresActivityStore(pool, clock),
|
|
2385
2622
|
approvals: new PostgresApprovalTimelineStore(pool, clock),
|
|
2386
|
-
devices
|
|
2387
|
-
|
|
2623
|
+
devices,
|
|
2624
|
+
// One durable code authority, projected into issuance and enrollment ports
|
|
2625
|
+
// so consumers cannot reopen the old redeem-then-register sequence.
|
|
2626
|
+
pairingCodes: { issue: pairing.issue.bind(pairing) },
|
|
2627
|
+
pairing: { redeemAndRegister: pairing.redeemAndRegister.bind(pairing) },
|
|
2388
2628
|
nonces: new PostgresNonceStore(pool, clock, crypto),
|
|
2389
2629
|
dedup: new PostgresInboundDedupStore(pool),
|
|
2390
2630
|
tasks: new PostgresTaskAttemptStore(pool, clock),
|
|
@@ -3673,7 +3913,7 @@ function toReceipt4(tenant, row) {
|
|
|
3673
3913
|
recordedAt: row.recorded_at.toISOString()
|
|
3674
3914
|
};
|
|
3675
3915
|
}
|
|
3676
|
-
function
|
|
3916
|
+
function sameBinding2(receipt, input) {
|
|
3677
3917
|
return receipt.operation === input.operation && receipt.resource === input.resource && receipt.bodySha256 === input.proofBodySha256 && receipt.bodySize === input.proofBodySize;
|
|
3678
3918
|
}
|
|
3679
3919
|
function bodyColumns2(body) {
|
|
@@ -3712,7 +3952,7 @@ var PostgresTruthCommitter = class {
|
|
|
3712
3952
|
]);
|
|
3713
3953
|
const replay = await this.#readReceipt(client, tenant, input.deviceId, input.requestId);
|
|
3714
3954
|
if (replay !== void 0) {
|
|
3715
|
-
if (!
|
|
3955
|
+
if (!sameBinding2(replay, input)) {
|
|
3716
3956
|
throw new TruthCommitError(
|
|
3717
3957
|
"proof_request_conflict",
|
|
3718
3958
|
`Request ${input.requestId} was already used with a different binding.`
|