okengine 0.6.1 → 0.8.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 (167) hide show
  1. package/AGENTS.md +2 -2
  2. package/README.md +73 -181
  3. package/package.json +3 -3
  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 +42 -25
  8. package/site/content/docs/elements/clock.mdx +59 -25
  9. package/site/content/docs/elements/flow.mdx +8 -2
  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 +595 -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 +52 -14
  16. package/site/content/docs/get-started/introduction.mdx +1 -1
  17. package/site/content/docs/plugins/magic-link.mdx +42 -0
  18. package/site/content/docs/plugins/phone-number.mdx +78 -17
  19. package/site/content/docs/plugins/two-factor.mdx +1 -0
  20. package/site/content/docs/reference/cli.md +32 -2
  21. package/site/content/docs/reference/configuration.mdx +15 -13
  22. package/site/content/docs/reference/environment-variables.mdx +32 -18
  23. package/site/content/docs/reference/errors.mdx +12 -11
  24. package/site/content/docs/reference/fx.mdx +15 -13
  25. package/site/content/docs/reference/security.md +3 -1
  26. package/src/cli/ai-setup/ai-setup.test.ts +144 -0
  27. package/src/cli/ai-setup/apply.ts +214 -0
  28. package/src/cli/ai-setup/catalog.ts +263 -0
  29. package/src/cli/ai-setup/detect-ollama.ts +166 -0
  30. package/src/cli/ai-setup/index.ts +228 -0
  31. package/src/cli/ai-setup/prompts.ts +649 -0
  32. package/src/cli/ai-setup/recommend.test.ts +100 -0
  33. package/src/cli/ai-setup/recommend.ts +203 -0
  34. package/src/cli/ai.ts +38 -0
  35. package/src/cli/db-auto-push.test.ts +6 -1
  36. package/src/cli/db-auto-push.ts +8 -3
  37. package/src/cli/db-seed.ts +359 -0
  38. package/src/cli/db.test.ts +341 -3
  39. package/src/cli/db.ts +75 -8
  40. package/src/cli/dev-db-push.test.ts +59 -0
  41. package/src/cli/dev.test.ts +40 -0
  42. package/src/cli/dev.ts +55 -1
  43. package/src/cli/docker-clean.test.ts +179 -0
  44. package/src/cli/docker-clean.ts +406 -0
  45. package/src/cli/docker.ts +16 -4
  46. package/src/cli/ensure-drizzle-config.ts +1 -1
  47. package/src/cli/hero-meta.test.ts +6 -5
  48. package/src/cli/hero-meta.ts +7 -1
  49. package/src/cli/index.ts +5 -0
  50. package/src/cli/load-config.images.test.ts +22 -0
  51. package/src/cli/load-config.ts +7 -2
  52. package/src/cli/openbao-bootstrap.test.ts +1 -0
  53. package/src/cli/openbao-bootstrap.ts +9 -2
  54. package/src/cli/registry.ts +116 -2
  55. package/src/client/live-gap.test.ts +35 -0
  56. package/src/client/transport.test.ts +21 -0
  57. package/src/client/transport.ts +27 -5
  58. package/src/compiler/effects-infer.ts +1 -0
  59. package/src/compiler/fixtures/skyport/oke.config.ts +1 -1
  60. package/src/config/define-config.test.ts +6 -6
  61. package/src/config/index.ts +5 -1
  62. package/src/console/server/console.test.ts +45 -0
  63. package/src/console/server/flows.ts +39 -9
  64. package/src/console/server/i18n.ts +41 -0
  65. package/src/console/server/vault.ts +12 -38
  66. package/src/console/ui/dist/assets/index-ClX0ZCe5.js +10 -0
  67. package/src/console/ui/dist/assets/{panel-access-BGv45snf.js → panel-access-ri1X9Otj.js} +1 -1
  68. package/src/console/ui/dist/assets/{panel-ai-B2S7LEii.js → panel-ai-DLHjRoJk.js} +1 -1
  69. package/src/console/ui/dist/assets/{panel-architecture-D7UJh91v.js → panel-architecture-DI3bQF3J.js} +1 -1
  70. package/src/console/ui/dist/assets/{panel-channels-9T3ybqRu.js → panel-channels-BDYWX29p.js} +1 -1
  71. package/src/console/ui/dist/assets/{panel-clock-Cb1UXGRQ.js → panel-clock-i-JpTjVB.js} +1 -1
  72. package/src/console/ui/dist/assets/{panel-diff-DmYbKWmN.js → panel-diff-CFuWxiXy.js} +1 -1
  73. package/src/console/ui/dist/assets/{panel-flows-PiHwT55z.js → panel-flows-B8TRhcoe.js} +1 -1
  74. package/src/console/ui/dist/assets/{panel-gates-BQGYXvjT.js → panel-gates-CfkBgco7.js} +1 -1
  75. package/src/console/ui/dist/assets/{panel-overview-BBnRO18l.js → panel-overview-DONDzBd2.js} +1 -1
  76. package/src/console/ui/dist/assets/{panel-plugins-D0PsmVw2.js → panel-plugins-DDJwTehL.js} +1 -1
  77. package/src/console/ui/dist/assets/{panel-runs-CWuRDe0r.js → panel-runs-D5zf-D9c.js} +1 -1
  78. package/src/console/ui/dist/assets/{panel-signals-Bbg4ewpP.js → panel-signals-Dsxdu_AR.js} +1 -1
  79. package/src/console/ui/dist/assets/{panel-store-CPCbsDRa.js → panel-store-hy7O8HOs.js} +1 -1
  80. package/src/console/ui/dist/assets/{panel-traces-DVAzuA_S.js → panel-traces-CBiAe5go.js} +1 -1
  81. package/src/console/ui/dist/assets/{panel-vault-D1_MvOmo.js → panel-vault-Za1GyJfM.js} +1 -1
  82. package/src/console/ui/dist/index.html +1 -1
  83. package/src/console/ui/shell/components/ui.tsx +5 -1
  84. package/src/console/ui/shell/setup/Wizard.tsx +26 -5
  85. package/src/docker/cleanup.test.ts +193 -0
  86. package/src/docker/cleanup.ts +355 -0
  87. package/src/docker/docker.test.ts +4 -0
  88. package/src/docker/index.ts +19 -0
  89. package/src/docker/recipes/ollama.ts +10 -5
  90. package/src/drivers/ai-ollama.ts +45 -2
  91. package/src/drivers/ai-providers.test.ts +31 -0
  92. package/src/drivers/channel-sently.test.ts +8 -0
  93. package/src/drivers/channel-taqnyat-mail.ts +34 -0
  94. package/src/drivers/channel-types.ts +71 -0
  95. package/src/drivers/clock-postgres.test.ts +258 -0
  96. package/src/drivers/clock-postgres.ts +410 -0
  97. package/src/drivers/index.ts +21 -0
  98. package/src/drivers/journal-postgres.test.ts +175 -0
  99. package/src/drivers/journal-postgres.ts +492 -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/runtime.ts +51 -0
  107. package/src/elements/channel.test.ts +71 -0
  108. package/src/elements/clock/chaos-child.ts +389 -0
  109. package/src/elements/clock/chaos.test.ts +204 -0
  110. package/src/elements/clock/durable.ts +7 -0
  111. package/src/elements/clock/reconcile.ts +110 -0
  112. package/src/elements/clock/runtime.ts +13 -0
  113. package/src/elements/clock.test.ts +164 -2
  114. package/src/elements/clock.ts +2 -1
  115. package/src/elements/gate/runtime.ts +6 -0
  116. package/src/elements/signal/chaos-child.ts +41 -4
  117. package/src/elements/signal/declare.ts +4 -1
  118. package/src/elements/signal/delivery-modes.test.ts +179 -0
  119. package/src/elements/signal/key-ordering.test.ts +306 -0
  120. package/src/elements/signal/lease-reclaim.test.ts +123 -0
  121. package/src/elements/signal/optional-emit.test.ts +76 -0
  122. package/src/elements/signal/order-lifecycle.test.ts +130 -0
  123. package/src/elements/signal/orphan-messages.test.ts +152 -0
  124. package/src/elements/signal/runtime.ts +11 -4
  125. package/src/elements/signal/schema-emit.test.ts +107 -0
  126. package/src/elements/signal.test.ts +89 -5
  127. package/src/elements/store/files-fx.ts +104 -0
  128. package/src/elements/store/files-image.test.ts +251 -0
  129. package/src/elements/store/files-image.ts +494 -0
  130. package/src/elements/store/runtime.ts +60 -0
  131. package/src/elements/store/seed.test.ts +27 -0
  132. package/src/elements/store/seed.ts +68 -0
  133. package/src/elements/store/sql-session.test.ts +39 -0
  134. package/src/elements/store/sql-session.ts +55 -0
  135. package/src/elements/store/upsert-app.test.ts +103 -0
  136. package/src/elements/store.ts +29 -0
  137. package/src/elements/vault/boot-chain.ts +150 -0
  138. package/src/elements/vault/runtime.ts +8 -0
  139. package/src/index.ts +15 -0
  140. package/src/kernel/app.ts +168 -15
  141. package/src/kernel/boot-bind/channel.test.ts +16 -0
  142. package/src/kernel/boot-bind/channel.ts +13 -0
  143. package/src/kernel/boot-bind/clock.ts +63 -7
  144. package/src/kernel/boot-bind/gate.ts +71 -3
  145. package/src/kernel/boot-bind/honor-config.test.ts +348 -0
  146. package/src/kernel/boot-bind/journal.ts +89 -0
  147. package/src/kernel/boot-bind/signal.ts +64 -7
  148. package/src/kernel/boot-bind/vault.ts +27 -8
  149. package/src/kernel/boot.test.ts +6 -4
  150. package/src/kernel/boot.ts +63 -15
  151. package/src/kernel/concurrency.ts +1 -1
  152. package/src/kernel/errors.ts +8 -2
  153. package/src/kernel/fx.test.ts +19 -0
  154. package/src/kernel/fx.ts +153 -9
  155. package/src/kernel/index.ts +6 -0
  156. package/src/kernel/journal-boot.test.ts +397 -0
  157. package/src/kernel/journal-suspend.ts +35 -0
  158. package/src/kernel/journal.test.ts +142 -0
  159. package/src/kernel/journal.ts +202 -27
  160. package/src/plugins/auth-methods.security.test.ts +10 -7
  161. package/src/plugins/phone-number.ts +67 -10
  162. package/src/plugins/taqnyat.live.test.ts +174 -0
  163. package/src/runtime/dev-request-log.test.ts +20 -1
  164. package/src/runtime/dev-request-log.ts +38 -0
  165. package/src/term.test.ts +15 -0
  166. package/src/term.ts +8 -3
  167. package/src/console/ui/dist/assets/index-CjxwRGVv.js +0 -10
