okengine 0.4.3 → 0.5.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 (127) hide show
  1. package/package.json +3 -1
  2. package/site/content/docs/ai/index.mdx +24 -0
  3. package/site/content/docs/ai/llms-txt.mdx +3 -0
  4. package/site/content/docs/ai/meta.json +1 -1
  5. package/site/content/docs/console/gates.mdx +46 -8
  6. package/site/content/docs/console/index.mdx +54 -0
  7. package/site/content/docs/console/meta.json +1 -0
  8. package/site/content/docs/elements/gate.mdx +187 -48
  9. package/site/content/docs/elements/index.mdx +45 -0
  10. package/site/content/docs/elements/meta.json +1 -1
  11. package/site/content/docs/get-started/basic-usage.mdx +4 -3
  12. package/site/content/docs/get-started/index.mdx +33 -0
  13. package/site/content/docs/get-started/meta.json +1 -1
  14. package/site/content/docs/index.mdx +9 -31
  15. package/site/content/docs/plugins/anonymous.mdx +95 -0
  16. package/site/content/docs/plugins/compression.mdx +2 -2
  17. package/site/content/docs/plugins/cors.mdx +2 -2
  18. package/site/content/docs/plugins/csrf.mdx +2 -2
  19. package/site/content/docs/plugins/email-otp.mdx +111 -0
  20. package/site/content/docs/plugins/{security-headers.mdx → headers.mdx} +1 -1
  21. package/site/content/docs/plugins/index.mdx +69 -0
  22. package/site/content/docs/plugins/magic-link.mdx +112 -0
  23. package/site/content/docs/plugins/maintenance-mode.mdx +8 -8
  24. package/site/content/docs/plugins/meta.json +10 -1
  25. package/site/content/docs/plugins/passkey.mdx +128 -0
  26. package/site/content/docs/plugins/phone-number.mdx +111 -0
  27. package/site/content/docs/plugins/two-factor.mdx +116 -0
  28. package/site/content/docs/plugins/username.mdx +117 -0
  29. package/site/content/docs/reference/client.mdx +331 -0
  30. package/site/content/docs/reference/fx.mdx +20 -5
  31. package/site/content/docs/reference/index.mdx +45 -0
  32. package/site/content/docs/reference/meta.json +11 -1
  33. package/site/content/docs/reference/plugins.mdx +25 -14
  34. package/src/auth/auth.test.ts +20 -2
  35. package/src/auth/bindings.ts +439 -0
  36. package/src/auth/breach-check.ts +112 -0
  37. package/src/auth/config.ts +288 -0
  38. package/src/auth/cookies.ts +123 -0
  39. package/src/auth/gate-auth.test.ts +379 -0
  40. package/src/auth/identity.ts +190 -0
  41. package/src/auth/index.ts +117 -1
  42. package/src/auth/method-context.ts +33 -0
  43. package/src/auth/operator.ts +27 -1
  44. package/src/auth/password-policy.test.ts +126 -0
  45. package/src/auth/password-policy.ts +77 -0
  46. package/src/auth/plugin.ts +62 -4
  47. package/src/auth/rate.ts +45 -0
  48. package/src/auth/schema.ts +260 -0
  49. package/src/auth/secondary-storage.ts +37 -0
  50. package/src/auth/sessions.ts +58 -1
  51. package/src/auth/tables.ts +4 -0
  52. package/src/auth/verification.ts +78 -0
  53. package/src/cli/dev.test.ts +3 -3
  54. package/src/cli/schema.ts +95 -23
  55. package/src/client/auth.ts +120 -0
  56. package/src/client-react/index.ts +93 -0
  57. package/src/compiler/aot.test.ts +2 -1
  58. package/src/compiler/extract.ts +19 -0
  59. package/src/compiler/response.ts +16 -2
  60. package/src/console/server/app.ts +10 -6
  61. package/src/console/server/auth-rate.test.ts +3 -3
  62. package/src/console/server/bind.ts +12 -1
  63. package/src/console/server/channels.test.ts +1 -1
  64. package/src/console/server/console-gates.ts +14 -0
  65. package/src/console/server/console.test.ts +6 -6
  66. package/src/console/server/flows-invoke.test.ts +2 -2
  67. package/src/console/server/flows.ts +2 -0
  68. package/src/console/server/gates.ts +8 -1
  69. package/src/console/server/operator-db.test.ts +4 -4
  70. package/src/console/server/operator-db.ts +22 -4
  71. package/src/console/server/security.gate.test.ts +3 -3
  72. package/src/console/ui/gates/fixture.ts +4 -0
  73. package/src/console/ui/gates/types.ts +2 -0
  74. package/src/console/ui/shell/client.ts +1 -0
  75. package/src/elements/gate/boot.ts +136 -0
  76. package/src/elements/gate/config.ts +69 -0
  77. package/src/elements/gate/declare.ts +51 -1
  78. package/src/elements/gate/runtime.ts +3 -1
  79. package/src/elements/gate.test.ts +77 -0
  80. package/src/elements/gate.ts +20 -1
  81. package/src/elements/index.ts +8 -0
  82. package/src/index.ts +11 -0
  83. package/src/kernel/app.ts +253 -32
  84. package/src/kernel/boot.test.ts +40 -3
  85. package/src/kernel/boot.ts +8 -0
  86. package/src/kernel/call.test.ts +46 -2
  87. package/src/kernel/edge.test.ts +3 -3
  88. package/src/kernel/flow.test.ts +2 -2
  89. package/src/kernel/fx.test.ts +1 -0
  90. package/src/kernel/fx.ts +60 -0
  91. package/src/kernel/hooks.test.ts +4 -4
  92. package/src/kernel/index.ts +12 -0
  93. package/src/kernel/pipeline.test.ts +12 -8
  94. package/src/kernel/pipeline.ts +23 -4
  95. package/src/kernel/plugin/decorate.test.ts +3 -3
  96. package/src/kernel/plugin/scoping.test.ts +3 -3
  97. package/src/kernel/plugin-elements.test.ts +51 -0
  98. package/src/kernel/plugin-needs.test.ts +83 -0
  99. package/src/kernel/plugin-needs.ts +129 -0
  100. package/src/kernel/plugin.ts +101 -0
  101. package/src/kernel/registry-isolation.test.ts +5 -5
  102. package/src/kernel/registry.ts +102 -3
  103. package/src/manifest/types.ts +2 -0
  104. package/src/plugins/anonymous.ts +58 -0
  105. package/src/plugins/auth/shared.ts +121 -0
  106. package/src/plugins/auth-methods.test.ts +176 -0
  107. package/src/plugins/compression.test.ts +5 -5
  108. package/src/plugins/config-source.test.ts +1 -1
  109. package/src/plugins/cors.test.ts +16 -10
  110. package/src/plugins/csrf.test.ts +1 -1
  111. package/src/plugins/email-otp.ts +161 -0
  112. package/src/plugins/index.ts +31 -0
  113. package/src/plugins/ip-allowlist.test.ts +19 -9
  114. package/src/plugins/magic-link.ts +163 -0
  115. package/src/plugins/maintenance-mode.test.ts +9 -5
  116. package/src/plugins/passkey.ts +216 -0
  117. package/src/plugins/phone-number.ts +149 -0
  118. package/src/plugins/security-headers.test.ts +14 -14
  119. package/src/plugins/two-factor.ts +249 -0
  120. package/src/plugins/username.ts +148 -0
  121. package/src/runs/runs.test.ts +6 -2
  122. package/src/runtime/primitives.ts +37 -4
  123. package/src/runtime/serve.test.ts +3 -2
  124. package/src/runtime/types.ts +25 -2
  125. package/src/test/create-test-app.test.ts +1 -1
  126. package/src/test/create-test-app.ts +4 -1
  127. package/src/test/provisions.integration.test.ts +1 -1
