okengine 0.16.0 → 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 +14 -12
- package/site/content/docs/elements/gate.mdx +58 -8
- package/site/content/docs/elements/signal.mdx +46 -17
- package/site/content/docs/elements/store.mdx +23 -17
- package/site/content/docs/elements/vault.mdx +23 -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 +72 -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 +15 -9
- package/src/auth/api-key-sql.ts +11 -4
- package/src/auth/api-keys.ts +3 -0
- package/src/auth/config.ts +19 -0
- package/src/auth/index.ts +37 -0
- package/src/auth/plugin.ts +6 -1
- 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/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 +22 -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/app.ts +44 -11
- package/src/console/server/console.test.ts +6 -8
- package/src/console/server/gates.ts +2 -9
- 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-CAGHrA9H.js → access-page-CXVWWMmD.js} +1 -1
- package/src/console/ui-next/dist/assets/{flows-page-B-OUtiAu.js → flows-page-XDpAJO8f.js} +1 -1
- package/src/console/ui-next/dist/assets/{index-CGoZkILK.js → index-BBj2QJCu.js} +3 -3
- package/src/console/ui-next/dist/assets/{observability-page-BDyXalNR.js → observability-page-BFaay44m.js} +1 -1
- package/src/console/ui-next/dist/assets/{store-page-Xh8Kn3rx.js → store-page-CS5-aETQ.js} +1 -1
- package/src/console/ui-next/dist/assets/{tree-expand-toggle-DkOXA12R.js → tree-expand-toggle-BtyhmWb4.js} +2 -1
- package/src/console/ui-next/dist/assets/{units-page-Dpk40kOQ.js → units-page-Ca2Z-E52.js} +1 -1
- package/src/console/ui-next/dist/assets/{vault-page-B1dB9Ft0.js → vault-page-BmOeAFwg.js} +1 -1
- package/src/console/ui-next/dist/index.html +1 -1
- package/src/console/ui-next/src/features/flows/fixture.ts +0 -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/ui-next-seed-manifest-surface.ts +2 -9
- package/src/console/ui-next/ui-next-seed-manifest.ts +0 -1
- 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/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/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/schema-decl.ts +63 -3
- package/src/elements/store/schema-tenant.ts +41 -0
- package/src/elements/store/sql-rls-isolation.test.ts +39 -0
- package/src/elements/store.ts +2 -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 +1 -0
- package/src/kernel/adopt-routes.ts +56 -8
- package/src/kernel/app-tenant.ts +122 -0
- package/src/kernel/app.ts +183 -56
- package/src/kernel/auth-resolve.ts +3 -0
- package/src/kernel/boot-bind/clock.ts +9 -3
- 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.ts +6 -1
- 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 +91 -0
- package/src/kernel/fx.ts +173 -13
- 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-tenant.ts +49 -0
- package/src/kernel/pipeline.test.ts +1 -1
- package/src/kernel/pipeline.ts +37 -2
- 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 +52 -6
- package/src/release/build-lib.ts +13 -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/kernel/app.ts
CHANGED
|
@@ -17,6 +17,7 @@ import {
|
|
|
17
17
|
type RoutesFromNamespace,
|
|
18
18
|
type RuntimeRouteMap,
|
|
19
19
|
} from "./adopt-routes.ts";
|
|
20
|
+
import { liveExposureKey, liveGatesKey, liveMatchKeyFromPath } from "./live-http.ts";
|
|
20
21
|
// `./boot.ts` pulls in every element + driver module (vault, store, signal,
|
|
21
22
|
// clock, gate, channel, ai, runs) — a type-only import here keeps that whole
|
|
22
23
|
// graph out of the cold-start path; `doBoot` below loads it lazily, only
|
|
@@ -33,25 +34,24 @@ import {
|
|
|
33
34
|
type ResolvedGateConfig,
|
|
34
35
|
} from "../elements/gate/config.ts";
|
|
35
36
|
import { requirePackageModule } from "../shared/lazy-src.ts";
|
|
37
|
+
import { lazyRequire } from "./lazy-require.ts";
|
|
38
|
+
import type { DurableResult, RunDurableOptions } from "../elements/clock/durable.ts";
|
|
36
39
|
import type { TemplateCatalog } from "../elements/channel/runtime.ts";
|
|
37
40
|
import { parseAcceptLanguage } from "../elements/channel/locale.ts";
|
|
38
41
|
import { runWithLocale } from "../i18n/locale-context.ts";
|
|
39
|
-
import { runDurable } from "../elements/clock/durable.ts";
|
|
40
42
|
import { isFlow, type AnyFlowDef } from "./flow.ts";
|
|
41
43
|
import { failureFromUnknown, runCompensationPhase } from "./compensate.ts";
|
|
42
44
|
import { withAbortSignal } from "./abort-scope.ts";
|
|
43
45
|
import { fxRetry } from "./concurrency.ts";
|
|
44
|
-
import {
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
type FxPrincipal,
|
|
54
|
-
type NamedRef,
|
|
46
|
+
import type {
|
|
47
|
+
CreateFxOptions,
|
|
48
|
+
Fx,
|
|
49
|
+
FxAuth,
|
|
50
|
+
FxContext,
|
|
51
|
+
FxOperator,
|
|
52
|
+
FxPrincipal,
|
|
53
|
+
JsonStreamResult,
|
|
54
|
+
NamedRef,
|
|
55
55
|
} from "./fx.ts";
|
|
56
56
|
import {
|
|
57
57
|
currentDevSurface,
|
|
@@ -152,6 +152,31 @@ function loadMessages(): typeof import("../i18n/messages.ts") {
|
|
|
152
152
|
return requirePackageModule("i18n/messages", "messages");
|
|
153
153
|
}
|
|
154
154
|
|
|
155
|
+
/**
|
|
156
|
+
* `createFx` / JSON result helpers — loaded on first execute, not on Store-only
|
|
157
|
+
* `oke()` construction (same boundary as `boot.ts`).
|
|
158
|
+
*/
|
|
159
|
+
function loadFx(): {
|
|
160
|
+
createFxContext: (options: CreateFxOptions) => FxContext;
|
|
161
|
+
freezePrincipal: (p: FxPrincipal) => FxPrincipal;
|
|
162
|
+
isJsonStreamResult: (value: unknown) => value is JsonStreamResult;
|
|
163
|
+
resolveName: (ref: NamedRef) => string;
|
|
164
|
+
} {
|
|
165
|
+
return lazyRequire(import.meta.dir, ["fx", "runtime"].join("-"));
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/** App-shell tenancy wiring — loaded only when `gate.auth.tenant` is on. */
|
|
169
|
+
function loadAppTenant(): { w: (kind: number, ...args: unknown[]) => unknown } {
|
|
170
|
+
return lazyRequire(import.meta.dir, ["app", "tenant"].join("-"));
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/** Durable runner — statically imports `createFx`; keep it off Store-only `oke()`. */
|
|
174
|
+
function loadDurable(): {
|
|
175
|
+
runDurable: (opts: RunDurableOptions) => Promise<DurableResult>;
|
|
176
|
+
} {
|
|
177
|
+
return lazyRequire(import.meta.dir, ["clock", "durable"].join("-"));
|
|
178
|
+
}
|
|
179
|
+
|
|
155
180
|
/** Options for {@link oke}. */
|
|
156
181
|
export interface OkeOptions {
|
|
157
182
|
/** Application name (Manifest `app`). */
|
|
@@ -465,6 +490,8 @@ export interface OkeApp<D extends Record<string, unknown> = {}, R extends AppRou
|
|
|
465
490
|
readonly parentId?: string;
|
|
466
491
|
/** Explicit run / WideEvent id (defaults to a new UUID). */
|
|
467
492
|
readonly runId?: string;
|
|
493
|
+
/** Tenant identity for cron / `fx.call` (propagated, unlike auth). */
|
|
494
|
+
readonly tenant?: { readonly id: string | null };
|
|
468
495
|
},
|
|
469
496
|
): Promise<ExecuteResult>;
|
|
470
497
|
/**
|
|
@@ -498,8 +525,12 @@ export interface OkeApp<D extends Record<string, unknown> = {}, R extends AppRou
|
|
|
498
525
|
* Invoke all flows bound to an `every` interval.
|
|
499
526
|
*
|
|
500
527
|
* @param interval - Interval string (e.g. `"1h"`)
|
|
528
|
+
* @param extras - Optional tenant / parent for per-tenant clocks
|
|
501
529
|
*/
|
|
502
|
-
dispatchEvery(
|
|
530
|
+
dispatchEvery(
|
|
531
|
+
interval: string,
|
|
532
|
+
extras?: { readonly tenant?: { readonly id: string | null } },
|
|
533
|
+
): Promise<ExecuteResult[]>;
|
|
503
534
|
/**
|
|
504
535
|
* Invoke all flows bound to a CDC table change.
|
|
505
536
|
*
|
|
@@ -610,6 +641,57 @@ function assertHttpBindingReady(binding: Binding): void {
|
|
|
610
641
|
}
|
|
611
642
|
}
|
|
612
643
|
|
|
644
|
+
/**
|
|
645
|
+
* Register one HTTP binding: unique method+path, unique live exposure key.
|
|
646
|
+
*
|
|
647
|
+
* @param binding - HTTP binding
|
|
648
|
+
* @param seenHttpRoutes - `METHOD path` keys
|
|
649
|
+
* @param seenLiveExposures - `(signal, gates, match)` keys
|
|
650
|
+
* @param smart - Router
|
|
651
|
+
* @param compiled - Compiled route map
|
|
652
|
+
* @param aot - AoT compile flag
|
|
653
|
+
*/
|
|
654
|
+
function registerHttpRoute(
|
|
655
|
+
binding: Binding,
|
|
656
|
+
seenHttpRoutes: Set<string>,
|
|
657
|
+
seenLiveExposures: Map<string, string>,
|
|
658
|
+
smart: { add(method: string, path: string, value: Binding): void },
|
|
659
|
+
compiled: WeakMap<Binding, CompiledRoute>,
|
|
660
|
+
aot: boolean,
|
|
661
|
+
): void {
|
|
662
|
+
const trigger = binding.trigger;
|
|
663
|
+
if (trigger.kind !== "http") return;
|
|
664
|
+
const routeKey = `${trigger.method} ${trigger.path}`;
|
|
665
|
+
if (seenHttpRoutes.has(routeKey)) {
|
|
666
|
+
throwOke("HTTP_ROUTE_DUPLICATE", {
|
|
667
|
+
method: trigger.method,
|
|
668
|
+
path: trigger.path,
|
|
669
|
+
flow: binding.flow.name || "(unnamed)",
|
|
670
|
+
});
|
|
671
|
+
}
|
|
672
|
+
seenHttpRoutes.add(routeKey);
|
|
673
|
+
if (trigger.liveSignal !== undefined) {
|
|
674
|
+
const matchKey = binding.flow.liveCustomMatch
|
|
675
|
+
? `custom:${binding.flow.name}`
|
|
676
|
+
: liveMatchKeyFromPath(trigger.path);
|
|
677
|
+
const exposure = liveExposureKey(
|
|
678
|
+
trigger.liveSignal.name,
|
|
679
|
+
liveGatesKey(trigger.gates),
|
|
680
|
+
matchKey,
|
|
681
|
+
);
|
|
682
|
+
if (seenLiveExposures.has(exposure)) {
|
|
683
|
+
throwOke("LIVE_EXPOSURE_DUPLICATE", {
|
|
684
|
+
signal: trigger.liveSignal.name,
|
|
685
|
+
gates: liveGatesKey(trigger.gates) || "(none)",
|
|
686
|
+
match: matchKey || "(firehose)",
|
|
687
|
+
});
|
|
688
|
+
}
|
|
689
|
+
seenLiveExposures.set(exposure, binding.flow.name);
|
|
690
|
+
}
|
|
691
|
+
smart.add(trigger.method, trigger.path, binding);
|
|
692
|
+
compiled.set(binding, compileHttpBinding(binding, aot));
|
|
693
|
+
}
|
|
694
|
+
|
|
613
695
|
/**
|
|
614
696
|
* Fold `generated.ts` units into `$routes` and `flowsByName`.
|
|
615
697
|
*
|
|
@@ -790,20 +872,11 @@ export function oke(options: OkeOptions): OkeApp {
|
|
|
790
872
|
|
|
791
873
|
const smart = createRouter<Binding>(options.router ?? "default");
|
|
792
874
|
const seenHttpRoutes = new Set<string>();
|
|
875
|
+
const seenLiveExposures = new Map<string, string>();
|
|
793
876
|
for (const b of adopted) {
|
|
794
877
|
if (b.trigger.kind === "http") {
|
|
795
878
|
assertHttpBindingReady(b);
|
|
796
|
-
|
|
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);
|
|
805
|
-
smart.add(b.trigger.method, b.trigger.path, b);
|
|
806
|
-
compiled.set(b, compileHttpBinding(b, aot));
|
|
879
|
+
registerHttpRoute(b, seenHttpRoutes, seenLiveExposures, smart, compiled, aot);
|
|
807
880
|
}
|
|
808
881
|
}
|
|
809
882
|
// Defer SmartRouter selection until first match so `.plug()` can still
|
|
@@ -815,17 +888,7 @@ export function oke(options: OkeOptions): OkeApp {
|
|
|
815
888
|
registerFlow(b.flow);
|
|
816
889
|
if (b.trigger.kind === "http") {
|
|
817
890
|
assertHttpBindingReady(b);
|
|
818
|
-
|
|
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);
|
|
827
|
-
smart.add(b.trigger.method, b.trigger.path, b);
|
|
828
|
-
compiled.set(b, compileHttpBinding(b, aot));
|
|
891
|
+
registerHttpRoute(b, seenHttpRoutes, seenLiveExposures, smart, compiled, aot);
|
|
829
892
|
}
|
|
830
893
|
}
|
|
831
894
|
|
|
@@ -944,12 +1007,15 @@ export function oke(options: OkeOptions): OkeApp {
|
|
|
944
1007
|
}
|
|
945
1008
|
|
|
946
1009
|
async function handleCronFire(name: string): Promise<void> {
|
|
947
|
-
|
|
1010
|
+
const extras = gateConfig.auth?.tenant
|
|
1011
|
+
? (loadAppTenant().w(0, name) as { readonly tenant: { readonly id: string } } | undefined)
|
|
1012
|
+
: undefined;
|
|
1013
|
+
await app.dispatchEvery(name, extras);
|
|
948
1014
|
// Named clocks (e.g. `clock("expire-stale", { every: "1h" })`) reconcile
|
|
949
1015
|
// to a store row whose effective interval may differ from the cron name.
|
|
950
1016
|
const row = await bootResult?.clock?.store.get(name);
|
|
951
1017
|
if (row?.effectiveEvery && row.effectiveEvery !== name) {
|
|
952
|
-
await app.dispatchEvery(row.effectiveEvery);
|
|
1018
|
+
await app.dispatchEvery(row.effectiveEvery, extras);
|
|
953
1019
|
}
|
|
954
1020
|
}
|
|
955
1021
|
|
|
@@ -1004,15 +1070,20 @@ export function oke(options: OkeOptions): OkeApp {
|
|
|
1004
1070
|
return;
|
|
1005
1071
|
}
|
|
1006
1072
|
const { store, instanceId, leaseMs } = activeJournal();
|
|
1073
|
+
const existing = await store.get(runId);
|
|
1007
1074
|
try {
|
|
1008
|
-
await runDurable({
|
|
1075
|
+
await loadDurable().runDurable({
|
|
1009
1076
|
flow: flowDef,
|
|
1010
1077
|
input,
|
|
1011
1078
|
journalStore: store,
|
|
1012
1079
|
runId,
|
|
1013
1080
|
...(hasJournalLease(store) ? { lease: { instanceId, leaseMs } } : {}),
|
|
1014
1081
|
now,
|
|
1015
|
-
fx:
|
|
1082
|
+
fx: {
|
|
1083
|
+
...durableResumeFx(),
|
|
1084
|
+
...(gateConfig.auth?.tenant ? (loadAppTenant().w(5, gateConfig.auth) as object) : {}),
|
|
1085
|
+
...(existing?.tenant ? { tenant: { id: existing.tenant } } : {}),
|
|
1086
|
+
},
|
|
1016
1087
|
});
|
|
1017
1088
|
} catch (err) {
|
|
1018
1089
|
if (isJournalLeaseBusy(err)) return;
|
|
@@ -1176,9 +1247,15 @@ export function oke(options: OkeOptions): OkeApp {
|
|
|
1176
1247
|
onCronFire: overrides?.onCronFire ?? handleCronFire,
|
|
1177
1248
|
onSignal: overrides?.onSignal ?? handleSignalFire,
|
|
1178
1249
|
onDurableResume: overrides?.onDurableResume ?? handleDurableResume,
|
|
1250
|
+
...(gateConfig.auth?.tenantStore
|
|
1251
|
+
? (loadAppTenant().w(1, gateConfig.auth.tenantStore) as object)
|
|
1252
|
+
: {}),
|
|
1179
1253
|
};
|
|
1180
1254
|
const result = await bootApplication(merged);
|
|
1181
1255
|
bootResult = result;
|
|
1256
|
+
if (gateConfig.auth?.tenantStore && result.clock) {
|
|
1257
|
+
loadAppTenant().w(2, gateConfig.auth.tenantStore, result.clock.store, merged.clocks ?? []);
|
|
1258
|
+
}
|
|
1182
1259
|
if (gateConfig.auth?.apiKeyStore && result.store) {
|
|
1183
1260
|
const { bindHostApiKeySqlFromStore } = await import("../auth/api-key-sql.ts");
|
|
1184
1261
|
await bindHostApiKeySqlFromStore(result.store, gateConfig.auth.apiKeyStore);
|
|
@@ -1266,6 +1343,8 @@ export function oke(options: OkeOptions): OkeApp {
|
|
|
1266
1343
|
readonly parentId?: string;
|
|
1267
1344
|
/** Explicit run / WideEvent id (defaults to a new UUID). */
|
|
1268
1345
|
readonly runId?: string;
|
|
1346
|
+
/** Tenant identity for cron / `fx.call` (propagated, unlike auth). */
|
|
1347
|
+
readonly tenant?: { readonly id: string | null };
|
|
1269
1348
|
},
|
|
1270
1349
|
): Promise<ExecuteResult> {
|
|
1271
1350
|
registerFlow(flowDef);
|
|
@@ -1290,8 +1369,8 @@ export function oke(options: OkeOptions): OkeApp {
|
|
|
1290
1369
|
resolvedLocale,
|
|
1291
1370
|
defaultLocale,
|
|
1292
1371
|
});
|
|
1293
|
-
const
|
|
1294
|
-
return
|
|
1372
|
+
const abort = extras?.request?.signal;
|
|
1373
|
+
return abort ? withAbortSignal(abort, run) : run();
|
|
1295
1374
|
});
|
|
1296
1375
|
}
|
|
1297
1376
|
|
|
@@ -1314,12 +1393,15 @@ export function oke(options: OkeOptions): OkeApp {
|
|
|
1314
1393
|
readonly locale?: string;
|
|
1315
1394
|
readonly parentId?: string;
|
|
1316
1395
|
readonly runId?: string;
|
|
1396
|
+
readonly tenant?: { readonly id: string | null };
|
|
1317
1397
|
};
|
|
1318
1398
|
readonly resolvedLocale: string;
|
|
1319
1399
|
readonly defaultLocale: string;
|
|
1320
1400
|
}): Promise<ExecuteResult> {
|
|
1321
1401
|
const { flowDef, input, trigger, extras, resolvedLocale, defaultLocale } = args;
|
|
1322
1402
|
const booted = await ensureBoot();
|
|
1403
|
+
const tenantEnabled = gateConfig.auth?.tenant !== undefined;
|
|
1404
|
+
const flowTenantScoped = flowDef.tenantScoped ?? tenantEnabled;
|
|
1323
1405
|
|
|
1324
1406
|
const unitBag = flowDef.unit !== undefined ? unitHooks.get(flowDef.unit) : undefined;
|
|
1325
1407
|
// app (hooks + plugs) → unit (hooks + plugs) → flow (hooks + plugs)
|
|
@@ -1375,6 +1457,7 @@ export function oke(options: OkeOptions): OkeApp {
|
|
|
1375
1457
|
apiKeyId: options.fx?.auth?.apiKeyId,
|
|
1376
1458
|
},
|
|
1377
1459
|
operator: { id: options.fx?.operator?.id ?? null },
|
|
1460
|
+
tenant: { id: extras?.tenant?.id ?? options.fx?.tenant?.id ?? null },
|
|
1378
1461
|
};
|
|
1379
1462
|
|
|
1380
1463
|
// Principal injection: test harness (`env: "test"`) OR console-trusted
|
|
@@ -1391,6 +1474,8 @@ export function oke(options: OkeOptions): OkeApp {
|
|
|
1391
1474
|
const cookieOpts = gateConfig.auth?.cookies;
|
|
1392
1475
|
const cookieToken = wiredAuth?.tokenFromCookieHeader;
|
|
1393
1476
|
const apiKeyStore = gateConfig.auth?.apiKeyStore;
|
|
1477
|
+
const tenantEnabled = gateConfig.auth?.tenant !== undefined;
|
|
1478
|
+
const flowTenantScoped = flowDef.tenantScoped ?? tenantEnabled;
|
|
1394
1479
|
const elementHooks = createElementPipelineHooks({
|
|
1395
1480
|
gates: booted.gate,
|
|
1396
1481
|
principals,
|
|
@@ -1415,13 +1500,29 @@ export function oke(options: OkeOptions): OkeApp {
|
|
|
1415
1500
|
return cookieToken(request.headers.get("cookie"), cookieOpts);
|
|
1416
1501
|
}
|
|
1417
1502
|
: undefined,
|
|
1503
|
+
...(gateConfig.auth?.tenant && gateConfig.auth.tenantStore
|
|
1504
|
+
? (loadAppTenant().w(
|
|
1505
|
+
3,
|
|
1506
|
+
gateConfig.auth.tenant,
|
|
1507
|
+
gateConfig.auth.tenantStore,
|
|
1508
|
+
flowTenantScoped,
|
|
1509
|
+
flowDef.plane,
|
|
1510
|
+
) as object)
|
|
1511
|
+
: {}),
|
|
1418
1512
|
});
|
|
1419
1513
|
|
|
1420
1514
|
// Element hooks run first in the app-level onAuth/beforeHandle chain —
|
|
1421
1515
|
// principal resolution and gate checks precede user-registered hooks.
|
|
1422
1516
|
hooks = mergeHooks(
|
|
1423
1517
|
{
|
|
1424
|
-
onAuth: [
|
|
1518
|
+
onAuth: [
|
|
1519
|
+
elementHooks.onAuth,
|
|
1520
|
+
async () => {
|
|
1521
|
+
if (journalSession && principals?.tenant.id) {
|
|
1522
|
+
await journalSession.stampTenant(principals.tenant.id);
|
|
1523
|
+
}
|
|
1524
|
+
},
|
|
1525
|
+
],
|
|
1425
1526
|
beforeHandle: [elementHooks.beforeHandle],
|
|
1426
1527
|
},
|
|
1427
1528
|
composedHooks,
|
|
@@ -1446,12 +1547,13 @@ export function oke(options: OkeOptions): OkeApp {
|
|
|
1446
1547
|
}
|
|
1447
1548
|
|
|
1448
1549
|
const effects = flowDef.effects ?? capability?.declared;
|
|
1449
|
-
const { fx, ledger } = createFxContext({
|
|
1550
|
+
const { fx, ledger } = loadFx().createFxContext({
|
|
1450
1551
|
...options.fx,
|
|
1451
1552
|
flow: flowDef.name,
|
|
1452
1553
|
effects,
|
|
1453
1554
|
runTelemetry: telemetry,
|
|
1454
1555
|
runId,
|
|
1556
|
+
lastEventId: extras?.request?.headers.get("last-event-id") || undefined,
|
|
1455
1557
|
now,
|
|
1456
1558
|
i18n: {
|
|
1457
1559
|
locale: resolvedLocale,
|
|
@@ -1459,8 +1561,28 @@ export function oke(options: OkeOptions): OkeApp {
|
|
|
1459
1561
|
catalogs: loadMessages().getMessageCatalogs(),
|
|
1460
1562
|
...options.fx?.i18n,
|
|
1461
1563
|
},
|
|
1462
|
-
...(principals
|
|
1564
|
+
...(principals
|
|
1565
|
+
? {
|
|
1566
|
+
auth: principals.auth,
|
|
1567
|
+
operator: principals.operator,
|
|
1568
|
+
tenant: principals.tenant,
|
|
1569
|
+
}
|
|
1570
|
+
: extras?.tenant
|
|
1571
|
+
? { tenant: { id: extras.tenant.id } }
|
|
1572
|
+
: {}),
|
|
1463
1573
|
apiKeyStore: gateConfig.auth?.apiKeyStore,
|
|
1574
|
+
sessions: authBinding?.sessions,
|
|
1575
|
+
manifest: options.manifest,
|
|
1576
|
+
...(tenantEnabled || extras?.tenant
|
|
1577
|
+
? (loadAppTenant().w(4, {
|
|
1578
|
+
enabled: tenantEnabled,
|
|
1579
|
+
store: gateConfig.auth?.tenantStore,
|
|
1580
|
+
scoped: flowTenantScoped,
|
|
1581
|
+
plane: flowDef.plane,
|
|
1582
|
+
tenant: principals?.tenant ?? (extras?.tenant ? { id: extras.tenant.id } : undefined),
|
|
1583
|
+
bind: authBinding,
|
|
1584
|
+
}) as object)
|
|
1585
|
+
: {}),
|
|
1464
1586
|
...(extras?.originPrincipal ? { principal: extras.originPrincipal } : {}),
|
|
1465
1587
|
...(extras?.trustedInvoke === true && extras.revealPii === true ? { revealPii: true } : {}),
|
|
1466
1588
|
rlsGateNames: gateNamesOf(trigger),
|
|
@@ -1488,9 +1610,10 @@ export function oke(options: OkeOptions): OkeApp {
|
|
|
1488
1610
|
callInput,
|
|
1489
1611
|
{ kind: "internal" } satisfies InternalTrigger,
|
|
1490
1612
|
{
|
|
1491
|
-
originPrincipal: freezePrincipal(fx.principal),
|
|
1613
|
+
originPrincipal: loadFx().freezePrincipal(fx.principal),
|
|
1492
1614
|
locale: resolvedLocale,
|
|
1493
1615
|
parentId: runId,
|
|
1616
|
+
...(tenantEnabled ? { tenant: { id: fx.tenant.id } } : {}),
|
|
1494
1617
|
...(extras?.trustedInvoke === true && extras.revealPii === true
|
|
1495
1618
|
? { trustedInvoke: true, revealPii: true }
|
|
1496
1619
|
: {}),
|
|
@@ -1592,7 +1715,7 @@ export function oke(options: OkeOptions): OkeApp {
|
|
|
1592
1715
|
durable: flowDef.durable,
|
|
1593
1716
|
effects: putEffects,
|
|
1594
1717
|
});
|
|
1595
|
-
if (storeAfter && output !== undefined && !isJsonStreamResult(output)) {
|
|
1718
|
+
if (storeAfter && output !== undefined && !loadFx().isJsonStreamResult(output)) {
|
|
1596
1719
|
const ttlMs =
|
|
1597
1720
|
typeof flowDef.cache === "string" ? cache.parseTtlMs(flowDef.cache) : undefined;
|
|
1598
1721
|
storeRt.putTier1(
|
|
@@ -1641,7 +1764,7 @@ export function oke(options: OkeOptions): OkeApp {
|
|
|
1641
1764
|
|
|
1642
1765
|
const endedAt = now();
|
|
1643
1766
|
const durationMs = resolveDurationMs(endedAt - startedAt, performance.now() - startedHr);
|
|
1644
|
-
const streamOut = isJsonStreamResult(result.output) ? result.output : undefined;
|
|
1767
|
+
const streamOut = loadFx().isJsonStreamResult(result.output) ? result.output : undefined;
|
|
1645
1768
|
|
|
1646
1769
|
const finalizeRun = async (): Promise<void> => {
|
|
1647
1770
|
// Stream rows land in Traces only after close. Measure wall time then
|
|
@@ -1710,7 +1833,11 @@ export function oke(options: OkeOptions): OkeApp {
|
|
|
1710
1833
|
}
|
|
1711
1834
|
};
|
|
1712
1835
|
|
|
1713
|
-
if (
|
|
1836
|
+
if (
|
|
1837
|
+
streamOut &&
|
|
1838
|
+
!isTerminalFailure(result) &&
|
|
1839
|
+
result.response?.headers.get("content-type")?.includes("text/event-stream")
|
|
1840
|
+
) {
|
|
1714
1841
|
streamOut.finalize = finalizeRun;
|
|
1715
1842
|
} else {
|
|
1716
1843
|
await finalizeRun();
|
|
@@ -1817,7 +1944,7 @@ export function oke(options: OkeOptions): OkeApp {
|
|
|
1817
1944
|
for (const [runId, sleeper] of [...sleepingRuns.entries()]) {
|
|
1818
1945
|
if (t < sleeper.wakeAt) continue;
|
|
1819
1946
|
sleepingRuns.delete(runId);
|
|
1820
|
-
const result = await runDurable({
|
|
1947
|
+
const result = await loadDurable().runDurable({
|
|
1821
1948
|
flow: sleeper.flow,
|
|
1822
1949
|
input: sleeper.input,
|
|
1823
1950
|
journalStore: store,
|
|
@@ -1873,7 +2000,7 @@ export function oke(options: OkeOptions): OkeApp {
|
|
|
1873
2000
|
},
|
|
1874
2001
|
flow(ref) {
|
|
1875
2002
|
if (isFlow(ref)) return flowsByName.get(ref.name) ?? ref;
|
|
1876
|
-
return flowsByName.get(resolveName(ref as NamedRef));
|
|
2003
|
+
return flowsByName.get(loadFx().resolveName(ref as NamedRef));
|
|
1877
2004
|
},
|
|
1878
2005
|
adopt(...args: readonly unknown[]) {
|
|
1879
2006
|
const flows = accumulateAdoptArgs(args, routes);
|
|
@@ -1969,7 +2096,7 @@ export function oke(options: OkeOptions): OkeApp {
|
|
|
1969
2096
|
);
|
|
1970
2097
|
runLabel = internalResult.runId;
|
|
1971
2098
|
cacheLabel = internalResult.cache;
|
|
1972
|
-
return respond(encodeExecuteResult(internalResult));
|
|
2099
|
+
return respond(await encodeExecuteResult(internalResult));
|
|
1973
2100
|
}
|
|
1974
2101
|
}
|
|
1975
2102
|
|
|
@@ -2048,10 +2175,10 @@ export function oke(options: OkeOptions): OkeApp {
|
|
|
2048
2175
|
runLabel = result.runId;
|
|
2049
2176
|
cacheLabel = result.cache;
|
|
2050
2177
|
|
|
2051
|
-
return respond(encodeExecuteResult(result));
|
|
2178
|
+
return respond(await encodeExecuteResult(result));
|
|
2052
2179
|
},
|
|
2053
2180
|
async dispatchSignal(signal, payload, meta) {
|
|
2054
|
-
const name = resolveName(signal);
|
|
2181
|
+
const name = loadFx().resolveName(signal);
|
|
2055
2182
|
const results: ExecuteResult[] = [];
|
|
2056
2183
|
for (const b of adopted) {
|
|
2057
2184
|
if (b.trigger.kind === "signal" && b.trigger.name === name) {
|
|
@@ -2064,11 +2191,11 @@ export function oke(options: OkeOptions): OkeApp {
|
|
|
2064
2191
|
}
|
|
2065
2192
|
return results;
|
|
2066
2193
|
},
|
|
2067
|
-
async dispatchEvery(interval) {
|
|
2194
|
+
async dispatchEvery(interval, extras) {
|
|
2068
2195
|
const results: ExecuteResult[] = [];
|
|
2069
2196
|
for (const b of adopted) {
|
|
2070
2197
|
if (b.trigger.kind === "every" && b.trigger.interval === interval) {
|
|
2071
|
-
results.push(await execute(b.flow, undefined, b.trigger as EveryTrigger));
|
|
2198
|
+
results.push(await execute(b.flow, undefined, b.trigger as EveryTrigger, extras));
|
|
2072
2199
|
}
|
|
2073
2200
|
}
|
|
2074
2201
|
return results;
|
|
@@ -2089,7 +2216,7 @@ export function oke(options: OkeOptions): OkeApp {
|
|
|
2089
2216
|
async call(ref, input) {
|
|
2090
2217
|
const flowDef = isFlow(ref)
|
|
2091
2218
|
? (flowsByName.get(ref.name) ?? ref)
|
|
2092
|
-
: flowsByName.get(resolveName(ref as NamedRef));
|
|
2219
|
+
: flowsByName.get(loadFx().resolveName(ref as NamedRef));
|
|
2093
2220
|
if (!flowDef) {
|
|
2094
2221
|
return undefined;
|
|
2095
2222
|
}
|
|
@@ -84,6 +84,7 @@ export function claimsToPrincipal(claims: AccessClaims): ResolvedPrincipal {
|
|
|
84
84
|
userId: claims.sub,
|
|
85
85
|
scopes: claims.scopes,
|
|
86
86
|
verified: true,
|
|
87
|
+
...(claims.tid !== undefined ? { tenantId: claims.tid } : {}),
|
|
87
88
|
};
|
|
88
89
|
}
|
|
89
90
|
|
|
@@ -104,6 +105,7 @@ export function apiKeyRowToPrincipal(row: ApiKeyRow): ResolvedPrincipal {
|
|
|
104
105
|
scopes: row.scopes,
|
|
105
106
|
verified: true,
|
|
106
107
|
apiKeyId: row.id,
|
|
108
|
+
...(row.tenantId !== undefined ? { tenantId: row.tenantId } : {}),
|
|
107
109
|
};
|
|
108
110
|
}
|
|
109
111
|
return {
|
|
@@ -112,6 +114,7 @@ export function apiKeyRowToPrincipal(row: ApiKeyRow): ResolvedPrincipal {
|
|
|
112
114
|
scopes: row.scopes,
|
|
113
115
|
verified: true,
|
|
114
116
|
apiKeyId: row.id,
|
|
117
|
+
...(row.tenantId !== undefined ? { tenantId: row.tenantId } : {}),
|
|
115
118
|
};
|
|
116
119
|
}
|
|
117
120
|
|
|
@@ -60,12 +60,16 @@ export async function bindClock(
|
|
|
60
60
|
if (prebuilt) {
|
|
61
61
|
clock = prebuilt;
|
|
62
62
|
} else if (clockDriver === "frozen" || env === "test") {
|
|
63
|
-
clock = createTestClockRuntime(now(), {
|
|
63
|
+
clock = createTestClockRuntime(now(), {
|
|
64
|
+
instanceId: options.instanceId,
|
|
65
|
+
...(options.tenantIds ? { tenantIds: options.tenantIds } : {}),
|
|
66
|
+
});
|
|
64
67
|
} else if (clockDriver === "memory") {
|
|
65
68
|
clock = createClockRuntime({
|
|
66
69
|
instanceId: options.instanceId,
|
|
67
70
|
now,
|
|
68
71
|
store: createMemoryCronStore(),
|
|
72
|
+
...(options.tenantIds ? { tenantIds: options.tenantIds } : {}),
|
|
69
73
|
});
|
|
70
74
|
} else if (clockDriver === "file") {
|
|
71
75
|
const path = resolve(process.cwd(), DEFAULT_FILE_CRON_PATH);
|
|
@@ -74,6 +78,7 @@ export async function bindClock(
|
|
|
74
78
|
instanceId: options.instanceId,
|
|
75
79
|
now,
|
|
76
80
|
store: createFileCronStore(path),
|
|
81
|
+
...(options.tenantIds ? { tenantIds: options.tenantIds } : {}),
|
|
77
82
|
});
|
|
78
83
|
} else if (clockDriver === "postgres") {
|
|
79
84
|
const url = process.env.DATABASE_URL ?? process.env.OKE_STORE_SQL_URL ?? undefined;
|
|
@@ -89,6 +94,7 @@ export async function bindClock(
|
|
|
89
94
|
instanceId: options.instanceId,
|
|
90
95
|
now,
|
|
91
96
|
store,
|
|
97
|
+
...(options.tenantIds ? { tenantIds: options.tenantIds } : {}),
|
|
92
98
|
});
|
|
93
99
|
} else {
|
|
94
100
|
throw new Error(
|
|
@@ -118,8 +124,8 @@ export async function bindClock(
|
|
|
118
124
|
if (options.onCronFire) {
|
|
119
125
|
const fire = options.onCronFire;
|
|
120
126
|
for (const name of clockDecls.keys()) {
|
|
121
|
-
clock.onCron(name, async () => {
|
|
122
|
-
await fire(name);
|
|
127
|
+
clock.onCron(name, async (row) => {
|
|
128
|
+
await fire(row.name);
|
|
123
129
|
});
|
|
124
130
|
}
|
|
125
131
|
}
|
package/src/kernel/boot.ts
CHANGED
|
@@ -152,6 +152,8 @@ export interface BootOptions {
|
|
|
152
152
|
* @param name - Cron name or every-interval
|
|
153
153
|
*/
|
|
154
154
|
readonly onCronFire?: (name: string) => void | Promise<void>;
|
|
155
|
+
/** Tenant ids for per-tenant clock expansion at reconcile. */
|
|
156
|
+
readonly tenantIds?: () => readonly string[] | Promise<readonly string[]>;
|
|
155
157
|
/**
|
|
156
158
|
* Dispatch a signal message to bound flows.
|
|
157
159
|
*
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Clock per-tenant row hooks — lazy chunk.
|
|
3
|
+
*
|
|
4
|
+
* A static import from `app.ts` would pin Clock reconcile on every `oke()`
|
|
5
|
+
* graph, including Store-only apps that never enable `gate.auth.tenant`.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
export { orphanPerTenantCronRows, putPerTenantCronRows } from "../elements/clock/reconcile.ts";
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OKE1014 — kept off the Store-only `oke()` graph.
|
|
3
|
+
*
|
|
4
|
+
* Signal drivers throw this when a Last-Event-ID is missing from the tape.
|
|
5
|
+
* Loaded via computed `import.meta.require` from {@link lookupOkeError}.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { OkeErrorDefinition } from "./errors.ts";
|
|
9
|
+
|
|
10
|
+
/** Last-Event-ID is missing from the retained live tape. */
|
|
11
|
+
export const LIVE_RESUME_GAP: OkeErrorDefinition = {
|
|
12
|
+
code: 1014,
|
|
13
|
+
cause: 'Cursor "{afterId}" missing on "{signal}".',
|
|
14
|
+
fix: "Reconnect without Last-Event-ID.",
|
|
15
|
+
};
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OKE1015–1017 — kept off the Store-only `oke()` graph.
|
|
3
|
+
*
|
|
4
|
+
* Tenant KV/vault and membership failures. Loaded via computed
|
|
5
|
+
* `import.meta.require` from {@link throwOke} / {@link lookupOkeError}.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { OkeErrorDefinition } from "./errors.ts";
|
|
9
|
+
|
|
10
|
+
/** Tenant-scoped op ran with no resolved `fx.tenant.id`. */
|
|
11
|
+
export const TENANT_REQUIRED: OkeErrorDefinition = {
|
|
12
|
+
code: 1015,
|
|
13
|
+
cause: "This operation needs a tenant, but none is resolved for this request.",
|
|
14
|
+
fix: "Call fx.auth.switchTenant(id), send a signed tid claim, or pass the tenant header.",
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
/** Client-supplied tenant id is not a real membership. */
|
|
18
|
+
export const TENANT_NOT_MEMBER: OkeErrorDefinition = {
|
|
19
|
+
code: 1016,
|
|
20
|
+
cause: 'The caller is not a member of tenant "{tenant}".',
|
|
21
|
+
fix: "Pick a tenant from fx.auth.listTenants() or add the user as a member.",
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
/** Tenant role mapped an invented or operator-plane scope. */
|
|
25
|
+
export const TENANT_UNKNOWN_SCOPE: OkeErrorDefinition = {
|
|
26
|
+
code: 1017,
|
|
27
|
+
cause: 'Tenant role scope "{scope}" is not a declared application scope.',
|
|
28
|
+
fix: "Use a name from this app's Manifest catalog (never console:*).",
|
|
29
|
+
};
|