okengine 0.19.9 → 0.21.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 (156) hide show
  1. package/AGENTS.md +1 -1
  2. package/package.json +2 -1
  3. package/site/content/docs/client/auth.mdx +1 -2
  4. package/site/content/docs/client/calling.mdx +125 -37
  5. package/site/content/docs/client/index.mdx +7 -7
  6. package/site/content/docs/client/react.mdx +5 -0
  7. package/site/content/docs/elements/channel/email.mdx +25 -36
  8. package/site/content/docs/elements/channel/index.mdx +88 -46
  9. package/site/content/docs/elements/channel/push.mdx +7 -9
  10. package/site/content/docs/elements/channel/sms.mdx +6 -4
  11. package/site/content/docs/elements/channel/whatsapp.mdx +11 -9
  12. package/site/content/docs/elements/clock/index.mdx +14 -25
  13. package/site/content/docs/elements/flow/http.mdx +42 -23
  14. package/site/content/docs/elements/flow/index.mdx +41 -30
  15. package/site/content/docs/elements/flow/routing.mdx +166 -122
  16. package/site/content/docs/elements/gate/rls.mdx +2 -2
  17. package/site/content/docs/elements/gate/tenancy.mdx +1 -1
  18. package/site/content/docs/elements/store/files.mdx +11 -5
  19. package/site/content/docs/elements/store/index.mdx +13 -6
  20. package/site/content/docs/elements/store/kv.mdx +8 -2
  21. package/site/content/docs/elements/store/search.mdx +5 -5
  22. package/site/content/docs/elements/store/sql.mdx +100 -39
  23. package/site/content/docs/elements/vault/index.mdx +14 -13
  24. package/site/content/docs/elements/vault/secrets.mdx +6 -3
  25. package/site/content/docs/index.mdx +1 -1
  26. package/site/content/docs/plugins/magic-link.mdx +4 -3
  27. package/site/content/docs/plugins/otp.mdx +4 -3
  28. package/site/content/docs/plugins/two-factor.mdx +4 -0
  29. package/site/content/docs/providers/index.mdx +1 -1
  30. package/site/content/docs/recipes/index.mdx +1 -1
  31. package/site/content/docs/recipes/rustfs.mdx +1 -1
  32. package/site/content/docs/reference/cli.mdx +7 -4
  33. package/site/content/docs/reference/configuration.mdx +8 -8
  34. package/site/content/docs/reference/errors.mdx +229 -55
  35. package/site/content/docs/reference/fx.mdx +22 -9
  36. package/site/content/docs/reference/i18n.mdx +4 -4
  37. package/site/content/docs/reference/plugins.mdx +4 -4
  38. package/site/content/docs/understand/the-architecture.mdx +2 -2
  39. package/site/content/docs/understand/try-it.mdx +759 -24
  40. package/src/cli/ai-setup/ai-setup.test.ts +40 -0
  41. package/src/cli/ai-setup/apply.ts +28 -46
  42. package/src/cli/build.test.ts +3 -3
  43. package/src/cli/build.ts +5 -5
  44. package/src/cli/db-auto-push.test.ts +11 -0
  45. package/src/cli/db-auto-push.ts +6 -2
  46. package/src/cli/db.test.ts +1 -1
  47. package/src/cli/db.ts +6 -6
  48. package/src/cli/dev-app-runner.ts +2 -1
  49. package/src/cli/dev-db-push.test.ts +6 -2
  50. package/src/cli/dev-schema-sync.ts +1 -1
  51. package/src/cli/dev.test.ts +10 -7
  52. package/src/cli/dev.ts +13 -11
  53. package/src/cli/ensure-drizzle-config.ts +4 -3
  54. package/src/cli/start.ts +2 -1
  55. package/src/client/create.ts +3 -3
  56. package/src/client/explain.test.ts +252 -0
  57. package/src/client/explain.ts +272 -0
  58. package/src/client/live.test.ts +44 -0
  59. package/src/client/notes-contract.test.ts +10 -0
  60. package/src/client/sse.ts +6 -2
  61. package/src/client/transport.test.ts +67 -0
  62. package/src/client/transport.ts +24 -40
  63. package/src/client/types.ts +17 -6
  64. package/src/client-react/browser.test.ts +23 -0
  65. package/src/client-react/live-resource.ts +6 -2
  66. package/src/client-react/use-live-query.ts +1 -1
  67. package/src/compiler/flow-path.test.ts +1 -0
  68. package/src/compiler/flow-path.ts +1 -1
  69. package/src/compiler/generate-adopt.test.ts +55 -1
  70. package/src/compiler/generate-adopt.ts +111 -21
  71. package/src/compiler/response.ts +17 -27
  72. package/src/config/index.ts +6 -4
  73. package/src/console/server/invoke-user-flow.test.ts +8 -2
  74. package/src/console/server/invoke-user-flow.ts +12 -18
  75. package/src/console/server/security.gate.test.ts +1 -1
  76. package/src/console/ui-next/dist/assets/{access-page-BoC83Ubl.js → access-page-tIbsiphz.js} +1 -1
  77. package/src/console/ui-next/dist/assets/{agent-disclosure-CKjAEOqA.js → agent-disclosure-CSKumwS2.js} +1 -1
  78. package/src/console/ui-next/dist/assets/{cache-glyph-Ceaq9pYh.js → cache-glyph-BCC-DxKT.js} +1 -1
  79. package/src/console/ui-next/dist/assets/{call-pii-button-C4lmY7ck.js → call-pii-button-jOmYISdc.js} +1 -1
  80. package/src/console/ui-next/dist/assets/{collapsible-82y257sL.js → collapsible-DC2xNaAb.js} +1 -1
  81. package/src/console/ui-next/dist/assets/{duration-tone-W63jaMZ8.js → duration-tone-CwoV56jn.js} +1 -1
  82. package/src/console/ui-next/dist/assets/{flows-page-KFTFj2rK.js → flows-page-gT1lsWQK.js} +1 -1
  83. package/src/console/ui-next/dist/assets/{highlighted-json-yE9zNTqC.js → highlighted-json-C2GZEJNI.js} +1 -1
  84. package/src/console/ui-next/dist/assets/{http-method-CwFeFroN.js → http-method-BdYjcIrD.js} +1 -1
  85. package/src/console/ui-next/dist/assets/{index-r7xXt_VV.js → index-Cul17AcV.js} +3 -3
  86. package/src/console/ui-next/dist/assets/{observability-page-BDiliMiC.js → observability-page-oY9vdYBk.js} +1 -1
  87. package/src/console/ui-next/dist/assets/{replica-lag-DYDzWUFT.js → replica-lag-C4QdAF7J.js} +1 -1
  88. package/src/console/ui-next/dist/assets/{request-meta-CzrOfgiz.js → request-meta-C43DHyld.js} +1 -1
  89. package/src/console/ui-next/dist/assets/{store-page-CZC2cwaw.js → store-page-CL0D9dOq.js} +1 -1
  90. package/src/console/ui-next/dist/assets/{trace-detail-sheet-DgeeejW7.js → trace-detail-sheet-Dcpo4Us_.js} +1 -1
  91. package/src/console/ui-next/dist/assets/{tree-expand-toggle-CzGIyOPY.js → tree-expand-toggle-CKJTuv43.js} +1 -1
  92. package/src/console/ui-next/dist/assets/{units-page-DBiDCLIB.js → units-page-1PlT19ft.js} +1 -1
  93. package/src/console/ui-next/dist/assets/{vault-page-CcHsthPe.js → vault-page-BwZ9YjTW.js} +1 -1
  94. package/src/console/ui-next/dist/index.html +1 -1
  95. package/src/docker/docker.test.ts +3 -3
  96. package/src/docker/images-config.test.ts +4 -4
  97. package/src/docker/stack-id.test.ts +1 -1
  98. package/src/drivers/clock-postgres.test.ts +10 -2
  99. package/src/drivers/clock-postgres.ts +18 -2
  100. package/src/drivers/vault-driver-removal.test.ts +2 -2
  101. package/src/elements/channel/declare.ts +66 -3
  102. package/src/elements/channel/runtime.ts +9 -11
  103. package/src/elements/channel.test.ts +42 -0
  104. package/src/elements/channel.ts +4 -2
  105. package/src/elements/clock/reconcile.ts +45 -24
  106. package/src/elements/clock.test.ts +33 -0
  107. package/src/elements/store/emit-drizzle.ts +285 -65
  108. package/src/elements/store/files-errors.test.ts +149 -0
  109. package/src/elements/store/files-errors.ts +189 -0
  110. package/src/elements/store/kv-errors.test.ts +98 -0
  111. package/src/elements/store/kv-errors.ts +139 -0
  112. package/src/elements/store/load-plugin-tables.ts +1 -1
  113. package/src/elements/store/prepare-row.test.ts +57 -4
  114. package/src/elements/store/resource.ts +11 -7
  115. package/src/elements/store/runtime.ts +18 -13
  116. package/src/elements/store/schema-decl.test.ts +178 -0
  117. package/src/elements/store/sql-errors.test.ts +197 -0
  118. package/src/elements/store/sql-errors.ts +294 -0
  119. package/src/elements/store/sql-session.test.ts +52 -0
  120. package/src/elements/store/sql-session.ts +50 -2
  121. package/src/elements/store/store-errors.ts +47 -0
  122. package/src/elements/store/table.ts +8 -6
  123. package/src/http.ts +9 -1
  124. package/src/i18n/catalogs/ar.ts +18 -0
  125. package/src/i18n/catalogs/en.ts +18 -0
  126. package/src/index.ts +9 -1
  127. package/src/kernel/adopt-barrel-fresh.test.ts +1 -1
  128. package/src/kernel/app.ts +40 -34
  129. package/src/kernel/auto-registry.test.ts +26 -1
  130. package/src/kernel/boot.ts +2 -2
  131. package/src/kernel/boundary-contract.ts +6 -1
  132. package/src/kernel/builtin-errors.test.ts +117 -0
  133. package/src/kernel/builtin-errors.ts +129 -0
  134. package/src/kernel/call.test.ts +182 -0
  135. package/src/kernel/errors-vault.ts +16 -0
  136. package/src/kernel/errors.registry.test.ts +7 -0
  137. package/src/kernel/errors.ts +97 -24
  138. package/src/kernel/fail-helpers.ts +34 -0
  139. package/src/kernel/flow-units.ts +3 -3
  140. package/src/kernel/fx.test.ts +8 -0
  141. package/src/kernel/fx.ts +24 -8
  142. package/src/kernel/index.ts +12 -1
  143. package/src/kernel/mutation-id.ts +8 -0
  144. package/src/kernel/plugin.ts +4 -3
  145. package/src/kernel/project-out.test.ts +176 -0
  146. package/src/kernel/project-out.ts +91 -0
  147. package/src/kernel/realtime-bind.ts +2 -3
  148. package/src/kernel/router/linear.ts +12 -6
  149. package/src/kernel/router.test.ts +13 -0
  150. package/src/plugins/magic-link.ts +25 -24
  151. package/src/plugins/otp.ts +35 -24
  152. package/src/plugins/two-factor.ts +15 -0
  153. package/src/runs/duckdb.test.ts +2 -2
  154. package/src/runtime/dev-request-log.ts +29 -11
  155. package/src/term.test.ts +76 -0
  156. package/src/term.ts +166 -3
