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
package/manifest.v1.schema.json
CHANGED
|
@@ -298,7 +298,10 @@
|
|
|
298
298
|
"enum": ["user", "operator"]
|
|
299
299
|
},
|
|
300
300
|
"durable": { "type": "boolean" },
|
|
301
|
-
"live": {
|
|
301
|
+
"live": {
|
|
302
|
+
"type": "string",
|
|
303
|
+
"description": "Live signal name when this flow streams delivery: live SSE."
|
|
304
|
+
},
|
|
302
305
|
"cache": {
|
|
303
306
|
"oneOf": [{ "type": "boolean" }, { "type": "string", "minLength": 1 }]
|
|
304
307
|
},
|
|
@@ -342,7 +345,23 @@
|
|
|
342
345
|
"retries": { "type": "integer", "minimum": 0 },
|
|
343
346
|
"deadLetter": { "type": "boolean" },
|
|
344
347
|
"schema": { "$ref": "#/$defs/JsonSchema" },
|
|
345
|
-
"optional": { "type": "boolean" }
|
|
348
|
+
"optional": { "type": "boolean" },
|
|
349
|
+
"retention": {
|
|
350
|
+
"description": "Live-tape cap (delivery: live only). Omitted = unbounded.",
|
|
351
|
+
"type": "object",
|
|
352
|
+
"additionalProperties": false,
|
|
353
|
+
"properties": {
|
|
354
|
+
"maxAge": {
|
|
355
|
+
"type": "string",
|
|
356
|
+
"description": "Drop events older than this duration (7d, 1h, 30s, …)."
|
|
357
|
+
},
|
|
358
|
+
"maxCount": {
|
|
359
|
+
"type": "integer",
|
|
360
|
+
"minimum": 1,
|
|
361
|
+
"description": "Keep only the newest N live events."
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
}
|
|
346
365
|
}
|
|
347
366
|
},
|
|
348
367
|
"ColumnClassification": {
|
package/package.json
CHANGED
|
@@ -102,6 +102,12 @@ difference.
|
|
|
102
102
|
| `timezone` | string | `"UTC"` | IANA timezone for cron evaluation |
|
|
103
103
|
| `overridable` | boolean | `false` | Allow the Console to **edit** the schedule (pause is separate) |
|
|
104
104
|
| `description` | string | — | Human title in the Console (falls back to the clock name) |
|
|
105
|
+
| `perTenant` | boolean | `false` | Expand to one `oke_crons` row per tenant (`{name}#{tenantId}`). Prefer `clock.perTenant(name, opts)` |
|
|
106
|
+
|
|
107
|
+
<Callout title="Per-tenant clocks never tick the template">
|
|
108
|
+
Bind `on(clock.perTenant("invoices", { every: "1h" }), flow(...))`. Rows are `invoices#acme`;
|
|
109
|
+
tenant create/delete adds or orphans them. Fire stamps `fx.tenant.id`.
|
|
110
|
+
</Callout>
|
|
105
111
|
|
|
106
112
|
### `fx.clock`
|
|
107
113
|
|
|
@@ -222,6 +228,7 @@ The registry is off in `test` and when there is no shared SQL URL. `dev`/`prod`
|
|
|
222
228
|
|
|
223
229
|
- [Project structure](/docs/get-started/project-structure) — clock flows in the tree get a name, never a URL
|
|
224
230
|
- [Flow](/docs/elements/flow) — `on(trigger, flow)` and the `fx` surface
|
|
231
|
+
- [Gate](/docs/elements/gate) — `clock.perTenant` when `gate.auth.tenant` is on
|
|
225
232
|
- [fx · Runs](/docs/reference/fx#runs-observability-read) — native SLO checkers via `on(every(…))` + `fx.runs`
|
|
226
233
|
- [Signal](/docs/elements/signal) — reacting to events instead of time
|
|
227
234
|
|
|
@@ -71,16 +71,17 @@ curl -X POST localhost:6530/orders -d '{"sku":"SKU-1","qty":2}' -H 'content-type
|
|
|
71
71
|
|
|
72
72
|
## Anatomy of a Flow
|
|
73
73
|
|
|
74
|
-
| Part
|
|
75
|
-
|
|
|
76
|
-
| trigger
|
|
77
|
-
| `in`
|
|
78
|
-
| `out`
|
|
79
|
-
| `errors`
|
|
80
|
-
| `retry`
|
|
81
|
-
| `cache`
|
|
82
|
-
| `
|
|
83
|
-
| `
|
|
74
|
+
| Part | Role |
|
|
75
|
+
| -------------- | ------------------------------------------------------------------------------------ |
|
|
76
|
+
| trigger | What starts the flow — `http`, a signal, `every`, a row change |
|
|
77
|
+
| `in` | Input contract — validated before `do` runs; bad input is a 422 |
|
|
78
|
+
| `out` | Output contract — the return value is checked against it |
|
|
79
|
+
| `errors` | Typed failures — **returned** with `fx.fail`, never thrown |
|
|
80
|
+
| `retry` | Optional whole-`do` backoff on thrown errors (same journal) |
|
|
81
|
+
| `cache` | Optional — `false` opts out; `"30s"` adds a TTL. Read-only flows cache by default |
|
|
82
|
+
| `tenantScoped` | Default `true` when `gate.auth.tenant` is on — set `false` to skip tenant-role union |
|
|
83
|
+
| `do` | The work — every read, write, emit, and call goes through `fx` |
|
|
84
|
+
| `compensate` | Durable only — optional hook after auto per-step `{ undo }` (same journal) |
|
|
84
85
|
|
|
85
86
|
Failures are values, not exceptions:
|
|
86
87
|
|
|
@@ -104,6 +105,12 @@ Built-in codes attach a localized `message` ([Errors](/docs/reference/errors));
|
|
|
104
105
|
[Store](/docs/elements/store#handwritten-http-lists).
|
|
105
106
|
</Callout>
|
|
106
107
|
|
|
108
|
+
<Callout title="Helpers take the real Fx">
|
|
109
|
+
Type a helper's `fx` as the exported `Fx` (`import type { Fx } from
|
|
110
|
+
"okengine"`). A narrower `{ store: … }` will not match `store()` overloads
|
|
111
|
+
— that is the `as never` trap. See [fx](/docs/reference/fx).
|
|
112
|
+
</Callout>
|
|
113
|
+
|
|
107
114
|
## The five triggers
|
|
108
115
|
|
|
109
116
|
<FlowTriggers />
|
|
@@ -133,8 +140,8 @@ export const findOrder = on(
|
|
|
133
140
|
### signal — another flow emits
|
|
134
141
|
|
|
135
142
|
The producer emits through `fx`. For `once` and `broadcast` the consumer is the same species —
|
|
136
|
-
`on(signal, flow)`, no `subscribe()` registration. `live` replay is
|
|
137
|
-
the
|
|
143
|
+
`on(signal, flow)`, no `subscribe()` registration. `live` replay is HTTP SSE: `.live(signal)` on a
|
|
144
|
+
GET trigger, `fx.live` as the stream carrier, `api.live` on the client.
|
|
138
145
|
|
|
139
146
|
```typescript
|
|
140
147
|
await fx.emit(orderPlaced, { orderId: id }); // inside the producing flow
|
|
@@ -203,6 +210,7 @@ Everything a flow may touch, on one object:
|
|
|
203
210
|
| `fx.store(db).select/insert/…` | read / write | SQL, KV, files, index sessions |
|
|
204
211
|
| `fx.emit(signal, payload)` | emit | Publish a signal (transactional with writes) |
|
|
205
212
|
| `fx.deadLetters(signal)` | read | Dead-lettered messages for that signal |
|
|
213
|
+
| `fx.live(signal, { match? })` | read | Live signal SSE carrier (`JsonStreamResult`) |
|
|
206
214
|
| `fx.send(template, opts)` | send | Reach a human (email · SMS · …) |
|
|
207
215
|
| `fx.ask(prompt, input)` | ask | Call a versioned AI prompt |
|
|
208
216
|
| `fx.run(agent, input)` | ask | Run a bounded agent |
|
|
@@ -236,6 +236,7 @@ export const app = oke({
|
|
|
236
236
|
| `session.singleSessionPerUser` | `false` | Opt-in: issuing a session revokes other families for that user |
|
|
237
237
|
| `cookies.enabled` | `false` | Opt-in HttpOnly cookie mirror (Bearer stays default) |
|
|
238
238
|
| `secondaryStorage.enabled` | `false` | Hot auth data in `store.kv` when configured |
|
|
239
|
+
| `tenant` | unset | Opt-in identity dimension — `true` or `{ required, source, header, resolve, authoritative }` |
|
|
239
240
|
| `user` / `session` / … | defaults | `modelName` / `fields` / `additionalFields` |
|
|
240
241
|
|
|
241
242
|
HIBP via `createHibpBreachCheck` from `okengine/auth` (k-anonymity range API; requires
|
|
@@ -256,6 +257,83 @@ Customize tables, then `oke schema generate` (`.oke/schema/oke.ts`; `--check` in
|
|
|
256
257
|
sign-in shapes: [Plugins](/docs/plugins). Call auth from
|
|
257
258
|
[createClient](/docs/reference/client) — helpers in `okengine/client/auth`.
|
|
258
259
|
|
|
260
|
+
## Tenants (identity dimension)
|
|
261
|
+
|
|
262
|
+
`gate.auth.tenant: true` (or an options bag) is **not** a second authorization system and not an
|
|
263
|
+
organizations product. Tenant is a dimension of identity, like `fx.auth` / `fx.operator`.
|
|
264
|
+
|
|
265
|
+
```typescript
|
|
266
|
+
export const app = oke({
|
|
267
|
+
name: "shop",
|
|
268
|
+
gate: { auth: { tenant: true } },
|
|
269
|
+
});
|
|
270
|
+
```
|
|
271
|
+
|
|
272
|
+
| Option | Default | Meaning |
|
|
273
|
+
| --------------- | -------------- | ------------------------------------------------------------------------------------------------ |
|
|
274
|
+
| `required` | `false` | Pure B2B: authenticated user-plane requests without a tenant are `Forbidden` / `tenant_required` |
|
|
275
|
+
| `source` | `"claim"` | `"claim"` · `"header"` · `"subdomain"` · `"resolve"` |
|
|
276
|
+
| `header` | `x-oke-tenant` | Header name when `source` is `"header"` |
|
|
277
|
+
| `resolve` | unset | Callback that returns an id; membership is still checked unless `authoritative` |
|
|
278
|
+
| `authoritative` | `false` | Trust `resolve` without a membership query |
|
|
279
|
+
|
|
280
|
+
Three tiers, fail-safe:
|
|
281
|
+
|
|
282
|
+
1. **Claim** — signed JWT `tid` or API-key `tenantId`. The id is trusted; no membership query.
|
|
283
|
+
2. **Header / subdomain** — never trust a client-supplied id without membership.
|
|
284
|
+
3. **`resolve`** — still membership-checked unless `authoritative: true`.
|
|
285
|
+
|
|
286
|
+
`required: false` (default) is B2C+B2B: `fx.tenant.id` may be `null`. Internal / cron / `fx.call`
|
|
287
|
+
have no HTTP request — header and subdomain sources keep the stamped claim.
|
|
288
|
+
|
|
289
|
+
`fx.auth.switchTenant(id)` issues a **new** access+refresh pair and a **new** family (so tab A
|
|
290
|
+
rotation cannot reuse-detect tab B). It never Set-Cookies.
|
|
291
|
+
|
|
292
|
+
Refresh copies that row's `tid`; tenant-role scopes never melt into the JWT.
|
|
293
|
+
|
|
294
|
+
`fx.auth.listTenants` / `createTenant` / `addMember` / `upsertTenantRole` are session-only
|
|
295
|
+
(`auth:tenants`). Tenant roles may grant application scopes only — `console:*` fails like an
|
|
296
|
+
unknown name.
|
|
297
|
+
|
|
298
|
+
Live `fx.auth.scopes` unions those grants only when `fx.tenant.id` is set, the flow is
|
|
299
|
+
tenant-scoped (default on; `flow({ tenantScoped: false })` opts out), and user-plane.
|
|
300
|
+
`gate.scope()` still reads `auth.scopes.has(name)`.
|
|
301
|
+
|
|
302
|
+
SQL stamps `oke.tenant()` when tenancy is on — [Store](/docs/elements/store) and
|
|
303
|
+
built-in [Vault](/docs/elements/vault#tenant-isolation-built-in-store) ciphertext.
|
|
304
|
+
|
|
305
|
+
## API keys
|
|
306
|
+
|
|
307
|
+
A key is the **issuer with fewer gates** — not a second permission system.
|
|
308
|
+
Bearer secrets authenticate as the creator; `fx.auth.scopes` shrink to the key.
|
|
309
|
+
|
|
310
|
+
| Field | Session | API key |
|
|
311
|
+
| ----------------------------------- | -------------- | ----------------------- |
|
|
312
|
+
| `fx.auth.userId` / `fx.operator.id` | subject | creator (`creatorId`) |
|
|
313
|
+
| `fx.auth.scopes` | session scopes | key scopes (attenuated) |
|
|
314
|
+
| `fx.auth.apiKeyId` | `null` | key id |
|
|
315
|
+
| WideEvent `principal` | subject | issuer |
|
|
316
|
+
| `dimensions.api_key` | absent | key id |
|
|
317
|
+
|
|
318
|
+
`gate.auth` always creates a shared key store (HMAC-SHA-256 with `gate.auth.secret`).
|
|
319
|
+
Do not pass `apiKeyStore` unless you are injecting a test Map.
|
|
320
|
+
|
|
321
|
+
<Callout title="Keys live on the app database">
|
|
322
|
+
With `store.sql()`, boot hydrates public `oke_api_keys` on that connection (`DATABASE_URL`) and
|
|
323
|
+
write-through persists create / rotate / revoke. Console **Access** attaches this host store — it
|
|
324
|
+
does not keep a second key database.
|
|
325
|
+
</Callout>
|
|
326
|
+
|
|
327
|
+
Session Flows manage keys via [fx](/docs/reference/fx) (`createApiKey`, `listApiKeys`,
|
|
328
|
+
`revokeApiKey`, `rotateApiKey`, `updateApiKey`). A key-authenticated caller cannot.
|
|
329
|
+
|
|
330
|
+
Declare `effects.reads: ["auth:api-keys"]` / `effects.writes: ["auth:api-keys"]` — never
|
|
331
|
+
`sql:oke_api_keys`. Console **Access** (`:6533` → `/access`) mints operator keys as the
|
|
332
|
+
operator, and user-plane keys only as a picked issuer (`creatorUserId`).
|
|
333
|
+
|
|
334
|
+
Verify rejects expired, revoked, allowlist-miss, and over-rate secrets with `401`
|
|
335
|
+
and no `api_key` dimension.
|
|
336
|
+
|
|
259
337
|
## Identity and decisions
|
|
260
338
|
|
|
261
339
|
Gates decide on principals that already exist. `gate.auth` issues hybrid sessions onto
|
|
@@ -268,8 +346,8 @@ edit live today.
|
|
|
268
346
|
|
|
269
347
|
<Callout title="RLS reads the stamped principal">
|
|
270
348
|
After HTTP Gate passes, user-plane `fx.store` stamps `oke.gate()`, `oke.user()`, and
|
|
271
|
-
`oke.has_scope()`.
|
|
272
|
-
[Store](/docs/elements/store).
|
|
349
|
+
`oke.has_scope()`. With `gate.auth.tenant` on, a fourth GUC `oke.tenant()` is set. Rate, cron,
|
|
350
|
+
CDC, and signal stay unstamped. Helpers: [Store](/docs/elements/store).
|
|
273
351
|
</Callout>
|
|
274
352
|
|
|
275
353
|
## Troubleshooting
|
|
@@ -292,6 +370,12 @@ decisions belong inside `do` as typed `errors`.
|
|
|
292
370
|
`Forbidden` means authenticated but a policy said no. Check `verified` or a missing scope —
|
|
293
371
|
the failure includes the gate name and reason.
|
|
294
372
|
|
|
373
|
+
</Accordion>
|
|
374
|
+
<Accordion title="Forbidden tenant_required / not_member">
|
|
375
|
+
|
|
376
|
+
`required: true` and no resolved tenant → `tenant_required`. Header/subdomain id that is not a
|
|
377
|
+
membership → `not_member`. Call `fx.auth.switchTenant(id)` or send a signed `tid`.
|
|
378
|
+
|
|
295
379
|
</Accordion>
|
|
296
380
|
<Accordion title="Prod boot: gate.auth: secret is required">
|
|
297
381
|
|
|
@@ -310,6 +394,27 @@ character classes.
|
|
|
310
394
|
`"ip"` for public surfaces (sign-up, password reset), `"user"` for authenticated quotas.
|
|
311
395
|
Keying an authenticated endpoint by IP punishes shared NAT.
|
|
312
396
|
|
|
397
|
+
</Accordion>
|
|
398
|
+
<Accordion title="API key Bearer returns 401">
|
|
399
|
+
|
|
400
|
+
Expired, revoked, allowlist miss, or over `rateLimit`. `ipAllowlist` uses the
|
|
401
|
+
same X-Forwarded-For hop as `ip-allowlist` (IP or hostname; lookup failure is closed).
|
|
402
|
+
Rotate if the secret was shown once and then lost.
|
|
403
|
+
|
|
404
|
+
</Accordion>
|
|
405
|
+
<Accordion title="Keys vanish after an app restart">
|
|
406
|
+
|
|
407
|
+
No SQL store, or a new `gate.auth.secret`. Declare `store.sql()`, keep
|
|
408
|
+
`DATABASE_URL` (or the same PGlite datadir) and the auth secret stable.
|
|
409
|
+
Console does not persist keys in `oke_console`.
|
|
410
|
+
|
|
411
|
+
</Accordion>
|
|
412
|
+
<Accordion title="Key can call a route the issuer cannot">
|
|
413
|
+
|
|
414
|
+
That is a bug in the mint. `createApiKey` / `fx.auth.createApiKey` re-run
|
|
415
|
+
`assertAttenuated` against the issuer's live scopes. Custom `gate.policy`
|
|
416
|
+
predicates that ignore scopes still see `verified: true`.
|
|
417
|
+
|
|
313
418
|
</Accordion>
|
|
314
419
|
</Accordions>
|
|
315
420
|
|
|
@@ -317,7 +422,7 @@ Keying an authenticated endpoint by IP punishes shared NAT.
|
|
|
317
422
|
|
|
318
423
|
- [Project structure](/docs/get-started/project-structure) — `http.get()` and `flow({…})` fill path and name from the file tree
|
|
319
424
|
- [Flow](/docs/elements/flow) — the trigger pipeline gates plug into
|
|
320
|
-
- [fx](/docs/reference/fx) — `fx.auth`, `fx.operator`, `fx.principal`
|
|
425
|
+
- [fx](/docs/reference/fx) — `fx.auth`, `fx.operator`, `fx.principal`, `fx.tenant`
|
|
321
426
|
- [Client](/docs/reference/client) — Bearer `createClient` + `okengine/client/auth`
|
|
322
427
|
- [Plugins](/docs/plugins) — username, magic link, OTP, TOTP, passkeys
|
|
323
428
|
- [Vault](/docs/elements/vault) — credentials your policies protect
|
|
@@ -72,7 +72,7 @@ export const sendConfirmation = on(
|
|
|
72
72
|
</Steps>
|
|
73
73
|
|
|
74
74
|
That's the loop for `once` and `broadcast`: declare → `fx.emit` → `on(signal, flow)`.
|
|
75
|
-
`live` keeps the same emit;
|
|
75
|
+
`live` keeps the same emit; expose it over HTTP with `.live(signal)` and subscribe from `api.live`.
|
|
76
76
|
|
|
77
77
|
## The three delivery physics
|
|
78
78
|
|
|
@@ -90,6 +90,7 @@ The declaration is identical in shape for all three — switching physics later
|
|
|
90
90
|
| `retries` | number | `3` | Retry budget for `once` — dead-letter when `attempts > retries` (`retries + 1` total invocations) |
|
|
91
91
|
| `deadLetter` | boolean | `true` | Preserve exhausted messages in the DLQ (`once`) |
|
|
92
92
|
| `optional` | boolean | `false` | Allow emitting while nobody subscribes (skip the orphan check) |
|
|
93
|
+
| `retention` | `{ maxAge?, maxCount? }` | unbounded | Live tape cap (`delivery: "live"` only). Both limits AND-combine when set |
|
|
93
94
|
|
|
94
95
|
## One order, three modes
|
|
95
96
|
|
|
@@ -124,6 +125,7 @@ export const orderStatus = signal("order-status", {
|
|
|
124
125
|
}),
|
|
125
126
|
delivery: "live",
|
|
126
127
|
optional: true, // clients may not be connected yet
|
|
128
|
+
retention: { maxAge: "24h", maxCount: 500 },
|
|
127
129
|
});
|
|
128
130
|
```
|
|
129
131
|
|
|
@@ -182,19 +184,31 @@ export const notifyCustomer = on(
|
|
|
182
184
|
);
|
|
183
185
|
```
|
|
184
186
|
|
|
185
|
-
`live` — retain the tape. A late subscriber replays
|
|
187
|
+
`live` — retain the tape. A late subscriber replays history (`placed → fulfilling → shipped`), then keeps going. Omit `retention` and that history is **unbounded** — declare `maxAge` / `maxCount` in production.
|
|
186
188
|
|
|
187
|
-
The listener is
|
|
189
|
+
The listener is **not** `on(orderStatus, flow)` — that trigger does not replay retained payloads. Expose GET SSE with `.live(signal)`, then subscribe from the typed client. `optional: true` is required so emits succeed before anyone is connected.
|
|
188
190
|
|
|
189
191
|
```typescript
|
|
190
|
-
const
|
|
191
|
-
if (!bus) throw new Error("signal runtime not booted");
|
|
192
|
+
export const events = on(http.get("/orders/:orderId/events").gate(member).live(orderStatus));
|
|
192
193
|
|
|
193
|
-
const
|
|
194
|
-
|
|
195
|
-
|
|
194
|
+
export const adminFeed = on(http.get("/admin/order-status").gate(admin).live(orderStatus));
|
|
195
|
+
|
|
196
|
+
const stop = api.live(
|
|
197
|
+
orderStatus,
|
|
198
|
+
{ orderId: "ord_1" },
|
|
199
|
+
{
|
|
200
|
+
onEvent: (event) => {
|
|
201
|
+
/* placed → fulfilling → shipped */
|
|
202
|
+
},
|
|
203
|
+
},
|
|
204
|
+
);
|
|
205
|
+
stop();
|
|
196
206
|
```
|
|
197
207
|
|
|
208
|
+
Same signal, two audiences: member filter on `:orderId`, admin firehose. Duplicate `(signal, gates, match)` is **OKE1013**. Same HTTP path is still **OKE1011**. Firehose default: `on(http.live(orderStatus).gate(member))` → `GET /_oke/live/order-status`.
|
|
209
|
+
|
|
210
|
+
Server-side `fx.live` **is** the SSE carrier (`JsonStreamResult`). Custom `do` is allowed; authors do not wrap `fx.json.stream`. Reconnects send `Last-Event-ID`; a missing cursor is **OKE1014** / HTTP 410, not a silent full replay. `autoResubscribe: true` backs off 500ms…30s and, after a 410, replays the remaining tape.
|
|
211
|
+
|
|
198
212
|
**Why separate signals:** delivery is fixed per declaration. Competing work stays on `once`; fan-out stays on `broadcast`; the timeline stays on `live`. Switching a word later is cheap; mixing physics on one name is not.
|
|
199
213
|
|
|
200
214
|
## When delivery fails
|
|
@@ -252,12 +266,16 @@ Committed `once` messages survive process death. A claim sets `lockedBy` and a v
|
|
|
252
266
|
|
|
253
267
|
<SignalLiveReplay />
|
|
254
268
|
|
|
255
|
-
`live` retains
|
|
269
|
+
`live` retains delivered messages and replays them to a late subscriber. Omit `retention` for an unbounded tape (a production hazard). `maxAge` (clock duration) and `maxCount` AND-combine when both are set. Drivers prune on write and when `live()` opens — no background sweeper.
|
|
270
|
+
|
|
271
|
+
Reconnects send SSE `Last-Event-ID`. If that id was pruned or never existed, the server returns **410** `{ error: { code: "LiveResumeGap" } }` (**OKE1014**) before opening the stream. The client clears the cursor; `autoResubscribe` then replays whatever remains.
|
|
256
272
|
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
273
|
+
The Console payload monitor still shows only the newest 50 for display — that cap is not the retained tape.
|
|
274
|
+
|
|
275
|
+
<Callout title="HTTP live is GET SSE">
|
|
276
|
+
Expose with `http.get(path).live(signal)` or `http.live(signal)`. Subscribe with `api.live(signal,
|
|
277
|
+
input?, {onEvent})` — callback + unsubscribe, not `for await`. See
|
|
278
|
+
[Client](/docs/reference/client#live-signals).
|
|
261
279
|
</Callout>
|
|
262
280
|
|
|
263
281
|
## Orphaned signal config
|
|
@@ -291,7 +309,7 @@ Pairing an arbitrary Store insert with emit inside **one** shared SQL transactio
|
|
|
291
309
|
You emitted a signal that nobody currently subscribes to. For `once` and `broadcast`, wire
|
|
292
310
|
`on(signal, flow)` before emit.
|
|
293
311
|
|
|
294
|
-
`live` listeners are `
|
|
312
|
+
`live` listeners are HTTP SSE (`api.live` / `fx.live`), not a Flow — set `optional: true` when they may connect later.
|
|
295
313
|
|
|
296
314
|
</Accordion>
|
|
297
315
|
<Accordion title="Emit fails with OKE1043 (schema)">
|
|
@@ -306,13 +324,24 @@ When `attempts > retries` the message moves to the DLQ (if `deadLetter: true`)
|
|
|
306
324
|
</Accordion>
|
|
307
325
|
<Accordion title="fx.deadLetters throws OKE1001">
|
|
308
326
|
|
|
309
|
-
The flow read a signal it did not declare. The compiler infers `reads: ["signal:<name>"]` from `fx.deadLetters(signal)`. A different handle needs its own read.
|
|
327
|
+
The flow read a signal it did not declare. The compiler infers `reads: ["signal:<name>"]` from `fx.deadLetters(signal)` and `fx.live(signal)`. A different handle needs its own read.
|
|
328
|
+
|
|
329
|
+
</Accordion>
|
|
330
|
+
<Accordion title="Boot fails with OKE1013 (live exposure duplicate)">
|
|
331
|
+
|
|
332
|
+
Two GET routes expose the same live signal with the same gates and match.
|
|
333
|
+
Change the gate or path-param filter, or drop the extra route (same path is **OKE1011**).
|
|
334
|
+
|
|
335
|
+
</Accordion>
|
|
336
|
+
<Accordion title="Subscribe gets HTTP 410 LiveResumeGap (OKE1014)">
|
|
337
|
+
|
|
338
|
+
The `Last-Event-ID` cursor is not on the retained tape (pruned, never existed, or the memory bus restarted). The gap is visible in `onError`. With `autoResubscribe: true` the next request omits the header and replays what remains.
|
|
310
339
|
|
|
311
340
|
</Accordion>
|
|
312
341
|
<Accordion title="on(liveSignal, flow) never replays history">
|
|
313
342
|
|
|
314
|
-
`on(signal, flow)` listens for `once` and `broadcast`. `live` replay is `
|
|
315
|
-
|
|
343
|
+
`on(signal, flow)` listens for `once` and `broadcast`. `live` replay is HTTP SSE (`api.live` /
|
|
344
|
+
`fx.live`) — a Flow trigger does not replay retained payloads.
|
|
316
345
|
|
|
317
346
|
</Accordion>
|
|
318
347
|
<Accordion title="once vs broadcast vs live — how do I choose?">
|
|
@@ -91,22 +91,17 @@ Install reviews the `CREATE EXTENSION` SQL; **Advanced** can set `SCHEMA`,
|
|
|
91
91
|
When a newer packaged version exists, the name chip shows **Upgrade**
|
|
92
92
|
(`ALTER EXTENSION … UPDATE`). Names link to the project or Postgres docs.
|
|
93
93
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
columns); two-finger click empty grid to create. Create policy pins
|
|
103
|
-
SQL | Code (`store.schema.policy` + `pgPolicy`); drag the handle to
|
|
104
|
-
resize the preview.
|
|
94
|
+
SQL table rows and the resource header show RLS (emerald / muted shield;
|
|
95
|
+
**N RLS** when policies exist) before **PII**. KV, files, and index have
|
|
96
|
+
no badge — enabling RLS on create flips the shield.
|
|
97
|
+
|
|
98
|
+
**RLS Policies** lists `pg_policies` plus Manifest `declared` / `live`.
|
|
99
|
+
Two-finger click a row for its sheet, or empty grid to create. Create
|
|
100
|
+
pins SQL | Code (`store.schema.policy` + `pgPolicy`); drag to resize.
|
|
101
|
+
|
|
105
102
|
**Templates** use `oke.user()` / `oke.gate()` / `oke.has_scope()`.
|
|
106
|
-
Owner cards
|
|
107
|
-
|
|
108
|
-
marks as schema cards. Create stays blocked until a real column is
|
|
109
|
-
picked.
|
|
103
|
+
Owner cards pick a **Column** (guess `creator_email` / `owner_email`)
|
|
104
|
+
with PK / FK marks. Create stays blocked until a column is picked.
|
|
110
105
|
|
|
111
106
|
Create policy picks a Gate posture (Read / Write / Both), or **Policy &
|
|
112
107
|
scope**. Those picks fill `USING` / `WITH CHECK`. `TO` stays `public` —
|
|
@@ -204,10 +199,10 @@ const notesR = store.resource(db, notes, {
|
|
|
204
199
|
},
|
|
205
200
|
});
|
|
206
201
|
|
|
207
|
-
const mounted = on(http.resource("/notes", notesR.all()).public()
|
|
202
|
+
const mounted = on(http.resource("/notes", notesR.all()).public());
|
|
208
203
|
```
|
|
209
204
|
|
|
210
|
-
`.gate(...)` / `.
|
|
205
|
+
`.gate(...)` / `.public()` chain like `http.get` — gates on every verb. Live SSE is `.live(signal)` on a GET, not on `http.resource`.
|
|
211
206
|
|
|
212
207
|
The list endpoint's URL is the whole query language:
|
|
213
208
|
|
|
@@ -296,6 +291,9 @@ await fx.store(db).update(notes).set({ title: input.title }).where(eq(notes.id,
|
|
|
296
291
|
await fx.store(db).delete(notes).where(lt(notes.createdAt, cutoff));
|
|
297
292
|
```
|
|
298
293
|
|
|
294
|
+
`select().from(notes)` infers the declared row (`string` / `number` / `null` from
|
|
295
|
+
`field.*`). A `select({ title: notes.title })` projection stays untyped.
|
|
296
|
+
|
|
299
297
|
<Callout title="One table per call — no relational with:">
|
|
300
298
|
`fx.store` is deliberately **single-table**: Drizzle's relational `findMany({ with: … })` is not
|
|
301
299
|
available through `fx`. Compose joins as separate single-table reads (or `fx.call`) so every
|
|
@@ -306,17 +304,18 @@ await fx.store(db).delete(notes).where(lt(notes.createdAt, cutoff));
|
|
|
306
304
|
|
|
307
305
|
The recommended path: declare tables ORM-agnostically, then let `oke db` emit real Drizzle (`pgTable` for Postgres / PGLite) into `src/db/schema.drizzle.ts`.
|
|
308
306
|
|
|
309
|
-
| Field API
|
|
310
|
-
|
|
|
311
|
-
| `field.text()` / `field.integer()`
|
|
312
|
-
| `.primaryKey()` · `.notNull()` · `.unique()`
|
|
313
|
-
| `.default(v)` · `.defaultFn(id \| now)`
|
|
314
|
-
| `.pii()` · `.sensitive()` · `.retain("30d")`
|
|
315
|
-
| `.as("sql_name")`
|
|
316
|
-
| `.describe("…")`
|
|
317
|
-
| `.references(() => col, { onDelete })`
|
|
318
|
-
| `store.schema.rls()`
|
|
319
|
-
| `store.schema.policy.gate/owner/scope`
|
|
307
|
+
| Field API | Meaning |
|
|
308
|
+
| --------------------------------------------- | --------------------------------------------------------------------------------- |
|
|
309
|
+
| `field.text()` / `field.integer()` | v1 column primitives |
|
|
310
|
+
| `.primaryKey()` · `.notNull()` · `.unique()` | constraints |
|
|
311
|
+
| `.default(v)` · `.defaultFn(id \| now)` | defaults |
|
|
312
|
+
| `.pii()` · `.sensitive()` · `.retain("30d")` | privacy classification |
|
|
313
|
+
| `.as("sql_name")` | override the automatic `camelCase → snake_case` |
|
|
314
|
+
| `.describe("…")` | human title in the Console (falls back to key) |
|
|
315
|
+
| `.references(() => col, { onDelete })` | foreign key |
|
|
316
|
+
| `store.schema.rls()` | `pgTable.withRLS` when there are no policies |
|
|
317
|
+
| `store.schema.policy.gate/owner/scope/tenant` | happy-path RLS (`oke.gate()` / `oke.user()` / `oke.has_scope()` / `oke.tenant()`) |
|
|
318
|
+
| `store.schema.unscoped()` | Shared table — required when tenancy is on and there is no tenant policy |
|
|
320
319
|
|
|
321
320
|
Third argument on `store.schema.table` is Drizzle-shaped extras:
|
|
322
321
|
|
|
@@ -331,6 +330,7 @@ export const bookings = store.schema.table(
|
|
|
331
330
|
store.schema.policy.gate("member", { for: "select" }),
|
|
332
331
|
store.schema.policy.owner("owner", { for: "all" }),
|
|
333
332
|
store.schema.policy.scope("booking:create", { for: "insert" }),
|
|
333
|
+
store.schema.policy.tenant("tenant_id"),
|
|
334
334
|
],
|
|
335
335
|
);
|
|
336
336
|
```
|
|
@@ -340,9 +340,9 @@ Helpers emit stable names (`gate_member_select`). Raw
|
|
|
340
340
|
escape hatch — predicates use `oke.*`, never `current_setting`.
|
|
341
341
|
|
|
342
342
|
<Callout title="User-plane fx.store applies RLS">
|
|
343
|
-
HTTP / resource flows stamp Gate identity
|
|
344
|
-
|
|
345
|
-
|
|
343
|
+
HTTP / resource flows stamp Gate identity (`SET LOCAL ROLE oke_app` + `set_config`). Operator /
|
|
344
|
+
cron / CDC / signal stay unstamped. Tenancy on: also `oke.tenant()`; tables need `policy.tenant`
|
|
345
|
+
or `unscoped()`.
|
|
346
346
|
</Callout>
|
|
347
347
|
|
|
348
348
|
#### Foreign keys and relations
|
|
@@ -692,6 +692,10 @@ export const drafts = store.kv("drafts", { durable: true, description: "Compose
|
|
|
692
692
|
**Consequence:** `oke db seed` into cache Redis looks fine until compose recreates the
|
|
693
693
|
container — Console Store then shows **No rows.** Seeded namespaces need `{ durable: true }`.
|
|
694
694
|
|
|
695
|
+
With `gate.auth.tenant` on, KV keys are prefixed `{tenantId}:` (logical keys in `do` stay
|
|
696
|
+
unprefixed). Missing `fx.tenant.id` throws **OKE1015**. Opt out with
|
|
697
|
+
`store.kv("sessions", { tenantScoped: false })`.
|
|
698
|
+
|
|
695
699
|
Gate rates and Signal stay on `REDIS_URL`. Missing `DATABASE_URL` with the postgres driver
|
|
696
700
|
fails boot: `oke boot: durable store.kv needs DATABASE_URL`.
|
|
697
701
|
|
|
@@ -1130,7 +1134,7 @@ Default `store.kv` is cache Redis with no AOF — a recreate drops keys. SQL see
|
|
|
1130
1134
|
- [Project structure](/docs/get-started/project-structure) — `list.ts` + `http.get()` is `GET /notes` named `notes.list`
|
|
1131
1135
|
- [Flow](/docs/elements/flow) — the `fx.store` session inside `do`
|
|
1132
1136
|
- [AI](/docs/elements/ai) — `ai.embed` into a vector `store.index`, searched via `fx.search`
|
|
1133
|
-
- [Gate](/docs/elements/gate) — `pii:reveal` and
|
|
1137
|
+
- [Gate](/docs/elements/gate) — `pii:reveal`, `gate.auth.tenant`, and `oke.tenant()`
|
|
1134
1138
|
- [CLI Reference](/docs/reference/cli) — `oke db push` · `generate` · `migrate`
|
|
1135
1139
|
- [Configuration](/docs/reference/configuration) — `drivers.store` maps and `images` pins
|
|
1136
1140
|
- [Environment variables](/docs/reference/environment-variables) — Redis · S3 · meilisearch URLs
|
|
@@ -151,6 +151,7 @@ Every name passed to `vault.env.required` is registered, so boot reports missing
|
|
|
151
151
|
| `schema` | zod / Standard Schema | Validated at boot; a bad value fails boot like a missing one |
|
|
152
152
|
| `dev` | string | Local-only fallback when no source provides a value (never used in prod) |
|
|
153
153
|
| `sensitive` | boolean | Override the default (`true` for secrets, `false` for config) |
|
|
154
|
+
| `perTenant` | boolean | Resolve at request time under `{tenantId}/{contract}`. Default `true` when `gate.auth.tenant` is on. Boot skips these contracts. |
|
|
154
155
|
|
|
155
156
|
### Zero-setup Compose fallback
|
|
156
157
|
|
|
@@ -201,6 +202,9 @@ The **Console** (`:6533`) **Vault** page (`/vault`) groups **Secrets** and
|
|
|
201
202
|
**Config**. Secrets are write-only: a salted fingerprint (`sha256:…`), never
|
|
202
203
|
the cleartext. Config rows show the value — that is `vault.config()`.
|
|
203
204
|
|
|
205
|
+
API keys are principals — mint them on **Access** (`/access`), not Vault.
|
|
206
|
+
See [Gate](/docs/elements/gate#api-keys).
|
|
207
|
+
|
|
204
208
|
Add (`+`) creates a contract from Console as well as from source. Those rows
|
|
205
209
|
show `console` until you declare the same name in the Manifest.
|
|
206
210
|
|
|
@@ -294,6 +298,20 @@ For production built-in stores, set `vault.encryption.masterKey` to a KMS source
|
|
|
294
298
|
`@aws-sdk/client-kms`) or use `managed` with a provider below. `oke doctor` warns when prod would
|
|
295
299
|
take the master key from the environment.
|
|
296
300
|
|
|
301
|
+
## Tenant isolation (built-in store)
|
|
302
|
+
|
|
303
|
+
When `gate.auth.tenant` is on, `perTenant` contracts resolve at request time under
|
|
304
|
+
`{tenantId}/{contract}`. Boot skips those contracts.
|
|
305
|
+
|
|
306
|
+
The built-in Postgres table `oke_vault_secrets` carries `tenant_id` and the same
|
|
307
|
+
`oke.tenant()` helper as domain tables — not a second GUC.
|
|
308
|
+
|
|
309
|
+
<Callout title="RLS on ciphertext rows">
|
|
310
|
+
`tenant_id = oke.tenant() OR tenant_id IS NULL`. ENABLE without FORCE: the adapter (owner) still
|
|
311
|
+
sees every row; Console / `oke_app` cannot list another tenant's ciphertext. Global `NULL` rows
|
|
312
|
+
stay visible to every stamped tenant.
|
|
313
|
+
</Callout>
|
|
314
|
+
|
|
297
315
|
## Managed providers (official)
|
|
298
316
|
|
|
299
317
|
Set `drivers.vault` to `"managed"` and `OKE_VAULT_PROVIDER` to an id below. Omit the provider when
|
|
@@ -346,6 +364,13 @@ radius before you rotate, and `is:overdue` when a secret is past its
|
|
|
346
364
|
Prefer `oke vault unseal --key -` and pipe the key on stdin, or omit `--key` on a
|
|
347
365
|
TTY and type it at the hidden prompt. Avoid `--key <base64>` on shared hosts.
|
|
348
366
|
|
|
367
|
+
</Accordion>
|
|
368
|
+
<Accordion title="Console SQL lists another tenant's vault rows">
|
|
369
|
+
|
|
370
|
+
User-plane SQL is stamped `oke.tenant()`. The policy hides other tenants' ciphertext
|
|
371
|
+
and keeps global `NULL` rows. The Vault adapter is table owner, so ENABLE (not FORCE)
|
|
372
|
+
does not hide rows from itself.
|
|
373
|
+
|
|
349
374
|
</Accordion>
|
|
350
375
|
<Accordion title="Expired secrets still take space in Postgres">
|
|
351
376
|
|
|
@@ -365,6 +390,7 @@ Schedule the live command with cron when you want automatic cleanup.
|
|
|
365
390
|
|
|
366
391
|
- [Project structure](/docs/get-started/project-structure) — `flow({…})` names as `unit.export`; `flow("billing.charge")` still wins
|
|
367
392
|
- [Flow](/docs/elements/flow) — how `fx.vault.get` reads secrets inside `do`
|
|
393
|
+
- [Gate](/docs/elements/gate) — `perTenant` contracts when `gate.auth.tenant` is on
|
|
368
394
|
- [CLI Reference](/docs/reference/cli) — `oke vault set` · `list` · `import`
|
|
369
395
|
|
|
370
396
|
## Next
|
|
@@ -701,7 +701,7 @@ Plugin `.binding()` is never inferred. File tree is app flows only.
|
|
|
701
701
|
|
|
702
702
|
```typescript title="flows/my/index.ts"
|
|
703
703
|
export const tasks = on(
|
|
704
|
-
http.get("/me/tasks").gate(member)
|
|
704
|
+
http.get("/me/tasks").gate(member),
|
|
705
705
|
flow("my.tasks", {
|
|
706
706
|
do: async (_input, fx) => fx.store(db).select().from(tasks),
|
|
707
707
|
}),
|
|
@@ -773,9 +773,9 @@ import { noteCreated } from "./signals";
|
|
|
773
773
|
|
|
774
774
|
### `http.resource` in a tree
|
|
775
775
|
|
|
776
|
-
Five reserved leaves reproduce `GET|POST /notes` and `GET|PATCH|DELETE /notes/:id`, but not one shared `.gate()`
|
|
776
|
+
Five reserved leaves reproduce `GET|POST /notes` and `GET|PATCH|DELETE /notes/:id`, but not one shared `.gate()` and not `store.resource().all()` in a single declaration.
|
|
777
777
|
|
|
778
|
-
Put `on(http.resource("/notes", ops).public()
|
|
778
|
+
Put `on(http.resource("/notes", ops).public())` in `route.ts`. Inference is skipped — the five verbs already declare those paths. Extra actions (`[id]/archive.ts`) sit beside it. Pathless `http.resource()` is not a thing.
|
|
779
779
|
|
|
780
780
|
| Verb | Path |
|
|
781
781
|
| -------- | ------------------- |
|
|
@@ -790,7 +790,7 @@ Put `on(http.resource("/notes", ops).public().live())` in `route.ts`. Inference
|
|
|
790
790
|
|
|
791
791
|
```typescript title="flows/notes/route.ts"
|
|
792
792
|
const notesR = store.resource(db, notes, { in: NewNote, out: Note });
|
|
793
|
-
export const mounted = on(http.resource("/notes", notesR.all()).public()
|
|
793
|
+
export const mounted = on(http.resource("/notes", notesR.all()).public());
|
|
794
794
|
```
|
|
795
795
|
|
|
796
796
|
</Tab>
|
|
@@ -65,7 +65,7 @@ oke docker clean --yes # non-TTY: current project only
|
|
|
65
65
|
oke docker clean --all --yes # non-TTY: every oke-dev-* project on this machine
|
|
66
66
|
oke images pin # tags → digests in oke.images.lock
|
|
67
67
|
|
|
68
|
-
oke build --target edge # <
|
|
68
|
+
oke build --target edge # < 16 kB kernel profile
|
|
69
69
|
oke eval # run prompt eval sets; fails CI on regression
|
|
70
70
|
oke ai setup # configure AI driver + models (TTY wizard or flags)
|
|
71
71
|
oke branch prod --at "yesterday" # fork journaled state into a sandbox
|