@byok-sdk/cloud-dataplane 0.11.0 → 0.13.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 +571 -124
- package/dist/index.js.map +1 -1
- package/dist/runtime.js +566 -124
- 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/sql/0018_task_attempt_claimed_runtime.sql +23 -0
- package/dist/sql/0019_agent_ref_replay_keys.sql +36 -0
- package/dist/sql/0020_agent_ref_request_keys.sql +7 -0
- package/dist/stores/agent-egress.d.ts +2 -1
- package/dist/stores/approval-timeline.d.ts +2 -1
- package/dist/stores/core/mailbox.d.ts +5 -4
- package/dist/stores/dedup.d.ts +8 -5
- 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 +27 -2
- package/dist/tenant-erasure.d.ts +2 -2
- package/package.json +4 -4
package/dist/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import pg from 'pg';
|
|
2
|
-
import { DEDUP_RING_CAPACITY, NONCE_TTL_MS, validateActivityAppend, projectTimelineEvents, ByokCloudError, validateApprovalTimelineAppend, ApprovalObservationSchema, AllowAllRateLimiter, TruthCommitError, TruthCommitResponseSchema, truthRecordMetadata, TRUTH_REQUEST_ID_MAX_LENGTH, parseTimelineEvents, activityCursor, parseApprovalObservations, approvalTimelineCursor } from '@byok-sdk/cloud';
|
|
2
|
+
import { DEDUP_RING_CAPACITY, NONCE_TTL_MS, assertTaskAttemptListLimit, validateActivityAppend, projectTimelineEvents, activitySourceBatchState, ByokCloudError, validateApprovalTimelineAppend, ApprovalObservationSchema, validateApprovalTimelineResolve, AllowAllRateLimiter, TruthCommitError, TruthCommitResponseSchema, truthRecordMetadata, TRUTH_REQUEST_ID_MAX_LENGTH, parseTimelineEvents, activityCursor, parseApprovalObservations, approvalTimelineCursor } from '@byok-sdk/cloud';
|
|
3
3
|
import { contentHash, ByokCoreError, assertCanonicalTimestamp, isContentHash, tenantObjectKey, objectKeyPrefix, CoreConflictError, isLegalBoardTransition, checkSkillPackManifest, checkSkillPackEntry, SKILL_PACK_ENTRY_PATH, tenantId, SKILL_PACK_MANIFEST_SCHEMA_ID } from '@byok-sdk/core';
|
|
4
4
|
import { AwsClient } from 'aws4fetch';
|
|
5
5
|
import { XMLParser } from 'fast-xml-parser';
|
|
@@ -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
|
|
@@ -1026,23 +1038,31 @@ var PostgresInboundDedupStore = class {
|
|
|
1026
1038
|
this.#capacity = capacity;
|
|
1027
1039
|
}
|
|
1028
1040
|
async checkAndRecord(tenant, deviceId, envelopeId) {
|
|
1041
|
+
return this.#checkAndRecord(tenant, deviceId, "", "", envelopeId);
|
|
1042
|
+
}
|
|
1043
|
+
async checkAndRecordAgent(tenant, deviceId, agentRef, envelopeId) {
|
|
1044
|
+
return this.#checkAndRecord(tenant, deviceId, agentRef.agentId, agentRef.profileRevision, envelopeId);
|
|
1045
|
+
}
|
|
1046
|
+
async #checkAndRecord(tenant, deviceId, agentId, agentProfileRevision, envelopeId) {
|
|
1029
1047
|
const inserted = await this.#pool.query(
|
|
1030
|
-
`INSERT INTO inbound_dedup (tenant_id, device_id, envelope_id)
|
|
1031
|
-
VALUES ($1, $2, $3)
|
|
1032
|
-
ON CONFLICT (tenant_id, device_id, envelope_id) DO NOTHING
|
|
1048
|
+
`INSERT INTO inbound_dedup (tenant_id, device_id, agent_id, agent_profile_revision, envelope_id)
|
|
1049
|
+
VALUES ($1, $2, $3, $4, $5)
|
|
1050
|
+
ON CONFLICT (tenant_id, device_id, agent_id, agent_profile_revision, envelope_id) DO NOTHING
|
|
1033
1051
|
RETURNING 1`,
|
|
1034
|
-
[tenant, deviceId, envelopeId]
|
|
1052
|
+
[tenant, deviceId, agentId, agentProfileRevision, envelopeId]
|
|
1035
1053
|
);
|
|
1036
1054
|
if (inserted.rowCount === 0) return true;
|
|
1037
1055
|
await this.#pool.query(
|
|
1038
1056
|
`DELETE FROM inbound_dedup
|
|
1039
1057
|
WHERE tenant_id = $1 AND device_id = $2
|
|
1058
|
+
AND agent_id = $3 AND agent_profile_revision = $4
|
|
1040
1059
|
AND recorded_seq <= (
|
|
1041
1060
|
SELECT recorded_seq FROM inbound_dedup
|
|
1042
1061
|
WHERE tenant_id = $1 AND device_id = $2
|
|
1062
|
+
AND agent_id = $3 AND agent_profile_revision = $4
|
|
1043
1063
|
ORDER BY recorded_seq DESC
|
|
1044
|
-
OFFSET $
|
|
1045
|
-
[tenant, deviceId, this.#capacity]
|
|
1064
|
+
OFFSET $5 LIMIT 1)`,
|
|
1065
|
+
[tenant, deviceId, agentId, agentProfileRevision, this.#capacity]
|
|
1046
1066
|
);
|
|
1047
1067
|
return false;
|
|
1048
1068
|
}
|
|
@@ -1074,24 +1094,58 @@ var PostgresNonceStore = class {
|
|
|
1074
1094
|
);
|
|
1075
1095
|
return nonce;
|
|
1076
1096
|
}
|
|
1077
|
-
async
|
|
1097
|
+
async consumeIfValid(tenant, deviceId, nonce) {
|
|
1078
1098
|
const result = await this.#pool.query(
|
|
1079
|
-
`
|
|
1080
|
-
|
|
1081
|
-
|
|
1099
|
+
`UPDATE auth_nonce
|
|
1100
|
+
SET used = true
|
|
1101
|
+
WHERE tenant_id = $1 AND device_id = $2 AND nonce = $3
|
|
1102
|
+
AND used = false AND expires_at >= $4
|
|
1103
|
+
RETURNING nonce`,
|
|
1082
1104
|
[tenant, deviceId, nonce, this.#clock.now().toISOString()]
|
|
1083
1105
|
);
|
|
1084
1106
|
return result.rowCount === 1;
|
|
1085
1107
|
}
|
|
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
1108
|
};
|
|
1093
1109
|
|
|
1094
1110
|
// src/stores/pairing-codes.ts
|
|
1111
|
+
function bindingOf(input) {
|
|
1112
|
+
return {
|
|
1113
|
+
deviceName: input.deviceName,
|
|
1114
|
+
devicePublicKey: input.devicePublicKey,
|
|
1115
|
+
proofKeyId: input.proofKeyId,
|
|
1116
|
+
proofKeyEpoch: input.proofKeyEpoch,
|
|
1117
|
+
...input.machineId === void 0 ? {} : { machineId: input.machineId }
|
|
1118
|
+
};
|
|
1119
|
+
}
|
|
1120
|
+
async function completionKey(pairingCode) {
|
|
1121
|
+
const digest = new Uint8Array(await globalThis.crypto.subtle.digest(
|
|
1122
|
+
"SHA-256",
|
|
1123
|
+
new TextEncoder().encode(`byok-pairing-completion-v1\0${pairingCode}`)
|
|
1124
|
+
));
|
|
1125
|
+
const hex = [...digest].map((byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
1126
|
+
return `pairing-completion:v1:sha256:${hex}`;
|
|
1127
|
+
}
|
|
1128
|
+
function sameBinding(left, right) {
|
|
1129
|
+
return left.deviceName === right.deviceName && left.devicePublicKey === right.devicePublicKey && left.proofKeyId === right.proofKeyId && left.proofKeyEpoch === right.proofKeyEpoch && left.machineId === right.machineId;
|
|
1130
|
+
}
|
|
1131
|
+
function sameDevice(device, claims, binding) {
|
|
1132
|
+
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;
|
|
1133
|
+
}
|
|
1134
|
+
function parseCompletion(body) {
|
|
1135
|
+
try {
|
|
1136
|
+
const parsed = JSON.parse(body);
|
|
1137
|
+
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;
|
|
1138
|
+
return parsed;
|
|
1139
|
+
} catch {
|
|
1140
|
+
return void 0;
|
|
1141
|
+
}
|
|
1142
|
+
}
|
|
1143
|
+
async function rollback2(client) {
|
|
1144
|
+
try {
|
|
1145
|
+
await client.query("ROLLBACK");
|
|
1146
|
+
} catch {
|
|
1147
|
+
}
|
|
1148
|
+
}
|
|
1095
1149
|
var PostgresPairingCodeStore = class {
|
|
1096
1150
|
#pool;
|
|
1097
1151
|
#clock;
|
|
@@ -1100,30 +1154,102 @@ var PostgresPairingCodeStore = class {
|
|
|
1100
1154
|
this.#clock = clock;
|
|
1101
1155
|
}
|
|
1102
1156
|
async issue(tenant, input) {
|
|
1103
|
-
await this.#pool.
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1157
|
+
const client = await this.#pool.connect();
|
|
1158
|
+
try {
|
|
1159
|
+
await client.query("BEGIN");
|
|
1160
|
+
const previous = await client.query(
|
|
1161
|
+
"SELECT tenant_id FROM pairing_code WHERE code = $1 FOR UPDATE",
|
|
1162
|
+
[input.code]
|
|
1163
|
+
);
|
|
1164
|
+
const key = await completionKey(input.code);
|
|
1165
|
+
const previousTenant = previous.rows[0]?.tenant_id;
|
|
1166
|
+
if (previousTenant !== void 0) {
|
|
1167
|
+
await client.query(
|
|
1168
|
+
"DELETE FROM device_request_receipts WHERE tenant_id = $1 AND key = $2",
|
|
1169
|
+
[previousTenant, key]
|
|
1170
|
+
);
|
|
1171
|
+
}
|
|
1172
|
+
await client.query(
|
|
1173
|
+
`INSERT INTO pairing_code (code, tenant_id, product_id, expires_at, redeemed_at)
|
|
1174
|
+
VALUES ($1, $2, $3, $4, NULL)
|
|
1175
|
+
ON CONFLICT (code) DO UPDATE
|
|
1176
|
+
SET tenant_id = EXCLUDED.tenant_id,
|
|
1177
|
+
product_id = EXCLUDED.product_id,
|
|
1178
|
+
expires_at = EXCLUDED.expires_at,
|
|
1179
|
+
redeemed_at = NULL`,
|
|
1180
|
+
[input.code, tenant, input.productId, input.expiresAt]
|
|
1181
|
+
);
|
|
1182
|
+
await client.query("COMMIT");
|
|
1183
|
+
} catch (cause) {
|
|
1184
|
+
await rollback2(client);
|
|
1185
|
+
throw cause;
|
|
1186
|
+
} finally {
|
|
1187
|
+
client.release();
|
|
1188
|
+
}
|
|
1113
1189
|
return { code: input.code, expiresAt: input.expiresAt };
|
|
1114
1190
|
}
|
|
1115
|
-
async
|
|
1116
|
-
const now = this.#clock.now()
|
|
1117
|
-
const
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1191
|
+
async redeemAndRegister(input) {
|
|
1192
|
+
const now = this.#clock.now();
|
|
1193
|
+
const client = await this.#pool.connect();
|
|
1194
|
+
try {
|
|
1195
|
+
await client.query("BEGIN");
|
|
1196
|
+
const selected = await client.query(
|
|
1197
|
+
`SELECT tenant_id, product_id, expires_at, redeemed_at
|
|
1198
|
+
FROM pairing_code WHERE code = $1 FOR UPDATE`,
|
|
1199
|
+
[input.pairingCode]
|
|
1200
|
+
);
|
|
1201
|
+
const claims = selected.rows[0];
|
|
1202
|
+
if (claims === void 0) {
|
|
1203
|
+
await client.query("COMMIT");
|
|
1204
|
+
return void 0;
|
|
1205
|
+
}
|
|
1206
|
+
const tenant = claims.tenant_id;
|
|
1207
|
+
const binding = bindingOf(input);
|
|
1208
|
+
if (claims.redeemed_at !== null) {
|
|
1209
|
+
const stored = await client.query(
|
|
1210
|
+
"SELECT body FROM device_request_receipts WHERE tenant_id = $1 AND key = $2",
|
|
1211
|
+
[tenant, await completionKey(input.pairingCode)]
|
|
1212
|
+
);
|
|
1213
|
+
const completion2 = stored.rows[0] === void 0 ? void 0 : parseCompletion(stored.rows[0].body);
|
|
1214
|
+
if (completion2 === void 0 || !sameBinding(completion2.binding, binding)) {
|
|
1215
|
+
await client.query("COMMIT");
|
|
1216
|
+
return void 0;
|
|
1217
|
+
}
|
|
1218
|
+
const device2 = await getDeviceOnClient(client, tenant, completion2.deviceId);
|
|
1219
|
+
await client.query("COMMIT");
|
|
1220
|
+
return device2 === void 0 || !sameDevice(device2, claims, binding) ? void 0 : device2;
|
|
1221
|
+
}
|
|
1222
|
+
if (claims.expires_at.getTime() < now.getTime()) {
|
|
1223
|
+
await client.query("COMMIT");
|
|
1224
|
+
return void 0;
|
|
1225
|
+
}
|
|
1226
|
+
await client.query(
|
|
1227
|
+
"UPDATE pairing_code SET redeemed_at = $2 WHERE code = $1 AND redeemed_at IS NULL",
|
|
1228
|
+
[input.pairingCode, now.toISOString()]
|
|
1229
|
+
);
|
|
1230
|
+
const device = await registerDeviceOnClient(client, tenant, {
|
|
1231
|
+
productId: claims.product_id,
|
|
1232
|
+
deviceId: input.deviceId,
|
|
1233
|
+
deviceName: input.deviceName,
|
|
1234
|
+
devicePublicKey: input.devicePublicKey,
|
|
1235
|
+
proofKeyId: input.proofKeyId,
|
|
1236
|
+
proofKeyEpoch: input.proofKeyEpoch,
|
|
1237
|
+
...input.machineId === void 0 ? {} : { machineId: input.machineId }
|
|
1238
|
+
});
|
|
1239
|
+
const completion = { deviceId: device.deviceId, binding };
|
|
1240
|
+
await client.query(
|
|
1241
|
+
`INSERT INTO device_request_receipts (tenant_id, key, body, recorded_at)
|
|
1242
|
+
VALUES ($1, $2, $3, $4)`,
|
|
1243
|
+
[tenant, await completionKey(input.pairingCode), JSON.stringify(completion), now.toISOString()]
|
|
1244
|
+
);
|
|
1245
|
+
await client.query("COMMIT");
|
|
1246
|
+
return device;
|
|
1247
|
+
} catch (cause) {
|
|
1248
|
+
await rollback2(client);
|
|
1249
|
+
throw cause;
|
|
1250
|
+
} finally {
|
|
1251
|
+
client.release();
|
|
1252
|
+
}
|
|
1127
1253
|
}
|
|
1128
1254
|
};
|
|
1129
1255
|
|
|
@@ -1230,9 +1356,15 @@ var PostgresProofRequestReceiptStore = class {
|
|
|
1230
1356
|
return row === void 0 ? void 0 : toReceipt2(row);
|
|
1231
1357
|
}
|
|
1232
1358
|
};
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1359
|
+
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, claimed_runtime, claimed_runtime_capabilities, updated_at";
|
|
1360
|
+
var AGENT_MESSAGE_ADMISSION_SELECT_COLUMNS = "message_id, payload_body, terminal_body";
|
|
1361
|
+
function admissionRowToRecord(row) {
|
|
1362
|
+
return {
|
|
1363
|
+
messageId: row.message_id,
|
|
1364
|
+
payloadBody: row.payload_body,
|
|
1365
|
+
...row.terminal_body === null ? {} : { terminalBody: row.terminal_body }
|
|
1366
|
+
};
|
|
1367
|
+
}
|
|
1236
1368
|
function taskRowToAttempt(row) {
|
|
1237
1369
|
return {
|
|
1238
1370
|
tenantId: row.tenant_id,
|
|
@@ -1250,6 +1382,10 @@ function taskRowToAttempt(row) {
|
|
|
1250
1382
|
// former.
|
|
1251
1383
|
...row.owner_device_id === null ? {} : { ownerDeviceId: row.owner_device_id },
|
|
1252
1384
|
status: row.status,
|
|
1385
|
+
// `claimed_runtime` is written only by the guarded claim UPDATE below, so a
|
|
1386
|
+
// non-null value here is always the winning claim's own self-report.
|
|
1387
|
+
...row.claimed_runtime === null ? {} : { claimedRuntime: row.claimed_runtime },
|
|
1388
|
+
...row.claimed_runtime_capabilities === null ? {} : { claimedRuntimeCapabilities: row.claimed_runtime_capabilities },
|
|
1253
1389
|
...row.terminal_cause == null ? {} : { terminalCause: row.terminal_cause },
|
|
1254
1390
|
...row.cancel_requested_at === null ? {} : {
|
|
1255
1391
|
cancellation: {
|
|
@@ -1308,6 +1444,80 @@ var PostgresTaskAttemptStore = class {
|
|
|
1308
1444
|
if (existing === void 0) throw new Error(`task ${input.taskId} vanished during Agent offer reservation`);
|
|
1309
1445
|
return { attempt: existing, created: false };
|
|
1310
1446
|
}
|
|
1447
|
+
async reserveAgentMessage(tenant, input) {
|
|
1448
|
+
const client = await this.#pool.connect();
|
|
1449
|
+
try {
|
|
1450
|
+
await client.query("BEGIN");
|
|
1451
|
+
const selected = await client.query(
|
|
1452
|
+
`SELECT ${TASK_SELECT_COLUMNS} FROM task
|
|
1453
|
+
WHERE tenant_id = $1 AND task_id = $2 FOR UPDATE`,
|
|
1454
|
+
[tenant, input.taskId]
|
|
1455
|
+
);
|
|
1456
|
+
const attempt = selected.rows[0];
|
|
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
|
+
if (admission !== void 0) {
|
|
1464
|
+
await client.query("COMMIT");
|
|
1465
|
+
return admission.message_id === input.messageId && admission.payload_body === input.payloadBody ? "pending" : "rejected";
|
|
1466
|
+
}
|
|
1467
|
+
if (attempt === void 0 || attempt.device_id !== input.deviceId || attempt.cancel_requested_at !== null || !["offered", "claimed", "running"].includes(attempt.status)) {
|
|
1468
|
+
await client.query("COMMIT");
|
|
1469
|
+
return "rejected";
|
|
1470
|
+
}
|
|
1471
|
+
const inserted = await client.query(
|
|
1472
|
+
`INSERT INTO agent_message_admission (
|
|
1473
|
+
tenant_id, device_id, task_id, message_id, payload_body
|
|
1474
|
+
) VALUES ($1, $2, $3, $4, $5)
|
|
1475
|
+
ON CONFLICT (tenant_id, device_id, task_id) DO NOTHING
|
|
1476
|
+
RETURNING payload_body`,
|
|
1477
|
+
[tenant, input.deviceId, input.taskId, input.messageId, input.payloadBody]
|
|
1478
|
+
);
|
|
1479
|
+
if (inserted.rows[0] !== void 0) {
|
|
1480
|
+
await client.query("COMMIT");
|
|
1481
|
+
return "reserved";
|
|
1482
|
+
}
|
|
1483
|
+
const raced = await client.query(
|
|
1484
|
+
`SELECT message_id, payload_body FROM agent_message_admission
|
|
1485
|
+
WHERE tenant_id = $1 AND device_id = $2 AND task_id = $3`,
|
|
1486
|
+
[tenant, input.deviceId, input.taskId]
|
|
1487
|
+
);
|
|
1488
|
+
const racedAdmission = raced.rows[0];
|
|
1489
|
+
await client.query("COMMIT");
|
|
1490
|
+
return racedAdmission?.message_id === input.messageId && racedAdmission.payload_body === input.payloadBody ? "pending" : "rejected";
|
|
1491
|
+
} catch (cause) {
|
|
1492
|
+
await client.query("ROLLBACK").catch(() => {
|
|
1493
|
+
});
|
|
1494
|
+
throw cause;
|
|
1495
|
+
} finally {
|
|
1496
|
+
client.release();
|
|
1497
|
+
}
|
|
1498
|
+
}
|
|
1499
|
+
async readAgentMessage(tenant, input) {
|
|
1500
|
+
const result = await this.#pool.query(
|
|
1501
|
+
`SELECT ${AGENT_MESSAGE_ADMISSION_SELECT_COLUMNS}
|
|
1502
|
+
FROM agent_message_admission
|
|
1503
|
+
WHERE tenant_id = $1 AND device_id = $2 AND task_id = $3`,
|
|
1504
|
+
[tenant, input.deviceId, input.taskId]
|
|
1505
|
+
);
|
|
1506
|
+
const row = result.rows[0];
|
|
1507
|
+
return row !== void 0 && row.message_id === input.messageId && row.payload_body === input.payloadBody ? admissionRowToRecord(row) : void 0;
|
|
1508
|
+
}
|
|
1509
|
+
async finalizeAgentMessage(tenant, input) {
|
|
1510
|
+
const terminalized = await this.#pool.query(
|
|
1511
|
+
`UPDATE agent_message_admission
|
|
1512
|
+
SET terminal_body = $6
|
|
1513
|
+
WHERE tenant_id = $1 AND device_id = $2 AND task_id = $3
|
|
1514
|
+
AND message_id = $4 AND payload_body = $5 AND terminal_body IS NULL
|
|
1515
|
+
RETURNING ${AGENT_MESSAGE_ADMISSION_SELECT_COLUMNS}`,
|
|
1516
|
+
[tenant, input.deviceId, input.taskId, input.messageId, input.payloadBody, input.terminalBody]
|
|
1517
|
+
);
|
|
1518
|
+
const row = terminalized.rows[0];
|
|
1519
|
+
return row === void 0 ? this.readAgentMessage(tenant, input) : admissionRowToRecord(row);
|
|
1520
|
+
}
|
|
1311
1521
|
async get(tenant, taskId) {
|
|
1312
1522
|
const result = await this.#pool.query(
|
|
1313
1523
|
`SELECT ${TASK_SELECT_COLUMNS} FROM task WHERE tenant_id = $1 AND task_id = $2`,
|
|
@@ -1324,14 +1534,44 @@ var PostgresTaskAttemptStore = class {
|
|
|
1324
1534
|
);
|
|
1325
1535
|
return result.rows.map(taskRowToAttempt);
|
|
1326
1536
|
}
|
|
1537
|
+
async list(tenant, query) {
|
|
1538
|
+
assertTaskAttemptListLimit(query.limit);
|
|
1539
|
+
const result = await this.#pool.query(
|
|
1540
|
+
`SELECT ${TASK_SELECT_COLUMNS}
|
|
1541
|
+
FROM task
|
|
1542
|
+
WHERE tenant_id = $1 AND ($2::text IS NULL OR task_id > $2)
|
|
1543
|
+
ORDER BY task_id
|
|
1544
|
+
LIMIT $3`,
|
|
1545
|
+
[tenant, query.cursor ?? null, query.limit + 1]
|
|
1546
|
+
);
|
|
1547
|
+
const rows = result.rows.slice(0, query.limit);
|
|
1548
|
+
const attempts = rows.map(taskRowToAttempt);
|
|
1549
|
+
const hasMore = result.rows.length > rows.length;
|
|
1550
|
+
const last = attempts.at(-1);
|
|
1551
|
+
return {
|
|
1552
|
+
attempts,
|
|
1553
|
+
...hasMore && last !== void 0 ? { nextCursor: last.taskId } : {}
|
|
1554
|
+
};
|
|
1555
|
+
}
|
|
1327
1556
|
async claim(tenant, input) {
|
|
1328
1557
|
const claimed = await this.#pool.query(
|
|
1329
1558
|
`UPDATE task
|
|
1330
|
-
SET owner_device_id = $3,
|
|
1559
|
+
SET owner_device_id = $3,
|
|
1560
|
+
status = 'claimed',
|
|
1561
|
+
claimed_runtime = $5,
|
|
1562
|
+
claimed_runtime_capabilities = $6::jsonb,
|
|
1563
|
+
updated_at = $4
|
|
1331
1564
|
WHERE tenant_id = $1 AND task_id = $2 AND owner_device_id IS NULL
|
|
1332
1565
|
AND cancel_requested_at IS NULL AND status = 'offered'
|
|
1333
1566
|
RETURNING ${TASK_SELECT_COLUMNS}`,
|
|
1334
|
-
[
|
|
1567
|
+
[
|
|
1568
|
+
tenant,
|
|
1569
|
+
input.taskId,
|
|
1570
|
+
input.deviceId,
|
|
1571
|
+
this.#now(),
|
|
1572
|
+
input.runtime ?? null,
|
|
1573
|
+
input.capabilities === void 0 ? null : JSON.stringify(input.capabilities)
|
|
1574
|
+
]
|
|
1335
1575
|
);
|
|
1336
1576
|
const won = claimed.rows[0];
|
|
1337
1577
|
if (won !== void 0) return taskRowToAttempt(won);
|
|
@@ -1496,20 +1736,63 @@ var PostgresMailboxStore = class {
|
|
|
1496
1736
|
// Nothing above was mutated, so an identical call replays the same page.
|
|
1497
1737
|
// The returned position is a READ cursor and moves no ack.
|
|
1498
1738
|
nextSeq: page.at(-1)?.seq ?? query.afterSeq,
|
|
1499
|
-
hasMore: result.rows.length > page.length
|
|
1739
|
+
hasMore: result.rows.length > page.length,
|
|
1740
|
+
recoverableFrom: await this.#recoverableFrom(tenant, query.deviceId)
|
|
1500
1741
|
};
|
|
1501
1742
|
}
|
|
1743
|
+
/**
|
|
1744
|
+
* One past the highest row this device lost, which is exactly the highest
|
|
1745
|
+
* `expired` seq — `1` when it has lost nothing.
|
|
1746
|
+
*
|
|
1747
|
+
* No column and no migration, because `collectRetired` never deletes an
|
|
1748
|
+
* unacked row: §12.7.5 keeps the dead letter, so the loss is already durable
|
|
1749
|
+
* in `outbox` and a stored floor would be a second authority free to drift
|
|
1750
|
+
* from the sweep that moved it. Whatever eventually reaps dead letters (S4B,
|
|
1751
|
+
* O-009) is the change that has to carry this floor forward instead.
|
|
1752
|
+
*
|
|
1753
|
+
* `acked` rows are not counted: a consumed row is not a lost one.
|
|
1754
|
+
*
|
|
1755
|
+
* Read AFTER the page on purpose. A sweep landing between the two reads can
|
|
1756
|
+
* only report a floor HIGHER than the page it accompanies, which makes the
|
|
1757
|
+
* caller fail closed over a page that may already be missing rows; reading
|
|
1758
|
+
* the floor first would report one too low and serve that gap silently.
|
|
1759
|
+
*/
|
|
1760
|
+
async #recoverableFrom(tenant, deviceId) {
|
|
1761
|
+
const result = await this.#pool.query(
|
|
1762
|
+
`SELECT (COALESCE(MAX(seq), 0) + 1)::bigint AS recoverable_from
|
|
1763
|
+
FROM outbox
|
|
1764
|
+
WHERE tenant_id = $1 AND device_id = $2 AND state = 'expired'`,
|
|
1765
|
+
[tenant, deviceId]
|
|
1766
|
+
);
|
|
1767
|
+
return Number(result.rows[0].recoverable_from);
|
|
1768
|
+
}
|
|
1502
1769
|
async advanceCursor(tenant, input) {
|
|
1503
1770
|
this.#requireDeviceId(input.deviceId);
|
|
1504
1771
|
const now = this.#now();
|
|
1505
1772
|
const moved = await this.#pool.query(
|
|
1506
|
-
`WITH
|
|
1507
|
-
INSERT INTO device_stream (
|
|
1508
|
-
|
|
1773
|
+
`WITH moved_zero AS (
|
|
1774
|
+
INSERT INTO device_stream (
|
|
1775
|
+
tenant_id, device_id, next_seq, acked_seq, acked_at, delivered_seq
|
|
1776
|
+
)
|
|
1777
|
+
SELECT $1, $2, 1, 0, $4, 0
|
|
1778
|
+
WHERE $3::bigint = 0
|
|
1509
1779
|
ON CONFLICT (tenant_id, device_id) DO UPDATE
|
|
1510
|
-
SET acked_seq =
|
|
1511
|
-
WHERE device_stream.acked_seq <=
|
|
1512
|
-
|
|
1780
|
+
SET acked_seq = 0, acked_at = EXCLUDED.acked_at
|
|
1781
|
+
WHERE device_stream.acked_seq <= 0
|
|
1782
|
+
AND device_stream.delivered_seq >= 0
|
|
1783
|
+
RETURNING acked_seq, acked_at, delivered_seq
|
|
1784
|
+
), moved_positive AS (
|
|
1785
|
+
UPDATE device_stream
|
|
1786
|
+
SET acked_seq = $3::bigint, acked_at = $4
|
|
1787
|
+
WHERE tenant_id = $1 AND device_id = $2
|
|
1788
|
+
AND $3::bigint > 0
|
|
1789
|
+
AND acked_seq <= $3::bigint
|
|
1790
|
+
AND delivered_seq >= $3::bigint
|
|
1791
|
+
RETURNING acked_seq, acked_at, delivered_seq
|
|
1792
|
+
), moved AS (
|
|
1793
|
+
SELECT acked_seq, acked_at, delivered_seq FROM moved_zero
|
|
1794
|
+
UNION ALL
|
|
1795
|
+
SELECT acked_seq, acked_at, delivered_seq FROM moved_positive
|
|
1513
1796
|
), marked AS (
|
|
1514
1797
|
UPDATE outbox
|
|
1515
1798
|
SET state = 'acked'
|
|
@@ -1517,7 +1800,7 @@ var PostgresMailboxStore = class {
|
|
|
1517
1800
|
AND seq <= (SELECT acked_seq FROM moved)
|
|
1518
1801
|
RETURNING 1
|
|
1519
1802
|
)
|
|
1520
|
-
SELECT acked_seq, acked_at FROM moved`,
|
|
1803
|
+
SELECT acked_seq, acked_at, delivered_seq FROM moved`,
|
|
1521
1804
|
[tenant, input.deviceId, input.ackedSeq, now]
|
|
1522
1805
|
);
|
|
1523
1806
|
const row = moved.rows[0];
|
|
@@ -1525,21 +1808,51 @@ var PostgresMailboxStore = class {
|
|
|
1525
1808
|
return {
|
|
1526
1809
|
tenantId: tenant,
|
|
1527
1810
|
deviceId: input.deviceId,
|
|
1811
|
+
deliveredSeq: Number(row.delivered_seq),
|
|
1528
1812
|
ackedSeq: Number(row.acked_seq),
|
|
1529
1813
|
updatedAt: row.acked_at ?? now
|
|
1530
1814
|
};
|
|
1531
1815
|
}
|
|
1532
1816
|
const current = await this.readCursor(tenant, input.deviceId);
|
|
1817
|
+
if (input.ackedSeq < current.ackedSeq) {
|
|
1818
|
+
throw new CoreConflictError(
|
|
1819
|
+
"mailbox_cursor_regression",
|
|
1820
|
+
`Cursor for device ${input.deviceId} is at ${current.ackedSeq}; refusing to move it back to ${input.ackedSeq}.`,
|
|
1821
|
+
current,
|
|
1822
|
+
this.#now()
|
|
1823
|
+
);
|
|
1824
|
+
}
|
|
1533
1825
|
throw new CoreConflictError(
|
|
1534
|
-
"
|
|
1535
|
-
`Cursor for device ${input.deviceId}
|
|
1826
|
+
"mailbox_cursor_ahead_of_delivery",
|
|
1827
|
+
`Cursor for device ${input.deviceId} was delivered through ${current.deliveredSeq}; refusing to acknowledge future cursor ${input.ackedSeq}.`,
|
|
1536
1828
|
current,
|
|
1537
1829
|
this.#now()
|
|
1538
1830
|
);
|
|
1539
1831
|
}
|
|
1832
|
+
async recordDelivery(tenant, input) {
|
|
1833
|
+
this.#requireDeviceId(input.deviceId);
|
|
1834
|
+
const recorded = await this.#pool.query(
|
|
1835
|
+
`INSERT INTO device_stream (
|
|
1836
|
+
tenant_id, device_id, next_seq, acked_seq, acked_at, delivered_seq
|
|
1837
|
+
)
|
|
1838
|
+
VALUES ($1, $2, 1, 0, NULL, $3::bigint)
|
|
1839
|
+
ON CONFLICT (tenant_id, device_id) DO UPDATE
|
|
1840
|
+
SET delivered_seq = GREATEST(device_stream.delivered_seq, EXCLUDED.delivered_seq)
|
|
1841
|
+
RETURNING acked_seq, acked_at, delivered_seq`,
|
|
1842
|
+
[tenant, input.deviceId, input.deliveredSeq]
|
|
1843
|
+
);
|
|
1844
|
+
const row = recorded.rows[0];
|
|
1845
|
+
return {
|
|
1846
|
+
tenantId: tenant,
|
|
1847
|
+
deviceId: input.deviceId,
|
|
1848
|
+
deliveredSeq: Number(row.delivered_seq),
|
|
1849
|
+
ackedSeq: Number(row.acked_seq),
|
|
1850
|
+
updatedAt: row.acked_at ?? this.#now()
|
|
1851
|
+
};
|
|
1852
|
+
}
|
|
1540
1853
|
async readCursor(tenant, deviceId) {
|
|
1541
1854
|
const result = await this.#pool.query(
|
|
1542
|
-
`SELECT acked_seq, acked_at FROM device_stream
|
|
1855
|
+
`SELECT acked_seq, acked_at, delivered_seq FROM device_stream
|
|
1543
1856
|
WHERE tenant_id = $1 AND device_id = $2`,
|
|
1544
1857
|
[tenant, deviceId]
|
|
1545
1858
|
);
|
|
@@ -1547,6 +1860,7 @@ var PostgresMailboxStore = class {
|
|
|
1547
1860
|
return {
|
|
1548
1861
|
tenantId: tenant,
|
|
1549
1862
|
deviceId,
|
|
1863
|
+
deliveredSeq: row === void 0 ? 0 : Number(row.delivered_seq),
|
|
1550
1864
|
ackedSeq: row === void 0 ? 0 : Number(row.acked_seq),
|
|
1551
1865
|
updatedAt: row?.acked_at ?? this.#now()
|
|
1552
1866
|
};
|
|
@@ -1712,8 +2026,35 @@ var PostgresActivityStore = class {
|
|
|
1712
2026
|
const receivedAt = now.toISOString();
|
|
1713
2027
|
const incoming = projectTimelineEvents(input, receivedAt);
|
|
1714
2028
|
const expiresAt = new Date(now.getTime() + input.ttlMs).toISOString();
|
|
1715
|
-
const
|
|
1716
|
-
|
|
2029
|
+
const client = await this.pool.connect();
|
|
2030
|
+
try {
|
|
2031
|
+
await client.query("BEGIN");
|
|
2032
|
+
await client.query(
|
|
2033
|
+
`SELECT pg_advisory_xact_lock(hashtextextended($1 || E'\\x1f' || $2, 0))`,
|
|
2034
|
+
[tenant, input.taskId]
|
|
2035
|
+
);
|
|
2036
|
+
const locked = await client.query(
|
|
2037
|
+
`SELECT tenant_id, task_id, entries, dropped, capacity, expires_at
|
|
2038
|
+
FROM activity_tail
|
|
2039
|
+
WHERE tenant_id = $1 AND task_id = $2
|
|
2040
|
+
FOR UPDATE`,
|
|
2041
|
+
[tenant, input.taskId]
|
|
2042
|
+
);
|
|
2043
|
+
const stored = locked.rows[0];
|
|
2044
|
+
const live = stored !== void 0 && receivedAt < stored.expires_at ? toTail(stored) : void 0;
|
|
2045
|
+
const sourceState = activitySourceBatchState(live?.entries ?? [], input);
|
|
2046
|
+
if (sourceState === "same") {
|
|
2047
|
+
await client.query("COMMIT");
|
|
2048
|
+
return live;
|
|
2049
|
+
}
|
|
2050
|
+
if (sourceState === "conflict") {
|
|
2051
|
+
throw new ByokCloudError(
|
|
2052
|
+
"coordination_input_invalid",
|
|
2053
|
+
`Activity source envelope ${input.sourceEnvelopeId} already belongs to another canonical batch.`
|
|
2054
|
+
);
|
|
2055
|
+
}
|
|
2056
|
+
const result = await client.query(
|
|
2057
|
+
`WITH incoming AS (
|
|
1717
2058
|
SELECT entry
|
|
1718
2059
|
FROM jsonb_array_elements($4::jsonb) AS element(entry)
|
|
1719
2060
|
), trimmed AS (
|
|
@@ -1780,16 +2121,24 @@ var PostgresActivityStore = class {
|
|
|
1780
2121
|
)
|
|
1781
2122
|
)
|
|
1782
2123
|
RETURNING tenant_id, task_id, entries, dropped, capacity, expires_at`,
|
|
1783
|
-
|
|
1784
|
-
);
|
|
1785
|
-
const row = result.rows[0];
|
|
1786
|
-
if (row === void 0) {
|
|
1787
|
-
throw new ByokCloudError(
|
|
1788
|
-
"coordination_input_invalid",
|
|
1789
|
-
`Activity batch ${input.batchSeq} conflicts with the existing typed tail authority.`
|
|
2124
|
+
[tenant, input.taskId, receivedAt, JSON.stringify(incoming), input.dropped, capacity, expiresAt]
|
|
1790
2125
|
);
|
|
2126
|
+
const row = result.rows[0];
|
|
2127
|
+
if (row === void 0) {
|
|
2128
|
+
throw new ByokCloudError(
|
|
2129
|
+
"coordination_input_invalid",
|
|
2130
|
+
`Activity batch ${input.batchSeq} conflicts with the existing typed tail authority.`
|
|
2131
|
+
);
|
|
2132
|
+
}
|
|
2133
|
+
await client.query("COMMIT");
|
|
2134
|
+
return toTail(row);
|
|
2135
|
+
} catch (caught) {
|
|
2136
|
+
await client.query("ROLLBACK").catch(() => {
|
|
2137
|
+
});
|
|
2138
|
+
throw caught;
|
|
2139
|
+
} finally {
|
|
2140
|
+
client.release();
|
|
1791
2141
|
}
|
|
1792
|
-
return toTail(row);
|
|
1793
2142
|
}
|
|
1794
2143
|
async read(tenant, taskId) {
|
|
1795
2144
|
const result = await this.pool.query(
|
|
@@ -1908,6 +2257,79 @@ var PostgresApprovalTimelineStore = class {
|
|
|
1908
2257
|
client.release();
|
|
1909
2258
|
}
|
|
1910
2259
|
}
|
|
2260
|
+
async resolvePending(tenant, input) {
|
|
2261
|
+
const { capacity, ttlMs, event } = validateApprovalTimelineResolve(input);
|
|
2262
|
+
const now = this.clock.now();
|
|
2263
|
+
const receivedAt = now.toISOString();
|
|
2264
|
+
const expiresAt = new Date(now.getTime() + ttlMs).toISOString();
|
|
2265
|
+
const client = await this.pool.connect();
|
|
2266
|
+
try {
|
|
2267
|
+
await client.query("BEGIN");
|
|
2268
|
+
await client.query(
|
|
2269
|
+
`SELECT pg_advisory_xact_lock(hashtextextended($1 || E'\\x1f' || $2, 0))`,
|
|
2270
|
+
[tenant, input.taskId]
|
|
2271
|
+
);
|
|
2272
|
+
const stored = await readLocked(client, tenant, input.taskId);
|
|
2273
|
+
const live = stored !== void 0 && receivedAt < stored.expires_at ? toTail2(stored) : void 0;
|
|
2274
|
+
if (live === void 0) {
|
|
2275
|
+
await client.query("COMMIT");
|
|
2276
|
+
return { status: "absent" };
|
|
2277
|
+
}
|
|
2278
|
+
const duplicate = live.entries.find((entry) => entry.sourceEnvelopeId === input.sourceEnvelopeId);
|
|
2279
|
+
if (duplicate !== void 0) {
|
|
2280
|
+
await client.query("COMMIT");
|
|
2281
|
+
return JSON.stringify(duplicate.event) === JSON.stringify(event) ? { status: "replayed", tail: live } : { status: "conflict", tail: live };
|
|
2282
|
+
}
|
|
2283
|
+
const pending = pendingObservation(live.entries);
|
|
2284
|
+
if (pending === void 0) {
|
|
2285
|
+
await client.query("COMMIT");
|
|
2286
|
+
return { status: "absent", tail: live };
|
|
2287
|
+
}
|
|
2288
|
+
if (pending.sourceEnvelopeId !== input.expectedSourceEnvelopeId || pending.revision !== input.expectedRevision) {
|
|
2289
|
+
await client.query("COMMIT");
|
|
2290
|
+
return { status: "superseded", tail: live };
|
|
2291
|
+
}
|
|
2292
|
+
const revision = Number(stored.next_revision);
|
|
2293
|
+
const expectedRevision = (live.cursor ?? 0) + 1;
|
|
2294
|
+
if (!Number.isSafeInteger(revision) || revision <= 0 || revision !== expectedRevision) {
|
|
2295
|
+
throw new ByokCloudError(
|
|
2296
|
+
"coordination_input_invalid",
|
|
2297
|
+
"Approval timeline revision authority is malformed."
|
|
2298
|
+
);
|
|
2299
|
+
}
|
|
2300
|
+
const observation = ApprovalObservationSchema.parse({
|
|
2301
|
+
taskId: input.taskId,
|
|
2302
|
+
sourceEnvelopeId: input.sourceEnvelopeId,
|
|
2303
|
+
revision,
|
|
2304
|
+
receivedAt,
|
|
2305
|
+
event
|
|
2306
|
+
});
|
|
2307
|
+
const allEntries = [...live.entries, observation];
|
|
2308
|
+
const evicted = Math.max(allEntries.length - capacity, 0);
|
|
2309
|
+
const entries = allEntries.slice(evicted);
|
|
2310
|
+
const dropped = live.dropped + evicted;
|
|
2311
|
+
const result = await client.query(
|
|
2312
|
+
`INSERT INTO approval_timeline_tail
|
|
2313
|
+
(tenant_id, task_id, entries, next_revision, dropped, capacity, expires_at)
|
|
2314
|
+
VALUES ($1, $2, $3::jsonb, $4, $5, $6, $7)
|
|
2315
|
+
ON CONFLICT (tenant_id, task_id) DO UPDATE
|
|
2316
|
+
SET entries = EXCLUDED.entries,
|
|
2317
|
+
next_revision = EXCLUDED.next_revision,
|
|
2318
|
+
dropped = EXCLUDED.dropped,
|
|
2319
|
+
capacity = EXCLUDED.capacity,
|
|
2320
|
+
expires_at = EXCLUDED.expires_at
|
|
2321
|
+
RETURNING tenant_id, task_id, entries, next_revision, dropped, capacity, expires_at`,
|
|
2322
|
+
[tenant, input.taskId, JSON.stringify(entries), revision + 1, dropped, capacity, expiresAt]
|
|
2323
|
+
);
|
|
2324
|
+
await client.query("COMMIT");
|
|
2325
|
+
return { status: "applied", tail: toTail2(result.rows[0]) };
|
|
2326
|
+
} catch (caught) {
|
|
2327
|
+
await client.query("ROLLBACK");
|
|
2328
|
+
throw caught;
|
|
2329
|
+
} finally {
|
|
2330
|
+
client.release();
|
|
2331
|
+
}
|
|
2332
|
+
}
|
|
1911
2333
|
async read(tenant, taskId) {
|
|
1912
2334
|
const result = await this.pool.query(
|
|
1913
2335
|
`SELECT tenant_id, task_id, entries, next_revision, dropped, capacity, expires_at
|
|
@@ -1919,6 +2341,19 @@ var PostgresApprovalTimelineStore = class {
|
|
|
1919
2341
|
return row === void 0 ? void 0 : toTail2(row);
|
|
1920
2342
|
}
|
|
1921
2343
|
};
|
|
2344
|
+
function pendingObservation(entries) {
|
|
2345
|
+
let pending;
|
|
2346
|
+
for (const entry of entries) {
|
|
2347
|
+
if (entry.event.type === "approval_requested") {
|
|
2348
|
+
pending = entry;
|
|
2349
|
+
continue;
|
|
2350
|
+
}
|
|
2351
|
+
if (pending !== void 0 && (pending.event.approvalId === void 0 || pending.event.approvalId === entry.event.approvalId)) {
|
|
2352
|
+
pending = void 0;
|
|
2353
|
+
}
|
|
2354
|
+
}
|
|
2355
|
+
return pending;
|
|
2356
|
+
}
|
|
1922
2357
|
var SELECT_COLUMNS4 = [
|
|
1923
2358
|
"tenant_id",
|
|
1924
2359
|
"device_id",
|
|
@@ -1965,7 +2400,7 @@ var PostgresAgentEgressStore = class {
|
|
|
1965
2400
|
const inserted = await this.pool.query(
|
|
1966
2401
|
`INSERT INTO agent_egress_event (${SELECT_COLUMNS4})
|
|
1967
2402
|
VALUES ($1, $2, $3::uuid, $4, $5, $6, $7, $8::bigint, $9::jsonb, $10, $11::integer, $12::uuid, $13)
|
|
1968
|
-
ON CONFLICT (tenant_id, device_id, event_id) DO NOTHING
|
|
2403
|
+
ON CONFLICT (tenant_id, device_id, agent_id, agent_profile_revision, event_id) DO NOTHING
|
|
1969
2404
|
RETURNING ${SELECT_COLUMNS4}`,
|
|
1970
2405
|
[
|
|
1971
2406
|
tenant,
|
|
@@ -1985,16 +2420,18 @@ var PostgresAgentEgressStore = class {
|
|
|
1985
2420
|
);
|
|
1986
2421
|
const row = inserted.rows[0];
|
|
1987
2422
|
if (row !== void 0) return { record: toRecord2(row), created: true };
|
|
1988
|
-
const existing = await this.get(tenant, input.deviceId, payload.eventId);
|
|
2423
|
+
const existing = await this.get(tenant, input.deviceId, payload.agentRef, payload.eventId);
|
|
1989
2424
|
if (existing === void 0) throw new Error(`Agent egress ${payload.eventId} vanished during first-write record.`);
|
|
1990
2425
|
return { record: existing, created: false };
|
|
1991
2426
|
}
|
|
1992
|
-
async get(tenant, deviceId, eventId) {
|
|
2427
|
+
async get(tenant, deviceId, agentRef, eventId) {
|
|
1993
2428
|
const result = await this.pool.query(
|
|
1994
2429
|
`SELECT ${SELECT_COLUMNS4}
|
|
1995
2430
|
FROM agent_egress_event
|
|
1996
|
-
WHERE tenant_id = $1 AND device_id = $2
|
|
1997
|
-
|
|
2431
|
+
WHERE tenant_id = $1 AND device_id = $2
|
|
2432
|
+
AND agent_id = $3 AND agent_profile_revision = $4
|
|
2433
|
+
AND event_id = $5::uuid`,
|
|
2434
|
+
[tenant, deviceId, agentRef.agentId, agentRef.profileRevision, eventId]
|
|
1998
2435
|
);
|
|
1999
2436
|
const row = result.rows[0];
|
|
2000
2437
|
return row === void 0 ? void 0 : toRecord2(row);
|
|
@@ -2151,7 +2588,7 @@ var PostgresAgentMemoryProjectionStore = class {
|
|
|
2151
2588
|
}
|
|
2152
2589
|
});
|
|
2153
2590
|
} catch (cause) {
|
|
2154
|
-
await
|
|
2591
|
+
await rollback3(client);
|
|
2155
2592
|
throw cause;
|
|
2156
2593
|
} finally {
|
|
2157
2594
|
client.release();
|
|
@@ -2188,7 +2625,7 @@ var PostgresAgentMemoryProjectionStore = class {
|
|
|
2188
2625
|
await client.query("COMMIT");
|
|
2189
2626
|
return AgentMemoryProjectionEraseResultSchema.parse({ nextWriterEpoch });
|
|
2190
2627
|
} catch (cause) {
|
|
2191
|
-
await
|
|
2628
|
+
await rollback3(client);
|
|
2192
2629
|
throw cause;
|
|
2193
2630
|
} finally {
|
|
2194
2631
|
client.release();
|
|
@@ -2325,7 +2762,7 @@ function toBase64(bytes) {
|
|
|
2325
2762
|
function toBase64Url(bytes) {
|
|
2326
2763
|
return toBase64(bytes).replaceAll("+", "-").replaceAll("/", "_").replace(/=+$/u, "");
|
|
2327
2764
|
}
|
|
2328
|
-
async function
|
|
2765
|
+
async function rollback3(client) {
|
|
2329
2766
|
try {
|
|
2330
2767
|
await client.query("ROLLBACK");
|
|
2331
2768
|
} catch {
|
|
@@ -2384,11 +2821,16 @@ var PostgresDeviceAssertionReplayAuthority = class {
|
|
|
2384
2821
|
// src/stores/index.ts
|
|
2385
2822
|
function createPostgresCloudStores(options) {
|
|
2386
2823
|
const { pool, clock, crypto } = options;
|
|
2824
|
+
const devices = new PostgresDeviceDirectory(pool, clock);
|
|
2825
|
+
const pairing = new PostgresPairingCodeStore(pool, clock);
|
|
2387
2826
|
return {
|
|
2388
2827
|
activity: new PostgresActivityStore(pool, clock),
|
|
2389
2828
|
approvals: new PostgresApprovalTimelineStore(pool, clock),
|
|
2390
|
-
devices
|
|
2391
|
-
|
|
2829
|
+
devices,
|
|
2830
|
+
// One durable code authority, projected into issuance and enrollment ports
|
|
2831
|
+
// so consumers cannot reopen the old redeem-then-register sequence.
|
|
2832
|
+
pairingCodes: { issue: pairing.issue.bind(pairing) },
|
|
2833
|
+
pairing: { redeemAndRegister: pairing.redeemAndRegister.bind(pairing) },
|
|
2392
2834
|
nonces: new PostgresNonceStore(pool, clock, crypto),
|
|
2393
2835
|
dedup: new PostgresInboundDedupStore(pool),
|
|
2394
2836
|
tasks: new PostgresTaskAttemptStore(pool, clock),
|
|
@@ -3677,7 +4119,7 @@ function toReceipt4(tenant, row) {
|
|
|
3677
4119
|
recordedAt: row.recorded_at.toISOString()
|
|
3678
4120
|
};
|
|
3679
4121
|
}
|
|
3680
|
-
function
|
|
4122
|
+
function sameBinding2(receipt, input) {
|
|
3681
4123
|
return receipt.operation === input.operation && receipt.resource === input.resource && receipt.bodySha256 === input.proofBodySha256 && receipt.bodySize === input.proofBodySize;
|
|
3682
4124
|
}
|
|
3683
4125
|
function bodyColumns2(body) {
|
|
@@ -3716,7 +4158,7 @@ var PostgresTruthCommitter = class {
|
|
|
3716
4158
|
]);
|
|
3717
4159
|
const replay = await this.#readReceipt(client, tenant, input.deviceId, input.requestId);
|
|
3718
4160
|
if (replay !== void 0) {
|
|
3719
|
-
if (!
|
|
4161
|
+
if (!sameBinding2(replay, input)) {
|
|
3720
4162
|
throw new TruthCommitError(
|
|
3721
4163
|
"proof_request_conflict",
|
|
3722
4164
|
`Request ${input.requestId} was already used with a different binding.`
|
|
@@ -4779,6 +5221,10 @@ var PostgresCloudCleanup = class {
|
|
|
4779
5221
|
), receipts AS (
|
|
4780
5222
|
DELETE FROM device_request_receipts
|
|
4781
5223
|
WHERE tenant_id = $1 AND recorded_at < $5::timestamptz
|
|
5224
|
+
-- Pairing completion is an immutable enrollment authority, not
|
|
5225
|
+
-- a TTL-bound request receipt. Removing it would turn an exact
|
|
5226
|
+
-- bearer-code retry into a new/failed enrollment after cleanup.
|
|
5227
|
+
AND key NOT LIKE 'pairing-completion:v1:%'
|
|
4782
5228
|
RETURNING 1
|
|
4783
5229
|
), presence AS (
|
|
4784
5230
|
DELETE FROM device_presence
|
|
@@ -5309,6 +5755,7 @@ var TENANT_ERASURE_TABLES = [
|
|
|
5309
5755
|
"agent_egress_event",
|
|
5310
5756
|
"device_request_receipts",
|
|
5311
5757
|
"proof_request_receipt",
|
|
5758
|
+
"agent_message_admission",
|
|
5312
5759
|
"task",
|
|
5313
5760
|
"device_presence",
|
|
5314
5761
|
"device_assertion_replay",
|