okengine 0.10.1 → 0.10.3
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/README.md +1 -1
- package/package.json +1 -1
- package/site/content/docs/ai/mcp.mdx +0 -1
- package/site/content/docs/elements/ai.mdx +0 -6
- package/site/content/docs/elements/channel.mdx +0 -6
- package/site/content/docs/elements/clock.mdx +2 -8
- package/site/content/docs/elements/flow.mdx +10 -17
- package/site/content/docs/elements/gate.mdx +4 -10
- package/site/content/docs/elements/signal.mdx +4 -13
- package/site/content/docs/elements/store.mdx +9 -16
- package/site/content/docs/elements/vault.mdx +1 -7
- package/site/content/docs/get-started/basic-usage.mdx +1 -9
- package/site/content/docs/get-started/introduction.mdx +1 -1
- package/site/content/docs/get-started/why.mdx +1 -2
- package/site/content/docs/index.mdx +0 -1
- package/site/content/docs/meta.json +0 -1
- package/site/content/docs/reference/cli.md +12 -12
- package/site/content/docs/reference/fx.mdx +6 -7
- package/src/auth/auth.test.ts +1 -2
- package/src/auth/bindings.ts +5 -15
- package/src/auth/gate-auth.test.ts +2 -2
- package/src/cli/ai-setup/ai-setup.test.ts +3 -1
- package/src/cli/ai-setup/apply.ts +13 -0
- package/src/cli/build.test.ts +67 -0
- package/src/cli/build.ts +33 -0
- package/src/cli/db-seed.ts +1 -1
- package/src/cli/dev-controls.test.ts +43 -48
- package/src/cli/dev-controls.ts +23 -172
- package/src/cli/dev.test.ts +90 -5
- package/src/cli/dev.ts +39 -16
- package/src/cli/doctor.ts +98 -5
- package/src/cli/load-config.images.test.ts +31 -1
- package/src/cli/load-config.ts +2 -2
- package/src/cli/safe-defaults.test.ts +1 -1
- package/src/client/notes-contract.test.ts +3 -4
- package/src/compiler/aot.test.ts +1 -2
- package/src/compiler/extract.test.ts +76 -21
- package/src/compiler/extract.ts +15 -3
- package/src/compiler/fixtures/raw/raw-unannotated.ts +2 -4
- package/src/compiler/fixtures/skyport/oke.config.ts +4 -2
- package/src/compiler/fixtures/skyport/src/flows/bookings/index.ts +4 -8
- package/src/compiler/fixtures/skyport/src/flows/payments/index.ts +1 -2
- package/src/compiler/fixtures/skyport/src/flows/support/index.ts +1 -2
- package/src/compiler/fixtures/skyport.expected.json +3 -3
- package/src/compiler/fixtures/triggers/five-triggers.ts +5 -10
- package/src/compiler/generate-adopt.test.ts +85 -0
- package/src/compiler/generate-adopt.ts +111 -0
- package/src/config/driver-defaults.test.ts +122 -0
- package/src/config/driver-defaults.ts +186 -0
- package/src/config/index.ts +97 -7
- package/src/console/server/dry-run.audit.test.ts +2 -2
- package/src/console/server/flows.ts +50 -150
- package/src/docker/compose.ts +13 -3
- package/src/docker/images-config.test.ts +168 -0
- package/src/docker/recipes/llama-cpp.ts +10 -1
- package/src/docker/stack-id.ts +1 -0
- package/src/elements/channel/declare.ts +27 -1
- package/src/elements/clock/chaos-child.ts +3 -6
- package/src/elements/clock/chaos.test.ts +1 -2
- package/src/elements/clock.test.ts +2 -4
- package/src/elements/signal/declare.ts +25 -1
- package/src/elements/store/declare.ts +27 -1
- package/src/elements/store/resource.ts +6 -11
- package/src/elements/store/upsert-app.test.ts +1 -2
- package/src/elements/vault/declare.ts +29 -1
- package/src/kernel/adopt-barrel-fresh.test.ts +103 -0
- package/src/kernel/app.ts +66 -9
- package/src/kernel/auto-registry.test.ts +198 -0
- package/src/kernel/boot-bind/channel.ts +12 -4
- package/src/kernel/boot-bind/clock.ts +3 -3
- package/src/kernel/boot-bind/gate.ts +7 -4
- package/src/kernel/boot-bind/honor-config.test.ts +5 -5
- package/src/kernel/boot-bind/journal.ts +3 -3
- package/src/kernel/boot-bind/signal.ts +3 -1
- package/src/kernel/boot-bind/store.test.ts +19 -0
- package/src/kernel/boot-bind/store.ts +24 -9
- package/src/kernel/boot-bind/vault.ts +3 -3
- package/src/kernel/boot.test.ts +19 -15
- package/src/kernel/boot.ts +81 -1
- package/src/kernel/call.test.ts +5 -10
- package/src/kernel/compensate.test.ts +3 -6
- package/src/kernel/concurrency.test.ts +3 -5
- package/src/kernel/correlation.test.ts +4 -8
- package/src/kernel/edge.test.ts +1 -1
- package/src/kernel/effects-stamping.test.ts +4 -7
- package/src/kernel/element-registries.ts +27 -0
- package/src/kernel/errors.ts +11 -0
- package/src/kernel/flow.test.ts +8 -15
- package/src/kernel/flow.ts +12 -14
- package/src/kernel/hook-timing.test.ts +2 -2
- package/src/kernel/hooks.test.ts +5 -10
- package/src/kernel/horizontal-child.ts +5 -10
- package/src/kernel/journal-boot.test.ts +2 -4
- package/src/kernel/pipeline.test.ts +6 -12
- package/src/kernel/plugin/capabilities.test.ts +1 -1
- package/src/kernel/plugin/decorate.test.ts +3 -8
- package/src/kernel/plugin/scoping.test.ts +6 -16
- package/src/kernel/plugin-elements.test.ts +1 -1
- package/src/kernel/plugin-needs.test.ts +1 -1
- package/src/kernel/ready.test.ts +1 -2
- package/src/kernel/registry-isolation.test.ts +5 -5
- package/src/kernel/triggers.ts +1 -1
- package/src/plugins/anonymous.ts +1 -3
- package/src/plugins/compression.test.ts +6 -8
- package/src/plugins/config-source.test.ts +1 -1
- package/src/plugins/config-source.ts +1 -2
- package/src/plugins/cors.test.ts +10 -10
- package/src/plugins/csrf.test.ts +2 -2
- package/src/plugins/headers.test.ts +14 -15
- package/src/plugins/ip-allowlist.test.ts +9 -9
- package/src/plugins/magic-link.ts +2 -6
- package/src/plugins/maintenance-mode.test.ts +6 -6
- package/src/plugins/otp.ts +5 -15
- package/src/plugins/passkey.ts +4 -12
- package/src/plugins/two-factor.ts +3 -9
- package/src/plugins/username.ts +2 -6
- package/src/release/measure.ts +1 -1
- package/src/runs/runs.test.ts +3 -5
- package/src/runtime/serve.test.ts +2 -4
- package/src/test/create-test-app.test.ts +3 -7
- package/src/test/provisions.integration.test.ts +4 -9
- package/src/test/reset-element-registries.ts +31 -0
- package/site/content/docs/console/access.mdx +0 -29
- package/site/content/docs/console/ai.mdx +0 -35
- package/site/content/docs/console/architecture.mdx +0 -35
- package/site/content/docs/console/channels.mdx +0 -37
- package/site/content/docs/console/clock.mdx +0 -51
- package/site/content/docs/console/flows.mdx +0 -31
- package/site/content/docs/console/gates.mdx +0 -73
- package/site/content/docs/console/index.mdx +0 -54
- package/site/content/docs/console/manifest-diff.mdx +0 -34
- package/site/content/docs/console/meta.json +0 -24
- package/site/content/docs/console/overview.mdx +0 -40
- package/site/content/docs/console/plugins.mdx +0 -41
- package/site/content/docs/console/privacy.mdx +0 -32
- package/site/content/docs/console/runs.mdx +0 -50
- package/site/content/docs/console/signals.mdx +0 -40
- package/site/content/docs/console/store.mdx +0 -32
- package/site/content/docs/console/tenancy.mdx +0 -32
- package/site/content/docs/console/traces.mdx +0 -38
- package/site/content/docs/console/vault.mdx +0 -41
package/src/kernel/app.ts
CHANGED
|
@@ -83,6 +83,12 @@ import {
|
|
|
83
83
|
import { releaseInstanceLeases } from "./graceful-shutdown.ts";
|
|
84
84
|
import type { JournalRuntime } from "./boot-bind/journal.ts";
|
|
85
85
|
import { listBindings, resetBindings, type Binding } from "./on.ts";
|
|
86
|
+
import {
|
|
87
|
+
channelTemplateRegistry,
|
|
88
|
+
secretRegistry,
|
|
89
|
+
signalRegistry,
|
|
90
|
+
storeRegistry,
|
|
91
|
+
} from "./element-registries.ts";
|
|
86
92
|
import {
|
|
87
93
|
applyPrincipal,
|
|
88
94
|
createElementPipelineHooks,
|
|
@@ -442,6 +448,21 @@ export interface OkeApp<D extends Record<string, unknown> = {}, R extends AppRou
|
|
|
442
448
|
readonly $options: OkeOptions;
|
|
443
449
|
}
|
|
444
450
|
|
|
451
|
+
/**
|
|
452
|
+
* Concat two decl lists, skipping registry entries already present by
|
|
453
|
+
* reference (the same call-site object explicitly re-passed).
|
|
454
|
+
*
|
|
455
|
+
* @param explicit - Hand-passed decls (kept in order, always wins position)
|
|
456
|
+
* @param fromRegistry - Auto-drained decls to append
|
|
457
|
+
*/
|
|
458
|
+
function mergeUnique<T>(explicit: readonly T[] | undefined, fromRegistry: readonly T[]): T[] {
|
|
459
|
+
const out: T[] = [...(explicit ?? [])];
|
|
460
|
+
for (const item of fromRegistry) {
|
|
461
|
+
if (!out.includes(item)) out.push(item);
|
|
462
|
+
}
|
|
463
|
+
return out;
|
|
464
|
+
}
|
|
465
|
+
|
|
445
466
|
/**
|
|
446
467
|
* Create an application. Adopts bindings registered via {@link on}.
|
|
447
468
|
*
|
|
@@ -456,6 +477,42 @@ export function oke(options: OkeOptions): OkeApp {
|
|
|
456
477
|
: [...listBindings(), ...(options.bindings ?? [])];
|
|
457
478
|
if (registry === "consume") resetBindings();
|
|
458
479
|
|
|
480
|
+
// Same registry mode drains store.sql/store.files, vault.secret, signal(),
|
|
481
|
+
// and channel.<medium>().template() — module-evaluation registries that
|
|
482
|
+
// mirror `on`'s trigger drain (`listBindings`/`resetBindings` above).
|
|
483
|
+
// Explicit `options.stores` / `secrets` / `signals` / `channel.templates`
|
|
484
|
+
// are additive, never silently ignored — deduped by reference so an
|
|
485
|
+
// explicitly-passed decl that is also in the registry is not doubled.
|
|
486
|
+
const registrySnapshot =
|
|
487
|
+
registry === "ignore"
|
|
488
|
+
? { stores: [], secrets: [], signals: [], channelTemplates: [] }
|
|
489
|
+
: {
|
|
490
|
+
stores: storeRegistry.slice(),
|
|
491
|
+
secrets: secretRegistry.slice(),
|
|
492
|
+
signals: signalRegistry.slice(),
|
|
493
|
+
channelTemplates: channelTemplateRegistry.slice(),
|
|
494
|
+
};
|
|
495
|
+
if (registry === "consume") {
|
|
496
|
+
storeRegistry.length = 0;
|
|
497
|
+
secretRegistry.length = 0;
|
|
498
|
+
signalRegistry.length = 0;
|
|
499
|
+
channelTemplateRegistry.length = 0;
|
|
500
|
+
}
|
|
501
|
+
const effectiveStores = mergeUnique(options.stores, registrySnapshot.stores);
|
|
502
|
+
const effectiveSecrets = mergeUnique(options.secrets, registrySnapshot.secrets);
|
|
503
|
+
const effectiveSignals = mergeUnique(options.signals, registrySnapshot.signals);
|
|
504
|
+
// Stay `undefined` (never a defined-but-empty object) when neither an
|
|
505
|
+
// explicit `options.channel` nor a registered template exists — a defined
|
|
506
|
+
// object here would flip `resolveElementNeeds`'s `channel` need to `true`
|
|
507
|
+
// for every app, whether or not it uses channel at all.
|
|
508
|
+
const effectiveChannel: BootOptions["channel"] =
|
|
509
|
+
options.channel === undefined && registrySnapshot.channelTemplates.length === 0
|
|
510
|
+
? undefined
|
|
511
|
+
: {
|
|
512
|
+
...(options.channel ?? {}),
|
|
513
|
+
templates: mergeUnique(options.channel?.templates, registrySnapshot.channelTemplates),
|
|
514
|
+
};
|
|
515
|
+
|
|
459
516
|
// Resolve Gate bag early so auth HTTP Bindings join `adopted` + the router
|
|
460
517
|
// before posture audit (same ensureBoot → doBoot path — never a side channel).
|
|
461
518
|
const gateConfig: ResolvedGateConfig = resolveGateConfig({
|
|
@@ -772,13 +829,13 @@ export function oke(options: OkeOptions): OkeApp {
|
|
|
772
829
|
docker: overrides?.docker ?? options.docker,
|
|
773
830
|
config: overrides?.config ?? options.config,
|
|
774
831
|
elements: overrides?.elements ?? options.elements,
|
|
775
|
-
secrets: overrides?.secrets ??
|
|
832
|
+
secrets: overrides?.secrets ?? effectiveSecrets,
|
|
776
833
|
vault: overrides?.vault ?? options.vault,
|
|
777
834
|
gates: [...baseGates, ...authGates],
|
|
778
|
-
signals: overrides?.signals ??
|
|
835
|
+
signals: overrides?.signals ?? effectiveSignals,
|
|
779
836
|
clocks: overrides?.clocks ?? options.clocks,
|
|
780
|
-
stores: overrides?.stores ??
|
|
781
|
-
channel: overrides?.channel ??
|
|
837
|
+
stores: overrides?.stores ?? effectiveStores,
|
|
838
|
+
channel: overrides?.channel ?? effectiveChannel,
|
|
782
839
|
ai: overrides?.ai ?? options.ai,
|
|
783
840
|
runs: overrides?.runs ?? options.runs,
|
|
784
841
|
bindings: adopted,
|
|
@@ -790,7 +847,7 @@ export function oke(options: OkeOptions): OkeApp {
|
|
|
790
847
|
if (d.startsWith("driver:")) driverIds.push(d.slice("driver:".length));
|
|
791
848
|
}
|
|
792
849
|
}
|
|
793
|
-
const stores = overrides?.stores ??
|
|
850
|
+
const stores = overrides?.stores ?? effectiveStores;
|
|
794
851
|
const available = buildAvailableNeedTokens({
|
|
795
852
|
elements: {
|
|
796
853
|
storeSql:
|
|
@@ -829,10 +886,10 @@ export function oke(options: OkeOptions): OkeApp {
|
|
|
829
886
|
if (gateConfig.auth) available.add("auth");
|
|
830
887
|
assertPluginNeeds(caps, { pluginNames, available });
|
|
831
888
|
|
|
832
|
-
const baseSecrets = overrides?.secrets ??
|
|
833
|
-
const baseSignals = overrides?.signals ??
|
|
889
|
+
const baseSecrets = overrides?.secrets ?? effectiveSecrets;
|
|
890
|
+
const baseSignals = overrides?.signals ?? effectiveSignals;
|
|
834
891
|
const baseClocks = overrides?.clocks ?? options.clocks ?? [];
|
|
835
|
-
const baseChannel = overrides?.channel ??
|
|
892
|
+
const baseChannel = overrides?.channel ?? effectiveChannel;
|
|
836
893
|
const mergedCatalog = mergeTemplateCatalogs(baseChannel?.catalog, ...pluginChannelCatalogs);
|
|
837
894
|
|
|
838
895
|
const merged: BootOptions = {
|
|
@@ -848,7 +905,7 @@ export function oke(options: OkeOptions): OkeApp {
|
|
|
848
905
|
unguardedHttp,
|
|
849
906
|
signals: [...baseSignals, ...pluginSignals],
|
|
850
907
|
clocks: [...baseClocks, ...pluginClocks],
|
|
851
|
-
stores: overrides?.stores ??
|
|
908
|
+
stores: overrides?.stores ?? effectiveStores,
|
|
852
909
|
channel: {
|
|
853
910
|
...(baseChannel ?? {}),
|
|
854
911
|
templates: [...(baseChannel?.templates ?? []), ...pluginChannelTemplates],
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `oke({...})` auto-populates `stores` / `secrets` / `signals` /
|
|
3
|
+
* `channel.templates` from the same kind of module-evaluation registry
|
|
4
|
+
* already proven for `on()`'s trigger drain (`registry-isolation.test.ts`).
|
|
5
|
+
*
|
|
6
|
+
* `store.sql` / `store.files`, `vault.secret`, `signal()`, and medium-binder
|
|
7
|
+
* `.template()` push into a shared registry (`src/kernel/element-registries.ts`)
|
|
8
|
+
* at call time — zero change to how developers call them. `oke()` drains it
|
|
9
|
+
* at construction under the same `registry: "consume" | "keep" | "ignore"`
|
|
10
|
+
* switch that already governs bindings.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { describe, expect, test } from "bun:test";
|
|
14
|
+
import { channel } from "../elements/channel/declare.ts";
|
|
15
|
+
import { signal } from "../elements/signal/declare.ts";
|
|
16
|
+
import { store } from "../elements/store/declare.ts";
|
|
17
|
+
import { vault } from "../elements/vault/declare.ts";
|
|
18
|
+
import { oke } from "./app.ts";
|
|
19
|
+
import { flow } from "./flow.ts";
|
|
20
|
+
import { on, resetBindings } from "./on.ts";
|
|
21
|
+
import { http } from "./triggers.ts";
|
|
22
|
+
|
|
23
|
+
describe("oke() auto-registry — stores/secrets/signals/channel.templates", () => {
|
|
24
|
+
test("before/after: zero explicit arrays boots identically to the explicit-array form", async () => {
|
|
25
|
+
resetBindings();
|
|
26
|
+
|
|
27
|
+
// "before" — today's explicit-array form (registry: "ignore" so this
|
|
28
|
+
// app cannot see anything auto-drained; proves the old path is unchanged).
|
|
29
|
+
const dbBefore = store.sql("before-app", { schema: {} });
|
|
30
|
+
const filesBefore = store.files("before-uploads");
|
|
31
|
+
const secretBefore = vault.secret("BEFORE_WEBHOOK_SECRET", { dev: "dev-secret-before" });
|
|
32
|
+
const signalBefore = signal("before-note-created", { delivery: "once", optional: true });
|
|
33
|
+
const mailBefore = channel.email({ from: "Before <before@localhost>" });
|
|
34
|
+
const templateBefore = mailBefore.template("before-note-created");
|
|
35
|
+
|
|
36
|
+
const beforeFlow = flow("before.onCreated", {
|
|
37
|
+
effects: {
|
|
38
|
+
secrets: ["BEFORE_WEBHOOK_SECRET"],
|
|
39
|
+
emits: ["before-note-created"],
|
|
40
|
+
sends: ["before-note-created"],
|
|
41
|
+
},
|
|
42
|
+
do: async (_input, fx) => {
|
|
43
|
+
const secret = fx.vault(secretBefore);
|
|
44
|
+
await fx.emit(signalBefore, {});
|
|
45
|
+
await fx.send(templateBefore, { to: "you@localhost", data: {} });
|
|
46
|
+
return { secret };
|
|
47
|
+
},
|
|
48
|
+
});
|
|
49
|
+
on(http.post("/before"), beforeFlow);
|
|
50
|
+
|
|
51
|
+
const appBefore = oke({
|
|
52
|
+
name: "before-app",
|
|
53
|
+
registry: "ignore",
|
|
54
|
+
bindings: [{ trigger: http.post("/before"), flow: beforeFlow }],
|
|
55
|
+
gate: { unguardedHttp: "allow" },
|
|
56
|
+
vault: { allowDevFallbacks: true },
|
|
57
|
+
stores: [dbBefore, filesBefore],
|
|
58
|
+
secrets: [secretBefore],
|
|
59
|
+
signals: [signalBefore],
|
|
60
|
+
channel: { templates: [templateBefore] },
|
|
61
|
+
startScheduler: false,
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
// "after" — same shape, zero explicit arrays; registry auto-populates.
|
|
65
|
+
// Declared (and registered) for their side effect only — never passed
|
|
66
|
+
// to `oke()` directly, proving the registry alone carries them.
|
|
67
|
+
store.sql("after-app", { schema: {} });
|
|
68
|
+
store.files("after-uploads");
|
|
69
|
+
const secretAfter = vault.secret("AFTER_WEBHOOK_SECRET", { dev: "dev-secret-after" });
|
|
70
|
+
const signalAfter = signal("after-note-created", { delivery: "once", optional: true });
|
|
71
|
+
const mailAfter = channel.email({ from: "After <after@localhost>" });
|
|
72
|
+
const templateAfter = mailAfter.template("after-note-created");
|
|
73
|
+
|
|
74
|
+
const afterFlow = flow("after.onCreated", {
|
|
75
|
+
effects: {
|
|
76
|
+
secrets: ["AFTER_WEBHOOK_SECRET"],
|
|
77
|
+
emits: ["after-note-created"],
|
|
78
|
+
sends: ["after-note-created"],
|
|
79
|
+
},
|
|
80
|
+
do: async (_input, fx) => {
|
|
81
|
+
const secret = fx.vault(secretAfter);
|
|
82
|
+
await fx.emit(signalAfter, {});
|
|
83
|
+
await fx.send(templateAfter, { to: "you@localhost", data: {} });
|
|
84
|
+
return { secret };
|
|
85
|
+
},
|
|
86
|
+
});
|
|
87
|
+
resetBindings();
|
|
88
|
+
on(http.post("/after"), afterFlow);
|
|
89
|
+
|
|
90
|
+
const appAfter = oke({
|
|
91
|
+
name: "after-app",
|
|
92
|
+
gate: { unguardedHttp: "allow" },
|
|
93
|
+
vault: { allowDevFallbacks: true },
|
|
94
|
+
startScheduler: false,
|
|
95
|
+
// No stores / secrets / signals / channel — registry supplies them.
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
await appBefore.boot({ env: "test", unguardedHttp: "allow" });
|
|
99
|
+
await appAfter.boot({ env: "test", unguardedHttp: "allow" });
|
|
100
|
+
|
|
101
|
+
// Both boots resolved the same shape from stores → secrets → signals → channel.
|
|
102
|
+
expect(appBefore.bootResult?.store?.declarations.has("sql:before-app")).toBe(true);
|
|
103
|
+
expect(appBefore.bootResult?.store?.declarations.has("files:before-uploads")).toBe(true);
|
|
104
|
+
expect(appAfter.bootResult?.store?.declarations.has("sql:after-app")).toBe(true);
|
|
105
|
+
expect(appAfter.bootResult?.store?.declarations.has("files:after-uploads")).toBe(true);
|
|
106
|
+
|
|
107
|
+
expect(appBefore.bootResult?.vault?.contracts.has("BEFORE_WEBHOOK_SECRET")).toBe(true);
|
|
108
|
+
expect(appAfter.bootResult?.vault?.contracts.has("AFTER_WEBHOOK_SECRET")).toBe(true);
|
|
109
|
+
expect(appBefore.bootResult?.vault?.read("BEFORE_WEBHOOK_SECRET")).toBe("dev-secret-before");
|
|
110
|
+
expect(appAfter.bootResult?.vault?.read("AFTER_WEBHOOK_SECRET")).toBe("dev-secret-after");
|
|
111
|
+
|
|
112
|
+
expect(appBefore.bootResult?.signal?.declarations.has("before-note-created")).toBe(true);
|
|
113
|
+
expect(appAfter.bootResult?.signal?.declarations.has("after-note-created")).toBe(true);
|
|
114
|
+
|
|
115
|
+
expect(appBefore.bootResult?.channel?.templates.has("before-note-created")).toBe(true);
|
|
116
|
+
expect(appAfter.bootResult?.channel?.templates.has("after-note-created")).toBe(true);
|
|
117
|
+
|
|
118
|
+
// Same flow execution shape through the real HTTP path either way —
|
|
119
|
+
// both resolve, emit, and send successfully; the secret value itself
|
|
120
|
+
// comes back redacted (fx output redaction), same on both sides.
|
|
121
|
+
const resBefore = await appBefore.fetch(
|
|
122
|
+
new Request("http://localhost/before", { method: "POST" }),
|
|
123
|
+
);
|
|
124
|
+
const resAfter = await appAfter.fetch(
|
|
125
|
+
new Request("http://localhost/after", { method: "POST" }),
|
|
126
|
+
);
|
|
127
|
+
expect(resBefore.status).toBe(200);
|
|
128
|
+
expect(resAfter.status).toBe(200);
|
|
129
|
+
const bodyBefore = (await resBefore.json()) as { data: { secret: string } };
|
|
130
|
+
const bodyAfter = (await resAfter.json()) as { data: { secret: string } };
|
|
131
|
+
expect(bodyBefore.data.secret).toBe(bodyAfter.data.secret);
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
test("explicit passing is additive, never silently ignored — same object via both paths is deduped, not doubled", async () => {
|
|
135
|
+
const db = store.sql("dedup-app", { schema: {} });
|
|
136
|
+
|
|
137
|
+
// `db` is already sitting in the registry from the call above *and*
|
|
138
|
+
// passed explicitly — must not double-register / conflict.
|
|
139
|
+
const app = oke({
|
|
140
|
+
name: "dedup-app",
|
|
141
|
+
autoBoot: false,
|
|
142
|
+
stores: [db],
|
|
143
|
+
startScheduler: false,
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
await app.boot({ env: "test", unguardedHttp: "allow" });
|
|
147
|
+
expect(app.bootResult?.store?.declarations.has("sql:dedup-app")).toBe(true);
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
test('registry: "ignore" does not auto-populate', async () => {
|
|
151
|
+
vault.secret("IGNORED_SECRET", { description: "never passed, never auto-read" });
|
|
152
|
+
|
|
153
|
+
const app = oke({
|
|
154
|
+
name: "ignore-mode-app",
|
|
155
|
+
registry: "ignore",
|
|
156
|
+
startScheduler: false,
|
|
157
|
+
});
|
|
158
|
+
// Nothing requires the secret, so boot succeeds — but it must not have
|
|
159
|
+
// silently picked up IGNORED_SECRET from the registry either way.
|
|
160
|
+
await app.boot({ env: "test", unguardedHttp: "allow" });
|
|
161
|
+
expect(app.bootResult?.vault?.contracts.has("IGNORED_SECRET")).toBeFalsy();
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
test('registry: "consume" (default) drains stores/secrets/signals/channel.templates — a later app does not inherit them', async () => {
|
|
165
|
+
store.sql("leak-app", { schema: {} });
|
|
166
|
+
vault.secret("LEAK_SECRET", { dev: "dev-leak" });
|
|
167
|
+
signal("leak-signal", { delivery: "once" });
|
|
168
|
+
channel.email().template("leak-template");
|
|
169
|
+
|
|
170
|
+
const appA = oke({
|
|
171
|
+
name: "leak-app-a",
|
|
172
|
+
autoBoot: false,
|
|
173
|
+
vault: { allowDevFallbacks: true },
|
|
174
|
+
startScheduler: false,
|
|
175
|
+
});
|
|
176
|
+
await appA.boot({ env: "test", unguardedHttp: "allow" });
|
|
177
|
+
expect(appA.bootResult?.store?.declarations.has("sql:leak-app")).toBe(true);
|
|
178
|
+
expect(appA.bootResult?.vault?.contracts.has("LEAK_SECRET")).toBe(true);
|
|
179
|
+
|
|
180
|
+
// Second app, constructed after the first drained the registry.
|
|
181
|
+
const appB = oke({
|
|
182
|
+
name: "leak-app-b",
|
|
183
|
+
autoBoot: false,
|
|
184
|
+
startScheduler: false,
|
|
185
|
+
});
|
|
186
|
+
await appB.boot({ env: "test", unguardedHttp: "allow" });
|
|
187
|
+
// Nothing was passed and nothing was left in the registry (App A drained
|
|
188
|
+
// it) — App B needed store/vault/signal not at all, so no runtime was
|
|
189
|
+
// built for them. Channel boots unconditionally regardless of registry
|
|
190
|
+
// content (pre-existing, unrelated to this feature — `resolveElementNeeds`
|
|
191
|
+
// gates channel on object *presence*, not template count) — assert its
|
|
192
|
+
// template map, not runtime absence.
|
|
193
|
+
expect(appB.bootResult?.store).toBeUndefined();
|
|
194
|
+
expect(appB.bootResult?.vault).toBeUndefined();
|
|
195
|
+
expect(appB.bootResult?.signal).toBeUndefined();
|
|
196
|
+
expect(appB.bootResult?.channel?.templates.has("leak-template") ?? false).toBe(false);
|
|
197
|
+
});
|
|
198
|
+
});
|
|
@@ -3,6 +3,11 @@
|
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
5
|
import { resolveDriverId, type ConfigEnv } from "../../config/index.ts";
|
|
6
|
+
import {
|
|
7
|
+
CHANNEL_EMAIL_DEFAULTS,
|
|
8
|
+
CHANNEL_SMS_DEFAULTS,
|
|
9
|
+
dockerFlagDefaults,
|
|
10
|
+
} from "../../config/driver-defaults.ts";
|
|
6
11
|
import { openConsoleChannel } from "../../drivers/channel-console.ts";
|
|
7
12
|
import { openMsegatChannel } from "../../drivers/channel-msegat.ts";
|
|
8
13
|
import { openResendChannel } from "../../drivers/channel-resend.ts";
|
|
@@ -81,9 +86,12 @@ export function resolveEmailDriverId(
|
|
|
81
86
|
env: ConfigEnv,
|
|
82
87
|
docker: boolean,
|
|
83
88
|
): string {
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
89
|
+
// Defaults cover every ConfigEnv key, so this is never undefined.
|
|
90
|
+
return resolveDriverId(
|
|
91
|
+
options.config?.drivers?.channel?.email,
|
|
92
|
+
env,
|
|
93
|
+
dockerFlagDefaults(CHANNEL_EMAIL_DEFAULTS, docker),
|
|
94
|
+
)!;
|
|
87
95
|
}
|
|
88
96
|
|
|
89
97
|
/**
|
|
@@ -93,7 +101,7 @@ export function resolveEmailDriverId(
|
|
|
93
101
|
* @param env - Active environment
|
|
94
102
|
*/
|
|
95
103
|
export function resolveSmsDriverId(options: BootOptions, env: ConfigEnv): string | undefined {
|
|
96
|
-
return resolveDriverId(options.config?.drivers?.channel?.sms, env);
|
|
104
|
+
return resolveDriverId(options.config?.drivers?.channel?.sms, env, CHANNEL_SMS_DEFAULTS);
|
|
97
105
|
}
|
|
98
106
|
|
|
99
107
|
/**
|
|
@@ -15,6 +15,7 @@ import {
|
|
|
15
15
|
} from "../../elements/clock.ts";
|
|
16
16
|
import { createPostgresCronStore } from "../../drivers/clock-postgres.ts";
|
|
17
17
|
import { resolveDriverId, type ConfigEnv } from "../../config/index.ts";
|
|
18
|
+
import { CLOCK_DEFAULTS } from "../../config/driver-defaults.ts";
|
|
18
19
|
import type { BootOptions } from "../boot.ts";
|
|
19
20
|
|
|
20
21
|
/** Result of binding a clock runtime. */
|
|
@@ -33,9 +34,8 @@ export const DEFAULT_FILE_CRON_PATH = ".oke/crons.json";
|
|
|
33
34
|
* @param env - Active environment
|
|
34
35
|
*/
|
|
35
36
|
export function resolveClockDriverId(options: BootOptions, env: ConfigEnv): string {
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
return env === "test" ? "frozen" : "memory";
|
|
37
|
+
// Defaults cover every ConfigEnv key, so this is never undefined.
|
|
38
|
+
return resolveDriverId(options.config?.drivers?.clock, env, CLOCK_DEFAULTS)!;
|
|
39
39
|
}
|
|
40
40
|
|
|
41
41
|
/**
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
5
|
import { resolveDriverId, type ConfigEnv } from "../../config/index.ts";
|
|
6
|
+
import { dockerFlagDefaults, STORE_KV_DEFAULTS } from "../../config/driver-defaults.ts";
|
|
6
7
|
import { memoryDrivers } from "../../drivers/memory.ts";
|
|
7
8
|
import { redisDriver } from "../../drivers/redis.ts";
|
|
8
9
|
import type { KvClientLike } from "../../drivers/types.ts";
|
|
@@ -27,10 +28,12 @@ export function resolveGateKvDriverId(
|
|
|
27
28
|
const fromEnv = process.env.OKE_KV_DRIVER?.trim();
|
|
28
29
|
if (docker && fromEnv === "redis") return "redis";
|
|
29
30
|
if (docker && fromEnv === "memory") return "memory";
|
|
30
|
-
const resolved = resolveDriverId(
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
31
|
+
const resolved = resolveDriverId(
|
|
32
|
+
options.config?.drivers?.store?.kv,
|
|
33
|
+
env,
|
|
34
|
+
dockerFlagDefaults(STORE_KV_DEFAULTS, docker),
|
|
35
|
+
);
|
|
36
|
+
return resolved === "redis" ? "redis" : "memory";
|
|
34
37
|
}
|
|
35
38
|
|
|
36
39
|
function kvUrlFor(docker: boolean): string {
|
|
@@ -157,7 +157,7 @@ describe("boot binders honour drivers.* config", () => {
|
|
|
157
157
|
const result = await bootApplication({
|
|
158
158
|
env: "local",
|
|
159
159
|
startScheduler: false,
|
|
160
|
-
flows: [flow(
|
|
160
|
+
flows: [flow("charge", { durable: true, do: () => ({ ok: true }) })],
|
|
161
161
|
});
|
|
162
162
|
try {
|
|
163
163
|
expect(result.journal?.driverId).toBe("memory");
|
|
@@ -172,7 +172,7 @@ describe("boot binders honour drivers.* config", () => {
|
|
|
172
172
|
const result = await bootApplication({
|
|
173
173
|
env: "local",
|
|
174
174
|
startScheduler: false,
|
|
175
|
-
flows: [flow(
|
|
175
|
+
flows: [flow("plain", { do: () => ({ ok: true }) })],
|
|
176
176
|
});
|
|
177
177
|
try {
|
|
178
178
|
expect(result.journal).toBeUndefined();
|
|
@@ -187,7 +187,7 @@ describe("boot binders honour drivers.* config", () => {
|
|
|
187
187
|
const result = await bootApplication({
|
|
188
188
|
env: "local",
|
|
189
189
|
startScheduler: false,
|
|
190
|
-
flows: [flow(
|
|
190
|
+
flows: [flow("charge", { durable: true, do: () => ({ ok: true }) })],
|
|
191
191
|
config: {
|
|
192
192
|
drivers: {
|
|
193
193
|
journal: { local: "file", docker: "postgres", test: "memory", prod: "postgres" },
|
|
@@ -211,7 +211,7 @@ describe("boot binders honour drivers.* config", () => {
|
|
|
211
211
|
bootApplication({
|
|
212
212
|
env: "local",
|
|
213
213
|
startScheduler: false,
|
|
214
|
-
flows: [flow(
|
|
214
|
+
flows: [flow("charge", { durable: true, do: () => ({ ok: true }) })],
|
|
215
215
|
config: {
|
|
216
216
|
drivers: {
|
|
217
217
|
journal: { local: "postgres" },
|
|
@@ -232,7 +232,7 @@ describe("boot binders honour drivers.* config", () => {
|
|
|
232
232
|
bootApplication({
|
|
233
233
|
env: "local",
|
|
234
234
|
startScheduler: false,
|
|
235
|
-
flows: [flow(
|
|
235
|
+
flows: [flow("charge", { durable: true, do: () => ({ ok: true }) })],
|
|
236
236
|
config: {
|
|
237
237
|
drivers: {
|
|
238
238
|
journal: { local: "neon" },
|
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
} from "../journal.ts";
|
|
13
13
|
import { createPostgresJournalStore } from "../../drivers/journal-postgres.ts";
|
|
14
14
|
import { resolveDriverId, type ConfigEnv } from "../../config/index.ts";
|
|
15
|
+
import { JOURNAL_DEFAULTS } from "../../config/driver-defaults.ts";
|
|
15
16
|
import type { BootOptions } from "../boot.ts";
|
|
16
17
|
|
|
17
18
|
/** Bound journal runtime — store + this instance's lease identity. */
|
|
@@ -37,9 +38,8 @@ export const DEFAULT_FILE_JOURNAL_PATH = ".oke/journal.json";
|
|
|
37
38
|
* @param env - Active environment
|
|
38
39
|
*/
|
|
39
40
|
export function resolveJournalDriverId(options: BootOptions, env: ConfigEnv): string {
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
return "memory";
|
|
41
|
+
// Defaults cover every ConfigEnv key, so this is never undefined.
|
|
42
|
+
return resolveDriverId(options.config?.drivers?.journal, env, JOURNAL_DEFAULTS)!;
|
|
43
43
|
}
|
|
44
44
|
|
|
45
45
|
/**
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
5
|
import { resolveDriverId, type ConfigEnv } from "../../config/index.ts";
|
|
6
|
+
import { SIGNAL_DEFAULTS } from "../../config/driver-defaults.ts";
|
|
6
7
|
import { memorySignalDriver } from "../../drivers/signal-memory.ts";
|
|
7
8
|
import { createBunSignalRedisClient, redisSignalDriver } from "../../drivers/signal-redis.ts";
|
|
8
9
|
import type { SignalRedisClientLike } from "../../drivers/signal-types.ts";
|
|
@@ -17,7 +18,8 @@ import type { BootOptions } from "../boot.ts";
|
|
|
17
18
|
* @param env - Active environment
|
|
18
19
|
*/
|
|
19
20
|
export function resolveSignalDriverId(options: BootOptions, env: ConfigEnv): string {
|
|
20
|
-
|
|
21
|
+
// Defaults cover every ConfigEnv key, so this is never undefined.
|
|
22
|
+
return resolveDriverId(options.config?.drivers?.signal, env, SIGNAL_DEFAULTS)!;
|
|
21
23
|
}
|
|
22
24
|
|
|
23
25
|
function redisUrlFor(docker: boolean): string | undefined {
|
|
@@ -78,6 +78,25 @@ describe("bindStore driver resolution", () => {
|
|
|
78
78
|
expect(resolveKvDriverId({}, "docker", true)).toBe("redis");
|
|
79
79
|
expect(resolveFilesDriverId({}, "docker", true)).toBe("s3");
|
|
80
80
|
});
|
|
81
|
+
|
|
82
|
+
test("pinning only `local` never leaks into docker/test/prod — each keeps its real default", () => {
|
|
83
|
+
const options = {
|
|
84
|
+
config: {
|
|
85
|
+
drivers: {
|
|
86
|
+
store: {
|
|
87
|
+
sql: { local: "pglite" },
|
|
88
|
+
},
|
|
89
|
+
},
|
|
90
|
+
},
|
|
91
|
+
};
|
|
92
|
+
expect(resolveSqlDriverId(options, "local", false)).toBe("pglite");
|
|
93
|
+
expect(resolveSqlDriverId(options, "docker", true)).toBe("postgres");
|
|
94
|
+
expect(resolveSqlDriverId(options, "test", false)).toBe("memory");
|
|
95
|
+
expect(resolveSqlDriverId(options, "prod", true)).toBe("postgres");
|
|
96
|
+
// kv / files are untouched by the sql-only override.
|
|
97
|
+
expect(resolveKvDriverId(options, "local", false)).toBe("memory");
|
|
98
|
+
expect(resolveKvDriverId(options, "docker", true)).toBe("redis");
|
|
99
|
+
});
|
|
81
100
|
});
|
|
82
101
|
|
|
83
102
|
describe("bindStore files fs multi-instance warn", () => {
|
|
@@ -3,6 +3,12 @@
|
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
5
|
import { resolveDomainDdlMode, resolveDriverId, type ConfigEnv } from "../../config/index.ts";
|
|
6
|
+
import {
|
|
7
|
+
dockerFlagDefaults,
|
|
8
|
+
STORE_FILES_DEFAULTS,
|
|
9
|
+
STORE_KV_DEFAULTS,
|
|
10
|
+
STORE_SQL_DEFAULTS,
|
|
11
|
+
} from "../../config/driver-defaults.ts";
|
|
6
12
|
import { fsDriver } from "../../drivers/fs.ts";
|
|
7
13
|
import { libsqlDriver, libsqlIndexDriver } from "../../drivers/libsql.ts";
|
|
8
14
|
import { meilisearchDriver } from "../../drivers/meilisearch.ts";
|
|
@@ -138,9 +144,12 @@ function resolveAutoPush(options: BootOptions): boolean {
|
|
|
138
144
|
export function resolveSqlDriverId(options: BootOptions, env: ConfigEnv, docker: boolean): string {
|
|
139
145
|
const fromEnv = process.env.OKE_SQL_DRIVER?.trim();
|
|
140
146
|
if (docker && fromEnv) return fromEnv;
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
147
|
+
// Defaults cover every ConfigEnv key, so this is never undefined.
|
|
148
|
+
return resolveDriverId(
|
|
149
|
+
options.config?.drivers?.store?.sql,
|
|
150
|
+
env,
|
|
151
|
+
dockerFlagDefaults(STORE_SQL_DEFAULTS, docker),
|
|
152
|
+
)!;
|
|
144
153
|
}
|
|
145
154
|
|
|
146
155
|
/**
|
|
@@ -151,9 +160,12 @@ export function resolveSqlDriverId(options: BootOptions, env: ConfigEnv, docker:
|
|
|
151
160
|
export function resolveKvDriverId(options: BootOptions, env: ConfigEnv, docker: boolean): string {
|
|
152
161
|
const fromEnv = process.env.OKE_KV_DRIVER?.trim();
|
|
153
162
|
if (docker && fromEnv) return fromEnv;
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
163
|
+
// Defaults cover every ConfigEnv key, so this is never undefined.
|
|
164
|
+
return resolveDriverId(
|
|
165
|
+
options.config?.drivers?.store?.kv,
|
|
166
|
+
env,
|
|
167
|
+
dockerFlagDefaults(STORE_KV_DEFAULTS, docker),
|
|
168
|
+
)!;
|
|
157
169
|
}
|
|
158
170
|
|
|
159
171
|
/**
|
|
@@ -168,9 +180,12 @@ export function resolveFilesDriverId(
|
|
|
168
180
|
): string {
|
|
169
181
|
const fromEnv = process.env.OKE_FILES_DRIVER?.trim();
|
|
170
182
|
if (docker && fromEnv) return fromEnv;
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
183
|
+
// Defaults cover every ConfigEnv key, so this is never undefined.
|
|
184
|
+
return resolveDriverId(
|
|
185
|
+
options.config?.drivers?.store?.files,
|
|
186
|
+
env,
|
|
187
|
+
dockerFlagDefaults(STORE_FILES_DEFAULTS, docker),
|
|
188
|
+
)!;
|
|
174
189
|
}
|
|
175
190
|
|
|
176
191
|
/**
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
5
|
import { resolveDriverId, type ConfigEnv } from "../../config/index.ts";
|
|
6
|
+
import { VAULT_DEFAULTS } from "../../config/driver-defaults.ts";
|
|
6
7
|
import { createVaultRuntime, type VaultRuntime } from "../../elements/vault.ts";
|
|
7
8
|
import { buildVaultBootChain, normalizeVaultDriverId } from "../../elements/vault/boot-chain.ts";
|
|
8
9
|
import type { BootOptions } from "../boot.ts";
|
|
@@ -14,9 +15,8 @@ import type { BootOptions } from "../boot.ts";
|
|
|
14
15
|
* @param env - Active environment
|
|
15
16
|
*/
|
|
16
17
|
export function resolveVaultDriverId(options: BootOptions, env: ConfigEnv): string {
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
return env === "test" ? "memory" : "env";
|
|
18
|
+
// Defaults cover every ConfigEnv key, so this is never undefined.
|
|
19
|
+
return resolveDriverId(options.config?.drivers?.vault, env, VAULT_DEFAULTS)!;
|
|
20
20
|
}
|
|
21
21
|
|
|
22
22
|
/**
|