@@ -0,0 +1,331 @@
1
+ ---
2
+ title: "Client"
3
+ description: "Typed caller for your flows — createClient from okengine/client, zero codegen, errors as values."
4
+ icon: "MonitorSmartphone"
5
+ source: "docs/spec/unified-theory.md"
6
+ ---
7
+
8
+ `okengine/client` is how a browser, CLI, or another service calls your app's flows. Adopt the unit, take `typeof app`, and `api.notes.get({ id })` is fully typed — same contracts the server already has, no separate schema project.
9
+
10
+ <Callout title="The one rule">
11
+ Treat every call as a result envelope: `{ data, error }`. Flow failures are values you switch on
12
+ (`error.code`); they are never thrown. Only transport / protocol problems use
13
+ `code: "TransportError"`.
14
+ </Callout>
15
+
16
+ ## Quick start
17
+
18
+ <Steps>
19
+
20
+ <Step>
21
+ ### Adopt flows and export `App`
22
+
23
+ ```typescript title="src/app.ts"
24
+ import { oke } from "okengine";
25
+ import * as main from "./flows/main";
26
+
27
+ export const app = oke({ name: "standard" }).adopt({ main });
28
+ export type App = typeof app;
29
+ ```
30
+
31
+ </Step>
32
+
33
+ <Step>
34
+ ### Create the client
35
+
36
+ Same repo — pass the app value so HTTP triggers hit REST (method + path from adopt):
37
+
38
+ ```typescript title="client"
39
+ import { createClient } from "okengine/client";
40
+ import { app } from "./app";
41
+
42
+ const api = createClient(app, "http://localhost:6530");
43
+ ```
44
+
45
+ Or type-only with `createClient<App>(url)` and pass `$routes: app.$routes` when you want REST
46
+ instead of RPC.
47
+
48
+ </Step>
49
+
50
+ <Step>
51
+ ### Call a flow and narrow the result
52
+
53
+ ```typescript
54
+ const { data, error } = await api.main.health();
55
+
56
+ if (error) {
57
+ // TransportError or a declared flow code
58
+ return;
59
+ }
60
+
61
+ // data inferred from the flow's `out`
62
+ console.log(data.ok);
63
+ ```
64
+
65
+ With the starter, that is `GET /health` on port **6530** when `$routes` are wired.
66
+
67
+ </Step>
68
+
69
+ </Steps>
70
+
71
+ ## `createClient` forms
72
+
73
+ | Form | Types from | Wire |
74
+ | ------------------------------- | -------------------------------------- | ------------------------------------------------ |
75
+ | `createClient(app, url, opts?)` | `typeof app` | REST from `app.$routes`; untriggered flows → RPC |
76
+ | `createClient<App>(url, opts?)` | Explicit `App` type | RPC unless `opts.$routes` or `opts.routes` |
77
+ | `createClient(url, opts?)` | Ambient `Register` (`oke-client.d.ts`) | Same — pass routes for REST |
78
+
79
+ `oke dev` regenerates `oke-client.d.ts` from `GET /_oke/client.json`. A separate frontend repo
80
+ runs `oke client add <url>` (default out: `oke-client.d.ts`).
81
+
82
+ ```bash
83
+ oke client add http://localhost:6530
84
+ oke client add https://api.example.com --out ./types/oke-client.d.ts
85
+ ```
86
+
87
+ ## Options
88
+
89
+ | Option | Type | Default | Meaning |
90
+ | --------------- | ---------------------------------------------- | ------------------ | -------------------------------------------------------------- |
91
+ | `fetch` | `(input, init?) => Promise<Response>` | `globalThis.fetch` | Inject a fetch implementation |
92
+ | `headers` | `Record<string, string>` \| pairs \| `() => …` | — | Static headers, or a getter per request |
93
+ | `timeout` | `number` (ms) | — | Abort after this many milliseconds |
94
+ | `retry.retries` | `number` | `0` | Extra attempts after the first (network / 5xx) |
95
+ | `retry.delay` | `number` (ms) | `50` | Initial backoff delay |
96
+ | `retry.backoff` | `number` | `2` | Multiplier after each retry |
97
+ | `auth.getToken` | `() => string \| null \| …` | — | Bearer access token (or null) |
98
+ | `auth.refresh` | `() => Promise<string \| null \| …>` | — | Runs once on HTTP 401, then the request retries |
99
+ | `$routes` | `ClientRouteMap` | — | Runtime map from `app.$routes` (REST when method+path present) |
100
+ | `routes` | `Record<"unit.flow", { method, path }>` | — | Flat REST table; wins over flattening `$routes` |
101
+
102
+ **Consequence:** `createClient<App>(url)` alone types the proxy but still posts
103
+ `POST /_oke/{unit}/{flow}` until you pass the app value, `$routes`, or `routes`.
104
+
105
+ ## REST vs RPC
106
+
107
+ | Situation | Request |
108
+ | -------------------------------------------------- | -------------------------------------------------------------------------------------------------------- |
109
+ | HTTP trigger with method + path on `$routes` | That method and path (`:id` filled from input; leftover fields → query on GET/HEAD, JSON body otherwise) |
110
+ | Adopted flow with no HTTP trigger | `POST {base}/_oke/{unit}/{flow}` with JSON body |
111
+ | Incomplete proxy path (`api.notes()` with no flow) | Result error: `Incomplete path: api.notes(…)` |
112
+
113
+ ```typescript
114
+ // REST — createClient(app, url) saw method/path from on(http.get("/notes/:id"), …)
115
+ await api.notes.get({ id: "n_1" }); // GET /notes/n_1
116
+
117
+ // RPC — untriggered flow named notes.stats
118
+ await api.notes.stats({ id: "n_1" }); // POST /_oke/notes/stats
119
+ ```
120
+
121
+ ## Result envelope and helpers
122
+
123
+ Success may include optional top-level `meta` (for example pagination). Declared flow errors and
124
+ transport failures share the failure arm:
125
+
126
+ ```typescript
127
+ import { isOk, isFail, isErrorCode, isTransportError } from "okengine/client";
128
+
129
+ const result = await api.bookings.create({ flightId: "SK1", seats: 9 });
130
+
131
+ if (isOk(result)) {
132
+ result.data.id;
133
+ } else if (result.error.code === "FlightFull") {
134
+ result.error.data.seatsLeft; // narrowed
135
+ } else if (isTransportError(result.error)) {
136
+ result.error.data.message;
137
+ }
138
+
139
+ isFail(result); // true when error !== null
140
+ isErrorCode(result.error, "FlightFull"); // type predicate helper
141
+ ```
142
+
143
+ Prefer `error?.code === "FlightFull"` for inference; use `isErrorCode` in shared helpers.
144
+
145
+ ## Auth on the client
146
+
147
+ One client: `createClient`. With `gate.auth`, the app exposes `/auth/*` Flows
148
+ (sign-in, refresh, me). Helpers under `okengine/client/auth` store tokens — they are
149
+ **not** a second factory.
150
+
151
+ ```typescript
152
+ import { createClient } from "okengine/client";
153
+ import { memorySession } from "okengine/client/auth";
154
+ import { app } from "./app";
155
+
156
+ const session = memorySession();
157
+
158
+ const api = createClient(app, "http://localhost:6530", {
159
+ auth: {
160
+ getToken: () => session.getToken(),
161
+ refresh: () => session.refresh(api),
162
+ },
163
+ });
164
+
165
+ const { data } = await api.auth.signInEmail({ email, password });
166
+ if (data) session.set(data);
167
+ ```
168
+
169
+ React: `useSession(api, session)` from `okengine/client-react`.
170
+
171
+ | Step | What happens |
172
+ | ---------------------- | ---------------------------------------------------------------- |
173
+ | Every request | `getToken()` → `Authorization: Bearer …` when a token is present |
174
+ | HTTP **401** | `refresh()` runs **once**, then the same call retries |
175
+ | HTTP **403** / **429** | No refresh — decode the failure envelope as usual |
176
+
177
+ **Consequence:** `refresh` must mutate whatever `getToken` reads. Returning a new string alone does
178
+ nothing if storage was not updated.
179
+
180
+ After a gated call, switch on the denial codes (values, not throws):
181
+
182
+ | Code | HTTP | `error.data` | Typical fix |
183
+ | -------------- | ---- | ------------------ | ------------------------------------------------------------ |
184
+ | `Unauthorized` | 401 | `{}` | Sign in, or let `auth.refresh` run; re-login if still denied |
185
+ | `Forbidden` | 403 | `{ gate, reason }` | Wrong scopes / policy — show denied |
186
+ | `RateLimited` | 429 | `{ retryAfterMs }` | Wait `retryAfterMs` before retrying |
187
+
188
+ These gate codes are **not** listed in each Flow’s `errors` map — they can appear on any gated
189
+ route.
190
+
191
+ A 401 with no `{ data, error }` body becomes `TransportError` with `data.status: 401`.
192
+
193
+ | Helper | Package | Role |
194
+ | --------------------------- | ----------------------- | ------------------------------------------------ |
195
+ | `memorySession` | `okengine/client/auth` | In-memory access/refresh bag for `auth.getToken` |
196
+ | `AUTH_ERROR_CODES` | `okengine/client/auth` | Common auth Flow / gate codes |
197
+ | `useSession(api, session?)` | `okengine/client-react` | React status + `auth.me` |
198
+
199
+ Core `okengine/client` stays under the size budget — helpers are separate exports. Not in
200
+ core today: cookie jars or plugin `.client()` decorations. Browser apps: also see
201
+ [CORS](/docs/plugins/cors) and [CSRF](/docs/plugins/csrf).
202
+
203
+ ## Elements from the client
204
+
205
+ <Callout title="Flows only">
206
+ The client calls **Flows**. Every other element runs on the server through `fx`. You reach its
207
+ outcome by calling a Flow that uses it — or by handling a gate denial on that call.
208
+ </Callout>
209
+
210
+ | Element | On the client | How |
211
+ | --------------------------------- | ------------- | --------------------------------------------------------------------------------------- |
212
+ | [Flow](/docs/elements/flow) | Direct | `api.unit.flow(input)` — the only public surface |
213
+ | [Gate](/docs/elements/gate) | Indirect | Bearer via `auth`; denials as `Unauthorized` / `Forbidden` / `RateLimited` |
214
+ | [Store](/docs/elements/store) | Via Flows | `fx.store` inside Flows; `store.resource` + `on(http.resource…)` → five Flows you adopt |
215
+ | [Signal](/docs/elements/signal) | Via Flows | Emit/consume server-side; no subscribe API on `okengine/client` yet |
216
+ | [Clock](/docs/elements/clock) | Via Flows | Schedules fire on the server — the client never ticks a clock |
217
+ | [Vault](/docs/elements/vault) | Via Flows | Secrets stay server-side; never ship them to the browser package |
218
+ | [Channel](/docs/elements/channel) | Via Flows | `fx.send` in a Flow — the client does not send email/SMS/push |
219
+ | [AI](/docs/elements/ai) | Via Flows | `fx.ask` / `fx.run` inside a Flow; the client gets that Flow’s `out` |
220
+
221
+ ### Store resources
222
+
223
+ Mount a resource, adopt the returned ops, then call the five Flows like any other:
224
+
225
+ ```typescript
226
+ const notesR = store.resource(db, notes, {/* in, out, list, unit: "notes" */});
227
+ const mounted = on(http.resource("/notes", notesR.all()));
228
+ // .adopt({ notes: mounted }) →
229
+ await api.notes.list({ limit: 20 }); // GET /notes?limit=20 — meta may carry nextCursor
230
+ await api.notes.get({ id }); // GET /notes/:id — NotFound when missing
231
+ await api.notes.remove({ id }); // DELETE → 204, data undefined
232
+ ```
233
+
234
+ See [Store](/docs/elements/store) for the list query language and schemas. Auth posture for HTTP
235
+ triggers is covered under [Gate](/docs/elements/gate).
236
+
237
+ ### Signal and live queries
238
+
239
+ `delivery: "live"` and `http.get(…).live()` are Manifest / driver flags today. `okengine/client`
240
+ does **not** expose WebSocket, SSE, or `api.*.subscribe`. Until that ships, poll or call an HTTP
241
+ Flow that returns the current state.
242
+
243
+ ## Exports
244
+
245
+ | Export | Kind | Role |
246
+ | -------------------------------------------- | --------- | ------------------------------------------------- |
247
+ | `createClient` | function | Typed proxy `api.unit.flow(input?)` |
248
+ | `flattenRoutes` | function | `$routes` → flat `unit.flow` REST table |
249
+ | `createTransport` | function | Low-level HTTP transport (timeout / retry / auth) |
250
+ | `isOk` / `isFail` | function | Envelope predicates |
251
+ | `isErrorCode` / `isTransportError` | function | Error narrowing |
252
+ | `Client`, `ClientOptions`, `ClientResult`, … | types | Contracts and options |
253
+ | `Register` | interface | Module-augmentation slot for ambient App types |
254
+ | `AppOf` | type | Brand a bare route map as an App |
255
+
256
+ Budget: the `./client` export stays under the measured client-runtime cap (hard gate in CI).
257
+
258
+ ## Troubleshooting
259
+
260
+ <Accordions>
261
+
262
+ <Accordion title="api.main.health is not a function / type error">
263
+
264
+ Confirm the flow is `export`ed from the module you `.adopt({ main })`, and that `createClient` is
265
+ typed with that `App` (or ambient `Register` after `oke-client.d.ts` regenerates). Restart
266
+ `oke dev` after renaming exports.
267
+
268
+ </Accordion>
269
+
270
+ <Accordion title="Calls hit /_oke/… instead of my HTTP path">
271
+
272
+ Types alone do not choose REST. Pass `createClient(app, url)`, or
273
+ `createClient(url, { $routes: app.$routes })`, or an explicit `routes` map.
274
+
275
+ </Accordion>
276
+
277
+ <Accordion title='error.code is "TransportError"'>
278
+
279
+ Network failure, abort (`timeout`), non-JSON body, empty error response, or HTTP status without a
280
+ `{ data, error }` envelope. Declared flow codes (`NotFound`, `FlightFull`, …) never use this code.
281
+ Message text lives in `error.data.message`; HTTP status may appear as `error.data.status`.
282
+
283
+ </Accordion>
284
+
285
+ <Accordion title="401 loops or refresh never sticks">
286
+
287
+ `auth.refresh` runs once per call on HTTP 401. It must update the store `getToken` reads — the
288
+ return value is ignored. With `gate.auth`, `POST /auth/refresh` is built in; `memorySession.refresh(api)`
289
+ calls `api.auth.refresh({ refreshToken })`. Re-login when rotation fails or no refresh token remains.
290
+
291
+ </Accordion>
292
+
293
+ <Accordion title="Failed to fetch …/_oke/client.json">
294
+
295
+ `oke client add` needs a running app that serves the descriptor. Start the app (`oke dev` /
296
+ `oke start`), check the URL, then retry. Usage when the URL is missing:
297
+ `Usage: oke client add <url> [--out oke-client.d.ts]`.
298
+
299
+ </Accordion>
300
+
301
+ </Accordions>
302
+
303
+ ## Learn more
304
+
305
+ - [Basic usage](/docs/get-started/basic-usage) — adopt → client → test loop
306
+ - [Gate](/docs/elements/gate) — policies, `gate.public`, denials
307
+ - [Store](/docs/elements/store) — `store.resource` and list query language
308
+ - [Flow](/docs/elements/flow) — `in` / `out` / `errors` and `fx.fail`
309
+ - [Errors](/docs/reference/errors) — framework codes vs failure values
310
+ - [CORS](/docs/plugins/cors) · [CSRF](/docs/plugins/csrf) — browser callers
311
+ - [CLI Reference](/docs/reference/cli) — `oke client add`, `oke dev`
312
+
313
+ ## Next
314
+
315
+ <Cards>
316
+ <Card
317
+ title="Gate"
318
+ description="Auth policies and rate limits before any effect."
319
+ href="/docs/elements/gate"
320
+ />
321
+ <Card
322
+ title="Store"
323
+ description="Resources that become five typed client Flows."
324
+ href="/docs/elements/store"
325
+ />
326
+ <Card
327
+ title="Errors"
328
+ description="OKE codes, denials, and failure values."
329
+ href="/docs/reference/errors"
330
+ />
331
+ </Cards>
@@ -48,6 +48,10 @@ See [Store](/docs/elements/store) for the query-builder surface.
48
48
  | `fx.signal` | — | Ambient `AbortSignal` for the current branch |
