@fabricorg/platform-host 8.9.0 → 8.10.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/CHANGELOG.md +12 -0
- package/README.md +23 -2
- package/dist/index.cjs +391 -123
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +120 -72
- package/dist/index.d.ts +120 -72
- package/dist/index.js +390 -124
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
package/dist/index.js
CHANGED
|
@@ -3,6 +3,201 @@ import { AdapterCircuitBreakerRegistry, AdapterRegistry, FABRIC_GOVERNANCE_CONTR
|
|
|
3
3
|
import { performance as performance$1 } from "perf_hooks";
|
|
4
4
|
import { assertAssemblyRuntimeCompatible } from "@fabricorg/assembly";
|
|
5
5
|
import { createHash } from "crypto";
|
|
6
|
+
//#region src/erasure-types.ts
|
|
7
|
+
const HOST_RECORDS_ERASED_EVENT = "HostRecordsErased";
|
|
8
|
+
/** Sentinel replacing required payload digests after stored-record erasure. */
|
|
9
|
+
const ERASED_DIGEST = "erased";
|
|
10
|
+
//#endregion
|
|
11
|
+
//#region src/types.ts
|
|
12
|
+
/** Durable Host lifecycle generation, independent from the npm package version. */
|
|
13
|
+
const PLATFORM_HOST_CONTRACT_VERSION = 2;
|
|
14
|
+
var IdempotencyConflictError = class extends Error {
|
|
15
|
+
conflict;
|
|
16
|
+
code = "IDEMPOTENCY_CONFLICT";
|
|
17
|
+
constructor(conflict) {
|
|
18
|
+
super(`Idempotency key "${conflict.idempotencyKey}" conflicts with invocation ${conflict.existingInvocationId}: ${conflict.reasons.join(", ")}`);
|
|
19
|
+
this.conflict = conflict;
|
|
20
|
+
this.name = "IdempotencyConflictError";
|
|
21
|
+
}
|
|
22
|
+
};
|
|
23
|
+
/**
|
|
24
|
+
* Thrown by `submitAction` when a declared admission check did not admit the
|
|
25
|
+
* submission. By default Host writes nothing for it. With recordRefusals
|
|
26
|
+
* enabled and an idempotency key, a definitive refusal can be recorded;
|
|
27
|
+
* `recorded` confirms persistence of the terminal refusal. The message holds
|
|
28
|
+
* the action id, the refusal and, for `refused`, the check's fixed code. It omits parameter
|
|
29
|
+
* payloads and caught callback error text.
|
|
30
|
+
*/
|
|
31
|
+
var ActionAdmissionError = class extends Error {
|
|
32
|
+
actionId;
|
|
33
|
+
refusal;
|
|
34
|
+
refusalCode;
|
|
35
|
+
recorded;
|
|
36
|
+
code = "ACTION_ADMISSION_REFUSED";
|
|
37
|
+
constructor(actionId, refusal, refusalCode, recorded = false) {
|
|
38
|
+
super(`Action ${actionId} submission was not admitted (${refusal}${refusalCode ? `: ${refusalCode}` : ""}); ${recorded ? "the refusal was recorded under the idempotency key" : "nothing was recorded"}.`);
|
|
39
|
+
this.actionId = actionId;
|
|
40
|
+
this.refusal = refusal;
|
|
41
|
+
this.refusalCode = refusalCode;
|
|
42
|
+
this.recorded = recorded;
|
|
43
|
+
this.name = "ActionAdmissionError";
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
/** A definitive authorization-boundary refusal before an invocation is created. */
|
|
47
|
+
var SubmissionRefusalError = class extends Error {
|
|
48
|
+
actionId;
|
|
49
|
+
refusal;
|
|
50
|
+
recorded;
|
|
51
|
+
code = "ACTION_SUBMISSION_REFUSED";
|
|
52
|
+
constructor(actionId, refusal, message, recorded = false) {
|
|
53
|
+
super(recorded ? `Action ${actionId} submission was refused (${refusal}); the refusal was recorded under the idempotency key.` : message);
|
|
54
|
+
this.actionId = actionId;
|
|
55
|
+
this.refusal = refusal;
|
|
56
|
+
this.recorded = recorded;
|
|
57
|
+
this.name = "SubmissionRefusalError";
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
/**
|
|
61
|
+
* Why a completion was not applied, typed so a webhook handler can answer
|
|
62
|
+
* the external system correctly: `retryable` means the invocation is still
|
|
63
|
+
* being finalized and the same callback will be accepted once it parks, so
|
|
64
|
+
* answer with something the sender retries; anything else is permanent and
|
|
65
|
+
* a retry will get the same refusal without a configuration/input change.
|
|
66
|
+
* `redaction_failed` means completion result or error-text redaction failed before writes;
|
|
67
|
+
* it is not retryable.
|
|
68
|
+
*/
|
|
69
|
+
var ExternalCompletionError = class extends Error {
|
|
70
|
+
refusal;
|
|
71
|
+
code = "EXTERNAL_COMPLETION_REFUSED";
|
|
72
|
+
constructor(refusal, message) {
|
|
73
|
+
super(message);
|
|
74
|
+
this.refusal = refusal;
|
|
75
|
+
this.name = "ExternalCompletionError";
|
|
76
|
+
}
|
|
77
|
+
get retryable() {
|
|
78
|
+
return this.refusal === "still_executing";
|
|
79
|
+
}
|
|
80
|
+
};
|
|
81
|
+
/** Maximum serialized UTF-8 evidence size. */
|
|
82
|
+
const MAX_APPROVAL_EVIDENCE_BYTES = 16384;
|
|
83
|
+
const APPROVAL_REQUESTED_EVENT = "ApprovalRequested";
|
|
84
|
+
const APPROVAL_DECIDED_EVENT = "ApprovalDecided";
|
|
85
|
+
const APPROVAL_EXPIRED_EVENT = "ApprovalExpired";
|
|
86
|
+
const APPROVAL_DECISION_REFUSED_EVENT = "ApprovalDecisionRefused";
|
|
87
|
+
//#endregion
|
|
88
|
+
//#region src/submission-refusal.ts
|
|
89
|
+
/** Validate and copy only the descriptor's declared fields, on both sides of storage. */
|
|
90
|
+
function submissionRefusalDescriptor(value) {
|
|
91
|
+
if (!value || typeof value !== "object") throw new Error("Invalid submission refusal descriptor.");
|
|
92
|
+
const fields = Object.getOwnPropertyDescriptors(value);
|
|
93
|
+
const data = (key) => fields[key]?.value;
|
|
94
|
+
const kind = data("class");
|
|
95
|
+
const keys = Reflect.ownKeys(value);
|
|
96
|
+
if ((kind === "entitlement" || kind === "authorization") && keys.length === 1) return { class: kind };
|
|
97
|
+
if (kind === "admission" && keys.every((key) => key === "class" || key === "refusal" || key === "refusalCode")) {
|
|
98
|
+
const refusal = data("refusal");
|
|
99
|
+
const code = data("refusalCode");
|
|
100
|
+
if (refusal === "invalid_parameters" && !Object.hasOwn(fields, "refusalCode")) return {
|
|
101
|
+
class: kind,
|
|
102
|
+
refusal
|
|
103
|
+
};
|
|
104
|
+
if (refusal === "refused" && (!Object.hasOwn(fields, "refusalCode") || typeof code === "string" && /^[a-z][A-Za-z0-9_.:-]{0,127}$/.test(code))) return {
|
|
105
|
+
class: kind,
|
|
106
|
+
refusal,
|
|
107
|
+
...code === void 0 ? {} : { refusalCode: code }
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
throw new Error("Invalid submission refusal descriptor.");
|
|
111
|
+
}
|
|
112
|
+
function validateRefusalRecord(record) {
|
|
113
|
+
if (record?.status === "submission_refused") submissionRefusalDescriptor(record.submissionRefusal);
|
|
114
|
+
return record;
|
|
115
|
+
}
|
|
116
|
+
/** Reconstruct errors from fixed validated evidence, without row error text or parameters. */
|
|
117
|
+
function recordedSubmissionRefusal(record) {
|
|
118
|
+
const descriptor = submissionRefusalDescriptor(record.submissionRefusal);
|
|
119
|
+
return descriptor.class === "admission" ? new ActionAdmissionError(record.actionId, descriptor.refusal, descriptor.refusalCode, true) : new SubmissionRefusalError(record.actionId, descriptor.class, "", true);
|
|
120
|
+
}
|
|
121
|
+
/** Refusals enter the ledger through the narrow insert, rather than generic status writes. */
|
|
122
|
+
function assertInvocationWriteStatus(status) {
|
|
123
|
+
if (status === "submission_refused") throw new Error("submission_refused can only be inserted by recordSubmissionRefusal.");
|
|
124
|
+
}
|
|
125
|
+
//#endregion
|
|
126
|
+
//#region src/submission-outcome.ts
|
|
127
|
+
var SubmissionOutcomeUnsupportedError = class extends Error {
|
|
128
|
+
constructor() {
|
|
129
|
+
super("Store lacks getSubmissionOutcomeByKey.");
|
|
130
|
+
this.name = "SubmissionOutcomeUnsupportedError";
|
|
131
|
+
}
|
|
132
|
+
};
|
|
133
|
+
/** Copy receipt fields only, without exposing execution data. */
|
|
134
|
+
function submissionOutcomeProjection(row) {
|
|
135
|
+
return structuredClone({
|
|
136
|
+
tenantId: row.tenantId,
|
|
137
|
+
spaceId: row.spaceId,
|
|
138
|
+
actionId: row.actionId,
|
|
139
|
+
idempotencyKey: row.idempotencyKey,
|
|
140
|
+
id: row.id,
|
|
141
|
+
actionVersion: row.actionVersion,
|
|
142
|
+
actorId: row.actorId,
|
|
143
|
+
actorType: row.actorType,
|
|
144
|
+
authorizationBindingId: row.authorizationBindingId,
|
|
145
|
+
idempotencyActorId: row.idempotencyActorId,
|
|
146
|
+
idempotencyAuthorizationBindingId: row.idempotencyAuthorizationBindingId,
|
|
147
|
+
status: row.status,
|
|
148
|
+
submissionRefusal: row.submissionRefusal,
|
|
149
|
+
storedRecordsErased: row.storedRecordsErased
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
function createSubmissionOutcomeReader(store) {
|
|
153
|
+
return { async readSubmissionOutcome(input) {
|
|
154
|
+
const snapshot = {
|
|
155
|
+
tenantId: input.tenantId,
|
|
156
|
+
spaceId: input.spaceId,
|
|
157
|
+
actionId: input.actionId,
|
|
158
|
+
idempotencyKey: input.idempotencyKey,
|
|
159
|
+
actorId: input.actorId,
|
|
160
|
+
actorType: input.actorType,
|
|
161
|
+
authorizationBindingId: input.authorizationBindingId
|
|
162
|
+
};
|
|
163
|
+
for (const field of [
|
|
164
|
+
"tenantId",
|
|
165
|
+
"spaceId",
|
|
166
|
+
"actionId",
|
|
167
|
+
"idempotencyKey",
|
|
168
|
+
"actorId",
|
|
169
|
+
"actorType"
|
|
170
|
+
]) if (typeof snapshot[field] !== "string") throw new TypeError(`Invalid ${field}.`);
|
|
171
|
+
if (!store.getSubmissionOutcomeByKey) throw new SubmissionOutcomeUnsupportedError();
|
|
172
|
+
const row = await store.getSubmissionOutcomeByKey({
|
|
173
|
+
tenantId: snapshot.tenantId,
|
|
174
|
+
spaceId: snapshot.spaceId,
|
|
175
|
+
actionId: snapshot.actionId,
|
|
176
|
+
idempotencyKey: snapshot.idempotencyKey
|
|
177
|
+
});
|
|
178
|
+
if (!row || (row.idempotencyActorId ?? row.actorId) !== snapshot.actorId || row.actorType !== snapshot.actorType || ((row.idempotencyAuthorizationBindingId ?? row.authorizationBindingId) || void 0) !== (snapshot.authorizationBindingId || void 0)) return { kind: "no_record" };
|
|
179
|
+
const identity = {
|
|
180
|
+
actionInvocationId: row.id,
|
|
181
|
+
actionVersion: row.actionVersion
|
|
182
|
+
};
|
|
183
|
+
if (row.storedRecordsErased) return {
|
|
184
|
+
...identity,
|
|
185
|
+
kind: "records_erased",
|
|
186
|
+
recordedStatus: row.status
|
|
187
|
+
};
|
|
188
|
+
if (row.status === "submission_refused") return {
|
|
189
|
+
...identity,
|
|
190
|
+
kind: "submission_refused",
|
|
191
|
+
submissionRefusal: submissionRefusalDescriptor(row.submissionRefusal)
|
|
192
|
+
};
|
|
193
|
+
return {
|
|
194
|
+
...identity,
|
|
195
|
+
kind: "invocation",
|
|
196
|
+
status: row.status
|
|
197
|
+
};
|
|
198
|
+
} };
|
|
199
|
+
}
|
|
200
|
+
//#endregion
|
|
6
201
|
//#region src/transaction-rollback.ts
|
|
7
202
|
/** Evidence from a transaction provider that rollback completed successfully. */
|
|
8
203
|
var ConfirmedTransactionRollback = class extends Error {
|
|
@@ -181,126 +376,6 @@ function createStoredRecordErasureHost(store, authorization) {
|
|
|
181
376
|
} };
|
|
182
377
|
}
|
|
183
378
|
//#endregion
|
|
184
|
-
//#region src/erasure-types.ts
|
|
185
|
-
const HOST_RECORDS_ERASED_EVENT = "HostRecordsErased";
|
|
186
|
-
/** Sentinel replacing required payload digests after stored-record erasure. */
|
|
187
|
-
const ERASED_DIGEST = "erased";
|
|
188
|
-
//#endregion
|
|
189
|
-
//#region src/types.ts
|
|
190
|
-
/** Durable Host lifecycle generation, independent from the npm package version. */
|
|
191
|
-
const PLATFORM_HOST_CONTRACT_VERSION = 2;
|
|
192
|
-
var IdempotencyConflictError = class extends Error {
|
|
193
|
-
conflict;
|
|
194
|
-
code = "IDEMPOTENCY_CONFLICT";
|
|
195
|
-
constructor(conflict) {
|
|
196
|
-
super(`Idempotency key "${conflict.idempotencyKey}" conflicts with invocation ${conflict.existingInvocationId}: ${conflict.reasons.join(", ")}`);
|
|
197
|
-
this.conflict = conflict;
|
|
198
|
-
this.name = "IdempotencyConflictError";
|
|
199
|
-
}
|
|
200
|
-
};
|
|
201
|
-
/**
|
|
202
|
-
* Thrown by `submitAction` when a declared admission check did not admit the
|
|
203
|
-
* submission. By default Host writes nothing for it. With recordRefusals
|
|
204
|
-
* enabled and an idempotency key, a definitive refusal can be recorded;
|
|
205
|
-
* `recorded` confirms persistence of the terminal refusal. The message holds
|
|
206
|
-
* the action id, the refusal and, for `refused`, the check's fixed code. It omits parameter
|
|
207
|
-
* payloads and caught callback error text.
|
|
208
|
-
*/
|
|
209
|
-
var ActionAdmissionError = class extends Error {
|
|
210
|
-
actionId;
|
|
211
|
-
refusal;
|
|
212
|
-
refusalCode;
|
|
213
|
-
recorded;
|
|
214
|
-
code = "ACTION_ADMISSION_REFUSED";
|
|
215
|
-
constructor(actionId, refusal, refusalCode, recorded = false) {
|
|
216
|
-
super(`Action ${actionId} submission was not admitted (${refusal}${refusalCode ? `: ${refusalCode}` : ""}); ${recorded ? "the refusal was recorded under the idempotency key" : "nothing was recorded"}.`);
|
|
217
|
-
this.actionId = actionId;
|
|
218
|
-
this.refusal = refusal;
|
|
219
|
-
this.refusalCode = refusalCode;
|
|
220
|
-
this.recorded = recorded;
|
|
221
|
-
this.name = "ActionAdmissionError";
|
|
222
|
-
}
|
|
223
|
-
};
|
|
224
|
-
/** A definitive authorization-boundary refusal before an invocation is created. */
|
|
225
|
-
var SubmissionRefusalError = class extends Error {
|
|
226
|
-
actionId;
|
|
227
|
-
refusal;
|
|
228
|
-
recorded;
|
|
229
|
-
code = "ACTION_SUBMISSION_REFUSED";
|
|
230
|
-
constructor(actionId, refusal, message, recorded = false) {
|
|
231
|
-
super(recorded ? `Action ${actionId} submission was refused (${refusal}); the refusal was recorded under the idempotency key.` : message);
|
|
232
|
-
this.actionId = actionId;
|
|
233
|
-
this.refusal = refusal;
|
|
234
|
-
this.recorded = recorded;
|
|
235
|
-
this.name = "SubmissionRefusalError";
|
|
236
|
-
}
|
|
237
|
-
};
|
|
238
|
-
/**
|
|
239
|
-
* Why a completion was not applied, typed so a webhook handler can answer
|
|
240
|
-
* the external system correctly: `retryable` means the invocation is still
|
|
241
|
-
* being finalized and the same callback will be accepted once it parks, so
|
|
242
|
-
* answer with something the sender retries; anything else is permanent and
|
|
243
|
-
* a retry will get the same refusal without a configuration/input change.
|
|
244
|
-
* `redaction_failed` means completion result or error-text redaction failed before writes;
|
|
245
|
-
* it is not retryable.
|
|
246
|
-
*/
|
|
247
|
-
var ExternalCompletionError = class extends Error {
|
|
248
|
-
refusal;
|
|
249
|
-
code = "EXTERNAL_COMPLETION_REFUSED";
|
|
250
|
-
constructor(refusal, message) {
|
|
251
|
-
super(message);
|
|
252
|
-
this.refusal = refusal;
|
|
253
|
-
this.name = "ExternalCompletionError";
|
|
254
|
-
}
|
|
255
|
-
get retryable() {
|
|
256
|
-
return this.refusal === "still_executing";
|
|
257
|
-
}
|
|
258
|
-
};
|
|
259
|
-
/** Maximum serialized UTF-8 evidence size. */
|
|
260
|
-
const MAX_APPROVAL_EVIDENCE_BYTES = 16384;
|
|
261
|
-
const APPROVAL_REQUESTED_EVENT = "ApprovalRequested";
|
|
262
|
-
const APPROVAL_DECIDED_EVENT = "ApprovalDecided";
|
|
263
|
-
const APPROVAL_EXPIRED_EVENT = "ApprovalExpired";
|
|
264
|
-
const APPROVAL_DECISION_REFUSED_EVENT = "ApprovalDecisionRefused";
|
|
265
|
-
//#endregion
|
|
266
|
-
//#region src/submission-refusal.ts
|
|
267
|
-
/** Validate and copy only the descriptor's declared fields, on both sides of storage. */
|
|
268
|
-
function submissionRefusalDescriptor(value) {
|
|
269
|
-
if (!value || typeof value !== "object") throw new Error("Invalid submission refusal descriptor.");
|
|
270
|
-
const fields = Object.getOwnPropertyDescriptors(value);
|
|
271
|
-
const data = (key) => fields[key]?.value;
|
|
272
|
-
const kind = data("class");
|
|
273
|
-
const keys = Reflect.ownKeys(value);
|
|
274
|
-
if ((kind === "entitlement" || kind === "authorization") && keys.length === 1) return { class: kind };
|
|
275
|
-
if (kind === "admission" && keys.every((key) => key === "class" || key === "refusal" || key === "refusalCode")) {
|
|
276
|
-
const refusal = data("refusal");
|
|
277
|
-
const code = data("refusalCode");
|
|
278
|
-
if (refusal === "invalid_parameters" && !Object.hasOwn(fields, "refusalCode")) return {
|
|
279
|
-
class: kind,
|
|
280
|
-
refusal
|
|
281
|
-
};
|
|
282
|
-
if (refusal === "refused" && (!Object.hasOwn(fields, "refusalCode") || typeof code === "string" && /^[a-z][A-Za-z0-9_.:-]{0,127}$/.test(code))) return {
|
|
283
|
-
class: kind,
|
|
284
|
-
refusal,
|
|
285
|
-
...code === void 0 ? {} : { refusalCode: code }
|
|
286
|
-
};
|
|
287
|
-
}
|
|
288
|
-
throw new Error("Invalid submission refusal descriptor.");
|
|
289
|
-
}
|
|
290
|
-
function validateRefusalRecord(record) {
|
|
291
|
-
if (record?.status === "submission_refused") submissionRefusalDescriptor(record.submissionRefusal);
|
|
292
|
-
return record;
|
|
293
|
-
}
|
|
294
|
-
/** Reconstruct errors from fixed validated evidence, without row error text or parameters. */
|
|
295
|
-
function recordedSubmissionRefusal(record) {
|
|
296
|
-
const descriptor = submissionRefusalDescriptor(record.submissionRefusal);
|
|
297
|
-
return descriptor.class === "admission" ? new ActionAdmissionError(record.actionId, descriptor.refusal, descriptor.refusalCode, true) : new SubmissionRefusalError(record.actionId, descriptor.class, "", true);
|
|
298
|
-
}
|
|
299
|
-
/** Refusals enter the ledger through the narrow insert, rather than generic status writes. */
|
|
300
|
-
function assertInvocationWriteStatus(status) {
|
|
301
|
-
if (status === "submission_refused") throw new Error("submission_refused can only be inserted by recordSubmissionRefusal.");
|
|
302
|
-
}
|
|
303
|
-
//#endregion
|
|
304
379
|
//#region src/action-finalization.ts
|
|
305
380
|
/** Remove private preparation and restore its compatible protocol on settlement. */
|
|
306
381
|
function finalizationSettlementPatch(record, patch) {
|
|
@@ -1392,7 +1467,7 @@ function snapshotSubmission(input) {
|
|
|
1392
1467
|
...causationId != null ? { causationId } : {},
|
|
1393
1468
|
...executionReason != null ? { executionReason } : {},
|
|
1394
1469
|
...idempotencyKey != null ? { idempotencyKey } : {},
|
|
1395
|
-
...authorizationBindingId != null ? { authorizationBindingId } : {},
|
|
1470
|
+
...authorizationBindingId != null && authorizationBindingId !== "" ? { authorizationBindingId } : {},
|
|
1396
1471
|
parametersArePrivate: false
|
|
1397
1472
|
};
|
|
1398
1473
|
Object.defineProperties(live, {
|
|
@@ -4960,6 +5035,7 @@ function createGovernedActionHost(options) {
|
|
|
4960
5035
|
}
|
|
4961
5036
|
return {
|
|
4962
5037
|
...createStoredRecordErasureHost(options.store, options.authorization),
|
|
5038
|
+
...createSubmissionOutcomeReader(options.store),
|
|
4963
5039
|
submitAction,
|
|
4964
5040
|
hasStagedInvocationActions: declaresStaging,
|
|
4965
5041
|
...stagedStore ? { dispatchStagedInvocations } : {},
|
|
@@ -5073,7 +5149,7 @@ function idempotencyConflict(existing, incoming) {
|
|
|
5073
5149
|
const reasons = [];
|
|
5074
5150
|
if (!existing.storedRecordsErased && digestParameters(existing.agentProposal ?? null) !== digestParameters(incoming.agentProposal ?? null)) reasons.push("agent_proposal");
|
|
5075
5151
|
if ((existing.idempotencyActorId ?? existing.actorId) !== incoming.actorId) reasons.push("actor");
|
|
5076
|
-
if ((existing.idempotencyAuthorizationBindingId ?? existing.authorizationBindingId) !== incoming.authorizationBindingId) reasons.push("authority_binding");
|
|
5152
|
+
if (((existing.idempotencyAuthorizationBindingId ?? existing.authorizationBindingId) || void 0) !== (incoming.authorizationBindingId || void 0)) reasons.push("authority_binding");
|
|
5077
5153
|
if (existing.actionVersion !== incoming.actionVersion) reasons.push("action_version");
|
|
5078
5154
|
if (!existing.storedRecordsErased && existing.parameterDigest !== incoming.parameterDigest) reasons.push("parameters");
|
|
5079
5155
|
return reasons.length ? {
|
|
@@ -5090,7 +5166,7 @@ function isKnownAdapterOutcome(outcome) {
|
|
|
5090
5166
|
function refusalIdentityConflict(row, submission, actionVersion, parameterDigest) {
|
|
5091
5167
|
const reasons = [];
|
|
5092
5168
|
if (row.actorId !== submission.actorId || row.actorType !== submission.actorType) reasons.push("actor");
|
|
5093
|
-
if (row.authorizationBindingId !== submission.authorizationBindingId) reasons.push("authority_binding");
|
|
5169
|
+
if ((row.authorizationBindingId || void 0) !== (submission.authorizationBindingId || void 0)) reasons.push("authority_binding");
|
|
5094
5170
|
if (row.actionVersion !== actionVersion) reasons.push("action_version");
|
|
5095
5171
|
if (!row.storedRecordsErased && row.parameterDigest !== void 0 && row.parameterDigest !== parameterDigest) reasons.push("parameters");
|
|
5096
5172
|
return reasons.length ? {
|
|
@@ -5715,6 +5791,13 @@ function emitRelayTelemetry(telemetry, name, occurredAt, workerId, attributes) {
|
|
|
5715
5791
|
//#region src/memory-store.ts
|
|
5716
5792
|
var MemoryPlatformHostStore = class {
|
|
5717
5793
|
db;
|
|
5794
|
+
async getSubmissionOutcomeByKey(key) {
|
|
5795
|
+
const row = this.invocations.find((row) => row.tenantId === key.tenantId && row.spaceId === key.spaceId && row.actionId === key.actionId && row.idempotencyKey !== void 0 && row.idempotencyKey === key.idempotencyKey);
|
|
5796
|
+
return row ? submissionOutcomeProjection({
|
|
5797
|
+
...row,
|
|
5798
|
+
idempotencyKey: row.idempotencyKey
|
|
5799
|
+
}) : void 0;
|
|
5800
|
+
}
|
|
5718
5801
|
transactionalOutbox;
|
|
5719
5802
|
transactionalApprovalLifecycle;
|
|
5720
5803
|
transactionalApprovalAuthorization = true;
|
|
@@ -7481,6 +7564,34 @@ var PostgresPlatformHostStore = class PostgresPlatformHostStore {
|
|
|
7481
7564
|
async transaction(run) {
|
|
7482
7565
|
return run(this.db);
|
|
7483
7566
|
}
|
|
7567
|
+
async getSubmissionOutcomeByKey(key) {
|
|
7568
|
+
const row = (await this.sql.query(`SELECT id,tenant_id,space_id,action_id,idempotency_key,action_version,actor_id,actor_type,
|
|
7569
|
+
authorization_binding_id,idempotency_actor_id,idempotency_authorization_binding_id,status,
|
|
7570
|
+
submission_refusal,records_erased_at FROM fabric_platform.action_invocations
|
|
7571
|
+
WHERE tenant_id=$1 AND space_id=$2 AND action_id=$3 AND idempotency_key=$4`, [
|
|
7572
|
+
key.tenantId,
|
|
7573
|
+
key.spaceId,
|
|
7574
|
+
key.actionId,
|
|
7575
|
+
key.idempotencyKey
|
|
7576
|
+
])).rows[0];
|
|
7577
|
+
if (!row) return void 0;
|
|
7578
|
+
return {
|
|
7579
|
+
tenantId: row.tenant_id,
|
|
7580
|
+
spaceId: row.space_id,
|
|
7581
|
+
actionId: row.action_id,
|
|
7582
|
+
idempotencyKey: row.idempotency_key,
|
|
7583
|
+
id: row.id,
|
|
7584
|
+
actionVersion: Number(row.action_version),
|
|
7585
|
+
actorId: row.actor_id,
|
|
7586
|
+
actorType: row.actor_type,
|
|
7587
|
+
authorizationBindingId: row.authorization_binding_id ?? void 0,
|
|
7588
|
+
idempotencyActorId: row.idempotency_actor_id ?? void 0,
|
|
7589
|
+
idempotencyAuthorizationBindingId: row.idempotency_authorization_binding_id ?? void 0,
|
|
7590
|
+
status: row.status,
|
|
7591
|
+
submissionRefusal: row.submission_refusal ?? void 0,
|
|
7592
|
+
storedRecordsErased: row.records_erased_at ? true : void 0
|
|
7593
|
+
};
|
|
7594
|
+
}
|
|
7484
7595
|
async createActionInvocation(input) {
|
|
7485
7596
|
assertInvocationWriteStatus(input.status);
|
|
7486
7597
|
const now = /* @__PURE__ */ new Date();
|
|
@@ -14299,6 +14410,161 @@ async function runInvocationIdempotencyChecks(subject) {
|
|
|
14299
14410
|
};
|
|
14300
14411
|
}
|
|
14301
14412
|
//#endregion
|
|
14302
|
-
|
|
14413
|
+
//#region src/submission-outcome-conformance.ts
|
|
14414
|
+
/**
|
|
14415
|
+
* Isolated destructive probe with fresh identities and timestamps from new Date().
|
|
14416
|
+
* Refusal and erasure sections run only with their respective store capabilities;
|
|
14417
|
+
* returned evidence identifies each unexercised optional section with `skipped:`.
|
|
14418
|
+
*/
|
|
14419
|
+
async function checkSubmissionOutcomeStore(store) {
|
|
14420
|
+
if (!store.getSubmissionOutcomeByKey) throw new Error("Missing getSubmissionOutcomeByKey");
|
|
14421
|
+
const evidence = ["verified: submission outcome metadata and exact key scope"];
|
|
14422
|
+
const id = globalThis.crypto.randomUUID();
|
|
14423
|
+
const input = {
|
|
14424
|
+
id,
|
|
14425
|
+
tenantId: `tenant-${id}`,
|
|
14426
|
+
spaceId: `space-${id}`,
|
|
14427
|
+
actionId: "probe.read",
|
|
14428
|
+
idempotencyKey: "",
|
|
14429
|
+
actionVersion: 7,
|
|
14430
|
+
actorId: "actor",
|
|
14431
|
+
actorType: "natural_person",
|
|
14432
|
+
authorizationBindingId: "authorization-binding",
|
|
14433
|
+
idempotencyActorId: "idempotency-actor",
|
|
14434
|
+
idempotencyAuthorizationBindingId: "idempotency-binding",
|
|
14435
|
+
status: "pending",
|
|
14436
|
+
parameters: {},
|
|
14437
|
+
result: {},
|
|
14438
|
+
correlationId: id
|
|
14439
|
+
};
|
|
14440
|
+
const key = {
|
|
14441
|
+
tenantId: input.tenantId,
|
|
14442
|
+
spaceId: input.spaceId,
|
|
14443
|
+
actionId: input.actionId,
|
|
14444
|
+
idempotencyKey: ""
|
|
14445
|
+
};
|
|
14446
|
+
const expected = {
|
|
14447
|
+
...key,
|
|
14448
|
+
id,
|
|
14449
|
+
actionVersion: input.actionVersion,
|
|
14450
|
+
actorId: input.actorId,
|
|
14451
|
+
actorType: input.actorType,
|
|
14452
|
+
authorizationBindingId: input.authorizationBindingId,
|
|
14453
|
+
idempotencyActorId: input.idempotencyActorId,
|
|
14454
|
+
idempotencyAuthorizationBindingId: input.idempotencyAuthorizationBindingId,
|
|
14455
|
+
status: input.status,
|
|
14456
|
+
submissionRefusal: void 0,
|
|
14457
|
+
storedRecordsErased: void 0
|
|
14458
|
+
};
|
|
14459
|
+
const assertProjection = (row, receipt) => {
|
|
14460
|
+
for (const field of Object.keys(receipt)) assertEqual(row?.[field], receipt[field], `receipt ${field}`);
|
|
14461
|
+
};
|
|
14462
|
+
assertEqual(await store.getSubmissionOutcomeByKey(key), void 0, "absence");
|
|
14463
|
+
await store.createActionInvocation(input);
|
|
14464
|
+
const before = structuredClone(await store.getActionInvocation(id, key.tenantId, key.spaceId));
|
|
14465
|
+
for (const field of [
|
|
14466
|
+
"tenantId",
|
|
14467
|
+
"spaceId",
|
|
14468
|
+
"actionId",
|
|
14469
|
+
"idempotencyKey"
|
|
14470
|
+
]) assertEqual(await store.getSubmissionOutcomeByKey({
|
|
14471
|
+
...key,
|
|
14472
|
+
[field]: "other.value"
|
|
14473
|
+
}), void 0, field);
|
|
14474
|
+
const spaced = {
|
|
14475
|
+
...input,
|
|
14476
|
+
id: `${id}-spaced`,
|
|
14477
|
+
idempotencyKey: " key "
|
|
14478
|
+
};
|
|
14479
|
+
await store.createActionInvocation(spaced);
|
|
14480
|
+
assertProjection(await store.getSubmissionOutcomeByKey(spaced), {
|
|
14481
|
+
...expected,
|
|
14482
|
+
id: spaced.id,
|
|
14483
|
+
idempotencyKey: spaced.idempotencyKey
|
|
14484
|
+
});
|
|
14485
|
+
assertEqual(await store.getSubmissionOutcomeByKey({
|
|
14486
|
+
...key,
|
|
14487
|
+
idempotencyKey: "key"
|
|
14488
|
+
}), void 0, "key is not trimmed");
|
|
14489
|
+
const unkeyed = {
|
|
14490
|
+
...input,
|
|
14491
|
+
id: `${id}-unkeyed`,
|
|
14492
|
+
actionId: "probe.unkeyed",
|
|
14493
|
+
idempotencyKey: void 0
|
|
14494
|
+
};
|
|
14495
|
+
await store.createActionInvocation(unkeyed);
|
|
14496
|
+
assertEqual(await store.getSubmissionOutcomeByKey({
|
|
14497
|
+
...key,
|
|
14498
|
+
actionId: unkeyed.actionId
|
|
14499
|
+
}), void 0, "null key cannot match empty key");
|
|
14500
|
+
const row = await store.getSubmissionOutcomeByKey(key);
|
|
14501
|
+
assertProjection(row, expected);
|
|
14502
|
+
if (row) row.actorId = "changed";
|
|
14503
|
+
assertProjection(await store.getSubmissionOutcomeByKey(key), expected);
|
|
14504
|
+
assertEqual(await store.getActionInvocation(id, key.tenantId, key.spaceId), before, "read writes nothing");
|
|
14505
|
+
const erasureReceipts = [{
|
|
14506
|
+
...expected,
|
|
14507
|
+
status: "completed"
|
|
14508
|
+
}];
|
|
14509
|
+
if (store.recordSubmissionRefusal) {
|
|
14510
|
+
const now = /* @__PURE__ */ new Date();
|
|
14511
|
+
const refused = {
|
|
14512
|
+
...key,
|
|
14513
|
+
id: `${id}-refused`,
|
|
14514
|
+
idempotencyKey: "refused",
|
|
14515
|
+
actionVersion: 8,
|
|
14516
|
+
actorId: "refused-actor",
|
|
14517
|
+
actorType: "agent",
|
|
14518
|
+
authorizationBindingId: "refused-binding",
|
|
14519
|
+
correlationId: id,
|
|
14520
|
+
submissionRefusal: { class: "authorization" },
|
|
14521
|
+
createdAt: now,
|
|
14522
|
+
updatedAt: now
|
|
14523
|
+
};
|
|
14524
|
+
await store.recordSubmissionRefusal(refused);
|
|
14525
|
+
const refusedReceipt = {
|
|
14526
|
+
...expected,
|
|
14527
|
+
id: refused.id,
|
|
14528
|
+
idempotencyKey: refused.idempotencyKey,
|
|
14529
|
+
actionVersion: refused.actionVersion,
|
|
14530
|
+
actorId: refused.actorId,
|
|
14531
|
+
actorType: refused.actorType,
|
|
14532
|
+
authorizationBindingId: refused.authorizationBindingId,
|
|
14533
|
+
idempotencyActorId: void 0,
|
|
14534
|
+
idempotencyAuthorizationBindingId: void 0,
|
|
14535
|
+
status: "submission_refused",
|
|
14536
|
+
submissionRefusal: refused.submissionRefusal
|
|
14537
|
+
};
|
|
14538
|
+
assertProjection(await store.getSubmissionOutcomeByKey(refused), refusedReceipt);
|
|
14539
|
+
erasureReceipts.push(refusedReceipt);
|
|
14540
|
+
evidence.push("verified: recorded refusal receipt");
|
|
14541
|
+
} else evidence.push("skipped: store lacks recordSubmissionRefusal");
|
|
14542
|
+
const erasable = store;
|
|
14543
|
+
if (erasable.eraseStoredRecords) {
|
|
14544
|
+
await store.updateActionInvocation(id, key.tenantId, key.spaceId, { status: "completed" });
|
|
14545
|
+
for (const receipt of erasureReceipts) {
|
|
14546
|
+
await erasable.eraseStoredRecords({
|
|
14547
|
+
tenantId: key.tenantId,
|
|
14548
|
+
spaceId: key.spaceId,
|
|
14549
|
+
actorId: "operator",
|
|
14550
|
+
actorType: "natural_person",
|
|
14551
|
+
requestId: globalThis.crypto.randomUUID(),
|
|
14552
|
+
targets: [{
|
|
14553
|
+
kind: "invocation",
|
|
14554
|
+
invocationId: receipt.id
|
|
14555
|
+
}],
|
|
14556
|
+
redact: (view) => view
|
|
14557
|
+
});
|
|
14558
|
+
assertProjection(await store.getSubmissionOutcomeByKey(receipt), {
|
|
14559
|
+
...receipt,
|
|
14560
|
+
storedRecordsErased: true
|
|
14561
|
+
});
|
|
14562
|
+
}
|
|
14563
|
+
evidence.push("verified: erased receipts for created probe rows");
|
|
14564
|
+
} else evidence.push("skipped: store lacks eraseStoredRecords");
|
|
14565
|
+
return evidence;
|
|
14566
|
+
}
|
|
14567
|
+
//#endregion
|
|
14568
|
+
export { APPROVAL_DECIDED_EVENT, APPROVAL_DECISION_REFUSED_EVENT, APPROVAL_EXPIRED_EVENT, APPROVAL_REQUESTED_EVENT, ActionAdmissionError, ConfirmedTransactionRollback, ERASED_DIGEST, ErasureBusyError, ExternalCompletionError, HOST_RECORDS_ERASED_EVENT, IdempotencyConflictError, LEGACY_BATCH_DIGEST, MAX_APPROVAL_EVIDENCE_BYTES, MAX_STAGED_INVOCATIONS, MAX_STAGED_INVOCATION_BATCH_BYTES, MAX_STAGED_INVOCATION_BYTES, MAX_STAGED_INVOCATION_KEY_LENGTH, MemoryPlatformHostStore, PARAMETER_DIGEST_ALGORITHM, PLATFORM_HOST_CONTRACT_VERSION, PLATFORM_HOST_HEALTH_CONTRACT_VERSION, PLATFORM_HOST_METRIC_NAMES, PostgresPlatformHostStore, STAGED_INVOCATION_KEY_PATTERN, STAGED_INVOCATION_PRINCIPAL_PATTERN, StoredRecordErasureAuthorizationError, StoredRecordsErasedError, SubmissionOutcomeUnsupportedError, SubmissionRefusalError, applyPostgresMigrations, approvalChecks, asStagedInvocationStore, canonicalJson, checkSubmissionOutcomeStore, cloneOutboxRecord, cloneStagedInvocationRecord, createDurableSagaParentLifecycle, createGovernedActionHost, createStoreBackedActionDispatcher, createStoredRecordErasureHost, digestParameters, emitPlatformHostTelemetry, erasureChecks, eventAppendChecks, getPlatformHostHealthSnapshot, invocationIdempotencyChecks, isConfirmedTransactionRollback, isTransientPostgresTransactionError, outboxActorDisclosure, outboxRelayChecks, outboxRequeueChecks, prepareStagedInvocationBatch, runApprovalChecks, runErasureChecks, runEventAppendChecks, runInvocationIdempotencyChecks, runOutboxRelayChecks, runOutboxRelayCycle, runOutboxRequeueChecks, runPlatformActionWorker, runPlatformActionWorkerCycle, runStagedInvocationChecks, runTenantIsolationChecks, stagedInvocationChecks, stagedInvocationRequestIdentity, stagedInvocationRequestMatches, tenantIsolationChecks, toEnterpriseEventEnvelope };
|
|
14303
14569
|
|
|
14304
14570
|
//# sourceMappingURL=index.js.map
|