okengine 0.6.0 → 0.7.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.
Files changed (162) hide show
  1. package/AGENTS.md +2 -2
  2. package/README.md +98 -71
  3. package/package.json +4 -4
  4. package/site/content/docs/console/clock.mdx +25 -5
  5. package/site/content/docs/console/signals.mdx +13 -4
  6. package/site/content/docs/elements/ai.mdx +29 -2
  7. package/site/content/docs/elements/channel.mdx +90 -20
  8. package/site/content/docs/elements/clock.mdx +56 -24
  9. package/site/content/docs/elements/flow.mdx +2 -0
  10. package/site/content/docs/elements/gate.mdx +8 -1
  11. package/site/content/docs/elements/signal.mdx +173 -29
  12. package/site/content/docs/elements/store.mdx +464 -73
  13. package/site/content/docs/elements/vault.mdx +15 -5
  14. package/site/content/docs/get-started/basic-usage.mdx +13 -6
  15. package/site/content/docs/get-started/installation.mdx +49 -13
  16. package/site/content/docs/get-started/introduction.mdx +1 -1
  17. package/site/content/docs/reference/cli.md +30 -2
  18. package/site/content/docs/reference/configuration.mdx +18 -17
  19. package/site/content/docs/reference/environment-variables.mdx +44 -18
  20. package/site/content/docs/reference/errors.mdx +12 -11
  21. package/site/content/docs/reference/fx.mdx +15 -13
  22. package/site/content/docs/reference/security.md +3 -1
  23. package/src/cli/ai-setup/ai-setup.test.ts +144 -0
  24. package/src/cli/ai-setup/apply.ts +214 -0
  25. package/src/cli/ai-setup/catalog.ts +263 -0
  26. package/src/cli/ai-setup/detect-ollama.ts +166 -0
  27. package/src/cli/ai-setup/index.ts +228 -0
  28. package/src/cli/ai-setup/prompts.ts +649 -0
  29. package/src/cli/ai-setup/recommend.test.ts +100 -0
  30. package/src/cli/ai-setup/recommend.ts +203 -0
  31. package/src/cli/ai.ts +38 -0
  32. package/src/cli/db-auto-push.test.ts +6 -1
  33. package/src/cli/db-auto-push.ts +8 -3
  34. package/src/cli/dev-db-push.test.ts +59 -0
  35. package/src/cli/dev.test.ts +40 -0
  36. package/src/cli/dev.ts +55 -1
  37. package/src/cli/docker-clean.test.ts +179 -0
  38. package/src/cli/docker-clean.ts +406 -0
  39. package/src/cli/docker.ts +16 -4
  40. package/src/cli/ensure-drizzle-config.ts +1 -1
  41. package/src/cli/hero-meta.test.ts +6 -5
  42. package/src/cli/hero-meta.ts +7 -1
  43. package/src/cli/index.ts +5 -0
  44. package/src/cli/load-config.ts +4 -4
  45. package/src/cli/openbao-bootstrap.test.ts +1 -0
  46. package/src/cli/openbao-bootstrap.ts +9 -2
  47. package/src/cli/openbao-restart.integration.test.ts +106 -97
  48. package/src/cli/registry.ts +79 -1
  49. package/src/client/live-gap.test.ts +35 -0
  50. package/src/client/transport.test.ts +21 -0
  51. package/src/client/transport.ts +27 -5
  52. package/src/compiler/fixtures/skyport/oke.config.ts +1 -1
  53. package/src/config/define-config.test.ts +6 -6
  54. package/src/config/index.ts +1 -1
  55. package/src/console/server/console.test.ts +45 -0
  56. package/src/console/server/flows.ts +39 -9
  57. package/src/console/server/i18n.ts +41 -0
  58. package/src/console/server/vault.ts +12 -38
  59. package/src/console/ui/dist/assets/index-ClX0ZCe5.js +10 -0
  60. package/src/console/ui/dist/assets/{panel-access-BGv45snf.js → panel-access-ri1X9Otj.js} +1 -1
  61. package/src/console/ui/dist/assets/{panel-ai-B2S7LEii.js → panel-ai-DLHjRoJk.js} +1 -1
  62. package/src/console/ui/dist/assets/{panel-architecture-D7UJh91v.js → panel-architecture-DI3bQF3J.js} +1 -1
  63. package/src/console/ui/dist/assets/{panel-channels-9T3ybqRu.js → panel-channels-BDYWX29p.js} +1 -1
  64. package/src/console/ui/dist/assets/{panel-clock-Cb1UXGRQ.js → panel-clock-i-JpTjVB.js} +1 -1
  65. package/src/console/ui/dist/assets/{panel-diff-DmYbKWmN.js → panel-diff-CFuWxiXy.js} +1 -1
  66. package/src/console/ui/dist/assets/{panel-flows-PiHwT55z.js → panel-flows-B8TRhcoe.js} +1 -1
  67. package/src/console/ui/dist/assets/{panel-gates-BQGYXvjT.js → panel-gates-CfkBgco7.js} +1 -1
  68. package/src/console/ui/dist/assets/{panel-overview-BBnRO18l.js → panel-overview-DONDzBd2.js} +1 -1
  69. package/src/console/ui/dist/assets/{panel-plugins-D0PsmVw2.js → panel-plugins-DDJwTehL.js} +1 -1
  70. package/src/console/ui/dist/assets/{panel-runs-CWuRDe0r.js → panel-runs-D5zf-D9c.js} +1 -1
  71. package/src/console/ui/dist/assets/{panel-signals-Bbg4ewpP.js → panel-signals-Dsxdu_AR.js} +1 -1
  72. package/src/console/ui/dist/assets/{panel-store-CPCbsDRa.js → panel-store-hy7O8HOs.js} +1 -1
  73. package/src/console/ui/dist/assets/{panel-traces-DVAzuA_S.js → panel-traces-CBiAe5go.js} +1 -1
  74. package/src/console/ui/dist/assets/{panel-vault-D1_MvOmo.js → panel-vault-Za1GyJfM.js} +1 -1
  75. package/src/console/ui/dist/index.html +1 -1
  76. package/src/console/ui/shell/components/ui.tsx +5 -1
  77. package/src/console/ui/shell/setup/Wizard.tsx +26 -5
  78. package/src/docker/cleanup.test.ts +193 -0
  79. package/src/docker/cleanup.ts +355 -0
  80. package/src/docker/docker.test.ts +4 -0
  81. package/src/docker/dockerfile.integration.test.ts +126 -119
  82. package/src/docker/index.ts +19 -0
  83. package/src/docker/recipes/ollama.ts +10 -5
  84. package/src/docker/stack.integration.test.ts +118 -102
  85. package/src/drivers/ai-ollama-tools.integration.test.ts +8 -6
  86. package/src/drivers/ai-ollama.integration.test.ts +3 -19
  87. package/src/drivers/ai-ollama.ts +45 -2
  88. package/src/drivers/ai-providers.test.ts +31 -0
  89. package/src/drivers/channel-fcm.ts +49 -53
  90. package/src/drivers/channel-msegat.ts +61 -0
  91. package/src/drivers/channel-sently-map.ts +57 -0
  92. package/src/drivers/channel-sently.test.ts +99 -0
  93. package/src/drivers/channel-sndr.ts +28 -0
  94. package/src/drivers/channel-taqnyat.ts +57 -0
  95. package/src/drivers/channel-types.ts +79 -2
  96. package/src/drivers/channel-unifonic.ts +26 -43
  97. package/src/drivers/channel-wa-cloud.ts +33 -47
  98. package/src/drivers/channel-webpush.ts +39 -239
  99. package/src/drivers/index.ts +7 -0
  100. package/src/drivers/signal-engine.ts +98 -25
  101. package/src/drivers/signal-nats.ts +4 -4
  102. package/src/drivers/signal-postgres.ts +158 -64
  103. package/src/drivers/signal-redis.ts +4 -4
  104. package/src/drivers/signal-types.ts +61 -2
  105. package/src/drivers/vault-driver-removal.test.ts +5 -1
  106. package/src/elements/channel/costs.test.ts +2 -2
  107. package/src/elements/channel/costs.ts +14 -2
  108. package/src/elements/channel/mime.ts +11 -0
  109. package/src/elements/channel/runtime.ts +94 -0
  110. package/src/elements/channel/sndr-webhooks.test.ts +26 -0
  111. package/src/elements/channel.ts +10 -1
  112. package/src/elements/clock/chaos-child.ts +150 -0
  113. package/src/elements/clock/chaos.test.ts +204 -0
  114. package/src/elements/clock/reconcile.ts +110 -0
  115. package/src/elements/clock/runtime.ts +11 -0
  116. package/src/elements/clock.test.ts +164 -2
  117. package/src/elements/clock.ts +1 -0
  118. package/src/elements/gate/runtime.ts +6 -0
  119. package/src/elements/index.ts +9 -0
  120. package/src/elements/signal/chaos-child.ts +41 -4
  121. package/src/elements/signal/declare.ts +4 -1
  122. package/src/elements/signal/delivery-modes.test.ts +179 -0
  123. package/src/elements/signal/key-ordering.test.ts +306 -0
  124. package/src/elements/signal/lease-reclaim.test.ts +123 -0
  125. package/src/elements/signal/optional-emit.test.ts +76 -0
  126. package/src/elements/signal/order-lifecycle.test.ts +130 -0
  127. package/src/elements/signal/orphan-messages.test.ts +152 -0
  128. package/src/elements/signal/runtime.ts +11 -4
  129. package/src/elements/signal/schema-emit.test.ts +107 -0
  130. package/src/elements/signal.test.ts +89 -5
  131. package/src/elements/store/files-fx.ts +104 -0
  132. package/src/elements/store/files-image.test.ts +251 -0
  133. package/src/elements/store/files-image.ts +494 -0
  134. package/src/elements/store/runtime.ts +60 -0
  135. package/src/elements/store.ts +24 -0
  136. package/src/elements/vault/boot-chain.ts +150 -0
  137. package/src/elements/vault/runtime.ts +8 -0
  138. package/src/kernel/app.ts +3 -1
  139. package/src/kernel/boot-bind/channel.test.ts +68 -3
  140. package/src/kernel/boot-bind/channel.ts +93 -2
  141. package/src/kernel/boot-bind/clock.ts +52 -7
  142. package/src/kernel/boot-bind/gate.ts +71 -3
  143. package/src/kernel/boot-bind/honor-config.test.ts +247 -0
  144. package/src/kernel/boot-bind/signal.ts +64 -7
  145. package/src/kernel/boot-bind/vault.ts +27 -8
  146. package/src/kernel/boot.ts +10 -2
  147. package/src/kernel/errors.ts +8 -2
  148. package/src/kernel/fx.test.ts +13 -0
  149. package/src/kernel/fx.ts +27 -4
  150. package/src/plugins/auth-delivery.mailpit.integration.test.ts +10 -4
  151. package/src/release/exports.test.ts +26 -0
  152. package/src/release/exports.ts +64 -5
  153. package/src/release/index.ts +5 -0
  154. package/src/release/measure.exports.test.ts +13 -1
  155. package/src/release/measure.ts +76 -13
  156. package/src/release/official-plugins.ts +46 -0
  157. package/src/release/readme.test.ts +30 -2
  158. package/src/runtime/dev-request-log.test.ts +20 -1
  159. package/src/runtime/dev-request-log.ts +38 -0
  160. package/src/term.test.ts +15 -0
  161. package/src/term.ts +8 -3
  162. package/src/console/ui/dist/assets/index-CjxwRGVv.js +0 -10