49
49
  | `fx.fail(code, data, opts?)` | — | Typed failure value (`opts.message` overrides) |
50
50
 
51
+ `fx.call` starts the callee with an **empty** `fx.auth` (fail-closed for authorization). For
52
+ audit/attribution only, read `fx.principal` — it propagates the originating identity without
53
+ copying into `fx.auth`. Gates never consult `fx.principal`.
54
+
51
55
  ## Concurrency and retry
52
56
 
53
57
  Pass **thunks** to `all` / `race` — not already-started Promises — so each branch gets an abort scope before work begins.
@@ -157,11 +161,22 @@ Returning a plain value instead answers 200 with `{ data: value, error: null }`
157
161
 
158
162
  ## Principals
159
163
 
160
- | Property | Shape |
161
- | ------------- | --------------------------------------------------------------------- |
162
- | `fx.auth` | `{ userId: string \| null, scopes: Set<string>, verified?: boolean }` |
163
- | `fx.operator` | `{ id: string \| null }` — Console plane |
164
- | `fx.tenant` | `{ id: string \| null }` active tenant |
164
+ | Property | Shape |
165
+ | -------------- | ------------------------------------------------------------------------- |
166
+ | `fx.auth` | `{ userId: string \| null, scopes: Set<string>, verified?: boolean }` |
167
+ | `fx.operator` | `{ id: string \| null }` — Console plane |
168
+ | `fx.principal` | Read-only origin: `userId`, `operatorId`, `scopes`, `verified?`, `plane?` |
169
+ | `fx.tenant` | `{ id: string \| null }` — active tenant |
170
+
171
+ **Consequence:** use `fx.auth` / gates for authorization; use `fx.principal` only when a callee
172
+ must log who started the call chain.
173
+
174
+ ## Not on `fx`
175
+
176
+ <Callout title="No fx.metric">
177
+ Investigated and declined. [Runs](/docs/console/runs) already own per-invocation observability as
178
+ wide events; a parallel counter/gauge API would be a second instrumentation path without a sink.
179
+ </Callout>
165
180
 
