@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 CHANGED
@@ -1,5 +1,17 @@
1
1
  # @fabricorg/platform-host
2
2
 
3
+ ## 8.10.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 6147d14: Add read-only, principal-scoped historical submission receipts without rerunning submission checks. Preserve submission ordering, hide receipts belonging to other principals, and expose erased records without refusal evidence.
8
+
9
+ Store conformance checks verify each receipt field and report skipped optional refusal and erasure probes. Normalize absent PostgreSQL refusal metadata to `undefined` in the receipt projection.
10
+
11
+ ### Patch Changes
12
+
13
+ - fbbc664: Treat an empty authorization binding id as absent for submission idempotency identity and receipt ownership. Normalize new submissions and match stored empty binding ids from older Hosts, including recorded refusals, while preserving conflicts with non-empty binding ids.
14
+
3
15
  ## 8.9.0
4
16
 
5
17
  ### Minor Changes
package/README.md CHANGED
@@ -255,7 +255,9 @@ recovery within `recoveryMaxAttempts`. A handler error without eligible confirme
255
255
  not an automatic retry or a claim that the current transaction committed.
256
256
 
257
257
  New rows bind that key to the action version, actor/authority binding, and a versioned canonical
258
- parameter digest. Host 3 defaults to `enforce`, which throws `IdempotencyConflictError`; configure
258
+ parameter digest. An empty `authorizationBindingId` is treated as absent for submission
259
+ idempotency identity and receipt ownership, including stored empty IDs from older Hosts.
260
+ Host 3 defaults to `enforce`, which throws `IdempotencyConflictError`; configure
259
261
  `idempotency.conflictMode` as `audit-only` for a preflight soak. Legacy null-digest rows skip only the
260
262
  parameter comparison and call `onLegacyRecord`; actor, authority-binding, and action-version mismatches
261
263
  still reject in enforce mode. See `MIGRATION-2-IDEMPOTENCY.md`.
@@ -1348,7 +1350,7 @@ these rows inert. The stored-record erasure operator can redact their payload fi
1348
1350
  Host does not schedule automatic retention. See **Erasing stored records**.
1349
1351
 
1350
1352
  Entitlement, authorization and admission checks still run on replay before Host tries an insert;
1351
- there is no refusal lookup before the checks. A matching caller receives the stored refusal,
1353
+ there is no refusal lookup before the checks (use the separate `readSubmissionOutcome` receipt read below). A matching caller receives the stored refusal,
1352
1354
  including after the checks start passing. Identity compares actor id and type, authorization
1353
1355
  binding id, action version, and a parameter digest when the stored refusal has one. A refused
1354
1356
  caller with a different identity receives its own unrecorded refusal. An admitted caller with a
@@ -1403,6 +1405,25 @@ submitted …"). Before submitting an action that declares `admitSubmission`: (1
1403
1405
  migration 6, and nothing is written; (2) upgrade every worker, since a worker on an older Host would
1404
1406
  execute such an invocation without checking `admittedAt`.
1405
1407
 
1408
+ ### Read a historical submission receipt
1409
+
1410
+ `host.readSubmissionOutcome({ tenantId, spaceId, actionId, idempotencyKey, actorId,
1411
+ actorType, authorizationBindingId })` reads receipt metadata without rerunning mutation
1412
+ permission or admission checks. The embedding application must supply trusted, authenticated
1413
+ tenant, space and actor identity. The original principal can read its receipt after mutation
1414
+ permission is revoked. The key is exact, including the valid empty string.
1415
+
1416
+ A refusal error with `recorded: false` establishes nothing about key ownership: an admitted
1417
+ invocation or another principal's record may hold it. Use the receipt read to observe a
1418
+ visible durable outcome. `no_record` means no record visible to this principal at this
1419
+ snapshot; it does not establish that the key is free. A returned invocation status is an
1420
+ observation and may change. Refused receipts include only their validated descriptor;
1421
+ erased receipts include only identity, version and recorded status. No pending work is dispatched.
1422
+
1423
+ The operation is available independently of refusal recording and conflict mode. Custom
1424
+ stores implement optional `getSubmissionOutcomeByKey`; an unsupported store throws
1425
+ `SubmissionOutcomeUnsupportedError`. Storage errors propagate. See ADR 0022.
1426
+
1406
1427
  ## External mutation governance
1407
1428
 
1408
1429
  Hosts can configure `resolveMutationGovernance` to persist an approved, provider-neutral
package/dist/index.cjs CHANGED
@@ -4,6 +4,201 @@ let _fabricorg_platform = require("@fabricorg/platform");
4
4
  let perf_hooks = require("perf_hooks");
