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