166
181
  ## Learn more
167
182
 
@@ -0,0 +1,45 @@
1
+ ---
2
+ title: "Reference"
3
+ description: "Lookup pages — config, fx, client, env vars, errors, plugins API, CLI, and Console security."
4
+ icon: "BookMarked"
5
+ source: "docs/spec/unified-theory.md"
6
+ ---
7
+
8
+ Dense tables and command lists. Reach for these when you already know what you are looking for.
9
+
10
+ ## Pages
11
+
12
+ <Cards>
13
+ <Card
14
+ title="Configuration"
15
+ description="Every option in oke.config.ts."
16
+ href="/docs/reference/configuration"
17
+ />
18
+ <Card title="fx" description="The complete fx surface and effects." href="/docs/reference/fx" />
19
+ <Card
20
+ title="Client"
21
+ description="Typed createClient — REST, RPC, envelope helpers."
22
+ href="/docs/reference/client"
23
+ />
24
+ <Card
25
+ title="Environment Variables"
26
+ description="Every variable OKE reads."
27
+ href="/docs/reference/environment-variables"
28
+ />
29
+ <Card title="Errors" description="OKE codes, denials, fixes." href="/docs/reference/errors" />
30
+ <Card
31
+ title="Plugins"
32
+ description="Plugin API — hooks, schemas, identity."
33
+ href="/docs/reference/plugins"
34
+ />
35
+ <Card
36
+ title="CLI Reference"
37
+ description="oke and create-oke commands."
38
+ href="/docs/reference/cli"
39
+ />
40
+ <Card
41
+ title="Security"
42
+ description="Console Host, Origin, MCP posture."
43
+ href="/docs/reference/security"
44
+ />
45
+ </Cards>
@@ -1,5 +1,15 @@
1
1
  {
2
2
  "title": "Reference",
3
3
  "icon": "BookMarked",
4
- "pages": ["configuration", "fx", "environment-variables", "errors", "plugins", "cli", "security"]
4
+ "pages": [
5
+ "index",
6
+ "configuration",
7
+ "fx",
8
+ "client",
9
+ "environment-variables",
10
+ "errors",
11
+ "plugins",
12
+ "cli",
13
+ "security"
14
+ ]
5
15
  }
