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
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
|
|
@@ -24,6 +25,7 @@ import {
|
|
|
24
25
|
import type { BootOptions, BootResult, ElementRuntimes } from "./boot.ts";
|
|
25
26
|
import type { CapabilityToken } from "./capability.ts";
|
|
26
27
|
import type { AppAuthBinding } from "./auth-resolve.ts";
|
|
28
|
+
import type { ApiKeyStore } from "../auth/api-keys.ts";
|
|
27
29
|
import type { AuthHttpMaterialization } from "../auth/bindings.ts";
|
|
28
30
|
import type { WiredGateAuth } from "./app-auth.ts";
|
|
29
31
|
import {
|
|
@@ -32,25 +34,24 @@ import {
|
|
|
32
34
|
type ResolvedGateConfig,
|
|
33
35
|
} from "../elements/gate/config.ts";
|
|
34
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";
|
|
35
39
|
import type { TemplateCatalog } from "../elements/channel/runtime.ts";
|
|
36
40
|
import { parseAcceptLanguage } from "../elements/channel/locale.ts";
|
|
37
41
|
import { runWithLocale } from "../i18n/locale-context.ts";
|
|
38
|
-
import { runDurable } from "../elements/clock/durable.ts";
|
|
39
42
|
import { isFlow, type AnyFlowDef } from "./flow.ts";
|
|
40
43
|
import { failureFromUnknown, runCompensationPhase } from "./compensate.ts";
|
|
41
44
|
import { withAbortSignal } from "./abort-scope.ts";
|
|
42
45
|
import { fxRetry } from "./concurrency.ts";
|
|
43
|
-
import {
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
type FxPrincipal,
|
|
53
|
-
type NamedRef,
|
|
46
|
+
import type {
|
|
47
|
+
CreateFxOptions,
|
|
48
|
+
Fx,
|
|
49
|
+
FxAuth,
|
|
50
|
+
FxContext,
|
|
51
|
+
FxOperator,
|
|
52
|
+
FxPrincipal,
|
|
53
|
+
JsonStreamResult,
|
|
54
|
+
NamedRef,
|
|
54
55
|
} from "./fx.ts";
|
|
55
56
|
import {
|
|
56
57
|
currentDevSurface,
|
|
@@ -151,6 +152,31 @@ function loadMessages(): typeof import("../i18n/messages.ts") {
|
|
|
151
152
|
return requirePackageModule("i18n/messages", "messages");
|
|
152
153
|
}
|
|
153
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
|
+
|
|
154
180
|
/** Options for {@link oke}. */
|
|
155
181
|
export interface OkeOptions {
|
|
156
182
|
/** Application name (Manifest `app`). */
|
|
@@ -371,6 +397,8 @@ export interface OkeApp<D extends Record<string, unknown> = {}, R extends AppRou
|
|
|
371
397
|
* Builtin auth binding after boot (session store + secret), when configured.
|
|
372
398
|
*/
|
|
373
399
|
readonly authBinding: AppAuthBinding | undefined;
|
|
400
|
+
/** Shared API key store when `gate.auth` is enabled. */
|
|
401
|
+
readonly apiKeys: ApiKeyStore | undefined;
|
|
374
402
|
/**
|
|
375
403
|
* Attach a plugin app-wide. Scope is the attachment point.
|
|
376
404
|
* Types accumulate — decorations are visible on downstream handlers.
|
|
@@ -462,6 +490,8 @@ export interface OkeApp<D extends Record<string, unknown> = {}, R extends AppRou
|
|
|
462
490
|
readonly parentId?: string;
|
|
463
491
|
/** Explicit run / WideEvent id (defaults to a new UUID). */
|
|
464
492
|
readonly runId?: string;
|
|
493
|
+
/** Tenant identity for cron / `fx.call` (propagated, unlike auth). */
|
|
494
|
+
readonly tenant?: { readonly id: string | null };
|
|
465
495
|
},
|
|
466
496
|
): Promise<ExecuteResult>;
|
|
467
497
|
/**
|
|
@@ -495,8 +525,12 @@ export interface OkeApp<D extends Record<string, unknown> = {}, R extends AppRou
|
|
|
495
525
|
* Invoke all flows bound to an `every` interval.
|
|
496
526
|
*
|
|
497
527
|
* @param interval - Interval string (e.g. `"1h"`)
|
|
528
|
+
* @param extras - Optional tenant / parent for per-tenant clocks
|
|
498
529
|
*/
|
|
499
|
-
dispatchEvery(
|
|
530
|
+
dispatchEvery(
|
|
531
|
+
interval: string,
|
|
532
|
+
extras?: { readonly tenant?: { readonly id: string | null } },
|
|
533
|
+
): Promise<ExecuteResult[]>;
|
|
500
534
|
/**
|
|
501
535
|
* Invoke all flows bound to a CDC table change.
|
|
502
536
|
*
|
|
@@ -607,6 +641,57 @@ function assertHttpBindingReady(binding: Binding): void {
|
|
|
607
641
|
}
|
|
608
642
|
}
|
|
609
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
|
+
|
|
610
695
|
/**
|
|
611
696
|
* Fold `generated.ts` units into `$routes` and `flowsByName`.
|
|
612
697
|
*
|
|
@@ -787,20 +872,11 @@ export function oke(options: OkeOptions): OkeApp {
|
|
|
787
872
|
|
|
788
873
|
const smart = createRouter<Binding>(options.router ?? "default");
|
|
789
874
|
const seenHttpRoutes = new Set<string>();
|
|
875
|
+
const seenLiveExposures = new Map<string, string>();
|
|
790
876
|
for (const b of adopted) {
|
|
791
877
|
if (b.trigger.kind === "http") {
|
|
792
878
|
assertHttpBindingReady(b);
|
|
793
|
-
|
|
794
|
-
if (seenHttpRoutes.has(key)) {
|
|
795
|
-
throwOke("HTTP_ROUTE_DUPLICATE", {
|
|
796
|
-
method: b.trigger.method,
|
|
797
|
-
path: b.trigger.path,
|
|
798
|
-
flow: b.flow.name || "(unnamed)",
|
|
799
|
-
});
|
|
800
|
-
}
|
|
801
|
-
seenHttpRoutes.add(key);
|
|
802
|
-
smart.add(b.trigger.method, b.trigger.path, b);
|
|
803
|
-
compiled.set(b, compileHttpBinding(b, aot));
|
|
879
|
+
registerHttpRoute(b, seenHttpRoutes, seenLiveExposures, smart, compiled, aot);
|
|
804
880
|
}
|
|
805
881
|
}
|
|
806
882
|
// Defer SmartRouter selection until first match so `.plug()` can still
|
|
@@ -812,17 +888,7 @@ export function oke(options: OkeOptions): OkeApp {
|
|
|
812
888
|
registerFlow(b.flow);
|
|
813
889
|
if (b.trigger.kind === "http") {
|
|
814
890
|
assertHttpBindingReady(b);
|
|
815
|
-
|
|
816
|
-
if (seenHttpRoutes.has(key)) {
|
|
817
|
-
throwOke("HTTP_ROUTE_DUPLICATE", {
|
|
818
|
-
method: b.trigger.method,
|
|
819
|
-
path: b.trigger.path,
|
|
820
|
-
flow: b.flow.name || "(unnamed)",
|
|
821
|
-
});
|
|
822
|
-
}
|
|
823
|
-
seenHttpRoutes.add(key);
|
|
824
|
-
smart.add(b.trigger.method, b.trigger.path, b);
|
|
825
|
-
compiled.set(b, compileHttpBinding(b, aot));
|
|
891
|
+
registerHttpRoute(b, seenHttpRoutes, seenLiveExposures, smart, compiled, aot);
|
|
826
892
|
}
|
|
827
893
|
}
|
|
828
894
|
|
|
@@ -941,12 +1007,15 @@ export function oke(options: OkeOptions): OkeApp {
|
|
|
941
1007
|
}
|
|
942
1008
|
|
|
943
1009
|
async function handleCronFire(name: string): Promise<void> {
|
|
944
|
-
|
|
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);
|
|
945
1014
|
// Named clocks (e.g. `clock("expire-stale", { every: "1h" })`) reconcile
|
|
946
1015
|
// to a store row whose effective interval may differ from the cron name.
|
|
947
1016
|
const row = await bootResult?.clock?.store.get(name);
|
|
948
1017
|
if (row?.effectiveEvery && row.effectiveEvery !== name) {
|
|
949
|
-
await app.dispatchEvery(row.effectiveEvery);
|
|
1018
|
+
await app.dispatchEvery(row.effectiveEvery, extras);
|
|
950
1019
|
}
|
|
951
1020
|
}
|
|
952
1021
|
|
|
@@ -1001,15 +1070,20 @@ export function oke(options: OkeOptions): OkeApp {
|
|
|
1001
1070
|
return;
|
|
1002
1071
|
}
|
|
1003
1072
|
const { store, instanceId, leaseMs } = activeJournal();
|
|
1073
|
+
const existing = await store.get(runId);
|
|
1004
1074
|
try {
|
|
1005
|
-
await runDurable({
|
|
1075
|
+
await loadDurable().runDurable({
|
|
1006
1076
|
flow: flowDef,
|
|
1007
1077
|
input,
|
|
1008
1078
|
journalStore: store,
|
|
1009
1079
|
runId,
|
|
1010
1080
|
...(hasJournalLease(store) ? { lease: { instanceId, leaseMs } } : {}),
|
|
1011
1081
|
now,
|
|
1012
|
-
fx:
|
|
1082
|
+
fx: {
|
|
1083
|
+
...durableResumeFx(),
|
|
1084
|
+
...(gateConfig.auth?.tenant ? (loadAppTenant().w(5, gateConfig.auth) as object) : {}),
|
|
1085
|
+
...(existing?.tenant ? { tenant: { id: existing.tenant } } : {}),
|
|
1086
|
+
},
|
|
1013
1087
|
});
|
|
1014
1088
|
} catch (err) {
|
|
1015
1089
|
if (isJournalLeaseBusy(err)) return;
|
|
@@ -1173,9 +1247,19 @@ export function oke(options: OkeOptions): OkeApp {
|
|
|
1173
1247
|
onCronFire: overrides?.onCronFire ?? handleCronFire,
|
|
1174
1248
|
onSignal: overrides?.onSignal ?? handleSignalFire,
|
|
1175
1249
|
onDurableResume: overrides?.onDurableResume ?? handleDurableResume,
|
|
1250
|
+
...(gateConfig.auth?.tenantStore
|
|
1251
|
+
? (loadAppTenant().w(1, gateConfig.auth.tenantStore) as object)
|
|
1252
|
+
: {}),
|
|
1176
1253
|
};
|
|
1177
1254
|
const result = await bootApplication(merged);
|
|
1178
1255
|
bootResult = result;
|
|
1256
|
+
if (gateConfig.auth?.tenantStore && result.clock) {
|
|
1257
|
+
loadAppTenant().w(2, gateConfig.auth.tenantStore, result.clock.store, merged.clocks ?? []);
|
|
1258
|
+
}
|
|
1259
|
+
if (gateConfig.auth?.apiKeyStore && result.store) {
|
|
1260
|
+
const { bindHostApiKeySqlFromStore } = await import("../auth/api-key-sql.ts");
|
|
1261
|
+
await bindHostApiKeySqlFromStore(result.store, gateConfig.auth.apiKeyStore);
|
|
1262
|
+
}
|
|
1179
1263
|
// otp() provider / app mode capability — fail loud at boot, never silent downgrade.
|
|
1180
1264
|
if (result.channel) {
|
|
1181
1265
|
const { assertOtpPluginCapability } = await import("../auth/otp-capability.ts");
|
|
@@ -1259,6 +1343,8 @@ export function oke(options: OkeOptions): OkeApp {
|
|
|
1259
1343
|
readonly parentId?: string;
|
|
1260
1344
|
/** Explicit run / WideEvent id (defaults to a new UUID). */
|
|
1261
1345
|
readonly runId?: string;
|
|
1346
|
+
/** Tenant identity for cron / `fx.call` (propagated, unlike auth). */
|
|
1347
|
+
readonly tenant?: { readonly id: string | null };
|
|
1262
1348
|
},
|
|
1263
1349
|
): Promise<ExecuteResult> {
|
|
1264
1350
|
registerFlow(flowDef);
|
|
@@ -1283,8 +1369,8 @@ export function oke(options: OkeOptions): OkeApp {
|
|
|
1283
1369
|
resolvedLocale,
|
|
1284
1370
|
defaultLocale,
|
|
1285
1371
|
});
|
|
1286
|
-
const
|
|
1287
|
-
return
|
|
1372
|
+
const abort = extras?.request?.signal;
|
|
1373
|
+
return abort ? withAbortSignal(abort, run) : run();
|
|
1288
1374
|
});
|
|
1289
1375
|
}
|
|
1290
1376
|
|
|
@@ -1307,12 +1393,15 @@ export function oke(options: OkeOptions): OkeApp {
|
|
|
1307
1393
|
readonly locale?: string;
|
|
1308
1394
|
readonly parentId?: string;
|
|
1309
1395
|
readonly runId?: string;
|
|
1396
|
+
readonly tenant?: { readonly id: string | null };
|
|
1310
1397
|
};
|
|
1311
1398
|
readonly resolvedLocale: string;
|
|
1312
1399
|
readonly defaultLocale: string;
|
|
1313
1400
|
}): Promise<ExecuteResult> {
|
|
1314
1401
|
const { flowDef, input, trigger, extras, resolvedLocale, defaultLocale } = args;
|
|
1315
1402
|
const booted = await ensureBoot();
|
|
1403
|
+
const tenantEnabled = gateConfig.auth?.tenant !== undefined;
|
|
1404
|
+
const flowTenantScoped = flowDef.tenantScoped ?? tenantEnabled;
|
|
1316
1405
|
|
|
1317
1406
|
const unitBag = flowDef.unit !== undefined ? unitHooks.get(flowDef.unit) : undefined;
|
|
1318
1407
|
// app (hooks + plugs) → unit (hooks + plugs) → flow (hooks + plugs)
|
|
@@ -1365,8 +1454,10 @@ export function oke(options: OkeOptions): OkeApp {
|
|
|
1365
1454
|
userId: options.fx?.auth?.userId ?? null,
|
|
1366
1455
|
scopes: new Set(options.fx?.auth?.scopes ?? []),
|
|
1367
1456
|
verified: options.fx?.auth?.verified,
|
|
1457
|
+
apiKeyId: options.fx?.auth?.apiKeyId,
|
|
1368
1458
|
},
|
|
1369
1459
|
operator: { id: options.fx?.operator?.id ?? null },
|
|
1460
|
+
tenant: { id: extras?.tenant?.id ?? options.fx?.tenant?.id ?? null },
|
|
1370
1461
|
};
|
|
1371
1462
|
|
|
1372
1463
|
// Principal injection: test harness (`env: "test"`) OR console-trusted
|
|
@@ -1383,6 +1474,8 @@ export function oke(options: OkeOptions): OkeApp {
|
|
|
1383
1474
|
const cookieOpts = gateConfig.auth?.cookies;
|
|
1384
1475
|
const cookieToken = wiredAuth?.tokenFromCookieHeader;
|
|
1385
1476
|
const apiKeyStore = gateConfig.auth?.apiKeyStore;
|
|
1477
|
+
const tenantEnabled = gateConfig.auth?.tenant !== undefined;
|
|
1478
|
+
const flowTenantScoped = flowDef.tenantScoped ?? tenantEnabled;
|
|
1386
1479
|
const elementHooks = createElementPipelineHooks({
|
|
1387
1480
|
gates: booted.gate,
|
|
1388
1481
|
principals,
|
|
@@ -1393,7 +1486,8 @@ export function oke(options: OkeOptions): OkeApp {
|
|
|
1393
1486
|
: {}),
|
|
1394
1487
|
verifyBearer:
|
|
1395
1488
|
binding && wiredAuth
|
|
1396
|
-
?
|
|
1489
|
+
? (token, request) =>
|
|
1490
|
+
wiredAuth.verifyBearerOrApiKey(binding, token, apiKeyStore, request)
|
|
1397
1491
|
: undefined,
|
|
1398
1492
|
// Phase 1a: opt-in cookie → Bearer when Authorization is absent.
|
|
1399
1493
|
resolveToken:
|
|
@@ -1406,13 +1500,29 @@ export function oke(options: OkeOptions): OkeApp {
|
|
|
1406
1500
|
return cookieToken(request.headers.get("cookie"), cookieOpts);
|
|
1407
1501
|
}
|
|
1408
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
|
+
: {}),
|
|
1409
1512
|
});
|
|
1410
1513
|
|
|
1411
1514
|
// Element hooks run first in the app-level onAuth/beforeHandle chain —
|
|
1412
1515
|
// principal resolution and gate checks precede user-registered hooks.
|
|
1413
1516
|
hooks = mergeHooks(
|
|
1414
1517
|
{
|
|
1415
|
-
onAuth: [
|
|
1518
|
+
onAuth: [
|
|
1519
|
+
elementHooks.onAuth,
|
|
1520
|
+
async () => {
|
|
1521
|
+
if (journalSession && principals?.tenant.id) {
|
|
1522
|
+
await journalSession.stampTenant(principals.tenant.id);
|
|
1523
|
+
}
|
|
1524
|
+
},
|
|
1525
|
+
],
|
|
1416
1526
|
beforeHandle: [elementHooks.beforeHandle],
|
|
1417
1527
|
},
|
|
1418
1528
|
composedHooks,
|
|
@@ -1437,12 +1547,13 @@ export function oke(options: OkeOptions): OkeApp {
|
|
|
1437
1547
|
}
|
|
1438
1548
|
|
|
1439
1549
|
const effects = flowDef.effects ?? capability?.declared;
|
|
1440
|
-
const { fx, ledger } = createFxContext({
|
|
1550
|
+
const { fx, ledger } = loadFx().createFxContext({
|
|
1441
1551
|
...options.fx,
|
|
1442
1552
|
flow: flowDef.name,
|
|
1443
1553
|
effects,
|
|
1444
1554
|
runTelemetry: telemetry,
|
|
1445
1555
|
runId,
|
|
1556
|
+
lastEventId: extras?.request?.headers.get("last-event-id") || undefined,
|
|
1446
1557
|
now,
|
|
1447
1558
|
i18n: {
|
|
1448
1559
|
locale: resolvedLocale,
|
|
@@ -1450,7 +1561,28 @@ export function oke(options: OkeOptions): OkeApp {
|
|
|
1450
1561
|
catalogs: loadMessages().getMessageCatalogs(),
|
|
1451
1562
|
...options.fx?.i18n,
|
|
1452
1563
|
},
|
|
1453
|
-
...(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
|
+
: {}),
|
|
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
|
+
: {}),
|
|
1454
1586
|
...(extras?.originPrincipal ? { principal: extras.originPrincipal } : {}),
|
|
1455
1587
|
...(extras?.trustedInvoke === true && extras.revealPii === true ? { revealPii: true } : {}),
|
|
1456
1588
|
rlsGateNames: gateNamesOf(trigger),
|
|
@@ -1478,9 +1610,10 @@ export function oke(options: OkeOptions): OkeApp {
|
|
|
1478
1610
|
callInput,
|
|
1479
1611
|
{ kind: "internal" } satisfies InternalTrigger,
|
|
1480
1612
|
{
|
|
1481
|
-
originPrincipal: freezePrincipal(fx.principal),
|
|
1613
|
+
originPrincipal: loadFx().freezePrincipal(fx.principal),
|
|
1482
1614
|
locale: resolvedLocale,
|
|
1483
1615
|
parentId: runId,
|
|
1616
|
+
...(tenantEnabled ? { tenant: { id: fx.tenant.id } } : {}),
|
|
1484
1617
|
...(extras?.trustedInvoke === true && extras.revealPii === true
|
|
1485
1618
|
? { trustedInvoke: true, revealPii: true }
|
|
1486
1619
|
: {}),
|
|
@@ -1582,7 +1715,7 @@ export function oke(options: OkeOptions): OkeApp {
|
|
|
1582
1715
|
durable: flowDef.durable,
|
|
1583
1716
|
effects: putEffects,
|
|
1584
1717
|
});
|
|
1585
|
-
if (storeAfter && output !== undefined && !isJsonStreamResult(output)) {
|
|
1718
|
+
if (storeAfter && output !== undefined && !loadFx().isJsonStreamResult(output)) {
|
|
1586
1719
|
const ttlMs =
|
|
1587
1720
|
typeof flowDef.cache === "string" ? cache.parseTtlMs(flowDef.cache) : undefined;
|
|
1588
1721
|
storeRt.putTier1(
|
|
@@ -1604,6 +1737,7 @@ export function oke(options: OkeOptions): OkeApp {
|
|
|
1604
1737
|
}
|
|
1605
1738
|
return await invoke(ctx.input);
|
|
1606
1739
|
} catch (err) {
|
|
1740
|
+
if (isFlowFailure(err)) return err;
|
|
1607
1741
|
// A durable sleep that has not yet elapsed suspends the run — this
|
|
1608
1742
|
// is a park, not a pipeline failure, so it must not throw upward.
|
|
1609
1743
|
if (flowDef.durable && journalSession && isJournalSuspend(err)) {
|
|
@@ -1630,7 +1764,7 @@ export function oke(options: OkeOptions): OkeApp {
|
|
|
1630
1764
|
|
|
1631
1765
|
const endedAt = now();
|
|
1632
1766
|
const durationMs = resolveDurationMs(endedAt - startedAt, performance.now() - startedHr);
|
|
1633
|
-
const streamOut = isJsonStreamResult(result.output) ? result.output : undefined;
|
|
1767
|
+
const streamOut = loadFx().isJsonStreamResult(result.output) ? result.output : undefined;
|
|
1634
1768
|
|
|
1635
1769
|
const finalizeRun = async (): Promise<void> => {
|
|
1636
1770
|
// Stream rows land in Traces only after close. Measure wall time then
|
|
@@ -1699,7 +1833,11 @@ export function oke(options: OkeOptions): OkeApp {
|
|
|
1699
1833
|
}
|
|
1700
1834
|
};
|
|
1701
1835
|
|
|
1702
|
-
if (
|
|
1836
|
+
if (
|
|
1837
|
+
streamOut &&
|
|
1838
|
+
!isTerminalFailure(result) &&
|
|
1839
|
+
result.response?.headers.get("content-type")?.includes("text/event-stream")
|
|
1840
|
+
) {
|
|
1703
1841
|
streamOut.finalize = finalizeRun;
|
|
1704
1842
|
} else {
|
|
1705
1843
|
await finalizeRun();
|
|
@@ -1780,6 +1918,7 @@ export function oke(options: OkeOptions): OkeApp {
|
|
|
1780
1918
|
get authBinding() {
|
|
1781
1919
|
return authBinding;
|
|
1782
1920
|
},
|
|
1921
|
+
apiKeys: gateConfig.auth?.apiKeyStore,
|
|
1783
1922
|
async resumeDurable(now) {
|
|
1784
1923
|
const t = now ?? bootResult?.clock?.now() ?? options.fx?.now?.() ?? Date.now();
|
|
1785
1924
|
const { store, instanceId, leaseMs } = activeJournal();
|
|
@@ -1805,7 +1944,7 @@ export function oke(options: OkeOptions): OkeApp {
|
|
|
1805
1944
|
for (const [runId, sleeper] of [...sleepingRuns.entries()]) {
|
|
1806
1945
|
if (t < sleeper.wakeAt) continue;
|
|
1807
1946
|
sleepingRuns.delete(runId);
|
|
1808
|
-
const result = await runDurable({
|
|
1947
|
+
const result = await loadDurable().runDurable({
|
|
1809
1948
|
flow: sleeper.flow,
|
|
1810
1949
|
input: sleeper.input,
|
|
1811
1950
|
journalStore: store,
|
|
@@ -1861,7 +2000,7 @@ export function oke(options: OkeOptions): OkeApp {
|
|
|
1861
2000
|
},
|
|
1862
2001
|
flow(ref) {
|
|
1863
2002
|
if (isFlow(ref)) return flowsByName.get(ref.name) ?? ref;
|
|
1864
|
-
return flowsByName.get(resolveName(ref as NamedRef));
|
|
2003
|
+
return flowsByName.get(loadFx().resolveName(ref as NamedRef));
|
|
1865
2004
|
},
|
|
1866
2005
|
adopt(...args: readonly unknown[]) {
|
|
1867
2006
|
const flows = accumulateAdoptArgs(args, routes);
|
|
@@ -1957,7 +2096,7 @@ export function oke(options: OkeOptions): OkeApp {
|
|
|
1957
2096
|
);
|
|
1958
2097
|
runLabel = internalResult.runId;
|
|
1959
2098
|
cacheLabel = internalResult.cache;
|
|
1960
|
-
return respond(encodeExecuteResult(internalResult));
|
|
2099
|
+
return respond(await encodeExecuteResult(internalResult));
|
|
1961
2100
|
}
|
|
1962
2101
|
}
|
|
1963
2102
|
|
|
@@ -2036,10 +2175,10 @@ export function oke(options: OkeOptions): OkeApp {
|
|
|
2036
2175
|
runLabel = result.runId;
|
|
2037
2176
|
cacheLabel = result.cache;
|
|
2038
2177
|
|
|
2039
|
-
return respond(encodeExecuteResult(result));
|
|
2178
|
+
return respond(await encodeExecuteResult(result));
|
|
2040
2179
|
},
|
|
2041
2180
|
async dispatchSignal(signal, payload, meta) {
|
|
2042
|
-
const name = resolveName(signal);
|
|
2181
|
+
const name = loadFx().resolveName(signal);
|
|
2043
2182
|
const results: ExecuteResult[] = [];
|
|
2044
2183
|
for (const b of adopted) {
|
|
2045
2184
|
if (b.trigger.kind === "signal" && b.trigger.name === name) {
|
|
@@ -2052,11 +2191,11 @@ export function oke(options: OkeOptions): OkeApp {
|
|
|
2052
2191
|
}
|
|
2053
2192
|
return results;
|
|
2054
2193
|
},
|
|
2055
|
-
async dispatchEvery(interval) {
|
|
2194
|
+
async dispatchEvery(interval, extras) {
|
|
2056
2195
|
const results: ExecuteResult[] = [];
|
|
2057
2196
|
for (const b of adopted) {
|
|
2058
2197
|
if (b.trigger.kind === "every" && b.trigger.interval === interval) {
|
|
2059
|
-
results.push(await execute(b.flow, undefined, b.trigger as EveryTrigger));
|
|
2198
|
+
results.push(await execute(b.flow, undefined, b.trigger as EveryTrigger, extras));
|
|
2060
2199
|
}
|
|
2061
2200
|
}
|
|
2062
2201
|
return results;
|
|
@@ -2077,7 +2216,7 @@ export function oke(options: OkeOptions): OkeApp {
|
|
|
2077
2216
|
async call(ref, input) {
|
|
2078
2217
|
const flowDef = isFlow(ref)
|
|
2079
2218
|
? (flowsByName.get(ref.name) ?? ref)
|
|
2080
|
-
: flowsByName.get(resolveName(ref as NamedRef));
|
|
2219
|
+
: flowsByName.get(loadFx().resolveName(ref as NamedRef));
|
|
2081
2220
|
if (!flowDef) {
|
|
2082
2221
|
return undefined;
|
|
2083
2222
|
}
|
|
@@ -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,
|
|
@@ -84,12 +84,15 @@ 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
|
|
|
90
91
|
/**
|
|
91
92
|
* Map an authenticated API key row to the pipeline principal shape.
|
|
92
|
-
*
|
|
93
|
+
*
|
|
94
|
+
* The key is the issuer with fewer gates: `userId` / `operatorId` is
|
|
95
|
+
* {@link ApiKeyRow.creatorId}. The credential id is {@link ResolvedPrincipal.apiKeyId}.
|
|
93
96
|
*
|
|
94
97
|
* @param row - Authenticated key
|
|
95
98
|
*/
|
|
@@ -97,19 +100,21 @@ export function apiKeyRowToPrincipal(row: ApiKeyRow): ResolvedPrincipal {
|
|
|
97
100
|
if (row.plane === "operator") {
|
|
98
101
|
return {
|
|
99
102
|
plane: "operator",
|
|
100
|
-
operatorId: row.
|
|
103
|
+
operatorId: row.creatorId,
|
|
101
104
|
userId: null,
|
|
102
105
|
scopes: row.scopes,
|
|
103
106
|
verified: true,
|
|
104
107
|
apiKeyId: row.id,
|
|
108
|
+
...(row.tenantId !== undefined ? { tenantId: row.tenantId } : {}),
|
|
105
109
|
};
|
|
106
110
|
}
|
|
107
111
|
return {
|
|
108
112
|
plane: "user",
|
|
109
|
-
userId: row.
|
|
113
|
+
userId: row.creatorId,
|
|
110
114
|
scopes: row.scopes,
|
|
111
115
|
verified: true,
|
|
112
116
|
apiKeyId: row.id,
|
|
117
|
+
...(row.tenantId !== undefined ? { tenantId: row.tenantId } : {}),
|
|
113
118
|
};
|
|
114
119
|
}
|
|
115
120
|
|
|
@@ -119,17 +124,20 @@ export function apiKeyRowToPrincipal(row: ApiKeyRow): ResolvedPrincipal {
|
|
|
119
124
|
* @param auth - Session binding
|
|
120
125
|
* @param token - Raw Bearer token
|
|
121
126
|
* @param apiKeys - Optional key store
|
|
127
|
+
* @param request - Incoming request (allowlist IP is read here, not in `oke()`)
|
|
122
128
|
*/
|
|
123
129
|
export async function verifyBearerOrApiKey(
|
|
124
130
|
auth: AppAuthBinding,
|
|
125
131
|
token: string,
|
|
126
132
|
apiKeys?: ApiKeyStore,
|
|
133
|
+
request?: Request,
|
|
127
134
|
): Promise<ResolvedPrincipal> {
|
|
128
135
|
try {
|
|
129
136
|
return await verifyBearerToken(auth, token);
|
|
130
137
|
} catch (err) {
|
|
131
138
|
if (apiKeys) {
|
|
132
|
-
const
|
|
139
|
+
const ip = request ? clientIpFromRequest(request) : undefined;
|
|
140
|
+
const row = await authenticateApiKey(apiKeys, token, { now: auth.now, ip });
|
|
133
141
|
if (row) return apiKeyRowToPrincipal(row);
|
|
134
142
|
}
|
|
135
143
|
throw err;
|
|
@@ -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.test.ts
CHANGED
|
@@ -79,7 +79,7 @@ describe("boot — lazy element needs", () => {
|
|
|
79
79
|
expect(needs.signal).toBe(false);
|
|
80
80
|
});
|
|
81
81
|
|
|
82
|
-
test("oke() Store-only graph stays under the prior
|
|
82
|
+
test("oke() Store-only graph stays under the prior 50 kB baseline", async () => {
|
|
83
83
|
const dir = await mkdtemp(join(tmpdir(), "oke-store-only-"));
|
|
84
84
|
const entry = join(dir, "entry.ts");
|
|
85
85
|
const appPath = join(import.meta.dir, "app.ts");
|
|
@@ -116,9 +116,9 @@ describe("boot — lazy element needs", () => {
|
|
|
116
116
|
if (raw.byteLength === 0) continue;
|
|
117
117
|
total += Bun.gzipSync(new Uint8Array(raw)).byteLength;
|
|
118
118
|
}
|
|
119
|
-
// Rebased after
|
|
120
|
-
// (~
|
|
121
|
-
expect(total).toBeLessThan(
|
|
119
|
+
// Rebased after host API-key persist + allowlist on `oke()`.
|
|
120
|
+
// (~50.3 kB gzip with export externals).
|
|
121
|
+
expect(total).toBeLessThan(51_000);
|
|
122
122
|
} finally {
|
|
123
123
|
await rm(dir, { recursive: true, force: true });
|
|
124
124
|
}
|
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
|
*
|