okengine 0.15.2 → 0.17.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/manifest.v1.schema.json +21 -2
- package/package.json +1 -1
- package/site/content/docs/elements/clock.mdx +7 -0
- package/site/content/docs/elements/flow.mdx +20 -12
- package/site/content/docs/elements/gate.mdx +108 -3
- package/site/content/docs/elements/signal.mdx +46 -17
- package/site/content/docs/elements/store.mdx +36 -32
- package/site/content/docs/elements/vault.mdx +26 -0
- package/site/content/docs/get-started/project-structure.mdx +4 -4
- package/site/content/docs/reference/cli.md +1 -1
- package/site/content/docs/reference/client.mdx +75 -17
- package/site/content/docs/reference/configuration.mdx +7 -4
- package/site/content/docs/reference/errors.mdx +24 -17
- package/site/content/docs/reference/fx.mdx +39 -22
- package/site/content/docs/reference/security.md +1 -1
- package/src/auth/api-key-sql.test.ts +47 -0
- package/src/auth/api-key-sql.ts +261 -0
- package/src/auth/api-keys.ts +276 -14
- package/src/auth/auth.test.ts +56 -0
- package/src/auth/config.ts +24 -5
- package/src/auth/index.ts +62 -1
- package/src/auth/plugin.ts +6 -1
- package/src/auth/schema.ts +8 -0
- package/src/auth/sessions.ts +18 -0
- package/src/auth/tables.ts +32 -0
- package/src/auth/tenant-config.ts +74 -0
- package/src/auth/tenant-tables.ts +11 -0
- package/src/auth/tenants.test.ts +63 -0
- package/src/auth/tenants.ts +360 -0
- package/src/cli/attach-host-console.test.ts +37 -0
- package/src/cli/attach-host-console.ts +6 -0
- package/src/cli/build.ts +2 -2
- package/src/client/budget.test.ts +1 -1
- package/src/client/create.ts +75 -5
- package/src/client/index.ts +13 -0
- package/src/client/live.test.ts +422 -0
- package/src/client/live.ts +389 -0
- package/src/client/notes-contract.test.ts +41 -0
- package/src/client/types.ts +85 -6
- package/src/client-react/index.ts +85 -1
- package/src/client-react/use-live.test.ts +129 -0
- package/src/compiler/effects-infer.ts +37 -2
- package/src/compiler/extract.test.ts +143 -11
- package/src/compiler/extract.ts +210 -30
- package/src/compiler/fixtures/skyport/src/flows/bookings/index.ts +1 -2
- package/src/compiler/fixtures/skyport.expected.json +2 -3
- package/src/compiler/response.ts +41 -11
- package/src/console/server/access.test.ts +116 -8
- package/src/console/server/access.ts +70 -23
- package/src/console/server/app.ts +45 -11
- package/src/console/server/console.test.ts +6 -8
- package/src/console/server/dev-identities.test.ts +127 -0
- package/src/console/server/dev-identities.ts +229 -0
- package/src/console/server/flows-invoke.test.ts +21 -4
- package/src/console/server/flows.ts +113 -58
- package/src/console/server/gates.test.ts +24 -0
- package/src/console/server/gates.ts +10 -30
- package/src/console/server/index.ts +1 -0
- package/src/console/server/live.test.ts +26 -1
- package/src/console/server/live.ts +4 -2
- package/src/console/server/operator-db.ts +2 -0
- package/src/console/server/state.ts +29 -55
- package/src/console/server/store.test.ts +17 -0
- package/src/console/server/store.ts +43 -1
- package/src/console/ui-next/dist/assets/access-page-CXVWWMmD.js +4 -0
- package/src/console/ui-next/dist/assets/{agent-disclosure-C0X1fbWF.js → agent-disclosure-BP0Y0Sux.js} +1 -1
- package/src/console/ui-next/dist/assets/cache-glyph-B5X-NM0-.js +1 -0
- package/src/console/ui-next/dist/assets/call-pii-button-ChAUiLo9.js +1 -0
- package/src/console/ui-next/dist/assets/collapsible-DGnOM2ph.js +1 -0
- package/src/console/ui-next/dist/assets/copy-inline-button-DBHhgHKP.js +1 -0
- package/src/console/ui-next/dist/assets/detail-header-DhHM1iaZ.js +1 -0
- package/src/console/ui-next/dist/assets/dropdown-menu-_NjTEo5_.js +1 -0
- package/src/console/ui-next/dist/assets/element-icons-BVXtRyd3.js +1 -0
- package/src/console/ui-next/dist/assets/explorer-empty-2uIhBu0_.js +1 -0
- package/src/console/ui-next/dist/assets/flows-page-XDpAJO8f.js +1 -0
- package/src/console/ui-next/dist/assets/{highlighted-json-CS_O8L-r.js → highlighted-json-Awq7gYdu.js} +1 -1
- package/src/console/ui-next/dist/assets/http-method-_UHM2ODJ.js +1 -0
- package/src/console/ui-next/dist/assets/index-BBj2QJCu.js +66 -0
- package/src/console/ui-next/dist/assets/index-Ck88Jmv8.css +2 -0
- package/src/console/ui-next/dist/assets/link-BX6Vqztd.js +1 -0
- package/src/console/ui-next/dist/assets/observability-page-BFaay44m.js +4 -0
- package/src/console/ui-next/dist/assets/replica-lag-DKRrbvdo.js +18 -0
- package/src/console/ui-next/dist/assets/request-meta-DV0ywz7t.js +1 -0
- package/src/console/ui-next/dist/assets/shortcut-JQIZlWfm.js +1 -0
- package/src/console/ui-next/dist/assets/shortcut-keys-DKxNTe_m.js +1 -0
- package/src/console/ui-next/dist/assets/skeleton-D-czQJT6.js +1 -0
- package/src/console/ui-next/dist/assets/store-page-CS5-aETQ.js +41 -0
- package/src/console/ui-next/dist/assets/trace-detail-sheet-B09O8rA6.js +2 -0
- package/src/console/ui-next/dist/assets/tree-expand-toggle-BtyhmWb4.js +55 -0
- package/src/console/ui-next/dist/assets/units-page-Ca2Z-E52.js +1 -0
- package/src/console/ui-next/dist/assets/{use-vault-list-BLbZhzyF.js → use-vault-list-uk4WVboC.js} +1 -1
- package/src/console/ui-next/dist/assets/useMutation-B8EO02Ej.js +1 -0
- package/src/console/ui-next/dist/assets/useRender-BE2A9BWC.js +1 -0
- package/src/console/ui-next/dist/assets/vault-page-BmOeAFwg.js +2 -0
- package/src/console/ui-next/dist/index.html +11 -9
- package/src/console/ui-next/index.html +3 -3
- package/src/console/ui-next/src/client.ts +167 -0
- package/src/console/ui-next/src/components/shell/app-sidebar.tsx +2 -0
- package/src/console/ui-next/src/components/shell/command-items.test.ts +1 -0
- package/src/console/ui-next/src/components/shell/command-items.ts +8 -0
- package/src/console/ui-next/src/components/shell/shell-layout.tsx +2 -1
- package/src/console/ui-next/src/components/ui/sheet-form.tsx +8 -3
- package/src/console/ui-next/src/features/access/access-page.tsx +347 -0
- package/src/console/ui-next/src/features/access/data/use-access.ts +23 -0
- package/src/console/ui-next/src/features/access/detail/access-detail.tsx +552 -0
- package/src/console/ui-next/src/features/access/explorer/access-list.tsx +125 -0
- package/src/console/ui-next/src/features/access/lib/format-when.test.ts +139 -0
- package/src/console/ui-next/src/features/access/lib/format-when.ts +296 -0
- package/src/console/ui-next/src/features/access/lib/scope-groups.test.ts +34 -0
- package/src/console/ui-next/src/features/access/lib/scope-groups.ts +40 -0
- package/src/console/ui-next/src/features/access/sheets/access-allow-fields.tsx +236 -0
- package/src/console/ui-next/src/features/access/sheets/access-create-sheet.tsx +316 -0
- package/src/console/ui-next/src/features/access/sheets/access-edit-sheet.tsx +240 -0
- package/src/console/ui-next/src/features/access/sheets/access-expiry-fields.tsx +66 -0
- package/src/console/ui-next/src/features/access/sheets/access-refresh-sheet.tsx +104 -0
- package/src/console/ui-next/src/features/access/sheets/access-scope-field.tsx +115 -0
- package/src/console/ui-next/src/features/access/sheets/access-secret-sheet.tsx +89 -0
- package/src/console/ui-next/src/features/access/sheets/access-sheet-search.tsx +49 -0
- package/src/console/ui-next/src/features/access/state/access-selection.ts +84 -0
- package/src/console/ui-next/src/features/auth/auth-redirect.test.ts +5 -0
- package/src/console/ui-next/src/features/auth/auth-redirect.ts +18 -2
- package/src/console/ui-next/src/features/flows/fixture.ts +0 -1
- package/src/console/ui-next/src/features/not-found/not-found-page.tsx +1 -1
- package/src/console/ui-next/src/features/units/detail/flow-contract-panel.tsx +5 -1
- package/src/console/ui-next/src/features/units/lib/unit-tree.test.ts +15 -4
- package/src/console/ui-next/src/lib/document-meta.test.ts +1 -0
- package/src/console/ui-next/src/lib/document-meta.ts +2 -1
- package/src/console/ui-next/src/lib/last-module-search.test.ts +1 -0
- package/src/console/ui-next/src/lib/last-module-search.ts +2 -1
- package/src/console/ui-next/src/lib/shortcut.test.ts +2 -0
- package/src/console/ui-next/src/lib/shortcut.ts +3 -0
- package/src/console/ui-next/src/router.tsx +10 -1
- package/src/console/ui-next/ui-next-seed-app-schema.ts +88 -37
- package/src/console/ui-next/ui-next-seed-manifest-surface.ts +2 -9
- package/src/console/ui-next/ui-next-seed-manifest.ts +0 -1
- package/src/console/ui-next/ui-next-seed-store.ts +43 -15
- package/src/console/xss.gate.test.ts +1 -0
- package/src/drivers/index.ts +2 -0
- package/src/drivers/journal-postgres.ts +12 -3
- package/src/drivers/pg-rls.ts +26 -2
- package/src/drivers/pg-vault-rls.ts +68 -0
- package/src/drivers/signal-engine.ts +69 -15
- package/src/drivers/signal-live-iter.ts +65 -0
- package/src/drivers/signal-nats.ts +2 -1
- package/src/drivers/signal-postgres.ts +95 -12
- package/src/drivers/signal-redis.ts +2 -1
- package/src/drivers/signal-retention.ts +64 -0
- package/src/drivers/signal-types.ts +35 -5
- package/src/elements/clock/declare.ts +64 -2
- package/src/elements/clock/reconcile.ts +98 -25
- package/src/elements/clock/runtime.ts +13 -2
- package/src/elements/clock.test.ts +28 -0
- package/src/elements/clock.ts +9 -1
- package/src/elements/gate/declare.ts +2 -0
- package/src/elements/gate/permissions.ts +12 -0
- package/src/elements/gate.ts +6 -1
- package/src/elements/signal/declare.ts +44 -10
- package/src/elements/signal/delivery-modes.test.ts +90 -4
- package/src/elements/signal/order-lifecycle.test.ts +20 -4
- package/src/elements/signal/runtime.ts +42 -0
- package/src/elements/signal.test.ts +21 -8
- package/src/elements/signal.ts +1 -1
- package/src/elements/store/cache.ts +1 -1
- package/src/elements/store/declare.ts +7 -0
- package/src/elements/store/rls-identity.test.ts +29 -0
- package/src/elements/store/rls-identity.ts +3 -0
- package/src/elements/store/runtime.ts +14 -0
- package/src/elements/store/schema-decl.ts +162 -44
- package/src/elements/store/schema-tenant.ts +41 -0
- package/src/elements/store/sql-rls-isolation.test.ts +39 -0
- package/src/elements/store/sql-select.types.test.ts +71 -0
- package/src/elements/store/sql-session.ts +54 -32
- package/src/elements/store.ts +5 -0
- package/src/elements/vault/builtin-adapter.ts +15 -2
- package/src/elements/vault/declare.ts +8 -0
- package/src/elements/vault/runtime.ts +7 -0
- package/src/elements/vault/sql-rls-isolation.test.ts +145 -0
- package/src/elements/vault/storage.ts +9 -0
- package/src/elements/vault/test-helpers.ts +2 -1
- package/src/i18n/catalogs/ar.ts +19 -0
- package/src/i18n/catalogs/en.ts +19 -0
- package/src/index.ts +2 -0
- package/src/kernel/adopt-routes.ts +56 -8
- package/src/kernel/api-key-host-persist.test.ts +143 -0
- package/src/kernel/app-tenant.ts +122 -0
- package/src/kernel/app.ts +196 -57
- package/src/kernel/auth-resolve.ts +13 -5
- package/src/kernel/boot-bind/clock.ts +9 -3
- package/src/kernel/boot.test.ts +4 -4
- package/src/kernel/boot.ts +2 -0
- package/src/kernel/budget.test.ts +1 -1
- package/src/kernel/clock-durable.ts +8 -0
- package/src/kernel/clock-per-tenant-name.ts +5 -0
- package/src/kernel/clock-reconcile.ts +8 -0
- package/src/kernel/errors-live-resume.ts +15 -0
- package/src/kernel/errors-tenant.ts +29 -0
- package/src/kernel/errors.registry.test.ts +31 -3
- package/src/kernel/errors.ts +43 -3
- package/src/kernel/flow.ts +26 -5
- package/src/kernel/fx-auth-keys.test.ts +88 -0
- package/src/kernel/fx-auth-keys.ts +203 -0
- package/src/kernel/fx-auth-tenants.test.ts +87 -0
- package/src/kernel/fx-auth-tenants.ts +286 -0
- package/src/kernel/fx-live-stream.ts +149 -0
- package/src/kernel/fx-live.test.ts +157 -0
- package/src/kernel/fx-runtime.ts +15 -0
- package/src/kernel/fx-tenant-store.ts +213 -0
- package/src/kernel/fx.test.ts +108 -1
- package/src/kernel/fx.ts +206 -28
- package/src/kernel/hooks.ts +2 -2
- package/src/kernel/http-resource.ts +9 -18
- package/src/kernel/index.ts +2 -0
- package/src/kernel/journal.ts +12 -0
- package/src/kernel/live-http.test.ts +78 -0
- package/src/kernel/live-http.ts +114 -0
- package/src/kernel/live-resume.test.ts +125 -0
- package/src/kernel/on.ts +51 -0
- package/src/kernel/pipeline-api-key.test.ts +192 -7
- package/src/kernel/pipeline-tenant.ts +49 -0
- package/src/kernel/pipeline.test.ts +1 -1
- package/src/kernel/pipeline.ts +41 -4
- package/src/kernel/resource-mount.test.ts +10 -27
- package/src/kernel/tenant-resolve.test.ts +101 -0
- package/src/kernel/tenant-resolve.ts +124 -0
- package/src/kernel/tenant-roles.test.ts +87 -0
- package/src/kernel/triggers.ts +59 -21
- package/src/manifest/diff.test.ts +11 -2
- package/src/manifest/diff.ts +53 -11
- package/src/manifest/fixtures/skyport.excerpt.json +1 -1
- package/src/manifest/fixtures/skyport.manifest.json +0 -1
- package/src/manifest/types.ts +58 -6
- package/src/release/build-lib.ts +14 -1
- package/src/release/limits.ts +2 -2
- package/src/release/measure.ts +55 -1
- package/src/client/live-gap.test.ts +0 -35
- package/src/console/ui-next/dist/assets/cache-glyph-F1FI122b.js +0 -1
- package/src/console/ui-next/dist/assets/call-pii-button-DEDCl_j3.js +0 -1
- package/src/console/ui-next/dist/assets/collapsible-LPqGvfoz.js +0 -1
- package/src/console/ui-next/dist/assets/confirm-sheet-3ptDzXbz.js +0 -1
- package/src/console/ui-next/dist/assets/explorer-empty-BvYrygyO.js +0 -1
- package/src/console/ui-next/dist/assets/flows-page-Dluo1Bd7.js +0 -1
- package/src/console/ui-next/dist/assets/http-method--sWDdXSB.js +0 -1
- package/src/console/ui-next/dist/assets/index-Ca3HZMVq.js +0 -66
- package/src/console/ui-next/dist/assets/index-UtTDRQpS.css +0 -2
- package/src/console/ui-next/dist/assets/link-COeyggt-.js +0 -1
- package/src/console/ui-next/dist/assets/observability-page-HK9-BO8z.js +0 -4
- package/src/console/ui-next/dist/assets/replica-lag-DBIFFf7d.js +0 -18
- package/src/console/ui-next/dist/assets/sheet-form-Dnwa5oOR.js +0 -1
- package/src/console/ui-next/dist/assets/shortcut-Cgzuv4k1.js +0 -1
- package/src/console/ui-next/dist/assets/shortcut-keys-CoqwEIi0.js +0 -1
- package/src/console/ui-next/dist/assets/skeleton-BHcfDYcb.js +0 -1
- package/src/console/ui-next/dist/assets/store-page-BSohH3wM.js +0 -41
- package/src/console/ui-next/dist/assets/trace-detail-sheet-CFIGRnnA.js +0 -2
- package/src/console/ui-next/dist/assets/tree-expand-toggle-BgLl34w7.js +0 -54
- package/src/console/ui-next/dist/assets/units-page-B0cFE76A.js +0 -1
- package/src/console/ui-next/dist/assets/vault-page-B1SbYe10.js +0 -2
|
@@ -172,7 +172,7 @@ const { data } = await api.auth.signInEmail({ email, password });
|
|
|
172
172
|
if (data) session.set(data);
|
|
173
173
|
```
|
|
174
174
|
|
|
175
|
-
React: `useSession(api, session)` from `okengine/client-react`.
|
|
175
|
+
React: `useSession(api, session)` and `useLive(api, signal, input?)` from `okengine/client-react`.
|
|
176
176
|
|
|
177
177
|
| Step | What happens |
|
|
178
178
|
| ---------------------- | ---------------------------------------------------------------- |
|
|
@@ -180,6 +180,9 @@ React: `useSession(api, session)` from `okengine/client-react`.
|
|
|
180
180
|
| HTTP **401** | `refresh()` runs **once**, then the same call retries |
|
|
181
181
|
| HTTP **403** / **429** | No refresh — decode the failure envelope as usual |
|
|
182
182
|
|
|
183
|
+
`getToken` may return a session access token or an API key secret — both are Bearer.
|
|
184
|
+
See [Gate](/docs/elements/gate#api-keys). `refresh` applies to sessions only.
|
|
185
|
+
|
|
183
186
|
**Consequence:** `refresh` must mutate whatever `getToken` reads. Returning a new string alone does
|
|
184
187
|
nothing if storage was not updated.
|
|
185
188
|
|
|
@@ -201,6 +204,7 @@ A 401 with no `{ data, error }` body becomes `TransportError` with `data.status:
|
|
|
201
204
|
| `memorySession` | `okengine/client/auth` | In-memory access/refresh bag for `auth.getToken` |
|
|
202
205
|
| `AUTH_ERROR_CODES` | `okengine/client/auth` | Common auth Flow / gate codes |
|
|
203
206
|
| `useSession(api, session?)` | `okengine/client-react` | React status + `auth.me` |
|
|
207
|
+
| `useLive(api, signal, …)` | `okengine/client-react` | React `events` / `latest` / `isConnected` |
|
|
204
208
|
|
|
205
209
|
Core `okengine/client` stays under the size budget — helpers are separate exports. Not in
|
|
206
210
|
core today: cookie jars or plugin `.client()` decorations. Browser apps: also see
|
|
@@ -218,7 +222,7 @@ core today: cookie jars or plugin `.client()` decorations. Browser apps: also se
|
|
|
218
222
|
| [Flow](/docs/elements/flow) | Direct | `api.unit.flow(input)` — the only public surface |
|
|
219
223
|
| [Gate](/docs/elements/gate) | Indirect | Bearer via `auth`; denials as `Unauthorized` / `Forbidden` / `RateLimited` |
|
|
220
224
|
| [Store](/docs/elements/store) | Via Flows | `fx.store` inside Flows; `store.resource` + `on(http.resource…)` → five Flows on `$routes` |
|
|
221
|
-
| [Signal](/docs/elements/signal) |
|
|
225
|
+
| [Signal](/docs/elements/signal) | Live SSE | `api.live(signal, input?, { onEvent })` — HTTP GET, callback + unsubscribe |
|
|
222
226
|
| [Clock](/docs/elements/clock) | Via Flows | Schedules fire on the server — the client never ticks a clock |
|
|
223
227
|
| [Vault](/docs/elements/vault) | Via Flows | Secrets stay server-side; never ship them to the browser package |
|
|
224
228
|
| [Channel](/docs/elements/channel) | Via Flows | `fx.send` in a Flow — the client does not send email/SMS/push |
|
|
@@ -230,7 +234,7 @@ Mount a resource, adopt the returned ops, then call the five Flows like any othe
|
|
|
230
234
|
|
|
231
235
|
```typescript
|
|
232
236
|
const notesR = store.resource(db, notes, {/* in, out, list */});
|
|
233
|
-
const mounted = on(http.resource("/notes", notesR.all()).public()
|
|
237
|
+
const mounted = on(http.resource("/notes", notesR.all()).public());
|
|
234
238
|
// .adopt({ notes: mounted }) →
|
|
235
239
|
const page = await api.notes.list({ limit: 20 });
|
|
236
240
|
const more = await page.next();
|
|
@@ -256,24 +260,63 @@ See [Store](/docs/elements/store) for the list query language and schemas. Handw
|
|
|
256
260
|
`fx.json.withQuery(rows, input)` for the same envelope. Auth posture for HTTP triggers is
|
|
257
261
|
covered under [Gate](/docs/elements/gate).
|
|
258
262
|
|
|
259
|
-
###
|
|
263
|
+
### Live signals
|
|
264
|
+
|
|
265
|
+
`delivery: "live"` is HTTP SSE. Expose with `.live(signal)` on GET (or `http.live(signal)` for
|
|
266
|
+
`GET /_oke/live/{name}`), then subscribe with a callback. `for await` stays on the server.
|
|
267
|
+
|
|
268
|
+
```typescript
|
|
269
|
+
const stop = api.live(
|
|
270
|
+
orderStatus,
|
|
271
|
+
{ orderId: "ord_1" },
|
|
272
|
+
{
|
|
273
|
+
onEvent: (event) => {
|
|
274
|
+
/* event: { orderId, status } */
|
|
275
|
+
},
|
|
276
|
+
onError: (err) => {
|
|
277
|
+
/* optional — 4xx, envelope, network drop */
|
|
278
|
+
},
|
|
279
|
+
autoResubscribe: false, // default — true reopens after a drop (500ms…30s backoff)
|
|
280
|
+
},
|
|
281
|
+
);
|
|
282
|
+
stop(); // useEffect cleanup
|
|
283
|
+
```
|
|
260
284
|
|
|
261
|
-
`
|
|
262
|
-
|
|
263
|
-
|
|
285
|
+
`api.orders.events({ orderId }, { onEvent })` is the same shape on the exposing flow.
|
|
286
|
+
|
|
287
|
+
The client picks the unique exposure whose `matchKey` fields are a subset of the input, preferring
|
|
288
|
+
the largest match (`{ orderId }` beats firehose). A tie needs `via: "unit.flow"`.
|
|
289
|
+
|
|
290
|
+
Reconnects send `Last-Event-ID` from the last `id:` the client actually received.
|
|
291
|
+
|
|
292
|
+
A **410** `LiveResumeGap` (**OKE1014**) means that cursor is gone — `onError` fires, the
|
|
293
|
+
cursor is dropped, and `autoResubscribe` replays the remaining tape after backoff.
|
|
294
|
+
|
|
295
|
+
```typescript
|
|
296
|
+
import { useLive } from "okengine/client-react";
|
|
297
|
+
|
|
298
|
+
const { events, latest, error, isConnected } = useLive(
|
|
299
|
+
api,
|
|
300
|
+
orderStatus,
|
|
301
|
+
{ orderId: "ord_1" },
|
|
302
|
+
{
|
|
303
|
+
autoResubscribe: true,
|
|
304
|
+
},
|
|
305
|
+
);
|
|
306
|
+
```
|
|
264
307
|
|
|
265
308
|
## Exports
|
|
266
309
|
|
|
267
|
-
| Export | Kind | Role
|
|
268
|
-
| ----------------------------------------- | --------- |
|
|
269
|
-
| `createClient` | function | Typed proxy `api.unit.flow(input?)`
|
|
270
|
-
| `flattenRoutes` | function | `$routes` → flat `unit.flow` REST table
|
|
271
|
-
| `createTransport` | function | Low-level HTTP transport (timeout / retry / auth)
|
|
272
|
-
| `isOk` / `isFail` | function | Envelope predicates
|
|
273
|
-
| `isErrorCode` / `isTransportError` | function | Error narrowing
|
|
274
|
-
| `Client`, `ClientCall`, `ClientResult`, … | types | Contracts, `page.next()` / `for await` of `list()`
|
|
275
|
-
| `Register` | interface | Module-augmentation slot for ambient App types
|
|
276
|
-
| `AppOf` | type | Brand a bare route map as an App
|
|
310
|
+
| Export | Kind | Role |
|
|
311
|
+
| ----------------------------------------- | --------- | --------------------------------------------------- |
|
|
312
|
+
| `createClient` | function | Typed proxy `api.unit.flow(input?)` plus `api.live` |
|
|
313
|
+
| `flattenRoutes` | function | `$routes` → flat `unit.flow` REST table |
|
|
314
|
+
| `createTransport` | function | Low-level HTTP transport (timeout / retry / auth) |
|
|
315
|
+
| `isOk` / `isFail` | function | Envelope predicates |
|
|
316
|
+
| `isErrorCode` / `isTransportError` | function | Error narrowing |
|
|
317
|
+
| `Client`, `ClientCall`, `ClientResult`, … | types | Contracts, `page.next()` / `for await` of `list()` |
|
|
318
|
+
| `Register` | interface | Module-augmentation slot for ambient App types |
|
|
319
|
+
| `AppOf` | type | Brand a bare route map as an App |
|
|
277
320
|
|
|
278
321
|
Budget: the `./client` export stays under the measured client-runtime cap (hard gate in CI).
|
|
279
322
|
|
|
@@ -320,6 +363,20 @@ calls `api.auth.refresh({ refreshToken })`. Re-login when rotation fails or no r
|
|
|
320
363
|
|
|
321
364
|
</Accordion>
|
|
322
365
|
|
|
366
|
+
<Accordion title="api.live throws Multiple live exposures">
|
|
367
|
+
|
|
368
|
+
Two routes share the same match shape. Pass `via: "unit.flow"` or call the exposing flow
|
|
369
|
+
(`api.orders.events(input, { onEvent })`).
|
|
370
|
+
|
|
371
|
+
</Accordion>
|
|
372
|
+
|
|
373
|
+
<Accordion title="onError sees LiveResumeGap / HTTP 410">
|
|
374
|
+
|
|
375
|
+
The last `id:` is not on the server tape. The client drops the cursor. With `autoResubscribe: true`
|
|
376
|
+
the next request omits `Last-Event-ID` and replays what remains.
|
|
377
|
+
|
|
378
|
+
</Accordion>
|
|
379
|
+
|
|
323
380
|
</Accordions>
|
|
324
381
|
|
|
325
382
|
## Learn more
|
|
@@ -329,6 +386,7 @@ calls `api.auth.refresh({ refreshToken })`. Re-login when rotation fails or no r
|
|
|
329
386
|
- [Gate](/docs/elements/gate) — policies, `gate.public`, denials
|
|
330
387
|
- [Store](/docs/elements/store) — `store.resource` and list query language
|
|
331
388
|
- [Flow](/docs/elements/flow) — `in` / `out` / `errors` and `fx.fail`
|
|
389
|
+
- [Signal](/docs/elements/signal) — `api.live` for `delivery: "live"` SSE
|
|
332
390
|
- [Errors](/docs/reference/errors) — framework codes vs failure values
|
|
333
391
|
- [CORS](/docs/plugins/cors) · [CSRF](/docs/plugins/csrf) — browser callers
|
|
334
392
|
- [CLI Reference](/docs/reference/cli) — `oke client add`, `oke dev`
|
|
@@ -181,10 +181,13 @@ i18n: { locales: ["en"], default: "en" },
|
|
|
181
181
|
|
|
182
182
|
## tenancy
|
|
183
183
|
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
|
184
|
+
`oke.config.ts` `tenancy` is **isolation posture** (how rows are separated). Identity —
|
|
185
|
+
who the tenant is — is `gate.auth.tenant` on [Gate](/docs/elements/gate#tenants-identity-dimension).
|
|
186
|
+
|
|
187
|
+
| Option | Type | Meaning |
|
|
188
|
+
| ----------- | --------------------------------- | ------------------------------------------------- |
|
|
189
|
+
| `isolation` | `"row" \| "schema" \| "database"` | How tenants are separated in the store |
|
|
190
|
+
| `resolve` | string \| function | Observational resolver (isolation-only manifests) |
|
|
188
191
|
|
|
189
192
|
## privacy
|
|
190
193
|
|
|
@@ -29,18 +29,25 @@ string. Custom app codes stay message-less until registered.
|
|
|
29
29
|
|
|
30
30
|
## OKE numeric codes
|
|
31
31
|
|
|
32
|
-
| Code | Name
|
|
33
|
-
| ------ |
|
|
34
|
-
| `1001` | undeclared read
|
|
35
|
-
| `1002` | undeclared write
|
|
36
|
-
| `1003` | undeclared emit
|
|
37
|
-
| `1004` | undeclared send
|
|
38
|
-
| `1005` | undeclared ask
|
|
39
|
-
| `1006` | undeclared secret
|
|
40
|
-
| `1007` | undeclared call
|
|
41
|
-
| `
|
|
42
|
-
| `
|
|
43
|
-
| `
|
|
32
|
+
| Code | Name | Cause | Fix |
|
|
33
|
+
| ------ | -------------------- | ------------------------------------------------- | ---------------------------------------------------- |
|
|
34
|
+
| `1001` | undeclared read | Flow reads a resource not in `effects.reads` | Add it to the flow's `effects.reads` |
|
|
35
|
+
| `1002` | undeclared write | Flow writes a resource not in `effects.writes` | Add it to the flow's `effects.writes` |
|
|
36
|
+
| `1003` | undeclared emit | Flow emits a signal not in `effects.emits` | Add it to the flow's `effects.emits` |
|
|
37
|
+
| `1004` | undeclared send | Flow sends a template not in `effects.sends` | Add it to the flow's `effects.sends` |
|
|
38
|
+
| `1005` | undeclared ask | Flow asks a prompt not in `effects.asks` | Add it to the flow's `effects.asks` |
|
|
39
|
+
| `1006` | undeclared secret | Flow reads a secret not in `effects.secrets` | Add it to the flow's `effects.secrets` |
|
|
40
|
+
| `1007` | undeclared call | Flow calls a flow not in `effects.calls` | Add it to the flow's `effects.calls` |
|
|
41
|
+
| `1011` | HTTP route clash | Two HTTP flows share the same method + path | Give each flow a unique method + path |
|
|
42
|
+
| `1012` | HTTP flow unnamed | Adopted HTTP flow still has no `unit.export` | Export from `flows/<unit>/` or pass a named `flow` |
|
|
43
|
+
| `1013` | live exposure dup | Same signal, gates, and match on two GET routes | Change the gate or path-param filter |
|
|
44
|
+
| `1014` | live resume gap | `Last-Event-ID` is not on the retained tape | Reconnect without the cursor; remaining tape replays |
|
|
45
|
+
| `1015` | tenant required | Tenant-scoped op with no `fx.tenant.id` | `switchTenant`, signed `tid`, or tenant header |
|
|
46
|
+
| `1016` | tenant not member | Client-supplied tenant id is not a membership | Pick from `listTenants` or add the user as a member |
|
|
47
|
+
| `1017` | tenant unknown scope | Tenant role used an invented or `console:*` scope | Use a declared application scope |
|
|
48
|
+
| `1042` | orphan emit | Emit with zero subscribers and `optional` false | Add `on(signal, …)` or declare `optional: true` |
|
|
49
|
+
| `1043` | signal schema | Emit payload failed the signal's `schema` | Pass a payload that matches `schema`, or remove it |
|
|
50
|
+
| `1101` | schema missing | Domain table absent in `prod` — no auto-DDL | Run `oke db migrate` against this environment |
|
|
44
51
|
|
|
45
52
|
<Callout title="Effects are usually inferred">
|
|
46
53
|
The 1001–1007 family exists for flows that declare effects explicitly. Most apps never write an
|
|
@@ -52,11 +59,11 @@ string. Custom app codes stay message-less until registered.
|
|
|
52
59
|
|
|
53
60
|
Returned, not thrown — the request never reached `do`:
|
|
54
61
|
|
|
55
|
-
| Code | When | Payload
|
|
56
|
-
| -------------- | -------------------------------------------- |
|
|
57
|
-
| `Unauthorized` | Policy denied, request not authenticated | —
|
|
58
|
-
| `Forbidden` | Policy denied, authenticated but not allowed | `gate`, `reason` |
|
|
59
|
-
| `RateLimited` | Rate gate budget exhausted | `retryAfterMs`
|
|
62
|
+
| Code | When | Payload |
|
|
63
|
+
| -------------- | -------------------------------------------- | ------------------------------------------------------------------------------------------ |
|
|
64
|
+
| `Unauthorized` | Policy denied, request not authenticated | — |
|
|
65
|
+
| `Forbidden` | Policy denied, authenticated but not allowed | `gate`, `reason` (`tenant_required` · `not_member` · `unknown_scope` · `session_only` · …) |
|
|
66
|
+
| `RateLimited` | Rate gate budget exhausted | `retryAfterMs` |
|
|
60
67
|
|
|
61
68
|
## Framework validation failures
|
|
62
69
|
|
|
@@ -14,11 +14,16 @@ flow("orders.example", {
|
|
|
14
14
|
});
|
|
15
15
|
```
|
|
16
16
|
|
|
17
|
+
<Callout title="Extracted helpers">
|
|
18
|
+
Type a helper's `fx` parameter as `Fx` (`import type {Fx} from "okengine"`). A narrower structural
|
|
19
|
+
type will not match `store()` overloads. See [Flow](/docs/elements/flow).
|
|
20
|
+
</Callout>
|
|
21
|
+
|
|
17
22
|
## Stores
|
|
18
23
|
|
|
19
24
|
| Signature | Records | Returns |
|
|
20
25
|
| ------------------------------------------------------------------- | ------------ | ------------------------------------------------------------------------------------------- |
|
|
21
|
-
| `fx.store(sqlDecl).select().from(t)…` | `read` | rows (`where` · `orderBy` · `limit` · `offset` chainable)
|
|
26
|
+
| `fx.store(sqlDecl).select().from(t)…` | `read` | inferred rows (`where` · `orderBy` · `limit` · `offset` chainable) |
|
|
22
27
|
| `fx.store(sqlDecl).insert(t).values(v)` | `write` | `Promise<void>` |
|
|
23
28
|
| `fx.store(sqlDecl).update(t).set(v).where(…)` | `write` | `Promise<void>` |
|
|
24
29
|
| `fx.store(sqlDecl).delete(t).where(…)` | `write` | `Promise<void>` |
|
|
@@ -37,6 +42,7 @@ See [Store](/docs/elements/store) for the query-builder surface.
|
|
|
37
42
|
| ------------------------------------- | ---------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
38
43
|
| `fx.emit(signal, payload?, { key? })` | `emit` | Commits the signal outbox when the call resolves; optional `key` serializes `once` per key; stamps producer run id as `parentRunId` for trace chains; throws **OKE1042** (orphan) or **OKE1043** (schema) |
|
|
39
44
|
| `fx.deadLetters(signal)` | `read` `signal:<name>` | Dead-lettered messages for that signal. Payload typed from `SignalDecl<T>`. Page with `fx.json.withQuery`. Cross-signal throws **OKE1001**. |
|
|
45
|
+
| `fx.live(signal, { match? })` | `read` `signal:<name>` | Live tape as SSE. Returns `JsonStreamResult` (object chunks, `id:` on the wire). Cross-signal throws **OKE1001**. Do not wrap with `fx.json.stream`. |
|
|
40
46
|
|
|
41
47
|
## Runs (observability read)
|
|
42
48
|
|
|
@@ -72,20 +78,29 @@ on(
|
|
|
72
78
|
|
|
73
79
|
## Flows
|
|
74
80
|
|
|
75
|
-
| Signature
|
|
76
|
-
|
|
|
77
|
-
| `fx.call(flow, input?)`
|
|
78
|
-
| `fx.step(name, fn)`
|
|
79
|
-
| `fx.all([...thunks])`
|
|
80
|
-
| `fx.race([...thunks])`
|
|
81
|
-
| `fx.retry(fn, opts?)`
|
|
82
|
-
| `fx.using(acq, rel, use)`
|
|
83
|
-
| `fx.signal`
|
|
84
|
-
| `fx.fail(code, data, opts?)`
|
|
85
|
-
|
|
86
|
-
`fx.
|
|
87
|
-
|
|
88
|
-
|
|
81
|
+
| Signature | Records | Returns / notes |
|
|
82
|
+
| ------------------------------------------------------------------------------ | ----------------------- | -------------------------------------------------------------------------------------------------- |
|
|
83
|
+
| `fx.call(flow, input?)` | `call` | The callee's `out` — runs through the same pipeline |
|
|
84
|
+
| `fx.step(name, fn)` | — | Durable step: replays from the journal, never re-runs |
|
|
85
|
+
| `fx.all([...thunks])` | — | Parallel; first rejection aborts siblings |
|
|
86
|
+
| `fx.race([...thunks])` | — | First settle wins; losers aborted |
|
|
87
|
+
| `fx.retry(fn, opts?)` | — | Exponential backoff + jitter (plain Promise) |
|
|
88
|
+
| `fx.using(acq, rel, use)` | — | `release` runs once on settle or ambient abort |
|
|
89
|
+
| `fx.signal` | — | Ambient `AbortSignal` for the current branch |
|
|
90
|
+
| `fx.fail(code, data, opts?)` | — | Typed failure value (`opts.message` overrides) |
|
|
91
|
+
| `fx.auth.createApiKey({ name, scopes, expiresIn?, ipAllowlist?, rateLimit? })` | `write` `auth:api-keys` | Secret once. Creator is live `userId` / `scopes`. Session only. `ipAllowlist` is IPs or hostnames. |
|
|
92
|
+
| `fx.auth.listApiKeys()` | `read` `auth:api-keys` | Keys this session minted |
|
|
93
|
+
| `fx.auth.revokeApiKey(id)` | `write` `auth:api-keys` | Owner only |
|
|
94
|
+
| `fx.auth.rotateApiKey(id)` | `write` `auth:api-keys` | New secret once. Owner only |
|
|
95
|
+
| `fx.auth.updateApiKey(id, …)` | `write` `auth:api-keys` | Name / scopes / expiry / allowlist / rate. Re-attenuates |
|
|
96
|
+
| `fx.auth.listTenants()` | `read` `auth:tenants` | Memberships for the live session. Session only |
|
|
97
|
+
| `fx.auth.switchTenant(id)` | `write` `auth:tenants` | New access+refresh, new family, `tid` on both. Never Set-Cookie. Session only |
|
|
98
|
+
| `fx.auth.createTenant({ name, slug?, id? })` | `write` `auth:tenants` | Creator becomes a member. Session only |
|
|
99
|
+
| `fx.auth.upsertTenantRole({ tenantId, roleName, scopes })` | `write` `auth:tenants` | Application scopes only — `console:*` is unknown_scope |
|
|
100
|
+
|
|
101
|
+
`fx.call` starts the callee with an **empty** `fx.auth` (fail-closed for authorization) and
|
|
102
|
+
propagates `fx.tenant.id`. For audit/attribution only, read `fx.principal` — it propagates the
|
|
103
|
+
originating identity without copying into `fx.auth`. Gates never consult `fx.principal`.
|
|
89
104
|
|
|
90
105
|
## Concurrency and retry
|
|
91
106
|
|
|
@@ -204,6 +219,7 @@ keys. Use `cache: false` to opt out, or `cache: "30s"` for a TTL.
|
|
|
204
219
|
| `fx.json.with(page)` / `with(data, meta)` | 200 | `{ data, meta, error: null }` — already-built pager |
|
|
205
220
|
| `fx.json.withQuery(rows, input, spec?)` | 200 | In-memory list page — zero-config `q` / auto-eq / PostgREST |
|
|
206
221
|
| `fx.json.stream(chunks)` | 200 | `text/event-stream` — JSON `data:` frames, then `data: [DONE]` |
|
|
222
|
+
| `fx.live(signal)` | 200 | Same SSE carrier for a live signal (payload frames + `id:`) |
|
|
207
223
|
|
|
208
224
|
Returning a plain value instead answers 200 with `{ data: value, error: null }` — the helpers exist for status and `meta` control. Pass `fx.stream(...)` into `fx.json.stream` to reach the HTTP client token-by-token.
|
|
209
225
|
|
|
@@ -265,15 +281,16 @@ Built-in `errors.*` / `oke.*` catalogs localize `fx.fail` messages and thrown
|
|
|
265
281
|
|
|
266
282
|
## Principals
|
|
267
283
|
|
|
268
|
-
| Property | Shape
|
|
269
|
-
| -------------- |
|
|
270
|
-
| `fx.auth` | `{ userId
|
|
271
|
-
| `fx.operator` | `{ id: string \| null }` — Console plane
|
|
272
|
-
| `fx.principal` | Read-only origin: `userId`, `operatorId`, `scopes`, `verified?`, `plane?`
|
|
273
|
-
| `fx.tenant` | `{ id: string \| null }` — active tenant
|
|
284
|
+
| Property | Shape |
|
|
285
|
+
| -------------- | ------------------------------------------------------------------------------------- |
|
|
286
|
+
| `fx.auth` | `{ userId, scopes, verified?, apiKeyId? }` plus key and tenant methods (session only) |
|
|
287
|
+
| `fx.operator` | `{ id: string \| null }` — Console plane |
|
|
288
|
+
| `fx.principal` | Read-only origin: `userId`, `operatorId`, `scopes`, `verified?`, `plane?` |
|
|
289
|
+
| `fx.tenant` | `{ id: string \| null }` — active tenant (propagates on `fx.call`) |
|
|
274
290
|
|
|
275
291
|
**Consequence:** use `fx.auth` / gates for authorization; use `fx.principal` only when a callee
|
|
276
|
-
must log who started the call chain.
|
|
292
|
+
must log who started the call chain. A key Bearer sets `userId` to the issuer and `apiKeyId`
|
|
293
|
+
to the key — see [Gate](/docs/elements/gate#api-keys).
|
|
277
294
|
|
|
278
295
|
## Not on `fx`
|
|
279
296
|
|
|
@@ -47,7 +47,7 @@ Our path is concrete: a booking name containing "ignore previous instructions an
|
|
|
47
47
|
- **Store KV performance** (`QUERY /console/store/kv/stats`) is Redis-wire `INFO` / `COMMANDSTATS` / `SLOWLOG` / `LATENCY` — not Store browse. INFO is instance-wide (`StoreKvStatsServerWideGap`). SLOWLOG args are keys and values — collapsed until `revealPii: true`, which writes `console.store.kv.stats.reveal` (operator, ref). Named limitation `StoreKvStatsSlowlogArgsGap`. `memory` returns `KvStatsUnsupported`. This surface does not run `MONITOR` and does not invent a hot-key table.
|
|
48
48
|
- **Runs SQL** (`POST /console/runs/query`, Observability SQL tab) is operator-session, read-only, 5s timeout, 1000-row cap. DuckDB filesystem access is disabled for the statement. Masking is column-key only (`dim_*` and JSON blobs `input` / `output` / `logs` / `dimensions`). Aliases and expressions can leak classified values — named limitation `RunsQueryPiiProjectionGap`. This is **not** the `projectRun` guarantee. `revealPii: true` writes `console.runs.query.reveal`. Flow `fx.runs.query` stays unrestricted app SQL.
|
|
49
49
|
- **Plugin panels** run in a sandboxed iframe without `allow-same-origin`, communicating only over a `postMessage` bridge with their own CSP, no access to the operator session token, and exposure limited to that plugin's declared flows.
|
|
50
|
-
- **Session and framing:** `frame-ancestors 'none'`, `SameSite=Strict`, step-up authentication before destructive actions. An expired operator session redirects to Sign in with `?next=` set to the current `/overview`, `/flows`, `/store`, `/vault`, or `/observability` href (search included); after login the operator returns there. `/monitoring` is rewritten to `/observability`. Any other `next` (including `/units`) is dropped so the gate cannot be used as an open redirect. Unknown Console paths are 404.
|
|
50
|
+
- **Session and framing:** `frame-ancestors 'none'`, `SameSite=Strict`, step-up authentication before destructive actions. An expired operator session redirects to Sign in with `?next=` set to the current `/overview`, `/flows`, `/store`, `/vault`, `/access`, or `/observability` href (search included); after login the operator returns there. `/monitoring` is rewritten to `/observability`. Any other `next` (including `/units`) is dropped so the gate cannot be used as an open redirect. Unknown Console paths are 404.
|
|
51
51
|
- **Secret write path:** TLS required, no autocomplete, never echoed, never logged, not retained in browser memory after submission. Vault set / rotate / rotate-master use typed confirm (`SET` / `ROTATE` / `ROTATE_MASTER`). The Console never accepts a master key in the HTTP body (`OKE_VAULT_MASTER_KEY` only). `GET /console/vault/audit/verify` is read-only (no typed phrase).
|
|
52
52
|
- **The setup claim code** is rate-limited and compared in constant time. It is
|
|
53
53
|
printed on the `oke dev` board (TTY only — never on `GET /console/setup/status`)
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import { createApiKey, createApiKeyStore } from "./api-keys.ts";
|
|
3
|
+
import { bindApiKeySqlPersist, hydrateApiKeyStore, type ApiKeySqlExec } from "./api-key-sql.ts";
|
|
4
|
+
|
|
5
|
+
describe("api-key SQL persist", () => {
|
|
6
|
+
test("hydrate + persist round-trip", async () => {
|
|
7
|
+
const rows = new Map<string, Record<string, unknown>>();
|
|
8
|
+
const sql: ApiKeySqlExec = {
|
|
9
|
+
async execute(_q, params) {
|
|
10
|
+
const id = String(params[0]);
|
|
11
|
+
rows.set(id, {
|
|
12
|
+
id,
|
|
13
|
+
plane: params[1],
|
|
14
|
+
hash: params[2],
|
|
15
|
+
name: params[3],
|
|
16
|
+
scopes: params[4],
|
|
17
|
+
expires_at: params[5],
|
|
18
|
+
rate_limit: params[6],
|
|
19
|
+
ip_allowlist: params[7],
|
|
20
|
+
creator_id: params[8],
|
|
21
|
+
creator_scopes: params[9],
|
|
22
|
+
created_at: params[10],
|
|
23
|
+
last_used_at: params[11],
|
|
24
|
+
revoked_at: params[12],
|
|
25
|
+
});
|
|
26
|
+
},
|
|
27
|
+
async all() {
|
|
28
|
+
return [...rows.values()];
|
|
29
|
+
},
|
|
30
|
+
};
|
|
31
|
+
const store = createApiKeyStore();
|
|
32
|
+
bindApiKeySqlPersist(store, sql);
|
|
33
|
+
const created = await createApiKey(store, {
|
|
34
|
+
plane: "user",
|
|
35
|
+
name: "sql",
|
|
36
|
+
scopes: ["member"],
|
|
37
|
+
creatorId: "u1",
|
|
38
|
+
creatorScopes: ["member"],
|
|
39
|
+
id: "key_sql",
|
|
40
|
+
});
|
|
41
|
+
const other = createApiKeyStore();
|
|
42
|
+
await hydrateApiKeyStore(sql, other);
|
|
43
|
+
expect(other.keys.get("key_sql")?.name).toBe("sql");
|
|
44
|
+
expect(other.keys.get("key_sql")?.creatorId).toBe("u1");
|
|
45
|
+
expect(other.keys.get("key_sql")?.hash).toBe(created.row.hash);
|
|
46
|
+
});
|
|
47
|
+
});
|