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.
Files changed (141) hide show
  1. package/README.md +1 -1
  2. package/package.json +1 -1
  3. package/site/content/docs/ai/mcp.mdx +0 -1
  4. package/site/content/docs/elements/ai.mdx +0 -6
  5. package/site/content/docs/elements/channel.mdx +0 -6
  6. package/site/content/docs/elements/clock.mdx +2 -8
  7. package/site/content/docs/elements/flow.mdx +10 -17
  8. package/site/content/docs/elements/gate.mdx +4 -10
  9. package/site/content/docs/elements/signal.mdx +4 -13
  10. package/site/content/docs/elements/store.mdx +9 -16
  11. package/site/content/docs/elements/vault.mdx +1 -7
  12. package/site/content/docs/get-started/basic-usage.mdx +1 -9
  13. package/site/content/docs/get-started/introduction.mdx +1 -1
  14. package/site/content/docs/get-started/why.mdx +1 -2
  15. package/site/content/docs/index.mdx +0 -1
  16. package/site/content/docs/meta.json +0 -1
  17. package/site/content/docs/reference/cli.md +12 -12
  18. package/site/content/docs/reference/fx.mdx +6 -7
  19. package/src/auth/auth.test.ts +1 -2
  20. package/src/auth/bindings.ts +5 -15
  21. package/src/auth/gate-auth.test.ts +2 -2
  22. package/src/cli/ai-setup/ai-setup.test.ts +3 -1
  23. package/src/cli/ai-setup/apply.ts +13 -0
  24. package/src/cli/build.test.ts +67 -0
  25. package/src/cli/build.ts +33 -0
  26. package/src/cli/db-seed.ts +1 -1
  27. package/src/cli/dev-controls.test.ts +43 -48
  28. package/src/cli/dev-controls.ts +23 -172
  29. package/src/cli/dev.test.ts +90 -5
  30. package/src/cli/dev.ts +39 -16
  31. package/src/cli/doctor.ts +98 -5
  32. package/src/cli/load-config.images.test.ts +31 -1
  33. package/src/cli/load-config.ts +2 -2
  34. package/src/cli/safe-defaults.test.ts +1 -1
  35. package/src/client/notes-contract.test.ts +3 -4
  36. package/src/compiler/aot.test.ts +1 -2
  37. package/src/compiler/extract.test.ts +76 -21
  38. package/src/compiler/extract.ts +15 -3
  39. package/src/compiler/fixtures/raw/raw-unannotated.ts +2 -4
  40. package/src/compiler/fixtures/skyport/oke.config.ts +4 -2
  41. package/src/compiler/fixtures/skyport/src/flows/bookings/index.ts +4 -8
  42. package/src/compiler/fixtures/skyport/src/flows/payments/index.ts +1 -2
  43. package/src/compiler/fixtures/skyport/src/flows/support/index.ts +1 -2
  44. package/src/compiler/fixtures/skyport.expected.json +3 -3
  45. package/src/compiler/fixtures/triggers/five-triggers.ts +5 -10
  46. package/src/compiler/generate-adopt.test.ts +85 -0
  47. package/src/compiler/generate-adopt.ts +111 -0
  48. package/src/config/driver-defaults.test.ts +122 -0
  49. package/src/config/driver-defaults.ts +186 -0
  50. package/src/config/index.ts +97 -7
  51. package/src/console/server/dry-run.audit.test.ts +2 -2
  52. package/src/console/server/flows.ts +50 -150
  53. package/src/docker/compose.ts +13 -3
  54. package/src/docker/images-config.test.ts +168 -0
  55. package/src/docker/recipes/llama-cpp.ts +10 -1
  56. package/src/docker/stack-id.ts +1 -0
  57. package/src/elements/channel/declare.ts +27 -1
  58. package/src/elements/clock/chaos-child.ts +3 -6
  59. package/src/elements/clock/chaos.test.ts +1 -2
  60. package/src/elements/clock.test.ts +2 -4
  61. package/src/elements/signal/declare.ts +25 -1
  62. package/src/elements/store/declare.ts +27 -1
  63. package/src/elements/store/resource.ts +6 -11
  64. package/src/elements/store/upsert-app.test.ts +1 -2
  65. package/src/elements/vault/declare.ts +29 -1
  66. package/src/kernel/adopt-barrel-fresh.test.ts +103 -0
  67. package/src/kernel/app.ts +66 -9
  68. package/src/kernel/auto-registry.test.ts +198 -0
  69. package/src/kernel/boot-bind/channel.ts +12 -4
  70. package/src/kernel/boot-bind/clock.ts +3 -3
  71. package/src/kernel/boot-bind/gate.ts +7 -4
  72. package/src/kernel/boot-bind/honor-config.test.ts +5 -5
  73. package/src/kernel/boot-bind/journal.ts +3 -3
  74. package/src/kernel/boot-bind/signal.ts +3 -1
  75. package/src/kernel/boot-bind/store.test.ts +19 -0
  76. package/src/kernel/boot-bind/store.ts +24 -9
  77. package/src/kernel/boot-bind/vault.ts +3 -3
  78. package/src/kernel/boot.test.ts +19 -15
  79. package/src/kernel/boot.ts +81 -1
  80. package/src/kernel/call.test.ts +5 -10
  81. package/src/kernel/compensate.test.ts +3 -6
  82. package/src/kernel/concurrency.test.ts +3 -5
  83. package/src/kernel/correlation.test.ts +4 -8
  84. package/src/kernel/edge.test.ts +1 -1
  85. package/src/kernel/effects-stamping.test.ts +4 -7
  86. package/src/kernel/element-registries.ts +27 -0
  87. package/src/kernel/errors.ts +11 -0
  88. package/src/kernel/flow.test.ts +8 -15
  89. package/src/kernel/flow.ts +12 -14
  90. package/src/kernel/hook-timing.test.ts +2 -2
  91. package/src/kernel/hooks.test.ts +5 -10
  92. package/src/kernel/horizontal-child.ts +5 -10
  93. package/src/kernel/journal-boot.test.ts +2 -4
  94. package/src/kernel/pipeline.test.ts +6 -12
  95. package/src/kernel/plugin/capabilities.test.ts +1 -1
  96. package/src/kernel/plugin/decorate.test.ts +3 -8
  97. package/src/kernel/plugin/scoping.test.ts +6 -16
  98. package/src/kernel/plugin-elements.test.ts +1 -1
  99. package/src/kernel/plugin-needs.test.ts +1 -1
  100. package/src/kernel/ready.test.ts +1 -2
  101. package/src/kernel/registry-isolation.test.ts +5 -5
  102. package/src/kernel/triggers.ts +1 -1
  103. package/src/plugins/anonymous.ts +1 -3
  104. package/src/plugins/compression.test.ts +6 -8
  105. package/src/plugins/config-source.test.ts +1 -1
  106. package/src/plugins/config-source.ts +1 -2
  107. package/src/plugins/cors.test.ts +10 -10
  108. package/src/plugins/csrf.test.ts +2 -2
  109. package/src/plugins/headers.test.ts +14 -15
  110. package/src/plugins/ip-allowlist.test.ts +9 -9
  111. package/src/plugins/magic-link.ts +2 -6
  112. package/src/plugins/maintenance-mode.test.ts +6 -6
  113. package/src/plugins/otp.ts +5 -15
  114. package/src/plugins/passkey.ts +4 -12
  115. package/src/plugins/two-factor.ts +3 -9
  116. package/src/plugins/username.ts +2 -6
  117. package/src/release/measure.ts +1 -1
  118. package/src/runs/runs.test.ts +3 -5
  119. package/src/runtime/serve.test.ts +2 -4
  120. package/src/test/create-test-app.test.ts +3 -7
  121. package/src/test/provisions.integration.test.ts +4 -9
  122. package/src/test/reset-element-registries.ts +31 -0
  123. package/site/content/docs/console/access.mdx +0 -29
  124. package/site/content/docs/console/ai.mdx +0 -35
  125. package/site/content/docs/console/architecture.mdx +0 -35
  126. package/site/content/docs/console/channels.mdx +0 -37
  127. package/site/content/docs/console/clock.mdx +0 -51
  128. package/site/content/docs/console/flows.mdx +0 -31
  129. package/site/content/docs/console/gates.mdx +0 -73
  130. package/site/content/docs/console/index.mdx +0 -54
  131. package/site/content/docs/console/manifest-diff.mdx +0 -34
  132. package/site/content/docs/console/meta.json +0 -24
  133. package/site/content/docs/console/overview.mdx +0 -40
  134. package/site/content/docs/console/plugins.mdx +0 -41
  135. package/site/content/docs/console/privacy.mdx +0 -32
  136. package/site/content/docs/console/runs.mdx +0 -50
  137. package/site/content/docs/console/signals.mdx +0 -40
  138. package/site/content/docs/console/store.mdx +0 -32
  139. package/site/content/docs/console/tenancy.mdx +0 -32
  140. package/site/content/docs/console/traces.mdx +0 -38
  141. 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 ?? options.secrets,