@@ -66,20 +66,26 @@ Every method below exists on both the fluent definition and the boot-time builde
66
66
 
67
67
  | Method | Contributes |
68
68
  | -------------------------------- | --------------------------------------------------------------------------------------------- |
69
- | `.flow(def)` | An ordinary flow — Manifest, Console, client types |
69
+ | `.flow(def)` | An ordinary flow — Manifest / Console metadata (does **not** join the HTTP router alone) |
70
+ | `.binding({ trigger, flow })` | A real Binding — joins `adopted` + the router on `app.plug()` (auth method plugins) |
70
71
  | `.hook(stage, fn)` | A per-request intercept at one pipeline stage |
71
72
  | `.edge(fn)` | A handler for HTTP requests that match **no** flow |
72
73
  | `.decorate(key, value)` | A typed context decoration, visible to flows |
73
- | `.element({ kind, name })` | An element contribution (e.g. `store.sql` facet) |
74
+ | `.element({ kind, name })` | An opaque element contribution (e.g. `store.sql` facet) |
75
+ | `.vault(secret)` | A vault secret/config contract — merged into boot secrets |
76
+ | `.clock(decl)` | A named clock schedule — merged into boot clocks |
77
+ | `.signal(decl)` | A signal declaration — merged into boot signals |
78
+ | `.gate(decl)` | A gate declaration — merged into boot gates |
79
+ | `.channelTemplate(decl)` | A channel template — merged into boot channel templates |
74
80
  | `.driver(id, impl)` | A protocol-named driver for an existing element |
