okengine 0.14.1 → 0.16.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +3 -4
- package/site/content/docs/elements/ai.mdx +2 -1
- package/site/content/docs/elements/channel.mdx +1 -0
- package/site/content/docs/elements/clock.mdx +3 -0
- package/site/content/docs/elements/flow.mdx +9 -0
- package/site/content/docs/elements/gate.mdx +80 -22
- package/site/content/docs/elements/signal.mdx +4 -4
- package/site/content/docs/elements/store.mdx +84 -27
- package/site/content/docs/elements/vault.mdx +4 -0
- package/site/content/docs/get-started/basic-usage.mdx +22 -13
- package/site/content/docs/get-started/index.mdx +5 -0
- package/site/content/docs/get-started/introduction.mdx +11 -3
- package/site/content/docs/get-started/meta.json +1 -1
- package/site/content/docs/get-started/project-structure.mdx +932 -0
- package/site/content/docs/reference/cli.md +13 -10
- package/site/content/docs/reference/client.mdx +28 -21
- package/site/content/docs/reference/fx.mdx +29 -18
- package/site/content/docs/reference/security.md +2 -2
- package/src/auth/api-key-sql.test.ts +47 -0
- package/src/auth/api-key-sql.ts +254 -0
- package/src/auth/api-keys.ts +273 -14
- package/src/auth/auth.test.ts +56 -0
- package/src/auth/bindings.ts +14 -5
- package/src/auth/config.ts +5 -5
- package/src/auth/cross-plane.ts +10 -3
- package/src/auth/index.ts +25 -1
- package/src/auth/schema.ts +8 -0
- package/src/cli/ai-setup/ai-setup.test.ts +62 -0
- package/src/cli/ai-setup/apply.ts +32 -1
- package/src/cli/attach-host-console.test.ts +40 -2
- package/src/cli/attach-host-console.ts +9 -0
- package/src/cli/build.test.ts +4 -2
- package/src/cli/db-seed.ts +48 -15
- package/src/cli/db.test.ts +104 -0
- package/src/cli/db.ts +122 -10
- package/src/cli/dev-controls.test.ts +20 -0
- package/src/cli/dev-controls.ts +13 -0
- package/src/cli/dev.test.ts +15 -4
- package/src/cli/dev.ts +102 -29
- package/src/cli/ensure-drizzle-config.ts +2 -1
- package/src/cli/tui/DevLive.tsx +12 -2
- package/src/cli/tui/keys.test.ts +2 -1
- package/src/cli/tui/keys.ts +3 -2
- package/src/compiler/aot.test.ts +1 -1
- package/src/compiler/effects-infer.ts +17 -2
- package/src/compiler/extract.test.ts +409 -33
- package/src/compiler/extract.ts +318 -67
- package/src/compiler/flow-path.test.ts +96 -0
- package/src/compiler/flow-path.ts +173 -0
- package/src/compiler/generate-adopt.test.ts +112 -3
- package/src/compiler/generate-adopt.ts +280 -26
- package/src/compiler/schema-from-ast.ts +609 -0
- package/src/console/server/access.test.ts +116 -8
- package/src/console/server/access.ts +70 -23
- package/src/console/server/app.ts +1 -0
- package/src/console/server/bind.ts +2 -2
- package/src/console/server/console.test.ts +3 -0
- 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 +233 -98
- package/src/console/server/gates.test.ts +24 -0
- package/src/console/server/gates.ts +8 -21
- package/src/console/server/index.ts +1 -0
- package/src/console/server/invoke-user-flow.test.ts +3 -1
- package/src/console/server/invoke-user-flow.ts +17 -2
- package/src/console/server/iso-at.test.ts +9 -0
- package/src/console/server/iso-at.ts +11 -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/security-headers.ts +6 -3
- package/src/console/server/spa-proxy.test.ts +1 -0
- package/src/console/server/sql-catalog.ts +147 -7
- package/src/console/server/sql-rls.test.ts +16 -0
- package/src/console/server/state.ts +37 -55
- package/src/console/server/store.test.ts +7 -4
- package/src/console/server/store.ts +142 -58
- package/src/console/ui-next/dist/assets/access-page-CAGHrA9H.js +4 -0
- package/src/console/ui-next/dist/assets/agent-disclosure-BP0Y0Sux.js +1 -0
- 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/{duration-tone-oiRxPVsZ.js → duration-tone-sC3lGABz.js} +1 -1
- 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-B-OUtiAu.js +1 -0
- package/src/console/ui-next/dist/assets/{highlighted-json-CvDPvveV.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-CGoZkILK.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-BDyXalNR.js +4 -0
- package/src/console/ui-next/dist/assets/react-D8E3mtu1.js +1 -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-CL_X0GCj.js → skeleton-D-czQJT6.js} +1 -1
- package/src/console/ui-next/dist/assets/store-page-Xh8Kn3rx.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-DkOXA12R.js +54 -0
- package/src/console/ui-next/dist/assets/units-page-Dpk40kOQ.js +1 -0
- package/src/console/ui-next/dist/assets/{use-vault-list-Cl79j_ku.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-B1dB9Ft0.js +2 -0
- package/src/console/ui-next/dist/index.html +12 -10
- package/src/console/ui-next/index.html +3 -3
- package/src/console/ui-next/src/client.ts +202 -7
- package/src/console/ui-next/src/components/motion/table/index.tsx +16 -0
- package/src/console/ui-next/src/components/motion/table/types.ts +4 -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/kbd.tsx +2 -2
- 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/traces/http-method.ts +19 -0
- package/src/console/ui-next/src/features/flows/traces/trace-detail.test.ts +19 -1
- package/src/console/ui-next/src/features/flows/traces/traces-pane.tsx +10 -3
- package/src/console/ui-next/src/features/not-found/not-found-page.tsx +1 -1
- package/src/console/ui-next/src/features/store/detail/browse-section.tsx +45 -1
- package/src/console/ui-next/src/features/store/detail/resource-panel.tsx +78 -16
- package/src/console/ui-next/src/features/store/detail/store-row-detail-sheet.tsx +155 -14
- package/src/console/ui-next/src/features/store/explorer/store-tree.tsx +33 -133
- package/src/console/ui-next/src/features/store/files/file-preview.tsx +4 -1
- package/src/console/ui-next/src/features/store/grid/rls-policy-sheet.tsx +326 -165
- package/src/console/ui-next/src/features/store/grid/sql-insert-sheet.tsx +6 -0
- package/src/console/ui-next/src/features/store/grid/store-data-grid.tsx +50 -3
- package/src/console/ui-next/src/features/store/lib/files-meta.test.ts +3 -0
- package/src/console/ui-next/src/features/store/lib/files-meta.ts +12 -0
- package/src/console/ui-next/src/features/store/lib/query-defaults.test.ts +25 -0
- package/src/console/ui-next/src/features/store/lib/query-defaults.ts +85 -3
- package/src/console/ui-next/src/features/store/lib/query-gate.ts +30 -4
- package/src/console/ui-next/src/features/store/lib/rls-policy.test.ts +139 -0
- package/src/console/ui-next/src/features/store/lib/rls-policy.ts +223 -21
- package/src/console/ui-next/src/features/store/lib/schema-graph.ts +4 -0
- package/src/console/ui-next/src/features/store/lib/sql-catalog.test.ts +24 -0
- package/src/console/ui-next/src/features/store/lib/sql-catalog.ts +19 -0
- package/src/console/ui-next/src/features/store/lib/store-tree.test.ts +6 -0
- package/src/console/ui-next/src/features/store/lib/store-tree.ts +13 -0
- package/src/console/ui-next/src/features/store/query/query-console.tsx +48 -14
- package/src/console/ui-next/src/features/store/query/query-gate-menu.tsx +19 -20
- package/src/console/ui-next/src/features/store/query/query-gate-parts.tsx +2 -1
- package/src/console/ui-next/src/features/store/query/query-results.tsx +110 -83
- package/src/console/ui-next/src/features/store/schema/schema-table-node.tsx +17 -0
- package/src/console/ui-next/src/features/units/call/call-api-panel.tsx +112 -72
- package/src/console/ui-next/src/features/units/lib/contract-input.test.ts +206 -1
- package/src/console/ui-next/src/features/units/lib/contract-input.ts +55 -0
- package/src/console/ui-next/src/features/vault/lib/types.ts +1 -1
- 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-store.ts +43 -15
- package/src/console/xss.gate.test.ts +1 -0
- package/src/docker/compose-health.test.ts +27 -0
- package/src/docker/compose-health.ts +19 -3
- package/src/docker/docker.test.ts +3 -0
- package/src/docker/recipes/pgdog.ts +2 -0
- package/src/drivers/clock-postgres.ts +13 -9
- package/src/drivers/instances-postgres.ts +4 -6
- package/src/drivers/journal-postgres.ts +13 -9
- package/src/drivers/pg-rls.test.ts +87 -0
- package/src/drivers/pg-rls.ts +233 -7
- package/src/drivers/pglite.ts +26 -3
- package/src/drivers/postgres.test.ts +23 -0
- package/src/drivers/postgres.ts +133 -9
- package/src/drivers/types.ts +10 -0
- package/src/elements/clock/declare.ts +26 -1
- package/src/elements/gate/boot.ts +5 -5
- package/src/elements/gate/declare.ts +38 -4
- package/src/elements/gate/flatten.ts +1 -1
- package/src/elements/gate/runtime.ts +1 -1
- package/src/elements/store/cache.ts +1 -1
- package/src/elements/store/declare.ts +9 -6
- package/src/elements/store/emit-drizzle.ts +44 -8
- package/src/elements/store/resource.test.ts +1 -1
- package/src/elements/store/rls-identity.test.ts +95 -0
- package/src/elements/store/rls-identity.ts +116 -0
- package/src/elements/store/runtime.ts +18 -0
- package/src/elements/store/schema-decl.test.ts +37 -0
- package/src/elements/store/schema-decl.ts +260 -41
- package/src/elements/store/seed.ts +1 -1
- package/src/elements/store/sql-condition.test.ts +12 -0
- package/src/elements/store/sql-condition.ts +26 -2
- package/src/elements/store/sql-rls-isolation.test.ts +165 -0
- package/src/elements/store/sql-rls-stamp.test.ts +117 -0
- package/src/elements/store/sql-select.types.test.ts +71 -0
- package/src/elements/store/sql-session.ts +135 -35
- package/src/elements/store/upsert-app.test.ts +1 -1
- package/src/elements/store.ts +17 -0
- package/src/http.ts +20 -3
- package/src/index.ts +11 -1
- package/src/kernel/adopt-barrel-fresh.test.ts +1 -1
- package/src/kernel/adopt-routes.ts +30 -2
- package/src/kernel/api-key-host-persist.test.ts +143 -0
- package/src/kernel/app.ts +145 -16
- package/src/kernel/auth-resolve.ts +10 -5
- package/src/kernel/auto-cache.test.ts +8 -8
- package/src/kernel/auto-registry.test.ts +49 -9
- package/src/kernel/boot.test.ts +5 -5
- package/src/kernel/boot.ts +2 -2
- package/src/kernel/effects-stamping.test.ts +3 -3
- package/src/kernel/element-registries.ts +12 -5
- package/src/kernel/errors.ts +24 -0
- package/src/kernel/flow-units.ts +64 -0
- package/src/kernel/flow.ts +29 -17
- package/src/kernel/fx-auth-keys.test.ts +88 -0
- package/src/kernel/fx-auth-keys.ts +198 -0
- package/src/kernel/fx.test.ts +17 -1
- package/src/kernel/fx.ts +59 -18
- package/src/kernel/horizontal-child.ts +4 -4
- package/src/kernel/http-path-pending.ts +20 -0
- package/src/kernel/http-resource.ts +4 -0
- package/src/kernel/http-stream.test.ts +4 -4
- package/src/kernel/pipeline-api-key.test.ts +192 -7
- package/src/kernel/pipeline.test.ts +2 -2
- package/src/kernel/pipeline.ts +4 -2
- package/src/kernel/plugin-elements.test.ts +1 -1
- package/src/kernel/plugin-needs.test.ts +1 -1
- package/src/kernel/ready.test.ts +2 -2
- package/src/kernel/resource-mount.test.ts +14 -0
- package/src/kernel/run-duration.test.ts +2 -2
- package/src/kernel/stamp-http.test.ts +79 -0
- package/src/kernel/stamp-http.ts +75 -0
- package/src/kernel/triggers.ts +52 -27
- package/src/manifest/types.ts +22 -3
- package/src/release/build-lib.ts +1 -0
- package/src/release/measure.ts +2 -2
- package/src/runtime/json-code-block.test.ts +2 -2
- package/src/runtime/serve.test.ts +4 -4
- package/src/term.test.ts +52 -4
- package/src/term.ts +125 -17
- package/src/test/reset-element-registries.ts +8 -3
- package/src/cli/ask-seed.test.ts +0 -96
- package/src/cli/ask-seed.ts +0 -82
- package/src/console/ui-next/dist/assets/cache-glyph-BanhLsEY.js +0 -1
- package/src/console/ui-next/dist/assets/call-pii-button-bqkxMrJH.js +0 -1
- package/src/console/ui-next/dist/assets/collapsible-DYb0xU8C.js +0 -1
- package/src/console/ui-next/dist/assets/confirm-sheet-DDCRmG62.js +0 -1
- package/src/console/ui-next/dist/assets/explorer-empty-HjTnVQoR.js +0 -1
- package/src/console/ui-next/dist/assets/flows-page-DxDsOd4f.js +0 -1
- package/src/console/ui-next/dist/assets/http-method-CJCBYL2j.js +0 -1
- package/src/console/ui-next/dist/assets/index-Ce6WKWKM.js +0 -66
- package/src/console/ui-next/dist/assets/index-D4Ldtj79.css +0 -2
- package/src/console/ui-next/dist/assets/link-DF7SZ9Ek.js +0 -1
- package/src/console/ui-next/dist/assets/observability-page-BEZDzyYh.js +0 -4
- package/src/console/ui-next/dist/assets/query-gate-parts-1m8m1iNp.js +0 -2
- package/src/console/ui-next/dist/assets/react-B1ML8gxg.js +0 -1
- package/src/console/ui-next/dist/assets/replica-lag-C8_BRt2x.js +0 -18
- package/src/console/ui-next/dist/assets/sheet-form-D-ata7jy.js +0 -1
- package/src/console/ui-next/dist/assets/shortcut-Cgzuv4k1.js +0 -1
- package/src/console/ui-next/dist/assets/shortcut-keys-DO4IsVqv.js +0 -1
- package/src/console/ui-next/dist/assets/store-page-v3LXdYpr.js +0 -45
- package/src/console/ui-next/dist/assets/trace-detail-sheet-DFLFfUUX.js +0 -2
- package/src/console/ui-next/dist/assets/units-page-C0gW6Kdo.js +0 -1
- package/src/console/ui-next/dist/assets/vault-page-DuKzqwzW.js +0 -2
|
@@ -17,6 +17,7 @@ oke --help # command list (also what bare `oke` prints whe
|
|
|
17
17
|
|
|
18
18
|
oke dev # watch · hot reload · Console :6533 · app :6530 · MCP :6535 · docs MCP :6536
|
|
19
19
|
# → always Docker Compose under docker/ + host Bun
|
|
20
|
+
# → regenerates flows/generated.ts when the flows tree changes
|
|
20
21
|
# → client types + domain schema push on save
|
|
21
22
|
# → Console UI hot-reloads from source (okengine checkout; no bun run build)
|
|
22
23
|
oke dev --no-db-push # opt out of auto oke db push on schema input save
|
|
@@ -48,7 +49,7 @@ oke schema generate # core + plugin stubs → .oke/schema/oke.ts (-
|
|
|
48
49
|
oke db push # domain schema.ts → live DB (dev; drizzle-kit)
|
|
49
50
|
oke db generate # versioned SQL under drizzle/ (review)
|
|
50
51
|
oke db migrate # apply migrations (explicit; never auto in prod)
|
|
51
|
-
oke db seed # this app's defineSeed (name + essential + env)
|
|
52
|
+
oke db seed # this app's defineSeed (name + essential + env) — standard CLI path
|
|
52
53
|
oke db seed --env prod --force # CI: skip prod confirmation prompt
|
|
53
54
|
oke db studio # drizzle-kit Studio (long-running)
|
|
54
55
|
oke vault set STRIPE_KEY # also: list · import .env · key rotate
|
|
@@ -112,24 +113,26 @@ not spawn a second stack. Non-TTY / CI keeps the classic help listing
|
|
|
112
113
|
`oke dev` always starts Compose (requires a running Docker daemon). Boot chrome
|
|
113
114
|
prints immediately (wordmark + Starting + profile), then streams background work
|
|
114
115
|
(compose up, vault, per-service health, AI probe). Status ● is green ready ·
|
|
115
|
-
yellow pending/loading · red error · dim idle.
|
|
116
|
+
yellow pending/loading · red error · dim idle. After bind, Backend · Console ·
|
|
117
|
+
MCP · Docs MCP each get a box (URL + one-line purpose). Compose health keeps polling
|
|
116
118
|
(`docker compose ps -a`); AI ● tracks model phase while the AI container is up.
|
|
117
119
|
Boot does not wait for the model to become ready. A successful session writes
|
|
118
120
|
`.oke/dev.json` (pid · ports · startedAt) and clears it on stop. Durable local
|
|
119
|
-
markers live in `.oke/state.json`
|
|
120
|
-
prompt for this app's `defineSeed({ name })`) — not in the session lock. Console session signing uses
|
|
121
|
+
markers live in `.oke/state.json` — not in the session lock. Console session signing uses
|
|
121
122
|
`.oke/console.secret` (or `OKE_CONSOLE_SECRET`); that is not a Vault secret.
|
|
122
123
|
|
|
123
124
|
On a TTY, Ink keyboard controls stay active after boot (`useInput` — same
|
|
124
125
|
shortcuts as before). Press `r` to clear the log pane and reprint the latest
|
|
125
126
|
board.
|
|
126
127
|
|
|
127
|
-
| Key
|
|
128
|
-
|
|
|
129
|
-
| `?`
|
|
130
|
-
| `r`
|
|
131
|
-
| `
|
|
132
|
-
| `
|
|
128
|
+
| Key | Action |
|
|
129
|
+
| --- | ----------------------------------- |
|
|
130
|
+
| `?` | help |
|
|
131
|
+
| `r` | refresh — clear logs, show latest ● |
|
|
132
|
+
| `s` | `oke db seed` |
|
|
133
|
+
| `q` | quit `oke dev` |
|
|
134
|
+
| `u` | compose up (whole stack) |
|
|
135
|
+
| `x` | compose stop (whole stack) |
|
|
133
136
|
|
|
134
137
|
Compose stays quiet unless it fails. Process-local boot notices print once under
|
|
135
138
|
the hero (not once per process).
|
|
@@ -5,7 +5,7 @@ icon: "MonitorSmartphone"
|
|
|
5
5
|
source: "docs/spec/unified-theory.md"
|
|
6
6
|
---
|
|
7
7
|
|
|
8
|
-
`okengine/client` is how a browser, CLI, or another service calls your app's flows.
|
|
8
|
+
`okengine/client` is how a browser, CLI, or another service calls your app's flows. Import the generated barrel, take `typeof app`, and `api.notes.get({ id })` is fully typed — same contracts the server already has, no separate schema project.
|
|
9
9
|
|
|
10
10
|
<Callout title="The one rule">
|
|
11
11
|
Treat every call as a result envelope: `{ data, error }`. Flow failures are values you switch on
|
|
@@ -21,19 +21,22 @@ source: "docs/spec/unified-theory.md"
|
|
|
21
21
|
### Adopt flows and export `App`
|
|
22
22
|
|
|
23
23
|
```typescript title="src/app.ts"
|
|
24
|
-
import
|
|
25
|
-
import
|
|
24
|
+
import "@/core";
|
|
25
|
+
import "@/flows/generated";
|
|
26
|
+
import { oke } from "okengine/http";
|
|
26
27
|
|
|
27
|
-
export const app = oke({ name: "
|
|
28
|
+
export const app = oke({ name: "notes" });
|
|
28
29
|
export type App = typeof app;
|
|
29
30
|
```
|
|
30
31
|
|
|
32
|
+
`.adopt({ notes })` is optional and additive.
|
|
33
|
+
|
|
31
34
|
</Step>
|
|
32
35
|
|
|
33
36
|
<Step>
|
|
34
37
|
### Create the client
|
|
35
38
|
|
|
36
|
-
Same repo — pass the app value so HTTP triggers hit REST (method + path from
|
|
39
|
+
Same repo — pass the app value so HTTP triggers hit REST (method + path from `$routes`):
|
|
37
40
|
|
|
38
41
|
```typescript title="client"
|
|
39
42
|
import { createClient } from "okengine/client";
|
|
@@ -114,7 +117,7 @@ oke client add https://api.example.com --out ./types/oke-client.d.ts
|
|
|
114
117
|
| Incomplete proxy path (`api.notes()` with no flow) | Result error: `Incomplete path: api.notes(…)` |
|
|
115
118
|
|
|
116
119
|
```typescript
|
|
117
|
-
// REST —
|
|
120
|
+
// REST — method/path from the bound HTTP trigger (file tree or explicit)
|
|
118
121
|
await api.notes.get({ id: "n_1" }); // GET /notes/n_1
|
|
119
122
|
|
|
120
123
|
// RPC — untriggered flow named notes.stats
|
|
@@ -177,6 +180,9 @@ React: `useSession(api, session)` from `okengine/client-react`.
|
|
|
177
180
|
| HTTP **401** | `refresh()` runs **once**, then the same call retries |
|
|
178
181
|
| HTTP **403** / **429** | No refresh — decode the failure envelope as usual |
|
|
179
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
|
+
|
|
180
186
|
**Consequence:** `refresh` must mutate whatever `getToken` reads. Returning a new string alone does
|
|
181
187
|
nothing if storage was not updated.
|
|
182
188
|
|
|
@@ -210,16 +216,16 @@ core today: cookie jars or plugin `.client()` decorations. Browser apps: also se
|
|
|
210
216
|
outcome by calling a Flow that uses it — or by handling a gate denial on that call.
|
|
211
217
|
</Callout>
|
|
212
218
|
|
|
213
|
-
| Element | On the client | How
|
|
214
|
-
| --------------------------------- | ------------- |
|
|
215
|
-
| [Flow](/docs/elements/flow) | Direct | `api.unit.flow(input)` — the only public surface
|
|
216
|
-
| [Gate](/docs/elements/gate) | Indirect | Bearer via `auth`; denials as `Unauthorized` / `Forbidden` / `RateLimited`
|
|
217
|
-
| [Store](/docs/elements/store) | Via Flows | `fx.store` inside Flows; `store.resource` + `on(http.resource…)` → five Flows
|
|
218
|
-
| [Signal](/docs/elements/signal) | Via Flows | Emit/consume server-side; no subscribe API on `okengine/client` yet
|
|
219
|
-
| [Clock](/docs/elements/clock) | Via Flows | Schedules fire on the server — the client never ticks a clock
|
|
220
|
-
| [Vault](/docs/elements/vault) | Via Flows | Secrets stay server-side; never ship them to the browser package
|
|
221
|
-
| [Channel](/docs/elements/channel) | Via Flows | `fx.send` in a Flow — the client does not send email/SMS/push
|
|
222
|
-
| [AI](/docs/elements/ai) | Via Flows | `fx.ask` / `fx.run` inside a Flow; the client gets that Flow’s `out`
|
|
219
|
+
| Element | On the client | How |
|
|
220
|
+
| --------------------------------- | ------------- | ------------------------------------------------------------------------------------------ |
|
|
221
|
+
| [Flow](/docs/elements/flow) | Direct | `api.unit.flow(input)` — the only public surface |
|
|
222
|
+
| [Gate](/docs/elements/gate) | Indirect | Bearer via `auth`; denials as `Unauthorized` / `Forbidden` / `RateLimited` |
|
|
223
|
+
| [Store](/docs/elements/store) | Via Flows | `fx.store` inside Flows; `store.resource` + `on(http.resource…)` → five Flows on `$routes` |
|
|
224
|
+
| [Signal](/docs/elements/signal) | Via Flows | Emit/consume server-side; no subscribe API on `okengine/client` yet |
|
|
225
|
+
| [Clock](/docs/elements/clock) | Via Flows | Schedules fire on the server — the client never ticks a clock |
|
|
226
|
+
| [Vault](/docs/elements/vault) | Via Flows | Secrets stay server-side; never ship them to the browser package |
|
|
227
|
+
| [Channel](/docs/elements/channel) | Via Flows | `fx.send` in a Flow — the client does not send email/SMS/push |
|
|
228
|
+
| [AI](/docs/elements/ai) | Via Flows | `fx.ask` / `fx.run` inside a Flow; the client gets that Flow’s `out` |
|
|
223
229
|
|
|
224
230
|
### Store resources
|
|
225
231
|
|
|
@@ -227,7 +233,7 @@ Mount a resource, adopt the returned ops, then call the five Flows like any othe
|
|
|
227
233
|
|
|
228
234
|
```typescript
|
|
229
235
|
const notesR = store.resource(db, notes, {/* in, out, list */});
|
|
230
|
-
const mounted = on(http.resource("/notes", notesR.all()).
|
|
236
|
+
const mounted = on(http.resource("/notes", notesR.all()).public().live());
|
|
231
237
|
// .adopt({ notes: mounted }) →
|
|
232
238
|
const page = await api.notes.list({ limit: 20 });
|
|
233
239
|
const more = await page.next();
|
|
@@ -280,9 +286,9 @@ Budget: the `./client` export stays under the measured client-runtime cap (hard
|
|
|
280
286
|
|
|
281
287
|
<Accordion title="api.main.health is not a function / type error">
|
|
282
288
|
|
|
283
|
-
Confirm the
|
|
284
|
-
|
|
285
|
-
`oke dev` after renaming exports.
|
|
289
|
+
Confirm the Flow is `export`ed from a generated unit (`import "@/flows/generated"` then `oke({ name })`), or from a module you still `.adopt({ main })`.
|
|
290
|
+
|
|
291
|
+
Type `createClient` with that `App` (or ambient `Register` after `oke-client.d.ts` regenerates). Restart `oke dev` after renaming exports.
|
|
286
292
|
|
|
287
293
|
</Accordion>
|
|
288
294
|
|
|
@@ -321,7 +327,8 @@ calls `api.auth.refresh({ refreshToken })`. Re-login when rotation fails or no r
|
|
|
321
327
|
|
|
322
328
|
## Learn more
|
|
323
329
|
|
|
324
|
-
- [
|
|
330
|
+
- [Project structure](/docs/get-started/project-structure) — folders are the URL; `$routes` without `.adopt()`
|
|
331
|
+
- [Basic usage](/docs/get-started/basic-usage) — generated barrel → client → test loop
|
|
325
332
|
- [Gate](/docs/elements/gate) — policies, `gate.public`, denials
|
|
326
333
|
- [Store](/docs/elements/store) — `store.resource` and list query language
|
|
327
334
|
- [Flow](/docs/elements/flow) — `in` / `out` / `errors` and `fx.fail`
|
|
@@ -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>` |
|
|
@@ -72,16 +77,21 @@ on(
|
|
|
72
77
|
|
|
73
78
|
## Flows
|
|
74
79
|
|
|
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?)`
|
|
80
|
+
| Signature | Records | Returns / notes |
|
|
81
|
+
| ------------------------------------------------------------------------------ | ----------------------- | -------------------------------------------------------------------------------------------------- |
|
|
82
|
+
| `fx.call(flow, input?)` | `call` | The callee's `out` — runs through the same pipeline |
|
|
83
|
+
| `fx.step(name, fn)` | — | Durable step: replays from the journal, never re-runs |
|
|
84
|
+
| `fx.all([...thunks])` | — | Parallel; first rejection aborts siblings |
|
|
85
|
+
| `fx.race([...thunks])` | — | First settle wins; losers aborted |
|
|
86
|
+
| `fx.retry(fn, opts?)` | — | Exponential backoff + jitter (plain Promise) |
|
|
87
|
+
| `fx.using(acq, rel, use)` | — | `release` runs once on settle or ambient abort |
|
|
88
|
+
| `fx.signal` | — | Ambient `AbortSignal` for the current branch |
|
|
89
|
+
| `fx.fail(code, data, opts?)` | — | Typed failure value (`opts.message` overrides) |
|
|
90
|
+
| `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. |
|
|
91
|
+
| `fx.auth.listApiKeys()` | `read` `auth:api-keys` | Keys this session minted |
|
|
92
|
+
| `fx.auth.revokeApiKey(id)` | `write` `auth:api-keys` | Owner only |
|
|
93
|
+
| `fx.auth.rotateApiKey(id)` | `write` `auth:api-keys` | New secret once. Owner only |
|
|
94
|
+
| `fx.auth.updateApiKey(id, …)` | `write` `auth:api-keys` | Name / scopes / expiry / allowlist / rate. Re-attenuates |
|
|
85
95
|
|
|
86
96
|
`fx.call` starts the callee with an **empty** `fx.auth` (fail-closed for authorization). For
|
|
87
97
|
audit/attribution only, read `fx.principal` — it propagates the originating identity without
|
|
@@ -265,15 +275,16 @@ Built-in `errors.*` / `oke.*` catalogs localize `fx.fail` messages and thrown
|
|
|
265
275
|
|
|
266
276
|
## Principals
|
|
267
277
|
|
|
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
|
|
278
|
+
| Property | Shape |
|
|
279
|
+
| -------------- | -------------------------------------------------------------------------- |
|
|
280
|
+
| `fx.auth` | `{ userId, scopes, verified?, apiKeyId? }` plus key methods (session only) |
|
|
281
|
+
| `fx.operator` | `{ id: string \| null }` — Console plane |
|
|
282
|
+
| `fx.principal` | Read-only origin: `userId`, `operatorId`, `scopes`, `verified?`, `plane?` |
|
|
283
|
+
| `fx.tenant` | `{ id: string \| null }` — active tenant |
|
|
274
284
|
|
|
275
285
|
**Consequence:** use `fx.auth` / gates for authorization; use `fx.principal` only when a callee
|
|
276
|
-
must log who started the call chain.
|
|
286
|
+
must log who started the call chain. A key Bearer sets `userId` to the issuer and `apiKeyId`
|
|
287
|
+
to the key — see [Gate](/docs/elements/gate#api-keys).
|
|
277
288
|
|
|
278
289
|
## Not on `fx`
|
|
279
290
|
|
|
@@ -41,13 +41,13 @@ Our path is concrete: a booking name containing "ignore previous instructions an
|
|
|
41
41
|
#### 10.4 Remaining closures
|
|
42
42
|
|
|
43
43
|
- **`invoke-as` is attenuated** exactly like an API key: an operator cannot assume a scope set they could not grant. Impersonating a real user is development-only. Console `POST /console/flows/invoke` runs the target flow on a bound host under the selected identity’s scopes (`trustedInvoke` is in-process only — never from public HTTP); when the host adapter is unbound, invoke fails closed.
|
|
44
|
-
- **Store SQL browse** masks classified PII by default. `QUERY /console/store/query` with `revealPii: true` returns cleartext for the current page and writes an audited `console.store.query.reveal` log (operator, ref, child). The SQL query console (`POST /console/store/sql`) stays masked unless `revealPii: true`, which writes `console.store.sql.reveal` (operator, ref). Call API (`POST /console/flows/invoke`) masks the same classified field names on the handler response unless `revealPii: true`, which writes `console.flows.invoke.reveal` and opens the host store session with cleartext so columns the handler reads (for example `ownerEmail`) are not already `[redacted]`. `asGate`
|
|
44
|
+
- **Store SQL browse** masks classified PII by default. `QUERY /console/store/query` with `revealPii: true` returns cleartext for the current page and writes an audited `console.store.query.reveal` log (operator, ref, child). The SQL query console (`POST /console/store/sql`) stays masked unless `revealPii: true`, which writes `console.store.sql.reveal` (operator, ref). Call API (`POST /console/flows/invoke`) masks the same classified field names on the handler response unless `revealPii: true`, which writes `console.flows.invoke.reveal` and opens the host store session with cleartext so columns the handler reads (for example `ownerEmail`) are not already `[redacted]`. `{ asGate, asUserId }` on SQL, browse, row edit, and Call API stamps `oke.gate()` / `oke.user()` / `oke.has_scope()` on postgres / pglite (one identity bag; policy-only leaves `oke.user()` empty — never `gate:${name}`); Operator / omit / `bypassGates` still bypasses RLS. The response may include `rls: { gate, userId, applied }`. Catalog DDL stays Operator. Per-cell `POST /console/store/reveal` remains available. The Console auth schema (`sql:oke_console` — `oke_operators`, sessions, roles, keys) is hidden from Store browse unless `OKE_CONSOLE_AUTH_STORE=1`. When listed it is **read-only**; edit / delete / raw SQL are refused so operator-plane rows are never joined to `public`.
|
|
45
45
|
- **Host WideEvent ingest** (`POST /console/runs/ingest`, `oke dev` only) is secret-gated and never echoes the event body. Operators read runs only through `projectRun` / `maskWideEventForConsole` on `GET /console/runs` and `/console/live` — the same PII boundary as Console-local runs.
|
|
46
46
|
- **Store SQL performance** (`QUERY /console/store/sql/stats`, `/locks`, `POST /console/store/sql/advise`) is engine-native query text — not Store browse masking. Statements show `pg_stat_statements` fingerprints (`$n`). Live `pg_stat_activity.query` is collapsed until `revealPii: true`, which writes `console.store.sql.stats.reveal` (operator, ref). Named limitation `StoreSqlStatsQueryTextGap`. This surface does not terminate sessions or call `pg_stat_statements_reset`.
|
|
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
|
+
});
|
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SQL persist / hydrate for {@link ApiKeyRow} on `oke_api_keys`.
|
|
3
|
+
*
|
|
4
|
+
* Host path: public schema on the app `store.sql()` connection
|
|
5
|
+
* (`sharedSqlConn` / `DATABASE_URL`). Console attaches that same
|
|
6
|
+
* {@link ApiKeyStore} — it does not own a second key table.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import type { SqlConnection } from "../drivers/types.ts";
|
|
10
|
+
import type { ApiKeyRow } from "./tables.ts";
|
|
11
|
+
import { AUTH_TABLES } from "./tables.ts";
|
|
12
|
+
import type { ApiKeyStore } from "./api-keys.ts";
|
|
13
|
+
|
|
14
|
+
/** Minimal SQL executor for the auth key table. */
|
|
15
|
+
export interface ApiKeySqlExec {
|
|
16
|
+
execute(sql: string, params: readonly unknown[]): Promise<void>;
|
|
17
|
+
all(sql: string): Promise<readonly Record<string, unknown>[]>;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Persist one key row to `oke_api_keys`.
|
|
22
|
+
*
|
|
23
|
+
* @param sql - Executor
|
|
24
|
+
* @param row - Key row
|
|
25
|
+
* @param table - Physical table (`oke_api_keys` or schema-qualified)
|
|
26
|
+
*/
|
|
27
|
+
export async function persistApiKeyRow(
|
|
28
|
+
sql: ApiKeySqlExec,
|
|
29
|
+
row: ApiKeyRow,
|
|
30
|
+
table: string = AUTH_TABLES.apiKeys,
|
|
31
|
+
): Promise<void> {
|
|
32
|
+
await sql.execute(
|
|
33
|
+
`INSERT INTO ${table} (
|
|
34
|
+
id, plane, hash, name, scopes, expires_at, rate_limit, ip_allowlist,
|
|
35
|
+
creator_id, creator_scopes, created_at, last_used_at, revoked_at
|
|
36
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
37
|
+
ON CONFLICT(id) DO UPDATE SET
|
|
38
|
+
hash = excluded.hash,
|
|
39
|
+
name = excluded.name,
|
|
40
|
+
scopes = excluded.scopes,
|
|
41
|
+
expires_at = excluded.expires_at,
|
|
42
|
+
rate_limit = excluded.rate_limit,
|
|
43
|
+
ip_allowlist = excluded.ip_allowlist,
|
|
44
|
+
creator_id = excluded.creator_id,
|
|
45
|
+
creator_scopes = excluded.creator_scopes,
|
|
46
|
+
last_used_at = excluded.last_used_at,
|
|
47
|
+
revoked_at = excluded.revoked_at`,
|
|
48
|
+
[
|
|
49
|
+
row.id,
|
|
50
|
+
row.plane,
|
|
51
|
+
row.hash,
|
|
52
|
+
row.name,
|
|
53
|
+
JSON.stringify(row.scopes),
|
|
54
|
+
row.expiresAt,
|
|
55
|
+
row.rateLimit ? JSON.stringify(row.rateLimit) : null,
|
|
56
|
+
JSON.stringify(row.ipAllowlist),
|
|
57
|
+
row.creatorId,
|
|
58
|
+
JSON.stringify(row.creatorScopes),
|
|
59
|
+
row.createdAt,
|
|
60
|
+
row.lastUsedAt,
|
|
61
|
+
row.revokedAt,
|
|
62
|
+
],
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Load every key row from `oke_api_keys` into a store.
|
|
68
|
+
*
|
|
69
|
+
* @param sql - Executor
|
|
70
|
+
* @param store - Destination store
|
|
71
|
+
* @param table - Physical table
|
|
72
|
+
*/
|
|
73
|
+
export async function hydrateApiKeyStore(
|
|
74
|
+
sql: ApiKeySqlExec,
|
|
75
|
+
store: ApiKeyStore,
|
|
76
|
+
table: string = AUTH_TABLES.apiKeys,
|
|
77
|
+
): Promise<void> {
|
|
78
|
+
const rows = await sql.all(`SELECT * FROM ${table}`);
|
|
79
|
+
for (const raw of rows) {
|
|
80
|
+
const row = rowFromSql(raw);
|
|
81
|
+
if (row) store.keys.set(row.id, row);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Bind write-through persist on a store.
|
|
87
|
+
*
|
|
88
|
+
* @param store - Key store
|
|
89
|
+
* @param sql - Executor
|
|
90
|
+
* @param table - Physical table
|
|
91
|
+
*/
|
|
92
|
+
export function bindApiKeySqlPersist(
|
|
93
|
+
store: ApiKeyStore,
|
|
94
|
+
sql: ApiKeySqlExec,
|
|
95
|
+
table: string = AUTH_TABLES.apiKeys,
|
|
96
|
+
): void {
|
|
97
|
+
store.persist = (row) => persistApiKeyRow(sql, row, table);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/** Runtime that can open the app's shared primary SQL connection. */
|
|
101
|
+
export interface HostApiKeySqlRuntime {
|
|
102
|
+
primarySql(): Promise<SqlConnection | undefined>;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Adapt a driver connection to {@link ApiKeySqlExec}.
|
|
107
|
+
*
|
|
108
|
+
* @param conn - Shared `store.sql()` connection
|
|
109
|
+
*/
|
|
110
|
+
export function apiKeySqlExec(conn: SqlConnection): ApiKeySqlExec {
|
|
111
|
+
return {
|
|
112
|
+
execute: async (sql, params) => {
|
|
113
|
+
await conn.exec(sql, params);
|
|
114
|
+
},
|
|
115
|
+
all: async (sql) => conn.query(sql),
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Create public `oke_api_keys` when missing (host schema, not `oke_console`).
|
|
121
|
+
*
|
|
122
|
+
* @param conn - Shared SQL connection
|
|
123
|
+
* @param table - Physical table
|
|
124
|
+
*/
|
|
125
|
+
export async function ensureApiKeyTable(
|
|
126
|
+
conn: SqlConnection,
|
|
127
|
+
table: string = AUTH_TABLES.apiKeys,
|
|
128
|
+
): Promise<void> {
|
|
129
|
+
await conn.exec(`
|
|
130
|
+
CREATE TABLE IF NOT EXISTS ${table} (
|
|
131
|
+
id TEXT PRIMARY KEY NOT NULL,
|
|
132
|
+
plane TEXT NOT NULL,
|
|
133
|
+
hash TEXT NOT NULL,
|
|
134
|
+
name TEXT NOT NULL,
|
|
135
|
+
scopes TEXT NOT NULL DEFAULT '[]',
|
|
136
|
+
expires_at BIGINT,
|
|
137
|
+
rate_limit TEXT,
|
|
138
|
+
ip_allowlist TEXT NOT NULL DEFAULT '[]',
|
|
139
|
+
creator_id TEXT NOT NULL,
|
|
140
|
+
creator_scopes TEXT NOT NULL DEFAULT '[]',
|
|
141
|
+
created_at BIGINT NOT NULL,
|
|
142
|
+
last_used_at BIGINT,
|
|
143
|
+
revoked_at BIGINT
|
|
144
|
+
)
|
|
145
|
+
`);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Hydrate and write-through-persist a host {@link ApiKeyStore} on `conn`.
|
|
150
|
+
*
|
|
151
|
+
* Leaves an already-bound `persist` hook in place (injected test stores).
|
|
152
|
+
*
|
|
153
|
+
* @param conn - App SQL connection (`sharedSqlConn`)
|
|
154
|
+
* @param store - `gate.auth.apiKeyStore`
|
|
155
|
+
* @param table - Physical table (`oke_api_keys` in `public`)
|
|
156
|
+
*/
|
|
157
|
+
export async function bindHostApiKeySql(
|
|
158
|
+
conn: SqlConnection,
|
|
159
|
+
store: ApiKeyStore,
|
|
160
|
+
table: string = AUTH_TABLES.apiKeys,
|
|
161
|
+
): Promise<void> {
|
|
162
|
+
await ensureApiKeyTable(conn, table);
|
|
163
|
+
const exec = apiKeySqlExec(conn);
|
|
164
|
+
await hydrateApiKeyStore(exec, store, table);
|
|
165
|
+
if (!store.persist) bindApiKeySqlPersist(store, exec, table);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Bind host key persist through {@link HostApiKeySqlRuntime.primarySql}.
|
|
170
|
+
*
|
|
171
|
+
* No-ops when the store runtime has no SQL driver (in-memory-only apps).
|
|
172
|
+
*
|
|
173
|
+
* @param runtime - Booted `store` element
|
|
174
|
+
* @param store - `gate.auth.apiKeyStore`
|
|
175
|
+
*/
|
|
176
|
+
export async function bindHostApiKeySqlFromStore(
|
|
177
|
+
runtime: HostApiKeySqlRuntime,
|
|
178
|
+
store: ApiKeyStore,
|
|
179
|
+
): Promise<void> {
|
|
180
|
+
const conn = await runtime.primarySql();
|
|
181
|
+
if (!conn) return;
|
|
182
|
+
await bindHostApiKeySql(conn, store);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function rowFromSql(raw: Record<string, unknown>): ApiKeyRow | null {
|
|
186
|
+
const id = asString(raw.id ?? raw.ID);
|
|
187
|
+
const plane = raw.plane === "operator" ? "operator" : "user";
|
|
188
|
+
const hash = asString(raw.hash);
|
|
189
|
+
const name = asString(raw.name);
|
|
190
|
+
const creatorId = asString(raw.creator_id ?? raw.creatorId);
|
|
191
|
+
if (!id || !hash || !name || !creatorId) return null;
|
|
192
|
+
return {
|
|
193
|
+
id,
|
|
194
|
+
plane,
|
|
195
|
+
hash,
|
|
196
|
+
name,
|
|
197
|
+
scopes: asStringArray(raw.scopes),
|
|
198
|
+
expiresAt: asNumberOrNull(raw.expires_at ?? raw.expiresAt),
|
|
199
|
+
rateLimit: asRateLimit(raw.rate_limit ?? raw.rateLimit),
|
|
200
|
+
ipAllowlist: asStringArray(raw.ip_allowlist ?? raw.ipAllowlist),
|
|
201
|
+
creatorId,
|
|
202
|
+
creatorScopes: asStringArray(raw.creator_scopes ?? raw.creatorScopes),
|
|
203
|
+
createdAt: asNumberOrNull(raw.created_at ?? raw.createdAt) ?? 0,
|
|
204
|
+
lastUsedAt: asNumberOrNull(raw.last_used_at ?? raw.lastUsedAt),
|
|
205
|
+
revokedAt: asNumberOrNull(raw.revoked_at ?? raw.revokedAt),
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function asString(value: unknown): string | null {
|
|
210
|
+
return typeof value === "string" && value.length > 0 ? value : null;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function asNumberOrNull(value: unknown): number | null {
|
|
214
|
+
if (typeof value === "number" && Number.isFinite(value)) return value;
|
|
215
|
+
if (typeof value === "string" && value.length > 0) {
|
|
216
|
+
const n = Number(value);
|
|
217
|
+
return Number.isFinite(n) ? n : null;
|
|
218
|
+
}
|
|
219
|
+
return null;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function asStringArray(value: unknown): string[] {
|
|
223
|
+
if (Array.isArray(value)) return value.filter((item): item is string => typeof item === "string");
|
|
224
|
+
if (typeof value === "string" && value.length > 0) {
|
|
225
|
+
try {
|
|
226
|
+
const parsed: unknown = JSON.parse(value);
|
|
227
|
+
if (Array.isArray(parsed)) {
|
|
228
|
+
return parsed.filter((item): item is string => typeof item === "string");
|
|
229
|
+
}
|
|
230
|
+
} catch {
|
|
231
|
+
return [];
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
return [];
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
function asRateLimit(value: unknown): { max: number; per: string } | null {
|
|
238
|
+
const parsed: unknown =
|
|
239
|
+
typeof value === "string" && value.length > 0
|
|
240
|
+
? (() => {
|
|
241
|
+
try {
|
|
242
|
+
return JSON.parse(value) as unknown;
|
|
243
|
+
} catch {
|
|
244
|
+
return null;
|
|
245
|
+
}
|
|
246
|
+
})()
|
|
247
|
+
: value;
|
|
248
|
+
if (!parsed || typeof parsed !== "object") return null;
|
|
249
|
+
const rec = parsed as { max?: unknown; per?: unknown };
|
|
250
|
+
if (typeof rec.max === "number" && typeof rec.per === "string") {
|
|
251
|
+
return { max: rec.max, per: rec.per };
|
|
252
|
+
}
|
|
253
|
+
return null;
|
|
254
|
+
}
|