@ultimat3/core 24.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.
Files changed (76) hide show
  1. package/CLAUDE.md +24 -27
  2. package/README.md +71 -34
  3. package/package.json +4 -7
  4. package/src/actor.ts +9 -0
  5. package/src/address-class.ts +40 -4
  6. package/src/assert.ts +9 -5
  7. package/src/audit.ts +144 -0
  8. package/src/aws-sigv4.ts +275 -0
  9. package/src/backoff.ts +16 -0
  10. package/src/bunfs.ts +17 -0
  11. package/src/client-dispatch.ts +24 -3
  12. package/src/client-flight.ts +68 -13
  13. package/src/client-problem.ts +62 -6
  14. package/src/client-retry-after.ts +47 -0
  15. package/src/client-transport.ts +3 -1
  16. package/src/client-wire.ts +27 -3
  17. package/src/config-ai.ts +32 -0
  18. package/src/config-defaults.ts +18 -11
  19. package/src/config-fixes.ts +0 -10
  20. package/src/config-health.ts +9 -2
  21. package/src/config-jobs.ts +51 -0
  22. package/src/config-keys.ts +170 -0
  23. package/src/config-mail.ts +73 -0
  24. package/src/config-merge.ts +1 -1
  25. package/src/config-navigation.ts +1 -25
  26. package/src/config-pwa.ts +42 -5
  27. package/src/config-removed.ts +131 -0
  28. package/src/config-shape.ts +0 -33
  29. package/src/config.ts +71 -94
  30. package/src/context.ts +16 -12
  31. package/src/cookie.ts +267 -3
  32. package/src/core-error-codes.ts +2 -0
  33. package/src/cursor-page.ts +41 -0
  34. package/src/cursor.ts +23 -5
  35. package/src/deprecation.ts +77 -0
  36. package/src/dev-secrets.ts +18 -7
  37. package/src/drain-deadline.ts +43 -0
  38. package/src/env-example.ts +9 -29
  39. package/src/errors.ts +18 -9
  40. package/src/exports/error-contract.ts +0 -1
  41. package/src/exports/observability.ts +1 -1
  42. package/src/exports/secrets.ts +3 -0
  43. package/src/finite-option.ts +1 -1
  44. package/src/flight-gate.ts +29 -14
  45. package/src/generation-fence.ts +1 -1
  46. package/src/ids.ts +7 -7
  47. package/src/image/canvas.ts +76 -5
  48. package/src/image/pipeline.ts +17 -5
  49. package/src/image/raster.ts +24 -1
  50. package/src/index.ts +89 -35
  51. package/src/iso-date.ts +1 -1
  52. package/src/lifecycle-errors.ts +1 -1
  53. package/src/lifecycle-readiness.ts +60 -2
  54. package/src/lifecycle-signals.ts +27 -2
  55. package/src/lifecycle-types.ts +96 -0
  56. package/src/lifecycle.ts +44 -133
  57. package/src/locale-direction.ts +1 -1
  58. package/src/logger.ts +16 -7
  59. package/src/mcp-exposure.ts +70 -8
  60. package/src/measurement-actor.ts +16 -1
  61. package/src/metric-errors.ts +32 -0
  62. package/src/metric-registry.ts +151 -0
  63. package/src/metric-series.ts +94 -0
  64. package/src/metrics.ts +9 -255
  65. package/src/page.ts +4 -2
  66. package/src/registrar.ts +1 -0
  67. package/src/retry.ts +25 -5
  68. package/src/secrets-key-file.ts +139 -0
  69. package/src/secrets-store.ts +32 -16
  70. package/src/service.ts +5 -5
  71. package/src/single-flight.ts +1 -1
  72. package/src/telemetry.ts +1 -1
  73. package/src/theme-storage.ts +12 -0
  74. package/src/type-pins.ts +51 -1
  75. package/src/image/fixtures.ts +0 -263
  76. package/src/time-zone-name.ts +0 -14
package/CLAUDE.md CHANGED
@@ -40,11 +40,10 @@ top-level `UltimateError` use in `error-codes.ts`.
40
40
  - **`singleLine` keeps the 3-line contract to three lines, applied in the CONSTRUCTOR** so every
41
41
  door (`format()`, `.message`, `.cause`, `toJSON()`) is covered once. It touches only C0 controls
42
42
  and DEL — a cause keeps its quotes and backslashes. `@ultimat3/schema` carries a deliberate
43
- duplicate, pinned by `single-line-pin.test.ts` HERE, with `ERROR_DOCS_URL` and the brand key.
44
- - **`core → schema` is declared, and the five former copies are gone** (`describeValue`,
45
- `charCount`, `CURRENCY_CODE_PATTERN`, `SCHEMA_ERROR_CODES`, `isIanaZoneName`). Its bundle cost is
46
- measured in `docs/architecture/01-package-map.md`; it depends on `@ultimat3/schema` keeping
47
- `sideEffects: false`.
43
+ duplicate, pinned by `single-line-pin.test.ts` HERE, with `ERROR_DOCS_URL`. The brand symbol is
44
+ NOT a copy: `ULTIMATE_ERROR_BRAND` is schema's one declaration, imported by `errors.ts`.
45
+ - **`core → schema` is declared: import a schema value, never copy it.** Bundle cost:
46
+ `docs/architecture/01-package-map.md`; it depends on `@ultimat3/schema` keeping `sideEffects: false`.
48
47
  - **A string's length is CODE POINTS** — `validators.ts` rejects in that unit and
49
48
  `json-schema.ts` publishes `minLength` in it. `parseId`/`uuidTimestamp` describe a rejected id and
50
49
  never echo it: a value baked into a message has no log field key to redact.
@@ -60,11 +59,11 @@ top-level `UltimateError` use in `error-codes.ts`.
60
59
  | Concept | Owner | Note |
61
60
  |---|---|---|
62
61
  | which deploy this is | `environment.ts` (`ULTIMATE_ENV`) | the twin of `ROLE`; never a second env var |
63
- | one-home helpers | `store-mode` `html-escape` `cookie` `fnv1a` `pg-executor` | never copied (`X_HELPER_COPY`) |
62
+ | one-home helpers | `store-mode` `html-escape` `cookie` `fnv1a` `pg-executor` `deprecation` `aws-sigv4` | never copied (`X_HELPER_COPY`) |
64
63
  | what this process does | `roles.ts` (`ROLE`) | |
65
64
  | how a route renders, caches offline and hydrates | `route-vocabulary.ts` (`RENDER_MODES`, `OFFLINE_STRATEGIES`, `HYDRATE_STRATEGIES`) | every union is `(typeof ARRAY)[number]`, pinned in `type-pins.ts`; `scripts/render-modes.test.ts` refuses a second declaration. Re-export it, never restate it |
66
65
  | which of two route patterns wins a pathname | `route-rank.ts` (`routeRank`) | the request router's order as one integer: segment by segment, literal 3 > `:param` 2 > `*catch-all` 1, ENDED 4, packed base 5 over 22 segments. Read by `@ultimat3/render`'s `compilePattern` and `@ultimat3/pwa`'s rule order — both tier 4, so the one copy lives here. `@ultimat3/http`'s trie encodes the same order by its walk, not by this number. Never a sum: 100/10/1 ranked `/:a/b/c` above `/a/:x/:y` |
67
- | which rungs a cache ladder has | `cache-vocabulary.ts` (`CACHE_TIERS`) | `@ultimat3/cache`'s `TIER_ORDER` IS this array. `isr` is a `RenderMode`, never a tier |
66
+ | which rungs a cache ladder has | `cache-vocabulary.ts` (`CACHE_TIERS`) | `@ultimat3/cache`'s read order, imported (no `TIER_ORDER` alias). `isr` is a `RenderMode`, never a tier |
68
67
  | which build of the APP this is | `app-version.ts` (`APP_VERSION`) | one reader, `dev` by default |
