@byok-sdk/cloud-dataplane 0.10.2 → 0.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +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/index.js
CHANGED
|
@@ -826,6 +826,57 @@ async function rollback(client) {
|
|
|
826
826
|
} catch {
|
|
827
827
|
}
|
|
828
828
|
}
|
|
829
|
+
async function registerDeviceOnClient(client, tenant, input) {
|
|
830
|
+
if (input.machineId !== void 0) {
|
|
831
|
+
const superseded = await client.query(
|
|
832
|
+
`DELETE FROM device
|
|
833
|
+
WHERE tenant_id = $1 AND product_id = $2 AND machine_id = $3 AND device_id <> $4
|
|
834
|
+
RETURNING device_id`,
|
|
835
|
+
[tenant, input.productId, input.machineId, input.deviceId]
|
|
836
|
+
);
|
|
837
|
+
await deleteDeviceState(
|
|
838
|
+
client,
|
|
839
|
+
tenant,
|
|
840
|
+
superseded.rows.map((row) => row.device_id)
|
|
841
|
+
);
|
|
842
|
+
}
|
|
843
|
+
const result = await client.query(
|
|
844
|
+
`INSERT INTO device (
|
|
845
|
+
tenant_id, device_id, product_id, device_name, device_public_key,
|
|
846
|
+
proof_key_id, proof_key_epoch, revoked, machine_id
|
|
847
|
+
)
|
|
848
|
+
VALUES ($1, $2, $3, $4, $5, $6, $7, false, $8)
|
|
849
|
+
ON CONFLICT (tenant_id, device_id) DO UPDATE
|
|
850
|
+
SET product_id = EXCLUDED.product_id,
|
|
851
|
+
device_name = EXCLUDED.device_name,
|
|
852
|
+
device_public_key = EXCLUDED.device_public_key,
|
|
853
|
+
proof_key_id = EXCLUDED.proof_key_id,
|
|
854
|
+
proof_key_epoch = EXCLUDED.proof_key_epoch,
|
|
855
|
+
revoked = false,
|
|
856
|
+
machine_id = EXCLUDED.machine_id,
|
|
857
|
+
capabilities = NULL
|
|
858
|
+
RETURNING ${SELECT_COLUMNS}`,
|
|
859
|
+
[
|
|
860
|
+
tenant,
|
|
861
|
+
input.deviceId,
|
|
862
|
+
input.productId,
|
|
863
|
+
input.deviceName,
|
|
864
|
+
input.devicePublicKey,
|
|
865
|
+
input.proofKeyId,
|
|
866
|
+
input.proofKeyEpoch,
|
|
867
|
+
input.machineId ?? null
|
|
868
|
+
]
|
|
869
|
+
);
|
|
870
|
+
return toRecord(result.rows[0]);
|
|
871
|
+
}
|
|
872
|
+
async function getDeviceOnClient(client, tenant, deviceId) {
|
|
873
|
+
const result = await client.query(
|
|
874
|
+
`SELECT ${SELECT_COLUMNS} FROM device WHERE tenant_id = $1 AND device_id = $2`,
|
|
875
|
+
[tenant, deviceId]
|
|
876
|
+
);
|
|
877
|
+
const row = result.rows[0];
|
|
878
|
+
return row === void 0 ? void 0 : toRecord(row);
|
|
879
|
+
}
|
|
829
880
|
var PostgresDeviceDirectory = class {
|
|
830
881
|
#pool;
|
|
831
882
|
#clock;
|
|
@@ -837,48 +888,9 @@ var PostgresDeviceDirectory = class {
|
|
|
837
888
|
const client = await this.#pool.connect();
|
|
838
889
|
try {
|
|
839
890
|
await client.query("BEGIN");
|
|
840
|
-
|
|
841
|
-
const superseded = await client.query(
|
|
842
|
-
`DELETE FROM device
|
|
843
|
-
WHERE tenant_id = $1 AND product_id = $2 AND machine_id = $3 AND device_id <> $4
|
|
844
|
-
RETURNING device_id`,
|
|
845
|
-
[tenant, input.productId, input.machineId, input.deviceId]
|
|
846
|
-
);
|
|
847
|
-
await deleteDeviceState(
|
|
848
|
-
client,
|
|
849
|
-
tenant,
|
|
850
|
-
superseded.rows.map((row) => row.device_id)
|
|
851
|
-
);
|
|
852
|
-
}
|
|
853
|
-
const result = await client.query(
|
|
854
|
-
`INSERT INTO device (
|
|
855
|
-
tenant_id, device_id, product_id, device_name, device_public_key,
|
|
856
|
-
proof_key_id, proof_key_epoch, revoked, machine_id
|
|
857
|
-
)
|
|
858
|
-
VALUES ($1, $2, $3, $4, $5, $6, $7, false, $8)
|
|
859
|
-
ON CONFLICT (tenant_id, device_id) DO UPDATE
|
|
860
|
-
SET product_id = EXCLUDED.product_id,
|
|
861
|
-
device_name = EXCLUDED.device_name,
|
|
862
|
-
device_public_key = EXCLUDED.device_public_key,
|
|
863
|
-
proof_key_id = EXCLUDED.proof_key_id,
|
|
864
|
-
proof_key_epoch = EXCLUDED.proof_key_epoch,
|
|
865
|
-
revoked = false,
|
|
866
|
-
machine_id = EXCLUDED.machine_id,
|
|
867
|
-
capabilities = NULL
|
|
868
|
-
RETURNING ${SELECT_COLUMNS}`,
|
|
869
|
-
[
|
|
870
|
-
tenant,
|
|
871
|
-
input.deviceId,
|
|
872
|
-
input.productId,
|
|
873
|
-
input.deviceName,
|
|
874
|
-
input.devicePublicKey,
|
|
875
|
-
input.proofKeyId,
|
|
876
|
-
input.proofKeyEpoch,
|
|
877
|
-
input.machineId ?? null
|
|
878
|
-
]
|
|
879
|
-
);
|
|
891
|
+
const record = await registerDeviceOnClient(client, tenant, input);
|
|
880
892
|
await client.query("COMMIT");
|
|
881
|
-
return
|
|
893
|
+
return record;
|
|
882
894
|
} catch (cause) {
|
|
883
895
|
await rollback(client);
|
|
884
896
|
throw cause;
|
|
@@ -887,12 +899,12 @@ var PostgresDeviceDirectory = class {
|
|
|
887
899
|
}
|
|
888
900
|
}
|
|
889
901
|
async get(tenant, deviceId) {
|
|
890
|
-
const
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
902
|
+
const client = await this.#pool.connect();
|
|
903
|
+
try {
|
|
904
|
+
return await getDeviceOnClient(client, tenant, deviceId);
|
|
905
|
+
} finally {
|
|
906
|
+
client.release();
|
|
907
|
+
}
|
|
896
908
|
}
|
|
897
909
|
/**
|
|
898
910
|
* Revocation removes the registration and the device-scoped state that only
|
|
@@ -1074,24 +1086,58 @@ var PostgresNonceStore = class {
|
|
|
1074
1086
|
);
|
|
1075
1087
|
return nonce;
|
|
1076
1088
|
}
|
|
1077
|
-
async
|
|
1089
|
+
async consumeIfValid(tenant, deviceId, nonce) {
|
|
1078
1090
|
const result = await this.#pool.query(
|
|
1079
|
-
`
|
|
1080
|
-
|
|
1081
|
-
|
|
1091
|
+
`UPDATE auth_nonce
|
|
1092
|
+
SET used = true
|
|
1093
|
+
WHERE tenant_id = $1 AND device_id = $2 AND nonce = $3
|
|
1094
|
+
AND used = false AND expires_at >= $4
|
|
1095
|
+
RETURNING nonce`,
|
|
1082
1096
|
[tenant, deviceId, nonce, this.#clock.now().toISOString()]
|
|
1083
1097
|
);
|
|
1084
1098
|
return result.rowCount === 1;
|
|
1085
1099
|
}
|
|
1086
|
-
async markUsed(tenant, nonce) {
|
|
1087
|
-
await this.#pool.query("UPDATE auth_nonce SET used = true WHERE tenant_id = $1 AND nonce = $2", [
|
|
1088
|
-
tenant,
|
|
1089
|
-
nonce
|
|
1090
|
-
]);
|
|
1091
|
-
}
|
|
1092
1100
|
};
|
|
1093
1101
|
|
|
1094
1102
|
// src/stores/pairing-codes.ts
|
|
1103
|
+
function bindingOf(input) {
|
|
1104
|
+
return {
|
|
1105
|
+
deviceName: input.deviceName,
|
|
1106
|
+
devicePublicKey: input.devicePublicKey,
|
|
1107
|
+
proofKeyId: input.proofKeyId,
|
|
1108
|
+
proofKeyEpoch: input.proofKeyEpoch,
|
|
1109
|
+
...input.machineId === void 0 ? {} : { machineId: input.machineId }
|
|
1110
|
+
};
|
|
1111
|
+
}
|
|
1112
|
+
async function completionKey(pairingCode) {
|
|
1113
|
+
const digest = new Uint8Array(await globalThis.crypto.subtle.digest(
|
|
1114
|
+
"SHA-256",
|
|
1115
|
+
new TextEncoder().encode(`byok-pairing-completion-v1\0${pairingCode}`)
|
|
1116
|
+
));
|
|
1117
|
+
const hex = [...digest].map((byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
1118
|
+
return `pairing-completion:v1:sha256:${hex}`;
|
|
1119
|
+
}
|
|
1120
|
+
function sameBinding(left, right) {
|
|
1121
|
+
return left.deviceName === right.deviceName && left.devicePublicKey === right.devicePublicKey && left.proofKeyId === right.proofKeyId && left.proofKeyEpoch === right.proofKeyEpoch && left.machineId === right.machineId;
|
|
1122
|
+
}
|
|
1123
|
+
function sameDevice(device, claims, binding) {
|
|
1124
|
+
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;
|
|
1125
|
+
}
|
|
1126
|
+
function parseCompletion(body) {
|
|
1127
|
+
try {
|
|
1128
|
+
const parsed = JSON.parse(body);
|
|
1129
|
+
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;
|
|
1130
|
+
return parsed;
|
|
1131
|
+
} catch {
|
|
1132
|
+
return void 0;
|
|
1133
|
+
}
|
|
1134
|
+
}
|
|
1135
|
+
async function rollback2(client) {
|
|
1136
|
+
try {
|
|
1137
|
+
await client.query("ROLLBACK");
|
|
1138
|
+
} catch {
|
|
1139
|
+
}
|
|
1140
|
+
}
|
|
1095
1141
|
var PostgresPairingCodeStore = class {
|
|
1096
1142
|
#pool;
|
|
1097
1143
|
#clock;
|
|
@@ -1100,30 +1146,102 @@ var PostgresPairingCodeStore = class {
|
|
|
1100
1146
|
this.#clock = clock;
|
|
1101
1147
|
}
|
|
1102
1148
|
async issue(tenant, input) {
|
|
1103
|
-
await this.#pool.
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1149
|
+
const client = await this.#pool.connect();
|
|
1150
|
+
try {
|
|
1151
|
+
await client.query("BEGIN");
|
|
1152
|
+
const previous = await client.query(
|
|
1153
|
+
"SELECT tenant_id FROM pairing_code WHERE code = $1 FOR UPDATE",
|
|
1154
|
+
[input.code]
|
|
1155
|
+
);
|
|
1156
|
+
const key = await completionKey(input.code);
|
|
1157
|
+
const previousTenant = previous.rows[0]?.tenant_id;
|
|
1158
|
+
if (previousTenant !== void 0) {
|
|
1159
|
+
await client.query(
|
|
1160
|
+
"DELETE FROM device_request_receipts WHERE tenant_id = $1 AND key = $2",
|
|
1161
|
+
[previousTenant, key]
|
|
1162
|
+
);
|
|
1163
|
+
}
|
|
1164
|
+
await client.query(
|
|
1165
|
+
`INSERT INTO pairing_code (code, tenant_id, product_id, expires_at, redeemed_at)
|
|
1166
|
+
VALUES ($1, $2, $3, $4, NULL)
|
|
1167
|
+
ON CONFLICT (code) DO UPDATE
|
|
1168
|
+
SET tenant_id = EXCLUDED.tenant_id,
|
|
1169
|
+
product_id = EXCLUDED.product_id,
|
|
1170
|
+
expires_at = EXCLUDED.expires_at,
|
|
1171
|
+
redeemed_at = NULL`,
|
|
1172
|
+
[input.code, tenant, input.productId, input.expiresAt]
|
|
1173
|
+
);
|
|
1174
|
+
await client.query("COMMIT");
|
|
1175
|
+
} catch (cause) {
|
|
1176
|
+
await rollback2(client);
|
|
1177
|
+
throw cause;
|
|
1178
|
+
} finally {
|
|
1179
|
+
client.release();
|
|
1180
|
+
}
|
|
1113
1181
|
return { code: input.code, expiresAt: input.expiresAt };
|
|
1114
1182
|
}
|
|
1115
|
-
async
|
|
1116
|
-
const now = this.#clock.now()
|
|
1117
|
-
const
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1183
|
+
async redeemAndRegister(input) {
|
|
1184
|
+
const now = this.#clock.now();
|
|
1185
|
+
const client = await this.#pool.connect();
|
|
1186
|
+
try {
|
|
1187
|
+
await client.query("BEGIN");
|
|
1188
|
+
const selected = await client.query(
|
|
1189
|
+
`SELECT tenant_id, product_id, expires_at, redeemed_at
|
|
1190
|
+
FROM pairing_code WHERE code = $1 FOR UPDATE`,
|
|
1191
|
+
[input.pairingCode]
|
|
1192
|
+
);
|
|
1193
|
+
const claims = selected.rows[0];
|
|
1194
|
+
if (claims === void 0) {
|
|
1195
|
+
await client.query("COMMIT");
|
|
1196
|
+
return void 0;
|
|
1197
|
+
}
|
|
1198
|
+
const tenant = claims.tenant_id;
|
|
1199
|
+
const binding = bindingOf(input);
|
|
1200
|
+
if (claims.redeemed_at !== null) {
|
|
1201
|
+
const stored = await client.query(
|
|
1202
|
+
"SELECT body FROM device_request_receipts WHERE tenant_id = $1 AND key = $2",
|
|
1203
|
+
[tenant, await completionKey(input.pairingCode)]
|
|
1204
|
+
);
|
|
1205
|
+
const completion2 = stored.rows[0] === void 0 ? void 0 : parseCompletion(stored.rows[0].body);
|
|
1206
|
+
if (completion2 === void 0 || !sameBinding(completion2.binding, binding)) {
|
|
1207
|
+
await client.query("COMMIT");
|
|
1208
|
+
return void 0;
|
|
1209
|
+
}
|
|
1210
|
+
const device2 = await getDeviceOnClient(client, tenant, completion2.deviceId);
|
|
1211
|
+
await client.query("COMMIT");
|
|
1212
|
+
return device2 === void 0 || !sameDevice(device2, claims, binding) ? void 0 : device2;
|
|
1213
|
+
}
|
|
1214
|
+
if (claims.expires_at.getTime() < now.getTime()) {
|
|
1215
|
+
await client.query("COMMIT");
|
|
1216
|
+
return void 0;
|
|
1217
|
+
}
|
|
1218
|
+
await client.query(
|
|
1219
|
+
"UPDATE pairing_code SET redeemed_at = $2 WHERE code = $1 AND redeemed_at IS NULL",
|
|
1220
|
+
[input.pairingCode, now.toISOString()]
|
|
1221
|
+
);
|
|
1222
|
+
const device = await registerDeviceOnClient(client, tenant, {
|
|
1223
|
+
productId: claims.product_id,
|
|
1224
|
+
deviceId: input.deviceId,
|
|
1225
|
+
deviceName: input.deviceName,
|
|
1226
|
+
devicePublicKey: input.devicePublicKey,
|
|
1227
|
+
proofKeyId: input.proofKeyId,
|
|
1228
|
+
proofKeyEpoch: input.proofKeyEpoch,
|
|
1229
|
+
...input.machineId === void 0 ? {} : { machineId: input.machineId }
|
|
1230
|
+
});
|
|
1231
|
+
const completion = { deviceId: device.deviceId, binding };
|
|
1232
|
+
await client.query(
|
|
1233
|
+
`INSERT INTO device_request_receipts (tenant_id, key, body, recorded_at)
|
|
1234
|
+
VALUES ($1, $2, $3, $4)`,
|
|
1235
|
+
[tenant, await completionKey(input.pairingCode), JSON.stringify(completion), now.toISOString()]
|
|
1236
|
+
);
|
|
1237
|
+
await client.query("COMMIT");
|
|
1238
|
+
return device;
|
|
1239
|
+
} catch (cause) {
|
|
1240
|
+
await rollback2(client);
|
|
1241
|
+
throw cause;
|
|
1242
|
+
} finally {
|
|
1243
|
+
client.release();
|
|
1244
|
+
}
|
|
1127
1245
|
}
|
|
1128
1246
|
};
|
|
1129
1247
|
|
|
@@ -1230,9 +1348,15 @@ var PostgresProofRequestReceiptStore = class {
|
|
|
1230
1348
|
return row === void 0 ? void 0 : toReceipt2(row);
|
|
1231
1349
|
}
|
|
1232
1350
|
};
|
|
1233
|
-
|
|
1234
|
-
// src/stores/task-attempts.ts
|
|
1235
1351
|
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";
|
|
1352
|
+
var AGENT_MESSAGE_ADMISSION_SELECT_COLUMNS = "message_id, payload_body, terminal_body";
|
|
1353
|
+
function admissionRowToRecord(row) {
|
|
1354
|
+
return {
|
|
1355
|
+
messageId: row.message_id,
|
|
1356
|
+
payloadBody: row.payload_body,
|
|
1357
|
+
...row.terminal_body === null ? {} : { terminalBody: row.terminal_body }
|
|
1358
|
+
};
|
|
1359
|
+
}
|
|
1236
1360
|
function taskRowToAttempt(row) {
|
|
1237
1361
|
return {
|
|
1238
1362
|
tenantId: row.tenant_id,
|
|
@@ -1308,6 +1432,70 @@ var PostgresTaskAttemptStore = class {
|
|
|
1308
1432
|
if (existing === void 0) throw new Error(`task ${input.taskId} vanished during Agent offer reservation`);
|
|
1309
1433
|
return { attempt: existing, created: false };
|
|
1310
1434
|
}
|
|
1435
|
+
async reserveAgentMessage(tenant, input) {
|
|
1436
|
+
const client = await this.#pool.connect();
|
|
1437
|
+
try {
|
|
1438
|
+
await client.query("BEGIN");
|
|
1439
|
+
const selected = await client.query(
|
|
1440
|
+
`SELECT ${TASK_SELECT_COLUMNS} FROM task
|
|
1441
|
+
WHERE tenant_id = $1 AND task_id = $2 FOR UPDATE`,
|
|
1442
|
+
[tenant, input.taskId]
|
|
1443
|
+
);
|
|
1444
|
+
const attempt = selected.rows[0];
|
|
1445
|
+
if (attempt === void 0 || attempt.device_id !== input.deviceId || attempt.cancel_requested_at !== null || !["offered", "claimed", "running"].includes(attempt.status)) {
|
|
1446
|
+
await client.query("COMMIT");
|
|
1447
|
+
return "rejected";
|
|
1448
|
+
}
|
|
1449
|
+
const inserted = await client.query(
|
|
1450
|
+
`INSERT INTO agent_message_admission (
|
|
1451
|
+
tenant_id, device_id, task_id, message_id, payload_body
|
|
1452
|
+
) VALUES ($1, $2, $3, $4, $5)
|
|
1453
|
+
ON CONFLICT (tenant_id, device_id, task_id) DO NOTHING
|
|
1454
|
+
RETURNING payload_body`,
|
|
1455
|
+
[tenant, input.deviceId, input.taskId, input.messageId, input.payloadBody]
|
|
1456
|
+
);
|
|
1457
|
+
if (inserted.rows[0] !== void 0) {
|
|
1458
|
+
await client.query("COMMIT");
|
|
1459
|
+
return "reserved";
|
|
1460
|
+
}
|
|
1461
|
+
const existing = await client.query(
|
|
1462
|
+
`SELECT message_id, payload_body FROM agent_message_admission
|
|
1463
|
+
WHERE tenant_id = $1 AND device_id = $2 AND task_id = $3`,
|
|
1464
|
+
[tenant, input.deviceId, input.taskId]
|
|
1465
|
+
);
|
|
1466
|
+
const admission = existing.rows[0];
|
|
1467
|
+
await client.query("COMMIT");
|
|
1468
|
+
return admission?.message_id === input.messageId && admission.payload_body === input.payloadBody ? "pending" : "rejected";
|
|
1469
|
+
} catch (cause) {
|
|
1470
|
+
await client.query("ROLLBACK").catch(() => {
|
|
1471
|
+
});
|
|
1472
|
+
throw cause;
|
|
1473
|
+
} finally {
|
|
1474
|
+
client.release();
|
|
1475
|
+
}
|
|
1476
|
+
}
|
|
1477
|
+
async readAgentMessage(tenant, input) {
|
|
1478
|
+
const result = await this.#pool.query(
|
|
1479
|
+
`SELECT ${AGENT_MESSAGE_ADMISSION_SELECT_COLUMNS}
|
|
1480
|
+
FROM agent_message_admission
|
|
1481
|
+
WHERE tenant_id = $1 AND device_id = $2 AND task_id = $3`,
|
|
1482
|
+
[tenant, input.deviceId, input.taskId]
|
|
1483
|
+
);
|
|
1484
|
+
const row = result.rows[0];
|
|
1485
|
+
return row !== void 0 && row.message_id === input.messageId && row.payload_body === input.payloadBody ? admissionRowToRecord(row) : void 0;
|
|
1486
|
+
}
|
|
1487
|
+
async finalizeAgentMessage(tenant, input) {
|
|
1488
|
+
const terminalized = await this.#pool.query(
|
|
1489
|
+
`UPDATE agent_message_admission
|
|
1490
|
+
SET terminal_body = $6
|
|
1491
|
+
WHERE tenant_id = $1 AND device_id = $2 AND task_id = $3
|
|
1492
|
+
AND message_id = $4 AND payload_body = $5 AND terminal_body IS NULL
|
|
1493
|
+
RETURNING ${AGENT_MESSAGE_ADMISSION_SELECT_COLUMNS}`,
|
|
1494
|
+
[tenant, input.deviceId, input.taskId, input.messageId, input.payloadBody, input.terminalBody]
|
|
1495
|
+
);
|
|
1496
|
+
const row = terminalized.rows[0];
|
|
1497
|
+
return row === void 0 ? this.readAgentMessage(tenant, input) : admissionRowToRecord(row);
|
|
1498
|
+
}
|
|
1311
1499
|
async get(tenant, taskId) {
|
|
1312
1500
|
const result = await this.#pool.query(
|
|
1313
1501
|
`SELECT ${TASK_SELECT_COLUMNS} FROM task WHERE tenant_id = $1 AND task_id = $2`,
|
|
@@ -1503,13 +1691,29 @@ var PostgresMailboxStore = class {
|
|
|
1503
1691
|
this.#requireDeviceId(input.deviceId);
|
|
1504
1692
|
const now = this.#now();
|
|
1505
1693
|
const moved = await this.#pool.query(
|
|
1506
|
-
`WITH
|
|
1507
|
-
INSERT INTO device_stream (
|
|
1508
|
-
|
|
1694
|
+
`WITH moved_zero AS (
|
|
1695
|
+
INSERT INTO device_stream (
|
|
1696
|
+
tenant_id, device_id, next_seq, acked_seq, acked_at, delivered_seq
|
|
1697
|
+
)
|
|
1698
|
+
SELECT $1, $2, 1, 0, $4, 0
|
|
1699
|
+
WHERE $3::bigint = 0
|
|
1509
1700
|
ON CONFLICT (tenant_id, device_id) DO UPDATE
|
|
1510
|
-
SET acked_seq =
|
|
1511
|
-
WHERE device_stream.acked_seq <=
|
|
1512
|
-
|
|
1701
|
+
SET acked_seq = 0, acked_at = EXCLUDED.acked_at
|
|
1702
|
+
WHERE device_stream.acked_seq <= 0
|
|
1703
|
+
AND device_stream.delivered_seq >= 0
|
|
1704
|
+
RETURNING acked_seq, acked_at, delivered_seq
|
|
1705
|
+
), moved_positive AS (
|
|
1706
|
+
UPDATE device_stream
|
|
1707
|
+
SET acked_seq = $3::bigint, acked_at = $4
|
|
1708
|
+
WHERE tenant_id = $1 AND device_id = $2
|
|
1709
|
+
AND $3::bigint > 0
|
|
1710
|
+
AND acked_seq <= $3::bigint
|
|
1711
|
+
AND delivered_seq >= $3::bigint
|
|
1712
|
+
RETURNING acked_seq, acked_at, delivered_seq
|
|
1713
|
+
), moved AS (
|
|
1714
|
+
SELECT acked_seq, acked_at, delivered_seq FROM moved_zero
|
|
1715
|
+
UNION ALL
|
|
1716
|
+
SELECT acked_seq, acked_at, delivered_seq FROM moved_positive
|
|
1513
1717
|
), marked AS (
|
|
1514
1718
|
UPDATE outbox
|
|
1515
1719
|
SET state = 'acked'
|
|
@@ -1517,7 +1721,7 @@ var PostgresMailboxStore = class {
|
|
|
1517
1721
|
AND seq <= (SELECT acked_seq FROM moved)
|
|
1518
1722
|
RETURNING 1
|
|
1519
1723
|
)
|
|
1520
|
-
SELECT acked_seq, acked_at FROM moved`,
|
|
1724
|
+
SELECT acked_seq, acked_at, delivered_seq FROM moved`,
|
|
1521
1725
|
[tenant, input.deviceId, input.ackedSeq, now]
|
|
1522
1726
|
);
|
|
1523
1727
|
const row = moved.rows[0];
|
|
@@ -1525,21 +1729,51 @@ var PostgresMailboxStore = class {
|
|
|
1525
1729
|
return {
|
|
1526
1730
|
tenantId: tenant,
|
|
1527
1731
|
deviceId: input.deviceId,
|
|
1732
|
+
deliveredSeq: Number(row.delivered_seq),
|
|
1528
1733
|
ackedSeq: Number(row.acked_seq),
|
|
1529
1734
|
updatedAt: row.acked_at ?? now
|
|
1530
1735
|
};
|
|
1531
1736
|
}
|
|
1532
1737
|
const current = await this.readCursor(tenant, input.deviceId);
|
|
1738
|
+
if (input.ackedSeq < current.ackedSeq) {
|
|
1739
|
+
throw new CoreConflictError(
|
|
1740
|
+
"mailbox_cursor_regression",
|
|
1741
|
+
`Cursor for device ${input.deviceId} is at ${current.ackedSeq}; refusing to move it back to ${input.ackedSeq}.`,
|
|
1742
|
+
current,
|
|
1743
|
+
this.#now()
|
|
1744
|
+
);
|
|
1745
|
+
}
|
|
1533
1746
|
throw new CoreConflictError(
|
|
1534
|
-
"
|
|
1535
|
-
`Cursor for device ${input.deviceId}
|
|
1747
|
+
"mailbox_cursor_ahead_of_delivery",
|
|
1748
|
+
`Cursor for device ${input.deviceId} was delivered through ${current.deliveredSeq}; refusing to acknowledge future cursor ${input.ackedSeq}.`,
|
|
1536
1749
|
current,
|
|
1537
1750
|
this.#now()
|
|
1538
1751
|
);
|
|
1539
1752
|
}
|
|
1753
|
+
async recordDelivery(tenant, input) {
|
|
1754
|
+
this.#requireDeviceId(input.deviceId);
|
|
1755
|
+
const recorded = await this.#pool.query(
|
|
1756
|
+
`INSERT INTO device_stream (
|
|
1757
|
+
tenant_id, device_id, next_seq, acked_seq, acked_at, delivered_seq
|
|
1758
|
+
)
|
|
1759
|
+
VALUES ($1, $2, 1, 0, NULL, $3::bigint)
|
|
1760
|
+
ON CONFLICT (tenant_id, device_id) DO UPDATE
|
|
1761
|
+
SET delivered_seq = GREATEST(device_stream.delivered_seq, EXCLUDED.delivered_seq)
|
|
1762
|
+
RETURNING acked_seq, acked_at, delivered_seq`,
|
|
1763
|
+
[tenant, input.deviceId, input.deliveredSeq]
|
|
1764
|
+
);
|
|
1765
|
+
const row = recorded.rows[0];
|
|
1766
|
+
return {
|
|
1767
|
+
tenantId: tenant,
|
|
1768
|
+
deviceId: input.deviceId,
|
|
1769
|
+
deliveredSeq: Number(row.delivered_seq),
|
|
1770
|
+
ackedSeq: Number(row.acked_seq),
|
|
1771
|
+
updatedAt: row.acked_at ?? this.#now()
|
|
1772
|
+
};
|
|
1773
|
+
}
|
|
1540
1774
|
async readCursor(tenant, deviceId) {
|
|
1541
1775
|
const result = await this.#pool.query(
|
|
1542
|
-
`SELECT acked_seq, acked_at FROM device_stream
|
|
1776
|
+
`SELECT acked_seq, acked_at, delivered_seq FROM device_stream
|
|
1543
1777
|
WHERE tenant_id = $1 AND device_id = $2`,
|
|
1544
1778
|
[tenant, deviceId]
|
|
1545
1779
|
);
|
|
@@ -1547,6 +1781,7 @@ var PostgresMailboxStore = class {
|
|
|
1547
1781
|
return {
|
|
1548
1782
|
tenantId: tenant,
|
|
1549
1783
|
deviceId,
|
|
1784
|
+
deliveredSeq: row === void 0 ? 0 : Number(row.delivered_seq),
|
|
1550
1785
|
ackedSeq: row === void 0 ? 0 : Number(row.acked_seq),
|
|
1551
1786
|
updatedAt: row?.acked_at ?? this.#now()
|
|
1552
1787
|
};
|
|
@@ -2151,7 +2386,7 @@ var PostgresAgentMemoryProjectionStore = class {
|
|
|
2151
2386
|
}
|
|
2152
2387
|
});
|
|
2153
2388
|
} catch (cause) {
|
|
2154
|
-
await
|
|
2389
|
+
await rollback3(client);
|
|
2155
2390
|
throw cause;
|
|
2156
2391
|
} finally {
|
|
2157
2392
|
client.release();
|
|
@@ -2188,7 +2423,7 @@ var PostgresAgentMemoryProjectionStore = class {
|
|
|
2188
2423
|
await client.query("COMMIT");
|
|
2189
2424
|
return AgentMemoryProjectionEraseResultSchema.parse({ nextWriterEpoch });
|
|
2190
2425
|
} catch (cause) {
|
|
2191
|
-
await
|
|
2426
|
+
await rollback3(client);
|
|
2192
2427
|
throw cause;
|
|
2193
2428
|
} finally {
|
|
2194
2429
|
client.release();
|
|
@@ -2325,7 +2560,7 @@ function toBase64(bytes) {
|
|
|
2325
2560
|
function toBase64Url(bytes) {
|
|
2326
2561
|
return toBase64(bytes).replaceAll("+", "-").replaceAll("/", "_").replace(/=+$/u, "");
|
|
2327
2562
|
}
|
|
2328
|
-
async function
|
|
2563
|
+
async function rollback3(client) {
|
|
2329
2564
|
try {
|
|
2330
2565
|
await client.query("ROLLBACK");
|
|
2331
2566
|
} catch {
|
|
@@ -2384,11 +2619,16 @@ var PostgresDeviceAssertionReplayAuthority = class {
|
|
|
2384
2619
|
// src/stores/index.ts
|
|
2385
2620
|
function createPostgresCloudStores(options) {
|
|
2386
2621
|
const { pool, clock, crypto } = options;
|
|
2622
|
+
const devices = new PostgresDeviceDirectory(pool, clock);
|
|
2623
|
+
const pairing = new PostgresPairingCodeStore(pool, clock);
|
|
2387
2624
|
return {
|
|
2388
2625
|
activity: new PostgresActivityStore(pool, clock),
|
|
2389
2626
|
approvals: new PostgresApprovalTimelineStore(pool, clock),
|
|
2390
|
-
devices
|
|
2391
|
-
|
|
2627
|
+
devices,
|
|
2628
|
+
// One durable code authority, projected into issuance and enrollment ports
|
|
2629
|
+
// so consumers cannot reopen the old redeem-then-register sequence.
|
|
2630
|
+
pairingCodes: { issue: pairing.issue.bind(pairing) },
|
|
2631
|
+
pairing: { redeemAndRegister: pairing.redeemAndRegister.bind(pairing) },
|
|
2392
2632
|
nonces: new PostgresNonceStore(pool, clock, crypto),
|
|
2393
2633
|
dedup: new PostgresInboundDedupStore(pool),
|
|
2394
2634
|
tasks: new PostgresTaskAttemptStore(pool, clock),
|
|
@@ -3677,7 +3917,7 @@ function toReceipt4(tenant, row) {
|
|
|
3677
3917
|
recordedAt: row.recorded_at.toISOString()
|
|
3678
3918
|
};
|
|
3679
3919
|
}
|
|
3680
|
-
function
|
|
3920
|
+
function sameBinding2(receipt, input) {
|
|
3681
3921
|
return receipt.operation === input.operation && receipt.resource === input.resource && receipt.bodySha256 === input.proofBodySha256 && receipt.bodySize === input.proofBodySize;
|
|
3682
3922
|
}
|
|
3683
3923
|
function bodyColumns2(body) {
|
|
@@ -3716,7 +3956,7 @@ var PostgresTruthCommitter = class {
|
|
|
3716
3956
|
]);
|
|
3717
3957
|
const replay = await this.#readReceipt(client, tenant, input.deviceId, input.requestId);
|
|
3718
3958
|
if (replay !== void 0) {
|
|
3719
|
-
if (!
|
|
3959
|
+
if (!sameBinding2(replay, input)) {
|
|
3720
3960
|
throw new TruthCommitError(
|
|
3721
3961
|
"proof_request_conflict",
|
|
3722
3962
|
`Request ${input.requestId} was already used with a different binding.`
|
|
@@ -4779,6 +5019,10 @@ var PostgresCloudCleanup = class {
|
|
|
4779
5019
|
), receipts AS (
|
|
4780
5020
|
DELETE FROM device_request_receipts
|
|
4781
5021
|
WHERE tenant_id = $1 AND recorded_at < $5::timestamptz
|
|
5022
|
+
-- Pairing completion is an immutable enrollment authority, not
|
|
5023
|
+
-- a TTL-bound request receipt. Removing it would turn an exact
|
|
5024
|
+
-- bearer-code retry into a new/failed enrollment after cleanup.
|
|
5025
|
+
AND key NOT LIKE 'pairing-completion:v1:%'
|
|
4782
5026
|
RETURNING 1
|
|
4783
5027
|
), presence AS (
|
|
4784
5028
|
DELETE FROM device_presence
|
|
@@ -5309,6 +5553,7 @@ var TENANT_ERASURE_TABLES = [
|
|
|
5309
5553
|
"agent_egress_event",
|
|
5310
5554
|
"device_request_receipts",
|
|
5311
5555
|
"proof_request_receipt",
|
|
5556
|
+
"agent_message_admission",
|
|
5312
5557
|
"task",
|
|
5313
5558
|
"device_presence",
|
|
5314
5559
|
"device_assertion_replay",
|