okengine 0.6.1 → 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 +73 -181
- package/package.json +2 -2
- 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 +19 -13
- 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 +13 -13
- package/site/content/docs/reference/environment-variables.mdx +12 -10
- 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/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/index.ts +19 -0
- package/src/docker/recipes/ollama.ts +10 -5
- package/src/drivers/ai-ollama.ts +45 -2
- package/src/drivers/ai-providers.test.ts +31 -0
- package/src/drivers/index.ts +3 -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/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/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/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/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();
|
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,
|
|
@@ -7,8 +7,10 @@
|
|
|
7
7
|
import { z } from "zod";
|
|
8
8
|
import {
|
|
9
9
|
authenticateOperator,
|
|
10
|
+
BreachCheckError,
|
|
10
11
|
createOperator,
|
|
11
12
|
issueSession,
|
|
13
|
+
PasswordPolicyError,
|
|
12
14
|
scopesForRoles,
|
|
13
15
|
userPrincipal,
|
|
14
16
|
type IssuedSession,
|
|
@@ -24,6 +26,7 @@ import { maskWideEventForConsole, piiFieldNamesFromManifest } from "./runs-pii.t
|
|
|
24
26
|
import { ClockResourceNotFoundError, ScheduleNotOverridableError } from "./clock.ts";
|
|
25
27
|
import { createFileDiff, emitStructuralDiff } from "./structural.ts";
|
|
26
28
|
import type { ConsoleState } from "./state.ts";
|
|
29
|
+
import { consoleFailureMessage } from "./i18n.ts";
|
|
27
30
|
import { PUBLIC_CONSOLE_FLOWS } from "./public-flows.ts";
|
|
28
31
|
import { tenancyDeclared } from "./store.ts";
|
|
29
32
|
|
|
@@ -38,7 +41,8 @@ const ClaimIn = z.object({
|
|
|
38
41
|
claimCode: z.string().min(1),
|
|
39
42
|
email: z.string().email(),
|
|
40
43
|
name: z.string().min(1),
|
|
41
|
-
password
|
|
44
|
+
/** Matches default operator password policy (minLength 12). */
|
|
45
|
+
password: z.string().min(12),
|
|
42
46
|
});
|
|
43
47
|
|
|
44
48
|
const SessionOut = z.object({
|
|
@@ -292,7 +296,11 @@ const InvokeOut = z.object({
|
|
|
292
296
|
});
|
|
293
297
|
|
|
294
298
|
const SetupClosed = z.object({ reason: z.string() });
|
|
295
|
-
const ClaimFailed = z.object({
|
|
299
|
+
const ClaimFailed = z.object({
|
|
300
|
+
reason: z.string(),
|
|
301
|
+
/** Password-policy detail lines when `reason` is `password_policy`. */
|
|
302
|
+
reasons: z.array(z.string()).optional(),
|
|
303
|
+
});
|
|
296
304
|
const AuthFailed = z.object({});
|
|
297
305
|
const AuthRateLimited = z.object({ reason: z.string() });
|
|
298
306
|
const NotFound = z.object({ flowId: z.string() });
|
|
@@ -1688,17 +1696,39 @@ function createSetupClaim(state: ConsoleState) {
|
|
|
1688
1696
|
errors: { SetupClosed, ClaimFailed },
|
|
1689
1697
|
do: async (input: z.infer<typeof ClaimIn>, fx) => {
|
|
1690
1698
|
if (state.setupClosed) {
|
|
1691
|
-
|
|
1699
|
+
const data = { reason: "first_operator_exists" };
|
|
1700
|
+
return fail("SetupClosed", data, { message: consoleFailureMessage("SetupClosed", data) });
|
|
1692
1701
|
}
|
|
1693
1702
|
const verified = verifyClaimCode(state.claim, input.claimCode, state.now);
|
|
1694
1703
|
if (!verified.ok) {
|
|
1695
|
-
|
|
1704
|
+
const data = { reason: verified.reason };
|
|
1705
|
+
return fail("ClaimFailed", data, { message: consoleFailureMessage("ClaimFailed", data) });
|
|
1706
|
+
}
|
|
1707
|
+
let op: Awaited<ReturnType<typeof createOperator>>;
|
|
1708
|
+
try {
|
|
1709
|
+
op = await createOperator(state.operators, {
|
|
1710
|
+
email: input.email,
|
|
1711
|
+
name: input.name,
|
|
1712
|
+
password: input.password,
|
|
1713
|
+
});
|
|
1714
|
+
} catch (err) {
|
|
1715
|
+
if (err instanceof PasswordPolicyError) {
|
|
1716
|
+
const data = {
|
|
1717
|
+
reason: "password_policy",
|
|
1718
|
+
reasons: [...err.reasons],
|
|
1719
|
+
};
|
|
1720
|
+
return fail("ClaimFailed", data, {
|
|
1721
|
+
message: consoleFailureMessage("ClaimFailed", data),
|
|
1722
|
+
});
|
|
1723
|
+
}
|
|
1724
|
+
if (err instanceof BreachCheckError) {
|
|
1725
|
+
const data = { reason: "password_breached" };
|
|
1726
|
+
return fail("ClaimFailed", data, {
|
|
1727
|
+
message: consoleFailureMessage("ClaimFailed", data),
|
|
1728
|
+
});
|
|
1729
|
+
}
|
|
1730
|
+
throw err;
|
|
1696
1731
|
}
|
|
1697
|
-
const op = await createOperator(state.operators, {
|
|
1698
|
-
email: input.email,
|
|
1699
|
-
name: input.name,
|
|
1700
|
-
password: input.password,
|
|
1701
|
-
});
|
|
1702
1732
|
state.persistOperator(op.id);
|
|
1703
1733
|
const issued = await issueOperatorSession(state, op.id);
|
|
1704
1734
|
fx.log.info("console.setup.claim", { operatorId: op.id });
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Console-only failure copy — kept out of kernel built-in catalogs so the
|
|
3
|
+
* edge profile and app exports do not pay for operator-claim strings.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const en = {
|
|
7
|
+
ClaimFailed: "Could not create the first operator.",
|
|
8
|
+
"ClaimFailed.mismatch": "That claim code does not match. Copy it from the boot log.",
|
|
9
|
+
"ClaimFailed.expired": "That claim code expired. Restart `oke dev` for a new one.",
|
|
10
|
+
"ClaimFailed.rate_limited": "Too many claim attempts. Wait a minute and try again.",
|
|
11
|
+
"ClaimFailed.missing": "No claim code is active. Restart `oke dev` to mint one.",
|
|
12
|
+
"ClaimFailed.password_policy":
|
|
13
|
+
"Password needs at least 12 characters, including a letter and a number.",
|
|
14
|
+
"ClaimFailed.password_breached":
|
|
15
|
+
"Choose a different password — this one appears in a breach list.",
|
|
16
|
+
SetupClosed: "Setup is closed — an operator already exists. Sign in instead.",
|
|
17
|
+
"SetupClosed.first_operator_exists":
|
|
18
|
+
"Setup is closed — an operator already exists. Sign in instead.",
|
|
19
|
+
} as const;
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Resolve a Console claim/setup failure message (English; boot log is EN).
|
|
23
|
+
*
|
|
24
|
+
* @param code - Failure code
|
|
25
|
+
* @param data - Failure data (may include `reason`)
|
|
26
|
+
*/
|
|
27
|
+
export function consoleFailureMessage(code: string, data?: unknown): string {
|
|
28
|
+
const reason =
|
|
29
|
+
data !== null &&
|
|
30
|
+
data !== undefined &&
|
|
31
|
+
typeof data === "object" &&
|
|
32
|
+
"reason" in data &&
|
|
33
|
+
typeof (data as { reason: unknown }).reason === "string"
|
|
34
|
+
? (data as { reason: string }).reason
|
|
35
|
+
: undefined;
|
|
36
|
+
if (reason) {
|
|
37
|
+
const keyed = en[`${code}.${reason}` as keyof typeof en];
|
|
38
|
+
if (keyed !== undefined) return keyed;
|
|
39
|
+
}
|
|
40
|
+
return en[code as keyof typeof en] ?? code;
|
|
41
|
+
}
|
|
@@ -6,21 +6,16 @@
|
|
|
6
6
|
* rotation blast radius are derived — never reimplemented in the UI.
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
-
import { readFileSync } from "node:fs";
|
|
10
|
-
import { resolve } from "node:path";
|
|
11
9
|
import type { ConfigEnv } from "../../config/index.ts";
|
|
12
|
-
import {
|
|
13
|
-
import { parseDotenv } from "../../drivers/vault-dotenv-parse.ts";
|
|
10
|
+
import { buildVaultBootChain } from "../../elements/vault/boot-chain.ts";
|
|
14
11
|
import {
|
|
15
12
|
createVaultRuntime,
|
|
16
13
|
vault as declareVault,
|
|
17
|
-
type VaultChainLayer,
|
|
18
14
|
type VaultResolutionSource,
|
|
19
15
|
type VaultResolutionStep,
|
|
20
16
|
type VaultRuntime,
|
|
21
17
|
type VaultSecretDecl,
|
|
22
18
|
} from "../../elements/vault.ts";
|
|
23
|
-
import { resolveComposeEnvPath } from "../../elements/vault/chain.ts";
|
|
24
19
|
import type { JournalRun, JournalStore } from "../../kernel/journal.ts";
|
|
25
20
|
import type { Manifest } from "../../manifest/types.ts";
|
|
26
21
|
|
|
@@ -236,41 +231,20 @@ export async function createManifestVaultRuntime(
|
|
|
236
231
|
});
|
|
237
232
|
|
|
238
233
|
const seed = options.seed ?? {};
|
|
239
|
-
const
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
const openbaoToken = process.env.OKE_VAULT_TOKEN ?? envMap.get("OKE_VAULT_TOKEN");
|
|
243
|
-
const openbaoMount = process.env.OKE_VAULT_MOUNT ?? envMap.get("OKE_VAULT_MOUNT") ?? "secret";
|
|
244
|
-
const driverLayer: VaultChainLayer =
|
|
245
|
-
openbaoUrl && openbaoToken
|
|
246
|
-
? {
|
|
247
|
-
driver: openbaoVaultDriver,
|
|
248
|
-
source: "driver",
|
|
249
|
-
options: { url: openbaoUrl, token: openbaoToken, mount: openbaoMount },
|
|
250
|
-
}
|
|
251
|
-
: {
|
|
252
|
-
driver: memoryVaultDriver,
|
|
253
|
-
source: "driver",
|
|
254
|
-
options: { secrets: seed },
|
|
255
|
-
};
|
|
234
|
+
const env = options.env ?? "local";
|
|
235
|
+
// Auto-detect: prefer OpenBao when credentials exist; soft-fallback to memory
|
|
236
|
+
// (historical Console behaviour — never fail the Console for missing vault).
|
|
256
237
|
const runtime = createVaultRuntime({
|
|
257
238
|
secrets,
|
|
258
|
-
allowDevFallbacks: options.allowDevFallbacks ??
|
|
239
|
+
allowDevFallbacks: options.allowDevFallbacks ?? env !== "prod",
|
|
259
240
|
now: options.now,
|
|
260
|
-
chain:
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
{
|
|
268
|
-
driver: envVaultDriver,
|
|
269
|
-
source: composeEnv.source,
|
|
270
|
-
options: { path: composeEnv.path },
|
|
271
|
-
},
|
|
272
|
-
driverLayer,
|
|
273
|
-
],
|
|
241
|
+
chain: buildVaultBootChain({
|
|
242
|
+
cwd,
|
|
243
|
+
driverId: "openbao",
|
|
244
|
+
env,
|
|
245
|
+
seed,
|
|
246
|
+
missingOpenbao: "memory",
|
|
247
|
+
}),
|
|
274
248
|
});
|
|
275
249
|
await runtime.boot();
|
|
276
250
|
return runtime;
|