5
5
  let _fabricorg_assembly = require("@fabricorg/assembly");
6
6
  let crypto = require("crypto");
7
+ //#region src/erasure-types.ts
8
+ const HOST_RECORDS_ERASED_EVENT = "HostRecordsErased";
9
+ /** Sentinel replacing required payload digests after stored-record erasure. */
10
+ const ERASED_DIGEST = "erased";
11
+ //#endregion
12
+ //#region src/types.ts
13
+ /** Durable Host lifecycle generation, independent from the npm package version. */
14
+ const PLATFORM_HOST_CONTRACT_VERSION = 2;
15
+ var IdempotencyConflictError = class extends Error {
16
+ conflict;
17
+ code = "IDEMPOTENCY_CONFLICT";
18
+ constructor(conflict) {
19
+ super(`Idempotency key "${conflict.idempotencyKey}" conflicts with invocation ${conflict.existingInvocationId}: ${conflict.reasons.join(", ")}`);
20
+ this.conflict = conflict;
21
+ this.name = "IdempotencyConflictError";
22
+ }
23
+ };
24
+ /**
25
+ * Thrown by `submitAction` when a declared admission check did not admit the
26
+ * submission. By default Host writes nothing for it. With recordRefusals
27
+ * enabled and an idempotency key, a definitive refusal can be recorded;
28
+ * `recorded` confirms persistence of the terminal refusal. The message holds
29
+ * the action id, the refusal and, for `refused`, the check's fixed code. It omits parameter
30
+ * payloads and caught callback error text.
31
+ */
32
+ var ActionAdmissionError = class extends Error {
33
+ actionId;
34
+ refusal;
35
+ refusalCode;
36
+ recorded;
37
+ code = "ACTION_ADMISSION_REFUSED";
38
+ constructor(actionId, refusal, refusalCode, recorded = false) {
39
+ super(`Action ${actionId} submission was not admitted (${refusal}${refusalCode ? `: ${refusalCode}` : ""}); ${recorded ? "the refusal was recorded under the idempotency key" : "nothing was recorded"}.`);
40
+ this.actionId = actionId;
41
+ this.refusal = refusal;
42
+ this.refusalCode = refusalCode;
43
+ this.recorded = recorded;
44
+ this.name = "ActionAdmissionError";
45
+ }
46
+ };
47
+ /** A definitive authorization-boundary refusal before an invocation is created. */
48
+ var SubmissionRefusalError = class extends Error {
49
+ actionId;
50
+ refusal;
51
+ recorded;
52
+ code = "ACTION_SUBMISSION_REFUSED";
53
+ constructor(actionId, refusal, message, recorded = false) {
54
+ super(recorded ? `Action ${actionId} submission was refused (${refusal}); the refusal was recorded under the idempotency key.` : message);
55
+ this.actionId = actionId;
56
+ this.refusal = refusal;
57
+ this.recorded = recorded;
58
+ this.name = "SubmissionRefusalError";
59
+ }
60
+ };
61
+ /**
62
+ * Why a completion was not applied, typed so a webhook handler can answer
63
+ * the external system correctly: `retryable` means the invocation is still
64
+ * being finalized and the same callback will be accepted once it parks, so
65
+ * answer with something the sender retries; anything else is permanent and
66
+ * a retry will get the same refusal without a configuration/input change.
67
+ * `redaction_failed` means completion result or error-text redaction failed before writes;
68
+ * it is not retryable.
69
+ */
70
+ var ExternalCompletionError = class extends Error {
71
+ refusal;
72
+ code = "EXTERNAL_COMPLETION_REFUSED";
73
+ constructor(refusal, message) {
74
+ super(message);
75
+ this.refusal = refusal;
76
+ this.name = "ExternalCompletionError";
77
+ }
78
+ get retryable() {
79
+ return this.refusal === "still_executing";
80
+ }
81
+ };
82
+ /** Maximum serialized UTF-8 evidence size. */
83
+ const MAX_APPROVAL_EVIDENCE_BYTES = 16384;
84
+ const APPROVAL_REQUESTED_EVENT = "ApprovalRequested";
85
+ const APPROVAL_DECIDED_EVENT = "ApprovalDecided";
86
+ const APPROVAL_EXPIRED_EVENT = "ApprovalExpired";
87
+ const APPROVAL_DECISION_REFUSED_EVENT = "ApprovalDecisionRefused";
88
+ //#endregion
89
+ //#region src/submission-refusal.ts
90
+ /** Validate and copy only the descriptor's declared fields, on both sides of storage. */
91
+ function submissionRefusalDescriptor(value) {
92
+ if (!value || typeof value !== "object") throw new Error("Invalid submission refusal descriptor.");
93
+ const fields = Object.getOwnPropertyDescriptors(value);
94
+ const data = (key) => fields[key]?.value;
95
+ const kind = data("class");
96
+ const keys = Reflect.ownKeys(value);
97
+ if ((kind === "entitlement" || kind === "authorization") && keys.length === 1) return { class: kind };
98
+ if (kind === "admission" && keys.every((key) => key === "class" || key === "refusal" || key === "refusalCode")) {
99
+ const refusal = data("refusal");
100
+ const code = data("refusalCode");
101
+ if (refusal === "invalid_parameters" && !Object.hasOwn(fields, "refusalCode")) return {
102
+ class: kind,
103
+ refusal
104
+ };
105
+ if (refusal === "refused" && (!Object.hasOwn(fields, "refusalCode") || typeof code === "string" && /^[a-z][A-Za-z0-9_.:-]{0,127}$/.test(code))) return {
106
+ class: kind,
107
+ refusal,
108
+ ...code === void 0 ? {} : { refusalCode: code }
109
+ };
110
+ }
111
+ throw new Error("Invalid submission refusal descriptor.");
112
+ }
113
+ function validateRefusalRecord(record) {
114
+ if (record?.status === "submission_refused") submissionRefusalDescriptor(record.submissionRefusal);
115
+ return record;
116
+ }
117
+ /** Reconstruct errors from fixed validated evidence, without row error text or parameters. */
118
+ function recordedSubmissionRefusal(record) {
119
+ const descriptor = submissionRefusalDescriptor(record.submissionRefusal);
120
+ return descriptor.class === "admission" ? new ActionAdmissionError(record.actionId, descriptor.refusal, descriptor.refusalCode, true) : new SubmissionRefusalError(record.actionId, descriptor.class, "", true);
121
+ }
122
+ /** Refusals enter the ledger through the narrow insert, rather than generic status writes. */
123
+ function assertInvocationWriteStatus(status) {
124
+ if (status === "submission_refused") throw new Error("submission_refused can only be inserted by recordSubmissionRefusal.");
125
+ }
126
+ //#endregion
127
+ //#region src/submission-outcome.ts
128
+ var SubmissionOutcomeUnsupportedError = class extends Error {
129
+ constructor() {
130
+ super("Store lacks getSubmissionOutcomeByKey.");
131
+ this.name = "SubmissionOutcomeUnsupportedError";
132
+ }
133
+ };
134
+ /** Copy receipt fields only, without exposing execution data. */
135
+ function submissionOutcomeProjection(row) {
136
+ return structuredClone({
137
+ tenantId: row.tenantId,
138
+ spaceId: row.spaceId,
139
+ actionId: row.actionId,
140
+ idempotencyKey: row.idempotencyKey,
141
+ id: row.id,
142
+ actionVersion: row.actionVersion,
143
+ actorId: row.actorId,
144
+ actorType: row.actorType,
145
+ authorizationBindingId: row.authorizationBindingId,
146
+ idempotencyActorId: row.idempotencyActorId,
147
+ idempotencyAuthorizationBindingId: row.idempotencyAuthorizationBindingId,
148
+ status: row.status,
149
+ submissionRefusal: row.submissionRefusal,
150
+ storedRecordsErased: row.storedRecordsErased
151
+ });
152
+ }
153
+ function createSubmissionOutcomeReader(store) {
154
+ return { async readSubmissionOutcome(input) {
155
+ const snapshot = {
156
+ tenantId: input.tenantId,
157
+ spaceId: input.spaceId,
158
+ actionId: input.actionId,
159
+ idempotencyKey: input.idempotencyKey,
160
+ actorId: input.actorId,
161
+ actorType: input.actorType,
162
+ authorizationBindingId: input.authorizationBindingId
163
+ };
164
+ for (const field of [
165
+ "tenantId",
166
+ "spaceId",
167
+ "actionId",
168
+ "idempotencyKey",
169
+ "actorId",
170
+ "actorType"
171
+ ]) if (typeof snapshot[field] !== "string") throw new TypeError(`Invalid ${field}.`);
172
+ if (!store.getSubmissionOutcomeByKey) throw new SubmissionOutcomeUnsupportedError();
173
+ const row = await store.getSubmissionOutcomeByKey({
174
+ tenantId: snapshot.tenantId,
175
+ spaceId: snapshot.spaceId,
176
+ actionId: snapshot.actionId,
177
+ idempotencyKey: snapshot.idempotencyKey
178
+ });
179
+ 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" };
180
+ const identity = {
181
+ actionInvocationId: row.id,
182
+ actionVersion: row.actionVersion
183
+ };
184
+ if (row.storedRecordsErased) return {
185
+ ...identity,
186
+ kind: "records_erased",
187
+ recordedStatus: row.status
188
+ };
189
+ if (row.status === "submission_refused") return {
190
+ ...identity,
191
+ kind: "submission_refused",
192
+ submissionRefusal: submissionRefusalDescriptor(row.submissionRefusal)
193
+ };
194
+ return {
195
+ ...identity,
196
+ kind: "invocation",
197
+ status: row.status
198
+ };
199
+ } };
200
+ }
201
+ //#endregion
7
202
  //#region src/transaction-rollback.ts
