@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 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
- if (input.machineId !== void 0) {
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 toRecord(result.rows[0]);
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 result = await this.#pool.query(
891
- `SELECT ${SELECT_COLUMNS} FROM device WHERE tenant_id = $1 AND device_id = $2`,
892
- [tenant, deviceId]
893
- );
894
- const row = result.rows[0];
895
- return row === void 0 ? void 0 : toRecord(row);
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 validate(tenant, deviceId, nonce) {
1089
+ async consumeIfValid(tenant, deviceId, nonce) {
1078
1090
  const result = await this.#pool.query(
1079
- `SELECT 1 FROM auth_nonce
1080
- WHERE tenant_id = $1 AND device_id = $2 AND nonce = $3
1081
- AND used = false AND expires_at >= $4`,
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.query(
1104
- `INSERT INTO pairing_code (code, tenant_id, product_id, expires_at, redeemed_at)
1105
- VALUES ($1, $2, $3, $4, NULL)
1106
- ON CONFLICT (code) DO UPDATE
1107
- SET tenant_id = EXCLUDED.tenant_id,
1108
- product_id = EXCLUDED.product_id,
1109
- expires_at = EXCLUDED.expires_at,
1110
- redeemed_at = NULL`,
1111
- [input.code, tenant, input.productId, input.expiresAt]
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 redeem(code) {
1116
- const now = this.#clock.now().toISOString();
1117
- const result = await this.#pool.query(
1118
- `UPDATE pairing_code
1119
- SET redeemed_at = $2
1120
- WHERE code = $1 AND redeemed_at IS NULL AND expires_at >= $2
1121
- RETURNING tenant_id, product_id`,
1122
- [code, now]
1123
- );
1124
- const row = result.rows[0];
1125
- if (row === void 0) return void 0;
1126
- return { tenantId: row.tenant_id, productId: row.product_id };
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 moved AS (
1507
- INSERT INTO device_stream (tenant_id, device_id, next_seq, acked_seq, acked_at)
1508
- VALUES ($1, $2, 1, $3::bigint, $4)
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 = EXCLUDED.acked_seq, acked_at = EXCLUDED.acked_at
1511
- WHERE device_stream.acked_seq <= EXCLUDED.acked_seq
1512
- RETURNING acked_seq, acked_at
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
- "mailbox_cursor_regression",
1535
- `Cursor for device ${input.deviceId} is at ${current.ackedSeq}; refusing to move it back to ${input.ackedSeq}.`,
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 rollback2(client);
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 rollback2(client);
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 rollback2(client) {
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: new PostgresDeviceDirectory(pool, clock),
2391
- pairingCodes: new PostgresPairingCodeStore(pool, clock),
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 sameBinding(receipt, input) {
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 (!sameBinding(replay, input)) {
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",