@@ -0,0 +1,247 @@
1
+ /**
2
+ * Boot-level acceptance: binders honour `drivers.*` config pins.
3
+ *
4
+ * Asserts runtime identity (driverId / store kind / chain), not hero strings.
5
+ */
6
+
7
+ import { afterEach, describe, expect, test } from "bun:test";
8
+ import { mkdtemp, rm } from "node:fs/promises";
9
+ import { tmpdir } from "node:os";
10
+ import { join } from "node:path";
11
+ import { createRedisFakeClient } from "../../drivers/redis.ts";
12
+ import { createSignalRedisFake } from "../../drivers/signal-redis.ts";
13
+ import { clock } from "../../elements/clock.ts";
14
+ import { gate } from "../../elements/gate.ts";
15
+ import { signal } from "../../elements/signal.ts";
16
+ import { vault } from "../../elements/vault.ts";
17
+ import { buildVaultBootChain } from "../../elements/vault/boot-chain.ts";
18
+ import { bootApplication } from "../boot.ts";
19
+
20
+ describe("boot binders honour drivers.* config", () => {
21
+ const prevCwd = process.cwd();
22
+ let tmp: string | undefined;
23
+
24
+ afterEach(async () => {
25
+ process.chdir(prevCwd);
26
+ if (tmp) {
27
+ await rm(tmp, { recursive: true, force: true });
28
+ tmp = undefined;
29
+ }
30
+ delete process.env.OKE_VAULT_URL;
31
+ delete process.env.OKE_VAULT_TOKEN;
32
+ delete process.env.REDIS_URL;
33
+ });
34
+
35
+ test("vault: drivers.vault env builds env chain layers", async () => {
36
+ process.env.APP_NAME = "honor-config";
37
+ try {
38
+ const result = await bootApplication({
39
+ env: "local",
40
+ secrets: [vault.config("APP_NAME", { description: "app" })],
41
+ config: {
42
+ drivers: {
43
+ vault: { local: "env", docker: "openbao", test: "memory", prod: "openbao" },
44
+ },
45
+ },
46
+ });
47
+ try {
48
+ expect(result.vault).toBeDefined();
49
+ expect(result.vault!.chainDriverIds).toContain("env");
50
+ expect(result.vault!.chainDriverIds).toContain("memory");
51
+ expect(result.vault!.chainDriverIds[0]).toBe("env");
52
+ } finally {
53
+ await result.close();
54
+ }
55
+ } finally {
56
+ delete process.env.APP_NAME;
57
+ }
58
+ });
59
+
60
+ test("vault: drivers.vault openbao with credentials selects openbao layer", () => {
61
+ process.env.OKE_VAULT_URL = "http://127.0.0.1:8200";
62
+ process.env.OKE_VAULT_TOKEN = "test-token";
63
+ const chain = buildVaultBootChain({
64
+ driverId: "openbao",
65
+ env: "local",
66
+ missingOpenbao: "throw",
67
+ });
68
+ expect(chain.map((l) => l.driver.id)).toContain("openbao");
69
+ expect(chain.map((l) => l.driver.id)).toContain("env");
70
+ });
71
+
72
+ test("vault: openbao without credentials fails loud in prod", async () => {
73
+ delete process.env.OKE_VAULT_URL;
74
+ delete process.env.OKE_VAULT_TOKEN;
75
+ await expect(
76
+ bootApplication({
77
+ env: "prod",
78
+ secrets: [vault.config("APP_NAME", { description: "app", dev: "x" })],
79
+ vault: { allowDevFallbacks: true },
80
+ config: {
81
+ drivers: {
82
+ vault: { prod: "openbao" },
83
+ },
84
+ },
85
+ }),
86
+ ).rejects.toThrow(/openbao.*OKE_VAULT_URL/);
87
+ });
88
+
89
+ test("clock: drivers.clock memory binds memory CronStore", async () => {
90
+ const result = await bootApplication({
91
+ env: "local",
92
+ startScheduler: false,
93
+ clocks: [clock("tick", { every: "1h" })],
94
+ config: {
95
+ drivers: {
96
+ clock: { local: "memory", docker: "file", test: "frozen", prod: "file" },
97
+ },
98
+ },
99
+ });
100
+ try {
101
+ expect(result.clock!.driverId).toBe("memory");
102
+ expect(result.clock!.store.kind).toBe("memory");
103
+ } finally {
104
+ await result.close();
105
+ }
106
+ });
107
+
108
+ test("clock: drivers.clock file binds file CronStore", async () => {
109
+ tmp = await mkdtemp(join(tmpdir(), "oke-clock-file-"));
110
+ process.chdir(tmp);
111
+ const result = await bootApplication({
112
+ env: "local",
113
+ startScheduler: false,
114
+ clocks: [clock("tick", { every: "1h" })],
115
+ config: {
116
+ drivers: {
117
+ clock: { local: "file", docker: "file", test: "frozen", prod: "file" },
118
+ },
119
+ },
120
+ });
121
+ try {
122
+ expect(result.clock!.driverId).toBe("file");
123
+ expect(result.clock!.store.kind).toBe("file");
124
+ } finally {
125
+ await result.close();
126
+ }
127
+ });
128
+
129
+ test("clock: drivers.clock postgres fails loud", async () => {
130
+ await expect(
131
+ bootApplication({
132
+ env: "local",
133
+ startScheduler: false,
134
+ clocks: [clock("tick", { every: "1h" })],
135
+ config: {
136
+ drivers: {
137
+ clock: { local: "postgres" },
138
+ },
139
+ },
140
+ }),
141
+ ).rejects.toThrow(/clock driver "postgres" is not implemented/);
142
+ });
143
+
144
+ test("gate: drivers.store.kv redis opens redis-backed oke:gates", async () => {
145
+ const fake = createRedisFakeClient();
146
+ const result = await bootApplication({
147
+ env: "local",
148
+ gates: [gate.rate({ max: 10, per: "1m" })],
149
+ clients: { kv: fake },
150
+ config: {
151
+ drivers: {
152
+ store: {
153
+ kv: { local: "redis", docker: "redis", prod: "redis" },
154
+ },
155
+ },
156
+ },
157
+ });
158
+ try {
159
+ expect(result.gate!.kvDriverId).toBe("redis");
160
+ } finally {
161
+ await result.close();
162
+ }
163
+ });
164
+
165
+ test("gate: drivers.store.kv memory keeps memory oke:gates", async () => {
166
+ const result = await bootApplication({
167
+ env: "local",
168
+ gates: [gate.rate({ max: 10, per: "1m" })],
169
+ config: {
170
+ drivers: {
171
+ store: {
172
+ kv: { local: "memory", docker: "redis", prod: "redis" },
173
+ },
174
+ },
175
+ },
176
+ });
177
+ try {
178
+ expect(result.gate!.kvDriverId).toBe("memory");
179
+ } finally {
180
+ await result.close();
181
+ }
182
+ });
183
+
184
+ test("signal: drivers.signal memory binds memory", async () => {
185
+ const result = await bootApplication({
186
+ env: "local",
187
+ signals: [signal("ping", { delivery: "once" })],
188
+ config: {
189
+ drivers: {
190
+ signal: { local: "memory", docker: "redis", prod: "redis" },
191
+ },
192
+ },
193
+ });
194
+ try {
195
+ expect(result.signal!.driverId).toBe("memory");
196
+ } finally {
197
+ await result.close();
198
+ }
199
+ });
200
+
201
+ test("signal: drivers.signal redis binds redis", async () => {
202
+ const fake = createSignalRedisFake();
203
+ const result = await bootApplication({
204
+ env: "local",
205
+ signals: [signal("ping", { delivery: "once" })],
206
+ clients: { signalRedis: fake },
207
+ config: {
208
+ drivers: {
209
+ signal: { local: "redis", docker: "redis", prod: "redis" },
210
+ },
211
+ },
212
+ });
213
+ try {
214
+ expect(result.signal!.driverId).toBe("redis");
215
+ } finally {
216
+ await result.close();
217
+ }
218
+ });
219
+
220
+ test("signal: drivers.signal postgres fails loud (never silent memory)", async () => {
221
+ await expect(
222
+ bootApplication({
223
+ env: "local",
224
+ signals: [signal("ping", { delivery: "once" })],
225
+ config: {
226
+ drivers: {
227
+ signal: { local: "postgres" },
228
+ },
229
+ },
230
+ }),
231
+ ).rejects.toThrow(/signal driver "postgres"/);
232
+ });
233
+
234
+ test("signal: drivers.signal nats fails loud", async () => {
235
+ await expect(
236
+ bootApplication({
237
+ env: "local",
238
+ signals: [signal("ping", { delivery: "once" })],
239
+ config: {
240
+ drivers: {
241
+ signal: { local: "nats" },
242
+ },
243
+ },
244
+ }),
245
+ ).rejects.toThrow(/signal driver "nats"/);
246
+ });
247
+ });
@@ -2,29 +2,86 @@
2
2
  * Lazy signal binder — loaded only when Signal is declared.
