okengine 0.15.2 → 0.17.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/manifest.v1.schema.json +21 -2
- package/package.json +1 -1
- package/site/content/docs/elements/clock.mdx +7 -0
- package/site/content/docs/elements/flow.mdx +20 -12
- package/site/content/docs/elements/gate.mdx +108 -3
- package/site/content/docs/elements/signal.mdx +46 -17
- package/site/content/docs/elements/store.mdx +36 -32
- package/site/content/docs/elements/vault.mdx +26 -0
- package/site/content/docs/get-started/project-structure.mdx +4 -4
- package/site/content/docs/reference/cli.md +1 -1
- package/site/content/docs/reference/client.mdx +75 -17
- package/site/content/docs/reference/configuration.mdx +7 -4
- package/site/content/docs/reference/errors.mdx +24 -17
- package/site/content/docs/reference/fx.mdx +39 -22
- package/site/content/docs/reference/security.md +1 -1
- package/src/auth/api-key-sql.test.ts +47 -0
- package/src/auth/api-key-sql.ts +261 -0
- package/src/auth/api-keys.ts +276 -14
- package/src/auth/auth.test.ts +56 -0
- package/src/auth/config.ts +24 -5
- package/src/auth/index.ts +62 -1
- package/src/auth/plugin.ts +6 -1
- package/src/auth/schema.ts +8 -0
- package/src/auth/sessions.ts +18 -0
- package/src/auth/tables.ts +32 -0
- package/src/auth/tenant-config.ts +74 -0
- package/src/auth/tenant-tables.ts +11 -0
- package/src/auth/tenants.test.ts +63 -0
- package/src/auth/tenants.ts +360 -0
- package/src/cli/attach-host-console.test.ts +37 -0
- package/src/cli/attach-host-console.ts +6 -0
- package/src/cli/build.ts +2 -2
- package/src/client/budget.test.ts +1 -1
- package/src/client/create.ts +75 -5
- package/src/client/index.ts +13 -0
- package/src/client/live.test.ts +422 -0
- package/src/client/live.ts +389 -0
- package/src/client/notes-contract.test.ts +41 -0
- package/src/client/types.ts +85 -6
- package/src/client-react/index.ts +85 -1
- package/src/client-react/use-live.test.ts +129 -0
- package/src/compiler/effects-infer.ts +37 -2
- package/src/compiler/extract.test.ts +143 -11
- package/src/compiler/extract.ts +210 -30
- package/src/compiler/fixtures/skyport/src/flows/bookings/index.ts +1 -2
- package/src/compiler/fixtures/skyport.expected.json +2 -3
- package/src/compiler/response.ts +41 -11
- package/src/console/server/access.test.ts +116 -8
- package/src/console/server/access.ts +70 -23
- package/src/console/server/app.ts +45 -11
- package/src/console/server/console.test.ts +6 -8
- package/src/console/server/dev-identities.test.ts +127 -0
- package/src/console/server/dev-identities.ts +229 -0
- package/src/console/server/flows-invoke.test.ts +21 -4
- package/src/console/server/flows.ts +113 -58
- package/src/console/server/gates.test.ts +24 -0
- package/src/console/server/gates.ts +10 -30
- package/src/console/server/index.ts +1 -0
- package/src/console/server/live.test.ts +26 -1
- package/src/console/server/live.ts +4 -2
- package/src/console/server/operator-db.ts +2 -0
- package/src/console/server/state.ts +29 -55
- package/src/console/server/store.test.ts +17 -0
- package/src/console/server/store.ts +43 -1
- package/src/console/ui-next/dist/assets/access-page-CXVWWMmD.js +4 -0
- package/src/console/ui-next/dist/assets/{agent-disclosure-C0X1fbWF.js → agent-disclosure-BP0Y0Sux.js} +1 -1
- package/src/console/ui-next/dist/assets/cache-glyph-B5X-NM0-.js +1 -0
- package/src/console/ui-next/dist/assets/call-pii-button-ChAUiLo9.js +1 -0
- package/src/console/ui-next/dist/assets/collapsible-DGnOM2ph.js +1 -0
- package/src/console/ui-next/dist/assets/copy-inline-button-DBHhgHKP.js +1 -0
- package/src/console/ui-next/dist/assets/detail-header-DhHM1iaZ.js +1 -0
- package/src/console/ui-next/dist/assets/dropdown-menu-_NjTEo5_.js +1 -0
- package/src/console/ui-next/dist/assets/element-icons-BVXtRyd3.js +1 -0
- package/src/console/ui-next/dist/assets/explorer-empty-2uIhBu0_.js +1 -0
- package/src/console/ui-next/dist/assets/flows-page-XDpAJO8f.js +1 -0
- package/src/console/ui-next/dist/assets/{highlighted-json-CS_O8L-r.js → highlighted-json-Awq7gYdu.js} +1 -1
- package/src/console/ui-next/dist/assets/http-method-_UHM2ODJ.js +1 -0
- package/src/console/ui-next/dist/assets/index-BBj2QJCu.js +66 -0
- package/src/console/ui-next/dist/assets/index-Ck88Jmv8.css +2 -0
- package/src/console/ui-next/dist/assets/link-BX6Vqztd.js +1 -0
- package/src/console/ui-next/dist/assets/observability-page-BFaay44m.js +4 -0
- package/src/console/ui-next/dist/assets/replica-lag-DKRrbvdo.js +18 -0
- package/src/console/ui-next/dist/assets/request-meta-DV0ywz7t.js +1 -0
- package/src/console/ui-next/dist/assets/shortcut-JQIZlWfm.js +1 -0
- package/src/console/ui-next/dist/assets/shortcut-keys-DKxNTe_m.js +1 -0
- package/src/console/ui-next/dist/assets/skeleton-D-czQJT6.js +1 -0
- package/src/console/ui-next/dist/assets/store-page-CS5-aETQ.js +41 -0
- package/src/console/ui-next/dist/assets/trace-detail-sheet-B09O8rA6.js +2 -0
- package/src/console/ui-next/dist/assets/tree-expand-toggle-BtyhmWb4.js +55 -0
- package/src/console/ui-next/dist/assets/units-page-Ca2Z-E52.js +1 -0
- package/src/console/ui-next/dist/assets/{use-vault-list-BLbZhzyF.js → use-vault-list-uk4WVboC.js} +1 -1
- package/src/console/ui-next/dist/assets/useMutation-B8EO02Ej.js +1 -0
- package/src/console/ui-next/dist/assets/useRender-BE2A9BWC.js +1 -0
- package/src/console/ui-next/dist/assets/vault-page-BmOeAFwg.js +2 -0
- package/src/console/ui-next/dist/index.html +11 -9
- package/src/console/ui-next/index.html +3 -3
- package/src/console/ui-next/src/client.ts +167 -0
- package/src/console/ui-next/src/components/shell/app-sidebar.tsx +2 -0
- package/src/console/ui-next/src/components/shell/command-items.test.ts +1 -0
- package/src/console/ui-next/src/components/shell/command-items.ts +8 -0
- package/src/console/ui-next/src/components/shell/shell-layout.tsx +2 -1
- package/src/console/ui-next/src/components/ui/sheet-form.tsx +8 -3
- package/src/console/ui-next/src/features/access/access-page.tsx +347 -0
- package/src/console/ui-next/src/features/access/data/use-access.ts +23 -0
- package/src/console/ui-next/src/features/access/detail/access-detail.tsx +552 -0
- package/src/console/ui-next/src/features/access/explorer/access-list.tsx +125 -0
- package/src/console/ui-next/src/features/access/lib/format-when.test.ts +139 -0
- package/src/console/ui-next/src/features/access/lib/format-when.ts +296 -0
- package/src/console/ui-next/src/features/access/lib/scope-groups.test.ts +34 -0
- package/src/console/ui-next/src/features/access/lib/scope-groups.ts +40 -0
- package/src/console/ui-next/src/features/access/sheets/access-allow-fields.tsx +236 -0
- package/src/console/ui-next/src/features/access/sheets/access-create-sheet.tsx +316 -0
- package/src/console/ui-next/src/features/access/sheets/access-edit-sheet.tsx +240 -0
- package/src/console/ui-next/src/features/access/sheets/access-expiry-fields.tsx +66 -0
- package/src/console/ui-next/src/features/access/sheets/access-refresh-sheet.tsx +104 -0
- package/src/console/ui-next/src/features/access/sheets/access-scope-field.tsx +115 -0
- package/src/console/ui-next/src/features/access/sheets/access-secret-sheet.tsx +89 -0
- package/src/console/ui-next/src/features/access/sheets/access-sheet-search.tsx +49 -0
- package/src/console/ui-next/src/features/access/state/access-selection.ts +84 -0
- package/src/console/ui-next/src/features/auth/auth-redirect.test.ts +5 -0
- package/src/console/ui-next/src/features/auth/auth-redirect.ts +18 -2
- package/src/console/ui-next/src/features/flows/fixture.ts +0 -1
- package/src/console/ui-next/src/features/not-found/not-found-page.tsx +1 -1
- package/src/console/ui-next/src/features/units/detail/flow-contract-panel.tsx +5 -1
- package/src/console/ui-next/src/features/units/lib/unit-tree.test.ts +15 -4
- package/src/console/ui-next/src/lib/document-meta.test.ts +1 -0
- package/src/console/ui-next/src/lib/document-meta.ts +2 -1
- package/src/console/ui-next/src/lib/last-module-search.test.ts +1 -0
- package/src/console/ui-next/src/lib/last-module-search.ts +2 -1
- package/src/console/ui-next/src/lib/shortcut.test.ts +2 -0
- package/src/console/ui-next/src/lib/shortcut.ts +3 -0
- package/src/console/ui-next/src/router.tsx +10 -1
- package/src/console/ui-next/ui-next-seed-app-schema.ts +88 -37
- package/src/console/ui-next/ui-next-seed-manifest-surface.ts +2 -9
- package/src/console/ui-next/ui-next-seed-manifest.ts +0 -1
- package/src/console/ui-next/ui-next-seed-store.ts +43 -15
- package/src/console/xss.gate.test.ts +1 -0
- package/src/drivers/index.ts +2 -0
- package/src/drivers/journal-postgres.ts +12 -3
- package/src/drivers/pg-rls.ts +26 -2
- package/src/drivers/pg-vault-rls.ts +68 -0
- package/src/drivers/signal-engine.ts +69 -15
- package/src/drivers/signal-live-iter.ts +65 -0
- package/src/drivers/signal-nats.ts +2 -1
- package/src/drivers/signal-postgres.ts +95 -12
- package/src/drivers/signal-redis.ts +2 -1
- package/src/drivers/signal-retention.ts +64 -0
- package/src/drivers/signal-types.ts +35 -5
- package/src/elements/clock/declare.ts +64 -2
- package/src/elements/clock/reconcile.ts +98 -25
- package/src/elements/clock/runtime.ts +13 -2
- package/src/elements/clock.test.ts +28 -0
- package/src/elements/clock.ts +9 -1
- package/src/elements/gate/declare.ts +2 -0
- package/src/elements/gate/permissions.ts +12 -0
- package/src/elements/gate.ts +6 -1
- package/src/elements/signal/declare.ts +44 -10
- package/src/elements/signal/delivery-modes.test.ts +90 -4
- package/src/elements/signal/order-lifecycle.test.ts +20 -4
- package/src/elements/signal/runtime.ts +42 -0
- package/src/elements/signal.test.ts +21 -8
- package/src/elements/signal.ts +1 -1
- package/src/elements/store/cache.ts +1 -1
- package/src/elements/store/declare.ts +7 -0
- package/src/elements/store/rls-identity.test.ts +29 -0
- package/src/elements/store/rls-identity.ts +3 -0
- package/src/elements/store/runtime.ts +14 -0
- package/src/elements/store/schema-decl.ts +162 -44
- package/src/elements/store/schema-tenant.ts +41 -0
- package/src/elements/store/sql-rls-isolation.test.ts +39 -0
- package/src/elements/store/sql-select.types.test.ts +71 -0
- package/src/elements/store/sql-session.ts +54 -32
- package/src/elements/store.ts +5 -0
- package/src/elements/vault/builtin-adapter.ts +15 -2
- package/src/elements/vault/declare.ts +8 -0
- package/src/elements/vault/runtime.ts +7 -0
- package/src/elements/vault/sql-rls-isolation.test.ts +145 -0
- package/src/elements/vault/storage.ts +9 -0
- package/src/elements/vault/test-helpers.ts +2 -1
- package/src/i18n/catalogs/ar.ts +19 -0
- package/src/i18n/catalogs/en.ts +19 -0
- package/src/index.ts +2 -0
- package/src/kernel/adopt-routes.ts +56 -8
- package/src/kernel/api-key-host-persist.test.ts +143 -0
- package/src/kernel/app-tenant.ts +122 -0
- package/src/kernel/app.ts +196 -57
- package/src/kernel/auth-resolve.ts +13 -5
- package/src/kernel/boot-bind/clock.ts +9 -3
- package/src/kernel/boot.test.ts +4 -4
- package/src/kernel/boot.ts +2 -0
- package/src/kernel/budget.test.ts +1 -1
- package/src/kernel/clock-durable.ts +8 -0
- package/src/kernel/clock-per-tenant-name.ts +5 -0
- package/src/kernel/clock-reconcile.ts +8 -0
- package/src/kernel/errors-live-resume.ts +15 -0
- package/src/kernel/errors-tenant.ts +29 -0
- package/src/kernel/errors.registry.test.ts +31 -3
- package/src/kernel/errors.ts +43 -3
- package/src/kernel/flow.ts +26 -5
- package/src/kernel/fx-auth-keys.test.ts +88 -0
- package/src/kernel/fx-auth-keys.ts +203 -0
- package/src/kernel/fx-auth-tenants.test.ts +87 -0
- package/src/kernel/fx-auth-tenants.ts +286 -0
- package/src/kernel/fx-live-stream.ts +149 -0
- package/src/kernel/fx-live.test.ts +157 -0
- package/src/kernel/fx-runtime.ts +15 -0
- package/src/kernel/fx-tenant-store.ts +213 -0
- package/src/kernel/fx.test.ts +108 -1
- package/src/kernel/fx.ts +206 -28
- package/src/kernel/hooks.ts +2 -2
- package/src/kernel/http-resource.ts +9 -18
- package/src/kernel/index.ts +2 -0
- package/src/kernel/journal.ts +12 -0
- package/src/kernel/live-http.test.ts +78 -0
- package/src/kernel/live-http.ts +114 -0
- package/src/kernel/live-resume.test.ts +125 -0
- package/src/kernel/on.ts +51 -0
- package/src/kernel/pipeline-api-key.test.ts +192 -7
- package/src/kernel/pipeline-tenant.ts +49 -0
- package/src/kernel/pipeline.test.ts +1 -1
- package/src/kernel/pipeline.ts +41 -4
- package/src/kernel/resource-mount.test.ts +10 -27
- package/src/kernel/tenant-resolve.test.ts +101 -0
- package/src/kernel/tenant-resolve.ts +124 -0
- package/src/kernel/tenant-roles.test.ts +87 -0
- package/src/kernel/triggers.ts +59 -21
- package/src/manifest/diff.test.ts +11 -2
- package/src/manifest/diff.ts +53 -11
- package/src/manifest/fixtures/skyport.excerpt.json +1 -1
- package/src/manifest/fixtures/skyport.manifest.json +0 -1
- package/src/manifest/types.ts +58 -6
- package/src/release/build-lib.ts +14 -1
- package/src/release/limits.ts +2 -2
- package/src/release/measure.ts +55 -1
- package/src/client/live-gap.test.ts +0 -35
- package/src/console/ui-next/dist/assets/cache-glyph-F1FI122b.js +0 -1
- package/src/console/ui-next/dist/assets/call-pii-button-DEDCl_j3.js +0 -1
- package/src/console/ui-next/dist/assets/collapsible-LPqGvfoz.js +0 -1
- package/src/console/ui-next/dist/assets/confirm-sheet-3ptDzXbz.js +0 -1
- package/src/console/ui-next/dist/assets/explorer-empty-BvYrygyO.js +0 -1
- package/src/console/ui-next/dist/assets/flows-page-Dluo1Bd7.js +0 -1
- package/src/console/ui-next/dist/assets/http-method--sWDdXSB.js +0 -1
- package/src/console/ui-next/dist/assets/index-Ca3HZMVq.js +0 -66
- package/src/console/ui-next/dist/assets/index-UtTDRQpS.css +0 -2
- package/src/console/ui-next/dist/assets/link-COeyggt-.js +0 -1
- package/src/console/ui-next/dist/assets/observability-page-HK9-BO8z.js +0 -4
- package/src/console/ui-next/dist/assets/replica-lag-DBIFFf7d.js +0 -18
- package/src/console/ui-next/dist/assets/sheet-form-Dnwa5oOR.js +0 -1
- package/src/console/ui-next/dist/assets/shortcut-Cgzuv4k1.js +0 -1
- package/src/console/ui-next/dist/assets/shortcut-keys-CoqwEIi0.js +0 -1
- package/src/console/ui-next/dist/assets/skeleton-BHcfDYcb.js +0 -1
- package/src/console/ui-next/dist/assets/store-page-BSohH3wM.js +0 -41
- package/src/console/ui-next/dist/assets/trace-detail-sheet-CFIGRnnA.js +0 -2
- package/src/console/ui-next/dist/assets/tree-expand-toggle-BgLl34w7.js +0 -54
- package/src/console/ui-next/dist/assets/units-page-B0cFE76A.js +0 -1
- package/src/console/ui-next/dist/assets/vault-page-B1SbYe10.js +0 -2
|
@@ -8,7 +8,8 @@
|
|
|
8
8
|
import type { InferSchemaOutput } from "../validation/standard-schema.ts";
|
|
9
9
|
import type { AnyFlowDef, FlowDef, FlowErrorMap } from "./flow.ts";
|
|
10
10
|
import { isFlow } from "./flow.ts";
|
|
11
|
-
import type { HttpTrigger, Trigger } from "./triggers.ts";
|
|
11
|
+
import type { HttpTrigger, LiveHttpTrigger, Trigger } from "./triggers.ts";
|
|
12
|
+
import { httpPathParams } from "./live-http.ts";
|
|
12
13
|
|
|
13
14
|
/**
|
|
14
15
|
* One flow's route contract on `app.$routes` (compatible with client
|
|
@@ -52,6 +53,10 @@ export type AppRouteMap = {
|
|
|
52
53
|
readonly errors?: unknown;
|
|
53
54
|
readonly method?: string;
|
|
54
55
|
readonly path?: string;
|
|
56
|
+
readonly live?: string;
|
|
57
|
+
readonly matchKey?: readonly string[];
|
|
58
|
+
readonly gates?: readonly string[];
|
|
59
|
+
readonly stream?: true;
|
|
55
60
|
};
|
|
56
61
|
};
|
|
57
62
|
};
|
|
@@ -73,6 +78,28 @@ export type ErrorDataOf<E extends FlowErrorMap> = {
|
|
|
73
78
|
[K in keyof E]: InferSchemaOutput<E[K]>;
|
|
74
79
|
};
|
|
75
80
|
|
|
81
|
+
/**
|
|
82
|
+
* HTTP live exposure on `$routes` so `createClient(app)` types
|
|
83
|
+
* `api.unit.flow(input, { onEvent })` as subscribe, not JSON RPC.
|
|
84
|
+
*
|
|
85
|
+
* @typeParam I - Input
|
|
86
|
+
* @typeParam O - Output
|
|
87
|
+
* @typeParam E - Error map
|
|
88
|
+
* @typeParam M - HTTP method
|
|
89
|
+
* @typeParam P - Path
|
|
90
|
+
*/
|
|
91
|
+
export type LiveAppFlowRoute<
|
|
92
|
+
I = unknown,
|
|
93
|
+
O = unknown,
|
|
94
|
+
E extends Record<string, unknown> = Record<string, unknown>,
|
|
95
|
+
M extends string = string,
|
|
96
|
+
P extends string = string,
|
|
97
|
+
> = AppFlowRoute<I, O, E, M, P> & {
|
|
98
|
+
readonly live: string;
|
|
99
|
+
readonly stream: true;
|
|
100
|
+
readonly matchKey: readonly string[];
|
|
101
|
+
};
|
|
102
|
+
|
|
76
103
|
/**
|
|
77
104
|
* Derive a client route contract from a {@link FlowDef} (trigger → REST).
|
|
78
105
|
*
|
|
@@ -80,9 +107,11 @@ export type ErrorDataOf<E extends FlowErrorMap> = {
|
|
|
80
107
|
*/
|
|
81
108
|
export type RouteFromFlow<F> =
|
|
82
109
|
F extends FlowDef<infer I, infer O, infer E, any, infer T>
|
|
83
|
-
? [T] extends [
|
|
84
|
-
?
|
|
85
|
-
:
|
|
110
|
+
? [T] extends [LiveHttpTrigger<infer M, infer P>]
|
|
111
|
+
? LiveAppFlowRoute<I, O, ErrorDataOf<E>, M, P>
|
|
112
|
+
: [T] extends [HttpTrigger<infer M, infer P>]
|
|
113
|
+
? AppFlowRoute<I, O, ErrorDataOf<E>, M, P>
|
|
114
|
+
: AppFlowRoute<I, O, ErrorDataOf<E>>
|
|
86
115
|
: never;
|
|
87
116
|
|
|
88
117
|
/**
|
|
@@ -130,6 +159,14 @@ export type RoutesFromAdoptArgs<Args extends readonly unknown[]> = Args extends
|
|
|
130
159
|
export interface RuntimeFlowRoute {
|
|
131
160
|
readonly method?: string;
|
|
132
161
|
readonly path?: string;
|
|
162
|
+
/** SSE live signal name when the trigger used `.live(signal)`. */
|
|
163
|
+
readonly live?: string;
|
|
164
|
+
/** Sorted auto-match path-param names (empty = firehose). */
|
|
165
|
+
readonly matchKey?: readonly string[];
|
|
166
|
+
/** Flattened gate names on the HTTP trigger. */
|
|
167
|
+
readonly gates?: readonly string[];
|
|
168
|
+
/** True when this flow returns `text/event-stream`. */
|
|
169
|
+
readonly stream?: true;
|
|
133
170
|
}
|
|
134
171
|
|
|
135
172
|
/** Runtime `$routes` table (values; types live on {@link OkeApp}). */
|
|
@@ -146,10 +183,21 @@ export type RuntimeRouteMap = {
|
|
|
146
183
|
*/
|
|
147
184
|
export function runtimeRouteFromFlow(flowDef: AnyFlowDef): RuntimeFlowRoute {
|
|
148
185
|
const trigger: Trigger | undefined = flowDef.$trigger ?? flowDef.triggers[0];
|
|
149
|
-
if (trigger?.kind
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
return {
|
|
186
|
+
if (trigger?.kind !== "http") return {};
|
|
187
|
+
const liveName = trigger.liveSignal?.name ?? flowDef.live;
|
|
188
|
+
const gates = trigger.gates.map((g) => (typeof g === "string" ? g : g.name));
|
|
189
|
+
return {
|
|
190
|
+
method: trigger.method,
|
|
191
|
+
path: trigger.path,
|
|
192
|
+
...(liveName !== undefined
|
|
193
|
+
? {
|
|
194
|
+
live: liveName,
|
|
195
|
+
stream: true as const,
|
|
196
|
+
matchKey: httpPathParams(trigger.path),
|
|
197
|
+
...(gates.length > 0 ? { gates } : {}),
|
|
198
|
+
}
|
|
199
|
+
: {}),
|
|
200
|
+
};
|
|
153
201
|
}
|
|
154
202
|
|
|
155
203
|
/**
|
|
@@ -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
|
+
});
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* App-shell tenancy wiring — lazy chunk.
|
|
3
|
+
*
|
|
4
|
+
* One `w` dispatcher so Store-only `oke()` graphs do not spell Clock /
|
|
5
|
+
* pipeline tenant field names.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { ResolvedTenantAuth } from "../auth/tenant-config.ts";
|
|
9
|
+
import type { TenantStore } from "../auth/tenants.ts";
|
|
10
|
+
import type { ClockDecl } from "../elements/clock/declare.ts";
|
|
11
|
+
import { parsePerTenantCronName } from "./clock-per-tenant-name.ts";
|
|
12
|
+
import { orphanPerTenantCronRows, putPerTenantCronRows } from "./clock-reconcile.ts";
|
|
13
|
+
import type { AnyFlowDef } from "./flow.ts";
|
|
14
|
+
import type { PipelineTenantDeps } from "./pipeline.ts";
|
|
15
|
+
|
|
16
|
+
function cron(name: string): { readonly tenant: { readonly id: string } } | undefined {
|
|
17
|
+
const parsed = parsePerTenantCronName(name);
|
|
18
|
+
return parsed ? { tenant: { id: parsed.tenantId } } : undefined;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function boot(store: TenantStore): { readonly tenantIds: () => string[] } {
|
|
22
|
+
return { tenantIds: () => [...store.tenants.keys()] };
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function clock(
|
|
26
|
+
tenantStore: TenantStore,
|
|
27
|
+
clockStore: Parameters<typeof putPerTenantCronRows>[0],
|
|
28
|
+
templates: readonly ClockDecl[],
|
|
29
|
+
): void {
|
|
30
|
+
tenantStore.hooks = {
|
|
31
|
+
onCreate: (row) => putPerTenantCronRows(clockStore, templates, row.id),
|
|
32
|
+
onDelete: (row) => orphanPerTenantCronRows(clockStore, templates, row.id),
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function resume(auth: { readonly tenant?: unknown; readonly tenantStore?: TenantStore }): {
|
|
37
|
+
readonly tenantEnabled: boolean;
|
|
38
|
+
readonly tenantStore: TenantStore | undefined;
|
|
39
|
+
} {
|
|
40
|
+
return {
|
|
41
|
+
tenantEnabled: auth.tenant !== undefined,
|
|
42
|
+
tenantStore: auth.tenantStore,
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function pipe(
|
|
47
|
+
tenant: ResolvedTenantAuth,
|
|
48
|
+
store: TenantStore,
|
|
49
|
+
flowTenantScoped: boolean,
|
|
50
|
+
flowPlane: AnyFlowDef["plane"],
|
|
51
|
+
): { readonly tenant: PipelineTenantDeps } {
|
|
52
|
+
return {
|
|
53
|
+
tenant: {
|
|
54
|
+
config: tenant,
|
|
55
|
+
store,
|
|
56
|
+
flowTenantScoped,
|
|
57
|
+
flowPlane,
|
|
58
|
+
},
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function fx(args: {
|
|
63
|
+
readonly enabled: boolean;
|
|
64
|
+
readonly store: TenantStore | undefined;
|
|
65
|
+
readonly scoped: boolean;
|
|
66
|
+
readonly plane: AnyFlowDef["plane"];
|
|
67
|
+
readonly tenant: { readonly id: string | null } | undefined;
|
|
68
|
+
readonly bind?: { readonly secret: string; readonly now: () => number };
|
|
69
|
+
}): {
|
|
70
|
+
readonly tenantStore: TenantStore | undefined;
|
|
71
|
+
readonly tenantEnabled: boolean;
|
|
72
|
+
readonly flowTenantScoped: boolean;
|
|
73
|
+
readonly flowPlane: AnyFlowDef["plane"];
|
|
74
|
+
readonly sessionCrypto?: { readonly secret: string; readonly now: () => number };
|
|
75
|
+
readonly tenant?: { readonly id: string | null };
|
|
76
|
+
} {
|
|
77
|
+
return {
|
|
78
|
+
tenantStore: args.store,
|
|
79
|
+
tenantEnabled: args.enabled,
|
|
80
|
+
flowTenantScoped: args.scoped,
|
|
81
|
+
flowPlane: args.plane,
|
|
82
|
+
...(args.bind ? { sessionCrypto: { secret: args.bind.secret, now: args.bind.now } } : {}),
|
|
83
|
+
...(args.tenant ? { tenant: args.tenant } : {}),
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Dispatch tenancy wiring.
|
|
89
|
+
*
|
|
90
|
+
* 0 cron · 1 boot · 2 clock · 3 pipe · 4 fx · 5 resume
|
|
91
|
+
*
|
|
92
|
+
* @param kind - Operation
|
|
93
|
+
* @param args - Kind-specific arguments
|
|
94
|
+
*/
|
|
95
|
+
export function w(kind: number, ...args: unknown[]): unknown {
|
|
96
|
+
switch (kind) {
|
|
97
|
+
case 0:
|
|
98
|
+
return cron(args[0] as string);
|
|
99
|
+
case 1:
|
|
100
|
+
return boot(args[0] as TenantStore);
|
|
101
|
+
case 2:
|
|
102
|
+
clock(
|
|
103
|
+
args[0] as TenantStore,
|
|
104
|
+
args[1] as Parameters<typeof putPerTenantCronRows>[0],
|
|
105
|
+
args[2] as readonly ClockDecl[],
|
|
106
|
+
);
|
|
107
|
+
return undefined;
|
|
108
|
+
case 3:
|
|
109
|
+
return pipe(
|
|
110
|
+
args[0] as ResolvedTenantAuth,
|
|
111
|
+
args[1] as TenantStore,
|
|
112
|
+
args[2] as boolean,
|
|
113
|
+
args[3] as AnyFlowDef["plane"],
|
|
114
|
+
);
|
|
115
|
+
case 4:
|
|
116
|
+
return fx(args[0] as Parameters<typeof fx>[0]);
|
|
117
|
+
case 5:
|
|
118
|
+
return resume(args[0] as Parameters<typeof resume>[0]);
|
|
119
|
+
default:
|
|
120
|
+
return undefined;
|
|
121
|
+
}
|
|
122
|
+
}
|