69
68
  | the values | `env.ts` | `checkEnv().values` holds REAL secrets — printing goes through `maskedEnvValues()` |
70
69
  | `.env.example` | `env-example.ts` | a projection of the schema, never hand-maintained |
@@ -75,17 +74,19 @@ top-level `UltimateError` use in `error-codes.ts`.
75
74
  | whether an answer still applies | `generation-fence.ts` | `X_SUPERSEDED` / `isSuperseded` |
76
75
  | which HTTP statuses are worth repeating | `retryable-status.ts` | `>= 500` plus 408, 409, 425, 429 |
77
76
  | how long this request has left | `request-budget.ts` (`Ctx.deadlineAt`, `REQUEST_TIMEOUT_HEADER`) | `@ultimat3/http`'s `startDeadline` is the one writer of the instant; `traceHeaders()` the one writer of the header. A spent budget sends nothing, never `0` |
78
- | the above, composed into one typed-client call | `client-flight.ts` + `client-wire.ts` | shared by `@ultimat3/action` and `@ultimat3/query` (both tier 3), re-exported by both. Declares no code of its own |
79
- | the browser's one HTTP function, records envelope, per-tab handle and principal fence | `client-transport.ts`, `client-dispatch.ts`, `client-problem.ts`, `client-paths.ts`, `record-envelope.ts`, `record-sink.ts`, `client-scope.ts` | `pageClient()` is the ONE `globalThis` write (`Symbol.for('ultimate.client')`); the scope's listeners live ON the handle. `clientTransport` never value-imports `createClientFlight` or `traceHeaders()`: trace/budget headers reach it through the page handle's OUTBOUND SLOT (`outbound-headers.ts`). `isSuperseded` answers both `X_SUPERSEDED` and `X_CLIENT_SCOPE_CHANGED`. The fence never `bump()`s a caller's flight. A write never dedupes; a read dedupes only with a caller's `flight`. `pageClient()` reads `<meta name="ultimate-scope">` (`CLIENT_SCOPE_META`) once: content = principal, empty = anonymous (`null`), absent = UNSCOPED (`undefined`, nothing persisted). `actionPath(name)` with no `style` reads the document's `<meta name="ultimate-path-style">` (`renderedActionPathStyle()`; `CLIENT_PATH_STYLE_META` in `page-meta.ts`) and falls back to `'resource'`. Byte figures (`As of 2026-10-01`, `bun build --target=browser --minify`): `clientTransport` 14,251 B and `pageClient` 8,348 B through the barrel, 9,633 B and 1,156 B through `./page`; the whole `./page` entry 15,979 B against `page-bundle.test.ts`'s 16,384; `rpc`'s live in `packages/action/CLAUDE.md`, `queryClient`'s in `packages/query/CLAUDE.md` |
77
+ | the above, composed into one typed-client call | `client-flight.ts` + `client-wire.ts` | shared by `@ultimat3/action` and `@ultimat3/query` (both tier 3), re-exported by both. Declares no code of its own. A gate slot per ATTEMPT; the wait holds none, ends on `bump()` or the caller's abort |
78
+ | the browser's one HTTP function, records envelope, per-tab handle and principal fence | `client-transport.ts`, `client-dispatch.ts`, `client-problem.ts`, `client-retry-after.ts`, `client-paths.ts`, `record-envelope.ts`, `record-sink.ts`, `client-scope.ts` | `pageClient()` is the ONE `globalThis` write (`Symbol.for('ultimate.client')`); the scope's listeners live ON the handle. `clientTransport` never value-imports `clientFlight` or `traceHeaders()`: trace/budget headers reach it through the page handle's OUTBOUND SLOT (`outbound-headers.ts`). `isSuperseded` answers both `X_SUPERSEDED` and `X_CLIENT_SCOPE_CHANGED`. The fence never `bump()`s a caller's flight. A write never dedupes; a read dedupes only with a caller's `flight`. `pageClient()` reads `<meta name="ultimate-scope">` (`CLIENT_SCOPE_META`) once: content = principal, empty = anonymous (`null`), absent = UNSCOPED (`undefined`, nothing persisted). `actionPath(name)` with no `style` reads the document's `<meta name="ultimate-path-style">` (`renderedActionPathStyle()`; `CLIENT_PATH_STYLE_META` in `page-meta.ts`) and falls back to `'resource'`. A refusal's `Retry-After` and body `title` are read HERE (`client-retry-after.ts`, `remoteTitleOf`). Bytes: `page-bundle.test.ts`; `rpc`'s and `queryClient`'s in `action`'s and `query`'s CLAUDE.md |
80
79
  | a write's public name | `write-digest.ts` (`writeDigest`, `isWriteDigest`, also on `./page`) + `write-origin.ts` (`withWriteOrigin`, `currentWriteOrigin`, `WRITE_ORIGIN_WAL_PREFIX`, server-only) | SHA-256 of an idempotency key, 32 hex; carried action → entity → WAL → realtime `records` frame. A malformed value runs the work unnamed: a label, never a gate |
81
80
  | which row survives a conflict | `conflict-policy.ts` (`ConflictPolicy`, `resolveConflict`, `Row`) | read by `action`'s mutator and `realtime`'s rebase |
81
+ | where a visitor's light/dark choice is stored | `theme-storage.ts` (`THEME_STORAGE_KEY`, also on `./page`) | render's boot script reads it, ui's toggle writes it — both tier 4, so the one literal lives here. Persisted in browsers: never respelled |
82
82
  | the four shapes of an async region | `async-state.ts` (`AsyncState`) | `realtime` returns it, `ui` renders it. `bun run render-modes` refuses a second status union sharing three members |
83
+ | the audit record + one sink | `audit.ts` | `action` + `query`; `AUDIT_RECORD_FIELDS` pins both |
83
84
  | is this `unknown` a keyed record? | `json-object.ts` (`isJsonObject`) | narrows a shape; does not certify provenance |
84
85
  | may a caller read this 5xx `cause`? | `public-cause.ts` (`hasPublicCause`) | one table for http, mcp, ai. `registerPublicCause` is `@ultimat3/http`'s `registerProblemMeta` writing it — never an app's door |
85
86
  | is this field a credential? | `logger.ts` (`isRedactedKey`) | exact keys + `CREDENTIAL_NAME`; a bare `token` suffix is NOT one (`idempotencyToken`, `maxTokens`). Log line, monitor envelope and `action`'s audit ask it |
86
87
  | a value that must not be printed | `secret.ts` | redacted by VALUE; `revealSecret()` is the one, greppable, way out |
87
88
  | an `Intl` formatter cache, and the screen in front of it | `intl-cache.ts` (`cachedFormatter`, `canonicalLocale`, `assertLocale`, `MAX_CACHED_FORMATTERS`, `MAX_LOCALE_EXCERPT`) | a locale arrives from a header: refuse a non-tag (`X_LOCALE_INVALID`), key canonically AND bound the cache — never a copy of any of the three. The cause quotes at most `MAX_LOCALE_EXCERPT` (35) code points; the whole tag rides in `meta.locale` |
