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.
- package/package.json +3 -1
- package/site/content/docs/ai/index.mdx +24 -0
- package/site/content/docs/ai/llms-txt.mdx +3 -0
- package/site/content/docs/ai/meta.json +1 -1
- package/site/content/docs/console/gates.mdx +46 -8
- package/site/content/docs/console/index.mdx +54 -0
- package/site/content/docs/console/meta.json +1 -0
- package/site/content/docs/elements/gate.mdx +187 -48
- package/site/content/docs/elements/index.mdx +45 -0
- package/site/content/docs/elements/meta.json +1 -1
- package/site/content/docs/get-started/basic-usage.mdx +4 -3
- package/site/content/docs/get-started/index.mdx +33 -0
- package/site/content/docs/get-started/meta.json +1 -1
- package/site/content/docs/index.mdx +9 -31
- package/site/content/docs/plugins/anonymous.mdx +95 -0
- package/site/content/docs/plugins/compression.mdx +2 -2
- package/site/content/docs/plugins/cors.mdx +2 -2
- package/site/content/docs/plugins/csrf.mdx +2 -2
- package/site/content/docs/plugins/email-otp.mdx +111 -0
- package/site/content/docs/plugins/{security-headers.mdx → headers.mdx} +1 -1
- package/site/content/docs/plugins/index.mdx +69 -0
- package/site/content/docs/plugins/magic-link.mdx +112 -0
- package/site/content/docs/plugins/maintenance-mode.mdx +8 -8
- package/site/content/docs/plugins/meta.json +10 -1
- package/site/content/docs/plugins/passkey.mdx +128 -0
- package/site/content/docs/plugins/phone-number.mdx +111 -0
- package/site/content/docs/plugins/two-factor.mdx +116 -0
- package/site/content/docs/plugins/username.mdx +117 -0
- package/site/content/docs/reference/client.mdx +331 -0
- package/site/content/docs/reference/fx.mdx +20 -5
- package/site/content/docs/reference/index.mdx +45 -0
- package/site/content/docs/reference/meta.json +11 -1
- package/site/content/docs/reference/plugins.mdx +25 -14
- package/src/auth/auth.test.ts +20 -2
- package/src/auth/bindings.ts +439 -0
- package/src/auth/breach-check.ts +112 -0
- package/src/auth/config.ts +288 -0
- package/src/auth/cookies.ts +123 -0
- package/src/auth/gate-auth.test.ts +379 -0
- package/src/auth/identity.ts +190 -0
- package/src/auth/index.ts +117 -1
- package/src/auth/method-context.ts +33 -0
- package/src/auth/operator.ts +27 -1
- package/src/auth/password-policy.test.ts +126 -0
- package/src/auth/password-policy.ts +77 -0
- package/src/auth/plugin.ts +62 -4
- package/src/auth/rate.ts +45 -0
- package/src/auth/schema.ts +260 -0
- package/src/auth/secondary-storage.ts +37 -0
- package/src/auth/sessions.ts +58 -1
- package/src/auth/tables.ts +4 -0
- package/src/auth/verification.ts +78 -0
- package/src/cli/dev.test.ts +3 -3
- package/src/cli/schema.ts +95 -23
- package/src/client/auth.ts +120 -0
- package/src/client-react/index.ts +93 -0
- package/src/compiler/aot.test.ts +2 -1
- package/src/compiler/extract.ts +19 -0
- package/src/compiler/response.ts +16 -2
- package/src/console/server/app.ts +10 -6
- package/src/console/server/auth-rate.test.ts +3 -3
- package/src/console/server/bind.ts +12 -1
- package/src/console/server/channels.test.ts +1 -1
- package/src/console/server/console-gates.ts +14 -0
- package/src/console/server/console.test.ts +6 -6
- package/src/console/server/flows-invoke.test.ts +2 -2
- package/src/console/server/flows.ts +2 -0
- package/src/console/server/gates.ts +8 -1
- package/src/console/server/operator-db.test.ts +4 -4
- package/src/console/server/operator-db.ts +22 -4
- package/src/console/server/security.gate.test.ts +3 -3
- package/src/console/ui/gates/fixture.ts +4 -0
- package/src/console/ui/gates/types.ts +2 -0
- package/src/console/ui/shell/client.ts +1 -0
- package/src/elements/gate/boot.ts +136 -0
- package/src/elements/gate/config.ts +69 -0
- package/src/elements/gate/declare.ts +51 -1
- package/src/elements/gate/runtime.ts +3 -1
- package/src/elements/gate.test.ts +77 -0
- package/src/elements/gate.ts +20 -1
- package/src/elements/index.ts +8 -0
- package/src/index.ts +11 -0
- package/src/kernel/app.ts +253 -32
- package/src/kernel/boot.test.ts +40 -3
- package/src/kernel/boot.ts +8 -0
- package/src/kernel/call.test.ts +46 -2
- package/src/kernel/edge.test.ts +3 -3
- package/src/kernel/flow.test.ts +2 -2
- package/src/kernel/fx.test.ts +1 -0
- package/src/kernel/fx.ts +60 -0
- package/src/kernel/hooks.test.ts +4 -4
- package/src/kernel/index.ts +12 -0
- package/src/kernel/pipeline.test.ts +12 -8
- package/src/kernel/pipeline.ts +23 -4
- package/src/kernel/plugin/decorate.test.ts +3 -3
- package/src/kernel/plugin/scoping.test.ts +3 -3
- package/src/kernel/plugin-elements.test.ts +51 -0
- package/src/kernel/plugin-needs.test.ts +83 -0
- package/src/kernel/plugin-needs.ts +129 -0
- package/src/kernel/plugin.ts +101 -0
- package/src/kernel/registry-isolation.test.ts +5 -5
- package/src/kernel/registry.ts +102 -3
- package/src/manifest/types.ts +2 -0
- package/src/plugins/anonymous.ts +58 -0
- package/src/plugins/auth/shared.ts +121 -0
- package/src/plugins/auth-methods.test.ts +176 -0
- package/src/plugins/compression.test.ts +5 -5
- package/src/plugins/config-source.test.ts +1 -1
- package/src/plugins/cors.test.ts +16 -10
- package/src/plugins/csrf.test.ts +1 -1
- package/src/plugins/email-otp.ts +161 -0
- package/src/plugins/index.ts +31 -0
- package/src/plugins/ip-allowlist.test.ts +19 -9
- package/src/plugins/magic-link.ts +163 -0
- package/src/plugins/maintenance-mode.test.ts +9 -5
- package/src/plugins/passkey.ts +216 -0
- package/src/plugins/phone-number.ts +149 -0
- package/src/plugins/security-headers.test.ts +14 -14
- package/src/plugins/two-factor.ts +249 -0
- package/src/plugins/username.ts +148 -0
- package/src/runs/runs.test.ts +6 -2
- package/src/runtime/primitives.ts +37 -4
- package/src/runtime/serve.test.ts +3 -2
- package/src/runtime/types.ts +25 -2
- package/src/test/create-test-app.test.ts +1 -1
- package/src/test/create-test-app.ts +4 -1
- 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
|
|
161
|
-
|
|
|
162
|
-
| `fx.auth`
|
|
163
|
-
| `fx.operator`
|
|
164
|
-
| `fx.
|
|
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": [
|
|
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
|
|
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)` |
|
|
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
|
|
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
|
-
- [
|
|
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="
|
|
254
|
+
title="Headers"
|
|
244
255
|
description="The full secure-headers set on every response."
|
|
245
|
-
href="/docs/plugins/
|
|
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" />
|
package/src/auth/auth.test.ts
CHANGED
|
@@ -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
|
});
|