@fabricorg/platform-host 8.8.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,338 @@
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
+
15
+ ## 8.9.0
16
+
17
+ ### Minor Changes
18
+
19
+ - 7cb17c9: Add `RetryableHandlerError` and cross-copy recognition, plus optional confirmed rollback
20
+ retry inside one Host execution attempt. The default budget is three total tries (1–10).
21
+ Host requires a handler-call throw, identical-cause `ConfirmedTransactionRollback`, no adapter
22
+ or handoff evidence, fresh durable-derived inputs and transaction-scoped ownership fencing.
23
+ Policy evaluations, obligations, approvals and worker claim counts are retained; exhaustion
24
+ settles the last handler error through the existing redactor and statuses. Governance
25
+ `RetryableExecutionError`, returned failures and non-handler callback/commit errors retain
26
+ their existing failure semantics.
27
+
28
+ Expose `handlerRetry` classification, bounded jitter/delay and retry notification options,
29
+ and `isTransientPostgresTransactionError` for candidate SQLSTATE classification. Application
30
+ transaction providers can opt into rollback confirmation after successful rollback of a failed
31
+ callback. Built-in stores expose evidence for Host's internal handler callback request and
32
+ preserve prior error contracts for ordinary application transactions, approval, erasure and
33
+ other Host APIs, including callback-error identity and erasure's SQLSTATE mappings. Memory confirms after a configured provider's successful snapshot/restore
34
+ and ledger undo; commit failures and providerless internal transactions are not confirmed.
35
+ Custom stores exposing confirmation from their public transaction API remain supported through
36
+ normalization at non-handler Host and conformance boundaries. Retried callbacks must keep
37
+ effects inside the supplied transaction.
38
+
39
+ Retry observers run without awaiting them; synchronous throws and asynchronous rejections
40
+ are ignored. Memory rollback failures cannot authorize handler retry.
41
+
42
+ - be0938a: Export runner-independent Host store conformance suites: `stagedInvocationChecks`,
43
+ `runStagedInvocationChecks`, `outboxRequeueChecks`, `runOutboxRequeueChecks`,
44
+ `approvalChecks`, and `runApprovalChecks`, with their subject, check, and result types.
45
+ The destructive probes cover staged lease fencing and dispatch, operator outbox
46
+ requeue, and recorded and inline approval behavior on isolated test stores.
47
+ - db6ca3b: Export runner-independent `outboxRelayChecks` / `runOutboxRelayChecks` and
48
+ `invocationIdempotencyChecks` / `runInvocationIdempotencyChecks`, with subject,
49
+ check and result types. The destructive isolated-store probes pin shared relay
50
+ claim/settlement/renewal fences and first-row-wins invocation key arbitration,
51
+ including admission timestamp retention and optional refusal/admitted winners.
52
+ Document these store contracts without changing store signatures or conflict
53
+ classification, and leave claim result ordering unspecified.
54
+
55
+ The relay checks require numeric lease tokens that increase on each claim,
56
+ including reclaim, despite optional token fields in the general interfaces.
57
+ Use per-run future base times for relay and requeue probes so wall-clock
58
+ settlement guards can refuse expired leases, and set future relay availability
59
+ through retry settlement without requiring it to equal event recording time.
60
+
61
+ - d09f378: Add optional `ErasablePlatformHostStore`, the separate `StoredRecordErasureHost` operator
62
+ and `createStoredRecordErasureHost`, plus erasure conformance checks. Built-in stores
63
+ atomically redact settled invocation/event copies, record scoped markers and append
64
+ sequenced `HostRecordsErased` evidence. Mixed batches can erase settled targets and report
65
+ active/missing ones. Invalid redactors roll the batch back; repeats process stored redacted
66
+ data again. Explicit structure paths protect Host state and identity while application
67
+ values, including reserved-looking keys in policy facts and metadata, remain visible.
68
+
69
+ Erasure requests require trusted `actorId` and `actorType`. `validateErasureInput` rejects
70
+ blank actor fields and NUL/lone-surrogate characters in `actorId`, and checks `actorType`
71
+ with Platform's `isActorType`. `createStoredRecordErasureHost(store, authorization)`
72
+ requires the authorization object; the integrated Host passes its own. The operator awaits
73
+ `authorization.authorizeStoredRecordErasure` after validation and before the store call,
74
+ preserving class receivers and doing no preceding record read. The hook receives exactly
75
+ actor ID/type, tenant, space, request ID and detached validated targets, without `redact`
76
+ or stored payloads. The strict `authorized !== true` guard refuses absent hooks and
77
+ non-`true` results with exported `StoredRecordErasureAuthorizationError`
78
+ (`refusal: "not_authorized"`). The missing-hook message differs from the actor-denied
79
+ message; neither discloses target existence. A thrown hook error propagates unchanged
80
+ before store access, distinguishing a failed check from a denial. Validation errors
81
+ and `ErasureBusyError` remain distinct. Both stores retain the actor in the
82
+ `HostRecordsErased` envelope as identifying metadata outside the redaction view.
83
+ The conformance kit's success probes require exactly one scoped `HostRecordsErased` event
84
+ with the request's actor ID/type, rejecting stores that omit or misattribute the audit.
85
+ The store capability remains an unauthenticated persistence port, not an authorization
86
+ boundary; direct callers enforce authorization themselves. This revises the unreleased
87
+ erasure API rather than preserving its unauthenticated factory signature.
88
+
89
+ Event targets cover canonical and delivery/requeue records. They do not erase copies in
90
+ invocation checkpoints/results because checkpoint events lack event IDs. Use invocation
91
+ targets for those copies. Erasure is the supported exception to append-only event payload
92
+ storage; normal append retains first-write-wins behavior, identity, sequence and order.
93
+
94
+ PostgreSQL erasure requires a transaction provider running READ COMMITTED; it checks
95
+ `SHOW transaction_isolation` and refuses other levels. Ordinary writers at stricter
96
+ isolation levels may receive a serialization failure instead of an erased outcome.
97
+ Apply migration 16, upgrade **all Host instances sharing the database**, then enable
98
+ erasure. Older Host SQL and direct application SQL bypass these guards and are unsupported
99
+ once erasure is enabled. Migration 16 adds metadata columns, markers and erasure-lookup
100
+ indexes without triggers or write-path functions. Like migrations 1–15 it runs in the
101
+ migration runner's transaction and takes table DDL locks; ordinary index builds block
102
+ writes for their duration on large tables. Plan an appropriate maintenance window.
103
+
104
+ `eraseStoredRecords` processes sorted targets inside individual savepoints. An invocation
105
+ target locks its invocation and dependent adapter, governance, staged parent/child, event,
106
+ outbox and requeue rows. It also locks the stage identified by the child's stored
107
+ `staged_parent_invocation_id` and `staged_stage_key`, even before that stage has recorded
108
+ its child ID. A pending or leased origin stage keeps the child active; a settled origin
109
+ stage is redacted with the child. This follows the stage, not the parent invocation's
110
+ payloads or other stages; target the parent separately for its checkpoint copies. An event target locks its event and delivery/requeue rows, without
111
+ locking its invocation owner or sibling rows. Canonical delivery copies are found by event
112
+ ID. Legacy outbox-only rows with null owner columns are found through the indexed envelope
113
+ `actionInvocationId`; new inserts populate the owner column explicitly. No advisory lock
114
+ is taken by erasure. The migration runner retains its separate schema advisory lock.
115
+
116
+ The first existing target lock may wait within `lockTimeoutMs` (default 250 ms, including
117
+ provider acquisition). While retaining target locks, subsequent row locks, including the
118
+ final tenant/space sequence-row lock, use `NOWAIT`; contention rolls back with
119
+ `ErasureBusyError`. The audit allocation and insert are the final SQL statements before
120
+ provider commit. Before opening that transaction, a separate provider transaction commits
121
+ an `event_sequences` row with `next_sequence=0` using `ON CONFLICT DO NOTHING`. It takes no
122
+ target locks. It attempts the insert only when it sees no committed row for the tenant and
123
+ space, so it can wait, within the acquisition budget, only for another transaction's
124
+ uncommitted first insert of that row. The zero seed consumes no event
125
+ sequence: the first allocation returns 1. The erasure transaction then locks the existing
126
+ row with `NOWAIT`; it does not try to insert a missing sequence row while holding targets.
127
+ The seed can remain after a refused or rolled-back erasure without consuming a sequence.
128
+ Steady event appends in the same tenant and space can starve erasure: it can repeatedly
129
+ report busy on this final nonwaiting lock. Each retry repeats callbacks and rewrites before
130
+ attempting that lock; busy rolls those rewrites back.
131
+
132
+ Dependent rows are locked before settlement and callbacks. A claim/renewal committed before
133
+ those locks is observed. Active/missing targets roll back to their savepoints, releasing
134
+ locks acquired for that target before processing the next. Locks retained from an earlier
135
+ overlapping accepted target stay held. Invocation settlement is evaluated on locked rows.
136
+ For an event-only target the owner's status, stages and sibling leases are observed after
137
+ the event closure is locked, but are not themselves locked. A concurrent change to that
138
+ owner/sibling activity can therefore race that observation. Target the invocation when
139
+ settlement of its complete closure must be serialized.
140
+
141
+ Ordinary invocation updates predicate on their own `records_erased_at`. Existing dependent
142
+ updates use their row stamp; insertion paths without a proven transaction-held owner lock
143
+ use a materialized owner read with `FOR KEY SHARE` inside the insertion statement. Host's
144
+ ordinary owner locks change from the released version's `FOR UPDATE` to `FOR NO KEY UPDATE`. Transaction-held owner reads are reused
145
+ for event and reconciliation insertion. Staged creation fences parent and initial child;
146
+ dispatch/failure attachment fences the newly attached child. An erased child cannot acquire
147
+ a stage containing old parameters through these methods. Host terminally rejects an
148
+ unsettled stage if an older/custom store nevertheless returns an erased child by origin,
149
+ so the attachment refusal cannot keep that stage retrying indefinitely. Replay conflict updates preserve
150
+ the stored row, and event reappend remains first-write-wins.
151
+
152
+ Outbox, stage and invocation claims lock actual candidate rows with `SKIP LOCKED`, without
153
+ a blocking parent lock. Stage cancellation also selects candidates with `SKIP LOCKED`; transactional approval
154
+ expiry uses the optional `lockDueApprovals` transaction member for that selection. Delivery of erased outbox records remains usable: claims,
155
+ renewals, publish/fail acknowledgement and requeue use stored redacted envelopes; failure
156
+ withholds supplied error text and requeue withholds supplied reason text while retaining
157
+ stored prior errors. The optional `renewOutboxLeaseAndRead` capability returns the stored
158
+ envelope atomically with renewal. Relays use that envelope and reject expired responses;
159
+ custom stores retain the boolean `renewOutboxLease` API. A custom store implementing
160
+ erasure without optional renew-and-read keeps a window in which the relay can publish an
161
+ envelope claimed before erasure.
162
+
163
+ These row acquisitions keep erasure from waiting on an ordinary row writer while retaining
164
+ its earlier target locks; it cannot supply the waiting edge of such a row-lock deadlock
165
+ cycle. They do not promise freedom from indirect waits in arbitrary application transactions,
166
+ DDL interference, writer-configured timeouts or external transaction-provider behavior.
167
+ Memory mirrors erased outcomes using its transaction queue and synchronous commit; it does
168
+ not provide PostgreSQL's row-level lock footprint.
169
+
170
+ Indexed selection, callbacks, rewrites and audit work use `workTimeoutMs` (default 5000 ms).
171
+ Remaining-budget checks and PostgreSQL statement/idle-transaction timeouts bound SQL and idle
172
+ lock retention. Neither budget preempts synchronous JavaScript or promises a hard real-time
173
+ network deadline. Timeout options accept integer milliseconds from 1 to 60000. Providers
174
+ should commit promptly, preserve original rollback errors and discard failed connections.
175
+ Ordinary query timeouts retain their PostgreSQL identity; database-caused `ErasureBusyError`
176
+ retains its cause.
177
+
178
+ Callers that do not use erasure still pay for the guarded paths. Owned inserts outside a
179
+ supplied mutation transaction take `FOR KEY SHARE` on the owning invocation row. Host adds
180
+ one locking invocation read per transactional event append (`src/host.ts`, `appendEvent`),
181
+ and built-in relays add one renew-and-read round trip per claimed record. A zero-row payload
182
+ write that classifies erasure adds a classification `SELECT`, including retried owned
183
+ duplicate event appends. Conditional boolean/lease writes and ownerless duplicate events
184
+ do not need that classification query.
185
+
186
+ Local PostgreSQL 16.15 measurements of invocation/adapter inserts and updates observed
187
+ approximately +0.05 to +1.6 ms per write, with one 64-KiB adapter-update sample at -0.08 ms.
188
+ The two-client, 600-update case took 842.6 ms baseline and 1135.1 ms with store guards,
189
+ about 35% longer. These compare current store paths with representative migration 1–15
190
+ SQL, including local transport, commit, serialization and hydration; they do not isolate
191
+ lock cost or predict application/cloud latency. Event/outbox write paths and lookup plans
192
+ were not re-measured. Erasure latency is outside these write measurements. Reproduce with
193
+ `FABRIC_ERASURE_MEASURE=1 pnpm test:postgres`; see `tests/erasure-measurements.ts`,
194
+ `measureErasure`.
195
+
196
+ Erasure removes the invocation `parameterDigest` (SQL `NULL`, including
197
+ `submission_refused` rows). Store-owned rewrites replace
198
+ `authorizationBinding.parameterDigest`, `externalCompletion.digest` and the
199
+ `parameterDigest` of rewritten stage rows with exported `ERASED_DIGEST = "erased"`.
200
+ The sentinel is outside the 64-hex SHA-256 output space and preserves required string
201
+ fields and the stage column's `NOT NULL` constraint. `ERASURE_PROTECTED_PATHS` keeps
202
+ these nested fields out of the callback view and rejects their insertion;
203
+ `redactStoredFields` overwrites them after merging protected structure. The stores
204
+ remove or replace top-level digests in the row rewrite, alongside its erasure stamp.
205
+ Repeating erasure leaves these digest fields absent or set to the same sentinel.
206
+
207
+ Private proposal/event comparison columns, memory maps and equality ports are removed.
208
+ Migration 16 is edited in place because it has not been published; migrations 1–15
209
+ retain their SQL. `packages/platform-host/tests/erasure-digest-cases.ts` checks raw records in both stores,
210
+ including repeat erasure, and `erasureChecks` checks the four digest outcomes.
211
+
212
+ Host keyed resubmission and recorded-refusal paths evaluate retained actor, binding ID
213
+ and action-version conflicts first. An enforce-mode conflict raises
214
+ `IdempotencyConflictError`; otherwise an erased row raises `StoredRecordsErasedError`,
215
+ including in audit-only mode, before the legacy callback or redacted-result return.
216
+ A caller that is itself refused while the key holds an erased row is handled separately:
217
+ against an erased recorded refusal, a retained-identity conflict leaves the caller's own
218
+ refusal in place and a matching identity raises `StoredRecordsErasedError`; an erased row
219
+ of any other status leaves the caller's own refusal in place.
220
+ `createDurableSagaParentLifecycle` preserves its event-type conflict check, then refuses
221
+ an erased prior event before comparing payloads. Erasure-capable custom stores expose
222
+ `storedRecordsErased` on the mutation transaction's `getEvent` result.
223
+ Store batch-replay paths refuse erased stage rows before digest comparisons, including
224
+ an origin stage erased with its child before child-ID attachment.
225
+ `packages/platform-host/tests/erasure-digest-cases.ts` pins same/different payload refusals and unchanged events.
226
+
227
+ Host overwrites only the digests it computes itself. A hash that another package or the
228
+ application places inside stored evidence is shown to the redactor and is kept unless the
229
+ redactor removes it. `@fabricorg/policy-opa` stores one by default: `inputHash`, a SHA-256
230
+ of the policy input, in the policy outcome's engine metadata. Remove it in the redactor
231
+ when the policy input can contain erased values, or configure the evaluator's `hashInput`.
232
+
233
+ The staged `batchDigest` formula now covers parent ID and sorted keys, actions,
234
+ versions and optional principals, excluding parameters and parameter digests.
235
+ Migration 16 replaces persisted `batch_digest` values with `LEGACY_BATCH_DIGEST`.
236
+ Custom `PlatformHostStore` implementations persisting stages should overwrite stored
237
+ batch digests with that placeholder and skip only the batch-digest comparison for
238
+ those rows; the remaining request identity and batch-size comparisons still apply.
239
+ `stagedInvocationRequestMatches` and `LEGACY_BATCH_DIGEST` are exported from
240
+ `@fabricorg/platform-host` for this comparison.
241
+ During a rolling upgrade, an old Host can write payload-covering digests after the
242
+ migration runs. Finish the rollout before relying on this guarantee, and replace
243
+ any such late-written digests with the placeholder.
244
+ Erasure replaces external reconciliation IDs with fresh random `rec_` IDs, including
245
+ on repeated erasure, following the existing reapplication of the redactor.
246
+ See `packages/platform-host/tests/erasure-digests.test.ts` and
247
+ `packages/platform-host/tests/staged-invocations.test.ts` for regression coverage.
248
+
249
+ Application hashes outside the protected Host paths remain visible to the redactor.
250
+
251
+ An older Host reading the missing invocation digest treats the erased row as a legacy
252
+ record and can accept a changed request under its existing key. This is an additional
253
+ reason to upgrade Host instances sharing storage before the first erasure; older writers
254
+ also bypass the erasure guards.
255
+
256
+ Store SQL guards fence later writes with typed `StoredRecordsErasedError`. Conditional settled and
257
+ adapter evidence writes return `false` in both stores. Late approval recording retains its
258
+ normal outcome without writing attempt evidence for an erased invocation.
259
+ `ExternalCompletionRefusal` gains `erased`; exhaustive switches must handle it. External
260
+ completion refuses before supplied payload access. Requeue retains redacted prior errors
261
+ and withholds fresh reason text; late publisher error text is discarded. Existing custom
262
+ base stores keep compiling; unsupported erasure checks report skipped.
263
+
264
+ Applications authorize operator calls and supply pure synchronous redactors. Payloads
265
+ handed off before commit cannot be recalled. Backups, downstream systems and application
266
+ data require separate erasure procedures.
267
+
268
+ - 0d4204e: Add the separate `ApprovalDecisionRecordingHost.recordApprovalDecision` capability for HTTP
269
+ approval endpoints. Approved decisions commit to pending without executing handlers or adapters;
270
+ denials retain failed settlement. Scoped locked classification and optional events/outbox writes
271
+ commit together. PostgreSQL requires a transaction provider, with no non-atomic fallback.
272
+
273
+ Persist caller-stable request ids and recording attempt counts in decision JSON. Matching applied
274
+ retries preserve their original verdict after worker execution; contradictory verdicts or approvers
275
+ are already decided. The first post-approval claim uses approval_resume, excludes the earlier
276
+ parking count from that run's recovery budget and permits the approved non-idempotent first run.
277
+ Later claims compare their absolute attempt count to recoveryMaxAttempts, without subtraction,
278
+ and keep recovery checks and attempt history. Computed approval-resume executions retain
279
+ leased governance retry behavior; the existing inline resume override is unchanged.
280
+
281
+ Add opt-in ApprovalDecisionRefused events with invocation identity plus a SHA-256 digest of the
282
+ exact request id (digestParameters), preserving punctuation through lifecycleId sanitization.
283
+ Store event-id deduplication retains a refused request's first event. The factory runs after any
284
+ update with the pre-patch row; payloads carry scoped late approver identity without reason,
285
+ evidence or parameters. Non-applied outcomes omit the stored decision from the API result.
286
+ For different request ids, authorization refusal precedes existing-decision classification.
287
+ Previously expired rows with approvalExpiry and no decision return expired without another patch,
288
+ including authorized late retries and rows expired by a sweep; these remain replayed: false.
289
+ An unresolved waiting action returns unsupported without changing the row, with or without a
290
+ deadline. It skips approval authorization because the invocation remains unchanged; inline resume
291
+ retains its unknown-action settlement. After commit, an applied denial or expiry emits
292
+ invocation.failed telemetry. Recording stores return an applied flag so replays and unchanged
293
+ rows do not emit that transition again. Approval queues pending without a recording-time status
294
+ event; execution telemetry starts when the worker executes it. Keep inline resume and handwritten-host
295
+ compatibility. Saga-parent approvals are unsupported. Authorization still runs before the store
296
+ transaction (#141). Upgrade workers before calling the new API: older workers mishandle recorded
297
+ approvals' attempt/reason semantics. Hosts without a worker must arrange pending-row dispatch;
298
+ executeInvocation is not a concurrent-safe targeted claim.
299
+
300
+ Inline approval persistence allowlists documented decision fields, excluding caller-supplied
301
+ requestId and recordedAtAttempt so an interrupted inline execution cannot acquire the recording
302
+ path's first-claim exemption. Recording takes its attempt marker from the locked invocation row.
303
+
304
+ - 1e46ff0: Add opt-in `idempotency.recordRefusals` to reserve keyed terminal submission refusals.
305
+ The new `submission_refused` status is inert, and refusal identity includes actor type.
306
+ Defaults retain the free-key refusal behavior. Admission-declaring actions omit refusal
307
+ digests to preserve their pre-admission parameter boundary; refusal rows store fixed evidence
308
+ without parameters, proposal or provenance attributes. Recorded errors confirm persistence,
309
+ and store failures surface unchanged. Apply migration 15 and upgrade all hosts sharing a
310
+ database before activation. Unauthorized callers can occupy keys, and callbacks returning
311
+ false during outages can permanently reserve them. Refusal codes must be fixed action codes.
312
+
313
+ With recording enabled, an empty string is a present idempotency key. Stores preserve empty
314
+ key and authorization-binding identities on hydration, and memory insertion arbitrates empty
315
+ keys consistently with PostgreSQL. Default Host submissions keep their keyless empty-string behavior.
316
+
317
+ Generic creation and status-update methods reject `submission_refused` as a write input;
318
+ the narrow refusal insert owns new refusal rows and their fixed payloads.
319
+
320
+ Validate request-only metadata before recording an eligible refusal so malformed submissions
321
+ leave their keys free. The public tenant-isolation kit adds an optional recorded-refusal scope
322
+ check, including ordinary creation against a refusal in another scope. Its result list gains
323
+ `fabric.tenancy.submission-refusal-scoped.v1`; stores without the method skip that check.
324
+ A PostgreSQL store must have migration 15 applied (`ensureSchema()`) before running the kit, or the new check fails.
325
+
326
+ - 8644cc5: Add optional transaction-bound execution and approval authority hooks. Execution checks run after
327
+ state-machine validation directly before handler effects on both atomic and legacy transaction
328
+ paths. Approval checks run after the invocation row is locked and classified directly before a
329
+ decision write. False, thrown and non-boolean results commit nothing; approval stores advertise the
330
+ new transactional authorization capability. Existing pre-transaction callbacks remain unchanged.
331
+
332
+ A throw from the execution hook settles the invocation as `failed` and is not retried, including a
333
+ `RetryableExecutionError`. The execution hook shares the handler's transaction only on a store with
334
+ `transactionWithEvents`, and it does not run before a finalizing action's `finalize` transaction.
335
+
3
336
  ## 8.8.0
4
337
 
5
338
  ### Minor Changes