okengine 0.3.6 → 0.5.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/AGENTS.md +2 -0
- package/package.json +14 -12
- package/site/content/docs/ai/index.mdx +24 -0
- package/site/content/docs/ai/llms-txt.mdx +3 -0
- package/site/content/docs/ai/meta.json +1 -1
- package/site/content/docs/ai/skills.mdx +5 -3
- package/site/content/docs/console/gates.mdx +46 -8
- package/site/content/docs/console/index.mdx +54 -0
- package/site/content/docs/console/meta.json +1 -0
- package/site/content/docs/elements/ai.mdx +2 -0
- package/site/content/docs/elements/channel.mdx +2 -0
- package/site/content/docs/elements/clock.mdx +1 -4
- package/site/content/docs/elements/flow.mdx +4 -10
- package/site/content/docs/elements/gate.mdx +189 -48
- package/site/content/docs/elements/index.mdx +45 -0
- package/site/content/docs/elements/meta.json +1 -1
- package/site/content/docs/elements/signal.mdx +1 -5
- package/site/content/docs/elements/store.mdx +27 -6
- package/site/content/docs/elements/vault.mdx +10 -11
- package/site/content/docs/get-started/basic-usage.mdx +79 -43
- package/site/content/docs/get-started/index.mdx +33 -0
- package/site/content/docs/get-started/installation.mdx +95 -43
- package/site/content/docs/get-started/introduction.mdx +128 -75
- package/site/content/docs/get-started/meta.json +1 -1
- package/site/content/docs/get-started/why.mdx +141 -0
- package/site/content/docs/index.mdx +9 -31
- package/site/content/docs/plugins/anonymous.mdx +95 -0
- package/site/content/docs/plugins/compression.mdx +2 -2
- package/site/content/docs/plugins/cors.mdx +2 -2
- package/site/content/docs/plugins/csrf.mdx +2 -2
- package/site/content/docs/plugins/email-otp.mdx +111 -0
- package/site/content/docs/plugins/{security-headers.mdx → headers.mdx} +2 -2
- package/site/content/docs/plugins/index.mdx +69 -0
- package/site/content/docs/plugins/ip-allowlist.mdx +1 -2
- package/site/content/docs/plugins/magic-link.mdx +112 -0
- package/site/content/docs/plugins/maintenance-mode.mdx +8 -8
- package/site/content/docs/plugins/meta.json +10 -1
- package/site/content/docs/plugins/passkey.mdx +128 -0
- package/site/content/docs/plugins/phone-number.mdx +111 -0
- package/site/content/docs/plugins/two-factor.mdx +116 -0
- package/site/content/docs/plugins/username.mdx +117 -0
- package/site/content/docs/reference/client.mdx +331 -0
- package/site/content/docs/reference/configuration.mdx +1 -1
- package/site/content/docs/reference/environment-variables.mdx +5 -3
- package/site/content/docs/reference/fx.mdx +36 -8
- package/site/content/docs/reference/index.mdx +45 -0
- package/site/content/docs/reference/meta.json +11 -1
- package/site/content/docs/reference/plugins.mdx +25 -14
- package/src/auth/auth.test.ts +20 -2
- package/src/auth/bindings.ts +439 -0
- package/src/auth/breach-check.ts +112 -0
- package/src/auth/config.ts +288 -0
- package/src/auth/cookies.ts +123 -0
- package/src/auth/gate-auth.test.ts +379 -0
- package/src/auth/identity.ts +190 -0
- package/src/auth/index.ts +117 -1
- package/src/auth/method-context.ts +33 -0
- package/src/auth/operator.ts +27 -1
- package/src/auth/password-policy.test.ts +126 -0
- package/src/auth/password-policy.ts +77 -0
- package/src/auth/plugin.ts +62 -4
- package/src/auth/rate.ts +45 -0
- package/src/auth/schema.ts +260 -0
- package/src/auth/secondary-storage.ts +37 -0
- package/src/auth/sessions.ts +58 -1
- package/src/auth/tables.ts +4 -0
- package/src/auth/verification.ts +78 -0
- package/src/cli/competitor-mention-removal.test.ts +117 -0
- package/src/cli/dev.test.ts +3 -3
- package/src/cli/dev.ts +20 -0
- package/src/cli/meilisearch-local.test.ts +69 -0
- package/src/cli/meilisearch-local.ts +188 -0
- package/src/cli/schema.ts +95 -23
- package/src/client/auth.ts +120 -0
- package/src/client-react/index.ts +93 -0
- package/src/compiler/aot.test.ts +2 -1
- package/src/compiler/extract.ts +19 -0
- package/src/compiler/fixtures/skyport/src/flows/payments/index.ts +5 -1
- package/src/compiler/response.ts +16 -2
- package/src/console/server/app.ts +10 -6
- package/src/console/server/auth-rate.test.ts +3 -3
- package/src/console/server/bind.ts +12 -1
- package/src/console/server/channels.test.ts +1 -1
- package/src/console/server/console-gates.ts +14 -0
- package/src/console/server/console.test.ts +6 -6
- package/src/console/server/flows-invoke.test.ts +2 -2
- package/src/console/server/flows.ts +2 -0
- package/src/console/server/gates.ts +8 -1
- package/src/console/server/operator-db.test.ts +4 -4
- package/src/console/server/operator-db.ts +22 -4
- package/src/console/server/security.gate.test.ts +3 -3
- package/src/console/server/store.test.ts +1 -1
- package/src/console/server/store.ts +11 -1
- package/src/console/ui/dist/assets/index-CjxwRGVv.js +10 -0
- package/src/console/ui/dist/assets/panel-access-BGv45snf.js +64 -0
- package/src/console/ui/dist/assets/{panel-ai-D_m6WQI8.js → panel-ai-B2S7LEii.js} +1 -1
- package/src/console/ui/dist/assets/{panel-architecture-CKnXFyUx.js → panel-architecture-D7UJh91v.js} +1 -1
- package/src/console/ui/dist/assets/{panel-channels-DCDd4WAC.js → panel-channels-9T3ybqRu.js} +1 -1
- package/src/console/ui/dist/assets/panel-clock-Cb1UXGRQ.js +1 -0
- package/src/console/ui/dist/assets/{panel-diff-cdonmH8c.js → panel-diff-DmYbKWmN.js} +1 -1
- package/src/console/ui/dist/assets/panel-flows-PiHwT55z.js +48 -0
- package/src/console/ui/dist/assets/{panel-gates-B5eTE8XH.js → panel-gates-BQGYXvjT.js} +1 -1
- package/src/console/ui/dist/assets/panel-overview-BBnRO18l.js +1 -0
- package/src/console/ui/dist/assets/{panel-plugins-Cj7DK1er.js → panel-plugins-D0PsmVw2.js} +1 -1
- package/src/console/ui/dist/assets/panel-runs-CWuRDe0r.js +1 -0
- package/src/console/ui/dist/assets/{panel-signals-whmDXIg3.js → panel-signals-Bbg4ewpP.js} +1 -1
- package/src/console/ui/dist/assets/{panel-store-CEMHLvaw.js → panel-store-CPCbsDRa.js} +1 -1
- package/src/console/ui/dist/assets/panel-traces-DVAzuA_S.js +1 -0
- package/src/console/ui/dist/assets/{panel-vault-C9wjbki8.js → panel-vault-D1_MvOmo.js} +1 -1
- package/src/console/ui/dist/assets/{rolldown-runtime-CNC7AqOf.js → rolldown-runtime-B0Z9INg1.js} +1 -1
- package/src/console/ui/dist/index.html +2 -2
- package/src/console/ui/gates/fixture.ts +4 -0
- package/src/console/ui/gates/types.ts +2 -0
- package/src/console/ui/shell/client.ts +1 -0
- package/src/docker/compose.ts +5 -0
- package/src/docker/docker.test.ts +41 -0
- package/src/docker/recipes/index.ts +10 -2
- package/src/docker/recipes/meilisearch.ts +31 -0
- package/src/drivers/conformance.test.ts +16 -1
- package/src/drivers/conformance.ts +40 -3
- package/src/drivers/index.ts +14 -2
- package/src/drivers/libsql.ts +4 -4
- package/src/drivers/meilisearch.integration.test.ts +77 -0
- package/src/drivers/meilisearch.test.ts +181 -0
- package/src/drivers/meilisearch.ts +208 -0
- package/src/drivers/memory.ts +4 -4
- package/src/drivers/pgvector.ts +6 -6
- package/src/drivers/types.ts +93 -12
- package/src/drivers/vault-driver-removal.test.ts +6 -0
- package/src/drivers/vault-types.ts +4 -4
- package/src/elements/ai/runtime.ts +6 -0
- package/src/elements/ai.test.ts +22 -0
- package/src/elements/gate/boot.ts +136 -0
- package/src/elements/gate/config.ts +69 -0
- package/src/elements/gate/declare.ts +51 -1
- package/src/elements/gate/runtime.ts +3 -1
- package/src/elements/gate.test.ts +77 -0
- package/src/elements/gate.ts +20 -1
- package/src/elements/index.ts +8 -0
- package/src/elements/store/index-boot.test.ts +49 -7
- package/src/elements/store/runtime.ts +50 -15
- package/src/elements/store.ts +2 -0
- package/src/elements/vault.test.ts +27 -4
- package/src/elements/vault.ts +1 -1
- package/src/index.ts +15 -0
- package/src/kernel/app.ts +253 -32
- package/src/kernel/boot-bind/store.test.ts +9 -0
- package/src/kernel/boot-bind/store.ts +30 -2
- package/src/kernel/boot.test.ts +40 -3
- package/src/kernel/boot.ts +8 -0
- package/src/kernel/call.test.ts +46 -2
- package/src/kernel/concurrency.test.ts +58 -0
- package/src/kernel/concurrency.ts +48 -0
- package/src/kernel/edge.test.ts +3 -3
- package/src/kernel/flow.test.ts +2 -2
- package/src/kernel/fx.test.ts +12 -2
- package/src/kernel/fx.ts +97 -5
- package/src/kernel/hooks.test.ts +4 -4
- package/src/kernel/index.ts +22 -1
- package/src/kernel/pipeline.test.ts +12 -8
- package/src/kernel/pipeline.ts +23 -4
- package/src/kernel/plugin/decorate.test.ts +3 -3
- package/src/kernel/plugin/scoping.test.ts +3 -3
- package/src/kernel/plugin-elements.test.ts +51 -0
- package/src/kernel/plugin-needs.test.ts +83 -0
- package/src/kernel/plugin-needs.ts +129 -0
- package/src/kernel/plugin.ts +101 -0
- package/src/kernel/redacted.ts +74 -0
- package/src/kernel/registry-isolation.test.ts +5 -5
- package/src/kernel/registry.ts +102 -3
- package/src/kernel/router.ts +3 -3
- package/src/manifest/types.ts +2 -0
- package/src/plugins/anonymous.ts +58 -0
- package/src/plugins/auth/shared.ts +121 -0
- package/src/plugins/auth-methods.test.ts +176 -0
- package/src/plugins/compression.test.ts +5 -5
- package/src/plugins/config-source.test.ts +1 -1
- package/src/plugins/cors.test.ts +16 -10
- package/src/plugins/csrf.test.ts +1 -1
- package/src/plugins/email-otp.ts +161 -0
- package/src/plugins/index.ts +31 -0
- package/src/plugins/ip-allowlist.test.ts +19 -9
- package/src/plugins/magic-link.ts +163 -0
- package/src/plugins/maintenance-mode.test.ts +9 -5
- package/src/plugins/passkey.ts +216 -0
- package/src/plugins/phone-number.ts +149 -0
- package/src/plugins/security-headers.test.ts +14 -14
- package/src/plugins/two-factor.ts +249 -0
- package/src/plugins/username.ts +148 -0
- package/src/runs/runs.test.ts +6 -2
- package/src/runtime/primitives.ts +37 -4
- package/src/runtime/serve.test.ts +3 -2
- package/src/runtime/types.ts +25 -2
- package/src/test/create-test-app.test.ts +1 -1
- package/src/test/create-test-app.ts +4 -1
- package/src/test/provisions.integration.test.ts +2 -2
- package/site/content/docs/get-started/comparison.mdx +0 -65
- package/src/console/ui/dist/assets/index-CrKMmO__.js +0 -10
- package/src/console/ui/dist/assets/panel-access-C0J2D-a2.js +0 -64
- package/src/console/ui/dist/assets/panel-clock-DjGGFPzr.js +0 -1
- package/src/console/ui/dist/assets/panel-flows-DlCU5zjA.js +0 -45
- package/src/console/ui/dist/assets/panel-overview-BsFvDdts.js +0 -1
- package/src/console/ui/dist/assets/panel-runs-C0gmnoYL.js +0 -1
- package/src/console/ui/dist/assets/panel-traces-BDiAuVSK.js +0 -1
- package/src/drivers/vault-infisical.ts +0 -57
package/src/kernel/app.ts
CHANGED
|
@@ -25,22 +25,28 @@ import {
|
|
|
25
25
|
// when an app actually boots (AGENTS.md / unified-theory budget: cold start < 75 ms).
|
|
26
26
|
import type { BootOptions, BootResult, ElementRuntimes } from "./boot.ts";
|
|
27
27
|
import type { CapabilityToken } from "./capability.ts";
|
|
28
|
+
import { createAppAuthBinding, verifyBearerToken, type AppAuthBinding } from "./auth-resolve.ts";
|
|
29
|
+
import { createAuthHttpBindings, type AuthHttpMaterialization } from "../auth/bindings.ts";
|
|
30
|
+
import { auth as authPlugin } from "../auth/plugin.ts";
|
|
31
|
+
import { tokenFromCookieHeader } from "../auth/cookies.ts";
|
|
32
|
+
import { setActiveGateAuthContext } from "../auth/method-context.ts";
|
|
28
33
|
import {
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
type
|
|
32
|
-
|
|
33
|
-
} from "./auth-resolve.ts";
|
|
34
|
+
resolveGateConfig,
|
|
35
|
+
type GateOptions,
|
|
36
|
+
type ResolvedGateConfig,
|
|
37
|
+
} from "../elements/gate/config.ts";
|
|
34
38
|
import { runDurable } from "../elements/clock/durable.ts";
|
|
35
39
|
import { isFlow, type AnyFlowDef } from "./flow.ts";
|
|
36
40
|
import { fxRetry } from "./concurrency.ts";
|
|
37
41
|
import {
|
|
38
42
|
createFxContext,
|
|
43
|
+
freezePrincipal,
|
|
39
44
|
resolveName,
|
|
40
45
|
type CreateFxOptions,
|
|
41
46
|
type Fx,
|
|
42
47
|
type FxAuth,
|
|
43
48
|
type FxOperator,
|
|
49
|
+
type FxPrincipal,
|
|
44
50
|
type NamedRef,
|
|
45
51
|
} from "./fx.ts";
|
|
46
52
|
import {
|
|
@@ -149,8 +155,11 @@ export interface OkeOptions {
|
|
|
149
155
|
readonly vault?: BootOptions["vault"];
|
|
150
156
|
/** Vault secret contracts (shorthand for `vault.secrets`). */
|
|
151
157
|
readonly secrets?: BootOptions["secrets"];
|
|
152
|
-
/**
|
|
153
|
-
|
|
158
|
+
/**
|
|
159
|
+
* Nested Gate bag — auth, policies, rate limits, HTTP posture.
|
|
160
|
+
* Replaces root `auth` / `gates` / `unguardedHttp`.
|
|
161
|
+
*/
|
|
162
|
+
readonly gate?: GateOptions;
|
|
154
163
|
/** Signal declarations. */
|
|
155
164
|
readonly signals?: BootOptions["signals"];
|
|
156
165
|
/** Named clock declarations. */
|
|
@@ -163,8 +172,10 @@ export interface OkeOptions {
|
|
|
163
172
|
readonly ai?: BootOptions["ai"];
|
|
164
173
|
/**
|
|
165
174
|
* Boot automatically on first {@link OkeApp.execute} / {@link OkeApp.fetch}
|
|
166
|
-
* call. Default `
|
|
167
|
-
*
|
|
175
|
+
* call. Default `true` — gate posture, vault, capabilities, and the element
|
|
176
|
+
* pipeline must run before any real traffic. Opt out only for intentional
|
|
177
|
+
* pre-boot unit tests: `oke({ autoBoot: false })` skips `doBoot` entirely
|
|
178
|
+
* (no gates / elements) and must never ship as a serving default.
|
|
168
179
|
*/
|
|
169
180
|
readonly autoBoot?: boolean;
|
|
170
181
|
/**
|
|
@@ -174,11 +185,6 @@ export interface OkeOptions {
|
|
|
174
185
|
readonly startScheduler?: boolean;
|
|
175
186
|
/** Scheduler tick period ms (default 1000). Forwarded to boot. */
|
|
176
187
|
readonly schedulerIntervalMs?: number;
|
|
177
|
-
/**
|
|
178
|
-
* Builtin hybrid-session auth (HMAC access tokens).
|
|
179
|
-
* Required to accept `Authorization: Bearer` in production.
|
|
180
|
-
*/
|
|
181
|
-
readonly auth?: CreateAppAuthBindingOptions;
|
|
182
188
|
}
|
|
183
189
|
|
|
184
190
|
/** Payload for a CDC invocation. */
|
|
@@ -405,6 +411,22 @@ export function oke(options: OkeOptions): OkeApp {
|
|
|
405
411
|
? [...(options.bindings ?? [])]
|
|
406
412
|
: [...listBindings(), ...(options.bindings ?? [])];
|
|
407
413
|
if (registry === "consume") resetBindings();
|
|
414
|
+
|
|
415
|
+
// Resolve Gate bag early so auth HTTP Bindings join `adopted` + the router
|
|
416
|
+
// before posture audit (same ensureBoot → doBoot path — never a side channel).
|
|
417
|
+
const gateConfig: ResolvedGateConfig = resolveGateConfig({
|
|
418
|
+
gate: options.gate,
|
|
419
|
+
env: options.env,
|
|
420
|
+
});
|
|
421
|
+
let authMaterialization: AuthHttpMaterialization | undefined;
|
|
422
|
+
if (gateConfig.auth?.http) {
|
|
423
|
+
authMaterialization = createAuthHttpBindings(gateConfig.auth, {
|
|
424
|
+
rateLimitEnabled: gateConfig.rateLimitEnabled,
|
|
425
|
+
sessions: gateConfig.auth.sessions,
|
|
426
|
+
});
|
|
427
|
+
adopted.push(...authMaterialization.bindings);
|
|
428
|
+
}
|
|
429
|
+
|
|
408
430
|
/** Retained for test harness / boot merges. */
|
|
409
431
|
const $options = options;
|
|
410
432
|
|
|
@@ -417,6 +439,29 @@ export function oke(options: OkeOptions): OkeApp {
|
|
|
417
439
|
/** Runtime route table — types accumulate on the returned {@link OkeApp}. */
|
|
418
440
|
const routes: RuntimeRouteMap = {};
|
|
419
441
|
|
|
442
|
+
// Absorb auth() tables into gate.auth — `.needs("auth")` sees plugin name.
|
|
443
|
+
if (gateConfig.auth) {
|
|
444
|
+
applyPlugin(
|
|
445
|
+
pluginRegistry,
|
|
446
|
+
authPlugin({
|
|
447
|
+
secret: gateConfig.auth.secret,
|
|
448
|
+
accessTtlMs: gateConfig.auth.session.accessTtlMs,
|
|
449
|
+
refreshTtlMs: gateConfig.auth.session.refreshTtlMs,
|
|
450
|
+
session: {
|
|
451
|
+
accessTtlMs: gateConfig.auth.session.accessTtlMs,
|
|
452
|
+
refreshTtlMs: gateConfig.auth.session.refreshTtlMs,
|
|
453
|
+
idleTtlMs: gateConfig.auth.session.idleTtlMs,
|
|
454
|
+
absoluteTtlMs: gateConfig.auth.session.absoluteTtlMs,
|
|
455
|
+
singleSessionPerUser: gateConfig.auth.session.singleSessionPerUser,
|
|
456
|
+
},
|
|
457
|
+
password: gateConfig.auth.password,
|
|
458
|
+
passwordPolicy: gateConfig.auth.passwordPolicy,
|
|
459
|
+
breachCheck: gateConfig.auth.breachCheck,
|
|
460
|
+
}),
|
|
461
|
+
appPluginScope,
|
|
462
|
+
);
|
|
463
|
+
}
|
|
464
|
+
|
|
420
465
|
const smart = createRouter<Binding>(options.router ?? "default");
|
|
421
466
|
for (const b of adopted) {
|
|
422
467
|
if (b.trigger.kind === "http") {
|
|
@@ -424,9 +469,65 @@ export function oke(options: OkeOptions): OkeApp {
|
|
|
424
469
|
compiled.set(b, compileHttpBinding(b, aot));
|
|
425
470
|
}
|
|
426
471
|
}
|
|
427
|
-
|
|
472
|
+
// Defer SmartRouter selection until first match so `.plug()` can still
|
|
473
|
+
// contribute auth-method Bindings before traffic (plan Phase 2).
|
|
428
474
|
const router: Router<Binding> = smart;
|
|
429
475
|
|
|
476
|
+
function adoptBinding(b: Binding): void {
|
|
477
|
+
adopted.push(b);
|
|
478
|
+
registerFlow(b.flow);
|
|
479
|
+
if (b.trigger.kind === "http") {
|
|
480
|
+
smart.add(b.trigger.method, b.trigger.path, b);
|
|
481
|
+
compiled.set(b, compileHttpBinding(b, aot));
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
// Phase 1a: mirror tokens into Set-Cookie when gate.auth.cookies.enabled.
|
|
486
|
+
if (gateConfig.auth?.cookies.enabled) {
|
|
487
|
+
const cookieCfg = gateConfig.auth.cookies;
|
|
488
|
+
const basePath = gateConfig.auth.basePath;
|
|
489
|
+
const list = appHooks.afterHandle ?? (appHooks.afterHandle = []);
|
|
490
|
+
list.push(async (ctx) => {
|
|
491
|
+
const res = ctx.response;
|
|
492
|
+
if (!(res instanceof Response) || !ctx.request) return;
|
|
493
|
+
const url = new URL(ctx.request.url);
|
|
494
|
+
const path = url.pathname;
|
|
495
|
+
const underAuth = path === basePath || path.startsWith(`${basePath}/`);
|
|
496
|
+
const isRevoke = path === `${basePath}/revoke` || path.endsWith("/revoke");
|
|
497
|
+
const isIssue = underAuth && !isRevoke;
|
|
498
|
+
if (!isIssue && !isRevoke) return;
|
|
499
|
+
const { buildAuthSetCookies, clearAuthSetCookies } = await import("../auth/cookies.ts");
|
|
500
|
+
const { ACCESS_TTL_MS, REFRESH_TTL_MS } = await import("../auth/sessions.ts");
|
|
501
|
+
if (isRevoke || res.status >= 400) {
|
|
502
|
+
if (isRevoke) {
|
|
503
|
+
const headers = new Headers(res.headers);
|
|
504
|
+
for (const c of clearAuthSetCookies(cookieCfg)) headers.append("Set-Cookie", c);
|
|
505
|
+
ctx.response = new Response(res.body, { status: res.status, headers });
|
|
506
|
+
}
|
|
507
|
+
return;
|
|
508
|
+
}
|
|
509
|
+
try {
|
|
510
|
+
const body = (await res.clone().json()) as {
|
|
511
|
+
data?: { accessToken?: string; refreshToken?: string };
|
|
512
|
+
};
|
|
513
|
+
if (!body.data?.accessToken || !body.data?.refreshToken) return;
|
|
514
|
+
const headers = new Headers(res.headers);
|
|
515
|
+
const accessTtl = gateConfig.auth!.session.accessTtlMs ?? ACCESS_TTL_MS;
|
|
516
|
+
const refreshTtl = gateConfig.auth!.session.refreshTtlMs ?? REFRESH_TTL_MS;
|
|
517
|
+
for (const c of buildAuthSetCookies(
|
|
518
|
+
cookieCfg,
|
|
519
|
+
{ accessToken: body.data.accessToken, refreshToken: body.data.refreshToken },
|
|
520
|
+
{ access: Math.floor(accessTtl / 1000), refresh: Math.floor(refreshTtl / 1000) },
|
|
521
|
+
)) {
|
|
522
|
+
headers.append("Set-Cookie", c);
|
|
523
|
+
}
|
|
524
|
+
ctx.response = new Response(res.body, { status: res.status, headers });
|
|
525
|
+
} catch {
|
|
526
|
+
/* ignore non-JSON */
|
|
527
|
+
}
|
|
528
|
+
});
|
|
529
|
+
}
|
|
530
|
+
|
|
430
531
|
function flushFlowPlugins(flowDef: AnyFlowDef): void {
|
|
431
532
|
let done = flushedPlugins.get(flowDef);
|
|
432
533
|
if (!done) {
|
|
@@ -454,12 +555,22 @@ export function oke(options: OkeOptions): OkeApp {
|
|
|
454
555
|
let bootPromise: Promise<BootResult> | undefined;
|
|
455
556
|
let bootEnv: BootOptions["env"] = options.env ?? "local";
|
|
456
557
|
let authBinding: AppAuthBinding | undefined =
|
|
457
|
-
|
|
558
|
+
gateConfig.auth !== undefined
|
|
458
559
|
? createAppAuthBinding({
|
|
459
|
-
|
|
460
|
-
|
|
560
|
+
secret: gateConfig.auth.secret,
|
|
561
|
+
sessions: authMaterialization?.ctx.sessions ?? gateConfig.auth.sessions,
|
|
562
|
+
now: gateConfig.auth.now ?? options.fx?.now,
|
|
461
563
|
})
|
|
462
564
|
: undefined;
|
|
565
|
+
if (authBinding) {
|
|
566
|
+
setActiveGateAuthContext({
|
|
567
|
+
secret: authBinding.secret,
|
|
568
|
+
sessions: authBinding.sessions,
|
|
569
|
+
now: authBinding.now,
|
|
570
|
+
});
|
|
571
|
+
} else {
|
|
572
|
+
setActiveGateAuthContext(undefined);
|
|
573
|
+
}
|
|
463
574
|
const journalStore = createMemoryJournalStore();
|
|
464
575
|
const sleepingRuns = new Map<
|
|
465
576
|
string,
|
|
@@ -482,22 +593,108 @@ export function oke(options: OkeOptions): OkeApp {
|
|
|
482
593
|
}
|
|
483
594
|
|
|
484
595
|
async function doBoot(overrides?: Partial<BootOptions>): Promise<BootResult> {
|
|
485
|
-
const { bootApplication } = await import("./boot.ts");
|
|
596
|
+
const { bootApplication, resolveElementNeeds } = await import("./boot.ts");
|
|
597
|
+
const { assertHttpGatePosture } = await import("../elements/gate/boot.ts");
|
|
598
|
+
const { assertPluginNeeds, buildAvailableNeedTokens } = await import("./plugin-needs.ts");
|
|
486
599
|
bootEnv = overrides?.env ?? options.env ?? "local";
|
|
487
|
-
|
|
600
|
+
|
|
601
|
+
// Prod without a real secret fails even if construction minted a dev secret.
|
|
602
|
+
if (gateConfig.auth?.secretMinted && bootEnv === "prod") {
|
|
603
|
+
throw new Error(
|
|
604
|
+
"gate.auth: secret is required in production (set gate.auth.secret or OKE_AUTH_SECRET)",
|
|
605
|
+
);
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
const unguardedHttp = overrides?.unguardedHttp ?? gateConfig.unguardedHttp ?? "deny";
|
|
609
|
+
// Auth bindings are in `adopted` — missing posture fails with GateBootError.
|
|
610
|
+
assertHttpGatePosture(adopted, { unguardedHttp, env: bootEnv });
|
|
611
|
+
|
|
612
|
+
const caps = pluginRegistry.capabilities();
|
|
613
|
+
const pluginNames = new Set(Object.keys(caps));
|
|
614
|
+
const authGates = authMaterialization?.authGates ?? [];
|
|
615
|
+
const baseGates = overrides?.gates ?? gateConfig.policies ?? [];
|
|
616
|
+
const elementNeeds = resolveElementNeeds({
|
|
488
617
|
env: bootEnv,
|
|
489
618
|
docker: overrides?.docker ?? options.docker,
|
|
490
619
|
config: overrides?.config ?? options.config,
|
|
491
620
|
elements: overrides?.elements ?? options.elements,
|
|
492
621
|
secrets: overrides?.secrets ?? options.secrets,
|
|
493
622
|
vault: overrides?.vault ?? options.vault,
|
|
494
|
-
gates:
|
|
623
|
+
gates: [...baseGates, ...authGates],
|
|
495
624
|
signals: overrides?.signals ?? options.signals,
|
|
496
625
|
clocks: overrides?.clocks ?? options.clocks,
|
|
497
626
|
stores: overrides?.stores ?? options.stores,
|
|
498
627
|
channel: overrides?.channel ?? options.channel,
|
|
499
628
|
ai: overrides?.ai ?? options.ai,
|
|
500
629
|
runs: overrides?.runs ?? options.runs,
|
|
630
|
+
bindings: adopted,
|
|
631
|
+
flows: [...flowsByName.values()],
|
|
632
|
+
});
|
|
633
|
+
const driverIds: string[] = [];
|
|
634
|
+
for (const c of Object.values(caps)) {
|
|
635
|
+
for (const d of c.declares) {
|
|
636
|
+
if (d.startsWith("driver:")) driverIds.push(d.slice("driver:".length));
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
const stores = overrides?.stores ?? options.stores ?? [];
|
|
640
|
+
const available = buildAvailableNeedTokens({
|
|
641
|
+
elements: {
|
|
642
|
+
storeSql:
|
|
643
|
+
elementNeeds.store ||
|
|
644
|
+
stores.some((s) => s.facet === "sql") ||
|
|
645
|
+
pluginRegistry.tableContributions().length > 0,
|
|
646
|
+
storeKv:
|
|
647
|
+
elementNeeds.store ||
|
|
648
|
+
stores.some((s) => s.facet === "kv") ||
|
|
649
|
+
gateConfig.auth?.secondaryStorage.enabled === true,
|
|
650
|
+
storeFiles: elementNeeds.store || stores.some((s) => s.facet === "files"),
|
|
651
|
+
storeIndex: elementNeeds.store || stores.some((s) => s.facet === "index"),
|
|
652
|
+
signal: elementNeeds.signal,
|
|
653
|
+
clock: elementNeeds.clock,
|
|
654
|
+
gate: elementNeeds.gate || baseGates.length > 0 || authGates.length > 0,
|
|
655
|
+
vault: elementNeeds.vault,
|
|
656
|
+
channel: elementNeeds.channel,
|
|
657
|
+
ai: elementNeeds.ai,
|
|
658
|
+
},
|
|
659
|
+
driverIds,
|
|
660
|
+
});
|
|
661
|
+
const pluginSecrets = pluginRegistry.vaultContributions();
|
|
662
|
+
const pluginGates = pluginRegistry.gateContributions();
|
|
663
|
+
const pluginSignals = pluginRegistry.signalContributions();
|
|
664
|
+
const pluginClocks = pluginRegistry.clockContributions();
|
|
665
|
+
const pluginChannelTemplates = pluginRegistry.channelTemplateContributions();
|
|
666
|
+
if (pluginSecrets.length > 0) available.add("vault");
|
|
667
|
+
if (pluginGates.length > 0) available.add("gate");
|
|
668
|
+
if (pluginSignals.length > 0) available.add("signal");
|
|
669
|
+
if (pluginClocks.length > 0) available.add("clock");
|
|
670
|
+
if (pluginChannelTemplates.length > 0) available.add("channel");
|
|
671
|
+
// gate.auth (auto-absorbed auth plugin) satisfies `.needs("auth")`.
|
|
672
|
+
if (gateConfig.auth) available.add("auth");
|
|
673
|
+
assertPluginNeeds(caps, { pluginNames, available });
|
|
674
|
+
|
|
675
|
+
const baseSecrets = overrides?.secrets ?? options.secrets ?? [];
|
|
676
|
+
const baseSignals = overrides?.signals ?? options.signals ?? [];
|
|
677
|
+
const baseClocks = overrides?.clocks ?? options.clocks ?? [];
|
|
678
|
+
const baseChannel = overrides?.channel ?? options.channel;
|
|
679
|
+
|
|
680
|
+
const merged: BootOptions = {
|
|
681
|
+
env: bootEnv,
|
|
682
|
+
docker: overrides?.docker ?? options.docker,
|
|
683
|
+
config: overrides?.config ?? options.config,
|
|
684
|
+
elements: overrides?.elements ?? options.elements,
|
|
685
|
+
secrets: [...baseSecrets, ...pluginSecrets],
|
|
686
|
+
vault: overrides?.vault ?? options.vault,
|
|
687
|
+
gates: [...baseGates, ...authGates, ...pluginGates],
|
|
688
|
+
unguardedHttp,
|
|
689
|
+
signals: [...baseSignals, ...pluginSignals],
|
|
690
|
+
clocks: [...baseClocks, ...pluginClocks],
|
|
691
|
+
stores: overrides?.stores ?? options.stores,
|
|
692
|
+
channel: {
|
|
693
|
+
...(baseChannel ?? {}),
|
|
694
|
+
templates: [...(baseChannel?.templates ?? []), ...pluginChannelTemplates],
|
|
695
|
+
},
|
|
696
|
+
ai: overrides?.ai ?? options.ai,
|
|
697
|
+
runs: overrides?.runs ?? options.runs,
|
|
501
698
|
now: overrides?.now ?? options.fx?.now,
|
|
502
699
|
instanceId: overrides?.instanceId,
|
|
503
700
|
startScheduler: overrides?.startScheduler ?? options.startScheduler,
|
|
@@ -521,13 +718,14 @@ export function oke(options: OkeOptions): OkeApp {
|
|
|
521
718
|
}
|
|
522
719
|
|
|
523
720
|
/**
|
|
524
|
-
* Boot lazily
|
|
525
|
-
*
|
|
526
|
-
*
|
|
721
|
+
* Boot lazily on first execute/fetch unless {@link OkeOptions.autoBoot} is
|
|
722
|
+
* explicitly `false` (legacy pre-boot escape hatch for unit tests).
|
|
723
|
+
* Default is on — security posture and element runtimes are not optional
|
|
724
|
+
* for traffic-serving apps.
|
|
527
725
|
*/
|
|
528
726
|
async function ensureBoot(): Promise<BootResult | undefined> {
|
|
529
727
|
if (bootResult) return bootResult;
|
|
530
|
-
if (options.autoBoot
|
|
728
|
+
if (options.autoBoot === false) return undefined;
|
|
531
729
|
if (!bootPromise) {
|
|
532
730
|
bootPromise = doBoot();
|
|
533
731
|
}
|
|
@@ -554,6 +752,8 @@ export function oke(options: OkeOptions): OkeApp {
|
|
|
554
752
|
readonly auth?: ResolvedPrincipal | FxAuth;
|
|
555
753
|
readonly operator?: FxOperator;
|
|
556
754
|
readonly principal?: ResolvedPrincipal;
|
|
755
|
+
/** Frozen origin identity for {@link Fx.principal} across `fx.call`. */
|
|
756
|
+
readonly originPrincipal?: FxPrincipal;
|
|
557
757
|
},
|
|
558
758
|
): Promise<ExecuteResult> {
|
|
559
759
|
registerFlow(flowDef);
|
|
@@ -588,8 +788,8 @@ export function oke(options: OkeOptions): OkeApp {
|
|
|
588
788
|
const startedAt = now();
|
|
589
789
|
const telemetry = createRunTelemetry();
|
|
590
790
|
|
|
591
|
-
// Element pipeline wiring only kicks in once booted —
|
|
592
|
-
//
|
|
791
|
+
// Element pipeline wiring only kicks in once booted — `autoBoot: false`
|
|
792
|
+
// keeps intentional pre-boot unit tests ungated (no elements either).
|
|
593
793
|
let principals: PrincipalBag | undefined;
|
|
594
794
|
let hooks: HookMap = composedHooks;
|
|
595
795
|
let capability: CapabilityToken | undefined;
|
|
@@ -615,12 +815,24 @@ export function oke(options: OkeOptions): OkeApp {
|
|
|
615
815
|
}
|
|
616
816
|
|
|
617
817
|
const binding = authBinding;
|
|
818
|
+
const cookieOpts = gateConfig.auth?.cookies;
|
|
618
819
|
const elementHooks = createElementPipelineHooks({
|
|
619
820
|
gates: booted.gate,
|
|
620
821
|
principals,
|
|
621
822
|
telemetry,
|
|
622
823
|
allowTestPrincipals: testMode,
|
|
623
|
-
verifyBearer: binding ? (token) => verifyBearerToken(binding, token) : undefined,
|
|
824
|
+
verifyBearer: binding ? async (token) => verifyBearerToken(binding, token) : undefined,
|
|
825
|
+
// Phase 1a: opt-in cookie → Bearer when Authorization is absent.
|
|
826
|
+
resolveToken:
|
|
827
|
+
binding && cookieOpts?.enabled
|
|
828
|
+
? (request) => {
|
|
829
|
+
const header = request.headers.get("authorization");
|
|
830
|
+
if (header?.startsWith("Bearer ")) {
|
|
831
|
+
return header.slice("Bearer ".length).trim() || undefined;
|
|
832
|
+
}
|
|
833
|
+
return tokenFromCookieHeader(request.headers.get("cookie"), cookieOpts);
|
|
834
|
+
}
|
|
835
|
+
: undefined,
|
|
624
836
|
});
|
|
625
837
|
|
|
626
838
|
// Element hooks run first in the app-level onAuth/beforeHandle chain —
|
|
@@ -649,6 +861,7 @@ export function oke(options: OkeOptions): OkeApp {
|
|
|
649
861
|
runTelemetry: telemetry,
|
|
650
862
|
now,
|
|
651
863
|
...(principals ? { auth: principals.auth as FxAuth, operator: principals.operator } : {}),
|
|
864
|
+
...(extras?.originPrincipal ? { principal: extras.originPrincipal } : {}),
|
|
652
865
|
...(capability ? { capability } : {}),
|
|
653
866
|
...(booted
|
|
654
867
|
? {
|
|
@@ -665,9 +878,12 @@ export function oke(options: OkeOptions): OkeApp {
|
|
|
665
878
|
if (!target) {
|
|
666
879
|
return undefined;
|
|
667
880
|
}
|
|
668
|
-
const inner = await execute(
|
|
669
|
-
|
|
670
|
-
|
|
881
|
+
const inner = await execute(
|
|
882
|
+
target,
|
|
883
|
+
callInput,
|
|
884
|
+
{ kind: "internal" } satisfies InternalTrigger,
|
|
885
|
+
{ originPrincipal: freezePrincipal(fx.principal) },
|
|
886
|
+
);
|
|
671
887
|
if (inner.failure) return inner.failure;
|
|
672
888
|
return inner.output;
|
|
673
889
|
},
|
|
@@ -842,8 +1058,13 @@ export function oke(options: OkeOptions): OkeApp {
|
|
|
842
1058
|
}
|
|
843
1059
|
},
|
|
844
1060
|
plug(pluginDef) {
|
|
1061
|
+
const before = new Set(pluginRegistry.bindingContributions().map((b) => b.flow.name));
|
|
845
1062
|
applyPlugin(pluginRegistry, pluginDef, appPluginScope);
|
|
846
|
-
|
|
1063
|
+
for (const b of pluginRegistry.bindingContributions()) {
|
|
1064
|
+
if (before.has(b.flow.name)) continue;
|
|
1065
|
+
adoptBinding(b);
|
|
1066
|
+
}
|
|
1067
|
+
// Decorations accumulate on the interface; runtime object is unchanged.
|
|
847
1068
|
return app as never;
|
|
848
1069
|
},
|
|
849
1070
|
pluginCapabilities() {
|
|
@@ -114,6 +114,15 @@ describe("bindStore index driver resolution", () => {
|
|
|
114
114
|
expect(indexDriverFor("libsql").id).toBe("libsql");
|
|
115
115
|
});
|
|
116
116
|
|
|
117
|
+
test("meilisearch resolves from config as a fourth id", () => {
|
|
118
|
+
const options = {
|
|
119
|
+
config: { drivers: { store: { index: { local: "meilisearch", docker: "meilisearch" } } } },
|
|
120
|
+
};
|
|
121
|
+
expect(resolveIndexDriverId(options, "local", false)).toBe("meilisearch");
|
|
122
|
+
expect(resolveIndexDriverId(options, "docker", true)).toBe("meilisearch");
|
|
123
|
+
expect(indexDriverFor("meilisearch").id).toBe("meilisearch");
|
|
124
|
+
});
|
|
125
|
+
|
|
117
126
|
test("docker mode honours OKE_INDEX_DRIVER override", () => {
|
|
118
127
|
process.env.OKE_INDEX_DRIVER = "pgvector";
|
|
119
128
|
expect(resolveIndexDriverId({}, "docker", true)).toBe("pgvector");
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
import { resolveDomainDdlMode, resolveDriverId, type ConfigEnv } from "../../config/index.ts";
|
|
6
6
|
import { fsDriver } from "../../drivers/fs.ts";
|
|
7
7
|
import { libsqlDriver, libsqlIndexDriver } from "../../drivers/libsql.ts";
|
|
8
|
+
import { meilisearchDriver } from "../../drivers/meilisearch.ts";
|
|
8
9
|
import { memoryDrivers } from "../../drivers/memory.ts";
|
|
9
10
|
import { pgliteDriver } from "../../drivers/pglite.ts";
|
|
10
11
|
import { pgvectorDriver } from "../../drivers/pgvector.ts";
|
|
@@ -45,7 +46,7 @@ export function bindStore(
|
|
|
45
46
|
const sqlBindings: Record<string, { name: string; primary: { url: string } }> = {};
|
|
46
47
|
const kvBindings: Record<string, { url?: string }> = {};
|
|
47
48
|
const filesBindings: Record<string, { root?: string }> = {};
|
|
48
|
-
const indexBindings: Record<string, { url?: string }> = {};
|
|
49
|
+
const indexBindings: Record<string, { url?: string; apiKey?: string }> = {};
|
|
49
50
|
|
|
50
51
|
for (const decl of options.stores ?? []) {
|
|
51
52
|
if (isSqlDecl(decl)) {
|
|
@@ -60,7 +61,14 @@ export function bindStore(
|
|
|
60
61
|
} else if (isIndexDecl(decl)) {
|
|
61
62
|
// SQL-backed index drivers share the sql facet's URL — the runtime opens
|
|
62
63
|
// one connection and hands it to both (never a second, redundant one).
|
|
63
|
-
|
|
64
|
+
// Meilisearch is a standalone HTTP service: its own URL + key, never sqlUrl.
|
|
65
|
+
if (indexId === "memory") {
|
|
66
|
+
indexBindings[decl.name] = {};
|
|
67
|
+
} else if (indexId === "meilisearch") {
|
|
68
|
+
indexBindings[decl.name] = { url: indexUrlFor(docker), apiKey: indexApiKeyFor() };
|
|
69
|
+
} else {
|
|
70
|
+
indexBindings[decl.name] = { url: sqlUrl };
|
|
71
|
+
}
|
|
64
72
|
}
|
|
65
73
|
}
|
|
66
74
|
|
|
@@ -216,6 +224,8 @@ export function indexDriverFor(id: string): IndexDriver {
|
|
|
216
224
|
return pgvectorDriver;
|
|
217
225
|
case "libsql":
|
|
218
226
|
return libsqlIndexDriver;
|
|
227
|
+
case "meilisearch":
|
|
228
|
+
return meilisearchDriver;
|
|
219
229
|
default:
|
|
220
230
|
throw new Error(`oke boot: unknown index driver "${id}"`);
|
|
221
231
|
}
|
|
@@ -263,6 +273,24 @@ function filesRootFor(filesId: string): string | undefined {
|
|
|
263
273
|
return process.env.S3_BUCKET ?? process.env.OKE_STORE_FILES_DB ?? undefined;
|
|
264
274
|
}
|
|
265
275
|
|
|
276
|
+
/** Meilisearch base URL — standalone HTTP service, fail loud when missing. */
|
|
277
|
+
function indexUrlFor(docker: boolean): string {
|
|
278
|
+
const url = process.env.OKE_STORE_INDEX_URL ?? undefined;
|
|
279
|
+
if (!url) {
|
|
280
|
+
throw new Error(
|
|
281
|
+
docker
|
|
282
|
+
? "oke boot: meilisearch index needs OKE_STORE_INDEX_URL (did `oke dev -d` write docker/.env.docker?)"
|
|
283
|
+
: "oke boot: meilisearch index needs OKE_STORE_INDEX_URL",
|
|
284
|
+
);
|
|
285
|
+
}
|
|
286
|
+
return url;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/** Meilisearch API / master key. */
|
|
290
|
+
function indexApiKeyFor(): string | undefined {
|
|
291
|
+
return process.env.OKE_STORE_INDEX_KEY ?? process.env.MEILI_MASTER_KEY ?? undefined;
|
|
292
|
+
}
|
|
293
|
+
|
|
266
294
|
function isSqlDecl(decl: StoreDecl): decl is Extract<StoreDecl, { facet: "sql" }> {
|
|
267
295
|
return decl.facet === "sql";
|
|
268
296
|
}
|
package/src/kernel/boot.test.ts
CHANGED
|
@@ -190,8 +190,8 @@ describe("boot — cron fires without manual dispatch", () => {
|
|
|
190
190
|
});
|
|
191
191
|
});
|
|
192
192
|
|
|
193
|
-
describe("boot — HTTP gate wiring
|
|
194
|
-
test("
|
|
193
|
+
describe("boot — HTTP gate wiring on the default path", () => {
|
|
194
|
+
test("autoBoot: false still serves without gates (explicit escape hatch)", async () => {
|
|
195
195
|
resetBindings();
|
|
196
196
|
resetFlowSeq();
|
|
197
197
|
on(
|
|
@@ -201,9 +201,46 @@ describe("boot — HTTP gate wiring lives behind boot", () => {
|
|
|
201
201
|
do: () => ({ ok: true }),
|
|
202
202
|
}),
|
|
203
203
|
);
|
|
204
|
-
const app = oke({ name: "legacy" });
|
|
204
|
+
const app = oke({ name: "legacy", autoBoot: false });
|
|
205
205
|
const res = await app.fetch(new Request("http://localhost/ping", { method: "GET" }));
|
|
206
206
|
expect(res.status).toBe(200);
|
|
207
|
+
expect(app.booted).toBe(false);
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
test("bare oke().fetch() enforces gate posture by default (no flags)", async () => {
|
|
211
|
+
resetBindings();
|
|
212
|
+
resetFlowSeq();
|
|
213
|
+
const { GateBootError } = await import("../elements/gate/boot.ts");
|
|
214
|
+
on(
|
|
215
|
+
http.get("/ping"),
|
|
216
|
+
flow({
|
|
217
|
+
name: "ping",
|
|
218
|
+
do: () => ({ ok: true }),
|
|
219
|
+
}),
|
|
220
|
+
);
|
|
221
|
+
const app = oke({ name: "default-posture" });
|
|
222
|
+
await expect(
|
|
223
|
+
app.fetch(new Request("http://localhost/ping", { method: "GET" })),
|
|
224
|
+
).rejects.toThrow(GateBootError);
|
|
225
|
+
expect(app.booted).toBe(false);
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
test("bare oke().fetch() with gate.public boots and serves", async () => {
|
|
229
|
+
resetBindings();
|
|
230
|
+
resetFlowSeq();
|
|
231
|
+
const { gate } = await import("../elements/gate.ts");
|
|
232
|
+
on(
|
|
233
|
+
http.get("/ping").gate(gate.public),
|
|
234
|
+
flow({
|
|
235
|
+
name: "ping-public",
|
|
236
|
+
do: () => ({ ok: true }),
|
|
237
|
+
}),
|
|
238
|
+
);
|
|
239
|
+
const app = oke({ name: "default-public", env: "test", startScheduler: false });
|
|
240
|
+
const res = await app.fetch(new Request("http://localhost/ping", { method: "GET" }));
|
|
241
|
+
expect(res.status).toBe(200);
|
|
242
|
+
expect(app.booted).toBe(true);
|
|
243
|
+
await app.stop();
|
|
207
244
|
});
|
|
208
245
|
});
|
|
209
246
|
|
package/src/kernel/boot.ts
CHANGED
|
@@ -75,6 +75,14 @@ export interface BootOptions {
|
|
|
75
75
|
};
|
|
76
76
|
/** Gate declarations for the runtime. */
|
|
77
77
|
readonly gates?: readonly GateDecl[];
|
|
78
|
+
/**
|
|
79
|
+
* HTTP auth-posture enforcement at boot.
|
|
80
|
+
* - `"deny"` (default) — every HTTP trigger must carry a gate or `gate.public`
|
|
81
|
+
* - `"allow"` — skips the audit **only** when {@link env} is `"test"`;
|
|
82
|
+
* ignored in local/prod/docker (never a production-wide bypass).
|
|
83
|
+
* Migrate real apps with per-trigger `gate.public`.
|
|
84
|
+
*/
|
|
85
|
+
readonly unguardedHttp?: "deny" | "allow";
|
|
78
86
|
/** Signal declarations. */
|
|
79
87
|
readonly signals?: readonly SignalDecl[];
|
|
80
88
|
/** Named clock declarations. */
|
package/src/kernel/call.test.ts
CHANGED
|
@@ -28,7 +28,7 @@ describe("fx.call — untriggered flows", () => {
|
|
|
28
28
|
}),
|
|
29
29
|
);
|
|
30
30
|
|
|
31
|
-
const app = oke({ name: "call" }).adopt(stats);
|
|
31
|
+
const app = oke({ autoBoot: false, name: "call" }).adopt(stats);
|
|
32
32
|
|
|
33
33
|
// Direct call (Linkly ⑤)
|
|
34
34
|
await expect(app.call(stats, { code: "sa" })).resolves.toEqual({
|
|
@@ -53,6 +53,50 @@ describe("fx.call — untriggered flows", () => {
|
|
|
53
53
|
expect(stats.triggers).toHaveLength(0);
|
|
54
54
|
});
|
|
55
55
|
|
|
56
|
+
test("fx.call propagates fx.principal without filling fx.auth", async () => {
|
|
57
|
+
const audit = flow({
|
|
58
|
+
name: "audit.log",
|
|
59
|
+
do: (_input: { event: string }, fx) => ({
|
|
60
|
+
authUserId: fx.auth.userId,
|
|
61
|
+
principalUserId: fx.principal.userId,
|
|
62
|
+
principalScopes: [...fx.principal.scopes],
|
|
63
|
+
}),
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
const act = on(
|
|
67
|
+
http.post("/act"),
|
|
68
|
+
flow({
|
|
69
|
+
name: "act",
|
|
70
|
+
effects: { calls: ["audit.log"] },
|
|
71
|
+
do: async (_input: Record<string, never>, fx) => {
|
|
72
|
+
return fx.call("audit.log", { event: "act" });
|
|
73
|
+
},
|
|
74
|
+
}),
|
|
75
|
+
);
|
|
76
|
+
|
|
77
|
+
const app = oke({
|
|
78
|
+
autoBoot: false,
|
|
79
|
+
name: "principal-call",
|
|
80
|
+
gate: { unguardedHttp: "allow" },
|
|
81
|
+
}).adopt(audit);
|
|
82
|
+
await app.boot({ env: "test", unguardedHttp: "allow" });
|
|
83
|
+
|
|
84
|
+
const result = await app.execute(act, {}, act.$trigger ?? act.triggers[0]!, {
|
|
85
|
+
principal: {
|
|
86
|
+
userId: "user-1",
|
|
87
|
+
scopes: ["booking:create"],
|
|
88
|
+
verified: true,
|
|
89
|
+
},
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
expect(result.failure).toBeUndefined();
|
|
93
|
+
expect(result.output).toEqual({
|
|
94
|
+
authUserId: null,
|
|
95
|
+
principalUserId: "user-1",
|
|
96
|
+
principalScopes: ["booking:create"],
|
|
97
|
+
});
|
|
98
|
+
});
|
|
99
|
+
|
|
56
100
|
test("http and signal invocations of the same flow execute identically", async () => {
|
|
57
101
|
let runs = 0;
|
|
58
102
|
const work = flow({
|
|
@@ -66,7 +110,7 @@ describe("fx.call — untriggered flows", () => {
|
|
|
66
110
|
on(http.post("/work"), work);
|
|
67
111
|
on({ name: "tick", delivery: "broadcast" }, work);
|
|
68
112
|
|
|
69
|
-
const app = oke({ name: "identical" });
|
|
113
|
+
const app = oke({ autoBoot: false, name: "identical" });
|
|
70
114
|
|
|
71
115
|
const a = await app.fetch(
|
|
72
116
|
new Request("http://localhost/work", {
|