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
package/src/cli/index.ts
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
* `oke` CLI entry.
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
+
import { aiCli } from "./ai.ts";
|
|
6
7
|
import { branchCli } from "./branch.ts";
|
|
7
8
|
import { buildCli } from "./build.ts";
|
|
8
9
|
import { clientAddCli } from "./client-add.ts";
|
|
@@ -79,6 +80,10 @@ if (cmd === "eval") {
|
|
|
79
80
|
process.exit(await evalCli(sub ? [sub, ...rest] : rest));
|
|
80
81
|
}
|
|
81
82
|
|
|
83
|
+
if (cmd === "ai") {
|
|
84
|
+
process.exit(await aiCli(sub ? [sub, ...rest] : rest));
|
|
85
|
+
}
|
|
86
|
+
|
|
82
87
|
if (cmd === "branch") {
|
|
83
88
|
process.exit(await branchCli(sub ? [sub, ...rest] : rest));
|
|
84
89
|
}
|
package/src/cli/load-config.ts
CHANGED
|
@@ -47,14 +47,13 @@ export function defaultImagesFromConfig(config: OkeConfig): Readonly<Record<stri
|
|
|
47
47
|
const index = dockerOrProdId(config.drivers?.store?.index);
|
|
48
48
|
const kv = dockerOrProdId(config.drivers?.store?.kv);
|
|
49
49
|
const signal = dockerOrProdId(config.drivers?.signal);
|
|
50
|
-
const clock = dockerOrProdId(config.drivers?.clock);
|
|
51
50
|
|
|
51
|
+
// Only real SQL-consuming facets pull a SQL image. Clock/signal no longer
|
|
52
|
+
// advertise a fictional postgres CronStore / LISTEN bus via compose.
|
|
52
53
|
const needsSql =
|
|
53
54
|
sql === "postgres" ||
|
|
54
55
|
sql === "pgvector" ||
|
|
55
56
|
index === "pgvector" ||
|
|
56
|
-
signal === "postgres" ||
|
|
57
|
-
clock === "postgres" ||
|
|
58
57
|
(config.drivers?.prod ?? []).some((p) => p === "postgres" || p === "pgvector");
|
|
59
58
|
|
|
60
59
|
if (needsSql) {
|
|
@@ -62,7 +61,8 @@ export function defaultImagesFromConfig(config: OkeConfig): Readonly<Record<stri
|
|
|
62
61
|
sql === "pgvector" || index === "pgvector" ? DEFAULT_PGVECTOR_IMAGE : DEFAULT_SQL_IMAGE;
|
|
63
62
|
}
|
|
64
63
|
|
|
65
|
-
const needsKv =
|
|
64
|
+
const needsKv =
|
|
65
|
+
kv === "redis" || signal === "redis" || (config.drivers?.prod ?? []).includes("redis");
|
|
66
66
|
if (needsKv) {
|
|
67
67
|
out["store.kv"] = DEFAULT_KV_IMAGE;
|
|
68
68
|
}
|
|
@@ -221,8 +221,15 @@ export async function ensureOpenBao(
|
|
|
221
221
|
if (!existsSync(unsealPath) || !existsSync(rootPath)) {
|
|
222
222
|
throw new OpenBaoBootstrapError(
|
|
223
223
|
`openbao bootstrap: OpenBao is initialized but ${stateDir} is missing ` +
|
|
224
|
-
`unseal.key/root.token. Without the unseal key the data is unrecoverable
|
|
225
|
-
`
|
|
224
|
+
`unseal.key/root.token. Without the unseal key the vault data is unrecoverable.\n` +
|
|
225
|
+
`\n` +
|
|
226
|
+
`Usual cause in docker mode: a previous vault volume survived after the project\n` +
|
|
227
|
+
`(or .oke/openbao/) was deleted — Compose stop keeps volumes on purpose.\n` +
|
|
228
|
+
`\n` +
|
|
229
|
+
`Restore a backup of .oke/openbao/ if you have one.\n` +
|
|
230
|
+
`\n` +
|
|
231
|
+
`Dev reset (wipes vault secrets only, then re-init on next oke dev):\n` +
|
|
232
|
+
` cd docker && docker compose -f compose.yml -f compose.vault.yml down -v`,
|
|
226
233
|
);
|
|
227
234
|
}
|
|
228
235
|
unsealKey = readFileSync(unsealPath, "utf8").trim();
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Proves the acceptance criterion — set a secret, kill/restart the container
|
|
5
5
|
* on the same Raft volume, re-run the bootstrap unseal, and read the secret
|
|
6
|
-
* back.
|
|
6
|
+
* back. Opt-in via `OKE_TEST_DOCKER=1` plus a live Docker daemon.
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
9
|
import { describe, expect, test } from "bun:test";
|
|
@@ -13,10 +13,9 @@ import { join } from "node:path";
|
|
|
13
13
|
import { deriveInfrastructure, writeDerivedFiles } from "../docker/index.ts";
|
|
14
14
|
import { ensureOpenBao } from "./openbao-bootstrap.ts";
|
|
15
15
|
|
|
16
|
-
|
|
16
|
+
function dockerAvailable(): boolean {
|
|
17
17
|
try {
|
|
18
|
-
|
|
19
|
-
return (await proc.exited) === 0;
|
|
18
|
+
return Bun.spawnSync(["docker", "info"], { stdout: "pipe", stderr: "pipe" }).exitCode === 0;
|
|
20
19
|
} catch {
|
|
21
20
|
return false;
|
|
22
21
|
}
|
|
@@ -31,106 +30,116 @@ async function compose(project: string, dir: string, args: readonly string[]): P
|
|
|
31
30
|
return proc.exited;
|
|
32
31
|
}
|
|
33
32
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
const composeFiles = ["compose.yml", "compose.vault.yml"];
|
|
45
|
-
try {
|
|
46
|
-
const derived = deriveInfrastructure({
|
|
47
|
-
images: { vault: "openbao/openbao:2.6.1" },
|
|
48
|
-
app: "baotest",
|
|
49
|
-
includeApp: false,
|
|
50
|
-
composeDir: "docker",
|
|
51
|
-
});
|
|
52
|
-
await writeDerivedFiles(derived, dockerDir, { writeStackEnv: true });
|
|
53
|
-
const spec = derived.specs.find((s) => s.role === "vault")!;
|
|
54
|
-
const url = `http://127.0.0.1:${spec.hostPort}`;
|
|
55
|
-
|
|
56
|
-
const up = await compose(project, dockerDir, [
|
|
57
|
-
...composeFiles.flatMap((f) => ["-f", f]),
|
|
58
|
-
"up",
|
|
59
|
-
"-d",
|
|
60
|
-
]);
|
|
61
|
-
expect(up).toBe(0);
|
|
33
|
+
const WANT = process.env.OKE_TEST_DOCKER === "1";
|
|
34
|
+
const DOCKER = WANT && dockerAvailable();
|
|
35
|
+
if (!DOCKER) {
|
|
36
|
+
console.log(
|
|
37
|
+
WANT
|
|
38
|
+
? "skip: openbao restart e2e (docker daemon not available)"
|
|
39
|
+
: "skip: openbao restart e2e (OKE_TEST_DOCKER≠1)",
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
const live = DOCKER ? test : test.skip;
|
|
62
43
|
|
|
63
|
-
|
|
64
|
-
|
|
44
|
+
describe("openbao restart persistence (real container)", () => {
|
|
45
|
+
live(
|
|
46
|
+
"secret set before restart is readable after restart + unseal",
|
|
47
|
+
async () => {
|
|
48
|
+
const dir = await mkdtemp(join(tmpdir(), "oke-openbao-it-"));
|
|
49
|
+
const dockerDir = join(dir, "docker");
|
|
50
|
+
const project = `oke-bao-${Date.now()}`;
|
|
51
|
+
const composeFiles = ["compose.yml", "compose.vault.yml"];
|
|
65
52
|
try {
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
"logs",
|
|
76
|
-
"vault",
|
|
77
|
-
],
|
|
78
|
-
{ cwd: dockerDir, stdout: "pipe", stderr: "pipe" },
|
|
79
|
-
);
|
|
80
|
-
console.error(await new Response(logs.stdout).text());
|
|
81
|
-
console.error(await new Response(logs.stderr).text());
|
|
82
|
-
await logs.exited;
|
|
83
|
-
throw err;
|
|
84
|
-
}
|
|
85
|
-
expect(first.appToken.length).toBeGreaterThan(0);
|
|
53
|
+
const derived = deriveInfrastructure({
|
|
54
|
+
images: { vault: "openbao/openbao:2.6.1" },
|
|
55
|
+
app: "baotest",
|
|
56
|
+
includeApp: false,
|
|
57
|
+
composeDir: "docker",
|
|
58
|
+
});
|
|
59
|
+
await writeDerivedFiles(derived, dockerDir, { writeStackEnv: true });
|
|
60
|
+
const spec = derived.specs.find((s) => s.role === "vault")!;
|
|
61
|
+
const url = `http://127.0.0.1:${spec.hostPort}`;
|
|
86
62
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
expect(write.ok).toBe(true);
|
|
63
|
+
const up = await compose(project, dockerDir, [
|
|
64
|
+
...composeFiles.flatMap((f) => ["-f", f]),
|
|
65
|
+
"up",
|
|
66
|
+
"-d",
|
|
67
|
+
]);
|
|
68
|
+
expect(up).toBe(0);
|
|
94
69
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
...composeFiles.flatMap((f) => ["-f", f]),
|
|
98
|
-
"restart",
|
|
99
|
-
]);
|
|
100
|
-
expect(restart).toBe(0);
|
|
101
|
-
// Poll the API until the server is back (sealed or unsealed).
|
|
102
|
-
let apiUp = false;
|
|
103
|
-
for (let i = 0; i < 60; i++) {
|
|
70
|
+
const names = ["STRIPE_KEY"];
|
|
71
|
+
let first;
|
|
104
72
|
try {
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
73
|
+
first = await ensureOpenBao({ cwd: dir, url, names });
|
|
74
|
+
} catch (err) {
|
|
75
|
+
const logs = Bun.spawn(
|
|
76
|
+
[
|
|
77
|
+
"docker",
|
|
78
|
+
"compose",
|
|
79
|
+
"-p",
|
|
80
|
+
project,
|
|
81
|
+
...composeFiles.flatMap((f) => ["-f", f]),
|
|
82
|
+
"logs",
|
|
83
|
+
"vault",
|
|
84
|
+
],
|
|
85
|
+
{ cwd: dockerDir, stdout: "pipe", stderr: "pipe" },
|
|
86
|
+
);
|
|
87
|
+
console.error(await new Response(logs.stdout).text());
|
|
88
|
+
console.error(await new Response(logs.stderr).text());
|
|
89
|
+
await logs.exited;
|
|
90
|
+
throw err;
|
|
91
|
+
}
|
|
92
|
+
expect(first.appToken.length).toBeGreaterThan(0);
|
|
93
|
+
|
|
94
|
+
const write = await fetch(`${url}/v1/secret/data/STRIPE_KEY`, {
|
|
95
|
+
method: "POST",
|
|
96
|
+
headers: { "X-Vault-Token": first.appToken, "content-type": "application/json" },
|
|
97
|
+
body: JSON.stringify({ data: { value: "sk_restart_persistent" } }),
|
|
98
|
+
});
|
|
99
|
+
if (!write.ok) console.error("write failed", write.status, await write.text());
|
|
100
|
+
expect(write.ok).toBe(true);
|
|
101
|
+
|
|
102
|
+
// Kill + start the same volume (a real restart, not a fresh stack).
|
|
103
|
+
const restart = await compose(project, dockerDir, [
|
|
104
|
+
...composeFiles.flatMap((f) => ["-f", f]),
|
|
105
|
+
"restart",
|
|
106
|
+
]);
|
|
107
|
+
expect(restart).toBe(0);
|
|
108
|
+
// Poll the API until the server is back (sealed or unsealed).
|
|
109
|
+
let apiUp = false;
|
|
110
|
+
for (let i = 0; i < 60; i++) {
|
|
111
|
+
try {
|
|
112
|
+
const probe = await fetch(`${url}/v1/sys/seal-status`);
|
|
113
|
+
if (probe.ok) {
|
|
114
|
+
apiUp = true;
|
|
115
|
+
break;
|
|
116
|
+
}
|
|
117
|
+
} catch {
|
|
118
|
+
// not up yet
|
|
109
119
|
}
|
|
110
|
-
|
|
111
|
-
// not up yet
|
|
120
|
+
await Bun.sleep(500);
|
|
112
121
|
}
|
|
113
|
-
|
|
114
|
-
}
|
|
115
|
-
expect(apiUp).toBe(true);
|
|
122
|
+
expect(apiUp).toBe(true);
|
|
116
123
|
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
124
|
+
const second = await ensureOpenBao({ cwd: dir, url, names });
|
|
125
|
+
expect(second.initializedNow).toBe(false);
|
|
126
|
+
expect(second.appToken).toBe(first.appToken);
|
|
120
127
|
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
128
|
+
const read = await fetch(`${url}/v1/secret/data/STRIPE_KEY`, {
|
|
129
|
+
headers: { "X-Vault-Token": second.appToken },
|
|
130
|
+
});
|
|
131
|
+
expect(read.ok).toBe(true);
|
|
132
|
+
const body = (await read.json()) as { data?: { data?: { value?: string } } };
|
|
133
|
+
expect(body.data?.data?.value).toBe("sk_restart_persistent");
|
|
134
|
+
} finally {
|
|
135
|
+
await compose(project, dockerDir, [
|
|
136
|
+
...composeFiles.flatMap((f) => ["-f", f]),
|
|
137
|
+
"down",
|
|
138
|
+
"-v",
|
|
139
|
+
]).catch(() => 0);
|
|
140
|
+
await rm(dir, { recursive: true, force: true }).catch(() => {});
|
|
141
|
+
}
|
|
142
|
+
},
|
|
143
|
+
180_000,
|
|
144
|
+
);
|
|
136
145
|
});
|
package/src/cli/registry.ts
CHANGED
|
@@ -287,7 +287,7 @@ export const OKE_COMMANDS: readonly CliCommand[] = [
|
|
|
287
287
|
},
|
|
288
288
|
{
|
|
289
289
|
name: "docker",
|
|
290
|
-
summary: "
|
|
290
|
+
summary: "derive compose · clean leftover stacks",
|
|
291
291
|
leaf: true,
|
|
292
292
|
flags: [
|
|
293
293
|
{
|
|
@@ -300,6 +300,24 @@ export const OKE_COMMANDS: readonly CliCommand[] = [
|
|
|
300
300
|
MANIFEST,
|
|
301
301
|
HELP,
|
|
302
302
|
],
|
|
303
|
+
subcommands: [
|
|
304
|
+
{
|
|
305
|
+
name: "clean",
|
|
306
|
+
summary: "Remove oke-dev stacks (containers, networks, volumes)",
|
|
307
|
+
flags: [
|
|
308
|
+
{
|
|
309
|
+
long: "--yes",
|
|
310
|
+
short: "-y",
|
|
311
|
+
summary: "Non-interactive confirm",
|
|
312
|
+
},
|
|
313
|
+
{
|
|
314
|
+
long: "--all",
|
|
315
|
+
summary: "Every oke-dev-* stack (required with --yes outside a project)",
|
|
316
|
+
},
|
|
317
|
+
HELP,
|
|
318
|
+
],
|
|
319
|
+
},
|
|
320
|
+
],
|
|
303
321
|
},
|
|
304
322
|
{
|
|
305
323
|
name: "images",
|
|
@@ -346,6 +364,56 @@ export const OKE_COMMANDS: readonly CliCommand[] = [
|
|
|
346
364
|
leaf: true,
|
|
347
365
|
flags: [MANIFEST, HELP],
|
|
348
366
|
},
|
|
367
|
+
{
|
|
368
|
+
name: "ai",
|
|
369
|
+
summary: "configure AI driver + models",
|
|
370
|
+
subcommands: [
|
|
371
|
+
{
|
|
372
|
+
name: "setup",
|
|
373
|
+
summary: "Interactive / flagged AI provider + model setup",
|
|
374
|
+
flags: [
|
|
375
|
+
{
|
|
376
|
+
long: "--provider",
|
|
377
|
+
takesValue: true,
|
|
378
|
+
valueName: "id",
|
|
379
|
+
summary: "ollama · openai · anthropic · gemini · lmstudio · openrouter · custom",
|
|
380
|
+
},
|
|
381
|
+
{
|
|
382
|
+
long: "--chat",
|
|
383
|
+
takesValue: true,
|
|
384
|
+
valueName: "model",
|
|
385
|
+
summary: "Chat model id",
|
|
386
|
+
},
|
|
387
|
+
{
|
|
388
|
+
long: "--vision",
|
|
389
|
+
takesValue: true,
|
|
390
|
+
valueName: "model",
|
|
391
|
+
summary: "Vision model id",
|
|
392
|
+
},
|
|
393
|
+
{
|
|
394
|
+
long: "--embed",
|
|
395
|
+
takesValue: true,
|
|
396
|
+
valueName: "model",
|
|
397
|
+
summary: "Embedding model id",
|
|
398
|
+
},
|
|
399
|
+
{
|
|
400
|
+
long: "--pull",
|
|
401
|
+
summary: "Pull missing Ollama models (default)",
|
|
402
|
+
},
|
|
403
|
+
{
|
|
404
|
+
long: "--no-pull",
|
|
405
|
+
summary: "Skip ollama pull",
|
|
406
|
+
},
|
|
407
|
+
{
|
|
408
|
+
long: "--yes",
|
|
409
|
+
short: "-y",
|
|
410
|
+
summary: "Non-interactive (requires --provider)",
|
|
411
|
+
},
|
|
412
|
+
HELP,
|
|
413
|
+
],
|
|
414
|
+
},
|
|
415
|
+
],
|
|
416
|
+
},
|
|
349
417
|
{
|
|
350
418
|
name: "branch",
|
|
351
419
|
summary: "fork journaled state",
|
|
@@ -459,6 +527,16 @@ export function flagTokensFor(command: CliCommand, sub?: string): readonly strin
|
|
|
459
527
|
}
|
|
460
528
|
|
|
461
529
|
function formatCommandUsage(cmd: CliCommand): string {
|
|
530
|
+
// Leaf + subcommands (e.g. `oke docker` derive · `oke docker clean`).
|
|
531
|
+
if (cmd.leaf && cmd.subcommands && cmd.subcommands.length > 0) {
|
|
532
|
+
const flagHint = cmd.flags
|
|
533
|
+
?.filter((f) => f.long !== "--help")
|
|
534
|
+
.slice(0, 2)
|
|
535
|
+
.map((f) => formatFlagHint(f))
|
|
536
|
+
.join(" ");
|
|
537
|
+
const subs = cmd.subcommands.map((s) => s.name).join("|");
|
|
538
|
+
return `oke ${cmd.name}${flagHint ? ` ${flagHint}` : ""} · ${subs}`;
|
|
539
|
+
}
|
|
462
540
|
if (cmd.subcommands?.length === 1) {
|
|
463
541
|
const sub = cmd.subcommands[0]!;
|
|
464
542
|
const flagHint = sub.flags
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Evidence: createClient has no live-signal subscription today.
|
|
3
|
+
*
|
|
4
|
+
* `delivery: "live"` is consumed server-side via `bus.live()` (and
|
|
5
|
+
* `on(signal, flow)` for flow subscribers). `okengine/client` exposes only
|
|
6
|
+
* HTTP REST / RPC — no SSE, WebSocket, or `api.live(…)`.
|
|
7
|
+
*
|
|
8
|
+
* Design for `client.live(signalName)` is tracked with the Signal docs page;
|
|
9
|
+
* do not treat this file as a stub to fill — ship the real surface when ready.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { describe, expect, test } from "bun:test";
|
|
13
|
+
|
|
14
|
+
import * as client from "./index.ts";
|
|
15
|
+
import { createClient } from "./create.ts";
|
|
16
|
+
|
|
17
|
+
describe("createClient — live signal gap", () => {
|
|
18
|
+
test("public client module exports no live / subscribe helpers", () => {
|
|
19
|
+
const keys = Object.keys(client).sort();
|
|
20
|
+
expect(keys).not.toContain("live");
|
|
21
|
+
expect(keys).not.toContain("subscribe");
|
|
22
|
+
expect(keys).toContain("createClient");
|
|
23
|
+
expect(keys).toContain("createTransport");
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
test("createClient builds an HTTP call proxy only (no live method on the root)", async () => {
|
|
27
|
+
const api = createClient("http://app.test", {
|
|
28
|
+
fetch: async () => Response.json({ data: null, error: { code: "NotFound", data: {} } }),
|
|
29
|
+
});
|
|
30
|
+
// Root is a callable proxy for unit.flow — live is not a first-class API.
|
|
31
|
+
expect(typeof api).toBe("function");
|
|
32
|
+
expect(Object.prototype.hasOwnProperty.call(api, "live")).toBe(false);
|
|
33
|
+
expect("live" in (api as object)).toBe(false);
|
|
34
|
+
});
|
|
35
|
+
});
|
|
@@ -47,6 +47,27 @@ describe("transport — retry", () => {
|
|
|
47
47
|
expect(error?.code).toBe("TransportError");
|
|
48
48
|
expect(n).toBe(2);
|
|
49
49
|
});
|
|
50
|
+
|
|
51
|
+
test("structured 5xx envelope is returned (not TransportError)", async () => {
|
|
52
|
+
const api = createClient<PingApp>("http://app.test", {
|
|
53
|
+
fetch: async () =>
|
|
54
|
+
Response.json(
|
|
55
|
+
{
|
|
56
|
+
data: null,
|
|
57
|
+
error: {
|
|
58
|
+
code: "InternalError",
|
|
59
|
+
data: {},
|
|
60
|
+
message: "password policy failed: minLength 12",
|
|
61
|
+
},
|
|
62
|
+
},
|
|
63
|
+
{ status: 500 },
|
|
64
|
+
),
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
const { error } = await api.sys.ping();
|
|
68
|
+
expect(error?.code).toBe("InternalError");
|
|
69
|
+
expect(error?.message).toMatch(/password policy/i);
|
|
70
|
+
});
|
|
50
71
|
});
|
|
51
72
|
|
|
52
73
|
describe("transport — timeout", () => {
|
package/src/client/transport.ts
CHANGED
|
@@ -44,6 +44,11 @@ export function createTransport(base: string, opts: ClientOptions = {}): Transpo
|
|
|
44
44
|
await opts.auth.refresh();
|
|
45
45
|
continue;
|
|
46
46
|
}
|
|
47
|
+
if (res.status >= 500) {
|
|
48
|
+
const structured = await decodeIfEnvelope(res);
|
|
49
|
+
if (structured) return structured;
|
|
50
|
+
throw new Error(`HTTP ${res.status}`);
|
|
51
|
+
}
|
|
47
52
|
return decode(res);
|
|
48
53
|
} catch (err) {
|
|
49
54
|
const transient = isTransient(err);
|
|
@@ -110,21 +115,38 @@ async function once(
|
|
|
110
115
|
ctrl && opts.timeout !== undefined ? setTimeout(() => ctrl.abort(), opts.timeout) : undefined;
|
|
111
116
|
|
|
112
117
|
try {
|
|
113
|
-
|
|
118
|
+
return await fetchFn(url, {
|
|
114
119
|
method,
|
|
115
120
|
headers,
|
|
116
121
|
body,
|
|
117
122
|
signal: ctrl?.signal,
|
|
118
123
|
});
|
|
119
|
-
if (res.status >= 500) {
|
|
120
|
-
throw new Error(`HTTP ${res.status}`);
|
|
121
|
-
}
|
|
122
|
-
return res;
|
|
123
124
|
} finally {
|
|
124
125
|
if (timer !== undefined) clearTimeout(timer);
|
|
125
126
|
}
|
|
126
127
|
}
|
|
127
128
|
|
|
129
|
+
/**
|
|
130
|
+
* Decode a JSON `{ data, error }` envelope from a 5xx response, or `null`
|
|
131
|
+
* when the body is not a structured OKE failure (retry as transport error).
|
|
132
|
+
*
|
|
133
|
+
* @param res - HTTP response (body consumed)
|
|
134
|
+
*/
|
|
135
|
+
async function decodeIfEnvelope(res: Response): Promise<ClientResult | null> {
|
|
136
|
+
const text = await res.text();
|
|
137
|
+
if (!text) return null;
|
|
138
|
+
let json: unknown;
|
|
139
|
+
try {
|
|
140
|
+
json = JSON.parse(text);
|
|
141
|
+
} catch {
|
|
142
|
+
return null;
|
|
143
|
+
}
|
|
144
|
+
if (json !== null && typeof json === "object" && "data" in json && "error" in json) {
|
|
145
|
+
return json as ClientResult;
|
|
146
|
+
}
|
|
147
|
+
return null;
|
|
148
|
+
}
|
|
149
|
+
|
|
128
150
|
function rpcRequest(
|
|
129
151
|
base: string,
|
|
130
152
|
key: string,
|
|
@@ -9,7 +9,7 @@ export default defineConfig({
|
|
|
9
9
|
files: { prod: "s3" },
|
|
10
10
|
index: { prod: "pgvector" },
|
|
11
11
|
},
|
|
12
|
-
signal: { prod: "
|
|
12
|
+
signal: { prod: "redis" },
|
|
13
13
|
vault: { prod: "openbao" },
|
|
14
14
|
channel: { email: { prod: "smtp" } },
|
|
15
15
|
ai: { prod: { driver: "anthropic" } },
|
|
@@ -26,7 +26,7 @@ describe("fillDockerFromProd / defineConfig", () => {
|
|
|
26
26
|
});
|
|
27
27
|
|
|
28
28
|
test("vault prod → docker copies the prod driver", () => {
|
|
29
|
-
expect(fillDockerFromProd({ local: "
|
|
29
|
+
expect(fillDockerFromProd({ local: "env", prod: "openbao" })?.docker).toBe("openbao");
|
|
30
30
|
});
|
|
31
31
|
|
|
32
32
|
test("defineConfig fills every element from prod", () => {
|
|
@@ -37,9 +37,9 @@ describe("fillDockerFromProd / defineConfig", () => {
|
|
|
37
37
|
kv: { local: "memory", prod: "redis" },
|
|
38
38
|
files: { local: "fs", prod: "s3" },
|
|
39
39
|
},
|
|
40
|
-
signal: { local: "memory", prod: "
|
|
41
|
-
clock: { local: "memory", prod: "
|
|
42
|
-
vault: { local: "
|
|
40
|
+
signal: { local: "memory", prod: "redis" },
|
|
41
|
+
clock: { local: "memory", prod: "file" },
|
|
42
|
+
vault: { local: "env", prod: "openbao" },
|
|
43
43
|
channel: {
|
|
44
44
|
email: { local: "console", prod: "smtp" },
|
|
45
45
|
},
|
|
@@ -49,8 +49,8 @@ describe("fillDockerFromProd / defineConfig", () => {
|
|
|
49
49
|
expect(cfg.drivers?.store?.sql?.docker).toBe("postgres");
|
|
50
50
|
expect(cfg.drivers?.store?.kv?.docker).toBe("redis");
|
|
51
51
|
expect(cfg.drivers?.store?.files?.docker).toBe("s3");
|
|
52
|
-
expect(cfg.drivers?.signal?.docker).toBe("
|
|
53
|
-
expect(cfg.drivers?.clock?.docker).toBe("
|
|
52
|
+
expect(cfg.drivers?.signal?.docker).toBe("redis");
|
|
53
|
+
expect(cfg.drivers?.clock?.docker).toBe("file");
|
|
54
54
|
expect(cfg.drivers?.vault?.docker).toBe("openbao");
|
|
55
55
|
expect(cfg.drivers?.channel?.email?.docker).toBe("smtp");
|
|
56
56
|
expect(cfg.drivers?.ai?.docker).toBe("anthropic");
|
package/src/config/index.ts
CHANGED
|
@@ -344,7 +344,7 @@ export function fillDriversDockerFromProd(
|
|
|
344
344
|
* Define an `oke.config.ts` document.
|
|
345
345
|
*
|
|
346
346
|
* Missing `docker` driver pins are filled from `prod` so `oke dev -d` uses the
|
|
347
|
-
* same server protocols as production (vault →
|
|
347
|
+
* same server protocols as production (vault → `env` for `docker/.env.docker`).
|
|
348
348
|
* Legacy `dev`/`stack` keys are normalized to `local`/`docker`.
|
|
349
349
|
*
|
|
350
350
|
* @param config - Driver / tenancy / i18n / topology
|
|
@@ -53,6 +53,51 @@ describe("console kernel", () => {
|
|
|
53
53
|
}
|
|
54
54
|
});
|
|
55
55
|
|
|
56
|
+
test("claim rejects weak passwords with ClaimFailed password_policy (not 500)", async () => {
|
|
57
|
+
const handle = await startConsoleApp({
|
|
58
|
+
cwd,
|
|
59
|
+
secret: "test-secret-console",
|
|
60
|
+
silentClaim: true,
|
|
61
|
+
});
|
|
62
|
+
try {
|
|
63
|
+
const weak = await handle.app.fetch(
|
|
64
|
+
new Request("http://console.test/console/setup/claim", {
|
|
65
|
+
method: "POST",
|
|
66
|
+
headers: { "content-type": "application/json" },
|
|
67
|
+
body: JSON.stringify({
|
|
68
|
+
claimCode: handle.state.claim.code,
|
|
69
|
+
email: "ops@example.com",
|
|
70
|
+
name: "Ops",
|
|
71
|
+
password: "short1A",
|
|
72
|
+
}),
|
|
73
|
+
}),
|
|
74
|
+
);
|
|
75
|
+
// Zod min(12) rejects before createOperator — still not an opaque 500.
|
|
76
|
+
expect(weak.status).toBe(422);
|
|
77
|
+
const shortOkZod = await handle.app.fetch(
|
|
78
|
+
new Request("http://console.test/console/setup/claim", {
|
|
79
|
+
method: "POST",
|
|
80
|
+
headers: { "content-type": "application/json" },
|
|
81
|
+
body: JSON.stringify({
|
|
82
|
+
claimCode: handle.state.claim.code,
|
|
83
|
+
email: "ops@example.com",
|
|
84
|
+
name: "Ops",
|
|
85
|
+
password: "longenoughword",
|
|
86
|
+
}),
|
|
87
|
+
}),
|
|
88
|
+
);
|
|
89
|
+
expect(shortOkZod.status).toBe(400);
|
|
90
|
+
const body = (await shortOkZod.json()) as {
|
|
91
|
+
error: { code: string; message?: string; data?: { reason?: string } };
|
|
92
|
+
};
|
|
93
|
+
expect(body.error.code).toBe("ClaimFailed");
|
|
94
|
+
expect(body.error.data?.reason).toBe("password_policy");
|
|
95
|
+
expect(body.error.message).toMatch(/12 characters/i);
|
|
96
|
+
} finally {
|
|
97
|
+
await handle.app.stop();
|
|
98
|
+
}
|
|
99
|
+
});
|
|
100
|
+
|
|
56
101
|
test("claim creates first operator and closes the wizard permanently", async () => {
|
|
57
102
|
const handle = await startConsoleApp({
|
|
58
103
|
cwd,
|