@@ -0,0 +1,348 @@
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 { flow } from "../flow.ts";
19
+ import { bootApplication } from "../boot.ts";
20
+
21
+ describe("boot binders honour drivers.* config", () => {
22
+ const prevCwd = process.cwd();
23
+ let tmp: string | undefined;
24
+
25
+ afterEach(async () => {
26
+ process.chdir(prevCwd);
27
+ if (tmp) {
28
+ await rm(tmp, { recursive: true, force: true });
29
+ tmp = undefined;
30
+ }
31
+ delete process.env.OKE_VAULT_URL;
32
+ delete process.env.OKE_VAULT_TOKEN;
33
+ delete process.env.REDIS_URL;
34
+ });
35
+
36
+ test("vault: drivers.vault env builds env chain layers", async () => {
37
+ process.env.APP_NAME = "honor-config";
38
+ try {
39
+ const result = await bootApplication({
40
+ env: "local",
41
+ secrets: [vault.config("APP_NAME", { description: "app" })],
42
+ config: {
43
+ drivers: {
44
+ vault: { local: "env", docker: "openbao", test: "memory", prod: "openbao" },
45
+ },
46
+ },
47
+ });
48
+ try {
49
+ expect(result.vault).toBeDefined();
50
+ expect(result.vault!.chainDriverIds).toContain("env");
51
+ expect(result.vault!.chainDriverIds).toContain("memory");
52
+ expect(result.vault!.chainDriverIds[0]).toBe("env");
53
+ } finally {
54
+ await result.close();
55
+ }
56
+ } finally {
57
+ delete process.env.APP_NAME;
58
+ }
59
+ });
60
+
61
+ test("vault: drivers.vault openbao with credentials selects openbao layer", () => {
62
+ process.env.OKE_VAULT_URL = "http://127.0.0.1:8200";
63
+ process.env.OKE_VAULT_TOKEN = "test-token";
64
+ const chain = buildVaultBootChain({
65
+ driverId: "openbao",
66
+ env: "local",
67
+ missingOpenbao: "throw",
68
+ });
69
+ expect(chain.map((l) => l.driver.id)).toContain("openbao");
70
+ expect(chain.map((l) => l.driver.id)).toContain("env");
71
+ });
72
+
73
+ test("vault: openbao without credentials fails loud in prod", async () => {
74
+ delete process.env.OKE_VAULT_URL;
75
+ delete process.env.OKE_VAULT_TOKEN;
76
+ await expect(
77
+ bootApplication({
78
+ env: "prod",
79
+ secrets: [vault.config("APP_NAME", { description: "app", dev: "x" })],
80
+ vault: { allowDevFallbacks: true },
81
+ config: {
82
+ drivers: {
83
+ vault: { prod: "openbao" },
84
+ },
85
+ },
86
+ }),
87
+ ).rejects.toThrow(/openbao.*OKE_VAULT_URL/);
88
+ });
89
+
90
+ test("clock: drivers.clock memory binds memory CronStore", async () => {
91
+ const result = await bootApplication({
92
+ env: "local",
93
+ startScheduler: false,
94
+ clocks: [clock("tick", { every: "1h" })],
95
+ config: {
96
+ drivers: {
97
+ clock: { local: "memory", docker: "file", test: "frozen", prod: "file" },
98
+ },
99
+ },
100
+ });
101
+ try {
102
+ expect(result.clock!.driverId).toBe("memory");
103
+ expect(result.clock!.store.kind).toBe("memory");
104
+ } finally {
105
+ await result.close();
106
+ }
107
+ });
108
+
109
+ test("clock: drivers.clock file binds file CronStore", async () => {
110
+ tmp = await mkdtemp(join(tmpdir(), "oke-clock-file-"));
111
+ process.chdir(tmp);
112
+ const result = await bootApplication({
113
+ env: "local",
114
+ startScheduler: false,
115
+ clocks: [clock("tick", { every: "1h" })],
116
+ config: {
117
+ drivers: {
118
+ clock: { local: "file", docker: "file", test: "frozen", prod: "file" },
119
+ },
120
+ },
121
+ });
122
+ try {
123
+ expect(result.clock!.driverId).toBe("file");
124
+ expect(result.clock!.store.kind).toBe("file");
125
+ } finally {
126
+ await result.close();
127
+ }
128
+ });
129
+
130
+ test("clock: drivers.clock postgres fails without DATABASE_URL", async () => {
131
+ const prevDb = process.env.DATABASE_URL;
132
+ const prevStore = process.env.OKE_STORE_SQL_URL;
133
+ delete process.env.DATABASE_URL;
134
+ delete process.env.OKE_STORE_SQL_URL;
135
+ try {
136
+ await expect(
137
+ bootApplication({
138
+ env: "local",
139
+ startScheduler: false,
140
+ clocks: [clock("tick", { every: "1h" })],
141
+ config: {
142
+ drivers: {
143
+ clock: { local: "postgres" },
144
+ },
145
+ },
146
+ }),
147
+ ).rejects.toThrow(/clock driver "postgres" needs DATABASE_URL/);
148
+ } finally {
149
+ if (prevDb !== undefined) process.env.DATABASE_URL = prevDb;
150
+ else delete process.env.DATABASE_URL;
151
+ if (prevStore !== undefined) process.env.OKE_STORE_SQL_URL = prevStore;
152
+ else delete process.env.OKE_STORE_SQL_URL;
153
+ }
154
+ });
155
+
156
+ test("journal: durable flow binds memory journal by default", async () => {
157
+ const result = await bootApplication({
158
+ env: "local",
159
+ startScheduler: false,
160
+ flows: [flow({ name: "charge", durable: true, do: () => ({ ok: true }) })],
161
+ });
162
+ try {
163
+ expect(result.journal?.driverId).toBe("memory");
164
+ expect(typeof result.journal?.instanceId).toBe("string");
165
+ expect(result.journal?.leaseMs).toBeGreaterThan(0);
166
+ } finally {
167
+ await result.close();
168
+ }
169
+ });
170
+
171
+ test("journal: no durable flow → no journal runtime", async () => {
172
+ const result = await bootApplication({
173
+ env: "local",
174
+ startScheduler: false,
175
+ flows: [flow({ name: "plain", do: () => ({ ok: true }) })],
176
+ });
177
+ try {
178
+ expect(result.journal).toBeUndefined();
179
+ } finally {
180
+ await result.close();
181
+ }
182
+ });
183
+
184
+ test("journal: drivers.journal file binds file store", async () => {
185
+ tmp = await mkdtemp(join(tmpdir(), "oke-journal-file-"));
186
+ process.chdir(tmp);
187
+ const result = await bootApplication({
188
+ env: "local",
189
+ startScheduler: false,
190
+ flows: [flow({ name: "charge", durable: true, do: () => ({ ok: true }) })],
191
+ config: {
192
+ drivers: {
193
+ journal: { local: "file", docker: "postgres", test: "memory", prod: "postgres" },
194
+ },
195
+ },
196
+ });
197
+ try {
198
+ expect(result.journal?.driverId).toBe("file");
199
+ } finally {
200
+ await result.close();
201
+ }
202
+ });
203
+
204
+ test("journal: drivers.journal postgres fails without DATABASE_URL", async () => {
205
+ const prevDb = process.env.DATABASE_URL;
206
+ const prevStore = process.env.OKE_STORE_SQL_URL;
207
+ delete process.env.DATABASE_URL;
208
+ delete process.env.OKE_STORE_SQL_URL;
209
+ try {
210
+ await expect(
211
+ bootApplication({
212
+ env: "local",
213
+ startScheduler: false,
214
+ flows: [flow({ name: "charge", durable: true, do: () => ({ ok: true }) })],
215
+ config: {
216
+ drivers: {
217
+ journal: { local: "postgres" },
218
+ },
219
+ },
220
+ }),
221
+ ).rejects.toThrow(/journal driver "postgres" needs DATABASE_URL/);
222
+ } finally {
223
+ if (prevDb !== undefined) process.env.DATABASE_URL = prevDb;
224
+ else delete process.env.DATABASE_URL;
225
+ if (prevStore !== undefined) process.env.OKE_STORE_SQL_URL = prevStore;
226
+ else delete process.env.OKE_STORE_SQL_URL;
227
+ }
228
+ });
229
+
230
+ test("journal: unknown driver fails loud", async () => {
231
+ await expect(
232
+ bootApplication({
233
+ env: "local",
234
+ startScheduler: false,
235
+ flows: [flow({ name: "charge", durable: true, do: () => ({ ok: true }) })],
236
+ config: {
237
+ drivers: {
238
+ journal: { local: "neon" },
239
+ },
240
+ },
241
+ }),
242
+ ).rejects.toThrow(/unknown journal driver "neon"/);
243
+ });
244
+
245
+ test("gate: drivers.store.kv redis opens redis-backed oke:gates", async () => {
246
+ const fake = createRedisFakeClient();
247
+ const result = await bootApplication({
248
+ env: "local",
249
+ gates: [gate.rate({ max: 10, per: "1m" })],
250
+ clients: { kv: fake },
251
+ config: {
252
+ drivers: {
253
+ store: {
254
+ kv: { local: "redis", docker: "redis", prod: "redis" },
255
+ },
256
+ },
257
+ },
258
+ });
259
+ try {
260
+ expect(result.gate!.kvDriverId).toBe("redis");
261
+ } finally {
262
+ await result.close();
263
+ }
264
+ });
265
+
266
+ test("gate: drivers.store.kv memory keeps memory oke:gates", async () => {
267
+ const result = await bootApplication({
268
+ env: "local",
269
+ gates: [gate.rate({ max: 10, per: "1m" })],
270
+ config: {
271
+ drivers: {
272
+ store: {
273
+ kv: { local: "memory", docker: "redis", prod: "redis" },
274
+ },
275
+ },
276
+ },
277
+ });
278
+ try {
279
+ expect(result.gate!.kvDriverId).toBe("memory");
280
+ } finally {
281
+ await result.close();
282
+ }
283
+ });
284
+
285
+ test("signal: drivers.signal memory binds memory", async () => {
286
+ const result = await bootApplication({
287
+ env: "local",
288
+ signals: [signal("ping", { delivery: "once" })],
289
+ config: {
290
+ drivers: {
291
+ signal: { local: "memory", docker: "redis", prod: "redis" },
292
+ },
293
+ },
294
+ });
295
+ try {
296
+ expect(result.signal!.driverId).toBe("memory");
297
+ } finally {
298
+ await result.close();
299
+ }
300
+ });
301
+
302
+ test("signal: drivers.signal redis binds redis", async () => {
303
+ const fake = createSignalRedisFake();
304
+ const result = await bootApplication({
305
+ env: "local",
306
+ signals: [signal("ping", { delivery: "once" })],
307
+ clients: { signalRedis: fake },
308
+ config: {
309
+ drivers: {
310
+ signal: { local: "redis", docker: "redis", prod: "redis" },
311
+ },
312
+ },
313
+ });
314
+ try {
315
+ expect(result.signal!.driverId).toBe("redis");
316
+ } finally {
317
+ await result.close();
318
+ }
319
+ });
320
+
321
+ test("signal: drivers.signal postgres fails loud (never silent memory)", async () => {
322
+ await expect(
323
+ bootApplication({
324
+ env: "local",
325
+ signals: [signal("ping", { delivery: "once" })],
326
+ config: {
327
+ drivers: {
328
+ signal: { local: "postgres" },
329
+ },
330
+ },
331
+ }),
332
+ ).rejects.toThrow(/signal driver "postgres"/);
333
+ });
334
+
335
+ test("signal: drivers.signal nats fails loud", async () => {
336
+ await expect(
337
+ bootApplication({
338
+ env: "local",
339
+ signals: [signal("ping", { delivery: "once" })],
340
+ config: {
341
+ drivers: {
342
+ signal: { local: "nats" },
343
+ },
344
+ },
345
+ }),
346
+ ).rejects.toThrow(/signal driver "nats"/);
347
+ });
348
+ });
@@ -0,0 +1,89 @@
1
+ /**
2
+ * Lazy journal binder — loaded only when a flow declares `durable: true`.
3
+ */
4
+
5
+ import { mkdirSync } from "node:fs";
6
+ import { dirname, resolve } from "node:path";
7
+ import {
8
+ createFileJournalStore,
9
+ createMemoryJournalStore,
10
+ JOURNAL_DEFAULT_LEASE_MS,
11
+ type JournalStore,
12
+ } from "../journal.ts";
13
+ import { createPostgresJournalStore } from "../../drivers/journal-postgres.ts";
14
+ import { resolveDriverId, type ConfigEnv } from "../../config/index.ts";
15
+ import type { BootOptions } from "../boot.ts";
16
+
17
+ /** Bound journal runtime — store + this instance's lease identity. */
18
+ export interface JournalRuntime {
19
+ readonly store: JournalStore;
20
+ readonly instanceId: string;
21
+ readonly leaseMs: number;
22
+ readonly driverId: string;
23
+ }
24
+
25
+ /** Result of binding a journal runtime. */
26
+ export interface BindJournalResult {
27
+ readonly journal: JournalRuntime;
28
+ }
29
+
30
+ /** Default on-disk journal path (single-host file driver). */
31
+ export const DEFAULT_FILE_JOURNAL_PATH = ".oke/journal.json";
32
+
33
+ /**
34
+ * Resolve `drivers.journal` for the active env (default `memory`).
35
+ *
36
+ * @param options - Boot options
37
+ * @param env - Active environment
38
+ */
39
+ 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";
43
+ }
44
+
45
+ /**
46
+ * Construct a journal store. Supported ids: `memory` · `file` · `postgres`.
47
+ *
48
+ * @param options - Boot options
49
+ * @param env - Active environment
50
+ */
51
+ export async function bindJournal(
52
+ options: BootOptions,
53
+ env: ConfigEnv,
54
+ ): Promise<BindJournalResult> {
55
+ const driver = resolveJournalDriverId(options, env);
56
+ const instanceId = options.instanceId ?? `inst-${crypto.randomUUID()}`;
57
+
58
+ let store: JournalStore;
59
+ if (driver === "memory") {
60
+ store = createMemoryJournalStore();
61
+ } else if (driver === "file") {
62
+ const path = resolve(process.cwd(), DEFAULT_FILE_JOURNAL_PATH);
63
+ mkdirSync(dirname(path), { recursive: true });
64
+ store = createFileJournalStore(path);
65
+ } else if (driver === "postgres") {
66
+ const url = process.env.DATABASE_URL ?? process.env.OKE_STORE_SQL_URL ?? undefined;
67
+ if (!url) {
68
+ throw new Error(
69
+ env === "docker"
70
+ ? 'oke boot: journal driver "postgres" needs DATABASE_URL (did `oke dev -d` write docker/.env.docker?)'
71
+ : 'oke boot: journal driver "postgres" needs DATABASE_URL',
72
+ );
73
+ }
74
+ store = await createPostgresJournalStore({ url });
75
+ } else {
76
+ throw new Error(
77
+ `oke boot: unknown journal driver "${driver}" (expected memory · file · postgres)`,
78
+ );
79
+ }
80
+
81
+ return {
82
+ journal: {
83
+ store,
84
+ instanceId,
85
+ leaseMs: options.journalLeaseMs ?? JOURNAL_DEFAULT_LEASE_MS,
86
+ driverId: driver,
87
+ },
88
+ };
89
+ }
@@ -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;
@@ -79,7 +79,7 @@ describe("boot — lazy element needs", () => {
79
79
  expect(needs.signal).toBe(false);
80
80
  });