88
- | the text direction of a locale | `locale-direction.ts` (`directionOf`, `isRtl`, `Direction`) | re-exported by `@ultimat3/i18n`; lives here so `@ultimat3/ui` need not reach the i18n barrel |
89
+ | the text direction of a locale | `locale-direction.ts` (`directionOf`, `isRtl`, `Direction`) | import from here, never `@ultimat3/i18n`; here so `@ultimat3/ui` skips the i18n barrel |
89
90
  | the committed encrypted values | `secrets.ts` (envelope) + `secrets-store.ts` (files, `installSecrets`) | plaintext is a flat map of ENV NAMES; there is no `secrets.get()` |
90
91
  | ONE sealed value | `seal.ts` (`seal`, `open`, `openText`, `sealAll`) + `seal-keys.ts` (the ring) + `seal-errors.ts` | the framework's one AES call above the envelope: `entity`'s sealed column and `scraping`'s stored session call it, never WebCrypto. `purpose` is required and is AAD |
91
92
 
@@ -109,7 +110,7 @@ top-level `UltimateError` use in `error-codes.ts`.
109
110
  is `x2`, never an edit.
110
111
  - **`schema-error-codes.ts` registers `@ultimat3/schema`'s codes** (schema cannot call core), and
111
112
  derives their retry classification from the same set. It is a `SIDE_EFFECTS_ANCHORS` entry.
112
- - `timing-safe-equal.ts` is the one constant-time comparison (`@ultimat3/auth`, `@ultimat3/storage`).
113
+ - `timing-safe-equal.ts` is the one constant-time comparison (`auth`, `storage`).
113
114
  - **`canonical-json.ts`: `canonicalJson` is INJECTIVE and `fingerprint` is SHA-256/16 of it** — the
114
115
  hash every in-memory sharing key is taken over (`query`'s `queryHash`, `realtime`'s `qid`).
115
116
  `NaN`, `±Infinity` and `-0` are bare tokens; `Date`, `Map` and `Set` are TAGGED. Never a
@@ -121,8 +122,7 @@ top-level `UltimateError` use in `error-codes.ts`.
121
122
  caller that knows the column's kind may ask (`@ultimat3/entity`'s `compareByKind`) — never
122
123
  `@ultimat3/query`, whose `OrderKey` has no kind.
123
124
  - **`format-bytes.ts`: one `formatBytes(bytes)`, 1024-base, `b|kb|mb|gb`** for byte counts in error
124
- messages. Not `@ultimat3/ui`'s locale-formatted decimal one. Not mechanised — review catches a
125
- third copy.
125
+ messages. Not `@ultimat3/ui`'s locale-formatted decimal one. Unmechanised: review catches a third.
126
126
  - **The flight layer** (`backoff.ts`, `retry.ts`, `single-flight.ts`, `flight-gate.ts`,
127
127
  `generation-fence.ts`, `retryable-status.ts`, `client-flight.ts`, `client-wire.ts`) imports
128
128
  nothing but this package, runs nothing at import, and injects every source of non-determinism
@@ -134,7 +134,7 @@ top-level `UltimateError` use in `error-codes.ts`.
134
134
  option.** `finiteCount(subject, option, value, min)` takes `min: 0 | 1` because only the caller
135
135
  knows what zero means. `bun run finite-bounds` recognises a repair by the call's shape, so a
136
136
  package screen carries `Finite` in its name.
137
- - **`createFlightGate` HANDS its slot to a waiter** rather than releasing it. `X_FLIGHT_GATE_OVERLOADED`
137
+ - **`flightGate` HANDS its slot to a waiter** rather than releasing it. `X_FLIGHT_GATE_OVERLOADED`
138
138
  is core's own code (tier 0 cannot borrow http's `X_OVERLOADED`); `overflow:` lets a caller throw its own.
139
139
  - **`client-flight.ts` INVERTS `retryDecision`'s unclassified default** through its `transient:`
