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
|
@@ -44,7 +44,7 @@ describe("automatic tier-1 cache from effects", () => {
|
|
|
44
44
|
const db = store.sql("app", { schema: { notes } });
|
|
45
45
|
let lists = 0;
|
|
46
46
|
const list = on(
|
|
47
|
-
http.get("/notes").
|
|
47
|
+
http.get("/notes").public(),
|
|
48
48
|
flow("notes.list", {
|
|
49
49
|
out: z.array(NoteOut),
|
|
50
50
|
effects: { reads: ["sql:notes"] },
|
|
@@ -80,7 +80,7 @@ describe("automatic tier-1 cache from effects", () => {
|
|
|
80
80
|
resetFlowSeq();
|
|
81
81
|
const db = store.sql("app", { schema: { notes } });
|
|
82
82
|
const list = on(
|
|
83
|
-
http.get("/notes").
|
|
83
|
+
http.get("/notes").public(),
|
|
84
84
|
flow("notes.list", {
|
|
85
85
|
out: z.array(NoteOut),
|
|
86
86
|
effects: { reads: ["sql:notes"] },
|
|
@@ -115,7 +115,7 @@ describe("automatic tier-1 cache from effects", () => {
|
|
|
115
115
|
const db = store.sql("app", { schema: { notes } });
|
|
116
116
|
let lists = 0;
|
|
117
117
|
const list = on(
|
|
118
|
-
http.get("/notes").
|
|
118
|
+
http.get("/notes").public(),
|
|
119
119
|
flow("notes.list", {
|
|
120
120
|
cache: false,
|
|
121
121
|
effects: { reads: ["sql:notes"] },
|
|
@@ -146,7 +146,7 @@ describe("automatic tier-1 cache from effects", () => {
|
|
|
146
146
|
const db = store.sql("app", { schema: { notes } });
|
|
147
147
|
let lists = 0;
|
|
148
148
|
const list = on(
|
|
149
|
-
http.get("/notes").
|
|
149
|
+
http.get("/notes").public(),
|
|
150
150
|
flow("notes.list", {
|
|
151
151
|
out: z.array(NoteOut),
|
|
152
152
|
do: async (_input, fx) => {
|
|
@@ -191,7 +191,7 @@ describe("automatic tier-1 cache from effects", () => {
|
|
|
191
191
|
const db = store.sql("app", { schema: { notes } });
|
|
192
192
|
let lists = 0;
|
|
193
193
|
const list = on(
|
|
194
|
-
http.get("/notes").
|
|
194
|
+
http.get("/notes").public(),
|
|
195
195
|
flow("notes.list", {
|
|
196
196
|
effects: { reads: ["sql:notes"] },
|
|
197
197
|
do: () => {
|
|
@@ -201,7 +201,7 @@ describe("automatic tier-1 cache from effects", () => {
|
|
|
201
201
|
}),
|
|
202
202
|
);
|
|
203
203
|
const create = on(
|
|
204
|
-
http.post("/notes").
|
|
204
|
+
http.post("/notes").public(),
|
|
205
205
|
flow("notes.create", {
|
|
206
206
|
in: z.object({ title: z.string() }),
|
|
207
207
|
effects: { writes: ["sql:notes"] },
|
|
@@ -231,7 +231,7 @@ describe("automatic tier-1 cache from effects", () => {
|
|
|
231
231
|
const db = store.sql("app", { schema: { notes } });
|
|
232
232
|
let lists = 0;
|
|
233
233
|
const list = on(
|
|
234
|
-
http.get("/notes").
|
|
234
|
+
http.get("/notes").public(),
|
|
235
235
|
flow("notes.list", {
|
|
236
236
|
out: z.array(NoteOut),
|
|
237
237
|
do: async (_input, fx) => {
|
|
@@ -242,7 +242,7 @@ describe("automatic tier-1 cache from effects", () => {
|
|
|
242
242
|
}),
|
|
243
243
|
);
|
|
244
244
|
const create = on(
|
|
245
|
-
http.post("/notes").
|
|
245
|
+
http.post("/notes").public(),
|
|
246
246
|
flow("notes.create", {
|
|
247
247
|
in: z.object({ title: z.string() }),
|
|
248
248
|
do: async (input, fx) => {
|
|
@@ -1,17 +1,21 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* `oke({...})` auto-populates `stores` / `secrets` / `signals` /
|
|
3
|
-
* `channel.templates` from the same kind of
|
|
4
|
-
* already proven for `on()`'s trigger drain
|
|
2
|
+
* `oke({...})` auto-populates `stores` / `secrets` / `signals` / `clocks` /
|
|
3
|
+
* `gate.policies` / `channel.templates` from the same kind of
|
|
4
|
+
* module-evaluation registry already proven for `on()`'s trigger drain
|
|
5
|
+
* (`registry-isolation.test.ts`).
|
|
5
6
|
*
|
|
6
|
-
* `store
|
|
7
|
-
* `.template()` push into a shared registry
|
|
8
|
-
* at call time — zero change to how
|
|
9
|
-
*
|
|
10
|
-
* switch that already governs
|
|
7
|
+
* `store.*`, `vault.secret`, `signal()`, `clock()`, `gate.policy` / `.scope`
|
|
8
|
+
* / `.rate`, and medium-binder `.template()` push into a shared registry
|
|
9
|
+
* (`src/kernel/element-registries.ts`) at call time — zero change to how
|
|
10
|
+
* developers call them. `oke()` drains it at construction under the same
|
|
11
|
+
* `registry: "consume" | "keep" | "ignore"` switch that already governs
|
|
12
|
+
* bindings.
|
|
11
13
|
*/
|
|
12
14
|
|
|
13
15
|
import { describe, expect, test } from "bun:test";
|
|
14
16
|
import { channel } from "../elements/channel/declare.ts";
|
|
17
|
+
import { clock } from "../elements/clock/declare.ts";
|
|
18
|
+
import { gate } from "../elements/gate/declare.ts";
|
|
15
19
|
import { signal } from "../elements/signal/declare.ts";
|
|
16
20
|
import { store } from "../elements/store/declare.ts";
|
|
17
21
|
import { vault } from "../elements/vault/declare.ts";
|
|
@@ -20,7 +24,7 @@ import { flow } from "./flow.ts";
|
|
|
20
24
|
import { on, resetBindings } from "./on.ts";
|
|
21
25
|
import { http } from "./triggers.ts";
|
|
22
26
|
|
|
23
|
-
describe("oke() auto-registry — stores/secrets/signals/channel.templates/ai", () => {
|
|
27
|
+
describe("oke() auto-registry — stores/secrets/signals/clocks/gates/channel.templates/ai", () => {
|
|
24
28
|
test("before/after: zero explicit arrays boots identically to the explicit-array form", async () => {
|
|
25
29
|
resetBindings();
|
|
26
30
|
|
|
@@ -161,6 +165,42 @@ describe("oke() auto-registry — stores/secrets/signals/channel.templates/ai",
|
|
|
161
165
|
expect(app.bootResult?.vault?.contracts.has("IGNORED_SECRET")).toBeFalsy();
|
|
162
166
|
});
|
|
163
167
|
|
|
168
|
+
test("store.kv / store.index / clock() / gate.policy auto-drain — no explicit oke() arrays", async () => {
|
|
169
|
+
resetBindings();
|
|
170
|
+
store.kv("drafts", { description: "Compose drafts" });
|
|
171
|
+
store.index("tasks", { description: "Task search" });
|
|
172
|
+
clock("daily-digest", { cron: "0 8 * * *", every: "1d", timezone: "UTC" });
|
|
173
|
+
const member = gate.policy("member", ({ auth }) => !!auth.verified);
|
|
174
|
+
gate.scope("task:write");
|
|
175
|
+
gate.rate({ max: 60, per: "1m", keyBy: "user" });
|
|
176
|
+
|
|
177
|
+
const ping = flow("auto.ping", {
|
|
178
|
+
do: () => ({ ok: true as const }),
|
|
179
|
+
});
|
|
180
|
+
on(http.get("/auto-ping").gate(member), ping);
|
|
181
|
+
|
|
182
|
+
const app = oke({
|
|
183
|
+
name: "auto-kv-clock-gate",
|
|
184
|
+
autoBoot: false,
|
|
185
|
+
startScheduler: false,
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
expect(app.$options.stores?.some((s) => s.ref === "kv:drafts")).toBe(true);
|
|
189
|
+
expect(app.$options.stores?.some((s) => s.ref === "index:tasks")).toBe(true);
|
|
190
|
+
expect(app.$options.clocks?.some((c) => c.name === "daily-digest")).toBe(true);
|
|
191
|
+
expect(app.$options.gate?.policies?.some((g) => "name" in g && g.name === "member")).toBe(true);
|
|
192
|
+
expect(app.$options.gate?.policies?.some((g) => "name" in g && g.name === "task:write")).toBe(
|
|
193
|
+
true,
|
|
194
|
+
);
|
|
195
|
+
|
|
196
|
+
await app.boot({ env: "test" });
|
|
197
|
+
expect(app.bootResult?.store?.declarations.has("kv:drafts")).toBe(true);
|
|
198
|
+
expect(app.bootResult?.store?.declarations.has("index:tasks")).toBe(true);
|
|
199
|
+
expect(app.bootResult?.clock?.declarations.has("daily-digest")).toBe(true);
|
|
200
|
+
expect(app.bootResult?.gate?.gates.has("member")).toBe(true);
|
|
201
|
+
expect(app.bootResult?.gate?.gates.has("task:write")).toBe(true);
|
|
202
|
+
});
|
|
203
|
+
|
|
164
204
|
test('registry: "consume" (default) drains stores/secrets/signals/channel.templates — a later app does not inherit them', async () => {
|
|
165
205
|
store.sql("leak-app", { schema: {} });
|
|
166
206
|
vault.secret("LEAK_SECRET", { dev: "dev-leak" });
|
package/src/kernel/boot.test.ts
CHANGED
|
@@ -79,7 +79,7 @@ describe("boot — lazy element needs", () => {
|
|
|
79
79
|
expect(needs.signal).toBe(false);
|
|
80
80
|
});
|
|
81
81
|
|
|
82
|
-
test("oke() Store-only graph stays under the prior
|
|
82
|
+
test("oke() Store-only graph stays under the prior 50 kB baseline", async () => {
|
|
83
83
|
const dir = await mkdtemp(join(tmpdir(), "oke-store-only-"));
|
|
84
84
|
const entry = join(dir, "entry.ts");
|
|
85
85
|
const appPath = join(import.meta.dir, "app.ts");
|
|
@@ -116,9 +116,9 @@ describe("boot — lazy element needs", () => {
|
|
|
116
116
|
if (raw.byteLength === 0) continue;
|
|
117
117
|
total += Bun.gzipSync(new Uint8Array(raw)).byteLength;
|
|
118
118
|
}
|
|
119
|
-
// Rebased after
|
|
120
|
-
// (~
|
|
121
|
-
expect(total).toBeLessThan(
|
|
119
|
+
// Rebased after host API-key persist + allowlist on `oke()`.
|
|
120
|
+
// (~50.3 kB gzip with export externals).
|
|
121
|
+
expect(total).toBeLessThan(51_000);
|
|
122
122
|
} finally {
|
|
123
123
|
await rm(dir, { recursive: true, force: true });
|
|
124
124
|
}
|
|
@@ -227,7 +227,7 @@ describe("boot — HTTP gate wiring on the default path", () => {
|
|
|
227
227
|
resetBindings();
|
|
228
228
|
resetFlowSeq();
|
|
229
229
|
on(
|
|
230
|
-
http.get("/ping").
|
|
230
|
+
http.get("/ping").public(),
|
|
231
231
|
flow("ping-public", {
|
|
232
232
|
do: () => ({ ok: true }),
|
|
233
233
|
}),
|
package/src/kernel/boot.ts
CHANGED
|
@@ -101,10 +101,10 @@ export interface BootOptions {
|
|
|
101
101
|
readonly gates?: readonly GateDecl[];
|
|
102
102
|
/**
|
|
103
103
|
* HTTP auth-posture enforcement at boot.
|
|
104
|
-
* - `"deny"` (default) — every HTTP trigger must carry a gate or `.
|
|
104
|
+
* - `"deny"` (default) — every HTTP trigger must carry a gate or `.public()`
|
|
105
105
|
* - `"allow"` — skips the audit **only** when {@link env} is `"test"`;
|
|
106
106
|
* ignored in local/prod/docker (never a production-wide bypass).
|
|
107
|
-
* Migrate real apps with per-trigger `.
|
|
107
|
+
* Migrate real apps with per-trigger `.public()`.
|
|
108
108
|
*/
|
|
109
109
|
readonly unguardedHttp?: "deny" | "allow";
|
|
110
110
|
/** Signal declarations. */
|
|
@@ -47,7 +47,7 @@ const CreateOut = z.object({ ok: z.boolean() });
|
|
|
47
47
|
|
|
48
48
|
function buildUnannotatedCreateFlow(db: ReturnType<typeof store.sql>) {
|
|
49
49
|
return on(
|
|
50
|
-
http.post("/notes").
|
|
50
|
+
http.post("/notes").public(),
|
|
51
51
|
flow("notes.create", {
|
|
52
52
|
in: CreateIn,
|
|
53
53
|
out: CreateOut,
|
|
@@ -140,7 +140,7 @@ describe("boot-level: table-ref resolution stays backward compatible", () => {
|
|
|
140
140
|
resetFlowSeq();
|
|
141
141
|
const db = store.sql("app", { schema: { notes } });
|
|
142
142
|
const create = on(
|
|
143
|
-
http.post("/notes").
|
|
143
|
+
http.post("/notes").public(),
|
|
144
144
|
flow("notes.create", {
|
|
145
145
|
in: CreateIn,
|
|
146
146
|
out: CreateOut,
|
|
@@ -193,7 +193,7 @@ import { on, flow, http, gate } from "okengine";
|
|
|
193
193
|
const db = {} as any;
|
|
194
194
|
const notes = {} as any;
|
|
195
195
|
export const create = on(
|
|
196
|
-
http.post("/notes").
|
|
196
|
+
http.post("/notes").public(),
|
|
197
197
|
flow("notes.create", {
|
|
198
198
|
do: async (input, fx) => {
|
|
199
199
|
await fx.store(db).insert(notes).values({ id: input.id, title: input.title, createdAt: 1 });
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Module-evaluation registries for `store
|
|
3
|
-
* `vault.env.required`, `signal()`, `
|
|
4
|
-
*
|
|
5
|
-
*
|
|
2
|
+
* Module-evaluation registries for `store.*`, `vault.secret`,
|
|
3
|
+
* `vault.env.required`, `signal()`, `clock()`, `gate.policy` / `.scope` /
|
|
4
|
+
* `.rate`, `channel.<medium>().template()`, and `ai.model` / `.prompt` /
|
|
5
|
+
* `ai.embed` / `ai.agent` / `ai.mcpServer` — the plain arrays behind each
|
|
6
|
+
* declare module's `listX()` / `resetX()` pair (mirrors `on.ts`'s
|
|
6
7
|
* trigger-drain `bindings` array).
|
|
7
8
|
*
|
|
8
9
|
* Lives here, not in the declare modules themselves, so {@link oke} can read
|
|
@@ -24,8 +25,10 @@ import type { StoreDecl } from "../elements/store/declare.ts";
|
|
|
24
25
|
import type { VaultSecretDecl } from "../elements/vault/declare.ts";
|
|
25
26
|
import type { SignalDecl } from "../elements/signal/declare.ts";
|
|
26
27
|
import type { ChannelTemplateDecl } from "../elements/channel/declare.ts";
|
|
28
|
+
import type { ClockDecl } from "../elements/clock/declare.ts";
|
|
29
|
+
import type { GateDecl } from "../elements/gate/declare.ts";
|
|
27
30
|
|
|
28
|
-
/** `store.sql` / `store.files` declarations since the last reset. */
|
|
31
|
+
/** `store.sql` / `store.kv` / `store.files` / `store.index` declarations since the last reset. */
|
|
29
32
|
export const storeRegistry: StoreDecl[] = [];
|
|
30
33
|
/** `vault.secret` declarations since the last reset. */
|
|
31
34
|
export const secretRegistry: VaultSecretDecl[] = [];
|
|
@@ -33,6 +36,10 @@ export const secretRegistry: VaultSecretDecl[] = [];
|
|
|
33
36
|
export const requiredEnvRegistry: string[] = [];
|
|
34
37
|
/** `signal()` declarations since the last reset. */
|
|
35
38
|
export const signalRegistry: SignalDecl[] = [];
|
|
39
|
+
/** `clock()` declarations since the last reset. */
|
|
40
|
+
export const clockRegistry: ClockDecl[] = [];
|
|
41
|
+
/** `gate.policy` / `gate.scope` / `gate.rate` declarations since the last reset. */
|
|
42
|
+
export const gateRegistry: GateDecl[] = [];
|
|
36
43
|
/** Medium-binder `.template()` declarations since the last reset. */
|
|
37
44
|
export const channelTemplateRegistry: ChannelTemplateDecl[] = [];
|
|
38
45
|
/** `ai.model` declarations since the last reset. */
|
package/src/kernel/errors.ts
CHANGED
|
@@ -183,6 +183,30 @@ export const OKE_ERRORS = {
|
|
|
183
183
|
cause: "src/flows/{unit} exists on disk but adopted no flows — the .adopt() barrel is stale.",
|
|
184
184
|
fix: "Run `oke dev` or `oke build` to regenerate `src/flows/generated.ts`.",
|
|
185
185
|
},
|
|
186
|
+
/**
|
|
187
|
+
* `http.get()` was never stamped from the file tree — refuse a silent `/`.
|
|
188
|
+
*/
|
|
189
|
+
HTTP_PATH_UNRESOLVED: {
|
|
190
|
+
code: 1010,
|
|
191
|
+
cause: 'Flow "{flow}" bound {method} with no path — the file-tree stamp never ran.',
|
|
192
|
+
fix: 'Put the file under `src/flows/<unit>/` and import `@/flows/generated`, or pass an explicit path to `http.{method}("/…")`.',
|
|
193
|
+
},
|
|
194
|
+
/**
|
|
195
|
+
* Two HTTP bindings share method + path — last-add-wins is the opposite of this DX.
|
|
196
|
+
*/
|
|
197
|
+
HTTP_ROUTE_DUPLICATE: {
|
|
198
|
+
code: 1011,
|
|
199
|
+
cause: '{method} {path} is bound twice (flow "{flow}").',
|
|
200
|
+
fix: "Give each HTTP flow a unique method + path.",
|
|
201
|
+
},
|
|
202
|
+
/**
|
|
203
|
+
* Adopted HTTP flow still has no name (`flow({ do })` outside a unit).
|
|
204
|
+
*/
|
|
205
|
+
HTTP_FLOW_UNNAMED: {
|
|
206
|
+
code: 1012,
|
|
207
|
+
cause: "An HTTP flow on {method} {path} has no name.",
|
|
208
|
+
fix: 'Use `flow("unit.export", {…})` or export it from a `src/flows/<unit>/` file so the tree can stamp `unit.export`.',
|
|
209
|
+
},
|
|
186
210
|
/**
|
|
187
211
|
* Emit target has no subscriber (unified-theory §21).
|
|
188
212
|
* Thrown at emit when `optional` is false and nobody is subscribed.
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Module-evaluation registry for `src/flows/generated.ts`.
|
|
3
|
+
*
|
|
4
|
+
* Same consume/ignore posture as `on()` / `listBindings()`: generated.ts
|
|
5
|
+
* calls {@link registerFlowUnits}; {@link oke} drains the bag into `$routes`
|
|
6
|
+
* unless `registry: "ignore"`.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import type { AnyFlowDef } from "./flow.ts";
|
|
10
|
+
import { isFlow } from "./flow.ts";
|
|
11
|
+
|
|
12
|
+
/** Unit name → export name → Flow. */
|
|
13
|
+
export type FlowUnitBag = {
|
|
14
|
+
readonly [exportName: string]: unknown;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
const pending: Record<string, FlowUnitBag> = {};
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Record generated flow units for the next {@link oke} construction.
|
|
21
|
+
*
|
|
22
|
+
* @param units - `{ notes, main, … }` from `generated.ts`
|
|
23
|
+
*/
|
|
24
|
+
export function registerFlowUnits(units: Record<string, FlowUnitBag>): void {
|
|
25
|
+
Object.assign(pending, units);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Drain registered units (consume). Clears the registry.
|
|
30
|
+
*/
|
|
31
|
+
export function consumeRegisteredFlowUnits(): Record<string, FlowUnitBag> {
|
|
32
|
+
const snap: Record<string, FlowUnitBag> = { ...pending };
|
|
33
|
+
resetRegisteredFlowUnits();
|
|
34
|
+
return snap;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Snapshot without clearing (keep / tests).
|
|
39
|
+
*/
|
|
40
|
+
export function peekRegisteredFlowUnits(): Record<string, FlowUnitBag> {
|
|
41
|
+
return { ...pending };
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Clear the unit registry (tests / `registry: "consume"` after drain).
|
|
46
|
+
*/
|
|
47
|
+
export function resetRegisteredFlowUnits(): void {
|
|
48
|
+
for (const key of Object.keys(pending)) {
|
|
49
|
+
delete pending[key];
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Collect FlowDefs from a unit bag (skip non-flow exports).
|
|
55
|
+
*
|
|
56
|
+
* @param bag - Namespace object
|
|
57
|
+
*/
|
|
58
|
+
export function flowsInUnitBag(bag: FlowUnitBag): AnyFlowDef[] {
|
|
59
|
+
const found: AnyFlowDef[] = [];
|
|
60
|
+
for (const value of Object.values(bag)) {
|
|
61
|
+
if (isFlow(value)) found.push(value);
|
|
62
|
+
}
|
|
63
|
+
return found;
|
|
64
|
+
}
|
package/src/kernel/flow.ts
CHANGED
|
@@ -239,13 +239,34 @@ export const flowBrand: unique symbol = Symbol("oke.flow");
|
|
|
239
239
|
* Input / output types are inferred from Standard Schema `in` / `out` when
|
|
240
240
|
* present; otherwise from the `do` handler signature.
|
|
241
241
|
*
|
|
242
|
+
* A nameless `flow({ do })` is stamped `unit.export` by the file-tree
|
|
243
|
+
* generator, {@link unit}, or `.adopt()`. Explicit `flow("notes.get", {…})`
|
|
244
|
+
* still wins.
|
|
245
|
+
*
|
|
246
|
+
* @param options - Contract and handler (name stamped later)
|
|
247
|
+
*/
|
|
248
|
+
export function flow<Opts extends FlowOptions<any, any, any>>(
|
|
249
|
+
options: Opts,
|
|
250
|
+
): FlowDef<InferFlowIn<Opts>, InferFlowOut<Opts>, InferFlowErrors<Opts>>;
|
|
251
|
+
/**
|
|
242
252
|
* @param name - Stable name (used by `fx.call` and the Manifest)
|
|
243
253
|
* @param options - Contract and handler
|
|
244
254
|
*/
|
|
245
255
|
export function flow<Opts extends FlowOptions<any, any, any>>(
|
|
246
256
|
name: string,
|
|
247
257
|
options: Opts & { readonly name?: never },
|
|
248
|
-
): FlowDef<InferFlowIn<Opts>, InferFlowOut<Opts>, InferFlowErrors<Opts
|
|
258
|
+
): FlowDef<InferFlowIn<Opts>, InferFlowOut<Opts>, InferFlowErrors<Opts>>;
|
|
259
|
+
export function flow(
|
|
260
|
+
nameOrOptions: string | FlowOptions<any, any, any>,
|
|
261
|
+
maybeOptions?: FlowOptions<any, any, any> & { readonly name?: never },
|
|
262
|
+
): FlowDef {
|
|
263
|
+
const named = typeof nameOrOptions === "string";
|
|
264
|
+
const name = named ? nameOrOptions : "";
|
|
265
|
+
const options = (named ? maybeOptions : nameOrOptions) as FlowOptions<any, any, any> | undefined;
|
|
266
|
+
if (!options || typeof options.do !== "function") {
|
|
267
|
+
throw new TypeError("flow() expected an options bag with a do handler");
|
|
268
|
+
}
|
|
269
|
+
|
|
249
270
|
const triggers: Trigger[] = [];
|
|
250
271
|
const hooks: Partial<Record<HookStage, HookFn[]>> = {};
|
|
251
272
|
const pendingPlugins: PluginDef[] = [];
|
|
@@ -253,13 +274,13 @@ export function flow<Opts extends FlowOptions<any, any, any>>(
|
|
|
253
274
|
const dot = name.indexOf(".");
|
|
254
275
|
const unit = dot > 0 ? name.slice(0, dot) : undefined;
|
|
255
276
|
|
|
256
|
-
const def: FlowDef
|
|
277
|
+
const def: FlowDef = {
|
|
257
278
|
[flowBrand]: true,
|
|
258
279
|
name,
|
|
259
280
|
unit,
|
|
260
281
|
in: options.in,
|
|
261
282
|
out: options.out,
|
|
262
|
-
errors: (options.errors ?? undefined) as
|
|
283
|
+
errors: (options.errors ?? undefined) as FlowErrorMap | undefined,
|
|
263
284
|
effects: options.effects,
|
|
264
285
|
durable: options.durable ?? false,
|
|
265
286
|
retry: options.retry,
|
|
@@ -268,17 +289,8 @@ export function flow<Opts extends FlowOptions<any, any, any>>(
|
|
|
268
289
|
slo: options.slo,
|
|
269
290
|
plane: options.plane,
|
|
270
291
|
breaking: options.breaking ?? false,
|
|
271
|
-
compensate: options.compensate as
|
|
272
|
-
|
|
273
|
-
ctx: {
|
|
274
|
-
readonly input: InferFlowIn<Opts>;
|
|
275
|
-
readonly error: unknown;
|
|
276
|
-
readonly completedSteps: readonly string[];
|
|
277
|
-
},
|
|
278
|
-
fx: import("./fx.ts").Fx,
|
|
279
|
-
) => unknown | Promise<unknown>)
|
|
280
|
-
| undefined,
|
|
281
|
-
do: options.do as FlowHandler<InferFlowIn<Opts>, InferFlowOut<Opts>>,
|
|
292
|
+
compensate: options.compensate as FlowDef["compensate"],
|
|
293
|
+
do: options.do as FlowHandler,
|
|
282
294
|
triggers,
|
|
283
295
|
$trigger: undefined,
|
|
284
296
|
hooks,
|
|
@@ -320,9 +332,9 @@ export function isFlow(value: unknown): value is AnyFlowDef {
|
|
|
320
332
|
}
|
|
321
333
|
|
|
322
334
|
/**
|
|
323
|
-
* No-op. `
|
|
324
|
-
*
|
|
325
|
-
*
|
|
335
|
+
* No-op. Nameless flows start as `""` and are stamped by unit / adopt /
|
|
336
|
+
* the file-tree generator — kept so existing test `beforeEach` blocks
|
|
337
|
+
* don't need touching.
|
|
326
338
|
*
|
|
327
339
|
* @internal
|
|
328
340
|
*/
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Session-only `fx.auth` key methods — issuer derived, never from flow input.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { describe, expect, test } from "bun:test";
|
|
6
|
+
import { AttenuationError } from "../auth/attenuation.ts";
|
|
7
|
+
import { createApiKey, createApiKeyStore } from "../auth/api-keys.ts";
|
|
8
|
+
import { isFlowFailure } from "./hooks.ts";
|
|
9
|
+
import { createFx } from "./fx.ts";
|
|
10
|
+
|
|
11
|
+
describe("fx.auth key methods", () => {
|
|
12
|
+
test("createApiKey derives creator from the live session and rejects broader scopes", async () => {
|
|
13
|
+
const store = createApiKeyStore();
|
|
14
|
+
const fx = createFx({
|
|
15
|
+
flow: "keys.create",
|
|
16
|
+
effects: { writes: ["auth:api-keys"] },
|
|
17
|
+
apiKeyStore: store,
|
|
18
|
+
auth: { userId: "u1", scopes: new Set(["member"]), apiKeyId: null },
|
|
19
|
+
});
|
|
20
|
+
const created = await fx.auth.createApiKey({ name: "mine", scopes: ["member"] });
|
|
21
|
+
expect(created.key.scopes).toEqual(["member"]);
|
|
22
|
+
expect(created.secret.startsWith("oke_")).toBe(true);
|
|
23
|
+
expect(store.keys.get(created.key.id)?.creatorId).toBe("u1");
|
|
24
|
+
|
|
25
|
+
await expect(fx.auth.createApiKey({ name: "wide", scopes: ["admin"] })).rejects.toBeInstanceOf(
|
|
26
|
+
AttenuationError,
|
|
27
|
+
);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
test("key-authenticated caller cannot create / list / revoke / rotate / update", async () => {
|
|
31
|
+
const store = createApiKeyStore();
|
|
32
|
+
await createApiKey(store, {
|
|
33
|
+
plane: "user",
|
|
34
|
+
name: "existing",
|
|
35
|
+
scopes: ["member"],
|
|
36
|
+
creatorId: "u1",
|
|
37
|
+
creatorScopes: ["member"],
|
|
38
|
+
id: "key_existing",
|
|
39
|
+
});
|
|
40
|
+
const fx = createFx({
|
|
41
|
+
flow: "keys.as-key",
|
|
42
|
+
effects: { reads: ["auth:api-keys"], writes: ["auth:api-keys"] },
|
|
43
|
+
apiKeyStore: store,
|
|
44
|
+
auth: { userId: "u1", scopes: new Set(["member"]), apiKeyId: "key_existing" },
|
|
45
|
+
});
|
|
46
|
+
for (const call of [
|
|
47
|
+
() => fx.auth.createApiKey({ name: "nested", scopes: ["member"] }),
|
|
48
|
+
() => fx.auth.listApiKeys(),
|
|
49
|
+
() => fx.auth.revokeApiKey("key_existing"),
|
|
50
|
+
() => fx.auth.rotateApiKey("key_existing"),
|
|
51
|
+
() => fx.auth.updateApiKey("key_existing", { name: "nope" }),
|
|
52
|
+
]) {
|
|
53
|
+
try {
|
|
54
|
+
await call();
|
|
55
|
+
throw new Error("expected Forbidden");
|
|
56
|
+
} catch (err) {
|
|
57
|
+
expect(isFlowFailure(err)).toBe(true);
|
|
58
|
+
if (isFlowFailure(err)) expect(err.error.code).toBe("Forbidden");
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
test("list is self-only; cross-user revoke is Forbidden", async () => {
|
|
64
|
+
const store = createApiKeyStore();
|
|
65
|
+
await createApiKey(store, {
|
|
66
|
+
plane: "user",
|
|
67
|
+
name: "theirs",
|
|
68
|
+
scopes: ["member"],
|
|
69
|
+
creatorId: "u2",
|
|
70
|
+
creatorScopes: ["member"],
|
|
71
|
+
id: "key_theirs",
|
|
72
|
+
});
|
|
73
|
+
const fx = createFx({
|
|
74
|
+
flow: "keys.list",
|
|
75
|
+
effects: { reads: ["auth:api-keys"], writes: ["auth:api-keys"] },
|
|
76
|
+
apiKeyStore: store,
|
|
77
|
+
auth: { userId: "u1", scopes: new Set(["member"]), apiKeyId: null },
|
|
78
|
+
});
|
|
79
|
+
expect(await fx.auth.listApiKeys()).toEqual([]);
|
|
80
|
+
try {
|
|
81
|
+
await fx.auth.revokeApiKey("key_theirs");
|
|
82
|
+
throw new Error("expected Forbidden");
|
|
83
|
+
} catch (err) {
|
|
84
|
+
expect(isFlowFailure(err)).toBe(true);
|
|
85
|
+
if (isFlowFailure(err)) expect(err.error.code).toBe("Forbidden");
|
|
86
|
+
}
|
|
87
|
+
});
|
|
88
|
+
});
|