@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/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
- if (input.machineId !== void 0) {
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 toRecord(result.rows[0]);
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 result = await this.#pool.query(
887
- `SELECT ${SELECT_COLUMNS} FROM device WHERE tenant_id = $1 AND device_id = $2`,
888
- [tenant, deviceId]
889
- );
890
- const row = result.rows[0];
891
- return row === void 0 ? void 0 : toRecord(row);
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 validate(tenant, deviceId, nonce) {
1085
+ async consumeIfValid(tenant, deviceId, nonce) {
1074
1086
  const result = await this.#pool.query(
1075
- `SELECT 1 FROM auth_nonce
1076
- WHERE tenant_id = $1 AND device_id = $2 AND nonce = $3
1077
- AND used = false AND expires_at >= $4`,
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.query(
1100
- `INSERT INTO pairing_code (code, tenant_id, product_id, expires_at, redeemed_at)
1101
- VALUES ($1, $2, $3, $4, NULL)
1102
- ON CONFLICT (code) DO UPDATE
1103
- SET tenant_id = EXCLUDED.tenant_id,
1104
- product_id = EXCLUDED.product_id,
1105
- expires_at = EXCLUDED.expires_at,
1106
- redeemed_at = NULL`,
1107
- [input.code, tenant, input.productId, input.expiresAt]
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 redeem(code) {
1112
- const now = this.#clock.now().toISOString();
1113
- const result = await this.#pool.query(
1114
- `UPDATE pairing_code
1115
- SET redeemed_at = $2
1116
- WHERE code = $1 AND redeemed_at IS NULL AND expires_at >= $2
1117
- RETURNING tenant_id, product_id`,
1118
- [code, now]
1119
- );
1120
- const row = result.rows[0];
1121
- if (row === void 0) return void 0;
1122
- return { tenantId: row.tenant_id, productId: row.product_id };
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 moved AS (
1503
- INSERT INTO device_stream (tenant_id, device_id, next_seq, acked_seq, acked_at)
1504
- VALUES ($1, $2, 1, $3::bigint, $4)
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 = EXCLUDED.acked_seq, acked_at = EXCLUDED.acked_at
1507
- WHERE device_stream.acked_seq <= EXCLUDED.acked_seq
1508
- RETURNING acked_seq, acked_at
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
- "mailbox_cursor_regression",
1531
- `Cursor for device ${input.deviceId} is at ${current.ackedSeq}; refusing to move it back to ${input.ackedSeq}.`,
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 rollback2(client);
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 rollback2(client);
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 rollback2(client) {
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: new PostgresDeviceDirectory(pool, clock),
2387
- pairingCodes: new PostgresPairingCodeStore(pool, clock),
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 sameBinding(receipt, input) {
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 (!sameBinding(replay, input)) {
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.`