3
3
  */
4
4
 
5
+ import { resolveDriverId, type ConfigEnv } from "../../config/index.ts";
5
6
  import { memorySignalDriver } from "../../drivers/signal-memory.ts";
7
+ import { createBunSignalRedisClient, redisSignalDriver } from "../../drivers/signal-redis.ts";
8
+ import type { SignalRedisClientLike } from "../../drivers/signal-types.ts";
6
9
  import { createSignalRuntime, type SignalRuntime } from "../../elements/signal.ts";
7
- import { resolveDriverId, type ConfigEnv } from "../../config/index.ts";
8
10
  import type { BootOptions } from "../boot.ts";
9
11
 
12
+ /**
13
+ * Resolve `drivers.signal` for the active env (default `memory`).
14
+ *
15
+ * @param options - Boot options
16
+ * @param env - Active environment
17
+ */
18
+ export function resolveSignalDriverId(options: BootOptions, env: ConfigEnv): string {
19
+ return resolveDriverId(options.config?.drivers?.signal, env) ?? "memory";
20
+ }
21
+
22
+ function redisUrlFor(docker: boolean): string | undefined {
23
+ const url = process.env.REDIS_URL ?? process.env.OKE_STORE_KV_URL ?? undefined;
24
+ if (!url && docker) {
25
+ throw new Error(
26
+ "oke boot: signal redis driver needs REDIS_URL (did `oke dev -d` write docker/.env.docker?)",
27
+ );
28
+ }
29
+ return url;
30
+ }
31
+
10
32
  /**
11
33
  * Construct a Signal runtime, register decls / binding names, start the bus.
12
34
  *
35
+ * Supported ids: `memory` · `redis`. `postgres` / `nats` fail loud until a real
36
+ * native client can be constructed (never silently bind memory).
37
+ *
13
38
  * @param options - Boot options
14
39
  * @param env - Active environment
15
40
  * @param now - Clock
41
+ * @param docker - Docker mode
16
42
  */
