@byok-sdk/cloud-dataplane 0.5.0 → 0.6.1
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/README.md +74 -5
- package/dist/index.d.ts +4 -2
- package/dist/index.js +1199 -303
- package/dist/index.js.map +1 -1
- package/dist/migrate.d.ts +42 -0
- package/dist/runtime.d.ts +1 -1
- package/dist/runtime.js +603 -262
- package/dist/runtime.js.map +1 -1
- package/dist/sql/0009_task_cancellation.sql +6 -0
- package/dist/sql/0010_tenant_readiness.sql +21 -0
- package/dist/sql/0011_tenant_erasure.sql +46 -0
- package/dist/sql/0012_agent_home_contract.sql +41 -0
- package/dist/sql/0013_agent_egress_contract.sql +47 -0
- package/dist/stores/agent-egress.d.ts +14 -0
- package/dist/stores/core/mailbox.d.ts +13 -0
- package/dist/stores/devices.d.ts +7 -2
- package/dist/stores/index.d.ts +3 -1
- package/dist/stores/r2-blobs.d.ts +1 -1
- package/dist/stores/task-attempts.d.ts +29 -1
- package/dist/stores/task-cancellations.d.ts +9 -0
- package/dist/tenant-erasure.d.ts +77 -0
- package/package.json +4 -4
package/dist/runtime.js
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import pg from 'pg';
|
|
2
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';
|
|
3
|
-
import { ByokCoreError, assertCanonicalTimestamp,
|
|
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';
|
|
6
|
+
import { AgentEgressReliablePayloadSchema } from '@byok-sdk/protocol';
|
|
6
7
|
|
|
7
8
|
// src/pool.ts
|
|
8
9
|
var defaultTypeParser = pg.types.getTypeParser;
|
|
@@ -591,7 +592,7 @@ var R2ObjectMaintenanceStore = class {
|
|
|
591
592
|
`A ListObjectsV2 page limit of ${String(limit)} is not a whole number in [1, 1000].`
|
|
592
593
|
);
|
|
593
594
|
}
|
|
594
|
-
const prefix =
|
|
595
|
+
const prefix = tenantObjectKey(tenant, LIST_PREFIX_HASH, this.#keyPrefix).slice(0, -LIST_HASH_HEX_LENGTH);
|
|
595
596
|
const url = new URL(`${this.#origin}/${this.#bucket}`);
|
|
596
597
|
url.searchParams.set("list-type", "2");
|
|
597
598
|
url.searchParams.set("prefix", prefix);
|
|
@@ -775,6 +776,8 @@ function parseListObjectsV2(xml, prefix, attempts) {
|
|
|
775
776
|
return { objects, nextContinuationToken };
|
|
776
777
|
}
|
|
777
778
|
var HASH_KEY_SUFFIX = /^[0-9a-f]{64}$/;
|
|
779
|
+
var LIST_HASH_HEX_LENGTH = 64;
|
|
780
|
+
var LIST_PREFIX_HASH = contentHash(`sha256:${"0".repeat(LIST_HASH_HEX_LENGTH)}`);
|
|
778
781
|
function asRecord(value) {
|
|
779
782
|
return typeof value === "object" && value !== null && !Array.isArray(value) ? value : void 0;
|
|
780
783
|
}
|
|
@@ -786,8 +789,6 @@ function requiredText(record, field, attempts) {
|
|
|
786
789
|
attempts
|
|
787
790
|
);
|
|
788
791
|
}
|
|
789
|
-
|
|
790
|
-
// src/stores/devices.ts
|
|
791
792
|
function toRecord(row) {
|
|
792
793
|
return {
|
|
793
794
|
tenantId: row.tenant_id,
|
|
@@ -797,14 +798,17 @@ function toRecord(row) {
|
|
|
797
798
|
devicePublicKey: row.device_public_key,
|
|
798
799
|
proofKeyId: row.proof_key_id,
|
|
799
800
|
proofKeyEpoch: row.proof_key_epoch,
|
|
800
|
-
revoked: row.revoked
|
|
801
|
+
revoked: row.revoked,
|
|
802
|
+
...row.capabilities == null ? {} : { capabilities: Object.freeze([...row.capabilities]) }
|
|
801
803
|
};
|
|
802
804
|
}
|
|
803
|
-
var SELECT_COLUMNS = "tenant_id, device_id, product_id, device_name, device_public_key, proof_key_id, proof_key_epoch, revoked";
|
|
805
|
+
var SELECT_COLUMNS = "tenant_id, device_id, product_id, device_name, device_public_key, proof_key_id, proof_key_epoch, revoked, capabilities";
|
|
804
806
|
var PostgresDeviceDirectory = class {
|
|
805
807
|
#pool;
|
|
806
|
-
|
|
808
|
+
#clock;
|
|
809
|
+
constructor(pool, clock) {
|
|
807
810
|
this.#pool = pool;
|
|
811
|
+
this.#clock = clock;
|
|
808
812
|
}
|
|
809
813
|
async register(tenant, input) {
|
|
810
814
|
const result = await this.#pool.query(
|
|
@@ -819,7 +823,8 @@ var PostgresDeviceDirectory = class {
|
|
|
819
823
|
device_public_key = EXCLUDED.device_public_key,
|
|
820
824
|
proof_key_id = EXCLUDED.proof_key_id,
|
|
821
825
|
proof_key_epoch = EXCLUDED.proof_key_epoch,
|
|
822
|
-
revoked = false
|
|
826
|
+
revoked = false,
|
|
827
|
+
capabilities = NULL
|
|
823
828
|
RETURNING ${SELECT_COLUMNS}`,
|
|
824
829
|
[
|
|
825
830
|
tenant,
|
|
@@ -847,6 +852,17 @@ var PostgresDeviceDirectory = class {
|
|
|
847
852
|
deviceId
|
|
848
853
|
]);
|
|
849
854
|
}
|
|
855
|
+
async recordCapabilities(tenant, input) {
|
|
856
|
+
const result = await this.#pool.query(
|
|
857
|
+
`UPDATE device
|
|
858
|
+
SET capabilities = $3::jsonb
|
|
859
|
+
WHERE tenant_id = $1 AND device_id = $2 AND revoked = false
|
|
860
|
+
RETURNING ${SELECT_COLUMNS}`,
|
|
861
|
+
[tenant, input.deviceId, JSON.stringify([...input.capabilities])]
|
|
862
|
+
);
|
|
863
|
+
const row = result.rows[0];
|
|
864
|
+
return row === void 0 ? void 0 : toRecord(row);
|
|
865
|
+
}
|
|
850
866
|
async list(tenant) {
|
|
851
867
|
const result = await this.#pool.query(
|
|
852
868
|
`SELECT ${SELECT_COLUMNS} FROM device WHERE tenant_id = $1 ORDER BY device_id`,
|
|
@@ -854,6 +870,73 @@ var PostgresDeviceDirectory = class {
|
|
|
854
870
|
);
|
|
855
871
|
return result.rows.map(toRecord);
|
|
856
872
|
}
|
|
873
|
+
async readiness(tenant, _presence) {
|
|
874
|
+
const result = await this.#pool.query(
|
|
875
|
+
`SELECT
|
|
876
|
+
d.device_id,
|
|
877
|
+
d.product_id,
|
|
878
|
+
d.device_name,
|
|
879
|
+
d.revoked,
|
|
880
|
+
CASE WHEN NOT d.revoked THEN p.level END AS presence_level,
|
|
881
|
+
CASE WHEN NOT d.revoked THEN p.detail END AS presence_detail,
|
|
882
|
+
CASE WHEN NOT d.revoked THEN p.configured_toolsets END AS presence_configured_toolsets,
|
|
883
|
+
CASE WHEN NOT d.revoked THEN p.client_version END AS presence_client_version,
|
|
884
|
+
CASE WHEN NOT d.revoked THEN p.protocol_versions END AS presence_protocol_versions,
|
|
885
|
+
CASE WHEN NOT d.revoked THEN p.runtimes END AS presence_runtimes,
|
|
886
|
+
CASE WHEN NOT d.revoked THEN p.observed_at END AS presence_observed_at,
|
|
887
|
+
CASE WHEN NOT d.revoked THEN p.expires_at END AS presence_expires_at,
|
|
888
|
+
(COUNT(*) FILTER (WHERE NOT d.revoked) OVER ())::int AS active_paired_device_count,
|
|
889
|
+
(COUNT(*) FILTER (WHERE d.revoked) OVER ())::int AS revoked_device_count,
|
|
890
|
+
(COUNT(*) FILTER (WHERE NOT d.revoked AND p.device_id IS NOT NULL) OVER ())::int AS observed_presence_count,
|
|
891
|
+
(COUNT(*) FILTER (WHERE NOT d.revoked AND p.level = 'online') OVER ())::int AS observed_online_count,
|
|
892
|
+
(COUNT(*) FILTER (WHERE NOT d.revoked AND p.level = 'thinking') OVER ())::int AS observed_thinking_count,
|
|
893
|
+
(COUNT(*) FILTER (WHERE NOT d.revoked AND p.level = 'working') OVER ())::int AS observed_working_count,
|
|
894
|
+
(COUNT(*) FILTER (WHERE NOT d.revoked AND p.level = 'error') OVER ())::int AS observed_error_count,
|
|
895
|
+
(COUNT(*) FILTER (WHERE NOT d.revoked AND p.level = 'offline') OVER ())::int AS observed_offline_count
|
|
896
|
+
FROM device d
|
|
897
|
+
LEFT JOIN device_presence p
|
|
898
|
+
ON p.tenant_id = d.tenant_id
|
|
899
|
+
AND p.device_id = d.device_id
|
|
900
|
+
AND p.expires_at > $2
|
|
901
|
+
WHERE d.tenant_id = $1
|
|
902
|
+
ORDER BY d.device_id`,
|
|
903
|
+
[tenant, this.#clock?.now().toISOString() ?? (/* @__PURE__ */ new Date()).toISOString()]
|
|
904
|
+
);
|
|
905
|
+
const row = result.rows[0];
|
|
906
|
+
const count = (value) => Number(value);
|
|
907
|
+
const devices = result.rows.map((device) => ({
|
|
908
|
+
deviceId: device.device_id,
|
|
909
|
+
productId: device.product_id,
|
|
910
|
+
deviceName: device.device_name,
|
|
911
|
+
revoked: device.revoked,
|
|
912
|
+
...device.presence_level === null ? {} : {
|
|
913
|
+
presence: {
|
|
914
|
+
level: device.presence_level,
|
|
915
|
+
...device.presence_detail === null ? {} : { detail: device.presence_detail },
|
|
916
|
+
...device.presence_configured_toolsets === null ? {} : { configuredToolsets: Object.freeze([...device.presence_configured_toolsets]) },
|
|
917
|
+
...device.presence_client_version === null ? {} : { clientVersion: device.presence_client_version },
|
|
918
|
+
...device.presence_protocol_versions === null ? {} : { protocolVersions: Object.freeze([...device.presence_protocol_versions]) },
|
|
919
|
+
...device.presence_runtimes === null ? {} : { runtimes: Object.freeze(device.presence_runtimes.map((runtime) => Object.freeze({ ...runtime }))) },
|
|
920
|
+
observedAt: device.presence_observed_at,
|
|
921
|
+
expiresAt: device.presence_expires_at
|
|
922
|
+
}
|
|
923
|
+
}
|
|
924
|
+
}));
|
|
925
|
+
return {
|
|
926
|
+
tenantId: tenant,
|
|
927
|
+
activePairedDeviceCount: count(row?.active_paired_device_count ?? 0),
|
|
928
|
+
revokedDeviceCount: count(row?.revoked_device_count ?? 0),
|
|
929
|
+
observedPresenceCount: count(row?.observed_presence_count ?? 0),
|
|
930
|
+
observedPresenceByLevel: {
|
|
931
|
+
online: count(row?.observed_online_count ?? 0),
|
|
932
|
+
thinking: count(row?.observed_thinking_count ?? 0),
|
|
933
|
+
working: count(row?.observed_working_count ?? 0),
|
|
934
|
+
error: count(row?.observed_error_count ?? 0),
|
|
935
|
+
offline: count(row?.observed_offline_count ?? 0)
|
|
936
|
+
},
|
|
937
|
+
devices
|
|
938
|
+
};
|
|
939
|
+
}
|
|
857
940
|
async resolveByDeviceId(deviceId) {
|
|
858
941
|
const result = await this.#pool.query(
|
|
859
942
|
`SELECT ${SELECT_COLUMNS} FROM device WHERE device_id = $1`,
|
|
@@ -1077,18 +1160,31 @@ var PostgresProofRequestReceiptStore = class {
|
|
|
1077
1160
|
};
|
|
1078
1161
|
|
|
1079
1162
|
// src/stores/task-attempts.ts
|
|
1080
|
-
var
|
|
1081
|
-
function
|
|
1163
|
+
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";
|
|
1164
|
+
function taskRowToAttempt(row) {
|
|
1082
1165
|
return {
|
|
1083
1166
|
tenantId: row.tenant_id,
|
|
1084
1167
|
taskId: row.task_id,
|
|
1085
1168
|
deviceId: row.device_id,
|
|
1169
|
+
...row.agent_id == null || row.agent_profile_revision == null ? {} : {
|
|
1170
|
+
agentRef: {
|
|
1171
|
+
agentId: row.agent_id,
|
|
1172
|
+
profileRevision: row.agent_profile_revision
|
|
1173
|
+
}
|
|
1174
|
+
},
|
|
1086
1175
|
// `exactOptionalPropertyTypes` is off here, but an explicit absent key is
|
|
1087
1176
|
// still what the in-memory reference produces for an unclaimed attempt, and
|
|
1088
1177
|
// `toEqual` in the suite treats `undefined` and absent alike only for the
|
|
1089
1178
|
// former.
|
|
1090
1179
|
...row.owner_device_id === null ? {} : { ownerDeviceId: row.owner_device_id },
|
|
1091
1180
|
status: row.status,
|
|
1181
|
+
...row.terminal_cause == null ? {} : { terminalCause: row.terminal_cause },
|
|
1182
|
+
...row.cancel_requested_at === null ? {} : {
|
|
1183
|
+
cancellation: {
|
|
1184
|
+
requestedAt: row.cancel_requested_at.toISOString(),
|
|
1185
|
+
...row.cancel_reason === null ? {} : { reason: row.cancel_reason }
|
|
1186
|
+
}
|
|
1187
|
+
},
|
|
1092
1188
|
updatedAt: row.updated_at.toISOString()
|
|
1093
1189
|
};
|
|
1094
1190
|
}
|
|
@@ -1101,53 +1197,423 @@ var PostgresTaskAttemptStore = class {
|
|
|
1101
1197
|
}
|
|
1102
1198
|
async open(tenant, input) {
|
|
1103
1199
|
const inserted = await this.#pool.query(
|
|
1104
|
-
`INSERT INTO task (
|
|
1105
|
-
|
|
1200
|
+
`INSERT INTO task (
|
|
1201
|
+
tenant_id, task_id, device_id, agent_id, agent_profile_revision,
|
|
1202
|
+
owner_device_id, status, updated_at
|
|
1203
|
+
)
|
|
1204
|
+
VALUES ($1, $2, $3, $4, $5, NULL, 'offered', $6)
|
|
1106
1205
|
ON CONFLICT (tenant_id, task_id) DO NOTHING
|
|
1107
|
-
RETURNING ${
|
|
1108
|
-
[
|
|
1206
|
+
RETURNING ${TASK_SELECT_COLUMNS}`,
|
|
1207
|
+
[
|
|
1208
|
+
tenant,
|
|
1209
|
+
input.taskId,
|
|
1210
|
+
input.deviceId,
|
|
1211
|
+
input.agentRef?.agentId ?? null,
|
|
1212
|
+
input.agentRef?.profileRevision ?? null,
|
|
1213
|
+
this.#now()
|
|
1214
|
+
]
|
|
1109
1215
|
);
|
|
1110
1216
|
const created = inserted.rows[0];
|
|
1111
|
-
if (created !== void 0) return
|
|
1217
|
+
if (created !== void 0) return taskRowToAttempt(created);
|
|
1112
1218
|
const existing = await this.get(tenant, input.taskId);
|
|
1113
1219
|
if (existing === void 0) throw new Error(`task ${input.taskId} vanished during open`);
|
|
1114
1220
|
return existing;
|
|
1115
1221
|
}
|
|
1222
|
+
async reserveAgentOffer(tenant, input) {
|
|
1223
|
+
const inserted = await this.#pool.query(
|
|
1224
|
+
`INSERT INTO task (
|
|
1225
|
+
tenant_id, task_id, device_id, agent_id, agent_profile_revision,
|
|
1226
|
+
owner_device_id, status, updated_at
|
|
1227
|
+
)
|
|
1228
|
+
VALUES ($1, $2, $3, $4, $5, NULL, 'offered', $6)
|
|
1229
|
+
ON CONFLICT (tenant_id, task_id) DO NOTHING
|
|
1230
|
+
RETURNING ${TASK_SELECT_COLUMNS}`,
|
|
1231
|
+
[tenant, input.taskId, input.deviceId, input.agentRef.agentId, input.agentRef.profileRevision, this.#now()]
|
|
1232
|
+
);
|
|
1233
|
+
const created = inserted.rows[0];
|
|
1234
|
+
if (created !== void 0) return { attempt: taskRowToAttempt(created), created: true };
|
|
1235
|
+
const existing = await this.get(tenant, input.taskId);
|
|
1236
|
+
if (existing === void 0) throw new Error(`task ${input.taskId} vanished during Agent offer reservation`);
|
|
1237
|
+
return { attempt: existing, created: false };
|
|
1238
|
+
}
|
|
1116
1239
|
async get(tenant, taskId) {
|
|
1117
1240
|
const result = await this.#pool.query(
|
|
1118
|
-
`SELECT ${
|
|
1241
|
+
`SELECT ${TASK_SELECT_COLUMNS} FROM task WHERE tenant_id = $1 AND task_id = $2`,
|
|
1119
1242
|
[tenant, taskId]
|
|
1120
1243
|
);
|
|
1121
1244
|
const row = result.rows[0];
|
|
1122
|
-
return row === void 0 ? void 0 :
|
|
1245
|
+
return row === void 0 ? void 0 : taskRowToAttempt(row);
|
|
1246
|
+
}
|
|
1247
|
+
async getMany(tenant, taskIds) {
|
|
1248
|
+
if (taskIds.length === 0) return [];
|
|
1249
|
+
const result = await this.#pool.query(
|
|
1250
|
+
`SELECT ${TASK_SELECT_COLUMNS} FROM task WHERE tenant_id = $1 AND task_id = ANY($2::text[])`,
|
|
1251
|
+
[tenant, [...new Set(taskIds)]]
|
|
1252
|
+
);
|
|
1253
|
+
return result.rows.map(taskRowToAttempt);
|
|
1123
1254
|
}
|
|
1124
1255
|
async claim(tenant, input) {
|
|
1125
1256
|
const claimed = await this.#pool.query(
|
|
1126
1257
|
`UPDATE task
|
|
1127
1258
|
SET owner_device_id = $3, status = 'claimed', updated_at = $4
|
|
1128
1259
|
WHERE tenant_id = $1 AND task_id = $2 AND owner_device_id IS NULL
|
|
1129
|
-
|
|
1260
|
+
AND cancel_requested_at IS NULL AND status = 'offered'
|
|
1261
|
+
RETURNING ${TASK_SELECT_COLUMNS}`,
|
|
1130
1262
|
[tenant, input.taskId, input.deviceId, this.#now()]
|
|
1131
1263
|
);
|
|
1132
1264
|
const won = claimed.rows[0];
|
|
1133
|
-
if (won !== void 0) return
|
|
1265
|
+
if (won !== void 0) return taskRowToAttempt(won);
|
|
1134
1266
|
return this.get(tenant, input.taskId);
|
|
1135
1267
|
}
|
|
1136
1268
|
async recordStatus(tenant, input) {
|
|
1137
1269
|
const result = await this.#pool.query(
|
|
1138
1270
|
`UPDATE task
|
|
1139
|
-
SET status = $3,
|
|
1271
|
+
SET status = $3,
|
|
1272
|
+
terminal_cause = COALESCE($7, terminal_cause),
|
|
1273
|
+
updated_at = $4
|
|
1140
1274
|
WHERE tenant_id = $1 AND task_id = $2
|
|
1141
|
-
|
|
1142
|
-
|
|
1275
|
+
AND (
|
|
1276
|
+
(agent_id IS NULL AND agent_profile_revision IS NULL AND $5::text IS NULL AND $6::text IS NULL)
|
|
1277
|
+
OR (agent_id = $5 AND agent_profile_revision = $6)
|
|
1278
|
+
)
|
|
1279
|
+
AND (
|
|
1280
|
+
(cancel_requested_at IS NULL AND status NOT IN ('complete', 'failed', 'cancelled'))
|
|
1281
|
+
OR (cancel_requested_at IS NOT NULL AND $3 = 'cancelled' AND status <> 'cancelled')
|
|
1282
|
+
)
|
|
1283
|
+
RETURNING ${TASK_SELECT_COLUMNS}`,
|
|
1284
|
+
[
|
|
1285
|
+
tenant,
|
|
1286
|
+
input.taskId,
|
|
1287
|
+
input.status,
|
|
1288
|
+
this.#now(),
|
|
1289
|
+
input.agentRef?.agentId ?? null,
|
|
1290
|
+
input.agentRef?.profileRevision ?? null,
|
|
1291
|
+
input.terminalCause ?? null
|
|
1292
|
+
]
|
|
1293
|
+
);
|
|
1294
|
+
const row = result.rows[0];
|
|
1295
|
+
return row === void 0 ? this.get(tenant, input.taskId) : taskRowToAttempt(row);
|
|
1296
|
+
}
|
|
1297
|
+
#now() {
|
|
1298
|
+
return this.#clock.now().toISOString();
|
|
1299
|
+
}
|
|
1300
|
+
};
|
|
1301
|
+
|
|
1302
|
+
// src/stores/core/mailbox-sequence.ts
|
|
1303
|
+
async function allocateMailboxSequence(client, tenant, deviceId, now) {
|
|
1304
|
+
const allocation = await client.query(
|
|
1305
|
+
`INSERT INTO device_stream (tenant_id, device_id, next_seq, acked_seq, acked_at)
|
|
1306
|
+
VALUES ($1, $2, 2, 0, $3)
|
|
1307
|
+
ON CONFLICT (tenant_id, device_id) DO UPDATE
|
|
1308
|
+
SET next_seq = device_stream.next_seq + 1
|
|
1309
|
+
RETURNING next_seq - 1 AS seq`,
|
|
1310
|
+
[tenant, deviceId, now]
|
|
1311
|
+
);
|
|
1312
|
+
return Number(allocation.rows[0].seq);
|
|
1313
|
+
}
|
|
1314
|
+
|
|
1315
|
+
// src/stores/core/mailbox.ts
|
|
1316
|
+
var DEFAULT_READ_LIMIT = 50;
|
|
1317
|
+
var OUTBOX_COLUMNS = "tenant_id, device_id, seq, message_id, body, body_hash, byte_size, state, appended_at";
|
|
1318
|
+
function toMailboxMessage(row) {
|
|
1319
|
+
return {
|
|
1320
|
+
tenantId: row.tenant_id,
|
|
1321
|
+
deviceId: row.device_id,
|
|
1322
|
+
// `seq` is bigint in the column and `number` on the port, because it is the
|
|
1323
|
+
// envelope `seq` on the wire. The column is wide so the counter cannot wrap
|
|
1324
|
+
// into a redelivery bug; the narrowing happens once, here.
|
|
1325
|
+
seq: Number(row.seq),
|
|
1326
|
+
messageId: row.message_id,
|
|
1327
|
+
body: row.body,
|
|
1328
|
+
bodyHash: row.body_hash,
|
|
1329
|
+
byteSize: row.byte_size,
|
|
1330
|
+
state: row.state,
|
|
1331
|
+
appendedAt: row.appended_at
|
|
1332
|
+
};
|
|
1333
|
+
}
|
|
1334
|
+
var PostgresMailboxStore = class {
|
|
1335
|
+
#pool;
|
|
1336
|
+
#clock;
|
|
1337
|
+
constructor(pool, clock) {
|
|
1338
|
+
this.#pool = pool;
|
|
1339
|
+
this.#clock = clock;
|
|
1340
|
+
}
|
|
1341
|
+
async append(tenant, input) {
|
|
1342
|
+
this.#requireDeviceId(input.deviceId);
|
|
1343
|
+
const client = await this.#pool.connect();
|
|
1344
|
+
try {
|
|
1345
|
+
await client.query("BEGIN");
|
|
1346
|
+
const existing = await client.query(
|
|
1347
|
+
`SELECT ${OUTBOX_COLUMNS} FROM outbox
|
|
1348
|
+
WHERE tenant_id = $1 AND device_id = $2 AND message_id = $3`,
|
|
1349
|
+
[tenant, input.deviceId, input.messageId]
|
|
1350
|
+
);
|
|
1351
|
+
const replayed = existing.rows[0];
|
|
1352
|
+
if (replayed !== void 0) {
|
|
1353
|
+
await client.query("COMMIT");
|
|
1354
|
+
return toMailboxMessage(replayed);
|
|
1355
|
+
}
|
|
1356
|
+
const seq = await allocateMailboxSequence(client, tenant, input.deviceId, this.#now());
|
|
1357
|
+
const serializedExisting = await client.query(
|
|
1358
|
+
`SELECT ${OUTBOX_COLUMNS} FROM outbox
|
|
1359
|
+
WHERE tenant_id = $1 AND device_id = $2 AND message_id = $3`,
|
|
1360
|
+
[tenant, input.deviceId, input.messageId]
|
|
1361
|
+
);
|
|
1362
|
+
const winnerAfterLock = serializedExisting.rows[0];
|
|
1363
|
+
if (winnerAfterLock !== void 0) {
|
|
1364
|
+
await client.query("ROLLBACK");
|
|
1365
|
+
return toMailboxMessage(winnerAfterLock);
|
|
1366
|
+
}
|
|
1367
|
+
const materialized = await input.materialize(seq);
|
|
1368
|
+
const now = this.#now();
|
|
1369
|
+
const inserted = await client.query(
|
|
1370
|
+
`INSERT INTO outbox (${OUTBOX_COLUMNS})
|
|
1371
|
+
VALUES ($1, $2, $3::bigint, $4, $5, $6, $7::bigint, 'pending', $8)
|
|
1372
|
+
ON CONFLICT (tenant_id, device_id, message_id) DO NOTHING
|
|
1373
|
+
RETURNING ${OUTBOX_COLUMNS}`,
|
|
1374
|
+
[
|
|
1375
|
+
tenant,
|
|
1376
|
+
input.deviceId,
|
|
1377
|
+
seq,
|
|
1378
|
+
input.messageId,
|
|
1379
|
+
materialized.body,
|
|
1380
|
+
materialized.bodyHash,
|
|
1381
|
+
materialized.byteSize,
|
|
1382
|
+
now
|
|
1383
|
+
]
|
|
1384
|
+
);
|
|
1385
|
+
const row = inserted.rows[0];
|
|
1386
|
+
if (row !== void 0) {
|
|
1387
|
+
await client.query("COMMIT");
|
|
1388
|
+
return toMailboxMessage(row);
|
|
1389
|
+
}
|
|
1390
|
+
const winner = await client.query(
|
|
1391
|
+
`SELECT ${OUTBOX_COLUMNS} FROM outbox
|
|
1392
|
+
WHERE tenant_id = $1 AND device_id = $2 AND message_id = $3`,
|
|
1393
|
+
[tenant, input.deviceId, input.messageId]
|
|
1394
|
+
);
|
|
1395
|
+
await client.query("ROLLBACK");
|
|
1396
|
+
const won = winner.rows[0];
|
|
1397
|
+
if (won === void 0) {
|
|
1398
|
+
throw new ByokCoreError(
|
|
1399
|
+
"mailbox_message_not_found",
|
|
1400
|
+
`Message ${input.messageId} vanished during an idempotent append.`
|
|
1401
|
+
);
|
|
1402
|
+
}
|
|
1403
|
+
return toMailboxMessage(won);
|
|
1404
|
+
} catch (cause) {
|
|
1405
|
+
await client.query("ROLLBACK").catch(() => {
|
|
1406
|
+
});
|
|
1407
|
+
throw cause;
|
|
1408
|
+
} finally {
|
|
1409
|
+
client.release();
|
|
1410
|
+
}
|
|
1411
|
+
}
|
|
1412
|
+
async readAfter(tenant, query) {
|
|
1413
|
+
const limit = query.limit ?? DEFAULT_READ_LIMIT;
|
|
1414
|
+
const result = await this.#pool.query(
|
|
1415
|
+
`SELECT ${OUTBOX_COLUMNS} FROM outbox
|
|
1416
|
+
WHERE tenant_id = $1 AND device_id = $2 AND state = 'pending' AND seq > $3::bigint
|
|
1417
|
+
ORDER BY seq
|
|
1418
|
+
LIMIT $4`,
|
|
1419
|
+
[tenant, query.deviceId, query.afterSeq, limit + 1]
|
|
1420
|
+
);
|
|
1421
|
+
const page = result.rows.slice(0, limit).map(toMailboxMessage);
|
|
1422
|
+
return {
|
|
1423
|
+
messages: page,
|
|
1424
|
+
// Nothing above was mutated, so an identical call replays the same page.
|
|
1425
|
+
// The returned position is a READ cursor and moves no ack.
|
|
1426
|
+
nextSeq: page.at(-1)?.seq ?? query.afterSeq,
|
|
1427
|
+
hasMore: result.rows.length > page.length
|
|
1428
|
+
};
|
|
1429
|
+
}
|
|
1430
|
+
async advanceCursor(tenant, input) {
|
|
1431
|
+
this.#requireDeviceId(input.deviceId);
|
|
1432
|
+
const now = this.#now();
|
|
1433
|
+
const moved = await this.#pool.query(
|
|
1434
|
+
`WITH moved AS (
|
|
1435
|
+
INSERT INTO device_stream (tenant_id, device_id, next_seq, acked_seq, acked_at)
|
|
1436
|
+
VALUES ($1, $2, 1, $3::bigint, $4)
|
|
1437
|
+
ON CONFLICT (tenant_id, device_id) DO UPDATE
|
|
1438
|
+
SET acked_seq = EXCLUDED.acked_seq, acked_at = EXCLUDED.acked_at
|
|
1439
|
+
WHERE device_stream.acked_seq <= EXCLUDED.acked_seq
|
|
1440
|
+
RETURNING acked_seq, acked_at
|
|
1441
|
+
), marked AS (
|
|
1442
|
+
UPDATE outbox
|
|
1443
|
+
SET state = 'acked'
|
|
1444
|
+
WHERE tenant_id = $1 AND device_id = $2 AND state = 'pending'
|
|
1445
|
+
AND seq <= (SELECT acked_seq FROM moved)
|
|
1446
|
+
RETURNING 1
|
|
1447
|
+
)
|
|
1448
|
+
SELECT acked_seq, acked_at FROM moved`,
|
|
1449
|
+
[tenant, input.deviceId, input.ackedSeq, now]
|
|
1450
|
+
);
|
|
1451
|
+
const row = moved.rows[0];
|
|
1452
|
+
if (row !== void 0) {
|
|
1453
|
+
return {
|
|
1454
|
+
tenantId: tenant,
|
|
1455
|
+
deviceId: input.deviceId,
|
|
1456
|
+
ackedSeq: Number(row.acked_seq),
|
|
1457
|
+
updatedAt: row.acked_at ?? now
|
|
1458
|
+
};
|
|
1459
|
+
}
|
|
1460
|
+
const current = await this.readCursor(tenant, input.deviceId);
|
|
1461
|
+
throw new CoreConflictError(
|
|
1462
|
+
"mailbox_cursor_regression",
|
|
1463
|
+
`Cursor for device ${input.deviceId} is at ${current.ackedSeq}; refusing to move it back to ${input.ackedSeq}.`,
|
|
1464
|
+
current,
|
|
1465
|
+
this.#now()
|
|
1466
|
+
);
|
|
1467
|
+
}
|
|
1468
|
+
async readCursor(tenant, deviceId) {
|
|
1469
|
+
const result = await this.#pool.query(
|
|
1470
|
+
`SELECT acked_seq, acked_at FROM device_stream
|
|
1471
|
+
WHERE tenant_id = $1 AND device_id = $2`,
|
|
1472
|
+
[tenant, deviceId]
|
|
1143
1473
|
);
|
|
1144
1474
|
const row = result.rows[0];
|
|
1145
|
-
return
|
|
1475
|
+
return {
|
|
1476
|
+
tenantId: tenant,
|
|
1477
|
+
deviceId,
|
|
1478
|
+
ackedSeq: row === void 0 ? 0 : Number(row.acked_seq),
|
|
1479
|
+
updatedAt: row?.acked_at ?? this.#now()
|
|
1480
|
+
};
|
|
1481
|
+
}
|
|
1482
|
+
async collectRetired(tenant, input) {
|
|
1483
|
+
assertCanonicalTimestamp(input.ackedBefore, "ackedBefore");
|
|
1484
|
+
assertCanonicalTimestamp(input.expireUnackedBefore, "expireUnackedBefore");
|
|
1485
|
+
const swept = await this.#pool.query(
|
|
1486
|
+
`WITH deleted AS (
|
|
1487
|
+
DELETE FROM outbox
|
|
1488
|
+
WHERE tenant_id = $1
|
|
1489
|
+
AND ($2::text IS NULL OR device_id = $2::text)
|
|
1490
|
+
AND state = 'acked'
|
|
1491
|
+
AND appended_at < $3
|
|
1492
|
+
RETURNING byte_size
|
|
1493
|
+
), expired AS (
|
|
1494
|
+
UPDATE outbox
|
|
1495
|
+
SET state = 'expired'
|
|
1496
|
+
WHERE tenant_id = $1
|
|
1497
|
+
AND ($2::text IS NULL OR device_id = $2::text)
|
|
1498
|
+
AND state = 'pending'
|
|
1499
|
+
AND appended_at < $4
|
|
1500
|
+
RETURNING 1
|
|
1501
|
+
)
|
|
1502
|
+
SELECT (SELECT count(*) FROM deleted) AS deleted_count,
|
|
1503
|
+
(SELECT count(*) FROM expired) AS expired_count,
|
|
1504
|
+
(SELECT COALESCE(SUM(byte_size), 0) FROM deleted)::bigint AS released_bytes`,
|
|
1505
|
+
[tenant, input.deviceId ?? null, input.ackedBefore, input.expireUnackedBefore]
|
|
1506
|
+
);
|
|
1507
|
+
const row = swept.rows[0];
|
|
1508
|
+
return {
|
|
1509
|
+
deletedCount: Number(row.deleted_count),
|
|
1510
|
+
expiredCount: Number(row.expired_count),
|
|
1511
|
+
releasedBytes: row.released_bytes
|
|
1512
|
+
};
|
|
1513
|
+
}
|
|
1514
|
+
/**
|
|
1515
|
+
* The in-memory reference refuses an empty device id rather than opening a
|
|
1516
|
+
* mailbox nothing can address. Kept here so the two compositions answer the
|
|
1517
|
+
* same way; the table itself would happily store the row.
|
|
1518
|
+
*/
|
|
1519
|
+
#requireDeviceId(deviceId) {
|
|
1520
|
+
if (deviceId.length === 0) {
|
|
1521
|
+
throw new ByokCoreError("mailbox_message_not_found", "Device id must not be empty.");
|
|
1522
|
+
}
|
|
1146
1523
|
}
|
|
1147
1524
|
#now() {
|
|
1148
1525
|
return this.#clock.now().toISOString();
|
|
1149
1526
|
}
|
|
1150
1527
|
};
|
|
1528
|
+
|
|
1529
|
+
// src/stores/task-cancellations.ts
|
|
1530
|
+
var PostgresTaskCancellationStore = class {
|
|
1531
|
+
#pool;
|
|
1532
|
+
#clock;
|
|
1533
|
+
constructor(pool, clock) {
|
|
1534
|
+
this.#pool = pool;
|
|
1535
|
+
this.#clock = clock;
|
|
1536
|
+
}
|
|
1537
|
+
async request(tenant, input) {
|
|
1538
|
+
const client = await this.#pool.connect();
|
|
1539
|
+
try {
|
|
1540
|
+
await client.query("BEGIN");
|
|
1541
|
+
const selected = await client.query(
|
|
1542
|
+
`SELECT ${TASK_SELECT_COLUMNS} FROM task
|
|
1543
|
+
WHERE tenant_id = $1 AND task_id = $2
|
|
1544
|
+
FOR UPDATE`,
|
|
1545
|
+
[tenant, input.taskId]
|
|
1546
|
+
);
|
|
1547
|
+
const current = selected.rows[0];
|
|
1548
|
+
if (current === void 0) {
|
|
1549
|
+
await client.query("ROLLBACK");
|
|
1550
|
+
return void 0;
|
|
1551
|
+
}
|
|
1552
|
+
if (current.cancel_requested_at === null && (current.status === "complete" || current.status === "failed" || current.status === "cancelled")) {
|
|
1553
|
+
await client.query("COMMIT");
|
|
1554
|
+
return { attempt: taskRowToAttempt(current) };
|
|
1555
|
+
}
|
|
1556
|
+
if (current.cancel_message_id !== null) {
|
|
1557
|
+
const replayed = await client.query(
|
|
1558
|
+
`SELECT ${OUTBOX_COLUMNS} FROM outbox
|
|
1559
|
+
WHERE tenant_id = $1 AND device_id = $2 AND message_id = $3`,
|
|
1560
|
+
[tenant, current.device_id, current.cancel_message_id]
|
|
1561
|
+
);
|
|
1562
|
+
const message = replayed.rows[0];
|
|
1563
|
+
if (message === void 0) {
|
|
1564
|
+
if (current.status === "cancelled") {
|
|
1565
|
+
await client.query("COMMIT");
|
|
1566
|
+
return { attempt: taskRowToAttempt(current) };
|
|
1567
|
+
}
|
|
1568
|
+
throw new Error(`Cancellation delivery ${current.cancel_message_id} is missing for task ${input.taskId}`);
|
|
1569
|
+
}
|
|
1570
|
+
await client.query("COMMIT");
|
|
1571
|
+
return { attempt: taskRowToAttempt(current), message: toMailboxMessage(message) };
|
|
1572
|
+
}
|
|
1573
|
+
const now = this.#clock.now().toISOString();
|
|
1574
|
+
const messageId = input.proposedMessageId;
|
|
1575
|
+
const seq = await allocateMailboxSequence(client, tenant, current.device_id, now);
|
|
1576
|
+
const materialized = await input.materialize(seq, messageId);
|
|
1577
|
+
const insertedMessage = await client.query(
|
|
1578
|
+
`INSERT INTO outbox (${OUTBOX_COLUMNS})
|
|
1579
|
+
VALUES ($1, $2, $3::bigint, $4, $5, $6, $7::bigint, 'pending', $8)
|
|
1580
|
+
RETURNING ${OUTBOX_COLUMNS}`,
|
|
1581
|
+
[
|
|
1582
|
+
tenant,
|
|
1583
|
+
current.device_id,
|
|
1584
|
+
seq,
|
|
1585
|
+
messageId,
|
|
1586
|
+
materialized.body,
|
|
1587
|
+
materialized.bodyHash,
|
|
1588
|
+
materialized.byteSize,
|
|
1589
|
+
now
|
|
1590
|
+
]
|
|
1591
|
+
);
|
|
1592
|
+
const updated = await client.query(
|
|
1593
|
+
`UPDATE task
|
|
1594
|
+
SET status = CASE WHEN owner_device_id IS NULL THEN 'cancelled' ELSE 'cancel_requested' END,
|
|
1595
|
+
cancel_requested_at = $3,
|
|
1596
|
+
cancel_reason = $4,
|
|
1597
|
+
cancel_message_id = $5,
|
|
1598
|
+
updated_at = $3
|
|
1599
|
+
WHERE tenant_id = $1 AND task_id = $2
|
|
1600
|
+
RETURNING ${TASK_SELECT_COLUMNS}`,
|
|
1601
|
+
[tenant, input.taskId, now, input.reason ?? null, messageId]
|
|
1602
|
+
);
|
|
1603
|
+
await client.query("COMMIT");
|
|
1604
|
+
return {
|
|
1605
|
+
attempt: taskRowToAttempt(updated.rows[0]),
|
|
1606
|
+
message: toMailboxMessage(insertedMessage.rows[0])
|
|
1607
|
+
};
|
|
1608
|
+
} catch (cause) {
|
|
1609
|
+
await client.query("ROLLBACK").catch(() => {
|
|
1610
|
+
});
|
|
1611
|
+
throw cause;
|
|
1612
|
+
} finally {
|
|
1613
|
+
client.release();
|
|
1614
|
+
}
|
|
1615
|
+
}
|
|
1616
|
+
};
|
|
1151
1617
|
function toTail(row) {
|
|
1152
1618
|
const entries = parseTimelineEvents(row.entries);
|
|
1153
1619
|
const cursor = activityCursor(entries);
|
|
@@ -1381,6 +1847,87 @@ var PostgresApprovalTimelineStore = class {
|
|
|
1381
1847
|
return row === void 0 ? void 0 : toTail2(row);
|
|
1382
1848
|
}
|
|
1383
1849
|
};
|
|
1850
|
+
var SELECT_COLUMNS4 = [
|
|
1851
|
+
"tenant_id",
|
|
1852
|
+
"device_id",
|
|
1853
|
+
"event_id",
|
|
1854
|
+
"agent_id",
|
|
1855
|
+
"agent_profile_revision",
|
|
1856
|
+
"session_ref",
|
|
1857
|
+
"policy_revision",
|
|
1858
|
+
"cursor",
|
|
1859
|
+
"payload_json",
|
|
1860
|
+
"content_hash",
|
|
1861
|
+
"byte_count",
|
|
1862
|
+
"receipt_id",
|
|
1863
|
+
"recorded_at"
|
|
1864
|
+
].join(", ");
|
|
1865
|
+
function toRecord2(row) {
|
|
1866
|
+
const payload = AgentEgressReliablePayloadSchema.parse({
|
|
1867
|
+
agentRef: { agentId: row.agent_id, profileRevision: row.agent_profile_revision },
|
|
1868
|
+
sessionRef: row.session_ref,
|
|
1869
|
+
policyRevision: row.policy_revision,
|
|
1870
|
+
eventId: row.event_id,
|
|
1871
|
+
cursor: Number(row.cursor),
|
|
1872
|
+
payload: row.payload_json,
|
|
1873
|
+
contentHash: row.content_hash,
|
|
1874
|
+
byteCount: row.byte_count
|
|
1875
|
+
});
|
|
1876
|
+
return {
|
|
1877
|
+
tenantId: row.tenant_id,
|
|
1878
|
+
deviceId: row.device_id,
|
|
1879
|
+
payload,
|
|
1880
|
+
receiptId: row.receipt_id,
|
|
1881
|
+
recordedAt: row.recorded_at.toISOString()
|
|
1882
|
+
};
|
|
1883
|
+
}
|
|
1884
|
+
var PostgresAgentEgressStore = class {
|
|
1885
|
+
constructor(pool, clock) {
|
|
1886
|
+
this.pool = pool;
|
|
1887
|
+
this.clock = clock;
|
|
1888
|
+
}
|
|
1889
|
+
pool;
|
|
1890
|
+
clock;
|
|
1891
|
+
async record(tenant, input) {
|
|
1892
|
+
const payload = AgentEgressReliablePayloadSchema.parse(input.payload);
|
|
1893
|
+
const inserted = await this.pool.query(
|
|
1894
|
+
`INSERT INTO agent_egress_event (${SELECT_COLUMNS4})
|
|
1895
|
+
VALUES ($1, $2, $3::uuid, $4, $5, $6, $7, $8::bigint, $9::jsonb, $10, $11::integer, $12::uuid, $13)
|
|
1896
|
+
ON CONFLICT (tenant_id, device_id, event_id) DO NOTHING
|
|
1897
|
+
RETURNING ${SELECT_COLUMNS4}`,
|
|
1898
|
+
[
|
|
1899
|
+
tenant,
|
|
1900
|
+
input.deviceId,
|
|
1901
|
+
payload.eventId,
|
|
1902
|
+
payload.agentRef.agentId,
|
|
1903
|
+
payload.agentRef.profileRevision,
|
|
1904
|
+
payload.sessionRef,
|
|
1905
|
+
payload.policyRevision,
|
|
1906
|
+
payload.cursor,
|
|
1907
|
+
JSON.stringify(payload.payload),
|
|
1908
|
+
payload.contentHash,
|
|
1909
|
+
payload.byteCount,
|
|
1910
|
+
input.receiptId,
|
|
1911
|
+
this.clock.now().toISOString()
|
|
1912
|
+
]
|
|
1913
|
+
);
|
|
1914
|
+
const row = inserted.rows[0];
|
|
1915
|
+
if (row !== void 0) return { record: toRecord2(row), created: true };
|
|
1916
|
+
const existing = await this.get(tenant, input.deviceId, payload.eventId);
|
|
1917
|
+
if (existing === void 0) throw new Error(`Agent egress ${payload.eventId} vanished during first-write record.`);
|
|
1918
|
+
return { record: existing, created: false };
|
|
1919
|
+
}
|
|
1920
|
+
async get(tenant, deviceId, eventId) {
|
|
1921
|
+
const result = await this.pool.query(
|
|
1922
|
+
`SELECT ${SELECT_COLUMNS4}
|
|
1923
|
+
FROM agent_egress_event
|
|
1924
|
+
WHERE tenant_id = $1 AND device_id = $2 AND event_id = $3::uuid`,
|
|
1925
|
+
[tenant, deviceId, eventId]
|
|
1926
|
+
);
|
|
1927
|
+
const row = result.rows[0];
|
|
1928
|
+
return row === void 0 ? void 0 : toRecord2(row);
|
|
1929
|
+
}
|
|
1930
|
+
};
|
|
1384
1931
|
|
|
1385
1932
|
// src/stores/device-assertion-replay.ts
|
|
1386
1933
|
var PostgresDeviceAssertionReplayAuthority = class {
|
|
@@ -1437,12 +1984,14 @@ function createPostgresCloudStores(options) {
|
|
|
1437
1984
|
return {
|
|
1438
1985
|
activity: new PostgresActivityStore(pool, clock),
|
|
1439
1986
|
approvals: new PostgresApprovalTimelineStore(pool, clock),
|
|
1440
|
-
devices: new PostgresDeviceDirectory(pool),
|
|
1987
|
+
devices: new PostgresDeviceDirectory(pool, clock),
|
|
1441
1988
|
pairingCodes: new PostgresPairingCodeStore(pool, clock),
|
|
1442
1989
|
nonces: new PostgresNonceStore(pool, clock, crypto),
|
|
1443
1990
|
dedup: new PostgresInboundDedupStore(pool),
|
|
1444
1991
|
tasks: new PostgresTaskAttemptStore(pool, clock),
|
|
1992
|
+
cancellations: new PostgresTaskCancellationStore(pool, clock),
|
|
1445
1993
|
receipts: new PostgresRequestReceiptStore(pool, clock),
|
|
1994
|
+
egress: new PostgresAgentEgressStore(pool, clock),
|
|
1446
1995
|
proofReceipts: new PostgresProofRequestReceiptStore(pool, clock),
|
|
1447
1996
|
// A second `PostgresObjectStore` instance, not a shared one: it is a
|
|
1448
1997
|
// stateless wrapper over the pool, so the two read and write the same rows
|
|
@@ -1455,7 +2004,7 @@ function createPostgresCloudStores(options) {
|
|
|
1455
2004
|
rateLimiter: new AllowAllRateLimiter()
|
|
1456
2005
|
};
|
|
1457
2006
|
}
|
|
1458
|
-
var PRESENCE_COLUMNS = "tenant_id, device_id, level, detail, configured_toolsets, observed_at, expires_at";
|
|
2007
|
+
var PRESENCE_COLUMNS = "tenant_id, device_id, level, detail, configured_toolsets, client_version, protocol_versions, runtimes, observed_at, expires_at";
|
|
1459
2008
|
function toHint(row) {
|
|
1460
2009
|
return {
|
|
1461
2010
|
tenantId: row.tenant_id,
|
|
@@ -1463,6 +2012,19 @@ function toHint(row) {
|
|
|
1463
2012
|
level: row.level,
|
|
1464
2013
|
...row.detail === null ? {} : { detail: row.detail },
|
|
1465
2014
|
...row.configured_toolsets === null ? {} : { configuredToolsets: Object.freeze([...row.configured_toolsets]) },
|
|
2015
|
+
...row.client_version === null ? {} : { clientVersion: row.client_version },
|
|
2016
|
+
...row.protocol_versions === null ? {} : { protocolVersions: Object.freeze([...row.protocol_versions]) },
|
|
2017
|
+
...row.runtimes === null ? {} : {
|
|
2018
|
+
runtimes: Object.freeze(
|
|
2019
|
+
row.runtimes.map(
|
|
2020
|
+
(runtime) => Object.freeze({
|
|
2021
|
+
id: runtime.id,
|
|
2022
|
+
...runtime.version === void 0 ? {} : { version: runtime.version },
|
|
2023
|
+
...runtime.authPresent === void 0 ? {} : { authPresent: runtime.authPresent }
|
|
2024
|
+
})
|
|
2025
|
+
)
|
|
2026
|
+
)
|
|
2027
|
+
},
|
|
1466
2028
|
observedAt: row.observed_at,
|
|
1467
2029
|
expiresAt: row.expires_at
|
|
1468
2030
|
};
|
|
@@ -1498,15 +2060,18 @@ var PostgresPresenceStore = class {
|
|
|
1498
2060
|
const allowedBefore = new Date(now.getTime() - input.minimumIntervalMs).toISOString();
|
|
1499
2061
|
const result = await this.#pool.query(
|
|
1500
2062
|
`INSERT INTO device_presence (${PRESENCE_COLUMNS})
|
|
1501
|
-
VALUES ($1, $2, $3, $4, $5::jsonb, $6, $7)
|
|
2063
|
+
VALUES ($1, $2, $3, $4, $5::jsonb, $6, $7::jsonb, $8::jsonb, $9, $10)
|
|
1502
2064
|
ON CONFLICT (tenant_id, device_id) DO UPDATE
|
|
1503
2065
|
SET level = EXCLUDED.level,
|
|
1504
2066
|
detail = EXCLUDED.detail,
|
|
1505
2067
|
configured_toolsets = EXCLUDED.configured_toolsets,
|
|
2068
|
+
client_version = EXCLUDED.client_version,
|
|
2069
|
+
protocol_versions = EXCLUDED.protocol_versions,
|
|
2070
|
+
runtimes = EXCLUDED.runtimes,
|
|
1506
2071
|
observed_at = EXCLUDED.observed_at,
|
|
1507
2072
|
expires_at = EXCLUDED.expires_at
|
|
1508
2073
|
WHERE device_presence.expires_at <= EXCLUDED.observed_at
|
|
1509
|
-
OR device_presence.observed_at <= $
|
|
2074
|
+
OR device_presence.observed_at <= $11
|
|
1510
2075
|
RETURNING ${PRESENCE_COLUMNS}`,
|
|
1511
2076
|
[
|
|
1512
2077
|
tenant,
|
|
@@ -1514,6 +2079,9 @@ var PostgresPresenceStore = class {
|
|
|
1514
2079
|
input.level,
|
|
1515
2080
|
input.detail ?? null,
|
|
1516
2081
|
input.configuredToolsets === void 0 ? null : JSON.stringify(input.configuredToolsets),
|
|
2082
|
+
input.clientVersion ?? null,
|
|
2083
|
+
input.protocolVersions === void 0 ? null : JSON.stringify(input.protocolVersions),
|
|
2084
|
+
input.runtimes === void 0 ? null : JSON.stringify(input.runtimes),
|
|
1517
2085
|
observedAt,
|
|
1518
2086
|
new Date(now.getTime() + input.ttlMs).toISOString(),
|
|
1519
2087
|
allowedBefore
|
|
@@ -1781,233 +2349,6 @@ var PostgresBoardStore = class {
|
|
|
1781
2349
|
return this.#clock.now().toISOString();
|
|
1782
2350
|
}
|
|
1783
2351
|
};
|
|
1784
|
-
|
|
1785
|
-
// src/stores/core/mailbox-sequence.ts
|
|
1786
|
-
async function allocateMailboxSequence(client, tenant, deviceId, now) {
|
|
1787
|
-
const allocation = await client.query(
|
|
1788
|
-
`INSERT INTO device_stream (tenant_id, device_id, next_seq, acked_seq, acked_at)
|
|
1789
|
-
VALUES ($1, $2, 2, 0, $3)
|
|
1790
|
-
ON CONFLICT (tenant_id, device_id) DO UPDATE
|
|
1791
|
-
SET next_seq = device_stream.next_seq + 1
|
|
1792
|
-
RETURNING next_seq - 1 AS seq`,
|
|
1793
|
-
[tenant, deviceId, now]
|
|
1794
|
-
);
|
|
1795
|
-
return Number(allocation.rows[0].seq);
|
|
1796
|
-
}
|
|
1797
|
-
|
|
1798
|
-
// src/stores/core/mailbox.ts
|
|
1799
|
-
var DEFAULT_READ_LIMIT = 50;
|
|
1800
|
-
var OUTBOX_COLUMNS = "tenant_id, device_id, seq, message_id, body, body_hash, byte_size, state, appended_at";
|
|
1801
|
-
function toMessage(row) {
|
|
1802
|
-
return {
|
|
1803
|
-
tenantId: row.tenant_id,
|
|
1804
|
-
deviceId: row.device_id,
|
|
1805
|
-
// `seq` is bigint in the column and `number` on the port, because it is the
|
|
1806
|
-
// envelope `seq` on the wire. The column is wide so the counter cannot wrap
|
|
1807
|
-
// into a redelivery bug; the narrowing happens once, here.
|
|
1808
|
-
seq: Number(row.seq),
|
|
1809
|
-
messageId: row.message_id,
|
|
1810
|
-
body: row.body,
|
|
1811
|
-
bodyHash: row.body_hash,
|
|
1812
|
-
byteSize: row.byte_size,
|
|
1813
|
-
state: row.state,
|
|
1814
|
-
appendedAt: row.appended_at
|
|
1815
|
-
};
|
|
1816
|
-
}
|
|
1817
|
-
var PostgresMailboxStore = class {
|
|
1818
|
-
#pool;
|
|
1819
|
-
#clock;
|
|
1820
|
-
constructor(pool, clock) {
|
|
1821
|
-
this.#pool = pool;
|
|
1822
|
-
this.#clock = clock;
|
|
1823
|
-
}
|
|
1824
|
-
async append(tenant, input) {
|
|
1825
|
-
this.#requireDeviceId(input.deviceId);
|
|
1826
|
-
const client = await this.#pool.connect();
|
|
1827
|
-
try {
|
|
1828
|
-
await client.query("BEGIN");
|
|
1829
|
-
const existing = await client.query(
|
|
1830
|
-
`SELECT ${OUTBOX_COLUMNS} FROM outbox
|
|
1831
|
-
WHERE tenant_id = $1 AND device_id = $2 AND message_id = $3`,
|
|
1832
|
-
[tenant, input.deviceId, input.messageId]
|
|
1833
|
-
);
|
|
1834
|
-
const replayed = existing.rows[0];
|
|
1835
|
-
if (replayed !== void 0) {
|
|
1836
|
-
await client.query("COMMIT");
|
|
1837
|
-
return toMessage(replayed);
|
|
1838
|
-
}
|
|
1839
|
-
const seq = await allocateMailboxSequence(client, tenant, input.deviceId, this.#now());
|
|
1840
|
-
const serializedExisting = await client.query(
|
|
1841
|
-
`SELECT ${OUTBOX_COLUMNS} FROM outbox
|
|
1842
|
-
WHERE tenant_id = $1 AND device_id = $2 AND message_id = $3`,
|
|
1843
|
-
[tenant, input.deviceId, input.messageId]
|
|
1844
|
-
);
|
|
1845
|
-
const winnerAfterLock = serializedExisting.rows[0];
|
|
1846
|
-
if (winnerAfterLock !== void 0) {
|
|
1847
|
-
await client.query("ROLLBACK");
|
|
1848
|
-
return toMessage(winnerAfterLock);
|
|
1849
|
-
}
|
|
1850
|
-
const materialized = await input.materialize(seq);
|
|
1851
|
-
const now = this.#now();
|
|
1852
|
-
const inserted = await client.query(
|
|
1853
|
-
`INSERT INTO outbox (${OUTBOX_COLUMNS})
|
|
1854
|
-
VALUES ($1, $2, $3::bigint, $4, $5, $6, $7::bigint, 'pending', $8)
|
|
1855
|
-
ON CONFLICT (tenant_id, device_id, message_id) DO NOTHING
|
|
1856
|
-
RETURNING ${OUTBOX_COLUMNS}`,
|
|
1857
|
-
[
|
|
1858
|
-
tenant,
|
|
1859
|
-
input.deviceId,
|
|
1860
|
-
seq,
|
|
1861
|
-
input.messageId,
|
|
1862
|
-
materialized.body,
|
|
1863
|
-
materialized.bodyHash,
|
|
1864
|
-
materialized.byteSize,
|
|
1865
|
-
now
|
|
1866
|
-
]
|
|
1867
|
-
);
|
|
1868
|
-
const row = inserted.rows[0];
|
|
1869
|
-
if (row !== void 0) {
|
|
1870
|
-
await client.query("COMMIT");
|
|
1871
|
-
return toMessage(row);
|
|
1872
|
-
}
|
|
1873
|
-
const winner = await client.query(
|
|
1874
|
-
`SELECT ${OUTBOX_COLUMNS} FROM outbox
|
|
1875
|
-
WHERE tenant_id = $1 AND device_id = $2 AND message_id = $3`,
|
|
1876
|
-
[tenant, input.deviceId, input.messageId]
|
|
1877
|
-
);
|
|
1878
|
-
await client.query("ROLLBACK");
|
|
1879
|
-
const won = winner.rows[0];
|
|
1880
|
-
if (won === void 0) {
|
|
1881
|
-
throw new ByokCoreError(
|
|
1882
|
-
"mailbox_message_not_found",
|
|
1883
|
-
`Message ${input.messageId} vanished during an idempotent append.`
|
|
1884
|
-
);
|
|
1885
|
-
}
|
|
1886
|
-
return toMessage(won);
|
|
1887
|
-
} catch (cause) {
|
|
1888
|
-
await client.query("ROLLBACK").catch(() => {
|
|
1889
|
-
});
|
|
1890
|
-
throw cause;
|
|
1891
|
-
} finally {
|
|
1892
|
-
client.release();
|
|
1893
|
-
}
|
|
1894
|
-
}
|
|
1895
|
-
async readAfter(tenant, query) {
|
|
1896
|
-
const limit = query.limit ?? DEFAULT_READ_LIMIT;
|
|
1897
|
-
const result = await this.#pool.query(
|
|
1898
|
-
`SELECT ${OUTBOX_COLUMNS} FROM outbox
|
|
1899
|
-
WHERE tenant_id = $1 AND device_id = $2 AND state = 'pending' AND seq > $3::bigint
|
|
1900
|
-
ORDER BY seq
|
|
1901
|
-
LIMIT $4`,
|
|
1902
|
-
[tenant, query.deviceId, query.afterSeq, limit + 1]
|
|
1903
|
-
);
|
|
1904
|
-
const page = result.rows.slice(0, limit).map(toMessage);
|
|
1905
|
-
return {
|
|
1906
|
-
messages: page,
|
|
1907
|
-
// Nothing above was mutated, so an identical call replays the same page.
|
|
1908
|
-
// The returned position is a READ cursor and moves no ack.
|
|
1909
|
-
nextSeq: page.at(-1)?.seq ?? query.afterSeq,
|
|
1910
|
-
hasMore: result.rows.length > page.length
|
|
1911
|
-
};
|
|
1912
|
-
}
|
|
1913
|
-
async advanceCursor(tenant, input) {
|
|
1914
|
-
this.#requireDeviceId(input.deviceId);
|
|
1915
|
-
const now = this.#now();
|
|
1916
|
-
const moved = await this.#pool.query(
|
|
1917
|
-
`WITH moved AS (
|
|
1918
|
-
INSERT INTO device_stream (tenant_id, device_id, next_seq, acked_seq, acked_at)
|
|
1919
|
-
VALUES ($1, $2, 1, $3::bigint, $4)
|
|
1920
|
-
ON CONFLICT (tenant_id, device_id) DO UPDATE
|
|
1921
|
-
SET acked_seq = EXCLUDED.acked_seq, acked_at = EXCLUDED.acked_at
|
|
1922
|
-
WHERE device_stream.acked_seq <= EXCLUDED.acked_seq
|
|
1923
|
-
RETURNING acked_seq, acked_at
|
|
1924
|
-
), marked AS (
|
|
1925
|
-
UPDATE outbox
|
|
1926
|
-
SET state = 'acked'
|
|
1927
|
-
WHERE tenant_id = $1 AND device_id = $2 AND state = 'pending'
|
|
1928
|
-
AND seq <= (SELECT acked_seq FROM moved)
|
|
1929
|
-
RETURNING 1
|
|
1930
|
-
)
|
|
1931
|
-
SELECT acked_seq, acked_at FROM moved`,
|
|
1932
|
-
[tenant, input.deviceId, input.ackedSeq, now]
|
|
1933
|
-
);
|
|
1934
|
-
const row = moved.rows[0];
|
|
1935
|
-
if (row !== void 0) {
|
|
1936
|
-
return {
|
|
1937
|
-
tenantId: tenant,
|
|
1938
|
-
deviceId: input.deviceId,
|
|
1939
|
-
ackedSeq: Number(row.acked_seq),
|
|
1940
|
-
updatedAt: row.acked_at ?? now
|
|
1941
|
-
};
|
|
1942
|
-
}
|
|
1943
|
-
const current = await this.readCursor(tenant, input.deviceId);
|
|
1944
|
-
throw new CoreConflictError(
|
|
1945
|
-
"mailbox_cursor_regression",
|
|
1946
|
-
`Cursor for device ${input.deviceId} is at ${current.ackedSeq}; refusing to move it back to ${input.ackedSeq}.`,
|
|
1947
|
-
current,
|
|
1948
|
-
this.#now()
|
|
1949
|
-
);
|
|
1950
|
-
}
|
|
1951
|
-
async readCursor(tenant, deviceId) {
|
|
1952
|
-
const result = await this.#pool.query(
|
|
1953
|
-
`SELECT acked_seq, acked_at FROM device_stream
|
|
1954
|
-
WHERE tenant_id = $1 AND device_id = $2`,
|
|
1955
|
-
[tenant, deviceId]
|
|
1956
|
-
);
|
|
1957
|
-
const row = result.rows[0];
|
|
1958
|
-
return {
|
|
1959
|
-
tenantId: tenant,
|
|
1960
|
-
deviceId,
|
|
1961
|
-
ackedSeq: row === void 0 ? 0 : Number(row.acked_seq),
|
|
1962
|
-
updatedAt: row?.acked_at ?? this.#now()
|
|
1963
|
-
};
|
|
1964
|
-
}
|
|
1965
|
-
async collectRetired(tenant, input) {
|
|
1966
|
-
assertCanonicalTimestamp(input.ackedBefore, "ackedBefore");
|
|
1967
|
-
assertCanonicalTimestamp(input.expireUnackedBefore, "expireUnackedBefore");
|
|
1968
|
-
const swept = await this.#pool.query(
|
|
1969
|
-
`WITH deleted AS (
|
|
1970
|
-
DELETE FROM outbox
|
|
1971
|
-
WHERE tenant_id = $1
|
|
1972
|
-
AND ($2::text IS NULL OR device_id = $2::text)
|
|
1973
|
-
AND state = 'acked'
|
|
1974
|
-
AND appended_at < $3
|
|
1975
|
-
RETURNING byte_size
|
|
1976
|
-
), expired AS (
|
|
1977
|
-
UPDATE outbox
|
|
1978
|
-
SET state = 'expired'
|
|
1979
|
-
WHERE tenant_id = $1
|
|
1980
|
-
AND ($2::text IS NULL OR device_id = $2::text)
|
|
1981
|
-
AND state = 'pending'
|
|
1982
|
-
AND appended_at < $4
|
|
1983
|
-
RETURNING 1
|
|
1984
|
-
)
|
|
1985
|
-
SELECT (SELECT count(*) FROM deleted) AS deleted_count,
|
|
1986
|
-
(SELECT count(*) FROM expired) AS expired_count,
|
|
1987
|
-
(SELECT COALESCE(SUM(byte_size), 0) FROM deleted)::bigint AS released_bytes`,
|
|
1988
|
-
[tenant, input.deviceId ?? null, input.ackedBefore, input.expireUnackedBefore]
|
|
1989
|
-
);
|
|
1990
|
-
const row = swept.rows[0];
|
|
1991
|
-
return {
|
|
1992
|
-
deletedCount: Number(row.deleted_count),
|
|
1993
|
-
expiredCount: Number(row.expired_count),
|
|
1994
|
-
releasedBytes: row.released_bytes
|
|
1995
|
-
};
|
|
1996
|
-
}
|
|
1997
|
-
/**
|
|
1998
|
-
* The in-memory reference refuses an empty device id rather than opening a
|
|
1999
|
-
* mailbox nothing can address. Kept here so the two compositions answer the
|
|
2000
|
-
* same way; the table itself would happily store the row.
|
|
2001
|
-
*/
|
|
2002
|
-
#requireDeviceId(deviceId) {
|
|
2003
|
-
if (deviceId.length === 0) {
|
|
2004
|
-
throw new ByokCoreError("mailbox_message_not_found", "Device id must not be empty.");
|
|
2005
|
-
}
|
|
2006
|
-
}
|
|
2007
|
-
#now() {
|
|
2008
|
-
return this.#clock.now().toISOString();
|
|
2009
|
-
}
|
|
2010
|
-
};
|
|
2011
2352
|
var WARNING_NUMERATOR = 80n;
|
|
2012
2353
|
var WARNING_DENOMINATOR = 100n;
|
|
2013
2354
|
var ENTITLEMENT_COLUMNS = "tenant_id, version, hard_limit_bytes, max_object_bytes, max_inline_bytes, mailbox_limit_bytes, retention_policy_id, downgrade_grace_until";
|
|
@@ -2727,7 +3068,7 @@ var RECORD_COLUMNS = "tenant_id, kind, subject_id, rev, content_hash, byte_size,
|
|
|
2727
3068
|
function toBody(row) {
|
|
2728
3069
|
return row.body_kind === "inline" ? { kind: "inline", body: row.body_inline ?? "" } : { kind: "object", hash: row.body_object_hash ?? "" };
|
|
2729
3070
|
}
|
|
2730
|
-
function
|
|
3071
|
+
function toRecord3(row) {
|
|
2731
3072
|
return {
|
|
2732
3073
|
tenantId: row.tenant_id,
|
|
2733
3074
|
kind: row.kind,
|
|
@@ -2772,7 +3113,7 @@ var PostgresTruthStore = class {
|
|
|
2772
3113
|
]
|
|
2773
3114
|
);
|
|
2774
3115
|
const row = inserted.rows[0];
|
|
2775
|
-
if (row !== void 0) return
|
|
3116
|
+
if (row !== void 0) return toRecord3(row);
|
|
2776
3117
|
const existing = await this.getRecord(tenant, {
|
|
2777
3118
|
kind: "task.terminal",
|
|
2778
3119
|
recordKey: input.taskId
|
|
@@ -2838,7 +3179,7 @@ var PostgresTruthStore = class {
|
|
|
2838
3179
|
]
|
|
2839
3180
|
);
|
|
2840
3181
|
const row = written.rows[0];
|
|
2841
|
-
if (row !== void 0) return
|
|
3182
|
+
if (row !== void 0) return toRecord3(row);
|
|
2842
3183
|
const current = await this.getRecord(tenant, {
|
|
2843
3184
|
kind: input.kind,
|
|
2844
3185
|
recordKey: input.recordKey
|
|
@@ -2857,7 +3198,7 @@ var PostgresTruthStore = class {
|
|
|
2857
3198
|
[tenant, selector.kind, selector.recordKey]
|
|
2858
3199
|
);
|
|
2859
3200
|
const row = result.rows[0];
|
|
2860
|
-
return row === void 0 ? void 0 :
|
|
3201
|
+
return row === void 0 ? void 0 : toRecord3(row);
|
|
2861
3202
|
}
|
|
2862
3203
|
async listManifest(tenant, query) {
|
|
2863
3204
|
const result = await this.#pool.query(
|
|
@@ -2905,7 +3246,7 @@ var RECEIPT_COLUMNS = "tenant_id, device_id, request_id, operation, resource, bo
|
|
|
2905
3246
|
function toBody2(row) {
|
|
2906
3247
|
return row.body_kind === "inline" ? { kind: "inline", body: row.body_inline ?? "" } : { kind: "object", hash: row.body_object_hash ?? "" };
|
|
2907
3248
|
}
|
|
2908
|
-
function
|
|
3249
|
+
function toRecord4(tenant, row) {
|
|
2909
3250
|
return {
|
|
2910
3251
|
tenantId: tenant,
|
|
2911
3252
|
kind: row.kind,
|
|
@@ -3082,7 +3423,7 @@ var PostgresTruthCommitter = class {
|
|
|
3082
3423
|
);
|
|
3083
3424
|
current.set(
|
|
3084
3425
|
writeKey(write),
|
|
3085
|
-
result.rows[0] === void 0 ? void 0 :
|
|
3426
|
+
result.rows[0] === void 0 ? void 0 : toRecord4(tenant, result.rows[0])
|
|
3086
3427
|
);
|
|
3087
3428
|
}
|
|
3088
3429
|
return current;
|
|
@@ -3295,7 +3636,7 @@ var PostgresTruthCommitter = class {
|
|
|
3295
3636
|
applied.push({
|
|
3296
3637
|
input: write,
|
|
3297
3638
|
before,
|
|
3298
|
-
record:
|
|
3639
|
+
record: toRecord4(tenant, result.rows[0]),
|
|
3299
3640
|
mutated: true
|
|
3300
3641
|
});
|
|
3301
3642
|
}
|
|
@@ -3353,6 +3694,6 @@ var PostgresTruthCommitter = class {
|
|
|
3353
3694
|
}
|
|
3354
3695
|
};
|
|
3355
3696
|
|
|
3356
|
-
export { DEFAULT_MAX_ATTEMPTS, DEFAULT_PRESIGN_TTL_SECONDS, DEFAULT_RETRY_DELAY_MS, MAX_PRESIGN_TTL_SECONDS, MIN_PRESIGN_TTL_SECONDS, ObjectStoreRequestError, PostgresActivityStore, PostgresApprovalTimelineStore, PostgresBoardStore, PostgresDeviceAssertionReplayAuthority, PostgresDeviceDirectory, PostgresInboundDedupStore, PostgresMailboxStore, PostgresNonceStore, PostgresObjectStore, PostgresPairingCodeStore, PostgresPresenceStore, PostgresProofRequestReceiptStore, PostgresQuotaStore, PostgresRequestReceiptStore, PostgresSkillPackStore, PostgresTaskAttemptStore, PostgresTruthCommitter, PostgresTruthStore, R2BlobStoreError, R2CloudBlobStore, R2ObjectMaintenanceStore, R2_BLOB_ERROR_CODES, createByokPool, createPostgresCloudStores, createPostgresCoreStores };
|
|
3697
|
+
export { DEFAULT_MAX_ATTEMPTS, DEFAULT_PRESIGN_TTL_SECONDS, DEFAULT_RETRY_DELAY_MS, MAX_PRESIGN_TTL_SECONDS, MIN_PRESIGN_TTL_SECONDS, ObjectStoreRequestError, PostgresActivityStore, PostgresApprovalTimelineStore, PostgresBoardStore, PostgresDeviceAssertionReplayAuthority, PostgresDeviceDirectory, PostgresInboundDedupStore, PostgresMailboxStore, PostgresNonceStore, PostgresObjectStore, PostgresPairingCodeStore, PostgresPresenceStore, PostgresProofRequestReceiptStore, PostgresQuotaStore, PostgresRequestReceiptStore, PostgresSkillPackStore, PostgresTaskAttemptStore, PostgresTaskCancellationStore, PostgresTruthCommitter, PostgresTruthStore, R2BlobStoreError, R2CloudBlobStore, R2ObjectMaintenanceStore, R2_BLOB_ERROR_CODES, createByokPool, createPostgresCloudStores, createPostgresCoreStores };
|
|
3357
3698
|
//# sourceMappingURL=runtime.js.map
|
|
3358
3699
|
//# sourceMappingURL=runtime.js.map
|