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.
- package/AGENTS.md +2 -2
- package/README.md +98 -71
- package/package.json +4 -4
- package/site/content/docs/console/clock.mdx +25 -5
- package/site/content/docs/console/signals.mdx +13 -4
- package/site/content/docs/elements/ai.mdx +29 -2
- package/site/content/docs/elements/channel.mdx +90 -20
- package/site/content/docs/elements/clock.mdx +56 -24
- package/site/content/docs/elements/flow.mdx +2 -0
- package/site/content/docs/elements/gate.mdx +8 -1
- package/site/content/docs/elements/signal.mdx +173 -29
- package/site/content/docs/elements/store.mdx +464 -73
- package/site/content/docs/elements/vault.mdx +15 -5
- package/site/content/docs/get-started/basic-usage.mdx +13 -6
- package/site/content/docs/get-started/installation.mdx +49 -13
- package/site/content/docs/get-started/introduction.mdx +1 -1
- package/site/content/docs/reference/cli.md +30 -2
- package/site/content/docs/reference/configuration.mdx +18 -17
- package/site/content/docs/reference/environment-variables.mdx +44 -18
- package/site/content/docs/reference/errors.mdx +12 -11
- package/site/content/docs/reference/fx.mdx +15 -13
- package/site/content/docs/reference/security.md +3 -1
- package/src/cli/ai-setup/ai-setup.test.ts +144 -0
- package/src/cli/ai-setup/apply.ts +214 -0
- package/src/cli/ai-setup/catalog.ts +263 -0
- package/src/cli/ai-setup/detect-ollama.ts +166 -0
- package/src/cli/ai-setup/index.ts +228 -0
- package/src/cli/ai-setup/prompts.ts +649 -0
- package/src/cli/ai-setup/recommend.test.ts +100 -0
- package/src/cli/ai-setup/recommend.ts +203 -0
- package/src/cli/ai.ts +38 -0
- package/src/cli/db-auto-push.test.ts +6 -1
- package/src/cli/db-auto-push.ts +8 -3
- package/src/cli/dev-db-push.test.ts +59 -0
- package/src/cli/dev.test.ts +40 -0
- package/src/cli/dev.ts +55 -1
- package/src/cli/docker-clean.test.ts +179 -0
- package/src/cli/docker-clean.ts +406 -0
- package/src/cli/docker.ts +16 -4
- package/src/cli/ensure-drizzle-config.ts +1 -1
- package/src/cli/hero-meta.test.ts +6 -5
- package/src/cli/hero-meta.ts +7 -1
- package/src/cli/index.ts +5 -0
- package/src/cli/load-config.ts +4 -4
- package/src/cli/openbao-bootstrap.test.ts +1 -0
- package/src/cli/openbao-bootstrap.ts +9 -2
- package/src/cli/openbao-restart.integration.test.ts +106 -97
- package/src/cli/registry.ts +79 -1
- package/src/client/live-gap.test.ts +35 -0
- package/src/client/transport.test.ts +21 -0
- package/src/client/transport.ts +27 -5
- package/src/compiler/fixtures/skyport/oke.config.ts +1 -1
- package/src/config/define-config.test.ts +6 -6
- package/src/config/index.ts +1 -1
- package/src/console/server/console.test.ts +45 -0
- package/src/console/server/flows.ts +39 -9
- package/src/console/server/i18n.ts +41 -0
- package/src/console/server/vault.ts +12 -38
- package/src/console/ui/dist/assets/index-ClX0ZCe5.js +10 -0
- package/src/console/ui/dist/assets/{panel-access-BGv45snf.js → panel-access-ri1X9Otj.js} +1 -1
- package/src/console/ui/dist/assets/{panel-ai-B2S7LEii.js → panel-ai-DLHjRoJk.js} +1 -1
- package/src/console/ui/dist/assets/{panel-architecture-D7UJh91v.js → panel-architecture-DI3bQF3J.js} +1 -1
- package/src/console/ui/dist/assets/{panel-channels-9T3ybqRu.js → panel-channels-BDYWX29p.js} +1 -1
- package/src/console/ui/dist/assets/{panel-clock-Cb1UXGRQ.js → panel-clock-i-JpTjVB.js} +1 -1
- package/src/console/ui/dist/assets/{panel-diff-DmYbKWmN.js → panel-diff-CFuWxiXy.js} +1 -1
- package/src/console/ui/dist/assets/{panel-flows-PiHwT55z.js → panel-flows-B8TRhcoe.js} +1 -1
- package/src/console/ui/dist/assets/{panel-gates-BQGYXvjT.js → panel-gates-CfkBgco7.js} +1 -1
- package/src/console/ui/dist/assets/{panel-overview-BBnRO18l.js → panel-overview-DONDzBd2.js} +1 -1
- package/src/console/ui/dist/assets/{panel-plugins-D0PsmVw2.js → panel-plugins-DDJwTehL.js} +1 -1
- package/src/console/ui/dist/assets/{panel-runs-CWuRDe0r.js → panel-runs-D5zf-D9c.js} +1 -1
- package/src/console/ui/dist/assets/{panel-signals-Bbg4ewpP.js → panel-signals-Dsxdu_AR.js} +1 -1
- package/src/console/ui/dist/assets/{panel-store-CPCbsDRa.js → panel-store-hy7O8HOs.js} +1 -1
- package/src/console/ui/dist/assets/{panel-traces-DVAzuA_S.js → panel-traces-CBiAe5go.js} +1 -1
- package/src/console/ui/dist/assets/{panel-vault-D1_MvOmo.js → panel-vault-Za1GyJfM.js} +1 -1
- package/src/console/ui/dist/index.html +1 -1
- package/src/console/ui/shell/components/ui.tsx +5 -1
- package/src/console/ui/shell/setup/Wizard.tsx +26 -5
- package/src/docker/cleanup.test.ts +193 -0
- package/src/docker/cleanup.ts +355 -0
- package/src/docker/docker.test.ts +4 -0
- package/src/docker/dockerfile.integration.test.ts +126 -119
- package/src/docker/index.ts +19 -0
- package/src/docker/recipes/ollama.ts +10 -5
- package/src/docker/stack.integration.test.ts +118 -102
- package/src/drivers/ai-ollama-tools.integration.test.ts +8 -6
- package/src/drivers/ai-ollama.integration.test.ts +3 -19
- package/src/drivers/ai-ollama.ts +45 -2
- package/src/drivers/ai-providers.test.ts +31 -0
- package/src/drivers/channel-fcm.ts +49 -53
- package/src/drivers/channel-msegat.ts +61 -0
- package/src/drivers/channel-sently-map.ts +57 -0
- package/src/drivers/channel-sently.test.ts +99 -0
- package/src/drivers/channel-sndr.ts +28 -0
- package/src/drivers/channel-taqnyat.ts +57 -0
- package/src/drivers/channel-types.ts +79 -2
- package/src/drivers/channel-unifonic.ts +26 -43
- package/src/drivers/channel-wa-cloud.ts +33 -47
- package/src/drivers/channel-webpush.ts +39 -239
- package/src/drivers/index.ts +7 -0
- package/src/drivers/signal-engine.ts +98 -25
- package/src/drivers/signal-nats.ts +4 -4
- package/src/drivers/signal-postgres.ts +158 -64
- package/src/drivers/signal-redis.ts +4 -4
- package/src/drivers/signal-types.ts +61 -2
- package/src/drivers/vault-driver-removal.test.ts +5 -1
- package/src/elements/channel/costs.test.ts +2 -2
- package/src/elements/channel/costs.ts +14 -2
- package/src/elements/channel/mime.ts +11 -0
- package/src/elements/channel/runtime.ts +94 -0
- package/src/elements/channel/sndr-webhooks.test.ts +26 -0
- package/src/elements/channel.ts +10 -1
- package/src/elements/clock/chaos-child.ts +150 -0
- package/src/elements/clock/chaos.test.ts +204 -0
- package/src/elements/clock/reconcile.ts +110 -0
- package/src/elements/clock/runtime.ts +11 -0
- package/src/elements/clock.test.ts +164 -2
- package/src/elements/clock.ts +1 -0
- package/src/elements/gate/runtime.ts +6 -0
- package/src/elements/index.ts +9 -0
- package/src/elements/signal/chaos-child.ts +41 -4
- package/src/elements/signal/declare.ts +4 -1
- package/src/elements/signal/delivery-modes.test.ts +179 -0
- package/src/elements/signal/key-ordering.test.ts +306 -0
- package/src/elements/signal/lease-reclaim.test.ts +123 -0
- package/src/elements/signal/optional-emit.test.ts +76 -0
- package/src/elements/signal/order-lifecycle.test.ts +130 -0
- package/src/elements/signal/orphan-messages.test.ts +152 -0
- package/src/elements/signal/runtime.ts +11 -4
- package/src/elements/signal/schema-emit.test.ts +107 -0
- package/src/elements/signal.test.ts +89 -5
- package/src/elements/store/files-fx.ts +104 -0
- package/src/elements/store/files-image.test.ts +251 -0
- package/src/elements/store/files-image.ts +494 -0
- package/src/elements/store/runtime.ts +60 -0
- package/src/elements/store.ts +24 -0
- package/src/elements/vault/boot-chain.ts +150 -0
- package/src/elements/vault/runtime.ts +8 -0
- package/src/kernel/app.ts +3 -1
- package/src/kernel/boot-bind/channel.test.ts +68 -3
- package/src/kernel/boot-bind/channel.ts +93 -2
- package/src/kernel/boot-bind/clock.ts +52 -7
- package/src/kernel/boot-bind/gate.ts +71 -3
- package/src/kernel/boot-bind/honor-config.test.ts +247 -0
- package/src/kernel/boot-bind/signal.ts +64 -7
- package/src/kernel/boot-bind/vault.ts +27 -8
- package/src/kernel/boot.ts +10 -2
- package/src/kernel/errors.ts +8 -2
- package/src/kernel/fx.test.ts +13 -0
- package/src/kernel/fx.ts +27 -4
- package/src/plugins/auth-delivery.mailpit.integration.test.ts +10 -4
- package/src/release/exports.test.ts +26 -0
- package/src/release/exports.ts +64 -5
- package/src/release/index.ts +5 -0
- package/src/release/measure.exports.test.ts +13 -1
- package/src/release/measure.ts +76 -13
- package/src/release/official-plugins.ts +46 -0
- package/src/release/readme.test.ts +30 -2
- package/src/runtime/dev-request-log.test.ts +20 -1
- package/src/runtime/dev-request-log.ts +38 -0
- package/src/term.test.ts +15 -0
- package/src/term.ts +8 -3
- 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 =
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
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 {
|
|
5
|
+
import { resolveDriverId, type ConfigEnv } from "../../config/index.ts";
|
|
6
6
|
import { createVaultRuntime, type VaultRuntime } from "../../elements/vault.ts";
|
|
7
|
-
import
|
|
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
|
|
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;
|
package/src/kernel/boot.ts
CHANGED
|
@@ -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
|
package/src/kernel/errors.ts
CHANGED
|
@@ -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
|
|
147
|
-
*
|
|
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.
|
package/src/kernel/fx.test.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
*
|
|
5
|
-
* Meilisearch (`const live = … ? test : test.skip`).
|
|
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
|
|
40
|
+
const WANT = process.env.OKE_TEST_DOCKER === "1";
|
|
41
|
+
const DOCKER = WANT && dockerAvailable();
|
|
40
42
|
if (!DOCKER) {
|
|
41
|
-
console.log(
|
|
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
|
});
|