package/AGENTS.md CHANGED
@@ -84,7 +84,7 @@ After changing `src/kernel/`, `src/client/`, `src/compiler/`, `src/validation/`,
84
84
  Published packages:
85
85
 
86
86
  - `okengine` — framework. Subpath exports: `.`, `./client`, `./test`, `./config`, `./auth`, `./plugins`, `./drivers/*`. `"sideEffects": false`. CLI binary: `oke`.
87
- - `create-oke` — scaffold CLI (`bunx create-oke@latest <name>`). Lives in `packages/create-oke` and ships Notes starters from `packages/create-oke/templates/{standard,advanced}`.
87
+ - `create-oke` — scaffold CLI (`bunx create-oke@latest <name>`). Lives in `packages/create-oke` and ships starters from `packages/create-oke/templates/{blank,shorter}`.
88
88
 
89
89
  Engine: Bun `>=1.4.2`.
90
90
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "okengine",
3
- "version": "0.19.9",
3
+ "version": "0.21.0",
4
4
  "description": "One law. Eight elements. One contract. The backend model stays small; operational surfaces are derived from it instead of maintained separately.",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -70,6 +70,7 @@
70
70
  },
71
71
  "./client": "./src/client/index.ts",
72
72
  "./client/auth": "./src/client/auth.ts",