8
203
  /** Evidence from a transaction provider that rollback completed successfully. */
9
204
  var ConfirmedTransactionRollback = class extends Error {
@@ -182,126 +377,6 @@ function createStoredRecordErasureHost(store, authorization) {
182
377
  } };
183
378
  }
184
379
  //#endregion
185
- //#region src/erasure-types.ts
186
- const HOST_RECORDS_ERASED_EVENT = "HostRecordsErased";
187
- /** Sentinel replacing required payload digests after stored-record erasure. */
188
- const ERASED_DIGEST = "erased";
189
- //#endregion
190
- //#region src/types.ts
191
- /** Durable Host lifecycle generation, independent from the npm package version. */
192
- const PLATFORM_HOST_CONTRACT_VERSION = 2;
193
- var IdempotencyConflictError = class extends Error {
194
- conflict;
195
- code = "IDEMPOTENCY_CONFLICT";
196
- constructor(conflict) {
197
- super(`Idempotency key "${conflict.idempotencyKey}" conflicts with invocation ${conflict.existingInvocationId}: ${conflict.reasons.join(", ")}`);
198
- this.conflict = conflict;
199
- this.name = "IdempotencyConflictError";
200
- }
201
- };
202
- /**
203
- * Thrown by `submitAction` when a declared admission check did not admit the
204
- * submission. By default Host writes nothing for it. With recordRefusals
205
- * enabled and an idempotency key, a definitive refusal can be recorded;
206
- * `recorded` confirms persistence of the terminal refusal. The message holds
207
- * the action id, the refusal and, for `refused`, the check's fixed code. It omits parameter
208
- * payloads and caught callback error text.
209
- */
210
- var ActionAdmissionError = class extends Error {
211
- actionId;
212
- refusal;
213
- refusalCode;
214
- recorded;
215
- code = "ACTION_ADMISSION_REFUSED";
216
- constructor(actionId, refusal, refusalCode, recorded = false) {
217
- super(`Action ${actionId} submission was not admitted (${refusal}${refusalCode ? `: ${refusalCode}` : ""}); ${recorded ? "the refusal was recorded under the idempotency key" : "nothing was recorded"}.`);
218
- this.actionId = actionId;
219
- this.refusal = refusal;
220
- this.refusalCode = refusalCode;
221
- this.recorded = recorded;
222
- this.name = "ActionAdmissionError";
223
- }
224
- };
225
- /** A definitive authorization-boundary refusal before an invocation is created. */
226
- var SubmissionRefusalError = class extends Error {
227
- actionId;
228
- refusal;
229
- recorded;
230
- code = "ACTION_SUBMISSION_REFUSED";
231
- constructor(actionId, refusal, message, recorded = false) {
232
- super(recorded ? `Action ${actionId} submission was refused (${refusal}); the refusal was recorded under the idempotency key.` : message);
233
- this.actionId = actionId;
234
- this.refusal = refusal;
235
- this.recorded = recorded;
236
- this.name = "SubmissionRefusalError";
237
- }
238
- };
239
- /**
240
- * Why a completion was not applied, typed so a webhook handler can answer
241
- * the external system correctly: `retryable` means the invocation is still
242
- * being finalized and the same callback will be accepted once it parks, so
243
- * answer with something the sender retries; anything else is permanent and
244
- * a retry will get the same refusal without a configuration/input change.
245
- * `redaction_failed` means completion result or error-text redaction failed before writes;
246
- * it is not retryable.
247
- */
248
- var ExternalCompletionError = class extends Error {
249
- refusal;
250
- code = "EXTERNAL_COMPLETION_REFUSED";
251
- constructor(refusal, message) {
252
- super(message);
253
- this.refusal = refusal;
254
- this.name = "ExternalCompletionError";
255
- }
256
- get retryable() {
257
- return this.refusal === "still_executing";
258
- }
259
- };
260
- /** Maximum serialized UTF-8 evidence size. */
261
- const MAX_APPROVAL_EVIDENCE_BYTES = 16384;
262
- const APPROVAL_REQUESTED_EVENT = "ApprovalRequested";
263
- const APPROVAL_DECIDED_EVENT = "ApprovalDecided";
264
- const APPROVAL_EXPIRED_EVENT = "ApprovalExpired";
265
- const APPROVAL_DECISION_REFUSED_EVENT = "ApprovalDecisionRefused";
266
- //#endregion
267
- //#region src/submission-refusal.ts
268
- /** Validate and copy only the descriptor's declared fields, on both sides of storage. */
269
- function submissionRefusalDescriptor(value) {
270
- if (!value || typeof value !== "object") throw new Error("Invalid submission refusal descriptor.");
271
- const fields = Object.getOwnPropertyDescriptors(value);
272
- const data = (key) => fields[key]?.value;
273
- const kind = data("class");
274
- const keys = Reflect.ownKeys(value);
275
- if ((kind === "entitlement" || kind === "authorization") && keys.length === 1) return { class: kind };
276
- if (kind === "admission" && keys.every((key) => key === "class" || key === "refusal" || key === "refusalCode")) {
277
- const refusal = data("refusal");
278
- const code = data("refusalCode");
279
- if (refusal === "invalid_parameters" && !Object.hasOwn(fields, "refusalCode")) return {
280
- class: kind,
281
- refusal
282
- };
283
- if (refusal === "refused" && (!Object.hasOwn(fields, "refusalCode") || typeof code === "string" && /^[a-z][A-Za-z0-9_.:-]{0,127}$/.test(code))) return {
284
- class: kind,
285
- refusal,
286
- ...code === void 0 ? {} : { refusalCode: code }
287
- };
288
- }
289
- throw new Error("Invalid submission refusal descriptor.");
290
- }
291
- function validateRefusalRecord(record) {
292
- if (record?.status === "submission_refused") submissionRefusalDescriptor(record.submissionRefusal);
293
- return record;
294
- }
295
- /** Reconstruct errors from fixed validated evidence, without row error text or parameters. */
296
- function recordedSubmissionRefusal(record) {
297
- const descriptor = submissionRefusalDescriptor(record.submissionRefusal);
298
- return descriptor.class === "admission" ? new ActionAdmissionError(record.actionId, descriptor.refusal, descriptor.refusalCode, true) : new SubmissionRefusalError(record.actionId, descriptor.class, "", true);
299
- }
300
- /** Refusals enter the ledger through the narrow insert, rather than generic status writes. */
301
- function assertInvocationWriteStatus(status) {
302
- if (status === "submission_refused") throw new Error("submission_refused can only be inserted by recordSubmissionRefusal.");
303
- }
304
- //#endregion
305
380
  //#region src/action-finalization.ts
