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
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared Vault resolution-chain assembly for app boot and Console.
|
|
3
|
+
*
|
|
4
|
+
* Order (first hit wins): process.env → .env.local → compose .env.docker →
|
|
5
|
+
* backend layer (`env` seed / openbao / memory / managed).
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
9
|
+
import { resolve } from "node:path";
|
|
10
|
+
import type { ConfigEnv } from "../../config/index.ts";
|
|
11
|
+
import {
|
|
12
|
+
envVaultDriver,
|
|
13
|
+
managedVaultDriver,
|
|
14
|
+
memoryVaultDriver,
|
|
15
|
+
openbaoVaultDriver,
|
|
16
|
+
} from "../../drivers/index.ts";
|
|
17
|
+
import { parseDotenv } from "../../drivers/vault-dotenv-parse.ts";
|
|
18
|
+
import type { VaultDriverId } from "../../drivers/vault-types.ts";
|
|
19
|
+
import { resolveComposeEnvPath } from "./chain.ts";
|
|
20
|
+
import type { VaultChainLayer } from "./runtime.ts";
|
|
21
|
+
|
|
22
|
+
/** Options for {@link buildVaultBootChain}. */
|
|
23
|
+
export interface BuildVaultBootChainOptions {
|
|
24
|
+
/** Project root (defaults to `process.cwd()`). */
|
|
25
|
+
readonly cwd?: string;
|
|
26
|
+
/** Resolved `drivers.vault` id for the active env. */
|
|
27
|
+
readonly driverId: VaultDriverId;
|
|
28
|
+
/** Active config env — controls fail-loud vs soft fallback for openbao. */
|
|
29
|
+
readonly env?: ConfigEnv;
|
|
30
|
+
/** Seed secrets for the terminal memory / managed layer. */
|
|
31
|
+
readonly seed?: Readonly<Record<string, string>>;
|
|
32
|
+
/**
|
|
33
|
+
* When `openbao` is selected but credentials are missing:
|
|
34
|
+
* - `"throw"` — fail loud (default for `prod` / `docker`)
|
|
35
|
+
* - `"memory"` — soft-fallback to a seeded memory bag (Console / local)
|
|
36
|
+
*/
|
|
37
|
+
readonly missingOpenbao?: "throw" | "memory";
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Normalize a config label to a {@link VaultDriverId}.
|
|
42
|
+
*
|
|
43
|
+
* Accepts legacy `"dotenv"` as an alias for `"env"`.
|
|
44
|
+
*
|
|
45
|
+
* @param raw - Config string
|
|
46
|
+
*/
|
|
47
|
+
export function normalizeVaultDriverId(raw: string): VaultDriverId {
|
|
48
|
+
if (raw === "dotenv") return "env";
|
|
49
|
+
if (raw === "env" || raw === "openbao" || raw === "memory" || raw === "managed") {
|
|
50
|
+
return raw;
|
|
51
|
+
}
|
|
52
|
+
throw new Error(
|
|
53
|
+
`oke boot: unknown vault driver "${raw}" (expected env · openbao · memory · managed)`,
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Build the standard Vault resolution chain for app boot / Console.
|
|
59
|
+
*
|
|
60
|
+
* @param options - Driver id + cwd + seed
|
|
61
|
+
*/
|
|
62
|
+
export function buildVaultBootChain(options: BuildVaultBootChainOptions): VaultChainLayer[] {
|
|
63
|
+
const cwd = options.cwd ?? process.cwd();
|
|
64
|
+
const seed = options.seed ?? {};
|
|
65
|
+
const composeEnv = resolveComposeEnvPath(cwd);
|
|
66
|
+
|
|
67
|
+
let composeText = "";
|
|
68
|
+
if (existsSync(composeEnv.path)) {
|
|
69
|
+
composeText = readFileSync(composeEnv.path, "utf8").toString();
|
|
70
|
+
}
|
|
71
|
+
const envMap = parseDotenv(composeText);
|
|
72
|
+
const openbaoUrl = process.env.OKE_VAULT_URL ?? envMap.get("OKE_VAULT_URL");
|
|
73
|
+
const openbaoToken = process.env.OKE_VAULT_TOKEN ?? envMap.get("OKE_VAULT_TOKEN");
|
|
74
|
+
const openbaoMount = process.env.OKE_VAULT_MOUNT ?? envMap.get("OKE_VAULT_MOUNT") ?? "secret";
|
|
75
|
+
|
|
76
|
+
const envLayers: VaultChainLayer[] = [
|
|
77
|
+
{ driver: envVaultDriver, source: "process.env" },
|
|
78
|
+
{
|
|
79
|
+
driver: envVaultDriver,
|
|
80
|
+
source: ".env.local",
|
|
81
|
+
options: { path: resolve(cwd, ".env.local") },
|
|
82
|
+
},
|
|
83
|
+
{
|
|
84
|
+
driver: envVaultDriver,
|
|
85
|
+
source: composeEnv.source,
|
|
86
|
+
options: { path: composeEnv.path },
|
|
87
|
+
},
|
|
88
|
+
];
|
|
89
|
+
|
|
90
|
+
switch (options.driverId) {
|
|
91
|
+
case "memory":
|
|
92
|
+
return [
|
|
93
|
+
{
|
|
94
|
+
driver: memoryVaultDriver,
|
|
95
|
+
source: "driver",
|
|
96
|
+
options: { secrets: seed },
|
|
97
|
+
},
|
|
98
|
+
];
|
|
99
|
+
case "managed":
|
|
100
|
+
return [
|
|
101
|
+
{
|
|
102
|
+
driver: managedVaultDriver,
|
|
103
|
+
source: "driver",
|
|
104
|
+
options: { secrets: seed },
|
|
105
|
+
},
|
|
106
|
+
];
|
|
107
|
+
case "env":
|
|
108
|
+
return [
|
|
109
|
+
...envLayers,
|
|
110
|
+
{
|
|
111
|
+
driver: memoryVaultDriver,
|
|
112
|
+
source: "driver",
|
|
113
|
+
options: { secrets: seed },
|
|
114
|
+
},
|
|
115
|
+
];
|
|
116
|
+
case "openbao": {
|
|
117
|
+
if (openbaoUrl && openbaoToken) {
|
|
118
|
+
return [
|
|
119
|
+
...envLayers,
|
|
120
|
+
{
|
|
121
|
+
driver: openbaoVaultDriver,
|
|
122
|
+
source: "driver",
|
|
123
|
+
options: { url: openbaoUrl, token: openbaoToken, mount: openbaoMount },
|
|
124
|
+
},
|
|
125
|
+
];
|
|
126
|
+
}
|
|
127
|
+
const env = options.env ?? "local";
|
|
128
|
+
const missing =
|
|
129
|
+
options.missingOpenbao ?? (env === "prod" || env === "docker" ? "throw" : "memory");
|
|
130
|
+
if (missing === "throw") {
|
|
131
|
+
throw new Error(
|
|
132
|
+
'oke boot: vault driver "openbao" needs OKE_VAULT_URL and OKE_VAULT_TOKEN ' +
|
|
133
|
+
"(did `oke dev -d` bootstrap OpenBao / write docker/.env.docker?)",
|
|
134
|
+
);
|
|
135
|
+
}
|
|
136
|
+
return [
|
|
137
|
+
...envLayers,
|
|
138
|
+
{
|
|
139
|
+
driver: memoryVaultDriver,
|
|
140
|
+
source: "driver",
|
|
141
|
+
options: { secrets: seed },
|
|
142
|
+
},
|
|
143
|
+
];
|
|
144
|
+
}
|
|
145
|
+
default: {
|
|
146
|
+
const _exhaustive: never = options.driverId;
|
|
147
|
+
throw new Error(`oke boot: unhandled vault driver ${String(_exhaustive)}`);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
@@ -87,6 +87,11 @@ export interface CreateVaultRuntimeOptions {
|
|
|
87
87
|
export interface VaultRuntime {
|
|
88
88
|
/** Declared contracts by name. */
|
|
89
89
|
readonly contracts: ReadonlyMap<string, VaultSecretDecl>;
|
|
90
|
+
/**
|
|
91
|
+
* Driver ids of each chain layer in order (`env` · `openbao` · `memory` · `managed`).
|
|
92
|
+
* Available before {@link boot}.
|
|
93
|
+
*/
|
|
94
|
+
readonly chainDriverIds: readonly string[];
|
|
90
95
|
/**
|
|
91
96
|
* Open every chain layer, merge (first wins), validate all contracts,
|
|
92
97
|
* register redaction. Throws {@link VaultBootError} listing every gap.
|
|
@@ -245,8 +250,11 @@ export function createVaultRuntime(options: CreateVaultRuntimeOptions = {}): Vau
|
|
|
245
250
|
booted = true;
|
|
246
251
|
}
|
|
247
252
|
|
|
253
|
+
const chainDriverIds = (options.chain ?? []).map((layer) => layer.driver.id);
|
|
254
|
+
|
|
248
255
|
return {
|
|
249
256
|
contracts,
|
|
257
|
+
chainDriverIds,
|
|
250
258
|
get booted() {
|
|
251
259
|
return booted;
|
|
252
260
|
},
|
package/src/kernel/app.ts
CHANGED
|
@@ -55,6 +55,7 @@ import {
|
|
|
55
55
|
} from "./fx.ts";
|
|
56
56
|
import {
|
|
57
57
|
currentDevSurface,
|
|
58
|
+
failureDetailFromResponse,
|
|
58
59
|
logDevRequest,
|
|
59
60
|
shouldLogDevRequests,
|
|
60
61
|
} from "../runtime/dev-request-log.ts";
|
|
@@ -1180,7 +1181,7 @@ export function oke(options: OkeOptions): OkeApp {
|
|
|
1180
1181
|
const url = new URL(request.url);
|
|
1181
1182
|
const method = request.method.toUpperCase();
|
|
1182
1183
|
|
|
1183
|
-
const respond = (response: Response): Response => {
|
|
1184
|
+
const respond = async (response: Response): Promise<Response> => {
|
|
1184
1185
|
if (shouldLogDevRequests()) {
|
|
1185
1186
|
logDevRequest({
|
|
1186
1187
|
surface: currentDevSurface(),
|
|
@@ -1189,6 +1190,7 @@ export function oke(options: OkeOptions): OkeApp {
|
|
|
1189
1190
|
flow: flowLabel,
|
|
1190
1191
|
status: response.status,
|
|
1191
1192
|
ms: Math.round(performance.now() - started),
|
|
1193
|
+
detail: await failureDetailFromResponse(response),
|
|
1192
1194
|
});
|
|
1193
1195
|
}
|
|
1194
1196
|
return response;
|
|
@@ -2,9 +2,13 @@
|
|
|
2
2
|
* Lazy clock binder — loaded only when Clock is declared.
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
+
import { mkdirSync } from "node:fs";
|
|
6
|
+
import { dirname, resolve } from "node:path";
|
|
5
7
|
import {
|
|
6
8
|
clock as declareClock,
|
|
7
9
|
createClockRuntime,
|
|
10
|
+
createFileCronStore,
|
|
11
|
+
createMemoryCronStore,
|
|
8
12
|
createTestClockRuntime,
|
|
9
13
|
type ClockDecl,
|
|
10
14
|
type ClockRuntime,
|
|
@@ -18,9 +22,27 @@ export interface BindClockResult {
|
|
|
18
22
|
readonly clockDecls: ReadonlyMap<string, ClockDecl>;
|
|
19
23
|
}
|
|
20
24
|
|
|
25
|
+
/** Default on-disk cron store path (multi-process leader election). */
|
|
26
|
+
export const DEFAULT_FILE_CRON_PATH = ".oke/crons.json";
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Resolve `drivers.clock` for the active env.
|
|
30
|
+
*
|
|
31
|
+
* @param options - Boot options
|
|
32
|
+
* @param env - Active environment
|
|
33
|
+
*/
|
|
34
|
+
export function resolveClockDriverId(options: BootOptions, env: ConfigEnv): string {
|
|
35
|
+
const resolved = resolveDriverId(options.config?.drivers?.clock, env);
|
|
36
|
+
if (resolved) return resolved;
|
|
37
|
+
return env === "test" ? "frozen" : "memory";
|
|
38
|
+
}
|
|
39
|
+
|
|
21
40
|
/**
|
|
22
41
|
* Construct / adopt a Clock runtime, register decls, reconcile.
|
|
23
42
|
*
|
|
43
|
+
* Supported ids: `frozen` · `memory` · `file`. `postgres` (and any other id)
|
|
44
|
+
* fail loud — there is no postgres CronStore.
|
|
45
|
+
*
|
|
24
46
|
* @param options - Boot options
|
|
25
47
|
* @param env - Active environment
|
|
26
48
|
* @param now - Clock
|
|
@@ -32,13 +54,36 @@ export async function bindClock(
|
|
|
32
54
|
now: () => number,
|
|
33
55
|
prebuilt?: ClockRuntime,
|
|
34
56
|
): Promise<BindClockResult> {
|
|
35
|
-
const clockDriver =
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
57
|
+
const clockDriver = resolveClockDriverId(options, env);
|
|
58
|
+
|
|
59
|
+
let clock: ClockRuntime;
|
|
60
|
+
if (prebuilt) {
|
|
61
|
+
clock = prebuilt;
|
|
62
|
+
} else if (clockDriver === "frozen" || env === "test") {
|
|
63
|
+
clock = createTestClockRuntime(now(), { instanceId: options.instanceId });
|
|
64
|
+
} else if (clockDriver === "memory") {
|
|
65
|
+
clock = createClockRuntime({
|
|
66
|
+
instanceId: options.instanceId,
|
|
67
|
+
now,
|
|
68
|
+
store: createMemoryCronStore(),
|
|
69
|
+
});
|
|
70
|
+
} else if (clockDriver === "file") {
|
|
71
|
+
const path = resolve(process.cwd(), DEFAULT_FILE_CRON_PATH);
|
|
72
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
73
|
+
clock = createClockRuntime({
|
|
74
|
+
instanceId: options.instanceId,
|
|
75
|
+
now,
|
|
76
|
+
store: createFileCronStore(path),
|
|
77
|
+
});
|
|
78
|
+
} else if (clockDriver === "postgres") {
|
|
79
|
+
throw new Error(
|
|
80
|
+
'oke boot: clock driver "postgres" is not implemented — use "memory", "file", or "frozen"',
|
|
81
|
+
);
|
|
82
|
+
} else {
|
|
83
|
+
throw new Error(
|
|
84
|
+
`oke boot: unknown clock driver "${clockDriver}" (expected memory · file · frozen)`,
|
|
85
|
+
);
|
|
86
|
+
}
|
|
42
87
|
|
|
43
88
|
const clockDecls = new Map<string, ClockDecl>();
|
|
44
89
|
for (const c of options.clocks ?? []) {
|
|
@@ -2,18 +2,86 @@
|
|
|
2
2
|
* Lazy gate binder — loaded only when Gate is declared (or required by AI).
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
+
import { resolveDriverId, type ConfigEnv } from "../../config/index.ts";
|
|
5
6
|
import { memoryDrivers } from "../../drivers/memory.ts";
|
|
7
|
+
import { redisDriver } from "../../drivers/redis.ts";
|
|
8
|
+
import type { KvClientLike } from "../../drivers/types.ts";
|
|
6
9
|
import { createGateRuntime, type GateRuntime } from "../../elements/gate.ts";
|
|
7
10
|
import type { BootOptions } from "../boot.ts";
|
|
8
11
|
|
|
12
|
+
/** Namespace for gate rate-limit counters (never shares an app kv declaration). */
|
|
13
|
+
export const GATE_KV_NAMESPACE = "oke:gates";
|
|
14
|
+
|
|
9
15
|
/**
|
|
10
|
-
*
|
|
16
|
+
* Resolve the KV backend for Gate rates — same facet as `drivers.store.kv`.
|
|
17
|
+
*
|
|
18
|
+
* @param options - Boot options
|
|
19
|
+
* @param env - Active environment
|
|
20
|
+
* @param docker - Docker mode
|
|
21
|
+
*/
|
|
22
|
+
export function resolveGateKvDriverId(
|
|
23
|
+
options: BootOptions,
|
|
24
|
+
env: ConfigEnv,
|
|
25
|
+
docker = false,
|
|
26
|
+
): "memory" | "redis" {
|
|
27
|
+
const fromEnv = process.env.OKE_KV_DRIVER?.trim();
|
|
28
|
+
if (docker && fromEnv === "redis") return "redis";
|
|
29
|
+
if (docker && fromEnv === "memory") return "memory";
|
|
30
|
+
const resolved = resolveDriverId(options.config?.drivers?.store?.kv, env);
|
|
31
|
+
if (resolved === "redis") return "redis";
|
|
32
|
+
if (resolved === "memory") return "memory";
|
|
33
|
+
return docker ? "redis" : "memory";
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function kvUrlFor(docker: boolean): string | undefined {
|
|
37
|
+
const url = process.env.REDIS_URL ?? process.env.OKE_STORE_KV_URL ?? undefined;
|
|
38
|
+
if (!url) {
|
|
39
|
+
if (docker) {
|
|
40
|
+
throw new Error(
|
|
41
|
+
"oke boot: gate redis kv needs REDIS_URL (did `oke dev -d` write docker/.env.docker?)",
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
return undefined;
|
|
45
|
+
}
|
|
46
|
+
return url;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Construct a Gate runtime backed by `drivers.store.kv` (namespace {@link GATE_KV_NAMESPACE}).
|
|
11
51
|
*
|
|
12
52
|
* @param options - Boot options
|
|
13
53
|
* @param now - Clock
|
|
54
|
+
* @param env - Active environment
|
|
55
|
+
* @param docker - Docker mode
|
|
14
56
|
*/
|
|
15
|
-
export async function bindGate(
|
|
16
|
-
|
|
57
|
+
export async function bindGate(
|
|
58
|
+
options: BootOptions,
|
|
59
|
+
now: () => number,
|
|
60
|
+
env: ConfigEnv = "local",
|
|
61
|
+
docker = false,
|
|
62
|
+
): Promise<GateRuntime> {
|
|
63
|
+
const kvId = resolveGateKvDriverId(options, env, docker);
|
|
64
|
+
const injected = options.clients?.kv as KvClientLike | undefined;
|
|
65
|
+
|
|
66
|
+
let kvNs;
|
|
67
|
+
if (kvId === "redis") {
|
|
68
|
+
const url = injected ? undefined : kvUrlFor(docker);
|
|
69
|
+
if (!injected && url === undefined) {
|
|
70
|
+
// Local without REDIS_URL — soft-fallback to memory (same as missing redis for store would fail;
|
|
71
|
+
// gate rates should not brick local apps that only declared policy gates).
|
|
72
|
+
kvNs = await memoryDrivers.kv.open({ name: GATE_KV_NAMESPACE, nowMs: now });
|
|
73
|
+
} else {
|
|
74
|
+
kvNs = await redisDriver.open({
|
|
75
|
+
name: GATE_KV_NAMESPACE,
|
|
76
|
+
...(url !== undefined ? { url } : {}),
|
|
77
|
+
...(injected !== undefined ? { client: injected } : {}),
|
|
78
|
+
nowMs: now,
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
} else {
|
|
82
|
+
kvNs = await memoryDrivers.kv.open({ name: GATE_KV_NAMESPACE, nowMs: now });
|
|
83
|
+
}
|
|
84
|
+
|
|
17
85
|
return createGateRuntime({
|
|
18
86
|
gates: options.gates ?? [],
|
|
19
87
|
kv: kvNs,
|
|
@@ -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
|
+
});
|