832
+ secrets: overrides?.secrets ?? effectiveSecrets,
776
833
  vault: overrides?.vault ?? options.vault,
777
834
  gates: [...baseGates, ...authGates],
778
- signals: overrides?.signals ?? options.signals,
835
+ signals: overrides?.signals ?? effectiveSignals,
779
836
  clocks: overrides?.clocks ?? options.clocks,
780
- stores: overrides?.stores ?? options.stores,
781
- channel: overrides?.channel ?? options.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 ?? options.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 ?? options.secrets ?? [];
833
- const baseSignals = overrides?.signals ?? options.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 ?? options.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 ?? options.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
- return (
85
- resolveDriverId(options.config?.drivers?.channel?.email, env) ?? (docker ? "smtp" : "console")
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
- const resolved = resolveDriverId(options.config?.drivers?.clock, env);
37
- if (resolved) return resolved;
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(options.config?.drivers?.store?.kv, env);
31
- if (resolved === "redis") return "redis";
32
- if (resolved === "memory") return "memory";
33
- return docker ? "redis" : "memory";
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({ name: "charge", durable: true, do: () => ({ ok: true }) })],
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({ name: "plain", do: () => ({ ok: true }) })],
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({ name: "charge", durable: true, do: () => ({ ok: true }) })],
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({ name: "charge", durable: true, do: () => ({ ok: true }) })],
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({ name: "charge", durable: true, do: () => ({ ok: true }) })],
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
- const resolved = resolveDriverId(options.config?.drivers?.journal, env);
41
- if (resolved) return resolved;
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
- return resolveDriverId(options.config?.drivers?.signal, env) ?? "memory";
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
- const resolved = resolveDriverId(options.config?.drivers?.store?.sql, env);
142
- if (resolved) return resolved;
143
- return docker ? "postgres" : "memory";
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
- const resolved = resolveDriverId(options.config?.drivers?.store?.kv, env);
155
- if (resolved) return resolved;
156
- return docker ? "redis" : "memory";
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
- const resolved = resolveDriverId(options.config?.drivers?.store?.files, env);
172
- if (resolved) return resolved;
173
- return docker ? "s3" : "memory";
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
- const resolved = resolveDriverId(options.config?.drivers?.vault, env);
18
- if (resolved) return resolved;
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
  /**