@ultimat3/action 23.0.0 → 25.0.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/CLAUDE.md +91 -42
- package/README.md +167 -47
- package/package.json +10 -9
- package/src/action.ts +25 -69
- package/src/api-declaration.ts +1 -1
- package/src/audit-gate.ts +3 -4
- package/src/audit-memory.ts +5 -3
- package/src/audit-postgres.ts +16 -8
- package/src/audit.ts +7 -106
- package/src/cache-gate.ts +25 -6
- package/src/client.ts +18 -6
- package/src/contract-test.ts +50 -14
- package/src/error-titles.ts +51 -0
- package/src/errors-http.ts +3 -1
- package/src/errors-idempotency.ts +57 -5
- package/src/errors.ts +44 -61
- package/src/facade.ts +6 -7
- package/src/http-path.ts +1 -1
- package/src/http.ts +23 -20
- package/src/idempotency-memory.ts +92 -8
- package/src/idempotency-postgres.ts +91 -39
- package/src/idempotency-redact.ts +140 -0
- package/src/idempotency.ts +58 -7
- package/src/index.ts +37 -88
- package/src/invoke.ts +57 -21
- package/src/job-handle.ts +5 -3
- package/src/json-schema.ts +6 -10
- package/src/mutator.ts +14 -4
- package/src/naming.ts +2 -3
- package/src/openapi-complete.ts +16 -1
- package/src/openapi.ts +25 -2
- package/src/policy-gate.ts +8 -51
- package/src/rate-limit-gate.ts +114 -0
- package/src/record-wire.ts +2 -2
- package/src/registry.ts +5 -4
- package/src/request-deadline.ts +25 -0
- package/src/stable.ts +0 -7
- package/src/transition.ts +35 -9
- package/src/tx-scope.ts +57 -0
- package/src/type-pins.ts +98 -3
- package/src/deprecation.ts +0 -77
- package/src/mcp-tool.ts +0 -87
- package/src/wire-headers.ts +0 -20
package/CLAUDE.md
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
# @ultimat3/action
|
|
2
2
|
|
|
3
|
-
Owns the `action` + `mutator` primitives and
|
|
3
|
+
Owns the `action` + `mutator` primitives and five projections. Tier 3. The MCP tool is
|
|
4
|
+
`@ultimat3/mcp`'s one projection (O-tool, 25.0.0).
|
|
4
5
|
|
|
5
6
|
## Boundary
|
|
6
7
|
|
|
7
|
-
- May import: `core`, `schema` (t0), `cache`, `i18n`, `time` (t1), `entity`, `policy`, `http` (t2).
|
|
8
|
+
- May import: `core`, `schema` (t0), `cache`, `db`, `i18n`, `time` (t1), `entity`, `policy`, `http` (t2).
|
|
8
9
|
`entity` is a real edge since 21.0.0 (`record-wire.ts` → `hasEntityRows`/`rowsOf`), downward 3→2.
|
|
10
|
+
`db` is one since 2026-10-02, downward 3→1, through **`tx-scope.ts` only** (`currentTx`).
|
|
9
11
|
- Never import: `query`, `jobs`, `realtime` (sideways), or any tier 4-5 package.
|
|
10
12
|
- Never re-implement authz, validation or caching — call `policy`, `schema`, `cache`.
|
|
11
13
|
|
|
@@ -26,55 +28,59 @@ Owns the `action` + `mutator` primitives and their six projections. Tier 3.
|
|
|
26
28
|
| `http-path.ts` | where an action is served: the app's `pathStyle` + a per-action `http.path` pin, read lazily by every projection |
|
|
27
29
|
| `path-style-miss.ts` | `explainActionPathMiss` — `@ultimat3/http`'s `hooks.explainMiss`: a POST under the style this app does not serve is `X_CONTRACT_DRIFT` naming the one it does |
|
|
28
30
|
| `api-declaration.ts` | `defineApi({ http, openapi })` held process-wide for the boot and `x manifest` |
|
|
29
|
-
| `errors-http.ts` | the three HTTP-declaration refusals
|
|
31
|
+
| `errors-http.ts` | the three HTTP-declaration refusals |
|
|
32
|
+
| `error-titles.ts` | every code's title, the one `registerErrorCodes` |
|
|
30
33
|
| `client.ts` | typed RPC client (browser-safe: no server imports) — dispatches through core's `clientTransport` |
|
|
31
34
|
| `record-wire.ts` | the record envelope on the HTTP projection: `carriesRecords` (from the output schema), the enveloped 200, and its OpenAPI shape. Server-only — `client.ts` never imports it |
|
|
32
35
|
| `wire-issues.ts` | the ONE reader of a problem document's `issues` member — an untrusted array back into `@ultimat3/schema`'s `ValidationIssue` shape |
|
|
33
36
|
| `transition.ts` | `transition()`: a MUTATOR factory over one entity column's state machine. Declares no error code — entity's three propagate |
|
|
34
|
-
| — | opt-in flight control is **`@ultimat3/core`**'s `client-flight.ts` + `client-wire.ts`,
|
|
35
|
-
| `wire-headers.ts` | `BUILD_ID_HEADER` + `IDEMPOTENCY_HEADER`, and nothing else. Their own module so `client.ts` can name them without importing `http.ts` |
|
|
36
|
-
| `mcp-tool.ts` | MCP descriptor, same `invoke` |
|
|
37
|
+
| — | opt-in flight control is **`@ultimat3/core`**'s `client-flight.ts` + `client-wire.ts`, imported from core — `src/index.ts` re-exports only the `ClientFlight`/`ClientRetry` types. There is no local copy and must not be one |
|
|
37
38
|
| `job-handle.ts` | the `.job()` projection: an action as a queueable payload. **Not** consumed by `@ultimat3/jobs` — see Invariants |
|
|
38
39
|
| `contract-test.ts` | assertions `x g action` emits |
|
|
39
40
|
| `sample-input.ts` | a value `input:` accepts, from its own IR — what makes the policy assertion reach a policy |
|
|
40
41
|
| `idempotency.ts` | the store SEAM: types, the installed-store slot, the scope declaration + `assertIdempotencyScope`, and `withIdempotency` — the replay-or-run gate |
|
|
41
42
|
| `idempotency-key.ts` | the namespaced key — action + actor + the caller's key, as one JSON tuple — and the refusal of one that names no request |
|
|
42
43
|
| `idempotency-memory.ts` | the process default: bounded, swept, `scope: 'process'` |
|
|
44
|
+
| `idempotency-redact.ts` | `restingAnswer` — the copy of an answer a store may keep: core's `isRedactedKey` + `isSecret`, the SAME reference when nothing is redacted |
|
|
43
45
|
| `idempotency-postgres.ts` | the SHARED store — one table, one `insert … on conflict` |
|
|
44
|
-
| `deprecation.ts` | `Deprecation` + the RFC 9745/8594 render + the `deprecated_calls_total` counter |
|
|
45
46
|
| `policy-gate.ts` | **the only** runtime edge to `@ultimat3/policy` (`errors.ts` takes `SurfaceDenial` as a type, which erases) |
|
|
46
|
-
| `cache-gate.ts` | the post-
|
|
47
|
-
| `
|
|
47
|
+
| `cache-gate.ts` | the post-COMMIT bust — **the only** file that calls `invalidateTags` |
|
|
48
|
+
| `tx-scope.ts` | the open transaction — **the only** file that imports `@ultimat3/db` (`tx-scope.test.ts`): `openCommitScope` (the bust) and `liveTransaction` (the settle) |
|
|
49
|
+
| `rate-limit-gate.ts` | **the one place** a declared `rateLimit:` is spent, and the installed-store slot |
|
|
50
|
+
| `request-deadline.ts` | `requestDeadlineMs` — the app's `requestTimeoutMs`, resolved by http's own `defineHttpConfig` |
|
|
51
|
+
| `audit.ts` | the audit seam's TYPES, re-exported from `@ultimat3/core` (shared with `query`); the slot (`setAuditSink`) is core's alone |
|
|
48
52
|
| `audit-memory.ts` | the process default: a bounded ring that DROPS, and counts what it dropped |
|
|
49
53
|
| `audit-postgres.ts` | the DURABLE sink — one append-only `x_audit` table, one insert per record |
|
|
50
54
|
| `audit-input.ts` | what may be written DOWN: an `input` redacted through core's table and made JSON-representable on every path |
|
|
51
55
|
| `audit-gate.ts` | **the only** file that calls a sink, and where the two failure policies live |
|
|
52
56
|
| `type-pins.ts` | compile-time assertions `tsc` checks — what the erased view projects, and why `client()` is not part of it |
|
|
53
|
-
| `naming.ts`, `validate.ts`, `json-schema.ts`, `stable.ts` | pure helpers. `stable.ts` is the DOCUMENT serializer
|
|
57
|
+
| `naming.ts`, `validate.ts`, `json-schema.ts`, `stable.ts` | pure helpers. `stable.ts` is the DOCUMENT serializer (core's `isJsonObject` is imported from core, never re-exported) — the hash form is `@ultimat3/core`'s `canonicalJson`/`fingerprint` |
|
|
54
58
|
|
|
55
59
|
## Invariants — execution and authz
|
|
56
60
|
|
|
57
|
-
- Every surface goes through `invoke`: the policy's actor half (`
|
|
61
|
+
- Every surface goes through `invoke`: the policy's actor half (`guardActionBeforeInput`, 403 before the
|
|
58
62
|
parse), parse input, evaluate policy, handle, parse output. A second
|
|
59
63
|
execution path is the one unforgivable change here.
|
|
60
64
|
- **An explicit `ctx` is INSTALLED, never merely passed** — the ambient context (which
|
|
61
|
-
`@ultimat3/entity`'s tenant guard reads) must be the identity `
|
|
65
|
+
`@ultimat3/entity`'s tenant guard reads) must be the identity `guardAction()` decided about.
|
|
62
66
|
`invoke-context.test.ts` asserts ambient, `options.actor` and `options.ctx` are one caller.
|
|
63
67
|
- The declaration never leaves `invoke.ts`: `defOf`/`stashDef` are never re-exported from
|
|
64
68
|
`src/index.ts` (`index.test.ts`). An action has no `.def`; outside, read `.input`/`.output`/
|
|
65
69
|
`.policy`/`.mcp` or `describe()`. App code reaches a projection through the action
|
|
66
|
-
(`publishPost.
|
|
70
|
+
(`publishPost.openapi()`); new methods are bound in `facade.ts`. **No `.tool()`**: it was a second
|
|
71
|
+
MCP projection beside mcp's `toolFrom` (`index.test.ts`, `type-pins.ts`).
|
|
67
72
|
- **`toRoute` sets `enforcedBy: 'handler'`** — `invoke` is the one evaluation and the only one holding
|
|
68
73
|
the row `def.row` loaded; `meta.policy` stays set. `http.test.ts` counts exactly one evaluation.
|
|
69
|
-
- **`meta.auth` comes from `@ultimat3/policy`'s `admitsAnonymous`** (a walk of the tree,
|
|
70
|
-
|
|
74
|
+
- **`meta.auth` comes from `@ultimat3/policy`'s `admitsAnonymous`** (a walk of the tree, imported
|
|
75
|
+
from `@ultimat3/policy` by `http.ts`; 25.0.0 dropped this package's re-export, `index.test.ts`
|
|
76
|
+
pins the absence), never the root combinator — never a copy here (`@ultimat3/query` needs the same
|
|
71
77
|
answer).
|
|
72
78
|
- Authz goes through `enforce(surface, policy, { input, actor, ctx })`; a denial becomes
|
|
73
79
|
`ActionDeniedError`, keeping the policy's code. `policy-gate.ts` is the only RUNTIME edge to
|
|
74
80
|
`@ultimat3/policy` (`errors.ts`'s `import type { SurfaceDenial }` erases).
|
|
75
81
|
- No policy at registration → `X_ACTION_POLICY_MISSING`. No exceptions, no flag.
|
|
76
82
|
- **`registerAction` guards the derived PATH as well as the name** (`X_ACTION_PATH_DUPLICATE`; a
|
|
77
|
-
second index cleared by `
|
|
83
|
+
second index cleared by `resetActions`).
|
|
78
84
|
- **A path is the app's `pathStyle` or the action's pin, never a third answer** (`http-path.ts`).
|
|
79
85
|
`'resource'` stays the default (changing it moves every URL). `defineApi` sets the style FIRST and
|
|
80
86
|
`configureActionPathStyle` re-derives every seated action (the module scan may have seated them
|
|
@@ -98,6 +104,9 @@ Owns the `action` + `mutator` primitives and their six projections. Tier 3.
|
|
|
98
104
|
unchanged** — an upgrade must not make a committed contract stale by itself.
|
|
99
105
|
- Registration names the action the app exported, in place. Naming an already-named action is the
|
|
100
106
|
only case that twins.
|
|
107
|
+
- **A mutator declares `idempotent: true` or does not exist**: required by `MutatorDef`, and
|
|
108
|
+
`X_MUTATOR_NOT_IDEMPOTENT` at `mutator()` for anything else. The client replays a mutator's write
|
|
109
|
+
under one key; `transition()` and `x g mutator` declare it. Never a default.
|
|
101
110
|
- A mutator projects `.local`, `.server`, `.conflict` plus every action member — no aliases.
|
|
102
111
|
`mutator.server()` calls the action's own callable (lands in `invoke`); `.local()` never leaves the
|
|
103
112
|
client.
|
|
@@ -110,7 +119,8 @@ Owns the `action` + `mutator` primitives and their six projections. Tier 3.
|
|
|
110
119
|
- `src/index.ts` re-exports `t` from `@ultimat3/schema` **verbatim** (`index.test.ts` asserts identity).
|
|
111
120
|
- **`transition()` is a mutator factory that decides nothing about the machine**: entity's three
|
|
112
121
|
codes propagate; `from` is REQUIRED (it is the UPDATE's predicate); `conflict: 'server-wins'` fixed;
|
|
113
|
-
`audit` off unless declared.
|
|
122
|
+
`audit` off unless declared; `id` is the key `output.id` declares (`keySchemaOf`), never a fixed
|
|
123
|
+
`t.uuid`.
|
|
114
124
|
- **A lookup table is read with `Object.hasOwn`** (`IRREGULAR` in `naming.ts`, `BY_FORMAT` in
|
|
115
125
|
`sample-input.ts`) — caller- or provider-supplied keys.
|
|
116
126
|
|
|
@@ -145,11 +155,21 @@ Owns the `action` + `mutator` primitives and their six projections. Tier 3.
|
|
|
145
155
|
`json-schema.ts`). The HASH form is core's `canonicalJson`/`fingerprint` (injective: tags
|
|
146
156
|
`NaN`/`±Infinity`/`-0`, `Date`, `Map`, `Set`); `stable.test.ts` pins byte-equality for ordinary
|
|
147
157
|
payloads.
|
|
148
|
-
- **`tagKeys` is `@ultimat3/cache`'s and `toBucket` is `@ultimat3/http`'s** (
|
|
149
|
-
`X_RATE_LIMIT_INVALID`). Never restore a local copy
|
|
150
|
-
- **`rateLimit:`
|
|
151
|
-
(`
|
|
152
|
-
|
|
158
|
+
- **`tagKeys` is `@ultimat3/cache`'s and `toBucket` is `@ultimat3/http`'s** (imported, never
|
|
159
|
+
re-exported; `toBucket` raises `X_RATE_LIMIT_INVALID`). Never restore a local copy.
|
|
160
|
+
- **`rateLimit:` is spent ONCE, inside `invoke`** (`rate-limit-gate.ts`), so HTTP, MCP, the agent
|
|
161
|
+
tool and `.job()` share one bucket; `surface: 'server'` spends nothing. `toRoute` sets
|
|
162
|
+
`rateLimitedBy: 'handler'` and NO `meta.rateLimit`/`rateLimitBucket` — the stage spends neither
|
|
163
|
+
the action's bucket (the old HTTP-only point) nor `default`. Key `action:<name>|<subject>`.
|
|
164
|
+
HTTP gets `RateLimit-*` via `onRateLimit` → http's `publishRateLimit`. The store is
|
|
165
|
+
`@ultimat3/http`'s installed one, adopted by the boot on every role. Spent after
|
|
166
|
+
`guardActionBeforeInput`, BEFORE the parse and `def.row()` (an input/row-denied flood is refused). A key
|
|
167
|
+
the idempotency store holds spends nothing (replay); a reservation `withIdempotency` CREATES that
|
|
168
|
+
the peek did not pay for spends in `beforeRun`, released on refusal. A job run is charged to its
|
|
169
|
+
org, else `job:unattributed`. A surface's `clientAddress` is inherited by nested `invoke`s
|
|
170
|
+
(`withCallerAddress`); a `'job'` invoke opens a fresh frame (`withJobFrame`) so no visitor's
|
|
171
|
+
address follows work into the worker, and calls nested in it are the job's.
|
|
172
|
+
`rate-limit-surfaces.test.ts`. The computed rate must be finite and `limit` at least one token.
|
|
153
173
|
- **`deprecated:` is a compat WINDOW** — headers on every response including failures, a
|
|
154
174
|
`rel="successor-version"` link via `derivePath`, `deprecated: true` in OpenAPI,
|
|
155
175
|
`deprecated_calls_total`. Rendered ONCE at projection (`X_ACTION_DEPRECATION_INVALID` at mount).
|
|
@@ -157,11 +177,11 @@ Owns the `action` + `mutator` primitives and their six projections. Tier 3.
|
|
|
157
177
|
- **The span wraps `execute` whole** (including `def.row()`), with bounded attributes plus the
|
|
158
178
|
namespaced idempotency key (never a metric label). `telemetry.test.ts`.
|
|
159
179
|
- **`policyCapability` is a display label; `policyPermissions` (`ActionDescriptor.permissions`) is
|
|
160
|
-
what a report matches on** (a `not()` clause contributes its permissions).
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
180
|
+
what a report matches on** (a `not()` clause contributes its permissions). Both are
|
|
181
|
+
`@ultimat3/policy`'s, imported from there.
|
|
182
|
+
- **MCP exposure is core's `isMcpExposed`** (`describeAction`, `x-ultimate.mcpTool`); **a tool's
|
|
183
|
+
NAME is the export name verbatim** (`mcp-surface.test.ts`). `ActionFact.mcp` in the app manifest
|
|
184
|
+
carries only `{ expose, description? }`.
|
|
165
185
|
- **`ActionJobHandle` is not consumed by `@ultimat3/jobs`** (`isJobHandle` needs `job()`'s private
|
|
166
186
|
map). `.job()` gives `action:<name>` as a queue key, a payload-derived idempotency key and an
|
|
167
187
|
`invoke` under `surface: 'job'`. The bridge is `agentJob()` in `@ultimat3/ai`, or an app's own
|
|
@@ -169,24 +189,25 @@ Owns the `action` + `mutator` primitives and their six projections. Tier 3.
|
|
|
169
189
|
|
|
170
190
|
## Invariants — flight control
|
|
171
191
|
|
|
172
|
-
- **Flight control is `@ultimat3/core`'s, re-exported** —
|
|
173
|
-
|
|
174
|
-
retry loop here (`bun run flight-copies`).
|
|
192
|
+
- **Flight control is `@ultimat3/core`'s, and NOT re-exported** — its values are imported from
|
|
193
|
+
core (`X_HELPER_COPY` refuses a re-export); only the types `ClientFlight`/`ClientRetry`. Fix the
|
|
194
|
+
pipeline in `packages/core/src/client-flight.ts`. No new code, curve, fence or retry loop here (`bun run flight-copies`).
|
|
175
195
|
- **`isTransientFailure` INVERTS `retryDecision`'s unclassified default** (a caller's `AbortError`
|
|
176
196
|
is terminal). Must survive.
|
|
177
197
|
- **`ClientFlight` is a TYPE inside `client.ts`, never a value.** Measured,
|
|
178
198
|
`bun build --target=browser --minify`, one entry importing from `@ultimat3/action` — **the ONE
|
|
179
199
|
table for these figures** (`packages/core/CLAUDE.md` points here):
|
|
180
200
|
|
|
181
|
-
| Entry |
|
|
182
|
-
|
|
183
|
-
| `rpc` |
|
|
184
|
-
| `rpc` + `
|
|
201
|
+
| Entry | `As of 2026-10-01` |
|
|
202
|
+
|---|---|
|
|
203
|
+
| `rpc` | 19,671 B (+181 B for the document's path-style stamp) |
|
|
204
|
+
| `rpc` + core's `clientFlight` | 25,954 B |
|
|
185
205
|
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
206
|
+
Earlier columns (pre-transport 18,097 B, +977 B net for the envelope decoder) are in git history.
|
|
207
|
+
- **`sideEffects` is `["./src/error-titles.ts"]`**, never `false` (drops its bare imports): the
|
|
208
|
+
titles ride into every barrel chunk (`problemError` decodes by code), `errors.ts`'s classes do
|
|
209
|
+
not. An http refusal needs no http module: core's decoders take its title off the body and its
|
|
210
|
+
`retry-after` off the header (`rpc-refusal.test.ts`).
|
|
190
211
|
|
|
191
212
|
## Invariants — idempotency
|
|
192
213
|
|
|
@@ -201,22 +222,47 @@ Owns the `action` + `mutator` primitives and their six projections. Tier 3.
|
|
|
201
222
|
inside `withWriteOrigin(writeDigest(key))` so the page recognises its own `records` echo. A label,
|
|
202
223
|
never a gate.
|
|
203
224
|
- **Both stores FENCE a settlement on the reservation `id` AND `in-flight`**:
|
|
204
|
-
`settle(key, value, reservationId)` / `fail(key, failure, reservationId)`. A fenced no-op is logged,
|
|
225
|
+
`settle(key, value, reservationId, redacted)` / `fail(key, failure, reservationId)`. A fenced no-op is logged,
|
|
205
226
|
never thrown.
|
|
227
|
+
- **An answer rests REDACTED** (#591): `withIdempotency` passes the store `restingAnswer(value)`
|
|
228
|
+
(core's `isRedactedKey` + `isSecret` — never a second rule) and `redacted` as `settle`'s
|
|
229
|
+
REQUIRED 4th argument; both stores keep it (`x_idempotency.redacted`, memory's record field).
|
|
230
|
+
A flagged replay is `X_IDEMPOTENT_REPLAY_REDACTED`, decided by the FLAG, never by scanning for
|
|
231
|
+
`[redacted]`. Plain data is the same reference, replayed as before; anything else that
|
|
232
|
+
`JSON.stringify` would run app code on (`toJSON`, `Map`/`Set`, class instances, `Date`
|
|
233
|
+
subclasses) is redacted FAIL-CLOSED, and a getter is read once. `keepsRedaction: true` is a
|
|
234
|
+
required store declaration (`type-pins.ts`: a pre-25 store is a build error — a required
|
|
235
|
+
parameter alone is not). A reclaim and a `fail` reset the flag.
|
|
206
236
|
- **A stored status is NARROWED** (`isIdempotencyStatus`; unknown is
|
|
207
237
|
`X_IDEMPOTENCY_STATUS_UNKNOWN`), never cast.
|
|
208
238
|
- **Where records live is DECLARED and refused at registration**: `IdempotencyStore.scope` vs
|
|
209
239
|
`configureIdempotency({ scope })`, compared by `assertIdempotencyScope` in `registerAction`
|
|
210
240
|
(`X_IDEMPOTENCY_NOT_SHARED`). Default `'process'`.
|
|
211
241
|
- **The memory store is bounded; `in-flight` records are the last evicted.** Never an LRU.
|
|
212
|
-
- **`postgresIdempotencyStore` is the shared store** over
|
|
213
|
-
|
|
242
|
+
- **`postgresIdempotencyStore` is the shared store** over `@ultimat3/core`'s structural `PgExecutor`; the
|
|
243
|
+
reservation is ONE `insert … on conflict` statement. The CLI boot installs it.
|
|
244
|
+
- **Inside a transaction the SETTLE commits with the write, on BOTH stores** (`liveTransaction()`):
|
|
245
|
+
Postgres on the handler's connection, memory at `onCommit`. The reservation and `fail` stay
|
|
246
|
+
immediate. A tx-bound settle that matches no record THROWS `X_IDEMPOTENCY_RESERVATION_LOST` (the
|
|
247
|
+
rollback of an attempt a retry replaced); the autocommit one only logs. Only a tx-bound
|
|
248
|
+
`in-flight` record is reclaimed past the deadline — never an autocommit one.
|
|
249
|
+
`idempotency-parity.test.ts` (memory + PGlite), `idempotency-postgres.live.test.ts`.
|
|
250
|
+
- **The deadline is `reclaimAfterMs: () => number`, REQUIRED on the Postgres store** and read per
|
|
251
|
+
reservation — the app's `requestTimeoutMs` through `requestDeadlineMs` (`request-deadline.ts`,
|
|
252
|
+
http's own default, never a number here). **`origin: () => object` is required too**: a
|
|
253
|
+
transaction on another database settles on the pool. A FINISHED scope's handle binds nothing.
|
|
214
254
|
|
|
215
255
|
## Invariants — cache and audit
|
|
216
256
|
|
|
217
257
|
- **The post-commit bust never fails the write** — `cache-gate.ts` (the only `invalidateTags` caller)
|
|
218
258
|
absorbs a refusing fan-out and logs through core's `logger`, never rendering the tags. A replay
|
|
219
259
|
skips the bust.
|
|
260
|
+
- **The bust waits for the ROOT commit** when a transaction is open (`openCommitScope().onCommit`):
|
|
261
|
+
dropped on rollback, never fired before the rows are durable. A straggler past its scope gets
|
|
262
|
+
`DbTx.onCommit`'s own answer, as entity's row observer does. `cache-gate-tx.test.ts` (PGlite).
|
|
263
|
+
- **The OpenAPI contract assertion reads the registry-wide document** and compares `operationId`
|
|
264
|
+
— one built from the action alone could not fail. **`Problem` is `toProblem`'s members**, no
|
|
265
|
+
`code` pattern (`openapi-problem.test.ts`).
|
|
220
266
|
- **The policy contract test asserts `ActionDeniedError` and sends valid input** (`sampleInput` from
|
|
221
267
|
the input IR). Only `X_INPUT_INVALID` (and `X_AUDIT_SINK_MISSING`, the one refusal before the parse)
|
|
222
268
|
becomes `X_CONTRACT_DRIFT`; everything else keeps its own code; a non-`UltimateError` is rethrown.
|
|
@@ -225,6 +271,8 @@ Owns the `action` + `mutator` primitives and their six projections. Tier 3.
|
|
|
225
271
|
DENIED attempt is recorded. No audit entity, retention, hash chain or "who" convention. The
|
|
226
272
|
vocabulary matches `@ultimat3/admin`'s by name (tier 5, no edge); admin's `AuditSink` is a known
|
|
227
273
|
duplicate that unifying would need core for.
|
|
274
|
+
- **Records carry `name` + `primitive`, required** (`type-pins.ts`); no `action` alias since 25.0.0.
|
|
275
|
+
The `x_audit.action` column holds `name`.
|
|
228
276
|
- **The memory sink DROPS** — a ring at `DEFAULT_MAX_AUDIT_RECORDS`, oldest first, counting `dropped`;
|
|
229
277
|
no spelling of "unbounded".
|
|
230
278
|
- **A durable sink writes what `audit-input.ts` allows**: input redacted through core's
|
|
@@ -234,7 +282,8 @@ Owns the `action` + `mutator` primitives and their six projections. Tier 3.
|
|
|
234
282
|
`locale`, `tz`, `buildId`, `role`, actor `id`/`kind`/`orgId`/`onBehalfOf`) and keeps
|
|
235
283
|
`failure.code`, never the error.
|
|
236
284
|
- **`x_audit` ships no purge**, deliberately. **`SQL_AUDIT_INSERT` is positional**, pinned by
|
|
237
|
-
`audit-parity.test.ts` with a distinct value per field.
|
|
285
|
+
`audit-parity.test.ts` with a distinct value per field. Only `*_TABLE` DDL is exported
|
|
286
|
+
(`X_SQL_EXPORT_UNREAD`).
|
|
238
287
|
- **The two failure policies are opposites**: `X_AUDIT_SINK_MISSING` before the input parse (no
|
|
239
288
|
logger-backed default sink); a sink refusing an ALLOWED record is `X_AUDIT_SINK_FAILED`, whose `fix:`
|
|
240
289
|
branches on `record.idempotencyKey !== null` (`meta.replayable`); a sink refusing a denied or failed
|