73
+ "./client/explain": "./src/client/explain.ts",
73
74
  "./client-react": "./src/client-react/index.ts",
74
75
  "./test": "./src/test/index.ts",
75
76
  "./testing": {
@@ -171,8 +171,7 @@ auth.bind(api);
171
171
  | `Forbidden` | 403 | Wrong scopes / CSRF — read `error.data.reason` |
172
172
  | `RateLimited` | 429 | Wait `retryAfterMs` |
173
173
 
174
- Also see [CORS](/docs/plugins/cors) and [CSRF](/docs/plugins/csrf). Advanced create-oke starter
175
- demos cookie + passkey + `<Can>`.
174
+ Also see [CORS](/docs/plugins/cors) and [CSRF](/docs/plugins/csrf).
176
175
 
177
176
  ## Troubleshooting
178
177
 
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  title: "Calling"
3
- description: "createClient forms, REST vs RPC, options, result envelopes, store resources, and remote types."
3
+ description: "createClient forms, REST vs RPC, match/explain for unknown errors, options, envelopes, and remote types."
4
4
  icon: "Braces"
5
5
  source: "docs/spec/unified-theory.md"
6
6
  ---
@@ -13,8 +13,8 @@ For developers wiring a storefront, mobile app, or partner service to an okengin
13
13
  **6530**).
