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
|
@@ -89,7 +89,7 @@ describe("boot-level: stale barrel through a real oke() boot", () => {
|
|
|
89
89
|
effects: {},
|
|
90
90
|
do: () => ({ ok: true as const }),
|
|
91
91
|
});
|
|
92
|
-
on(http.get("/health").
|
|
92
|
+
on(http.get("/health").public(), mainFlow);
|
|
93
93
|
const app = oke({ name: "stale-barrel", gate: { unguardedHttp: "allow" } });
|
|
94
94
|
|
|
95
95
|
await expect(
|
|
@@ -39,10 +39,20 @@ export type AppFlowRoute<
|
|
|
39
39
|
readonly path?: undefined;
|
|
40
40
|
};
|
|
41
41
|
|
|
42
|
-
/**
|
|
42
|
+
/**
|
|
43
|
+
* Unit → flow → contract. Leaf is the HTTP / call-only union — do not
|
|
44
|
+
* index with `AppFlowRoute<any, any, any, any, any>` (`any extends string`
|
|
45
|
+
* makes `method`/`path` required and rejects cron / signal flows).
|
|
46
|
+
*/
|
|
43
47
|
export type AppRouteMap = {
|
|
44
48
|
readonly [unit: string]: {
|
|
45
|
-
readonly [flow: string]:
|
|
49
|
+
readonly [flow: string]: {
|
|
50
|
+
readonly in?: unknown;
|
|
51
|
+
readonly out?: unknown;
|
|
52
|
+
readonly errors?: unknown;
|
|
53
|
+
readonly method?: string;
|
|
54
|
+
readonly path?: string;
|
|
55
|
+
};
|
|
46
56
|
};
|
|
47
57
|
};
|
|
48
58
|
|
|
@@ -170,6 +180,7 @@ export function accumulateAdoptArgs(args: readonly unknown[], into: RuntimeRoute
|
|
|
170
180
|
const unitBag = into[unit] ?? (into[unit] = {});
|
|
171
181
|
for (const [flowName, flowDef] of Object.entries(value as Record<string, unknown>)) {
|
|
172
182
|
if (!isFlow(flowDef)) continue;
|
|
183
|
+
stampAdoptedFlow(flowDef, unit, flowName);
|
|
173
184
|
found.push(flowDef);
|
|
174
185
|
unitBag[flowName] = runtimeRouteFromFlow(flowDef);
|
|
175
186
|
}
|
|
@@ -178,3 +189,20 @@ export function accumulateAdoptArgs(args: readonly unknown[], into: RuntimeRoute
|
|
|
178
189
|
|
|
179
190
|
return found;
|
|
180
191
|
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Stamp `unit.export` on nameless flows — same rule as {@link unit}.
|
|
195
|
+
*
|
|
196
|
+
* @param flowDef - Flow to stamp
|
|
197
|
+
* @param unit - Client unit (folder / adopt key)
|
|
198
|
+
* @param exportName - Namespace export
|
|
199
|
+
*/
|
|
200
|
+
function stampAdoptedFlow(flowDef: AnyFlowDef, unit: string, exportName: string): void {
|
|
201
|
+
const f = flowDef as { name: string; unit: string | undefined };
|
|
202
|
+
if (!f.name || f.name.startsWith("flow_")) {
|
|
203
|
+
f.name = `${unit}.${exportName}`;
|
|
204
|
+
}
|
|
205
|
+
if (!f.unit) {
|
|
206
|
+
f.unit = unit;
|
|
207
|
+
}
|
|
208
|
+
}
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Host API-key durability — standalone `oke()` (no Console).
|
|
3
|
+
*
|
|
4
|
+
* Creates a key via `fx.auth.createApiKey`, stops, boots a fresh app against
|
|
5
|
+
* the same SQL URL, and verifies Bearer on the new `gate.auth.apiKeyStore`.
|
|
6
|
+
* Production uses postgres + `DATABASE_URL`; this suite uses file-backed
|
|
7
|
+
* PGlite at that same URL so the restart is hermetic.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { afterEach, describe, expect, test } from "bun:test";
|
|
11
|
+
import { mkdtemp, rm } from "node:fs/promises";
|
|
12
|
+
import { tmpdir } from "node:os";
|
|
13
|
+
import { join } from "node:path";
|
|
14
|
+
import { AUTH_TABLES } from "../auth/tables.ts";
|
|
15
|
+
import { gate } from "../elements/gate.ts";
|
|
16
|
+
import { resetStores, store } from "../elements/store/declare.ts";
|
|
17
|
+
import { oke } from "./app.ts";
|
|
18
|
+
import { flow, resetFlowSeq } from "./flow.ts";
|
|
19
|
+
import { on, resetBindings } from "./on.ts";
|
|
20
|
+
import { http } from "./triggers.ts";
|
|
21
|
+
|
|
22
|
+
const PEPPER = "oke-host-api-key-persist-pepper";
|
|
23
|
+
const member = gate.policy("member", ({ auth }) => !!auth.verified);
|
|
24
|
+
|
|
25
|
+
function resetIsolated(): void {
|
|
26
|
+
resetBindings();
|
|
27
|
+
resetFlowSeq();
|
|
28
|
+
resetStores();
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
afterEach(() => {
|
|
32
|
+
resetIsolated();
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
describe("host API key persist via store.sql()", () => {
|
|
36
|
+
test("standalone oke() persists keys across restart; Bearer verifies on a fresh store", async () => {
|
|
37
|
+
const dir = await mkdtemp(join(tmpdir(), "oke-host-keys-"));
|
|
38
|
+
const sqlUrl = join(dir, "pgdata");
|
|
39
|
+
const prevDb = process.env.DATABASE_URL;
|
|
40
|
+
const prevPglite = process.env.OKE_PGLITE_URL;
|
|
41
|
+
process.env.DATABASE_URL = sqlUrl;
|
|
42
|
+
process.env.OKE_PGLITE_URL = sqlUrl;
|
|
43
|
+
|
|
44
|
+
try {
|
|
45
|
+
resetIsolated();
|
|
46
|
+
const db1 = store.sql("app", { schema: {} });
|
|
47
|
+
on(
|
|
48
|
+
http.post("/keys").gate(member),
|
|
49
|
+
flow("keys.create", {
|
|
50
|
+
effects: { writes: ["auth:api-keys"] },
|
|
51
|
+
do: (_input, fx) => fx.auth.createApiKey({ name: "prod-ci", scopes: ["member"] }),
|
|
52
|
+
}),
|
|
53
|
+
);
|
|
54
|
+
on(
|
|
55
|
+
http.get("/secure").gate(member),
|
|
56
|
+
flow("secure.ping", {
|
|
57
|
+
effects: {},
|
|
58
|
+
do: () => ({ ok: true }),
|
|
59
|
+
}),
|
|
60
|
+
);
|
|
61
|
+
|
|
62
|
+
const first = oke({
|
|
63
|
+
name: "host-keys-first",
|
|
64
|
+
stores: [db1],
|
|
65
|
+
gate: {
|
|
66
|
+
auth: { secret: PEPPER, http: false },
|
|
67
|
+
policies: [member],
|
|
68
|
+
},
|
|
69
|
+
env: "test",
|
|
70
|
+
startScheduler: false,
|
|
71
|
+
});
|
|
72
|
+
await first.boot({ env: "test", startScheduler: false });
|
|
73
|
+
expect(first.apiKeys?.persist).toBeTypeOf("function");
|
|
74
|
+
|
|
75
|
+
const created = await first.execute(
|
|
76
|
+
first.flow("keys.create")!,
|
|
77
|
+
{},
|
|
78
|
+
http.post("/keys").gate(member),
|
|
79
|
+
{
|
|
80
|
+
principal: {
|
|
81
|
+
plane: "user",
|
|
82
|
+
userId: "u_issuer",
|
|
83
|
+
scopes: new Set(["member"]),
|
|
84
|
+
verified: true,
|
|
85
|
+
},
|
|
86
|
+
},
|
|
87
|
+
);
|
|
88
|
+
expect(created.failure).toBeUndefined();
|
|
89
|
+
const secret = (created.output as { secret?: string } | undefined)?.secret;
|
|
90
|
+
expect(secret?.startsWith("oke_")).toBe(true);
|
|
91
|
+
|
|
92
|
+
const firstStore = first.bootResult?.store;
|
|
93
|
+
await first.stop();
|
|
94
|
+
await firstStore?.close();
|
|
95
|
+
|
|
96
|
+
resetIsolated();
|
|
97
|
+
const db2 = store.sql("app", { schema: {} });
|
|
98
|
+
on(
|
|
99
|
+
http.get("/secure").gate(member),
|
|
100
|
+
flow("secure.ping", {
|
|
101
|
+
effects: {},
|
|
102
|
+
do: () => ({ ok: true }),
|
|
103
|
+
}),
|
|
104
|
+
);
|
|
105
|
+
const second = oke({
|
|
106
|
+
name: "host-keys-second",
|
|
107
|
+
stores: [db2],
|
|
108
|
+
gate: {
|
|
109
|
+
auth: { secret: PEPPER, http: false },
|
|
110
|
+
policies: [member],
|
|
111
|
+
},
|
|
112
|
+
env: "test",
|
|
113
|
+
startScheduler: false,
|
|
114
|
+
});
|
|
115
|
+
await second.boot({ env: "test", startScheduler: false });
|
|
116
|
+
expect(second.apiKeys).not.toBe(first.apiKeys);
|
|
117
|
+
expect(second.apiKeys?.keys.size).toBeGreaterThan(0);
|
|
118
|
+
|
|
119
|
+
const conn = await second.bootResult?.store?.primarySql();
|
|
120
|
+
const rows = await conn?.query(`SELECT id, name FROM ${AUTH_TABLES.apiKeys}`);
|
|
121
|
+
expect(rows?.some((r) => r.name === "prod-ci")).toBe(true);
|
|
122
|
+
|
|
123
|
+
const res = await second.fetch(
|
|
124
|
+
new Request("http://localhost/secure", {
|
|
125
|
+
method: "GET",
|
|
126
|
+
headers: { authorization: `Bearer ${secret}` },
|
|
127
|
+
}),
|
|
128
|
+
);
|
|
129
|
+
expect(res.status).toBe(200);
|
|
130
|
+
expect(await res.json()).toEqual({ data: { ok: true }, error: null });
|
|
131
|
+
|
|
132
|
+
const secondStore = second.bootResult?.store;
|
|
133
|
+
await second.stop();
|
|
134
|
+
await secondStore?.close();
|
|
135
|
+
} finally {
|
|
136
|
+
if (prevDb === undefined) delete process.env.DATABASE_URL;
|
|
137
|
+
else process.env.DATABASE_URL = prevDb;
|
|
138
|
+
if (prevPglite === undefined) delete process.env.OKE_PGLITE_URL;
|
|
139
|
+
else process.env.OKE_PGLITE_URL = prevPglite;
|
|
140
|
+
await rm(dir, { recursive: true, force: true });
|
|
141
|
+
}
|
|
142
|
+
}, 20_000);
|
|
143
|
+
});
|
package/src/kernel/app.ts
CHANGED
|
@@ -14,6 +14,7 @@ import {
|
|
|
14
14
|
accumulateAdoptArgs,
|
|
15
15
|
type AppRouteMap,
|
|
16
16
|
type RoutesFromAdoptArgs,
|
|
17
|
+
type RoutesFromNamespace,
|
|
17
18
|
type RuntimeRouteMap,
|
|
18
19
|
} from "./adopt-routes.ts";
|
|
19
20
|
// `./boot.ts` pulls in every element + driver module (vault, store, signal,
|
|
@@ -23,6 +24,7 @@ import {
|
|
|
23
24
|
import type { BootOptions, BootResult, ElementRuntimes } from "./boot.ts";
|
|
24
25
|
import type { CapabilityToken } from "./capability.ts";
|
|
25
26
|
import type { AppAuthBinding } from "./auth-resolve.ts";
|
|
27
|
+
import type { ApiKeyStore } from "../auth/api-keys.ts";
|
|
26
28
|
import type { AuthHttpMaterialization } from "../auth/bindings.ts";
|
|
27
29
|
import type { WiredGateAuth } from "./app-auth.ts";
|
|
28
30
|
import {
|
|
@@ -59,7 +61,8 @@ import {
|
|
|
59
61
|
} from "../runtime/dev-request-log.ts";
|
|
60
62
|
import { asBrowserJsonCodeBlock, httpNavGroups } from "../runtime/json-code-block.ts";
|
|
61
63
|
import { resolveDurationMs } from "./elapsed.ts";
|
|
62
|
-
import { fail } from "./errors.ts";
|
|
64
|
+
import { fail, throwOke } from "./errors.ts";
|
|
65
|
+
import { consumeRegisteredFlowUnits, type FlowUnitBag } from "./flow-units.ts";
|
|
63
66
|
import {
|
|
64
67
|
isFlowFailure,
|
|
65
68
|
mergeHooks,
|
|
@@ -91,6 +94,8 @@ import {
|
|
|
91
94
|
aiModelRegistry,
|
|
92
95
|
aiPromptRegistry,
|
|
93
96
|
channelTemplateRegistry,
|
|
97
|
+
clockRegistry,
|
|
98
|
+
gateRegistry,
|
|
94
99
|
requiredEnvRegistry,
|
|
95
100
|
secretRegistry,
|
|
96
101
|
signalRegistry,
|
|
@@ -99,6 +104,7 @@ import {
|
|
|
99
104
|
import {
|
|
100
105
|
applyPrincipal,
|
|
101
106
|
createElementPipelineHooks,
|
|
107
|
+
gateNamesOf,
|
|
102
108
|
type PrincipalBag,
|
|
103
109
|
type ResolvedPrincipal,
|
|
104
110
|
} from "./pipeline.ts";
|
|
@@ -118,6 +124,7 @@ import {
|
|
|
118
124
|
type RouterPreset,
|
|
119
125
|
type SmartRouter,
|
|
120
126
|
} from "./router.ts";
|
|
127
|
+
import { isPendingHttpPath } from "./http-path-pending.ts";
|
|
121
128
|
import type {
|
|
122
129
|
CdcTrigger,
|
|
123
130
|
EveryTrigger,
|
|
@@ -299,6 +306,21 @@ export interface UnitHooks<D extends Record<string, unknown> = {}> {
|
|
|
299
306
|
plug<P extends PluginDef>(pluginDef: P): UnitHooks<AccumulateDecorations<D, P>>;
|
|
300
307
|
}
|
|
301
308
|
|
|
309
|
+
/**
|
|
310
|
+
* Module-augmentation slot filled by `src/flows/generated.ts`.
|
|
311
|
+
* Kernel tests do not import an app generated file, so this stays `{}`.
|
|
312
|
+
*/
|
|
313
|
+
export interface RegisteredFlowUnits {}
|
|
314
|
+
|
|
315
|
+
/**
|
|
316
|
+
* `$routes` derived from {@link RegisteredFlowUnits} after `import generated`.
|
|
317
|
+
*
|
|
318
|
+
* @typeParam U - Augmented unit map
|
|
319
|
+
*/
|
|
320
|
+
export type RoutesFromRegisteredUnits<U = RegisteredFlowUnits> = {
|
|
321
|
+
[K in keyof U]: U[K] extends Record<string, unknown> ? RoutesFromNamespace<U[K]> : never;
|
|
322
|
+
};
|
|
323
|
+
|
|
302
324
|
/**
|
|
303
325
|
* Application instance — adopts flows, routes HTTP, runs the pipeline.
|
|
304
326
|
*
|
|
@@ -350,6 +372,8 @@ export interface OkeApp<D extends Record<string, unknown> = {}, R extends AppRou
|
|
|
350
372
|
* Builtin auth binding after boot (session store + secret), when configured.
|
|
351
373
|
*/
|
|
352
374
|
readonly authBinding: AppAuthBinding | undefined;
|
|
375
|
+
/** Shared API key store when `gate.auth` is enabled. */
|
|
376
|
+
readonly apiKeys: ApiKeyStore | undefined;
|
|
353
377
|
/**
|
|
354
378
|
* Attach a plugin app-wide. Scope is the attachment point.
|
|
355
379
|
* Types accumulate — decorations are visible on downstream handlers.
|
|
@@ -435,6 +459,8 @@ export interface OkeApp<D extends Record<string, unknown> = {}, R extends AppRou
|
|
|
435
459
|
* Takes effect only with {@link trustedInvoke}.
|
|
436
460
|
*/
|
|
437
461
|
readonly revealPii?: boolean;
|
|
462
|
+
/** Forced RLS bag (Console / Call API). Operator omit = no stamp. */
|
|
463
|
+
readonly rls?: import("../drivers/pg-rls.ts").RlsIdentity;
|
|
438
464
|
/** Parent WideEvent id when this execution was caused by another. */
|
|
439
465
|
readonly parentId?: string;
|
|
440
466
|
/** Explicit run / WideEvent id (defaults to a new UUID). */
|
|
@@ -562,11 +588,57 @@ function mergeAiOptions(
|
|
|
562
588
|
};
|
|
563
589
|
}
|
|
564
590
|
|
|
591
|
+
/**
|
|
592
|
+
* Refuse an unresolved path sentinel or a nameless HTTP flow.
|
|
593
|
+
*
|
|
594
|
+
* @param binding - HTTP binding
|
|
595
|
+
*/
|
|
596
|
+
function assertHttpBindingReady(binding: Binding): void {
|
|
597
|
+
const trigger = binding.trigger;
|
|
598
|
+
if (trigger.kind !== "http") return;
|
|
599
|
+
if (isPendingHttpPath(trigger.path)) {
|
|
600
|
+
throwOke("HTTP_PATH_UNRESOLVED", {
|
|
601
|
+
flow: binding.flow.name || "(unnamed)",
|
|
602
|
+
method: trigger.method,
|
|
603
|
+
});
|
|
604
|
+
}
|
|
605
|
+
if (!binding.flow.name || binding.flow.name.startsWith("flow_")) {
|
|
606
|
+
throwOke("HTTP_FLOW_UNNAMED", {
|
|
607
|
+
method: trigger.method,
|
|
608
|
+
path: trigger.path,
|
|
609
|
+
});
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
/**
|
|
614
|
+
* Fold `generated.ts` units into `$routes` and `flowsByName`.
|
|
615
|
+
*
|
|
616
|
+
* @param units - Drained {@link registerFlowUnits} bag
|
|
617
|
+
* @param routes - Runtime `$routes`
|
|
618
|
+
* @param registerFlow - App flow index
|
|
619
|
+
*/
|
|
620
|
+
function drainGeneratedUnits(
|
|
621
|
+
units: Record<string, FlowUnitBag>,
|
|
622
|
+
routes: RuntimeRouteMap,
|
|
623
|
+
registerFlow: (flowDef: AnyFlowDef) => void,
|
|
624
|
+
): void {
|
|
625
|
+
if (Object.keys(units).length === 0) return;
|
|
626
|
+
const flows = accumulateAdoptArgs([units], routes);
|
|
627
|
+
for (const flowDef of flows) {
|
|
628
|
+
registerFlow(flowDef);
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
|
|
565
632
|
/**
|
|
566
633
|
* Create an application. Adopts bindings registered via {@link on}.
|
|
634
|
+
* Drains {@link registerFlowUnits} into `$routes` unless `registry: "ignore"`.
|
|
567
635
|
*
|
|
568
636
|
* @param options - App name and router preset
|
|
569
637
|
*/
|
|
638
|
+
export function oke(
|
|
639
|
+
options: OkeOptions & { readonly registry: "ignore" },
|
|
640
|
+
): OkeApp<Record<string, never>, Record<string, never>>;
|
|
641
|
+
export function oke(options: OkeOptions): OkeApp<{}, RoutesFromRegisteredUnits>;
|
|
570
642
|
export function oke(options: OkeOptions): OkeApp {
|
|
571
643
|
const aot = options.aot !== false;
|
|
572
644
|
const registry = options.registry ?? "consume";
|
|
@@ -576,11 +648,12 @@ export function oke(options: OkeOptions): OkeApp {
|
|
|
576
648
|
: [...listBindings(), ...(options.bindings ?? [])];
|
|
577
649
|
if (registry === "consume") resetBindings();
|
|
578
650
|
|
|
579
|
-
// Same registry mode drains store
|
|
580
|
-
// channel.<medium>().template(), and
|
|
581
|
-
// module-evaluation registries
|
|
582
|
-
// (`listBindings`/`resetBindings` above).
|
|
583
|
-
// `
|
|
651
|
+
// Same registry mode drains store.*, vault.secret, signal(), clock(),
|
|
652
|
+
// gate.policy/scope/rate, channel.<medium>().template(), and
|
|
653
|
+
// ai.model/prompt/embed/agent/mcpServer — module-evaluation registries
|
|
654
|
+
// that mirror `on`'s trigger drain (`listBindings`/`resetBindings` above).
|
|
655
|
+
// Explicit `options.stores` / `secrets` / `signals` / `clocks` /
|
|
656
|
+
// `gate.policies` / `channel.templates` / `ai` are additive, never
|
|
584
657
|
// silently ignored — deduped by reference so an explicitly-passed decl
|
|
585
658
|
// that is also in the registry is not doubled.
|
|
586
659
|
const registrySnapshot =
|
|
@@ -590,6 +663,8 @@ export function oke(options: OkeOptions): OkeApp {
|
|
|
590
663
|
secrets: [],
|
|
591
664
|
requiredEnv: [],
|
|
592
665
|
signals: [],
|
|
666
|
+
clocks: [],
|
|
667
|
+
gates: [],
|
|
593
668
|
channelTemplates: [],
|
|
594
669
|
aiModels: [],
|
|
595
670
|
aiPrompts: [],
|
|
@@ -602,6 +677,8 @@ export function oke(options: OkeOptions): OkeApp {
|
|
|
602
677
|
secrets: secretRegistry.slice(),
|
|
603
678
|
requiredEnv: requiredEnvRegistry.slice(),
|
|
604
679
|
signals: signalRegistry.slice(),
|
|
680
|
+
clocks: clockRegistry.slice(),
|
|
681
|
+
gates: gateRegistry.slice(),
|
|
605
682
|
channelTemplates: channelTemplateRegistry.slice(),
|
|
606
683
|
aiModels: aiModelRegistry.slice(),
|
|
607
684
|
aiPrompts: aiPromptRegistry.slice(),
|
|
@@ -614,6 +691,8 @@ export function oke(options: OkeOptions): OkeApp {
|
|
|
614
691
|
secretRegistry.length = 0;
|
|
615
692
|
requiredEnvRegistry.length = 0;
|
|
616
693
|
signalRegistry.length = 0;
|
|
694
|
+
clockRegistry.length = 0;
|
|
695
|
+
gateRegistry.length = 0;
|
|
617
696
|
channelTemplateRegistry.length = 0;
|
|
618
697
|
aiModelRegistry.length = 0;
|
|
619
698
|
aiPromptRegistry.length = 0;
|
|
@@ -624,6 +703,8 @@ export function oke(options: OkeOptions): OkeApp {
|
|
|
624
703
|
const effectiveStores = mergeUnique(options.stores, registrySnapshot.stores);
|
|
625
704
|
const effectiveSecrets = mergeUnique(options.secrets, registrySnapshot.secrets);
|
|
626
705
|
const effectiveSignals = mergeUnique(options.signals, registrySnapshot.signals);
|
|
706
|
+
const effectiveClocks = mergeUnique(options.clocks, registrySnapshot.clocks);
|
|
707
|
+
const effectivePolicies = mergeUnique(options.gate?.policies, registrySnapshot.gates);
|
|
627
708
|
/**
|
|
628
709
|
* Fold registered `vault.env.required` names into the vault boot options so
|
|
629
710
|
* boot reports missing env vars in the same gap list as missing secrets.
|
|
@@ -658,16 +739,23 @@ export function oke(options: OkeOptions): OkeApp {
|
|
|
658
739
|
|
|
659
740
|
// Resolve Gate bag early so auth HTTP Bindings join `adopted` + the router
|
|
660
741
|
// before posture audit (same ensureBoot → doBoot path — never a side channel).
|
|
742
|
+
const mergedGate: GateOptions | undefined =
|
|
743
|
+
effectivePolicies.length > 0 ? { ...options.gate, policies: effectivePolicies } : options.gate;
|
|
661
744
|
const gateConfig: ResolvedGateConfig = resolveGateConfig({
|
|
662
|
-
gate:
|
|
745
|
+
gate: mergedGate,
|
|
663
746
|
env: options.env,
|
|
664
747
|
});
|
|
665
748
|
|
|
666
|
-
/** Retained for test harness / boot merges (includes auto-drained
|
|
667
|
-
const $options: OkeOptions =
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
749
|
+
/** Retained for test harness / boot merges (includes auto-drained decls). */
|
|
750
|
+
const $options: OkeOptions = {
|
|
751
|
+
...options,
|
|
752
|
+
...(effectiveAi !== undefined ? { ai: effectiveAi } : {}),
|
|
753
|
+
...(effectiveStores.length > 0 ? { stores: effectiveStores } : {}),
|
|
754
|
+
...(effectiveSecrets.length > 0 ? { secrets: effectiveSecrets } : {}),
|
|
755
|
+
...(effectiveSignals.length > 0 ? { signals: effectiveSignals } : {}),
|
|
756
|
+
...(effectiveClocks.length > 0 ? { clocks: effectiveClocks } : {}),
|
|
757
|
+
...(mergedGate !== undefined ? { gate: mergedGate } : {}),
|
|
758
|
+
};
|
|
671
759
|
|
|
672
760
|
const appHooks: HookMap = {};
|
|
673
761
|
const unitHooks = new Map<string, HookMap>();
|
|
@@ -701,8 +789,19 @@ export function oke(options: OkeOptions): OkeApp {
|
|
|
701
789
|
}
|
|
702
790
|
|
|
703
791
|
const smart = createRouter<Binding>(options.router ?? "default");
|
|
792
|
+
const seenHttpRoutes = new Set<string>();
|
|
704
793
|
for (const b of adopted) {
|
|
705
794
|
if (b.trigger.kind === "http") {
|
|
795
|
+
assertHttpBindingReady(b);
|
|
796
|
+
const key = `${b.trigger.method} ${b.trigger.path}`;
|
|
797
|
+
if (seenHttpRoutes.has(key)) {
|
|
798
|
+
throwOke("HTTP_ROUTE_DUPLICATE", {
|
|
799
|
+
method: b.trigger.method,
|
|
800
|
+
path: b.trigger.path,
|
|
801
|
+
flow: b.flow.name || "(unnamed)",
|
|
802
|
+
});
|
|
803
|
+
}
|
|
804
|
+
seenHttpRoutes.add(key);
|
|
706
805
|
smart.add(b.trigger.method, b.trigger.path, b);
|
|
707
806
|
compiled.set(b, compileHttpBinding(b, aot));
|
|
708
807
|
}
|
|
@@ -715,6 +814,16 @@ export function oke(options: OkeOptions): OkeApp {
|
|
|
715
814
|
adopted.push(b);
|
|
716
815
|
registerFlow(b.flow);
|
|
717
816
|
if (b.trigger.kind === "http") {
|
|
817
|
+
assertHttpBindingReady(b);
|
|
818
|
+
const key = `${b.trigger.method} ${b.trigger.path}`;
|
|
819
|
+
if (seenHttpRoutes.has(key)) {
|
|
820
|
+
throwOke("HTTP_ROUTE_DUPLICATE", {
|
|
821
|
+
method: b.trigger.method,
|
|
822
|
+
path: b.trigger.path,
|
|
823
|
+
flow: b.flow.name || "(unnamed)",
|
|
824
|
+
});
|
|
825
|
+
}
|
|
826
|
+
seenHttpRoutes.add(key);
|
|
718
827
|
smart.add(b.trigger.method, b.trigger.path, b);
|
|
719
828
|
compiled.set(b, compileHttpBinding(b, aot));
|
|
720
829
|
}
|
|
@@ -788,6 +897,11 @@ export function oke(options: OkeOptions): OkeApp {
|
|
|
788
897
|
registerFlow(b.flow);
|
|
789
898
|
}
|
|
790
899
|
|
|
900
|
+
if (registry !== "ignore") {
|
|
901
|
+
const generatedUnits = consumeRegisteredFlowUnits();
|
|
902
|
+
drainGeneratedUnits(generatedUnits, routes, registerFlow);
|
|
903
|
+
}
|
|
904
|
+
|
|
791
905
|
// --- boot (vault → store → signal → clock → channel → AI → runs → caps) ---
|
|
792
906
|
let bootResult: BootResult | undefined;
|
|
793
907
|
let bootPromise: Promise<BootResult> | undefined;
|
|
@@ -963,7 +1077,7 @@ export function oke(options: OkeOptions): OkeApp {
|
|
|
963
1077
|
vault: withRequiredEnv(overrides?.vault ?? options.vault),
|
|
964
1078
|
gates: [...baseGates, ...authGates],
|
|
965
1079
|
signals: overrides?.signals ?? effectiveSignals,
|
|
966
|
-
clocks: overrides?.clocks ??
|
|
1080
|
+
clocks: overrides?.clocks ?? effectiveClocks,
|
|
967
1081
|
stores: overrides?.stores ?? effectiveStores,
|
|
968
1082
|
channel: overrides?.channel ?? effectiveChannel,
|
|
969
1083
|
ai: overrides?.ai ?? effectiveAi,
|
|
@@ -1019,7 +1133,7 @@ export function oke(options: OkeOptions): OkeApp {
|
|
|
1019
1133
|
|
|
1020
1134
|
const baseSecrets = overrides?.secrets ?? effectiveSecrets;
|
|
1021
1135
|
const baseSignals = overrides?.signals ?? effectiveSignals;
|
|
1022
|
-
const baseClocks = overrides?.clocks ??
|
|
1136
|
+
const baseClocks = overrides?.clocks ?? effectiveClocks;
|
|
1023
1137
|
const baseChannel = overrides?.channel ?? effectiveChannel;
|
|
1024
1138
|
const mergedCatalog = mergeTemplateCatalogs(baseChannel?.catalog, ...pluginChannelCatalogs);
|
|
1025
1139
|
|
|
@@ -1065,6 +1179,10 @@ export function oke(options: OkeOptions): OkeApp {
|
|
|
1065
1179
|
};
|
|
1066
1180
|
const result = await bootApplication(merged);
|
|
1067
1181
|
bootResult = result;
|
|
1182
|
+
if (gateConfig.auth?.apiKeyStore && result.store) {
|
|
1183
|
+
const { bindHostApiKeySqlFromStore } = await import("../auth/api-key-sql.ts");
|
|
1184
|
+
await bindHostApiKeySqlFromStore(result.store, gateConfig.auth.apiKeyStore);
|
|
1185
|
+
}
|
|
1068
1186
|
// otp() provider / app mode capability — fail loud at boot, never silent downgrade.
|
|
1069
1187
|
if (result.channel) {
|
|
1070
1188
|
const { assertOtpPluginCapability } = await import("../auth/otp-capability.ts");
|
|
@@ -1138,6 +1256,8 @@ export function oke(options: OkeOptions): OkeApp {
|
|
|
1138
1256
|
readonly bypassGates?: boolean;
|
|
1139
1257
|
/** Console Call API — cleartext store PII. Requires {@link trustedInvoke}. */
|
|
1140
1258
|
readonly revealPii?: boolean;
|
|
1259
|
+
/** Forced RLS bag (Console / Call API). Operator omit = no stamp. */
|
|
1260
|
+
readonly rls?: import("../drivers/pg-rls.ts").RlsIdentity;
|
|
1141
1261
|
/** Frozen origin identity for {@link Fx.principal} across `fx.call`. */
|
|
1142
1262
|
readonly originPrincipal?: FxPrincipal;
|
|
1143
1263
|
/** Explicit locale override for {@link Fx.t} / channel sends. */
|
|
@@ -1189,6 +1309,7 @@ export function oke(options: OkeOptions): OkeApp {
|
|
|
1189
1309
|
readonly trustedInvoke?: boolean;
|
|
1190
1310
|
readonly bypassGates?: boolean;
|
|
1191
1311
|
readonly revealPii?: boolean;
|
|
1312
|
+
readonly rls?: import("../drivers/pg-rls.ts").RlsIdentity;
|
|
1192
1313
|
readonly originPrincipal?: FxPrincipal;
|
|
1193
1314
|
readonly locale?: string;
|
|
1194
1315
|
readonly parentId?: string;
|
|
@@ -1251,6 +1372,7 @@ export function oke(options: OkeOptions): OkeApp {
|
|
|
1251
1372
|
userId: options.fx?.auth?.userId ?? null,
|
|
1252
1373
|
scopes: new Set(options.fx?.auth?.scopes ?? []),
|
|
1253
1374
|
verified: options.fx?.auth?.verified,
|
|
1375
|
+
apiKeyId: options.fx?.auth?.apiKeyId,
|
|
1254
1376
|
},
|
|
1255
1377
|
operator: { id: options.fx?.operator?.id ?? null },
|
|
1256
1378
|
};
|
|
@@ -1279,7 +1401,8 @@ export function oke(options: OkeOptions): OkeApp {
|
|
|
1279
1401
|
: {}),
|
|
1280
1402
|
verifyBearer:
|
|
1281
1403
|
binding && wiredAuth
|
|
1282
|
-
?
|
|
1404
|
+
? (token, request) =>
|
|
1405
|
+
wiredAuth.verifyBearerOrApiKey(binding, token, apiKeyStore, request)
|
|
1283
1406
|
: undefined,
|
|
1284
1407
|
// Phase 1a: opt-in cookie → Bearer when Authorization is absent.
|
|
1285
1408
|
resolveToken:
|
|
@@ -1336,9 +1459,13 @@ export function oke(options: OkeOptions): OkeApp {
|
|
|
1336
1459
|
catalogs: loadMessages().getMessageCatalogs(),
|
|
1337
1460
|
...options.fx?.i18n,
|
|
1338
1461
|
},
|
|
1339
|
-
...(principals ? { auth: principals.auth
|
|
1462
|
+
...(principals ? { auth: principals.auth, operator: principals.operator } : {}),
|
|
1463
|
+
apiKeyStore: gateConfig.auth?.apiKeyStore,
|
|
1340
1464
|
...(extras?.originPrincipal ? { principal: extras.originPrincipal } : {}),
|
|
1341
1465
|
...(extras?.trustedInvoke === true && extras.revealPii === true ? { revealPii: true } : {}),
|
|
1466
|
+
rlsGateNames: gateNamesOf(trigger),
|
|
1467
|
+
rlsBypass: extras?.bypassGates === true || flowDef.plane === "operator",
|
|
1468
|
+
...(extras?.rls ? { rls: extras.rls } : {}),
|
|
1342
1469
|
...(capability ? { capability } : {}),
|
|
1343
1470
|
...(booted
|
|
1344
1471
|
? {
|
|
@@ -1487,6 +1614,7 @@ export function oke(options: OkeOptions): OkeApp {
|
|
|
1487
1614
|
}
|
|
1488
1615
|
return await invoke(ctx.input);
|
|
1489
1616
|
} catch (err) {
|
|
1617
|
+
if (isFlowFailure(err)) return err;
|
|
1490
1618
|
// A durable sleep that has not yet elapsed suspends the run — this
|
|
1491
1619
|
// is a park, not a pipeline failure, so it must not throw upward.
|
|
1492
1620
|
if (flowDef.durable && journalSession && isJournalSuspend(err)) {
|
|
@@ -1663,6 +1791,7 @@ export function oke(options: OkeOptions): OkeApp {
|
|
|
1663
1791
|
get authBinding() {
|
|
1664
1792
|
return authBinding;
|
|
1665
1793
|
},
|
|
1794
|
+
apiKeys: gateConfig.auth?.apiKeyStore,
|
|
1666
1795
|
async resumeDurable(now) {
|
|
1667
1796
|
const t = now ?? bootResult?.clock?.now() ?? options.fx?.now?.() ?? Date.now();
|
|
1668
1797
|
const { store, instanceId, leaseMs } = activeJournal();
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* separately in the pipeline (`allowTestPrincipals`).
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
-
import { authenticateApiKey, type ApiKeyStore } from "../auth/api-keys.ts";
|
|
9
|
+
import { authenticateApiKey, clientIpFromRequest, type ApiKeyStore } from "../auth/api-keys.ts";
|
|
10
10
|
import type { ApiKeyRow } from "../auth/tables.ts";
|
|
11
11
|
import {
|
|
12
12
|
createSessionStore,
|
|
@@ -89,7 +89,9 @@ export function claimsToPrincipal(claims: AccessClaims): ResolvedPrincipal {
|
|
|
89
89
|
|
|
90
90
|
/**
|
|
91
91
|
* Map an authenticated API key row to the pipeline principal shape.
|
|
92
|
-
*
|
|
92
|
+
*
|
|
93
|
+
* The key is the issuer with fewer gates: `userId` / `operatorId` is
|
|
94
|
+
* {@link ApiKeyRow.creatorId}. The credential id is {@link ResolvedPrincipal.apiKeyId}.
|
|
93
95
|
*
|
|
94
96
|
* @param row - Authenticated key
|
|
95
97
|
*/
|
|
@@ -97,7 +99,7 @@ export function apiKeyRowToPrincipal(row: ApiKeyRow): ResolvedPrincipal {
|
|
|
97
99
|
if (row.plane === "operator") {
|
|
98
100
|
return {
|
|
99
101
|
plane: "operator",
|
|
100
|
-
operatorId: row.
|
|
102
|
+
operatorId: row.creatorId,
|
|
101
103
|
userId: null,
|
|
102
104
|
scopes: row.scopes,
|
|
103
105
|
verified: true,
|
|
@@ -106,7 +108,7 @@ export function apiKeyRowToPrincipal(row: ApiKeyRow): ResolvedPrincipal {
|
|
|
106
108
|
}
|
|
107
109
|
return {
|
|
108
110
|
plane: "user",
|
|
109
|
-
userId: row.
|
|
111
|
+
userId: row.creatorId,
|
|
110
112
|
scopes: row.scopes,
|
|
111
113
|
verified: true,
|
|
112
114
|
apiKeyId: row.id,
|
|
@@ -119,17 +121,20 @@ export function apiKeyRowToPrincipal(row: ApiKeyRow): ResolvedPrincipal {
|
|
|
119
121
|
* @param auth - Session binding
|
|
120
122
|
* @param token - Raw Bearer token
|
|
121
123
|
* @param apiKeys - Optional key store
|
|
124
|
+
* @param request - Incoming request (allowlist IP is read here, not in `oke()`)
|
|
122
125
|
*/
|
|
123
126
|
export async function verifyBearerOrApiKey(
|
|
124
127
|
auth: AppAuthBinding,
|
|
125
128
|
token: string,
|
|
126
129
|
apiKeys?: ApiKeyStore,
|
|
130
|
+
request?: Request,
|
|
127
131
|
): Promise<ResolvedPrincipal> {
|
|
128
132
|
try {
|
|
129
133
|
return await verifyBearerToken(auth, token);
|
|
130
134
|
} catch (err) {
|
|
131
135
|
if (apiKeys) {
|
|
132
|
-
const
|
|
136
|
+
const ip = request ? clientIpFromRequest(request) : undefined;
|
|
137
|
+
const row = await authenticateApiKey(apiKeys, token, { now: auth.now, ip });
|
|
133
138
|
if (row) return apiKeyRowToPrincipal(row);
|
|
134
139
|
}
|
|
135
140
|
throw err;
|