306
381
  /** Remove private preparation and restore its compatible protocol on settlement. */
307
382
  function finalizationSettlementPatch(record, patch) {
@@ -1393,7 +1468,7 @@ function snapshotSubmission(input) {
1393
1468
  ...causationId != null ? { causationId } : {},
1394
1469
  ...executionReason != null ? { executionReason } : {},
1395
1470
  ...idempotencyKey != null ? { idempotencyKey } : {},
1396
- ...authorizationBindingId != null ? { authorizationBindingId } : {},
1471
+ ...authorizationBindingId != null && authorizationBindingId !== "" ? { authorizationBindingId } : {},
1397
1472
  parametersArePrivate: false
1398
1473
  };
1399
1474
  Object.defineProperties(live, {
@@ -4961,6 +5036,7 @@ function createGovernedActionHost(options) {
4961
5036
  }
4962
5037
  return {
4963
5038
  ...createStoredRecordErasureHost(options.store, options.authorization),
5039
+ ...createSubmissionOutcomeReader(options.store),
4964
5040
  submitAction,
4965
5041
  hasStagedInvocationActions: declaresStaging,
4966
5042
  ...stagedStore ? { dispatchStagedInvocations } : {},
@@ -5074,7 +5150,7 @@ function idempotencyConflict(existing, incoming) {
5074
5150
  const reasons = [];
5075
5151
  if (!existing.storedRecordsErased && digestParameters(existing.agentProposal ?? null) !== digestParameters(incoming.agentProposal ?? null)) reasons.push("agent_proposal");
5076
5152
  if ((existing.idempotencyActorId ?? existing.actorId) !== incoming.actorId) reasons.push("actor");
5077
- if ((existing.idempotencyAuthorizationBindingId ?? existing.authorizationBindingId) !== incoming.authorizationBindingId) reasons.push("authority_binding");
5153
+ if (((existing.idempotencyAuthorizationBindingId ?? existing.authorizationBindingId) || void 0) !== (incoming.authorizationBindingId || void 0)) reasons.push("authority_binding");
5078
5154
  if (existing.actionVersion !== incoming.actionVersion) reasons.push("action_version");
5079
5155
  if (!existing.storedRecordsErased && existing.parameterDigest !== incoming.parameterDigest) reasons.push("parameters");
5080
5156
  return reasons.length ? {
@@ -5091,7 +5167,7 @@ function isKnownAdapterOutcome(outcome) {
5091
5167
  function refusalIdentityConflict(row, submission, actionVersion, parameterDigest) {
5092
5168
  const reasons = [];
5093
5169
  if (row.actorId !== submission.actorId || row.actorType !== submission.actorType) reasons.push("actor");
5094
- if (row.authorizationBindingId !== submission.authorizationBindingId) reasons.push("authority_binding");
5170
+ if ((row.authorizationBindingId || void 0) !== (submission.authorizationBindingId || void 0)) reasons.push("authority_binding");
5095
5171
  if (row.actionVersion !== actionVersion) reasons.push("action_version");
5096
5172
  if (!row.storedRecordsErased && row.parameterDigest !== void 0 && row.parameterDigest !== parameterDigest) reasons.push("parameters");
5097
5173
  return reasons.length ? {
@@ -5716,6 +5792,13 @@ function emitRelayTelemetry(telemetry, name, occurredAt, workerId, attributes) {
5716
5792
  //#region src/memory-store.ts
5717
5793
  var MemoryPlatformHostStore = class {
5718
5794
  db;
5795
+ async getSubmissionOutcomeByKey(key) {
5796
+ 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);
5797
+ return row ? submissionOutcomeProjection({
5798
+ ...row,
5799
+ idempotencyKey: row.idempotencyKey
5800
+ }) : void 0;
5801
+ }
5719
5802
  transactionalOutbox;
5720
5803
  transactionalApprovalLifecycle;
5721
5804
  transactionalApprovalAuthorization = true;
@@ -7482,6 +7565,34 @@ var PostgresPlatformHostStore = class PostgresPlatformHostStore {
7482
7565
  async transaction(run) {
7483
7566
  return run(this.db);
7484
7567
  }
7568
+ async getSubmissionOutcomeByKey(key) {
7569
+ const row = (await this.sql.query(`SELECT id,tenant_id,space_id,action_id,idempotency_key,action_version,actor_id,actor_type,
7570
+ authorization_binding_id,idempotency_actor_id,idempotency_authorization_binding_id,status,
7571
+ submission_refusal,records_erased_at FROM fabric_platform.action_invocations
7572
+ WHERE tenant_id=$1 AND space_id=$2 AND action_id=$3 AND idempotency_key=$4`, [
7573
+ key.tenantId,
7574
+ key.spaceId,
7575
+ key.actionId,
7576
+ key.idempotencyKey
7577
+ ])).rows[0];
7578
+ if (!row) return void 0;
7579
+ return {
7580
+ tenantId: row.tenant_id,
7581
+ spaceId: row.space_id,
7582
+ actionId: row.action_id,
7583
+ idempotencyKey: row.idempotency_key,
7584
+ id: row.id,
7585
+ actionVersion: Number(row.action_version),
7586
+ actorId: row.actor_id,
7587
+ actorType: row.actor_type,
7588
+ authorizationBindingId: row.authorization_binding_id ?? void 0,
7589
+ idempotencyActorId: row.idempotency_actor_id ?? void 0,
7590
+ idempotencyAuthorizationBindingId: row.idempotency_authorization_binding_id ?? void 0,
7591
+ status: row.status,
7592
+ submissionRefusal: row.submission_refusal ?? void 0,
7593
+ storedRecordsErased: row.records_erased_at ? true : void 0
7594
+ };
7595
+ }
7485
7596
  async createActionInvocation(input) {
7486
7597
  assertInvocationWriteStatus(input.status);
7487
7598
  const now = /* @__PURE__ */ new Date();
@@ -14300,6 +14411,161 @@ async function runInvocationIdempotencyChecks(subject) {
14300
14411
  };
14301
14412
  }
14302
14413
  //#endregion
14414
+ //#region src/submission-outcome-conformance.ts
14415
+ /**
14416
+ * Isolated destructive probe with fresh identities and timestamps from new Date().
14417
+ * Refusal and erasure sections run only with their respective store capabilities;
14418
+ * returned evidence identifies each unexercised optional section with `skipped:`.
14419
+ */
14420
+ async function checkSubmissionOutcomeStore(store) {
14421
+ if (!store.getSubmissionOutcomeByKey) throw new Error("Missing getSubmissionOutcomeByKey");
14422
+ const evidence = ["verified: submission outcome metadata and exact key scope"];
14423
+ const id = globalThis.crypto.randomUUID();
14424
+ const input = {
14425
+ id,
14426
+ tenantId: `tenant-${id}`,
14427
+ spaceId: `space-${id}`,
14428
+ actionId: "probe.read",
14429
+ idempotencyKey: "",
14430
+ actionVersion: 7,
14431
+ actorId: "actor",
14432
+ actorType: "natural_person",
14433
+ authorizationBindingId: "authorization-binding",
14434
+ idempotencyActorId: "idempotency-actor",
14435
+ idempotencyAuthorizationBindingId: "idempotency-binding",
14436
+ status: "pending",
14437
+ parameters: {},
14438
+ result: {},
14439
+ correlationId: id
14440
+ };
14441
+ const key = {
14442
+ tenantId: input.tenantId,
14443
+ spaceId: input.spaceId,
14444
+ actionId: input.actionId,
14445
+ idempotencyKey: ""
14446
+ };
14447
+ const expected = {
14448
+ ...key,
14449
+ id,
14450
+ actionVersion: input.actionVersion,
14451
+ actorId: input.actorId,
14452
+ actorType: input.actorType,
14453
+ authorizationBindingId: input.authorizationBindingId,
14454
+ idempotencyActorId: input.idempotencyActorId,
14455
+ idempotencyAuthorizationBindingId: input.idempotencyAuthorizationBindingId,
14456
+ status: input.status,
14457
+ submissionRefusal: void 0,
14458
+ storedRecordsErased: void 0
14459
+ };
14460
+ const assertProjection = (row, receipt) => {
14461
+ for (const field of Object.keys(receipt)) assertEqual(row?.[field], receipt[field], `receipt ${field}`);
14462
+ };
14463
+ assertEqual(await store.getSubmissionOutcomeByKey(key), void 0, "absence");
14464
+ await store.createActionInvocation(input);
14465
+ const before = structuredClone(await store.getActionInvocation(id, key.tenantId, key.spaceId));
14466
+ for (const field of [
14467
+ "tenantId",
14468
+ "spaceId",
14469
+ "actionId",
14470
+ "idempotencyKey"
14471
+ ]) assertEqual(await store.getSubmissionOutcomeByKey({
14472
+ ...key,
14473
+ [field]: "other.value"
14474
+ }), void 0, field);
14475
+ const spaced = {
14476
+ ...input,
14477
+ id: `${id}-spaced`,
14478
+ idempotencyKey: " key "
14479
+ };
14480
+ await store.createActionInvocation(spaced);
14481
+ assertProjection(await store.getSubmissionOutcomeByKey(spaced), {
14482
+ ...expected,
14483
+ id: spaced.id,
14484
+ idempotencyKey: spaced.idempotencyKey
14485
+ });
14486
+ assertEqual(await store.getSubmissionOutcomeByKey({
14487
+ ...key,
14488
+ idempotencyKey: "key"
14489
+ }), void 0, "key is not trimmed");
14490
+ const unkeyed = {
14491
+ ...input,
14492
+ id: `${id}-unkeyed`,
14493
+ actionId: "probe.unkeyed",
14494
+ idempotencyKey: void 0
14495
+ };
14496
+ await store.createActionInvocation(unkeyed);
14497
+ assertEqual(await store.getSubmissionOutcomeByKey({
14498
+ ...key,
14499
+ actionId: unkeyed.actionId
14500
+ }), void 0, "null key cannot match empty key");
14501
+ const row = await store.getSubmissionOutcomeByKey(key);
14502
+ assertProjection(row, expected);
14503
+ if (row) row.actorId = "changed";
14504
+ assertProjection(await store.getSubmissionOutcomeByKey(key), expected);
14505
+ assertEqual(await store.getActionInvocation(id, key.tenantId, key.spaceId), before, "read writes nothing");
14506
+ const erasureReceipts = [{
14507
+ ...expected,
14508
+ status: "completed"
14509
+ }];
14510
+ if (store.recordSubmissionRefusal) {
14511
+ const now = /* @__PURE__ */ new Date();
14512
+ const refused = {
14513
+ ...key,
14514
+ id: `${id}-refused`,
14515
+ idempotencyKey: "refused",
14516
+ actionVersion: 8,
14517
+ actorId: "refused-actor",
14518
+ actorType: "agent",
14519
+ authorizationBindingId: "refused-binding",
14520
+ correlationId: id,
14521
+ submissionRefusal: { class: "authorization" },
14522
+ createdAt: now,
14523
+ updatedAt: now
14524
+ };
14525
+ await store.recordSubmissionRefusal(refused);
14526
+ const refusedReceipt = {
14527
+ ...expected,
14528
+ id: refused.id,
14529
+ idempotencyKey: refused.idempotencyKey,
14530
+ actionVersion: refused.actionVersion,
14531
+ actorId: refused.actorId,
14532
+ actorType: refused.actorType,
14533
+ authorizationBindingId: refused.authorizationBindingId,
14534
+ idempotencyActorId: void 0,
14535
+ idempotencyAuthorizationBindingId: void 0,
14536
+ status: "submission_refused",
14537
+ submissionRefusal: refused.submissionRefusal
14538
+ };
14539
+ assertProjection(await store.getSubmissionOutcomeByKey(refused), refusedReceipt);
14540
+ erasureReceipts.push(refusedReceipt);
14541
+ evidence.push("verified: recorded refusal receipt");
14542
+ } else evidence.push("skipped: store lacks recordSubmissionRefusal");
14543
+ const erasable = store;
14544
+ if (erasable.eraseStoredRecords) {
14545
+ await store.updateActionInvocation(id, key.tenantId, key.spaceId, { status: "completed" });
14546
+ for (const receipt of erasureReceipts) {
14547
+ await erasable.eraseStoredRecords({
14548
+ tenantId: key.tenantId,
14549
+ spaceId: key.spaceId,
14550
+ actorId: "operator",
14551
+ actorType: "natural_person",
14552
+ requestId: globalThis.crypto.randomUUID(),
14553
+ targets: [{
14554
+ kind: "invocation",
14555
+ invocationId: receipt.id
14556
+ }],
14557
+ redact: (view) => view
14558
+ });
14559
+ assertProjection(await store.getSubmissionOutcomeByKey(receipt), {
14560
+ ...receipt,
14561
+ storedRecordsErased: true
14562
+ });
14563
+ }
14564
+ evidence.push("verified: erased receipts for created probe rows");
14565
+ } else evidence.push("skipped: store lacks eraseStoredRecords");
14566
+ return evidence;
14567
+ }
14568
+ //#endregion
14303
14569
  exports.APPROVAL_DECIDED_EVENT = APPROVAL_DECIDED_EVENT;
14304
14570
  exports.APPROVAL_DECISION_REFUSED_EVENT = APPROVAL_DECISION_REFUSED_EVENT;
14305
14571
  exports.APPROVAL_EXPIRED_EVENT = APPROVAL_EXPIRED_EVENT;
@@ -14327,6 +14593,7 @@ exports.STAGED_INVOCATION_KEY_PATTERN = STAGED_INVOCATION_KEY_PATTERN;
14327
14593
  exports.STAGED_INVOCATION_PRINCIPAL_PATTERN = STAGED_INVOCATION_PRINCIPAL_PATTERN;
14328
14594
  exports.StoredRecordErasureAuthorizationError = StoredRecordErasureAuthorizationError;
14329
14595
  exports.StoredRecordsErasedError = StoredRecordsErasedError;
14596
+ exports.SubmissionOutcomeUnsupportedError = SubmissionOutcomeUnsupportedError;
14330
14597
  exports.SubmissionRefusalError = SubmissionRefusalError;
14331
14598
  exports.applyPostgresMigrations = applyPostgresMigrations;
14332
14599
  exports.approvalChecks = approvalChecks;
@@ -14337,6 +14604,7 @@ Object.defineProperty(exports, "canonicalJson", {
14337
14604
  return _fabricorg_platform.canonicalJson;
14338
14605
  }
14339
14606
  });
14607
+ exports.checkSubmissionOutcomeStore = checkSubmissionOutcomeStore;
14340
14608
  exports.cloneOutboxRecord = cloneOutboxRecord;
14341
14609
  exports.cloneStagedInvocationRecord = cloneStagedInvocationRecord;
14342
14610
  exports.createDurableSagaParentLifecycle = createDurableSagaParentLifecycle;