75
81
  | `.image(role, recipe)` | An image recipe for a docker role |
76
82
  | `.table(name, columns, options)` | A whole DB table, merged into the generated schema (`options.description` / `plane` optional) |
77
83
  | `.errors(map)` | Typed errors flows can fail with |
78
- | `.client(name, ext)` | A typed client extension |
84
+ | `.client(name, ext)` | A typed client extension (reserved seam — not yet merged into `createClient`) |
79
85
  | `.consolePanel(panel)` | A Console panel (ESM entry loaded at runtime) |
80
86
  | `.cli(name, handler)` | An `oke <name>` CLI command |
81
87
  | `.config(schema)` | A config schema; values live on the plugin identity |
82
- | `.needs(dep)` | A declared dependency (e.g. `"store.kv"`) |
88
+ | `.needs(dep)` | Runtime dependency plugin name or element/driver id; unmet → `PluginNeedsError` at boot |
83
89
 
84
90
  New infrastructure is a **driver** for an existing element, never a ninth element — plugins follow the same law.
85
91
 
@@ -202,13 +208,13 @@ Extending an existing **app-owned** table with plugin columns is not supported i
202
208
 
203
209
  ## Identity, config, and dependencies
204
210
 
205
- | Concept | Rule |
206
- | ---------- | ------------------------------------------------------------------------------ |
207
- | Name | Stable plugin id — the Manifest key and conflict namespace |
208
- | `version` | Semver string recorded in the Manifest |
209
- | `config` | Snapshot for identity dedup: same name + same config → no-op re-plug |
210
- | Conflict | Same name + **different** config → loud boot error, never a silent merge |
211
- | `.needs()` | Declares runtime dependencies so boot fails early when a capability is missing |
211
+ | Concept | Rule |
212
+ | ---------- | -------------------------------------------------------------------------------------------- |
213
+ | Name | Stable plugin id — the Manifest key and conflict namespace |
214
+ | `version` | Semver string recorded in the Manifest |
215
+ | `config` | Snapshot for identity dedup: same name + same config → no-op re-plug |
216
+ | Conflict | Same name + **different** config → loud boot error, never a silent merge |
217
+ | `.needs()` | Declares runtime dependencies (plugin name or element/driver id); unmet `PluginNeedsError` |
212
218
 