14
14
 
15
15
  <Callout title="The one rule">
16
- Treat every call as a result envelope: `{ data, error }`. Switch on `error.code`. Only transport
17
- / protocol problems use `code: "TransportError"`.
16
+ Treat every call as `{ data, error }`. Use `match` from `okengine/client/explain` for success plus
17
+ unknown failures. Name a code only when this screen has special recovery.
18
18
  </Callout>
19
19
 
20
20
  ## Smallest Example
@@ -81,11 +81,11 @@ export const publicApiUrl = vault.config("PUBLIC_API_URL", {
81
81
  });
82
82
  ```
83
83
 
84
- | Runtime | Base URL for `createClient` |
85
- | ------------------------------------------ | --------------------------------------------------------------- |
86
- | Browser / Bun (create-oke web) | `vault.env("PUBLIC_API_URL") ?? ""` — empty = same-origin proxy |
87
- | Node / worker / CLI | `vault.env.required("PUBLIC_API_URL")` |
88
- | Separate storefront after `oke client add` | Same `vault.env("PUBLIC_API_URL") ?? ""` |
84
+ | Runtime | Base URL for `createClient` |
85
+ | ------------------------------------------ | --------------------------------------------------------- |
86
+ | Browser / Bun | `vault.env("PUBLIC_API_URL") ?? ""` — empty = same origin |
87
+ | Node / worker / CLI | `vault.env.required("PUBLIC_API_URL")` |
88
+ | Separate storefront after `oke client add` | Same `vault.env("PUBLIC_API_URL") ?? ""` |
89
89
 
90
90
  Set `PUBLIC_API_URL` via environment or `oke vault set`. Prefer `vault.env` over raw
91
91
  `process.env` — same helpers as the rest of the measure. See
@@ -125,7 +125,7 @@ if (isOk(result)) {
125
125
  <Tab value="Failures">
126
126
 
127
127
  ```typescript
128
- import { isOk, isErrorCode } from "okengine/client";
128
+ import { match } from "okengine/client/explain";
129
129
 