17
43
  export async function bindSignal(
18
44
  options: BootOptions,
19
45
  env: ConfigEnv,
20
46
  now: () => number,
47
+ docker = false,
21
48
  ): Promise<SignalRuntime> {
22
- const signalId = resolveDriverId(options.config?.drivers?.signal, env) ?? "memory";
23
- void signalId;
24
- const signal = createSignalRuntime({
25
- driver: memorySignalDriver,
26
- now,
27
- });
49
+ const signalId = resolveSignalDriverId(options, env);
50
+ const injectedRedis = options.clients?.signalRedis as SignalRedisClientLike | undefined;
51
+
52
+ let signal: SignalRuntime;
53
+ switch (signalId) {
54
+ case "memory":
55
+ signal = createSignalRuntime({
56
+ driver: memorySignalDriver,
57
+ now,
58
+ });
59
+ break;
60
+ case "redis": {
61
+ const redis = injectedRedis ?? createBunSignalRedisClient(redisUrlFor(docker));
62
+ signal = createSignalRuntime({
63
+ driver: redisSignalDriver,
64
+ now,
65
+ redis,
66
+ });
67
+ break;
68
+ }
69
+ case "postgres":
70
+ throw new Error(
71
+ 'oke boot: signal driver "postgres" needs a LISTEN/NOTIFY-capable SQL client — ' +
72
+ 'not available via Bun.SQL yet. Use "redis" or "memory", or inject elements.signal.',
73
+ );
74
+ case "nats":
75
+ throw new Error(
76
+ 'oke boot: signal driver "nats" has no production client bind yet — ' +
77
+ 'use "redis" or "memory", or inject elements.signal.',
78
+ );
79
+ default:
80
+ throw new Error(
81
+ `oke boot: unknown signal driver "${signalId}" (expected memory · redis · postgres · nats)`,
82
+ );
83
+ }
84
+
28
85
  for (const decl of options.signals ?? []) {
29
86
  signal.register(decl);
30
87
  }
@@ -2,28 +2,47 @@
2
2
  * Lazy vault binder — loaded only when secrets are declared.
3
3
  */
4
4
 
5
- import { memoryVaultDriver } from "../../drivers/vault-memory.ts";
5
+ import { resolveDriverId, type ConfigEnv } from "../../config/index.ts";
6
6
  import { createVaultRuntime, type VaultRuntime } from "../../elements/vault.ts";
7
- import type { ConfigEnv } from "../../config/index.ts";
7
+ import { buildVaultBootChain, normalizeVaultDriverId } from "../../elements/vault/boot-chain.ts";
8
8
  import type { BootOptions } from "../boot.ts";
9
9
 
10
+ /**
11
+ * Resolve `drivers.vault` for the active env (default `env` locally, `memory` in test).
12
+ *
13
+ * @param options - Boot options
14
+ * @param env - Active environment
15
+ */
16
+ 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";
20
+ }
21
+
10
22
  /**
11
23
  * Construct and boot a Vault runtime from BootOptions.
12
24
  *
25
+ * Uses `drivers.vault` via the shared {@link buildVaultBootChain} helper
26
+ * (same chain Console uses). Injected `options.vault.chain` still wins for tests.
27
+ *
13
28
  * @param options - Boot options
14
29
  * @param env - Active environment
15
30
  */