213
219
  ## Troubleshooting
214
220
 
@@ -227,11 +233,16 @@ Extending an existing **app-owned** table with plugin columns is not supported i
227
233
  table above, and confirm the flow you expect actually reaches that stage (a gate denial never
228
234
  reaches `beforeHandle`).
229
235
  </Accordion>
236
+ <Accordion title="Boot: plugin boot failed — unmet .needs() dependencies">
237
+ A plugged plugin declared `.needs("auth")` or `.needs("store.sql")` (or another token) and
238
+ nothing satisfied it. For `"auth"`, enable `oke({ gate: { auth } })`. Otherwise plug the peer
239
+ plugin, or ensure the element/driver is available (tables imply `store.sql`, and so on).
240
+ </Accordion>
230
241
  </Accordions>
231
242
 
232
243
  ## Learn more
233
244
 
234
- - [Security Headers](/docs/plugins/security-headers) · [CORS](/docs/plugins/cors) · [CSRF](/docs/plugins/csrf) · [Compression](/docs/plugins/compression) · [Maintenance Mode](/docs/plugins/maintenance-mode) · [IP Allowlist](/docs/plugins/ip-allowlist) — first-party plugins built on exactly this API
245
+ - [Plugins](/docs/plugins) — [username](/docs/plugins/username) · [anonymous](/docs/plugins/anonymous) · [magic link](/docs/plugins/magic-link) · [email OTP](/docs/plugins/email-otp) · [phone](/docs/plugins/phone-number) · [two-factor](/docs/plugins/two-factor) · [passkey](/docs/plugins/passkey) · [Headers](/docs/plugins/headers) · [CORS](/docs/plugins/cors) · [CSRF](/docs/plugins/csrf) · [Compression](/docs/plugins/compression) · [Maintenance Mode](/docs/plugins/maintenance-mode) · [IP Allowlist](/docs/plugins/ip-allowlist)
235
246
  - [Flow](/docs/elements/flow) — what plugin flows and hooks plug into
