@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/runtime.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, SKILL_PACK_MANIFEST_SCHEMA_ID } from '@byok-sdk/core';
|
|
4
4
|
import { AwsClient } from 'aws4fetch';
|
|
5
5
|
import { XMLParser } from 'fast-xml-parser';
|
|
@@ -822,6 +822,57 @@ async function rollback(client) {
|
|
|
822
822
|
} catch {
|
|
823
823
|
}
|
|
824
824
|
}
|
|
825
|
+
async function registerDeviceOnClient(client, tenant, input) {
|
|
826
|
+
if (input.machineId !== void 0) {
|
|
827
|
+
const superseded = await client.query(
|
|
828
|
+
`DELETE FROM device
|
|
829
|
+
WHERE tenant_id = $1 AND product_id = $2 AND machine_id = $3 AND device_id <> $4
|
|
830
|
+
RETURNING device_id`,
|
|
831
|
+
[tenant, input.productId, input.machineId, input.deviceId]
|
|
832
|
+
);
|
|
833
|
+
await deleteDeviceState(
|
|
834
|
+
client,
|
|
835
|
+
tenant,
|
|
836
|
+
superseded.rows.map((row) => row.device_id)
|
|
837
|
+
);
|
|
838
|
+
}
|
|
839
|
+
const result = await client.query(
|
|
840
|
+
`INSERT INTO device (
|
|
841
|
+
tenant_id, device_id, product_id, device_name, device_public_key,
|
|
842
|
+
proof_key_id, proof_key_epoch, revoked, machine_id
|
|
843
|
+
)
|
|
844
|
+
VALUES ($1, $2, $3, $4, $5, $6, $7, false, $8)
|
|
845
|
+
ON CONFLICT (tenant_id, device_id) DO UPDATE
|
|
846
|
+
SET product_id = EXCLUDED.product_id,
|
|
847
|
+
device_name = EXCLUDED.device_name,
|
|
848
|
+
device_public_key = EXCLUDED.device_public_key,
|
|
849
|
+
proof_key_id = EXCLUDED.proof_key_id,
|
|
850
|
+
proof_key_epoch = EXCLUDED.proof_key_epoch,
|
|
851
|
+
revoked = false,
|
|
852
|
+
machine_id = EXCLUDED.machine_id,
|
|
853
|
+
capabilities = NULL
|
|
854
|
+
RETURNING ${SELECT_COLUMNS}`,
|
|
855
|
+
[
|
|
856
|
+
tenant,
|
|
857
|
+
input.deviceId,
|
|
858
|
+
input.productId,
|
|
859
|
+
input.deviceName,
|
|
860
|
+
input.devicePublicKey,
|
|
861
|
+
input.proofKeyId,
|
|
862
|
+
input.proofKeyEpoch,
|
|
863
|
+
input.machineId ?? null
|
|
864
|
+
]
|
|
865
|
+
);
|
|
866
|
+
return toRecord(result.rows[0]);
|
|
867
|
+
}
|
|
868
|
+
async function getDeviceOnClient(client, tenant, deviceId) {
|
|
869
|
+
const result = await client.query(
|
|
870
|
+
`SELECT ${SELECT_COLUMNS} FROM device WHERE tenant_id = $1 AND device_id = $2`,
|
|
871
|
+
[tenant, deviceId]
|
|
872
|
+
);
|
|
873
|
+
const row = result.rows[0];
|
|
874
|
+
return row === void 0 ? void 0 : toRecord(row);
|
|
875
|
+
}
|
|
825
876
|
var PostgresDeviceDirectory = class {
|
|
826
877
|
#pool;
|
|
827
878
|
#clock;
|
|
@@ -833,48 +884,9 @@ var PostgresDeviceDirectory = class {
|
|
|
833
884
|
const client = await this.#pool.connect();
|
|
834
885
|
try {
|
|
835
886
|
await client.query("BEGIN");
|
|
836
|
-
|
|
837
|
-
const superseded = await client.query(
|
|
838
|
-
`DELETE FROM device
|
|
839
|
-
WHERE tenant_id = $1 AND product_id = $2 AND machine_id = $3 AND device_id <> $4
|
|
840
|
-
RETURNING device_id`,
|
|
841
|
-
[tenant, input.productId, input.machineId, input.deviceId]
|
|
842
|
-
);
|
|
843
|
-
await deleteDeviceState(
|
|
844
|
-
client,
|
|
845
|
-
tenant,
|
|
846
|
-
superseded.rows.map((row) => row.device_id)
|
|
847
|
-
);
|
|
848
|
-
}
|
|
849
|
-
const result = await client.query(
|
|
850
|
-
`INSERT INTO device (
|
|
851
|
-
tenant_id, device_id, product_id, device_name, device_public_key,
|
|
852
|
-
proof_key_id, proof_key_epoch, revoked, machine_id
|
|
853
|
-
)
|
|
854
|
-
VALUES ($1, $2, $3, $4, $5, $6, $7, false, $8)
|
|
855
|
-
ON CONFLICT (tenant_id, device_id) DO UPDATE
|
|
856
|
-
SET product_id = EXCLUDED.product_id,
|
|
857
|
-
device_name = EXCLUDED.device_name,
|
|
858
|
-
device_public_key = EXCLUDED.device_public_key,
|
|
859
|
-
proof_key_id = EXCLUDED.proof_key_id,
|
|
860
|
-
proof_key_epoch = EXCLUDED.proof_key_epoch,
|
|
861
|
-
revoked = false,
|
|
862
|
-
machine_id = EXCLUDED.machine_id,
|
|
863
|
-
capabilities = NULL
|
|
864
|
-
RETURNING ${SELECT_COLUMNS}`,
|
|
865
|
-
[
|
|
866
|
-
tenant,
|
|
867
|
-
input.deviceId,
|
|
868
|
-
input.productId,
|
|
869
|
-
input.deviceName,
|
|
870
|
-
input.devicePublicKey,
|
|
871
|
-
input.proofKeyId,
|
|
872
|
-
input.proofKeyEpoch,
|
|
873
|
-
input.machineId ?? null
|
|
874
|
-
]
|
|
875
|
-
);
|
|
887
|
+
const record = await registerDeviceOnClient(client, tenant, input);
|
|
876
888
|
await client.query("COMMIT");
|
|
877
|
-
return
|
|
889
|
+
return record;
|
|
878
890
|
} catch (cause) {
|
|
879
891
|
await rollback(client);
|
|
880
892
|
throw cause;
|
|
@@ -883,12 +895,12 @@ var PostgresDeviceDirectory = class {
|
|
|
883
895
|
}
|
|
884
896
|
}
|
|
885
897
|
async get(tenant, deviceId) {
|
|
886
|
-
const
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
898
|
+
const client = await this.#pool.connect();
|
|
899
|
+
try {
|
|
900
|
+
return await getDeviceOnClient(client, tenant, deviceId);
|
|
901
|
+
} finally {
|
|
902
|
+
client.release();
|
|
903
|
+
}
|
|
892
904
|
}
|
|
893
905
|
/**
|
|
894
906
|
* Revocation removes the registration and the device-scoped state that only
|
|
@@ -1022,23 +1034,31 @@ var PostgresInboundDedupStore = class {
|
|
|
1022
1034
|
this.#capacity = capacity;
|
|
1023
1035
|
}
|
|
1024
1036
|
async checkAndRecord(tenant, deviceId, envelopeId) {
|
|
1037
|
+
return this.#checkAndRecord(tenant, deviceId, "", "", envelopeId);
|
|
1038
|
+
}
|
|
1039
|
+
async checkAndRecordAgent(tenant, deviceId, agentRef, envelopeId) {
|
|
1040
|
+
return this.#checkAndRecord(tenant, deviceId, agentRef.agentId, agentRef.profileRevision, envelopeId);
|
|
1041
|
+
}
|
|
1042
|
+
async #checkAndRecord(tenant, deviceId, agentId, agentProfileRevision, envelopeId) {
|
|
1025
1043
|
const inserted = await this.#pool.query(
|
|
1026
|
-
`INSERT INTO inbound_dedup (tenant_id, device_id, envelope_id)
|
|
1027
|
-
VALUES ($1, $2, $3)
|
|
1028
|
-
ON CONFLICT (tenant_id, device_id, envelope_id) DO NOTHING
|
|
1044
|
+
`INSERT INTO inbound_dedup (tenant_id, device_id, agent_id, agent_profile_revision, envelope_id)
|
|
1045
|
+
VALUES ($1, $2, $3, $4, $5)
|
|
1046
|
+
ON CONFLICT (tenant_id, device_id, agent_id, agent_profile_revision, envelope_id) DO NOTHING
|
|
1029
1047
|
RETURNING 1`,
|
|
1030
|
-
[tenant, deviceId, envelopeId]
|
|
1048
|
+
[tenant, deviceId, agentId, agentProfileRevision, envelopeId]
|
|
1031
1049
|
);
|
|
1032
1050
|
if (inserted.rowCount === 0) return true;
|
|
1033
1051
|
await this.#pool.query(
|
|
1034
1052
|
`DELETE FROM inbound_dedup
|
|
1035
1053
|
WHERE tenant_id = $1 AND device_id = $2
|
|
1054
|
+
AND agent_id = $3 AND agent_profile_revision = $4
|
|
1036
1055
|
AND recorded_seq <= (
|
|
1037
1056
|
SELECT recorded_seq FROM inbound_dedup
|
|
1038
1057
|
WHERE tenant_id = $1 AND device_id = $2
|
|
1058
|
+
AND agent_id = $3 AND agent_profile_revision = $4
|
|
1039
1059
|
ORDER BY recorded_seq DESC
|
|
1040
|
-
OFFSET $
|
|
1041
|
-
[tenant, deviceId, this.#capacity]
|
|
1060
|
+
OFFSET $5 LIMIT 1)`,
|
|
1061
|
+
[tenant, deviceId, agentId, agentProfileRevision, this.#capacity]
|
|
1042
1062
|
);
|
|
1043
1063
|
return false;
|
|
1044
1064
|
}
|
|
@@ -1070,24 +1090,58 @@ var PostgresNonceStore = class {
|
|
|
1070
1090
|
);
|
|
1071
1091
|
return nonce;
|
|
1072
1092
|
}
|
|
1073
|
-
async
|
|
1093
|
+
async consumeIfValid(tenant, deviceId, nonce) {
|
|
1074
1094
|
const result = await this.#pool.query(
|
|
1075
|
-
`
|
|
1076
|
-
|
|
1077
|
-
|
|
1095
|
+
`UPDATE auth_nonce
|
|
1096
|
+
SET used = true
|
|
1097
|
+
WHERE tenant_id = $1 AND device_id = $2 AND nonce = $3
|
|
1098
|
+
AND used = false AND expires_at >= $4
|
|
1099
|
+
RETURNING nonce`,
|
|
1078
1100
|
[tenant, deviceId, nonce, this.#clock.now().toISOString()]
|
|
1079
1101
|
);
|
|
1080
1102
|
return result.rowCount === 1;
|
|
1081
1103
|
}
|
|
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
1104
|
};
|
|
1089
1105
|
|
|
1090
1106
|
// src/stores/pairing-codes.ts
|
|
1107
|
+
function bindingOf(input) {
|
|
1108
|
+
return {
|
|
1109
|
+
deviceName: input.deviceName,
|
|
1110
|
+
devicePublicKey: input.devicePublicKey,
|
|
1111
|
+
proofKeyId: input.proofKeyId,
|
|
1112
|
+
proofKeyEpoch: input.proofKeyEpoch,
|
|
1113
|
+
...input.machineId === void 0 ? {} : { machineId: input.machineId }
|
|
1114
|
+
};
|
|
1115
|
+
}
|
|
1116
|
+
async function completionKey(pairingCode) {
|
|
1117
|
+
const digest = new Uint8Array(await globalThis.crypto.subtle.digest(
|
|
1118
|
+
"SHA-256",
|
|
1119
|
+
new TextEncoder().encode(`byok-pairing-completion-v1\0${pairingCode}`)
|
|
1120
|
+
));
|
|
1121
|
+
const hex = [...digest].map((byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
1122
|
+
return `pairing-completion:v1:sha256:${hex}`;
|
|
1123
|
+
}
|
|
1124
|
+
function sameBinding(left, right) {
|
|
1125
|
+
return left.deviceName === right.deviceName && left.devicePublicKey === right.devicePublicKey && left.proofKeyId === right.proofKeyId && left.proofKeyEpoch === right.proofKeyEpoch && left.machineId === right.machineId;
|
|
1126
|
+
}
|
|
1127
|
+
function sameDevice(device, claims, binding) {
|
|
1128
|
+
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;
|
|
1129
|
+
}
|
|
1130
|
+
function parseCompletion(body) {
|
|
1131
|
+
try {
|
|
1132
|
+
const parsed = JSON.parse(body);
|
|
1133
|
+
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;
|
|
1134
|
+
return parsed;
|
|
1135
|
+
} catch {
|
|
1136
|
+
return void 0;
|
|
1137
|
+
}
|
|
1138
|
+
}
|
|
1139
|
+
async function rollback2(client) {
|
|
1140
|
+
try {
|
|
1141
|
+
await client.query("ROLLBACK");
|
|
1142
|
+
} catch {
|
|
1143
|
+
}
|
|
1144
|
+
}
|
|
1091
1145
|
var PostgresPairingCodeStore = class {
|
|
1092
1146
|
#pool;
|
|
1093
1147
|
#clock;
|
|
@@ -1096,30 +1150,102 @@ var PostgresPairingCodeStore = class {
|
|
|
1096
1150
|
this.#clock = clock;
|
|
1097
1151
|
}
|
|
1098
1152
|
async issue(tenant, input) {
|
|
1099
|
-
await this.#pool.
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1153
|
+
const client = await this.#pool.connect();
|
|
1154
|
+
try {
|
|
1155
|
+
await client.query("BEGIN");
|
|
1156
|
+
const previous = await client.query(
|
|
1157
|
+
"SELECT tenant_id FROM pairing_code WHERE code = $1 FOR UPDATE",
|
|
1158
|
+
[input.code]
|
|
1159
|
+
);
|
|
1160
|
+
const key = await completionKey(input.code);
|
|
1161
|
+
const previousTenant = previous.rows[0]?.tenant_id;
|
|
1162
|
+
if (previousTenant !== void 0) {
|
|
1163
|
+
await client.query(
|
|
1164
|
+
"DELETE FROM device_request_receipts WHERE tenant_id = $1 AND key = $2",
|
|
1165
|
+
[previousTenant, key]
|
|
1166
|
+
);
|
|
1167
|
+
}
|
|
1168
|
+
await client.query(
|
|
1169
|
+
`INSERT INTO pairing_code (code, tenant_id, product_id, expires_at, redeemed_at)
|
|
1170
|
+
VALUES ($1, $2, $3, $4, NULL)
|
|
1171
|
+
ON CONFLICT (code) DO UPDATE
|
|
1172
|
+
SET tenant_id = EXCLUDED.tenant_id,
|
|
1173
|
+
product_id = EXCLUDED.product_id,
|
|
1174
|
+
expires_at = EXCLUDED.expires_at,
|
|
1175
|
+
redeemed_at = NULL`,
|
|
1176
|
+
[input.code, tenant, input.productId, input.expiresAt]
|
|
1177
|
+
);
|
|
1178
|
+
await client.query("COMMIT");
|
|
1179
|
+
} catch (cause) {
|
|
1180
|
+
await rollback2(client);
|
|
1181
|
+
throw cause;
|
|
1182
|
+
} finally {
|
|
1183
|
+
client.release();
|
|
1184
|
+
}
|
|
1109
1185
|
return { code: input.code, expiresAt: input.expiresAt };
|
|
1110
1186
|
}
|
|
1111
|
-
async
|
|
1112
|
-
const now = this.#clock.now()
|
|
1113
|
-
const
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1187
|
+
async redeemAndRegister(input) {
|
|
1188
|
+
const now = this.#clock.now();
|
|
1189
|
+
const client = await this.#pool.connect();
|
|
1190
|
+
try {
|
|
1191
|
+
await client.query("BEGIN");
|
|
1192
|
+
const selected = await client.query(
|
|
1193
|
+
`SELECT tenant_id, product_id, expires_at, redeemed_at
|
|
1194
|
+
FROM pairing_code WHERE code = $1 FOR UPDATE`,
|
|
1195
|
+
[input.pairingCode]
|
|
1196
|
+
);
|
|
1197
|
+
const claims = selected.rows[0];
|
|
1198
|
+
if (claims === void 0) {
|
|
1199
|
+
await client.query("COMMIT");
|
|
1200
|
+
return void 0;
|
|
1201
|
+
}
|
|
1202
|
+
const tenant = claims.tenant_id;
|
|
1203
|
+
const binding = bindingOf(input);
|
|
1204
|
+
if (claims.redeemed_at !== null) {
|
|
1205
|
+
const stored = await client.query(
|
|
1206
|
+
"SELECT body FROM device_request_receipts WHERE tenant_id = $1 AND key = $2",
|
|
1207
|
+
[tenant, await completionKey(input.pairingCode)]
|
|
1208
|
+
);
|
|
1209
|
+
const completion2 = stored.rows[0] === void 0 ? void 0 : parseCompletion(stored.rows[0].body);
|
|
1210
|
+
if (completion2 === void 0 || !sameBinding(completion2.binding, binding)) {
|
|
1211
|
+
await client.query("COMMIT");
|
|
1212
|
+
return void 0;
|
|
1213
|
+
}
|
|
1214
|
+
const device2 = await getDeviceOnClient(client, tenant, completion2.deviceId);
|
|
1215
|
+
await client.query("COMMIT");
|
|
1216
|
+
return device2 === void 0 || !sameDevice(device2, claims, binding) ? void 0 : device2;
|
|
1217
|
+
}
|
|
1218
|
+
if (claims.expires_at.getTime() < now.getTime()) {
|
|
1219
|
+
await client.query("COMMIT");
|
|
1220
|
+
return void 0;
|
|
1221
|
+
}
|
|
1222
|
+
await client.query(
|
|
1223
|
+
"UPDATE pairing_code SET redeemed_at = $2 WHERE code = $1 AND redeemed_at IS NULL",
|
|
1224
|
+
[input.pairingCode, now.toISOString()]
|
|
1225
|
+
);
|
|
1226
|
+
const device = await registerDeviceOnClient(client, tenant, {
|
|
1227
|
+
productId: claims.product_id,
|
|
1228
|
+
deviceId: input.deviceId,
|
|
1229
|
+
deviceName: input.deviceName,
|
|
1230
|
+
devicePublicKey: input.devicePublicKey,
|
|
1231
|
+
proofKeyId: input.proofKeyId,
|
|
1232
|
+
proofKeyEpoch: input.proofKeyEpoch,
|
|
1233
|
+
...input.machineId === void 0 ? {} : { machineId: input.machineId }
|
|
1234
|
+
});
|
|
1235
|
+
const completion = { deviceId: device.deviceId, binding };
|
|
1236
|
+
await client.query(
|
|
1237
|
+
`INSERT INTO device_request_receipts (tenant_id, key, body, recorded_at)
|
|
1238
|
+
VALUES ($1, $2, $3, $4)`,
|
|
1239
|
+
[tenant, await completionKey(input.pairingCode), JSON.stringify(completion), now.toISOString()]
|
|
1240
|
+
);
|
|
1241
|
+
await client.query("COMMIT");
|
|
1242
|
+
return device;
|
|
1243
|
+
} catch (cause) {
|
|
1244
|
+
await rollback2(client);
|
|
1245
|
+
throw cause;
|
|
1246
|
+
} finally {
|
|
1247
|
+
client.release();
|
|
1248
|
+
}
|
|
1123
1249
|
}
|
|
1124
1250
|
};
|
|
1125
1251
|
|
|
@@ -1226,9 +1352,15 @@ var PostgresProofRequestReceiptStore = class {
|
|
|
1226
1352
|
return row === void 0 ? void 0 : toReceipt2(row);
|
|
1227
1353
|
}
|
|
1228
1354
|
};
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1355
|
+
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";
|
|
1356
|
+
var AGENT_MESSAGE_ADMISSION_SELECT_COLUMNS = "message_id, payload_body, terminal_body";
|
|
1357
|
+
function admissionRowToRecord(row) {
|
|
1358
|
+
return {
|
|
1359
|
+
messageId: row.message_id,
|
|
1360
|
+
payloadBody: row.payload_body,
|
|
1361
|
+
...row.terminal_body === null ? {} : { terminalBody: row.terminal_body }
|
|
1362
|
+
};
|
|
1363
|
+
}
|
|
1232
1364
|
function taskRowToAttempt(row) {
|
|
1233
1365
|
return {
|
|
1234
1366
|
tenantId: row.tenant_id,
|
|
@@ -1246,6 +1378,10 @@ function taskRowToAttempt(row) {
|
|
|
1246
1378
|
// former.
|
|
1247
1379
|
...row.owner_device_id === null ? {} : { ownerDeviceId: row.owner_device_id },
|
|
1248
1380
|
status: row.status,
|
|
1381
|
+
// `claimed_runtime` is written only by the guarded claim UPDATE below, so a
|
|
1382
|
+
// non-null value here is always the winning claim's own self-report.
|
|
1383
|
+
...row.claimed_runtime === null ? {} : { claimedRuntime: row.claimed_runtime },
|
|
1384
|
+
...row.claimed_runtime_capabilities === null ? {} : { claimedRuntimeCapabilities: row.claimed_runtime_capabilities },
|
|
1249
1385
|
...row.terminal_cause == null ? {} : { terminalCause: row.terminal_cause },
|
|
1250
1386
|
...row.cancel_requested_at === null ? {} : {
|
|
1251
1387
|
cancellation: {
|
|
@@ -1304,6 +1440,80 @@ var PostgresTaskAttemptStore = class {
|
|
|
1304
1440
|
if (existing === void 0) throw new Error(`task ${input.taskId} vanished during Agent offer reservation`);
|
|
1305
1441
|
return { attempt: existing, created: false };
|
|
1306
1442
|
}
|
|
1443
|
+
async reserveAgentMessage(tenant, input) {
|
|
1444
|
+
const client = await this.#pool.connect();
|
|
1445
|
+
try {
|
|
1446
|
+
await client.query("BEGIN");
|
|
1447
|
+
const selected = await client.query(
|
|
1448
|
+
`SELECT ${TASK_SELECT_COLUMNS} FROM task
|
|
1449
|
+
WHERE tenant_id = $1 AND task_id = $2 FOR UPDATE`,
|
|
1450
|
+
[tenant, input.taskId]
|
|
1451
|
+
);
|
|
1452
|
+
const attempt = selected.rows[0];
|
|
1453
|
+
const existing = await client.query(
|
|
1454
|
+
`SELECT message_id, payload_body FROM agent_message_admission
|
|
1455
|
+
WHERE tenant_id = $1 AND device_id = $2 AND task_id = $3`,
|
|
1456
|
+
[tenant, input.deviceId, input.taskId]
|
|
1457
|
+
);
|
|
1458
|
+
const admission = existing.rows[0];
|
|
1459
|
+
if (admission !== void 0) {
|
|
1460
|
+
await client.query("COMMIT");
|
|
1461
|
+
return admission.message_id === input.messageId && admission.payload_body === input.payloadBody ? "pending" : "rejected";
|
|
1462
|
+
}
|
|
1463
|
+
if (attempt === void 0 || attempt.device_id !== input.deviceId || attempt.cancel_requested_at !== null || !["offered", "claimed", "running"].includes(attempt.status)) {
|
|
1464
|
+
await client.query("COMMIT");
|
|
1465
|
+
return "rejected";
|
|
1466
|
+
}
|
|
1467
|
+
const inserted = await client.query(
|
|
1468
|
+
`INSERT INTO agent_message_admission (
|
|
1469
|
+
tenant_id, device_id, task_id, message_id, payload_body
|
|
1470
|
+
) VALUES ($1, $2, $3, $4, $5)
|
|
1471
|
+
ON CONFLICT (tenant_id, device_id, task_id) DO NOTHING
|
|
1472
|
+
RETURNING payload_body`,
|
|
1473
|
+
[tenant, input.deviceId, input.taskId, input.messageId, input.payloadBody]
|
|
1474
|
+
);
|
|
1475
|
+
if (inserted.rows[0] !== void 0) {
|
|
1476
|
+
await client.query("COMMIT");
|
|
1477
|
+
return "reserved";
|
|
1478
|
+
}
|
|
1479
|
+
const raced = await client.query(
|
|
1480
|
+
`SELECT message_id, payload_body 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 racedAdmission = raced.rows[0];
|
|
1485
|
+
await client.query("COMMIT");
|
|
1486
|
+
return racedAdmission?.message_id === input.messageId && racedAdmission.payload_body === input.payloadBody ? "pending" : "rejected";
|
|
1487
|
+
} catch (cause) {
|
|
1488
|
+
await client.query("ROLLBACK").catch(() => {
|
|
1489
|
+
});
|
|
1490
|
+
throw cause;
|
|
1491
|
+
} finally {
|
|
1492
|
+
client.release();
|
|
1493
|
+
}
|
|
1494
|
+
}
|
|
1495
|
+
async readAgentMessage(tenant, input) {
|
|
1496
|
+
const result = await this.#pool.query(
|
|
1497
|
+
`SELECT ${AGENT_MESSAGE_ADMISSION_SELECT_COLUMNS}
|
|
1498
|
+
FROM agent_message_admission
|
|
1499
|
+
WHERE tenant_id = $1 AND device_id = $2 AND task_id = $3`,
|
|
1500
|
+
[tenant, input.deviceId, input.taskId]
|
|
1501
|
+
);
|
|
1502
|
+
const row = result.rows[0];
|
|
1503
|
+
return row !== void 0 && row.message_id === input.messageId && row.payload_body === input.payloadBody ? admissionRowToRecord(row) : void 0;
|
|
1504
|
+
}
|
|
1505
|
+
async finalizeAgentMessage(tenant, input) {
|
|
1506
|
+
const terminalized = await this.#pool.query(
|
|
1507
|
+
`UPDATE agent_message_admission
|
|
1508
|
+
SET terminal_body = $6
|
|
1509
|
+
WHERE tenant_id = $1 AND device_id = $2 AND task_id = $3
|
|
1510
|
+
AND message_id = $4 AND payload_body = $5 AND terminal_body IS NULL
|
|
1511
|
+
RETURNING ${AGENT_MESSAGE_ADMISSION_SELECT_COLUMNS}`,
|
|
1512
|
+
[tenant, input.deviceId, input.taskId, input.messageId, input.payloadBody, input.terminalBody]
|
|
1513
|
+
);
|
|
1514
|
+
const row = terminalized.rows[0];
|
|
1515
|
+
return row === void 0 ? this.readAgentMessage(tenant, input) : admissionRowToRecord(row);
|
|
1516
|
+
}
|
|
1307
1517
|
async get(tenant, taskId) {
|
|
1308
1518
|
const result = await this.#pool.query(
|
|
1309
1519
|
`SELECT ${TASK_SELECT_COLUMNS} FROM task WHERE tenant_id = $1 AND task_id = $2`,
|
|
@@ -1320,14 +1530,44 @@ var PostgresTaskAttemptStore = class {
|
|
|
1320
1530
|
);
|
|
1321
1531
|
return result.rows.map(taskRowToAttempt);
|
|
1322
1532
|
}
|
|
1533
|
+
async list(tenant, query) {
|
|
1534
|
+
assertTaskAttemptListLimit(query.limit);
|
|
1535
|
+
const result = await this.#pool.query(
|
|
1536
|
+
`SELECT ${TASK_SELECT_COLUMNS}
|
|
1537
|
+
FROM task
|
|
1538
|
+
WHERE tenant_id = $1 AND ($2::text IS NULL OR task_id > $2)
|
|
1539
|
+
ORDER BY task_id
|
|
1540
|
+
LIMIT $3`,
|
|
1541
|
+
[tenant, query.cursor ?? null, query.limit + 1]
|
|
1542
|
+
);
|
|
1543
|
+
const rows = result.rows.slice(0, query.limit);
|
|
1544
|
+
const attempts = rows.map(taskRowToAttempt);
|
|
1545
|
+
const hasMore = result.rows.length > rows.length;
|
|
1546
|
+
const last = attempts.at(-1);
|
|
1547
|
+
return {
|
|
1548
|
+
attempts,
|
|
1549
|
+
...hasMore && last !== void 0 ? { nextCursor: last.taskId } : {}
|
|
1550
|
+
};
|
|
1551
|
+
}
|
|
1323
1552
|
async claim(tenant, input) {
|
|
1324
1553
|
const claimed = await this.#pool.query(
|
|
1325
1554
|
`UPDATE task
|
|
1326
|
-
SET owner_device_id = $3,
|
|
1555
|
+
SET owner_device_id = $3,
|
|
1556
|
+
status = 'claimed',
|
|
1557
|
+
claimed_runtime = $5,
|
|
1558
|
+
claimed_runtime_capabilities = $6::jsonb,
|
|
1559
|
+
updated_at = $4
|
|
1327
1560
|
WHERE tenant_id = $1 AND task_id = $2 AND owner_device_id IS NULL
|
|
1328
1561
|
AND cancel_requested_at IS NULL AND status = 'offered'
|
|
1329
1562
|
RETURNING ${TASK_SELECT_COLUMNS}`,
|
|
1330
|
-
[
|
|
1563
|
+
[
|
|
1564
|
+
tenant,
|
|
1565
|
+
input.taskId,
|
|
1566
|
+
input.deviceId,
|
|
1567
|
+
this.#now(),
|
|
1568
|
+
input.runtime ?? null,
|
|
1569
|
+
input.capabilities === void 0 ? null : JSON.stringify(input.capabilities)
|
|
1570
|
+
]
|
|
1331
1571
|
);
|
|
1332
1572
|
const won = claimed.rows[0];
|
|
1333
1573
|
if (won !== void 0) return taskRowToAttempt(won);
|
|
@@ -1492,20 +1732,63 @@ var PostgresMailboxStore = class {
|
|
|
1492
1732
|
// Nothing above was mutated, so an identical call replays the same page.
|
|
1493
1733
|
// The returned position is a READ cursor and moves no ack.
|
|
1494
1734
|
nextSeq: page.at(-1)?.seq ?? query.afterSeq,
|
|
1495
|
-
hasMore: result.rows.length > page.length
|
|
1735
|
+
hasMore: result.rows.length > page.length,
|
|
1736
|
+
recoverableFrom: await this.#recoverableFrom(tenant, query.deviceId)
|
|
1496
1737
|
};
|
|
1497
1738
|
}
|
|
1739
|
+
/**
|
|
1740
|
+
* One past the highest row this device lost, which is exactly the highest
|
|
1741
|
+
* `expired` seq — `1` when it has lost nothing.
|
|
1742
|
+
*
|
|
1743
|
+
* No column and no migration, because `collectRetired` never deletes an
|
|
1744
|
+
* unacked row: §12.7.5 keeps the dead letter, so the loss is already durable
|
|
1745
|
+
* in `outbox` and a stored floor would be a second authority free to drift
|
|
1746
|
+
* from the sweep that moved it. Whatever eventually reaps dead letters (S4B,
|
|
1747
|
+
* O-009) is the change that has to carry this floor forward instead.
|
|
1748
|
+
*
|
|
1749
|
+
* `acked` rows are not counted: a consumed row is not a lost one.
|
|
1750
|
+
*
|
|
1751
|
+
* Read AFTER the page on purpose. A sweep landing between the two reads can
|
|
1752
|
+
* only report a floor HIGHER than the page it accompanies, which makes the
|
|
1753
|
+
* caller fail closed over a page that may already be missing rows; reading
|
|
1754
|
+
* the floor first would report one too low and serve that gap silently.
|
|
1755
|
+
*/
|
|
1756
|
+
async #recoverableFrom(tenant, deviceId) {
|
|
1757
|
+
const result = await this.#pool.query(
|
|
1758
|
+
`SELECT (COALESCE(MAX(seq), 0) + 1)::bigint AS recoverable_from
|
|
1759
|
+
FROM outbox
|
|
1760
|
+
WHERE tenant_id = $1 AND device_id = $2 AND state = 'expired'`,
|
|
1761
|
+
[tenant, deviceId]
|
|
1762
|
+
);
|
|
1763
|
+
return Number(result.rows[0].recoverable_from);
|
|
1764
|
+
}
|
|
1498
1765
|
async advanceCursor(tenant, input) {
|
|
1499
1766
|
this.#requireDeviceId(input.deviceId);
|
|
1500
1767
|
const now = this.#now();
|
|
1501
1768
|
const moved = await this.#pool.query(
|
|
1502
|
-
`WITH
|
|
1503
|
-
INSERT INTO device_stream (
|
|
1504
|
-
|
|
1769
|
+
`WITH moved_zero AS (
|
|
1770
|
+
INSERT INTO device_stream (
|
|
1771
|
+
tenant_id, device_id, next_seq, acked_seq, acked_at, delivered_seq
|
|
1772
|
+
)
|
|
1773
|
+
SELECT $1, $2, 1, 0, $4, 0
|
|
1774
|
+
WHERE $3::bigint = 0
|
|
1505
1775
|
ON CONFLICT (tenant_id, device_id) DO UPDATE
|
|
1506
|
-
SET acked_seq =
|
|
1507
|
-
WHERE device_stream.acked_seq <=
|
|
1508
|
-
|
|
1776
|
+
SET acked_seq = 0, acked_at = EXCLUDED.acked_at
|
|
1777
|
+
WHERE device_stream.acked_seq <= 0
|
|
1778
|
+
AND device_stream.delivered_seq >= 0
|
|
1779
|
+
RETURNING acked_seq, acked_at, delivered_seq
|
|
1780
|
+
), moved_positive AS (
|
|
1781
|
+
UPDATE device_stream
|
|
1782
|
+
SET acked_seq = $3::bigint, acked_at = $4
|
|
1783
|
+
WHERE tenant_id = $1 AND device_id = $2
|
|
1784
|
+
AND $3::bigint > 0
|
|
1785
|
+
AND acked_seq <= $3::bigint
|
|
1786
|
+
AND delivered_seq >= $3::bigint
|
|
1787
|
+
RETURNING acked_seq, acked_at, delivered_seq
|
|
1788
|
+
), moved AS (
|
|
1789
|
+
SELECT acked_seq, acked_at, delivered_seq FROM moved_zero
|
|
1790
|
+
UNION ALL
|
|
1791
|
+
SELECT acked_seq, acked_at, delivered_seq FROM moved_positive
|
|
1509
1792
|
), marked AS (
|
|
1510
1793
|
UPDATE outbox
|
|
1511
1794
|
SET state = 'acked'
|
|
@@ -1513,7 +1796,7 @@ var PostgresMailboxStore = class {
|
|
|
1513
1796
|
AND seq <= (SELECT acked_seq FROM moved)
|
|
1514
1797
|
RETURNING 1
|
|
1515
1798
|
)
|
|
1516
|
-
SELECT acked_seq, acked_at FROM moved`,
|
|
1799
|
+
SELECT acked_seq, acked_at, delivered_seq FROM moved`,
|
|
1517
1800
|
[tenant, input.deviceId, input.ackedSeq, now]
|
|
1518
1801
|
);
|
|
1519
1802
|
const row = moved.rows[0];
|
|
@@ -1521,21 +1804,51 @@ var PostgresMailboxStore = class {
|
|
|
1521
1804
|
return {
|
|
1522
1805
|
tenantId: tenant,
|
|
1523
1806
|
deviceId: input.deviceId,
|
|
1807
|
+
deliveredSeq: Number(row.delivered_seq),
|
|
1524
1808
|
ackedSeq: Number(row.acked_seq),
|
|
1525
1809
|
updatedAt: row.acked_at ?? now
|
|
1526
1810
|
};
|
|
1527
1811
|
}
|
|
1528
1812
|
const current = await this.readCursor(tenant, input.deviceId);
|
|
1813
|
+
if (input.ackedSeq < current.ackedSeq) {
|
|
1814
|
+
throw new CoreConflictError(
|
|
1815
|
+
"mailbox_cursor_regression",
|
|
1816
|
+
`Cursor for device ${input.deviceId} is at ${current.ackedSeq}; refusing to move it back to ${input.ackedSeq}.`,
|
|
1817
|
+
current,
|
|
1818
|
+
this.#now()
|
|
1819
|
+
);
|
|
1820
|
+
}
|
|
1529
1821
|
throw new CoreConflictError(
|
|
1530
|
-
"
|
|
1531
|
-
`Cursor for device ${input.deviceId}
|
|
1822
|
+
"mailbox_cursor_ahead_of_delivery",
|
|
1823
|
+
`Cursor for device ${input.deviceId} was delivered through ${current.deliveredSeq}; refusing to acknowledge future cursor ${input.ackedSeq}.`,
|
|
1532
1824
|
current,
|
|
1533
1825
|
this.#now()
|
|
1534
1826
|
);
|
|
1535
1827
|
}
|
|
1828
|
+
async recordDelivery(tenant, input) {
|
|
1829
|
+
this.#requireDeviceId(input.deviceId);
|
|
1830
|
+
const recorded = await this.#pool.query(
|
|
1831
|
+
`INSERT INTO device_stream (
|
|
1832
|
+
tenant_id, device_id, next_seq, acked_seq, acked_at, delivered_seq
|
|
1833
|
+
)
|
|
1834
|
+
VALUES ($1, $2, 1, 0, NULL, $3::bigint)
|
|
1835
|
+
ON CONFLICT (tenant_id, device_id) DO UPDATE
|
|
1836
|
+
SET delivered_seq = GREATEST(device_stream.delivered_seq, EXCLUDED.delivered_seq)
|
|
1837
|
+
RETURNING acked_seq, acked_at, delivered_seq`,
|
|
1838
|
+
[tenant, input.deviceId, input.deliveredSeq]
|
|
1839
|
+
);
|
|
1840
|
+
const row = recorded.rows[0];
|
|
1841
|
+
return {
|
|
1842
|
+
tenantId: tenant,
|
|
1843
|
+
deviceId: input.deviceId,
|
|
1844
|
+
deliveredSeq: Number(row.delivered_seq),
|
|
1845
|
+
ackedSeq: Number(row.acked_seq),
|
|
1846
|
+
updatedAt: row.acked_at ?? this.#now()
|
|
1847
|
+
};
|
|
1848
|
+
}
|
|
1536
1849
|
async readCursor(tenant, deviceId) {
|
|
1537
1850
|
const result = await this.#pool.query(
|
|
1538
|
-
`SELECT acked_seq, acked_at FROM device_stream
|
|
1851
|
+
`SELECT acked_seq, acked_at, delivered_seq FROM device_stream
|
|
1539
1852
|
WHERE tenant_id = $1 AND device_id = $2`,
|
|
1540
1853
|
[tenant, deviceId]
|
|
1541
1854
|
);
|
|
@@ -1543,6 +1856,7 @@ var PostgresMailboxStore = class {
|
|
|
1543
1856
|
return {
|
|
1544
1857
|
tenantId: tenant,
|
|
1545
1858
|
deviceId,
|
|
1859
|
+
deliveredSeq: row === void 0 ? 0 : Number(row.delivered_seq),
|
|
1546
1860
|
ackedSeq: row === void 0 ? 0 : Number(row.acked_seq),
|
|
1547
1861
|
updatedAt: row?.acked_at ?? this.#now()
|
|
1548
1862
|
};
|
|
@@ -1708,8 +2022,35 @@ var PostgresActivityStore = class {
|
|
|
1708
2022
|
const receivedAt = now.toISOString();
|
|
1709
2023
|
const incoming = projectTimelineEvents(input, receivedAt);
|
|
1710
2024
|
const expiresAt = new Date(now.getTime() + input.ttlMs).toISOString();
|
|
1711
|
-
const
|
|
1712
|
-
|
|
2025
|
+
const client = await this.pool.connect();
|
|
2026
|
+
try {
|
|
2027
|
+
await client.query("BEGIN");
|
|
2028
|
+
await client.query(
|
|
2029
|
+
`SELECT pg_advisory_xact_lock(hashtextextended($1 || E'\\x1f' || $2, 0))`,
|
|
2030
|
+
[tenant, input.taskId]
|
|
2031
|
+
);
|
|
2032
|
+
const locked = await client.query(
|
|
2033
|
+
`SELECT tenant_id, task_id, entries, dropped, capacity, expires_at
|
|
2034
|
+
FROM activity_tail
|
|
2035
|
+
WHERE tenant_id = $1 AND task_id = $2
|
|
2036
|
+
FOR UPDATE`,
|
|
2037
|
+
[tenant, input.taskId]
|
|
2038
|
+
);
|
|
2039
|
+
const stored = locked.rows[0];
|
|
2040
|
+
const live = stored !== void 0 && receivedAt < stored.expires_at ? toTail(stored) : void 0;
|
|
2041
|
+
const sourceState = activitySourceBatchState(live?.entries ?? [], input);
|
|
2042
|
+
if (sourceState === "same") {
|
|
2043
|
+
await client.query("COMMIT");
|
|
2044
|
+
return live;
|
|
2045
|
+
}
|
|
2046
|
+
if (sourceState === "conflict") {
|
|
2047
|
+
throw new ByokCloudError(
|
|
2048
|
+
"coordination_input_invalid",
|
|
2049
|
+
`Activity source envelope ${input.sourceEnvelopeId} already belongs to another canonical batch.`
|
|
2050
|
+
);
|
|
2051
|
+
}
|
|
2052
|
+
const result = await client.query(
|
|
2053
|
+
`WITH incoming AS (
|
|
1713
2054
|
SELECT entry
|
|
1714
2055
|
FROM jsonb_array_elements($4::jsonb) AS element(entry)
|
|
1715
2056
|
), trimmed AS (
|
|
@@ -1776,16 +2117,24 @@ var PostgresActivityStore = class {
|
|
|
1776
2117
|
)
|
|
1777
2118
|
)
|
|
1778
2119
|
RETURNING tenant_id, task_id, entries, dropped, capacity, expires_at`,
|
|
1779
|
-
|
|
1780
|
-
);
|
|
1781
|
-
const row = result.rows[0];
|
|
1782
|
-
if (row === void 0) {
|
|
1783
|
-
throw new ByokCloudError(
|
|
1784
|
-
"coordination_input_invalid",
|
|
1785
|
-
`Activity batch ${input.batchSeq} conflicts with the existing typed tail authority.`
|
|
2120
|
+
[tenant, input.taskId, receivedAt, JSON.stringify(incoming), input.dropped, capacity, expiresAt]
|
|
1786
2121
|
);
|
|
2122
|
+
const row = result.rows[0];
|
|
2123
|
+
if (row === void 0) {
|
|
2124
|
+
throw new ByokCloudError(
|
|
2125
|
+
"coordination_input_invalid",
|
|
2126
|
+
`Activity batch ${input.batchSeq} conflicts with the existing typed tail authority.`
|
|
2127
|
+
);
|
|
2128
|
+
}
|
|
2129
|
+
await client.query("COMMIT");
|
|
2130
|
+
return toTail(row);
|
|
2131
|
+
} catch (caught) {
|
|
2132
|
+
await client.query("ROLLBACK").catch(() => {
|
|
2133
|
+
});
|
|
2134
|
+
throw caught;
|
|
2135
|
+
} finally {
|
|
2136
|
+
client.release();
|
|
1787
2137
|
}
|
|
1788
|
-
return toTail(row);
|
|
1789
2138
|
}
|
|
1790
2139
|
async read(tenant, taskId) {
|
|
1791
2140
|
const result = await this.pool.query(
|
|
@@ -1904,6 +2253,79 @@ var PostgresApprovalTimelineStore = class {
|
|
|
1904
2253
|
client.release();
|
|
1905
2254
|
}
|
|
1906
2255
|
}
|
|
2256
|
+
async resolvePending(tenant, input) {
|
|
2257
|
+
const { capacity, ttlMs, event } = validateApprovalTimelineResolve(input);
|
|
2258
|
+
const now = this.clock.now();
|
|
2259
|
+
const receivedAt = now.toISOString();
|
|
2260
|
+
const expiresAt = new Date(now.getTime() + ttlMs).toISOString();
|
|
2261
|
+
const client = await this.pool.connect();
|
|
2262
|
+
try {
|
|
2263
|
+
await client.query("BEGIN");
|
|
2264
|
+
await client.query(
|
|
2265
|
+
`SELECT pg_advisory_xact_lock(hashtextextended($1 || E'\\x1f' || $2, 0))`,
|
|
2266
|
+
[tenant, input.taskId]
|
|
2267
|
+
);
|
|
2268
|
+
const stored = await readLocked(client, tenant, input.taskId);
|
|
2269
|
+
const live = stored !== void 0 && receivedAt < stored.expires_at ? toTail2(stored) : void 0;
|
|
2270
|
+
if (live === void 0) {
|
|
2271
|
+
await client.query("COMMIT");
|
|
2272
|
+
return { status: "absent" };
|
|
2273
|
+
}
|
|
2274
|
+
const duplicate = live.entries.find((entry) => entry.sourceEnvelopeId === input.sourceEnvelopeId);
|
|
2275
|
+
if (duplicate !== void 0) {
|
|
2276
|
+
await client.query("COMMIT");
|
|
2277
|
+
return JSON.stringify(duplicate.event) === JSON.stringify(event) ? { status: "replayed", tail: live } : { status: "conflict", tail: live };
|
|
2278
|
+
}
|
|
2279
|
+
const pending = pendingObservation(live.entries);
|
|
2280
|
+
if (pending === void 0) {
|
|
2281
|
+
await client.query("COMMIT");
|
|
2282
|
+
return { status: "absent", tail: live };
|
|
2283
|
+
}
|
|
2284
|
+
if (pending.sourceEnvelopeId !== input.expectedSourceEnvelopeId || pending.revision !== input.expectedRevision) {
|
|
2285
|
+
await client.query("COMMIT");
|
|
2286
|
+
return { status: "superseded", tail: live };
|
|
2287
|
+
}
|
|
2288
|
+
const revision = Number(stored.next_revision);
|
|
2289
|
+
const expectedRevision = (live.cursor ?? 0) + 1;
|
|
2290
|
+
if (!Number.isSafeInteger(revision) || revision <= 0 || revision !== expectedRevision) {
|
|
2291
|
+
throw new ByokCloudError(
|
|
2292
|
+
"coordination_input_invalid",
|
|
2293
|
+
"Approval timeline revision authority is malformed."
|
|
2294
|
+
);
|
|
2295
|
+
}
|
|
2296
|
+
const observation = ApprovalObservationSchema.parse({
|
|
2297
|
+
taskId: input.taskId,
|
|
2298
|
+
sourceEnvelopeId: input.sourceEnvelopeId,
|
|
2299
|
+
revision,
|
|
2300
|
+
receivedAt,
|
|
2301
|
+
event
|
|
2302
|
+
});
|
|
2303
|
+
const allEntries = [...live.entries, observation];
|
|
2304
|
+
const evicted = Math.max(allEntries.length - capacity, 0);
|
|
2305
|
+
const entries = allEntries.slice(evicted);
|
|
2306
|
+
const dropped = live.dropped + evicted;
|
|
2307
|
+
const result = await client.query(
|
|
2308
|
+
`INSERT INTO approval_timeline_tail
|
|
2309
|
+
(tenant_id, task_id, entries, next_revision, dropped, capacity, expires_at)
|
|
2310
|
+
VALUES ($1, $2, $3::jsonb, $4, $5, $6, $7)
|
|
2311
|
+
ON CONFLICT (tenant_id, task_id) DO UPDATE
|
|
2312
|
+
SET entries = EXCLUDED.entries,
|
|
2313
|
+
next_revision = EXCLUDED.next_revision,
|
|
2314
|
+
dropped = EXCLUDED.dropped,
|
|
2315
|
+
capacity = EXCLUDED.capacity,
|
|
2316
|
+
expires_at = EXCLUDED.expires_at
|
|
2317
|
+
RETURNING tenant_id, task_id, entries, next_revision, dropped, capacity, expires_at`,
|
|
2318
|
+
[tenant, input.taskId, JSON.stringify(entries), revision + 1, dropped, capacity, expiresAt]
|
|
2319
|
+
);
|
|
2320
|
+
await client.query("COMMIT");
|
|
2321
|
+
return { status: "applied", tail: toTail2(result.rows[0]) };
|
|
2322
|
+
} catch (caught) {
|
|
2323
|
+
await client.query("ROLLBACK");
|
|
2324
|
+
throw caught;
|
|
2325
|
+
} finally {
|
|
2326
|
+
client.release();
|
|
2327
|
+
}
|
|
2328
|
+
}
|
|
1907
2329
|
async read(tenant, taskId) {
|
|
1908
2330
|
const result = await this.pool.query(
|
|
1909
2331
|
`SELECT tenant_id, task_id, entries, next_revision, dropped, capacity, expires_at
|
|
@@ -1915,6 +2337,19 @@ var PostgresApprovalTimelineStore = class {
|
|
|
1915
2337
|
return row === void 0 ? void 0 : toTail2(row);
|
|
1916
2338
|
}
|
|
1917
2339
|
};
|
|
2340
|
+
function pendingObservation(entries) {
|
|
2341
|
+
let pending;
|
|
2342
|
+
for (const entry of entries) {
|
|
2343
|
+
if (entry.event.type === "approval_requested") {
|
|
2344
|
+
pending = entry;
|
|
2345
|
+
continue;
|
|
2346
|
+
}
|
|
2347
|
+
if (pending !== void 0 && (pending.event.approvalId === void 0 || pending.event.approvalId === entry.event.approvalId)) {
|
|
2348
|
+
pending = void 0;
|
|
2349
|
+
}
|
|
2350
|
+
}
|
|
2351
|
+
return pending;
|
|
2352
|
+
}
|
|
1918
2353
|
var SELECT_COLUMNS4 = [
|
|
1919
2354
|
"tenant_id",
|
|
1920
2355
|
"device_id",
|
|
@@ -1961,7 +2396,7 @@ var PostgresAgentEgressStore = class {
|
|
|
1961
2396
|
const inserted = await this.pool.query(
|
|
1962
2397
|
`INSERT INTO agent_egress_event (${SELECT_COLUMNS4})
|
|
1963
2398
|
VALUES ($1, $2, $3::uuid, $4, $5, $6, $7, $8::bigint, $9::jsonb, $10, $11::integer, $12::uuid, $13)
|
|
1964
|
-
ON CONFLICT (tenant_id, device_id, event_id) DO NOTHING
|
|
2399
|
+
ON CONFLICT (tenant_id, device_id, agent_id, agent_profile_revision, event_id) DO NOTHING
|
|
1965
2400
|
RETURNING ${SELECT_COLUMNS4}`,
|
|
1966
2401
|
[
|
|
1967
2402
|
tenant,
|
|
@@ -1981,16 +2416,18 @@ var PostgresAgentEgressStore = class {
|
|
|
1981
2416
|
);
|
|
1982
2417
|
const row = inserted.rows[0];
|
|
1983
2418
|
if (row !== void 0) return { record: toRecord2(row), created: true };
|
|
1984
|
-
const existing = await this.get(tenant, input.deviceId, payload.eventId);
|
|
2419
|
+
const existing = await this.get(tenant, input.deviceId, payload.agentRef, payload.eventId);
|
|
1985
2420
|
if (existing === void 0) throw new Error(`Agent egress ${payload.eventId} vanished during first-write record.`);
|
|
1986
2421
|
return { record: existing, created: false };
|
|
1987
2422
|
}
|
|
1988
|
-
async get(tenant, deviceId, eventId) {
|
|
2423
|
+
async get(tenant, deviceId, agentRef, eventId) {
|
|
1989
2424
|
const result = await this.pool.query(
|
|
1990
2425
|
`SELECT ${SELECT_COLUMNS4}
|
|
1991
2426
|
FROM agent_egress_event
|
|
1992
|
-
WHERE tenant_id = $1 AND device_id = $2
|
|
1993
|
-
|
|
2427
|
+
WHERE tenant_id = $1 AND device_id = $2
|
|
2428
|
+
AND agent_id = $3 AND agent_profile_revision = $4
|
|
2429
|
+
AND event_id = $5::uuid`,
|
|
2430
|
+
[tenant, deviceId, agentRef.agentId, agentRef.profileRevision, eventId]
|
|
1994
2431
|
);
|
|
1995
2432
|
const row = result.rows[0];
|
|
1996
2433
|
return row === void 0 ? void 0 : toRecord2(row);
|
|
@@ -2147,7 +2584,7 @@ var PostgresAgentMemoryProjectionStore = class {
|
|
|
2147
2584
|
}
|
|
2148
2585
|
});
|
|
2149
2586
|
} catch (cause) {
|
|
2150
|
-
await
|
|
2587
|
+
await rollback3(client);
|
|
2151
2588
|
throw cause;
|
|
2152
2589
|
} finally {
|
|
2153
2590
|
client.release();
|
|
@@ -2184,7 +2621,7 @@ var PostgresAgentMemoryProjectionStore = class {
|
|
|
2184
2621
|
await client.query("COMMIT");
|
|
2185
2622
|
return AgentMemoryProjectionEraseResultSchema.parse({ nextWriterEpoch });
|
|
2186
2623
|
} catch (cause) {
|
|
2187
|
-
await
|
|
2624
|
+
await rollback3(client);
|
|
2188
2625
|
throw cause;
|
|
2189
2626
|
} finally {
|
|
2190
2627
|
client.release();
|
|
@@ -2321,7 +2758,7 @@ function toBase64(bytes) {
|
|
|
2321
2758
|
function toBase64Url(bytes) {
|
|
2322
2759
|
return toBase64(bytes).replaceAll("+", "-").replaceAll("/", "_").replace(/=+$/u, "");
|
|
2323
2760
|
}
|
|
2324
|
-
async function
|
|
2761
|
+
async function rollback3(client) {
|
|
2325
2762
|
try {
|
|
2326
2763
|
await client.query("ROLLBACK");
|
|
2327
2764
|
} catch {
|
|
@@ -2380,11 +2817,16 @@ var PostgresDeviceAssertionReplayAuthority = class {
|
|
|
2380
2817
|
// src/stores/index.ts
|
|
2381
2818
|
function createPostgresCloudStores(options) {
|
|
2382
2819
|
const { pool, clock, crypto } = options;
|
|
2820
|
+
const devices = new PostgresDeviceDirectory(pool, clock);
|
|
2821
|
+
const pairing = new PostgresPairingCodeStore(pool, clock);
|
|
2383
2822
|
return {
|
|
2384
2823
|
activity: new PostgresActivityStore(pool, clock),
|
|
2385
2824
|
approvals: new PostgresApprovalTimelineStore(pool, clock),
|
|
2386
|
-
devices
|
|
2387
|
-
|
|
2825
|
+
devices,
|
|
2826
|
+
// One durable code authority, projected into issuance and enrollment ports
|
|
2827
|
+
// so consumers cannot reopen the old redeem-then-register sequence.
|
|
2828
|
+
pairingCodes: { issue: pairing.issue.bind(pairing) },
|
|
2829
|
+
pairing: { redeemAndRegister: pairing.redeemAndRegister.bind(pairing) },
|
|
2388
2830
|
nonces: new PostgresNonceStore(pool, clock, crypto),
|
|
2389
2831
|
dedup: new PostgresInboundDedupStore(pool),
|
|
2390
2832
|
tasks: new PostgresTaskAttemptStore(pool, clock),
|
|
@@ -3673,7 +4115,7 @@ function toReceipt4(tenant, row) {
|
|
|
3673
4115
|
recordedAt: row.recorded_at.toISOString()
|
|
3674
4116
|
};
|
|
3675
4117
|
}
|
|
3676
|
-
function
|
|
4118
|
+
function sameBinding2(receipt, input) {
|
|
3677
4119
|
return receipt.operation === input.operation && receipt.resource === input.resource && receipt.bodySha256 === input.proofBodySha256 && receipt.bodySize === input.proofBodySize;
|
|
3678
4120
|
}
|
|
3679
4121
|
function bodyColumns2(body) {
|
|
@@ -3712,7 +4154,7 @@ var PostgresTruthCommitter = class {
|
|
|
3712
4154
|
]);
|
|
3713
4155
|
const replay = await this.#readReceipt(client, tenant, input.deviceId, input.requestId);
|
|
3714
4156
|
if (replay !== void 0) {
|
|
3715
|
-
if (!
|
|
4157
|
+
if (!sameBinding2(replay, input)) {
|
|
3716
4158
|
throw new TruthCommitError(
|
|
3717
4159
|
"proof_request_conflict",
|
|
3718
4160
|
`Request ${input.requestId} was already used with a different binding.`
|