140
140
  predicate: a bare `TypeError` (dead network) and an `AbortError` (the caller's cancellation) are
@@ -147,13 +147,11 @@ top-level `UltimateError` use in `error-codes.ts`.
147
147
 
148
148
  ## Metrics, tracing, reporting
149
149
 
150
- `metrics.ts` is to `telemetry.ts` what a counter is to a span: always on, no-op exporter by
151
- default. `runtime-metrics.ts` is the only place that names a series the chart reads
152
- (`http_requests_total`, `connections`, `queue_depth`), keyed by `ScalingSignal` in
153
- `SCALING_METRICS`. `process-metrics.ts` names what the PROCESS costs (`process_*`: resident
154
- memory, heap, external, CPU seconds, event-loop lag, start time, `process_info{role}`); server-only
155
- — it reads `process`, so it is never exported from `page.ts`. One call site per package; a second
156
- is the bug:
150
+ `metrics.ts`: counters (spans are `telemetry.ts`), always on, no-op exporter by default.
151
+ `runtime-metrics.ts` alone names a series the chart reads (`http_requests_total`, `connections`,
152
+ `queue_depth`), keyed by `ScalingSignal` in `SCALING_METRICS`. `process-metrics.ts`: `process_*`
153
+ (resident memory, heap, external, CPU seconds, event-loop lag, start time, `process_info{role}`);
154
+ server-only, never on `page.ts`. One call site per recorder; a second is the bug:
157
155
 
158
156
  | Recorder | The one caller |
159
157
  |---|---|
@@ -218,8 +216,8 @@ bun test packages/core/src # from the REPO ROOT, never from packages/core
218
216
  bun run typecheck
219
217
  ```
220
218
 
221
- The root is not a preference: `bunfig.toml`'s preload installs `@ultimat3/testing`'s matchers, and
222
- Bun reads `bunfig.toml` from the cwd (`scripts/coverage-gate.ts` runs from the root for the same reason).
219
+ The root is load-bearing: Bun reads `bunfig.toml` (its preload installs `@ultimat3/testing`'s
220
+ matchers) from the cwd.
223
221
 
224
222
  Gotchas:
225
223
  - `exactOptionalPropertyTypes` is on — declare optional fields as `x?: T | undefined`.
@@ -227,12 +225,11 @@ Gotchas:
227
225
  - `Ctx` carries a string index signature so apps can augment `CtxServices`; the cost is that
228
226
  `ctx.anything` type-checks as `unknown`. Deleting it is a breaking change, measured to compile
229
227
  core clean; land it alone, with a full `bun run verify`.
230
- - **`Ctx extends CtxFacts, CtxServices`, and `createContext` holds the framework's ONE irreducible
228
+ - **`Ctx extends CtxFacts, CtxServices`, and `ctxOf` holds the framework's ONE irreducible
231
229
  `as Ctx`.** `CtxFacts` is what the framework sets (and what a `ServiceFactory` receives); an
232
230
  augmentation's named members are required of every `Ctx`, and no framework function can obtain
233
- them. `@ultimat3/http`'s `createRequestContext` composes `createContext()` and has no assertion.
234
- Four alternatives were measured and refused (listed in the file header); the structural repair is
235
- a major, alongside the index-signature deletion.
231
+ them. `@ultimat3/http`'s `requestContext` composes `ctxOf()` and has no assertion.
232
+ Refused alternatives: the file header. The repair is a major, with the index-signature deletion.
236
233
  - Tests that touch the registry, the lifecycle or the listener table call `resetErrorCodes()` /
237
234
  `resetLifecycle()` / `resetListeners()` — and a registry reset takes `errorCodeSnapshot()` first
238
235
  and restores it in `afterAll`, or every earlier package's titles render humanised for the run.
package/README.md CHANGED
@@ -28,6 +28,7 @@ Zero dependencies, zero `@ultimat3/*` imports.
28
28
  | which principal the page acts for, and the epoch that moves when it changes | `client-scope.ts` |
29
29
  | which row survives a conflict — `server-wins`, `last-write-wins`, `custom` | `conflict-policy.ts` |
30
30
  | the four shapes an async region can be in | `async-state.ts` |
31
+ | the audit seam `action` and `query` share — `AuditRecord` (`name`, `primitive`, `surface`, `outcome`, the parsed `input`; `action` is a deprecated alias of `name` until 25.0.0), `AuditSink`, the ONE installed sink (`setAuditSink` / `getAuditSink` / `resetAuditSink`, re-exported by `@ultimat3/action`), and `AUDIT_RECORD_FIELDS`, the field list both primitives' tests pin their records to | `audit.ts` |
31
32
  | is this `unknown` a keyed record? | `json-object.ts` |
32
33
  | typed env validated at boot | `env.ts` |
33
34
  | `.env.example` rendered from that schema, and its drift check | `env-example.ts` |
@@ -37,13 +38,16 @@ Zero dependencies, zero `@ultimat3/*` imports.
37
38
  | a value that cannot be printed by accident | `secret.ts` |
38
39
  | the committed encrypted secrets envelope, AES-256-GCM | `secrets.ts` |
39
40
  | the two secrets files, and decrypted values → `defineEnv` | `secrets-store.ts` |
41
+ | a key file only its owner can read — 0600, and an `icacls` ACL on Windows — written via temp + rename, the rename retried on EPERM/EBUSY (`writeMasterKeyFile`, `stageMasterKeyFile`, `promoteStagedMasterKey`) | `secrets-key-file.ts` |
40
42
  | one value sealed under the master key — `seal()` / `open()` | `seal.ts` |
41
43
  | the key ring those work under: the current key plus retired ones | `seal-keys.ts` |
42
44
  | `defineConfig()` for `app.config.ts` | `config.ts` |
43
45
  | how overlays layer onto it — per section, key by key | `config-merge.ts` |
44
46
  | what each key is when no layer says | `config-defaults.ts` |
45
- | the shape screens that run before any rule reads a value — section, list, boolean, closed set, path, locale list | `config-shape.ts` |
47
+ | the shape screens that run before any rule reads a value — section, list, boolean, closed set, path | `config-shape.ts` |
48
+ | the keys a major deleted (`locales`, `defaultLocale`, `defaultTimeZone`, `defaultCurrency`, `theme.tokens` in 25.0.0; `jobs.driver`, deleted in 5.0.0 and refused since 25.0.0) — each REFUSED by name with `X_CONFIG_INVALID` and its replacement, never ignored | `config-removed.ts` |
46
49
  | the `pwa` block — what an install needs, and the boot refusal when it is not there | `config-pwa.ts` |
50
+ | `isSameOriginPath(value)` — a path on THIS origin as a browser resolves it: refuses `//host`, `/\host`, a C0 control or DEL (`/\t/evil.example` parses to `//evil.example`) and a dot segment leaving a `//` pathname. The one predicate for every URL precached as an offline answer, `@ultimat3/pwa`'s build included | `config-pwa.ts` |
47
51
  | the closed route vocabulary every renderer names | `route-vocabulary.ts` |
48
52
  | which of two route patterns wins a pathname (`routeRank`) | `route-rank.ts` |
49
53
  | runtime roles + `ROLE` resolution | `roles.ts` |
@@ -65,19 +69,26 @@ Zero dependencies, zero `@ultimat3/*` imports.
65
69
  | graceful drain, `/healthz`, `/readyz` | `lifecycle.ts` |
66
70
  | what a health endpoint tells whom — `healthBody(report, role, detailed)`, `healthPeerListed(peers, address)`, `DEFAULT_HEALTH_DETAIL_PEERS`; the one rule `@ultimat3/http` and the sync node's own listener both call | `health-disclosure.ts` |
67
71
  | the readiness grace between `/readyz` → 503 and the listener closing (`drain.readinessGraceMs`) | `lifecycle-grace.ts` |
68
- | SIGTERM/SIGINT → the one drain | `lifecycle-signals.ts` |
72
+ | the drain budget's default and domain (`drain.deadlineMs`, 25 s, 1–3600000 ms) — `DRAIN_DEADLINE_DEFAULT_MS`, `DRAIN_DEADLINE_MAX_MS` | `drain-deadline.ts` |
73
+ | `jobs.concurrency`'s default and domain — one slot count for every queue a worker serves, or a table per queue (`{ banks: 4, 'banks-long': 2 }`; a queue the table does not name runs at the default). `JOBS_CONCURRENCY_DEFAULT` (8), `type JobsConcurrency` | `config-jobs.ts` |
74
+ | SIGTERM/SIGINT → the one drain; on Windows also SIGHUP (console close) and SIGBREAK (Ctrl-Break) — `drainSignals(platform)` | `lifecycle-signals.ts` |
75
+ | is this directory inside a `bun build --compile` binary? `isCompiledBundle(import.meta.dir)` — `/$bunfs/` and Windows' `B:\~BUN\` | `bunfs.ts` |
69
76
  | which network an IP literal belongs to — `classifyAddress`, for SSRF screens | `address-class.ts` |
77
+ | the network a caller's address keys a per-caller budget on — `addressNetwork`: IPv4 exact, IPv4-mapped as IPv4, IPv6 as its /64 | `address-class.ts` |
70
78
  | the sockets this process opened, so a self-request is not egress | `listeners.ts` |
71
79
  | `defineService('orgs', …)` → `ctx.orgs`, rebuilt per actor | `service.ts` |
72
80
  | the registrar table one same-tier package reaches another through | `registrar.ts` |
73
81
  | decode → resize → encode, the one image pipeline (over `Bun.Image`) | `image/` |
74
- | `assertNever`, `invariant` | `assert.ts` |
82
+ | `assertNever`, `assertCoded`, `assert` | `assert.ts` |
75
83
  | the one HTML character table — `escapeHtml`, text and attributes alike (`& < > " '`) | `html-escape.ts` |
76
84
  | the one `Cookie:` reader — `readCookie(header, name)`, `null` when absent, never a throw | `cookie.ts` |
85
+ | the one `Set-Cookie` writer — `serializeSetCookie(name, value, options?)`; defaults `Path=/; HttpOnly; Secure; SameSite=Lax`, the value percent-encoded so `readCookie` returns it exactly, `X_COOKIE_INVALID` for a non-token name, a pair over 4096 octets, `SameSite=None`/`Partitioned` without `Secure`, a broken `__Secure-`/`__Host-` prefix rule, or an injectable `Path`/`Domain`. `Expires` is an IMF-fixdate in UTC | `cookie.ts` |
86
+ | the one AWS Signature V4 signer — `signAwsRequest({ method, url, headers?, payload?, credentials, region, service, clock? })` → the URL, every header to send (`authorization`, `x-amz-date`, `x-amz-security-token`, `x-amz-content-sha256` by default on `s3`), the canonical request and string-to-sign. `payload`: `{ body }`, a pre-taken `{ sha256Hex }`, or `UNSIGNED_PAYLOAD`. S3 encodes the path once, every other service twice. Web Crypto, no SDK; proven on the aws-c-auth SigV4 suite and S3's worked examples. Shared by `storage`'s s3 disk and `mail`'s SES driver | `aws-sigv4.ts` |
77
87
  | 32-bit FNV-1a — a BUCKET (rollouts, factory seeds), never a sharing key (`fingerprint` is) | `fnv1a.ts` |
78
88
  | `PgExecutor` — the structural `query(text, values)` seam every Postgres store takes | `pg-executor.ts` |
89
+ | a declared retirement as headers — `renderDeprecation` (RFC 9745 `Deprecation`, RFC 8594 `Sunset`, the `successor-version` link) and `recordDeprecatedCall` on the one `deprecated_calls_total`; `action` and `query` both project through it | `deprecation.ts` |
79
90
 
80
- Each of the four, and `fingerprint`, `storeMode` and render's `contentHash`, has ONE implementation:
91
+ Each of the five, and `fingerprint`, `storeMode` and render's `contentHash`, has ONE implementation:
81
92
  `bun run flight-copies` refuses a second by its shape (`X_HELPER_COPY`), whatever it is named.
82
93
 
83
94
  ## Errors are instructions
@@ -192,9 +203,9 @@ a job boundary the class is gone and the `code` is what survives — match on th
192
203
  | Class | Code | Declared in |
193
204
  |---|---|---|
194
205
  | `ConfigInvalidError` | `X_CONFIG_INVALID` | `src/errors.ts` |
206
+ | `CookieInvalidError` | `X_COOKIE_INVALID` | `src/cookie.ts` |
195
207
  | `CursorInvalidError` | `X_CURSOR_INVALID` | `src/cursor.ts` |
196
208
  | `CursorSecretDevError` | `X_CURSOR_SECRET_DEV` | `src/dev-secrets.ts` |
197
- | `EnvExampleDriftError` | `X_ENV_EXAMPLE_DRIFT` | `src/env-example.ts` |
198
209
  | `EnvironmentInvalidError` | `X_ENVIRONMENT_INVALID` | `src/environment.ts` |
199
210
  | `EnvMissingError` | `X_ENV_MISSING` | `src/errors.ts` |
200
211
  | `ErrorReporterDsnInvalidError` | `X_ERROR_REPORTER_DSN_INVALID` | `src/error-reporter-sentry.ts` |
@@ -214,6 +225,7 @@ a job boundary the class is gone and the `code` is what survives — match on th
214
225
  | `SealKeyUnknownError` | `X_SEAL_KEY_UNKNOWN` | `src/seal-errors.ts` |
215
226
  | `SecretsFileInvalidError` | `X_SECRETS_FILE_INVALID` | `src/secrets-errors.ts` |
216
227
  | `SecretsFileMissingError` | `X_SECRETS_FILE_MISSING` | `src/secrets-errors.ts` |
228
+ | `SecretsKeyAclError` | `X_SECRETS_KEY_ACL_FAILED` — Windows only: `icacls` could not restrict a new key file, so none was written | `src/secrets-key-file.ts` |
217
229
  | `SecretsKeyInvalidError` | `X_SECRETS_KEY_INVALID` | `src/secrets-errors.ts` |
218
230
  | `SecretsRingKeyInvalidError` | `X_SECRETS_KEY_INVALID` — a malformed entry of `ULTIMATE_SECRETS_RETIRED_KEYS` | `src/secrets-errors.ts` |
219
231
  | `SecretsKeyMismatchError` | `X_SECRETS_KEY_MISMATCH` | `src/secrets-errors.ts` |
@@ -225,7 +237,7 @@ a job boundary the class is gone and the `code` is what survives — match on th
225
237
  ## Context
226
238
 
227
239
  ```ts
228
- const ctx = createContext({ actor: agentActor({ id: 'mcp-1', scopes: ['post:publish'] }) });
240
+ const ctx = ctxOf({ actor: agentActor({ id: 'mcp-1', scopes: ['post:publish'] }) });
229
241
  await runWithContext(ctx, async () => {
230
242
  const { actor, locale, tz, logger } = useContext(); // throws X_NO_CONTEXT outside
231
243
  await withChildContext({ locale: 'es' }, () => render());
@@ -237,13 +249,13 @@ automatically; so does the root `logger` while a context is active. Add typed se
237
249
  augmenting `CtxServices`; reach late-bound ones with `useService<T>('mail')`.
238
250
 
239
251
  A service that reads the actor (`ctx.posts`, scoped to `ctx.actor.orgId`) registers once with
240
- `defineService('posts', (ctx) => ({ ... }))`, at import time. `createContext` and
252
+ `defineService('posts', (ctx) => ({ ... }))`, at import time. `ctxOf` and
241
253
  `withChildContext` then build it fresh, bound to whichever actor they are constructing a ctx
242
254
  for — importing the module that calls `defineService` is the registration, the same convention
243
- `registerActions` uses. Passing `services: { posts: ... }` to `createContext` still works and
255
+ `registerActions` uses. Passing `services: { posts: ... }` to `ctxOf` still works and
244
256
  wins over a registered factory of the same name, for a test that wants to hand in a mock.
245
257
 
246
- A factory runs again on **every** `createContext` / `withChildContext` call and is never cached,
258
+ A factory runs again on **every** `ctxOf` / `withChildContext` call and is never cached,
247
259
  because it closes over the ctx (actor, clock, tz) it was built for. `withChildContext` drops a
248
260
  factory-managed name from what it carries forward on purpose: only an ad hoc service nobody
249
261
  registered survives an actor swap unrebuilt.
@@ -296,9 +308,11 @@ safe for `x.manifest.json`. Omit `required` for required — `required: false` i
296
308
  loosening. Never declare an env var for *which deploy this is* — that is `ULTIMATE_ENV`, below.
297
309
 
298
310
  `.env.example` is a **projection** of that schema, never a second list:
299
- `renderEnvExample(schema)` writes it, `assertEnvExample(schema, text)` fails with
300
- `X_ENV_EXAMPLE_DRIFT` when a declared key has no line — the failure that otherwise arrives as
301
- somebody else's `X_ENV_MISSING` on a variable nobody documented.
311
+ `renderEnvExample(schema)` writes it and `checkEnvExample(schema, text)` reports `missing` /
312
+ `extra` as data. The gate is `x verify`'s `manifest` step (`X_ENV_EXAMPLE_DRIFT`, fixed by
313
+ `x env example`) — the failure that otherwise arrives as somebody else's `X_ENV_MISSING` on a
314
+ variable nobody documented. `assertEnvExample` (key presence only, called by nothing) was deleted
315
+ in 25.0.0.
302
316
 
303
317
  Loading `.env` is **Bun's**, not ours. `envFileCandidates()` states what it does, measured:
304
318
  `.env` → `.env.<mode>` → `.env.local`, with `.env.local` skipped under test, and the mode being
@@ -344,12 +358,15 @@ name it was never told about: `password` / `passphrase` anywhere, `secret` as th
344
358
  `…token` that is not a dedupe or paging key (`resetToken`, `githubToken`, `NPM_TOKEN`), key
345
359
  material by its qualifier (`signingKey`, `masterKey`, `accessKeyId`), a value that embeds a
346
360
  credential (`connectionString`, `dsn`, `databaseUrl`), the one-time codes
347
- (`totpCode`, `recoveryCode`) and a stored hash of any of them (`passwordHash`, `tokenHash`,
348
- `keyHash`). It deliberately leaves `idempotencyToken`, a paging token, `maxTokens` and an error
349
- `code` readable — a redacted field is one an operator cannot correlate on.
361
+ (`totpCode`, `recoveryCode`), session and bearer material (`credentials`, `jwt`, `bearer`,
362
+ `cookie`, `sessionId`, `sessionKey`, `privateKeyPem`), card data (`cvv`, `cvc`, a whole-word `pin`
363
+ such as `cardPin` or `pinCode`) and a stored hash of any of them (`passwordHash`, `tokenHash`,
364
+ `keyHash`). `@ultimat3/action` also asks it before storing an idempotent answer. It deliberately
365
+ leaves `idempotencyToken`, a paging token, `maxTokens`, an error `code`, `spinner`, `isPinned`,
366
+ `sessionStart` and `cookieName` readable — a redacted field is one an operator cannot correlate on.
350
367
 
351
368
  `LOG_LEVEL` is refused when it is not one of `LOG_LEVELS` (lowercase), exactly as
352
- `createLogger({ level })` refuses it; unset or empty is `info`.
369
+ `structuredLogger({ level })` refuses it; unset or empty is `info`.
353
370
 
354
371
  A `Secret`
355
372
  box catches the other case: `String()`, template literals, `+`, `JSON.stringify`, `console.log`,
@@ -441,11 +458,11 @@ match with `sealAll()`; uniqueness cannot be held across keys.
441
458
  ## Time, ids, telemetry, drain
442
459
 
443
460
  - Never call `Date.now()`. Take a `Clock`; tests pass `frozenClock('2026-07-26T10:00:00Z')`.
444
- - `uuid()` is UUIDv7: time-prefixed, monotonic within a millisecond, never backwards on clock
461
+ - `uuidV7()` is UUIDv7: time-prefixed, monotonic within a millisecond, never backwards on clock
445
462
  skew. `typedId<'post'>()` brands it so a post id cannot be passed where a user id is wanted.
446
463
  - `withSpan('action.publishPost', fn)` is free until `configureTelemetry({ exporter })`.
447
464
  Traces cross process boundaries via `traceparent()` / `parseTraceparent()`, whose ids come from
448
- `traceId()` / `spanId()` — **never `uuid()`**, whose dashed 36 characters every collector
465
+ `traceId()` / `spanId()` — **never `uuidV7()`**, whose dashed 36 characters every collector
449
466
  rejects. `isTraceId()` / `isSpanId()` are the one definition of the valid shape.
450
467
  - **Sampling is honoured, not just propagated.** `startSpan` takes the parent's bit when there is
451
468
  one, else asks the `Sampler`; `span.end()` exports nothing when the bit is 0. The default reads
@@ -588,9 +605,25 @@ never a silently wrong page.
588
605
  | Secret | `configureCursorSigning()` at boot, else `ULTIMATE_CURSOR_SECRET`. An EMPTY value is unset — never an empty HMAC key — so `usesDevCursorSecret()` reports it and the boot refuses it outside a local environment. **Read when a cursor is signed, never at import** — an app whose `openSecrets()` sets the variable during boot would otherwise sign every cursor with the dev key. Rotating it invalidates every open cursor |
589
606
  | Also keys | `keyedFingerprint(value, purpose)` — `h1:<key id>:<HMAC>` over `canonicalJson`, under a per-purpose key derived from this secret; the fingerprint to PERSIST (`@ultimat3/action`'s idempotency `requestHash`). `compareFingerprint` answers `match` / `mismatch` / `unverifiable` (other key), and still checks a legacy bare `fingerprint()` exactly. Rotating the secret makes in-window stored fingerprints `unverifiable` |
590
607
  | Signed, not encrypted | the client already has these rows; what it must not do is *invent* a position |
591
- | `usesDevCursorSecret()` | true while the shipped dev key is in use |
608
+ | `usesDevCursorSecret({ env? })` | true while the shipped dev key is in use — this process, or the table `env` as signing would read it (empty and the published key count as unset) |
609
+ | `CURSOR_SECRET_KEY` / `CURSOR_SECRET_FIX` | `'ULTIMATE_CURSOR_SECRET'` and the one `fix:` for `X_CURSOR_SECRET_DEV` (`export ULTIMATE_CURSOR_SECRET="$(openssl rand -hex 32)"`) — the boot refusal and `@ultimat3/cli`'s deploy contract (`.env.example`, `x env check`, `x doctor`) read both |
610
+ | `devSecretsRefused({ env? })` | THE rule for refusing a shipped dev secret: anything but `development`/`test`, and no named environment counts as production. The boot (`assertNoDevSecretsOutsideLocal`), `@ultimat3/storage`'s disk and the CLI's diagnostics all ask it |
592
611
  | `resetCursorSigning()` | test seam: forget `configureCursorSigning` and fall back to the environment |
593
612
 
613
+ ### One page shape
614
+
615
+ `Page<Row>` (`src/cursor-page.ts`) is what every cursor page answers — an entity `findMany`, a
616
+ `query`'s `.page()`, the `?_first=` HTTP envelope and the typed client: `{ rows, nextCursor,
617
+ hasMore }`, with ONE meaning. `nextCursor` is the cursor for the next page and `null` exactly when
618
+ `hasMore` is false, so `while (page.hasMore)` and `while (page.nextCursor !== null)` are the same
619
+ loop and both stop on the last page without fetching an empty one.
620
+
621
+ | | |
622
+ |---|---|
623
+ | The type | a union of `{ nextCursor: string; hasMore: true }` and `{ nextCursor: null; hasMore: false }` — a literal that disagrees is a build error (`type-pins.ts`), and `if (page.hasMore)` narrows `nextCursor` to `string` |
624
+ | `pageOf(rows, nextCursor)` | the only constructor: `hasMore` is derived from the cursor, never passed beside it. `null` (or `''`) is the last page |
625
+ | Re-exported | by `@ultimat3/query` as its `Page`; `@ultimat3/entity` exports none — import it from here |
626
+
594
627
  ## One bounded cache for every `Intl` formatter
595
628
 
596
629
  ```ts
@@ -638,16 +671,16 @@ prose has none.
638
671
  ```ts
639
672
  import {
640
673
  backoffDelay,
641
- createFence,
642
- createFlightGate,
643
- createSingleFlight,
674
+ generationFence,
675
+ flightGate,
676
+ singleFlight,
644
677
  isRetryableStatus,
645
678
  } from '@ultimat3/core';
646
679
 
647
680
  // One ceiling on work whose cost is memory, one run per key, one fence over the answer.
648
- const gate = createFlightGate({ maxConcurrent: 8, maxQueued: 64 }, { subject: 'jwks fetches' });
649
- const flights = createSingleFlight({ deadlineMs: 30_000 });
650
- const fence = createFence('the jwks cache');
681
+ const gate = flightGate({ maxConcurrent: 8, maxQueued: 64 }, { subject: 'jwks fetches' });
682
+ const flights = singleFlight({ deadlineMs: 30_000 });
683
+ const fence = generationFence('the jwks cache');
651
684
 
652
685
  export async function jwks(url: string): Promise<Response> {
653
686
  const issued = fence.generation();
@@ -679,14 +712,18 @@ nothing consulted it before deciding to try again. `As of 2026-08-23`.
679
712
  | Export | The one answer | The question it settles |
680
713
  |---|---|---|
681
714
  | `backoffDelay({ attempt, base, max, factor?, curve?, jitter?, random? })` | one curve — `exponential \| linear \| fixed`, `full \| equal \| none` — 1-based `attempt`, clamped to `max` **before** jitter, rounded, and `0` rather than `NaN` | how long to wait. `random` is injectable, so a schedule is a unit test rather than a range |
682
- | `createSingleFlight({ deadlineMs?, schedule? })` → `run(key, work, join?)`, `size` | N callers on one key are ONE run | who pays for a miss. Eviction is identity-checked, so a load that settles late never drops the load that replaced it; `deadlineMs` frees the KEY a wedged load would hold forever — it never cancels the work and never rejects a joiner |
683
- | `createFlightGate({ maxConcurrent, maxQueued }, { subject?, overflow? })` | one bound, one queue, one refusal | how many at once. Past the queue the answer is `X_FLIGHT_GATE_OVERLOADED` (503) and never a longer queue; the slot is HANDED to a waiter, never released and re-acquired. Both limits are screened at construction (`finiteCount`, 0 allowed); a width of 0 refuses every caller rather than queueing for a slot that never frees |
684
- | `createFence(subject)` → `generation()`, `bump()`, `guard(issued)` | whether an answer still applies | `X_SUPERSEDED` (499) and `isSuperseded(error)` — the piece nothing in the tree had. `guard` compares `!==`, never `<` |
715
+ | `singleFlight({ deadlineMs?, schedule? })` → `run(key, work, join?)`, `size` | N callers on one key are ONE run | who pays for a miss. Eviction is identity-checked, so a load that settles late never drops the load that replaced it; `deadlineMs` frees the KEY a wedged load would hold forever — it never cancels the work and never rejects a joiner |
716
+ | `flightGate({ maxConcurrent, maxQueued }, { subject?, overflow? })` | one bound, one queue, one refusal | how many at once. Past the queue the answer is `X_FLIGHT_GATE_OVERLOADED` (503) and never a longer queue; the slot is HANDED to a waiter, never released and re-acquired. Both limits are screened at construction (`finiteCount`, 0 allowed); a width of 0 refuses every caller rather than queueing for a slot that never frees. `run(work, signal?)`: an abort while QUEUED removes the waiter and rejects with the abort's reason |
717
+ | `generationFence(subject)` → `generation()`, `bump()`, `guard(issued)` | whether an answer still applies | `X_SUPERSEDED` (499) and `isSuperseded(error)` — the piece nothing in the tree had. `guard` compares `!==`, never `<` |
685
718
  | `isRetryableStatus(status)`, `RETRYABLE_STATUSES` | `>= 500`, plus 408, 409, 425, 429 | which HTTP answers are worth repeating |
686
- | `retry(work, policy, { sleep, now?, random? })`, `retryDecision(policy, attempt, error, random?)` | the executor and the pure decision behind the classification | whether to try again at all. `createClientFlight` is its one caller in the framework; `jobs`, `ai` and `db` each keep their own loop and delegate only the arithmetic and the classification |
687
- | `createClientFlight({ principal?, retry?, deadlineMs?, limit?, … })` → `run(plan)`, `keyFor(url, opts?)`, `bump()`, `generation()` | the five above composed into one typed-client call | dedup, supersession, retry, one wall-clock deadline and a concurrency ceiling, for a call whose dispatch the caller supplies. `@ultimat3/action` and `@ultimat3/query` re-export it verbatim — it is one file because both are tier 3 and neither may import the other |
719
+ | `jitterStatedDelay(waitMs, capMs, random?)` | the ONE rule for a delay a responder named | the stated wait as a FLOOR plus a spread in `[0, min(wait / 2, cap))`, so a burst told `Retry-After: 1` does not replay in lockstep. `retryDecision`'s `retry-after` path (floor clamped to `max`; `jitter: 'none'` gets the bare floor) and `@ultimat3/jobs`' rate-limit deferral and webhook throttle all take it |
720
+ | `retry(work, policy, { sleep, now?, random? })`, `retryDecision(policy, attempt, error, random?)` | the executor and the pure decision behind the classification | whether to try again at all. `clientFlight` is its one caller in the framework; `jobs`, `ai` and `db` each keep their own loop and delegate only the arithmetic and the classification |
721
+ | `clientFlight({ principal?, retry?, deadlineMs?, limit?, … })` → `run(plan)`, `keyFor(url, opts?)`, `bump()`, `generation()` | the five above composed into one typed-client call | dedup, supersession, retry, one wall-clock deadline and a concurrency ceiling, for a call whose dispatch the caller supplies. `@ultimat3/action` and `@ultimat3/query` import it from here and re-export only its types — one file, because both are tier 3 and neither may import the other |
688
722
  | `isTransientFailure(error)` | what a CLIENT may send again | a declared `retryable`/`retry-after`, plus a dispatch that produced no response at all. It **inverts** `retryDecision`'s unclassified default on purpose: a caller's own `AbortError` and a foreign `TypeError` are terminal |
689
- | `traceHeaders()`, `problemOf(text)`, `retryForStatus(code, status)`, `FRAMEWORK_CODE` | what a typed client puts on the wire and reads back off it | the W3C header (nothing at all when the span context is incomplete), a total `problem+json` read, and the classification a STATUS is allowed to give when nobody declared one for the code |
723
+ | `traceHeaders()`, `problemOf(text)`, `retryForStatus(code, status, retryAfterSeconds?)`, `FRAMEWORK_CODE` | what a typed client puts on the wire and reads back off it | the W3C header (nothing at all when the span context is incomplete), a total `problem+json` read, and the classification a STATUS is allowed to give when nobody declared one for the code — `retry-after` for a 429 or 503 that stated a delay, unless the code is declared `terminal` |
724
+ | `retryAfterSecondsOf(header, date)`, `MAX_RETRY_AFTER_SECONDS` | the one `Retry-After` reader | delta-seconds, or an IMF-fixdate measured against the same response's `Date` header (never the client's clock), capped at a day; `0`, a date already past and anything else `undefined` — only a positive delay is a statement (`retryAfterOf`'s outbound rule), so unstated falls back to the jittered curve. `clientTransport` passes it to `problemError` and to a caller's `decodeError(status, text, retryAfterSeconds)`, which carry it as `meta.retryAfterSeconds` for `statedDelayMs` |
725
+ | `withStatedDelay(meta, retryAfterSeconds)` | a remote error's `meta`, for both decoders (`problemError`, `@ultimat3/action`'s `RemoteActionError`) | the copied wire keys minus any `retryAfterSeconds` — the server never writes one in a body, framework meta being operator-only — plus the header's value when there was one. A body can never drive the wait |
726
+ | `remoteTitleOf(value)`, `MAX_REMOTE_TITLE_LENGTH` | a problem document's `title`, as untrusted display text | a non-blank string cut at 120 characters, or nothing. Handed to `UltimateError` as `remoteTitle`, used only when this realm registered no title for the code |
690
727
 
691
728
  `retry`'s `policy.jitter` is **required** where `backoffDelay`'s defaults to `none`: a loop retrying
692
729
  without jitter IS the thundering herd, so the mode is a decision each caller makes rather than one
@@ -709,7 +746,7 @@ read; `@ultimat3/jobs` re-exports both rather than keeping a second pair.
709
746
  | `resolveConflict(policy, local, server, { clockField? })`, `ConflictPolicy`, `Row` | one conflict vocabulary, over ROWS | `last-write-wins` keeps the local row only when its numeric clock field (default `updatedAt`) is newer; no provable clock = the server's row |
710
747
  | `AsyncState<T>` | `pending \| refreshing \| ready \| failed` | the type `realtime` produces and `ui` renders; tier 0 because neither may import the other |
711
748
 
712
- `clientTransport` does not value-import `createClientFlight` or `traceHeaders()` — measured sizes
749
+ `clientTransport` does not value-import `clientFlight` or `traceHeaders()` — measured sizes
713
750
  are in its file header. A server-side caller that propagates a trace passes `traceHeaders()` in
714
751
  `headers` itself.
715
752
 
@@ -763,9 +800,9 @@ is read, mapping it onto `X_IMAGE_UNSUPPORTED` / `X_IMAGE_TOO_LARGE` / `X_IMAGE_
763
800
  caller branches on a Bun code.
764
801
 
765
802
  Two files in `image/` are past the 200-line target and neither splits without inventing a seam:
766
- `probe.ts` is one algorithm per format over header bytes, and `fixtures.ts` is data. The 500-line
803
+ `probe.ts` is one algorithm per format over header bytes, and `image-fixture.ts` is data. The 500-line
767
804
  hard ceiling applies to both. Everything else in `image/` is under the target — deleting the
768
805
  hand-rolled JPEG and PNG codecs is what put it there.
769
806
 
770
- `image/fixtures.ts` is byte-exact output from Pillow and ffmpeg on purpose: a codec that only round
807
+ `image/image-fixture.ts` is byte-exact output from Pillow and ffmpeg on purpose: a codec that only round
771
808
  trips against itself proves nothing. Never regenerate a fixture with our own encoder.
package/package.json CHANGED
@@ -1,15 +1,12 @@
1
1
  {
2
2
  "name": "@ultimat3/core",
3
- "version": "24.0.0",
3
+ "version": "25.0.0",
4
4
  "description": "Ultimate's foundation: errors, context, env, config, clock, ids, logging, telemetry, lifecycle",
5
5
  "license": "MIT",
6
6
  "type": "module",
7
7
  "sideEffects": [
8
- "./src/context.ts",
9
8
  "./src/core-error-codes.ts",
10
- "./src/lifecycle-errors.ts",
11
- "./src/schema-error-codes.ts",
12
- "./src/secrets-errors.ts"
9
+ "./src/schema-error-codes.ts"
13
10
  ],
14
11
  "repository": {
15
12
  "type": "git",
@@ -33,13 +30,13 @@
33
30
  "LICENSE"
34
31
  ],
35
32
  "engines": {
36
- "bun": ">=1.4.0"
33
+ "bun": ">=1.4.2"
37
34
  },
38
35
  "scripts": {
39
36
  "typecheck": "tsc --noEmit -p tsconfig.json",
40
37
  "test": "bun test"
41
38
  },
42
39
  "dependencies": {
43
- "@ultimat3/schema": "24.0.0"
40
+ "@ultimat3/schema": "25.0.0"
44
41
  }
45
42
  }
package/src/actor.ts CHANGED
@@ -185,6 +185,15 @@ export function isAnonymous(actor: Actor): boolean {
185
185
  return actor.kind === 'anonymous';
186
186
  }
187
187
 
188
+ /**
189
+ * The context's actor as a policy reads it: core models "nobody" as the anonymous actor, policy as
190
+ * `null` — the value that turns a missing session into `X_UNAUTHENTICATED` instead of a bare
191
+ * denial. Declared here, once, so action, query, MCP and a page all map it the same way.
192
+ */
193
+ export function actorOf(ctx: { readonly actor: Actor }): Actor | null {
194
+ return isAnonymous(ctx.actor) ? null : ctx.actor;
195
+ }
196
+
188
197
  export function hasRole(actor: Actor, role: string): boolean {
189
198
  return actor.roles.includes(role);
190
199
  }
@@ -119,16 +119,22 @@ function classifyV6(g: readonly number[]): AddressClass {
119
119
  return 'public';
120
120
  }
121
121
 
122
+ /** The literal itself: trimmed, unbracketed, its IPv6 zone id dropped. */
123
+ function literalOf(address: string): string {
124
+ let text = address.trim();
125
+ if (text.startsWith('[') && text.endsWith(']')) text = text.slice(1, -1);
126
+ const zone = text.indexOf('%');
127
+ if (zone !== -1 && text.includes(':')) text = text.slice(0, zone);
128
+ return text;
129
+ }
130
+
122
131
  /**
123
132
  * The class of an IP address LITERAL — IPv4, IPv6, bracketed `[::1]`, a zone id `fe80::1%eth0`,
124
133
  * and every IPv6 form carrying an IPv4 address. `undefined` means "not an address literal": a
125
134
  * hostname must be resolved first and each resolved address classified, never this string.
126
135
  */
127
136
  export function classifyAddress(address: string): AddressClass | undefined {
128
- let text = address.trim();
129
- if (text.startsWith('[') && text.endsWith(']')) text = text.slice(1, -1);
130
- const zone = text.indexOf('%');
131
- if (zone !== -1 && text.includes(':')) text = text.slice(0, zone);
137
+ const text = literalOf(address);
132
138
  if (text.includes(':')) {
133
139
  const groups = parseV6(text);
134
140
  return groups === undefined ? undefined : classifyV6(groups);
@@ -141,3 +147,33 @@ export function classifyAddress(address: string): AddressClass | undefined {
141
147
  export function isPublicAddress(address: string): boolean {
142
148
  return classifyAddress(address) === 'public';
143
149
  }
150
+
151
+ /**
152
+ * The NETWORK a caller's address names, for keying a per-caller budget: IPv4 as itself, an
153
+ * IPv4-mapped IPv6 (`::ffff:a.b.c.d`) as the IPv4 it carries, any other IPv6 as its /64 in RFC 5952
154
+ * form (`2001:db8:1:2::/64`). A /64 is the smallest block one subscriber is handed, so keying on
155
+ * the full IPv6 string gave one host ~2^64 budgets. Anything that is not an address literal is
156
+ * returned unchanged — the caller's key stays whatever it was, never a guessed network.
157
+ */
158
+ export function addressNetwork(address: string): string {
159
+ const text = literalOf(address);
160
+ if (!text.includes(':')) {
161
+ const v4 = parseV4(text);
162
+ return v4 === undefined ? address : text;
163
+ }
164
+ const g = parseV6(text);
165
+ if (g === undefined) return address;
166
+ if (g.slice(0, 5).every((group) => group === 0) && g[5] === 0xffff) {
167
+ const v4 = embeddedV4(g);
168
+ return [24, 16, 8, 0].map((shift) => String(Math.floor(v4 / 2 ** shift) % 256)).join('.');
169
+ }
170
+ const prefix = g.slice(0, 4);
171
+ // RFC 5952: the zero run reaching into the (zero) interface half is always the longest, so
172
+ // compression starts at the first zero group from which the prefix stays zero.
173
+ let cut = 4;
174
+ while (cut > 0 && prefix[cut - 1] === 0) cut -= 1;
175
+ return `${prefix
176
+ .slice(0, cut)
177
+ .map((group) => group.toString(16))
178
+ .join(':')}::/64`;
179
+ }
package/src/assert.ts CHANGED
@@ -4,7 +4,7 @@
4
4
  import { renderCauseValue } from './error-render';
5
5
  import { UltimateError } from './errors';
6
6
 
7
- export interface InvariantOptions {
7
+ export interface AssertCodedOptions {
8
8
  readonly docs?: string | undefined;
9
9
  readonly meta?: Readonly<Record<string, unknown>> | undefined;
10
10
  }
@@ -25,12 +25,16 @@ export function assertNever(value: never, fix?: string): never {
25
25
  });
26
26
  }
27
27
 
28
- export function invariant(
28
+ /**
29
+ * Throw `code` with `cause` and `fix` unless `condition` holds — `assert` with a dedicated code.
30
+ * Not `invariant`: that name is `@ultimat3/entity`'s, the declaration of an entity invariant.
31
+ */
32
+ export function assertCoded(
29
33
  condition: unknown,
30
34
  code: string,
31
35
  cause: string,
32
36
  fix: string,
33
- options?: InvariantOptions,
37
+ options?: AssertCodedOptions,
34
38
  ): asserts condition {
35
39
  if (condition) return;
36
40
  throw new UltimateError({
@@ -42,7 +46,7 @@ export function invariant(
42
46
  });
43
47
  }
44
48
 
45
- /** `invariant` with the generic code, for checks that have no dedicated code yet. */
49
+ /** `assertCoded` with the generic code, for checks that have no dedicated code yet. */
46
50
  export function assert(condition: unknown, cause: string, fix: string): asserts condition {
47
- invariant(condition, 'X_INVARIANT', cause, fix);
51
+ assertCoded(condition, 'X_INVARIANT', cause, fix);
48
52
  }