236
247
  - [Store](/docs/elements/store) — `field.*` builders and schema sync
237
248
  - [Configuration](/docs/reference/configuration) — where plugin config is declared
@@ -240,9 +251,9 @@ Extending an existing **app-owned** table with plugin columns is not supported i
240
251
 
241
252
  <Cards>
242
253
  <Card
243
- title="Security Headers"
254
+ title="Headers"
244
255
  description="The full secure-headers set on every response."
245
- href="/docs/plugins/security-headers"
256
+ href="/docs/plugins/headers"
246
257
  />
247
258
  <Card title="CORS" description="Cross-origin rules at the edge." href="/docs/plugins/cors" />
248
259
  <Card title="Flow" description="Triggers, effects, and durability." href="/docs/elements/flow" />
@@ -75,6 +75,9 @@ describe("auth plugin", () => {
75
75
  tables.push(name);
76
76
  return this;
77
77
  },
78
+ binding() {
79
+ return this;
80
+ },
78
81
  flow() {
79
82
  return this;
80
83
  },
@@ -84,6 +87,21 @@ describe("auth plugin", () => {
84
87
  config() {
85
88
  return this;
86
89
  },
90
+ vault() {
91
+ return this;
92
+ },
93
+ clock() {
94
+ return this;
95
+ },
96
+ signal() {
97
+ return this;
98
+ },
99
+ gate() {
100
+ return this;
101
+ },
102
+ channelTemplate() {
103
+ return this;
104
+ },
87
105
  });
88
106
  expect(tables).toContain("oke_operators");
89
107
  expect(tables).toContain("oke_operator_credentials");
@@ -218,7 +236,7 @@ describe("operator plane", () => {
218
236
  const op = await createOperator(store, {
219
237
  email: "ops@example.com",
220
238
  name: "Ops",
221
- password: "correct horse battery staple",
239
+ password: "correct horse battery staple 1",
222
240
  });
223
241
 
224
242
  expect(store.credentials.has(op.id)).toBe(true);
@@ -231,7 +249,7 @@ describe("operator plane", () => {
231
249
  const authed = await authenticateOperator(
232
250
  store,
233
251
  "ops@example.com",
234
- "correct horse battery staple",
252
+ "correct horse battery staple 1",
235
253
  );
236
254
  expect(authed?.id).toBe(op.id);
237
255
  });