130
130
  const result = await api.bookings.create({
131
131
  flightId: "SK481",
@@ -133,16 +133,18 @@ const result = await api.bookings.create({
133
133
  cabin: "economy",
134
134
  });
135
135
 
136
- if (isOk(result)) {
137
- result.data.confirmationCode;
138
- } else if (result.error.code === "FlightFull") {
139
- offerWaitlist(result.error.data.seatsLeft);
140
- } else if (isErrorCode(result.error, "NotFound")) {
141
- // shared helper narrowing
142
- }
136
+ match(result, {
137
+ ok: (data) => show(data.confirmationCode),
138
+ FlightFull: (data) => offerWaitlist(data.seatsLeft),
139
+ auth: () => goSignIn(),
140
+ invalid: (e) => setForm(e.fields),
141
+ _: (e) => toast(e.message),
142
+ });
143
143
  ```
144
144
 
145
- Prefer `error?.code === "FlightFull"` for inference; use `isErrorCode` in shared helpers.
145
+ One import. Named codes narrow `data`. Kind arms (`auth`, `invalid`) cover chrome. `_` is required
146
+ — store auto-map, transport, and unknown domain codes land there. Full table: [`match` /
147
+ `explain`](#match--explain).
146
148
 
147
149
  </Tab>
148
150
 
@@ -154,13 +156,13 @@ import { isTransportError } from "okengine/client";
154
156
  const result = await api.orders.list({ limit: 20, status: "shipped" });
155
157
 
156
158
  if (result.error && isTransportError(result.error)) {
157
- showOfflineBanner(result.error.data.message);
159
+ showOfflineBanner(result.error.message); // === error.data.message
158
160
  // optional: result.error.data.status
159
161
  }
160
162
  ```
161
163
 
162
164
  Network failure, abort (`timeout`), non-JSON body, or HTTP status without a `{ data, error }`
163
- envelope — never a declared Flow code.
165
+ envelope — never a declared Flow code. Prefer `error.message`; it always matches `error.data.message`.
164
166
 
165
167
  </Tab>
166
168
 
@@ -180,6 +182,80 @@ JSON envelopes.
180
182
 
181
183
  </Tabs>
182
184
 
185
+ ## `match` / `explain`
186
+
187
+ A `create` can return `ValidationError`, store auto-map (`Conflict`, `DatabaseError`), gates,
188
+ `TransportError`, plus any domain code. Do not switch on every `error.code`.
189
+
190
+ ```typescript
191
+ import { match } from "okengine/client/explain";
192
+
193
+ const result = await api.bookings.create({ flightId: "SK481", seats: 2 });
194
+
195
+ match(result, {
196
+ ok: (data) => show(data.confirmationCode),
197
+ FlightFull: (data) => offerWaitlist(data.seatsLeft),
198
+ auth: () => goSignIn(),
199
+ invalid: (e) => setForm(e.fields),
200
+ _: (e) => toast(e.message),
201
+ });
202
+ ```
203
+
204
+ <Callout title="Dispatch order">
205
+ `ok` → named code (`FlightFull`) → UX kind (`auth`, `invalid`) → required `_`. Kind names are
206
+ reserved — do not use `auth` or `failed` as domain codes.
207
+ </Callout>
208
+
209
+ Import from `okengine/client/explain` so the 5 kB `okengine/client` graph stays small. `isOk` /
210
+ `isFail` stay on `okengine/client` when you only need a predicate.
211
+
212
+ | Helper | When |
213
+ | ----------------------------- | ------------------------- |
214
+ | `match(result, { ok, …, _ })` | Whole envelope — one call |
215
+ | `matchError(error, { …, _ })` | You already have `error` |
216
+ | `explain(error)` | Chrome only — no matcher |
217
+
218
+ ### Kinds
219
+
220
+ | `kind` | Codes | Typical UI |
221
+ | ------------- | -------------------------------------------------------------------------------------- | --------------------------- |
222
+ | `auth` | `Unauthorized`, `AuthFailed` | Sign-in |
223
+ | `permission` | `Forbidden` | “You can’t do that” |
224
+ | `missing` | `NotFound`; `TransportError` HTTP 404 | Empty state |
225
+ | `conflict` | `Conflict`, `ForeignKey` | Retry or pick another value |
226
+ | `invalid` | `ValidationError`, `InvalidQuery`, `UnsupportedMediaType`; `DatabaseError` 422 reasons | Form fields |
227
+ | `limited` | `RateLimited`, `AuthRateLimited` | Wait / retry (`retryable`) |
228
+ | `unavailable` | `ServiceUnavailable`; `DatabaseError` `retryable`; other `TransportError` | Offline / retry |
229
+ | `failed` | `InternalError`, unknown `DatabaseError`, unnamed domain codes | Toast `message` |
230
+
231
+ Named code arms receive typed `data`. Kind arms and `_` receive `{ kind, code, message, retryable,
232
+ fields? }`. `_` is required even if you listed every kind.
233
+
234
+ `explain(error).fields` maps `ValidationError` issues to `{ email: "Required" }` (`.`-joined
235
+ path; empty path is `_`). `retryable` is true for `limited` and `unavailable`; `retryAfterMs` is
236
+ set when the payload has `retryAfterMs` or `retryAfter` (seconds).
237
+
238
+ ```typescript
239
+ import { explain, matchError } from "okengine/client/explain";
240
+
241
+ if (result.error) {
242
+ matchError(result.error, {
243
+ FlightFull: (data) => offerWaitlist(data.seatsLeft),
244
+ _: (e) => toast(e.message),
245
+ });
246
+ }
247
+
248
+ explain(result.error!); // { kind, code, message, retryable, fields? }
249
+ ```
250
+
251
+ `error.code === "FlightFull"` still narrows `error.data` if you prefer an `if`. HTTP status is on
252
+ the response, not on `error`.
253
+
254
+ Domain codes with no `{ message }` toast the code name (`OutOfStock`). Pass
255
+ `fx.fail("OutOfStock", data, { message })` on the Flow, or name that code in `match`.
256
+
257
+ See [Errors](/docs/reference/errors) for `fx.fail` helpers and store auto-map.
258
+
183
259
  ## `createClient` forms
184
260
 
185
261
  | Form | Types from | Wire |
@@ -324,24 +400,19 @@ See [Store](/docs/elements/store) for the list query language. Handwritten lists
324
400
  ## Result envelopes
325
401
 
326
402
  Success may include optional top-level `meta` (for example pagination). Declared flow errors and
327
- transport failures share the failure arm:
403
+ transport failures share the failure arm. Handle them with [`match`](#match--explain).
328
404
 
329
405
  ```typescript
330
- import { isOk, isFail, isErrorCode, isTransportError } from "okengine/client";
331
-
332
- const result = await api.bookings.create({ flightId: "SK481", seats: 2 });
333
-
334
- if (isOk(result)) {
335
- result.data.confirmationCode;
336
- } else if (result.error.code === "FlightFull") {
337
- result.error.data.seatsLeft;
338
- } else if (isTransportError(result.error)) {
339
- result.error.data.message;
340
- }
406
+ import { isOk, isFail, isTransportError } from "okengine/client";
341
407
 
342
408
  isFail(result); // true when error !== null
409
+ isOk(result); // narrows to success `data`
410
+ isTransportError(result.error); // network / non-envelope HTTP
343
411
  ```
344
412
 
413
+ `error.code === "FlightFull"` still narrows `error.data`. HTTP status is on the response, not on
414
+ `error`.
415
+
345
416
  ## Remote types
346
417
 
347
418
  `oke dev` regenerates `oke-client.d.ts` from `GET /_oke/client.json`. A separate storefront repo —
@@ -362,8 +433,8 @@ const api = createClient(import.meta.env.PUBLIC_API_URL ?? "", { $routes: routes
362
433
 
363
434
  Ambient `.d.ts` types `in` / `out` / live / stream stamps. The routes module supplies wire REST.
364
435
 
365
- create-oke starters ship `web/`: Vite proxies Flow paths plus `/auth` and `/_oke` to the app.
366
- Leave `PUBLIC_API_URL` unset in local web so `createClient("")` stays same-origin.
436
+ Point `createClient` at your app origin (`PUBLIC_API_URL`), or leave it empty when the
437
+ browser page is same-origin with the API.
367
438
 
368
439
  CLI details: [CLI Reference](/docs/reference/cli).
369
440
 
@@ -376,6 +447,7 @@ CLI details: [CLI Reference](/docs/reference/cli).
376
447
  | `createTransport` | function | Low-level HTTP transport (timeout / retry / auth) |
377
448
  | `isOk` / `isFail` | function | Envelope predicates |
378
449
  | `isErrorCode` / `isTransportError` | function | Error narrowing |
450
+ | `match` / `matchError` / `explain` | function | `okengine/client/explain` — `ok`, codes, kinds, `_` |
379
451
  | `Client`, `ClientCall`, `ClientResult`, … | types | Contracts, `page.next()` / `for await` of `list()` |
380
452
  | `Register` | interface | Module-augmentation slot for ambient App types |
381
453
  | `AppOf` | type | Brand a bare route map as an App |
@@ -387,7 +459,7 @@ Budget: the `./client` export stays under the measured client-runtime cap (hard
387
459
  <Accordions>
388
460
 
389
461
  <Accordion title="api.bookings.get is not a function / type error">
390
- Confirm the Flow is `export`ed from a generated unit (`import "@/flows/generated"` then
462
+ Confirm the Flow is `export`ed from a generated unit (`import "@/flows"` then
391
463
  `oke({ name })`), or from a module you still `.adopt({ bookings })`.
392
464
 
393
465
  Type `createClient` with that `App` (or ambient `Register` after `oke-client.d.ts` regenerates).
@@ -402,8 +474,24 @@ Restart `oke dev` after renaming exports.
402
474
 
403
475
  <Accordion title='error.code is "TransportError"'>
404
476
  Network failure, abort (`timeout`), non-JSON body, empty error response, or HTTP status without a
405
- `{ data, error }` envelope. Declared flow codes (`NotFound`, `FlightFull`, …) never use this code.
406
- Message text lives in `error.data.message`; HTTP status may appear as `error.data.status`.
477
+ `{ data, error }` envelope. Built-in and domain flow codes never use this code.
478
+ Prefer `error.message` — it always equals `error.data.message`. `explain` maps HTTP 404 transport
479
+ to kind `missing`.
480
+ </Accordion>
481
+
482
+ <Accordion title="Why import from okengine/client/explain?">
483
+ `match` / `explain` stay off the measured `okengine/client` gzip cap. Predicates (`isOk`,
484
+ `isTransportError`) stay on `okengine/client`.
485
+ </Accordion>
486
+
487
+ <Accordion title="Toast showed OutOfStock / FlightFull">
488
+ Domain codes with no `{message}` use the code name as `explain.message`. Handle the named arm, or
489
+ pass `fx.fail("OutOfStock", data, {message})` on the Flow.
490
+ </Accordion>
491
+
492
+ <Accordion title="404 — route miss or NotFound?">
493
+ Bare body `Not Found` means no HTTP route — `TransportError` (kind `missing`). JSON
494
+ `{ error: { code: "NotFound" } }` is `fx.fail.notFound`. Distinguish by envelope, not status.
407
495
  </Accordion>
408
496
 
409
497
  <Accordion title="Failed to fetch …/_oke/client.json">
@@ -427,7 +515,7 @@ Restart `oke dev` after renaming exports.
427
515
  - [Vault · Config](/docs/elements/vault/config) — `PUBLIC_API_URL`, `vault.env`
428
516
  - [Routing](/docs/elements/flow/routing) — `$routes` stamps
429
517
  - [HTTP](/docs/elements/flow/http) — verbs and envelopes on the server
430
- - [Errors](/docs/reference/errors) — framework codes vs failure values
518
+ - [Errors](/docs/reference/errors) — `fx.fail` helpers, store auto-map, `match` on the client
431
519
  - [CLI](/docs/reference/cli) — `oke client add`, `oke dev`
432
520
 
433
521
  ## Next
@@ -27,7 +27,7 @@ the envelope.
27
27
 
28
28
  ```typescript title="src/app.ts"
29
29
  import "@/core";
30
- import "@/flows/generated";
30
+ import "@/flows";
31
31
  import { oke } from "okengine/http";
32
32
 
33
33
  export const app = oke({ name: "commerce" });
@@ -46,12 +46,12 @@ import { createClient } from "okengine/client";
46
46
  import { vault } from "okengine/vault";
47
47
  import { app } from "../../src/app";
48
48
 
49
- // Base URL via vault.env (empty = same-origin proxy in create-oke web)
49
+ // Base URL via vault.env (empty = same origin as the page)
50
50
  const api = createClient(app, vault.env("PUBLIC_API_URL") ?? "");
51
51
  const { data, error } = await api.bookings.get({ id: "bkg_7f3a" });
52
52
 
53
53
  if (error) {
54
- // TransportError or a declared flow code (NotFound, …)
54
+ // TransportError or a flow code (NotFound, …)
55
55
  return;
56
56
  }
57
57
 
@@ -128,8 +128,8 @@ const api = createClient(vault.env("PUBLIC_API_URL") ?? "");
128
128
  ```
129
129
 
130
130
  Declare the origin on the server with [Vault · Config](/docs/elements/vault/config)
131
- (`PUBLIC_API_URL`), then set it via env or `oke vault set`. Leave it unset in local web
132
- dev so `createClient("")` stays same-origin behind the Vite proxy.
131
+ (`PUBLIC_API_URL`), then set it via env or `oke vault set`. Leave it unset in local frontend
132
+ dev so `createClient("")` stays same-origin behind your proxy.
133
133
 
134
134
  </Tab>
135
135
 
@@ -160,7 +160,7 @@ dev so `createClient("")` stays same-origin behind the Vite proxy.
160
160
  <Cards>
161
161
  <Card
162
162
  title="Calling"
163
- description="createClient forms, REST vs RPC, options, envelopes, resources, remote types."
163
+ description="createClient forms, REST vs RPC, match/explain, envelopes, remote types."
164
164
  href="/docs/client/calling"
165
165
  />
166
166
  <Card
@@ -192,7 +192,7 @@ dev so `createClient("")` stays same-origin behind the Vite proxy.
192
192
  <Cards>
193
193
  <Card
194
194
  title="Calling"
195
- description="REST vs RPC, options, helpers, and remote types."
195
+ description="REST vs RPC, match/explain, options, and remote types."
196
196
  href="/docs/client/calling"
197
197
  />
198
198
  <Card
@@ -221,6 +221,11 @@ Prefer [`createAuthClient`](/docs/client/auth) for cookie/Bearer sessions and me
221
221
  Without a token, status becomes `"unauthenticated"`.
222
222
  </Accordion>
223
223
 
224
+ <Accordion title="Vite SPA is a blank page">
225
+ The console shows `node:async_hooks` / `AsyncLocalStorage` from `okengine/client-react`. Upgrade
226
+ okengine — `Can` / `useLiveQuery` must not import the server realtime binder.
227
+ </Accordion>
228
+
224
229
  <Accordion title="useLiveQuery never connects">
225
230
  Pass `live: { method, path }` from `app.$routes` for the resource’s `/live` route. Set
226
231
  `enabled: true` (or omit). See [Live](/docs/client/live) for exposure and resume errors.
@@ -6,14 +6,14 @@ source: "docs/spec/unified-theory.md"
6
6
  ---
7
7
 
8
8
  Email (`channel.email`) is the default Channel medium. Declare a binder with a From address,
9
- register templates, put bodies in the catalog, and send with `fx.send`.
9
+ put `{{field}}` bodies on `.template({ catalog })`, and send with `fx.send`.
10
10
 
11
11
  For developers who need local catchers and production HTTP MTAs — same `fx.send` path, swap the
12
12
  driver.
13
13
 
14
14
  <Callout title="The one rule">
15
- Use `channel.email({ from }).template(name, { schema, locales })` — never
16
- `channel.email("name", { subject, body })`. Subject and body live in the catalog.
15
+ Use `channel.email({ from }).template(name, { schema, locales, catalog })`. Subject and body
16
+ live on the template — not on `oke()`.
17
17
  </Callout>
18
18
 
19
19
  ## Smallest Example
@@ -33,32 +33,20 @@ export const passwordReset = mail.template("auth.resetPassword", {
33
33
  description: "Password reset link",
34
34
  locales: ["en"],
35
35
  schema: z.object({ resetLink: z.string().url() }),
36
+ catalog: {
37
+ en: {
38
+ subject: "Reset your password",
39
+ text: "Open {{resetLink}} to choose a new password.",
40
+ html: '<p><a href="{{resetLink}}">Reset your password</a></p>',
41
+ },
42
+ },
36
43
  });
37
44
  ```
38
45
 
39
46
  </Step>
40
47
 
41
48
  <Step>
42
- ### Catalog + send
43
-
44
- ```typescript title="src/app.ts"
45
- import { oke } from "okengine";
46
-
47
- oke({
48
- name: "app",
49
- channel: {
50
- catalog: {
51
- "auth.resetPassword": {
52
- en: {
53
- subject: "Reset your password",
54
- text: "Open {{resetLink}} to choose a new password.",
55
- html: '<p><a href="{{resetLink}}">Reset your password</a></p>',
56
- },
57
- },
58
- },
59
- },
60
- });
61
- ```
49
+ ### Send from a Flow
62
50
 
63
51
  ```typescript title="src/flows/auth/reset.ts"
64
52
  import { on, flow, http } from "okengine";
@@ -118,13 +106,13 @@ Per-locale `subject` / `text` / `html` with `{{field}}` interpolation from `data
118
106
 
119
107
  ```typescript
120
108
  catalog: {
121
- "auth.resetPassword": {
122
- en: { subject: "Reset", text: "{{resetLink}}", html: "<a href=\"{{resetLink}}\">Reset</a>" },
123
- ar: { subject: "إعادة التعيين", text: "{{resetLink}}" },
124
- },
109
+ en: { subject: "Reset", text: "{{resetLink}}", html: "<a href=\"{{resetLink}}\">Reset</a>" },
110
+ ar: { subject: "إعادة التعيين", text: "{{resetLink}}" },
125
111
  }
126
112
  ```
127
113
 
114
+ Put that object on `.template({ catalog })`. Do not nest the template name again.
115
+
128
116
  </Tab>
129
117
 
130
118
  <Tab value="Failover">
@@ -146,8 +134,8 @@ Receipt status `fallback` means an earlier attempt failed and a later one succee
146
134
 
147
135
  <Tab value="Plugin catalog">
148
136
 
149
- Official plugins contribute catalogs with `.channelCatalog(…)` (e.g. OTP, magic link). App
150
- `channel.catalog` merges with plugin contributions at boot.
137
+ Official plugins put copy on `.template({ catalog })` (OTP, magic link, two-factor). App
138
+ `oke({ channel.catalog })` and plugin `.channelCatalog(…)` overlay those bodies at boot.
151
139
 
152
140
  </Tab>
153
141
 
@@ -164,11 +152,12 @@ Official plugins contribute catalogs with `.channelCatalog(…)` (e.g. OTP, magi
164
152
 
165
153
  ### `.template(name, options?)`
166
154
 
167
- | Option | Type | Meaning |
168
- | ------------- | ---------- | --------------------------- |
169
- | `description` | `string` | Console label |
170
- | `locales` | `string[]` | Declared locales |
171
- | `schema` | Schema | Payload contract for `data` |
155
+ | Option | Type | Meaning |
156
+ | ------------- | ---------- | -------------------------------------- |
157
+ | `description` | `string` | Console label |
158
+ | `locales` | `string[]` | Declared locales |
159
+ | `schema` | Schema | Payload contract for `data` |
160
+ | `catalog` | object | Per-locale `subject` / `text` / `html` |
172
161
 
173
162
  Default From when neither binder nor template sets one: `"oke@localhost.test"`.
174
163
 
@@ -229,8 +218,8 @@ Channel catalogs are **not** ICU — do not use `fx.t` for email bodies ([i18n](
229
218
  </Accordion>
230
219
 
231
220
  <Accordion title="Message in Mailpit has JSON body / template name as subject">
232
- No catalog entry for that template + locale. Add `channel.catalog` (or a plugin catalog) with
233
- `subject` / `text` / `html`.
221
+ No catalog entry for that template + locale. Add `catalog` on `.template(…)` (or overlay with
222
+ `oke({channel.catalog})` / plugin `.channelCatalog`) with `subject` / `text` / `html`.
234
223
  </Accordion>
235
224
 
236
225
  <Accordion title="suppressed/opted-out or prior-bounce — no provider call">