16
31
  export async function bindVault(options: BootOptions, env: ConfigEnv): Promise<VaultRuntime> {
17
32
  const vaultSecrets = options.vault?.secrets ?? options.secrets ?? [];
33
+ const chain =
34
+ options.vault?.chain ??
35
+ buildVaultBootChain({
36
+ driverId: normalizeVaultDriverId(resolveVaultDriverId(options, env)),
37
+ env,
38
+ cwd: process.cwd(),
39
+ seed: {},
40
+ });
18
41
  const vault = createVaultRuntime({
19
42
  secrets: vaultSecrets,
20
- chain: options.vault?.chain ?? [
21
- {
22
- driver: memoryVaultDriver,
23
- options: { secrets: {} },
24
- },
25
- ],
43
+ chain,
26
44
  allowDevFallbacks: options.vault?.allowDevFallbacks ?? env !== "prod",
45
+ now: options.vault?.now,
27
46
  });
28
47
  await vault.boot();
29
48
  return vault;
@@ -114,6 +114,14 @@ export interface BootOptions {
114
114
  readonly onSignal?: (signal: string, payload: unknown) => void | Promise<void>;
115
115
  /** Injectable clock for test / frozen harnesses. */
116
116
  readonly now?: () => number;
117
+ /**
118
+ * Test-only client injection for binders that open redis / signal redis.
119
+ * Production apps leave this unset.
120
+ */
121
+ readonly clients?: {
122
+ readonly kv?: import("../drivers/types.ts").KvClientLike;
123
+ readonly signalRedis?: import("../drivers/signal-types.ts").SignalRedisClientLike;
124
+ };
117
125
  /** Instance id for leader election. */
118
126
  readonly instanceId?: string;
119
127
  /**
@@ -344,7 +352,7 @@ export async function bootApplication(input: BootOptions = {}): Promise<BootResu
344
352
  let signal = pre.signal;
345
353
  if (needs.signal) {
346
354
  if (!signal) {
347
- signal = await signalBind!.bindSignal(options, env, now);
355
+ signal = await signalBind!.bindSignal(options, env, now, docker);
348
356
  } else {
349
357
  // Pre-built: still register decls / start if needed.
350
358
  for (const decl of options.signals ?? []) {
@@ -376,7 +384,7 @@ export async function bootApplication(input: BootOptions = {}): Promise<BootResu
376
384
  // Gate (before AI)
377
385
  let gate = pre.gate;
378
386
  if (needs.gate && !gate) {
379
- gate = await gateBind!.bindGate(options, now);
387
+ gate = await gateBind!.bindGate(options, now, env, docker);
380
388
  }
381
389
 
382
390
  // 5. Channel
@@ -143,14 +143,20 @@ export const OKE_ERRORS = {
143
143
  fix: 'Add "{resource}" to this flow\'s effects.calls.',
144
144
  },
145
145
  /**
146
- * Emit target has no subscriber (unified-theory §21 example).
147
- * Reserved now so the number stays permanent.
146
+ * Emit target has no subscriber (unified-theory §21).
147
+ * Thrown at emit when `optional` is false and nobody is subscribed.
148
148
  */
149
149
  ORPHAN_EMIT: {
150
150
  code: 1042,
151
151
  cause: 'Flow "{flow}" emits signal "{resource}" with no subscriber.',
152
152
  fix: "Add `on({resource}, …)` or mark the signal `{ optional: true }`.",
153
153
  },
154
+ /** Emit payload failed the signal's declared Standard Schema. */
155
+ SIGNAL_SCHEMA: {
156
+ code: 1043,
157
+ cause: '"{resource}": {detail}',
158
+ fix: "Fix schema payload.",
159
+ },
154
160
  /**
155
161
  * Domain table/column missing under docker/prod (migrations not applied).
156
162
  * Store/DDL band starts at 1100.
@@ -258,6 +258,19 @@ describe("errors — registry", () => {
258
258
  expect(err.message).toContain("→");
259
259
  });
260
260
 
261
+ test("OKE1043 is reserved for signal schema emit failures", () => {
262
+ const def = OKE_ERRORS.SIGNAL_SCHEMA;
263
+ expect(def.code).toBe(1043);
264
+ expect(lookupOkeError(1043)).toEqual(def);
265
+ const err = new OkeError(def, {
266
+ resource: "order-placed",
267
+ detail: "total: Expected number, received string",
268
+ });
269
+ expect(err.message).toContain("OKE1043");
270
+ expect(err.message).toContain("order-placed");
271
+ expect(err.message).toContain("https://oke.omqkhafi.dev/e/1043");
272
+ });
273
+
261
274
  test("fx.fail returns a value, not an exception", () => {
262
275
  const fx = createFx({ flow: "x", effects: {} });
263
276
  const result = fx.fail(
package/src/kernel/fx.ts CHANGED
@@ -19,6 +19,7 @@ import type {
19
19
  } from "../elements/store.ts";
20
20
  import type { SqlRow } from "../drivers/types.ts";
21
21
  import type { SignalRuntime } from "../elements/signal.ts";
22
+ import type { SignalEmitOptions } from "../drivers/signal-types.ts";
22
23
  import type { VaultRuntime } from "../elements/vault.ts";
23
24
  import type { ChannelRuntime } from "../elements/channel.ts";
24
25
  import type { AiRuntime } from "../elements/ai.ts";
@@ -295,8 +296,9 @@ export interface Fx {
295
296
  *
296
297
  * @param signal - Signal name or handle
297
298
  * @param payload - Payload
299
+ * @param options - Optional emit options (`key` for per-key once ordering)
298
300
  */
299
- emit(signal: NamedRef, payload?: unknown): Promise<void>;
301
+ emit(signal: NamedRef, payload?: unknown, options?: SignalEmitOptions): Promise<void>;
300
302
  /**
301
303
  * Call another flow (records `call`). Stub returns `undefined`.
302
304
  *
@@ -892,7 +894,28 @@ export function createFxContext(options: CreateFxOptions): FxContext {
892
894
  });
893
895
  }
894
896
 
895
- // KV / files / index — thin gated wrappers preserving driver methods.
897
+ if (decl.facet === "files") {
898
+ const ref = decl.ref;
899
+ return runtime.openFilesFx(
900
+ decl as Extract<StoreDecl, { facet: "files" }>,
901
+ {
902
+ effects: options.effects ?? {},
903
+ revealPii: options.revealPii,
904
+ },
905
+ {
906
+ gate: gated,
907
+ refuseDryRunWrite: () => {
908
+ if (isDryRun()) {
909
+ throw new DryRunWriteIsolationError(
910
+ `Driver-backed store "${ref}" cannot isolate writes during dry-run; dry-run refused rather than risk a double-write.`,
911
+ );
912
+ }
913
+ },
914
+ },
915
+ );
916
+ }
917
+
918
+ // KV / index — thin gated wrappers preserving driver methods.
896
919
  const baseRef = decl.ref;
897
920
  return new Proxy({} as StoreHandle, {
898
921
  get(_t, prop) {
@@ -1010,11 +1033,11 @@ export function createFxContext(options: CreateFxOptions): FxContext {
1010
1033
  store(ref) {
1011
1034
  return storeHandle(ref);
1012
1035
  },
1013
- emit(signal, payload) {
1036
+ emit(signal, payload, emitOptions) {
1014
1037
  const name = resolveName(signal);
1015
1038
  return gated("emit", name, async () => {
1016
1039
  if (options.signalRuntime) {
1017
- await options.signalRuntime.emit(name, payload);
1040
+ await options.signalRuntime.emit(name, payload, emitOptions);
1018
1041
  }
1019
1042
  });
1020
1043
  },
@@ -1,8 +1,9 @@
1
1
  /**
2
2
  * Real Mailpit end-to-end: magic-link / email-otp request → SMTP → Mailpit API.
3
3
  *
4
- * Gated on a live Docker daemon — same real-skip pattern as pgvector /
5
- * Meilisearch (`const live = … ? test : test.skip`). Never an empty pass.
4
+ * Opt-in via `OKE_TEST_DOCKER=1` plus a live Docker daemon — same real-skip
5
+ * pattern as pgvector / Meilisearch (`const live = … ? test : test.skip`).
6
+ * Never an empty pass.
6
7
  */
7
8
 
8
9
  import { afterEach, describe, expect, test } from "bun:test";
@@ -36,9 +37,14 @@ function dockerAvailable(): boolean {
36
37
  }
37
38
  }
38
39
 
39
- const DOCKER = dockerAvailable();
40
+ const WANT = process.env.OKE_TEST_DOCKER === "1";
41
+ const DOCKER = WANT && dockerAvailable();
40
42
  if (!DOCKER) {
41
- console.log("skip: mailpit e2e (docker daemon not available)");
43
+ console.log(
44
+ WANT
45
+ ? "skip: mailpit e2e (docker daemon not available)"
46
+ : "skip: mailpit e2e (OKE_TEST_DOCKER≠1)",
47
+ );
42
48
  }
43
49
  const live = DOCKER ? test : test.skip;
44
50
 
@@ -9,7 +9,9 @@ import {
9
9
  exportBudgetLabel,
10
10
  isMeasurableDriverFile,
11
11
  listDriverModules,
12
+ OFFICIAL_PLUGIN_BUDGETS,
12
13
  resolveExportBudgetTargets,
14
+ resolvePluginBudgetTargets,
13
15
  } from "./exports.ts";
14
16
 
15
17
  const ROOT = resolve(import.meta.dir, "../..");
@@ -29,12 +31,32 @@ describe("export budget targets", () => {
29
31
  expect(exportBudgetLabel(".")).toBe("okengine");
30
32
  expect(exportBudgetLabel("./channel")).toBe("channel");
31
33
  expect(exportBudgetLabel("./drivers/postgres")).toBe("postgres");
34
+ expect(exportBudgetLabel("./plugins/cors")).toBe("cors");
32
35
  expect(exportBudgetGroup(".")).toBe("exports");
33
36
  expect(exportBudgetGroup("./channel")).toBe("exports");
37
+ expect(exportBudgetGroup("./plugins")).toBe("exports");
38
+ expect(exportBudgetGroup("./plugins/cors")).toBe("plugins");
34
39
  expect(exportBudgetGroup("./drivers")).toBe("drivers");
35
40
  expect(exportBudgetGroup("./drivers/postgres")).toBe("drivers");
36
41
  });
37
42
 
43
+ test("official plugin catalogue files exist and resolve uniquely", async () => {
44
+ const plugins = resolvePluginBudgetTargets();
45
+ expect(plugins.length).toBe(OFFICIAL_PLUGIN_BUDGETS.length);
46
+ expect(new Set(plugins.map((t) => t.id)).size).toBe(plugins.length);
47
+
48
+ for (const plugin of OFFICIAL_PLUGIN_BUDGETS) {
49
+ const path = join(ROOT, "src/plugins", plugin.file);
50
+ expect(await Bun.file(path).exists()).toBe(true);
51
+ const target = plugins.find((t) => t.label === plugin.name);
52
+ expect(target).toBeDefined();
53
+ expect(target!.id).toBe(`export:./plugins/${plugin.name}`);
54
+ expect(target!.group).toBe("plugins");
55
+ expect(target!.category).toBe(plugin.category);
56
+ expect(target!.entry).toBe(path);
57
+ }
58
+ });
59
+
38
60
  test("every non-glob package export and every driver module is covered", async () => {
39
61
  const pkg = (await Bun.file(join(ROOT, "package.json")).json()) as {
40
62
  exports: Record<string, string>;
@@ -65,6 +87,10 @@ describe("export budget targets", () => {
65
87
  expect(target!.entry).toBe(join(ROOT, "src/drivers", file));
66
88
  }
67
89
 
90
+ for (const plugin of OFFICIAL_PLUGIN_BUDGETS) {
91
+ expect(bySubpath.get(`./plugins/${plugin.name}`)).toBeDefined();
92
+ }
93
+
68
94
  // No duplicates.
69
95
  expect(new Set(targets.map((t) => t.id)).size).toBe(targets.length);
70
96
  });