81
81
 
82
- test("oke() Store-only graph stays under the prior 50 kB baseline", async () => {
82
+ test("oke() Store-only graph stays under the prior 51 kB baseline", async () => {
83
83
  const dir = await mkdtemp(join(tmpdir(), "oke-store-only-"));
84
84
  const entry = join(dir, "entry.ts");
85
85
  const appPath = join(import.meta.dir, "app.ts");
@@ -116,9 +116,11 @@ describe("boot — lazy element needs", () => {
116
116
  if (raw.byteLength === 0) continue;
117
117
  total += Bun.gzipSync(new Uint8Array(raw)).byteLength;
118
118
  }
119
- // Rebased after AI stream/tool-loop + fx.t/i18n on the shared fx surface
120
- // (~49.5 kB gzip). Still far below an eager full-element bind.
121
- expect(total).toBeLessThan(50_000);
119
+ // Rebased after fx.sendOtp/verifyOtp on the shared fx surface (~50.1 kB
120
+ // gzip), then again after the durable-journal lease surface (SKIP LOCKED
121
+ // claim/release/orphan-scan on journal.ts + app.ts wiring, ~51.2 kB).
122
+ // Clock/channel/journal drivers stay lazy-bound; far below eager bind.
123
+ expect(total).toBeLessThan(51_500);
122
124
  } finally {
123
125
  await rm(dir, { recursive: true, force: true });
124
126
  }