okengine 0.2.8 → 0.3.2
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 +17 -15
- package/README.md +57 -29
- package/package.json +11 -16
- package/site/content/docs/ai/llms-txt.mdx +54 -0
- package/site/content/docs/ai/mcp.mdx +123 -0
- package/site/content/docs/ai/meta.json +5 -0
- package/site/content/docs/ai/skills.mdx +53 -0
- package/site/content/docs/console/access.mdx +29 -0
- package/site/content/docs/console/ai.mdx +35 -0
- package/site/content/docs/console/architecture.mdx +35 -0
- package/site/content/docs/console/channels.mdx +37 -0
- package/site/content/docs/console/clock.mdx +31 -0
- package/site/content/docs/console/flows.mdx +31 -0
- package/site/content/docs/console/gates.mdx +35 -0
- package/site/content/docs/console/manifest-diff.mdx +34 -0
- package/site/content/docs/console/meta.json +23 -0
- package/site/content/docs/console/overview.mdx +40 -0
- package/site/content/docs/console/plugins.mdx +41 -0
- package/site/content/docs/console/privacy.mdx +32 -0
- package/site/content/docs/console/runs.mdx +40 -0
- package/site/content/docs/console/signals.mdx +31 -0
- package/site/content/docs/console/store.mdx +32 -0
- package/site/content/docs/console/tenancy.mdx +32 -0
- package/site/content/docs/console/traces.mdx +34 -0
- package/site/content/docs/console/vault.mdx +37 -0
- package/site/content/docs/elements/ai.mdx +180 -0
- package/site/content/docs/elements/channel.mdx +167 -0
- package/site/content/docs/elements/clock.mdx +182 -0
- package/site/content/docs/elements/flow.mdx +288 -0
- package/site/content/docs/elements/gate.mdx +171 -0
- package/site/content/docs/elements/meta.json +5 -0
- package/site/content/docs/elements/signal.mdx +171 -0
- package/site/content/docs/elements/store.mdx +320 -0
- package/site/content/docs/elements/vault.mdx +263 -0
- package/site/content/docs/get-started/basic-usage.mdx +124 -0
- package/site/content/docs/get-started/comparison.mdx +65 -0
- package/site/content/docs/get-started/installation.mdx +113 -0
- package/site/content/docs/get-started/introduction.mdx +123 -0
- package/site/content/docs/get-started/meta.json +5 -0
- package/site/content/docs/index.mdx +63 -0
- package/site/content/docs/meta.json +5 -0
- package/site/content/docs/plugins/compression.mdx +60 -0
- package/site/content/docs/plugins/cors.mdx +92 -0
- package/site/content/docs/plugins/csrf.mdx +96 -0
- package/site/content/docs/plugins/ip-allowlist.mdx +92 -0
- package/site/content/docs/plugins/maintenance-mode.mdx +101 -0
- package/site/content/docs/plugins/meta.json +15 -0
- package/site/content/docs/plugins/security-headers.mdx +136 -0
- package/site/content/docs/reference/cli.md +101 -0
- package/site/content/docs/reference/configuration.mdx +159 -0
- package/site/content/docs/reference/environment-variables.mdx +87 -0
- package/site/content/docs/reference/errors.mdx +80 -0
- package/site/content/docs/reference/fx.mdx +117 -0
- package/site/content/docs/reference/meta.json +5 -0
- package/site/content/docs/reference/plugins.mdx +249 -0
- package/site/content/docs/reference/security.md +63 -0
- package/src/auth/auth.test.ts +3 -0
- package/src/cli/ask-dev-mode.ts +1 -1
- package/src/cli/db.ts +87 -17
- package/src/cli/dev-db-push.test.ts +32 -2
- package/src/cli/dev-schema-sync.test.ts +66 -0
- package/src/cli/dev-schema-sync.ts +139 -0
- package/src/cli/dev.test.ts +123 -1
- package/src/cli/dev.ts +150 -8
- package/src/cli/doc-staleness.test.ts +4 -4
- package/src/cli/docker-cli.test.ts +20 -0
- package/src/cli/docker.ts +10 -0
- package/src/cli/drizzle-env.test.ts +67 -0
- package/src/cli/drizzle-env.ts +78 -0
- package/src/cli/ensure-drizzle-config.ts +50 -0
- package/src/cli/hero-meta.test.ts +1 -1
- package/src/cli/load-config.ts +6 -0
- package/src/cli/mode.ts +24 -4
- package/src/cli/openbao-bootstrap.test.ts +147 -0
- package/src/cli/openbao-bootstrap.ts +280 -0
- package/src/cli/openbao-restart.integration.test.ts +136 -0
- package/src/cli/ports.test.ts +7 -5
- package/src/cli/ports.ts +6 -2
- package/src/cli/resolve-dev-sql-env.test.ts +48 -0
- package/src/cli/resolve-dev-sql-env.ts +42 -0
- package/src/cli/stack.ts +7 -4
- package/src/cli/vault-cmd.ts +63 -0
- package/src/client/types.ts +7 -1
- package/src/compiler/extract.test.ts +40 -0
- package/src/compiler/extract.ts +123 -1
- package/src/compiler/fixtures/skyport/oke.config.ts +2 -2
- package/src/compiler/fixtures/skyport.expected.json +1 -1
- package/src/compiler/response.ts +12 -0
- package/src/config/define-config.test.ts +4 -6
- package/src/config/index.ts +2 -15
- package/src/console/server/app.ts +2 -0
- package/src/console/server/vault.ts +21 -6
- package/src/docker/compose.ts +112 -16
- package/src/docker/derive.ts +7 -1
- package/src/docker/docker.test.ts +103 -0
- package/src/docker/index.ts +11 -1
- package/src/docker/recipes/index.ts +3 -2
- package/src/docker/recipes/openbao.ts +47 -0
- package/src/docker/recipes/redis.ts +5 -1
- package/src/docker/recipes/rustfs.ts +2 -3
- package/src/docker/stack-id.test.ts +43 -8
- package/src/docker/stack-id.ts +99 -20
- package/src/docker/stack.ts +36 -4
- package/src/docker/types.ts +3 -0
- package/src/docs-origin.ts +4 -4
- package/src/drivers/drizzle-dialect.test.ts +20 -0
- package/src/drivers/drizzle-dialect.ts +37 -0
- package/src/drivers/index.ts +1 -2
- package/src/drivers/memory.ts +278 -39
- package/src/drivers/s3.ts +10 -1
- package/src/drivers/vault-driver-removal.test.ts +55 -0
- package/src/drivers/vault-openbao.test.ts +97 -0
- package/src/drivers/vault-openbao.ts +102 -35
- package/src/drivers/vault-types.ts +3 -10
- package/src/elements/store/declare.ts +4 -1
- package/src/elements/store/resource-list-docs.fixture.ts +56 -0
- package/src/elements/store/resource-list-docs.test.ts +79 -0
- package/src/elements/store/resource.test.ts +253 -0
- package/src/elements/store/resource.ts +786 -0
- package/src/elements/store/sql-condition.test.ts +132 -0
- package/src/elements/store/sql-condition.ts +284 -46
- package/src/elements/store/sql-session.test.ts +86 -1
- package/src/elements/store/sql-session.ts +187 -27
- package/src/elements/store/table.ts +34 -4
- package/src/elements/store.ts +16 -0
- package/src/elements/vault/runtime.ts +1 -1
- package/src/elements/vault.test.ts +1 -28
- package/src/elements/vault.ts +1 -1
- package/src/kernel/app.ts +59 -25
- package/src/kernel/boot-bind/channel.test.ts +60 -0
- package/src/kernel/boot-bind/channel.ts +64 -2
- package/src/kernel/boot-bind/store.test.ts +10 -1
- package/src/kernel/boot-bind/store.ts +49 -2
- package/src/kernel/boot.test.ts +0 -1
- package/src/kernel/boot.ts +1 -1
- package/src/kernel/edge.test.ts +68 -0
- package/src/kernel/errors.registry.test.ts +1 -1
- package/src/kernel/flow.ts +8 -0
- package/src/kernel/fx.test.ts +23 -3
- package/src/kernel/fx.ts +115 -18
- package/src/kernel/hooks.test.ts +33 -0
- package/src/kernel/hooks.ts +22 -0
- package/src/kernel/index.ts +7 -0
- package/src/kernel/on.ts +44 -3
- package/src/kernel/plugin.ts +33 -3
- package/src/kernel/registry-isolation.test.ts +74 -0
- package/src/kernel/registry.ts +22 -1
- package/src/kernel/triggers.ts +59 -0
- package/src/manifest/fixtures/skyport.excerpt.json +1 -1
- package/src/manifest/fixtures/skyport.manifest.json +1 -1
- package/src/manifest/index.ts +1 -1
- package/src/manifest/types.ts +1 -1
- package/src/manifest/validate.ts +2 -2
- package/src/plugins/compression.test.ts +127 -0
- package/src/plugins/compression.ts +94 -0
- package/src/plugins/config-source.test.ts +204 -0
- package/src/plugins/config-source.ts +209 -0
- package/src/plugins/cors.test.ts +138 -0
- package/src/plugins/cors.ts +129 -0
- package/src/plugins/csrf.test.ts +102 -0
- package/src/plugins/csrf.ts +86 -0
- package/src/plugins/headers.ts +54 -0
- package/src/plugins/index.ts +26 -0
- package/src/plugins/ip-allowlist.test.ts +105 -0
- package/src/plugins/ip-allowlist.ts +76 -0
- package/src/plugins/maintenance-mode.test.ts +91 -0
- package/src/plugins/maintenance-mode.ts +85 -0
- package/src/plugins/security-headers.test.ts +243 -0
- package/src/plugins/security-headers.ts +255 -0
- package/src/release/measure.ts +1 -2
- package/src/test/create-test-app.ts +14 -2
- package/docs/spec/console.md +0 -762
- package/docs/spec/example.md +0 -1374
- package/docs/spec/four-applications.md +0 -1376
- package/docs/spec/unified-theory.md +0 -498
- package/src/cli/doc-drift.test.ts +0 -147
- package/src/cli/doc-drift.ts +0 -401
- package/src/cli/doctor-diff-examples.ts +0 -90
- package/src/drivers/vault-sops.ts +0 -246
- /package/{spec/manifest.v1.schema.json → manifest.v1.schema.json} +0 -0
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Active SQL env resolution from `.oke/mode` / session overrides.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { afterEach, describe, expect, test } from "bun:test";
|
|
6
|
+
import { mkdtemp, rm } from "node:fs/promises";
|
|
7
|
+
import { tmpdir } from "node:os";
|
|
8
|
+
import { join } from "node:path";
|
|
9
|
+
import { writeDevMode } from "./dev-mode.ts";
|
|
10
|
+
import { resolveDevSqlEnv } from "./resolve-dev-sql-env.ts";
|
|
11
|
+
|
|
12
|
+
const dirs: string[] = [];
|
|
13
|
+
|
|
14
|
+
afterEach(async () => {
|
|
15
|
+
await Promise.all(dirs.splice(0).map((d) => rm(d, { recursive: true, force: true })));
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
async function tempDir(): Promise<string> {
|
|
19
|
+
const dir = await mkdtemp(join(tmpdir(), "oke-sqlenv-"));
|
|
20
|
+
dirs.push(dir);
|
|
21
|
+
return dir;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
describe("resolveDevSqlEnv", () => {
|
|
25
|
+
test("defaults to local when nothing set", async () => {
|
|
26
|
+
const dir = await tempDir();
|
|
27
|
+
expect(await resolveDevSqlEnv(dir)).toBe("local");
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
test("explicit env override wins", async () => {
|
|
31
|
+
const dir = await tempDir();
|
|
32
|
+
await writeDevMode(dir, "local");
|
|
33
|
+
expect(await resolveDevSqlEnv(dir, { env: "docker" })).toBe("docker");
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
test("session docker flag wins over saved mode", async () => {
|
|
37
|
+
const dir = await tempDir();
|
|
38
|
+
await writeDevMode(dir, "local");
|
|
39
|
+
expect(await resolveDevSqlEnv(dir, { docker: true })).toBe("docker");
|
|
40
|
+
expect(await resolveDevSqlEnv(dir, { docker: false })).toBe("local");
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
test("reads saved .oke/mode", async () => {
|
|
44
|
+
const dir = await tempDir();
|
|
45
|
+
await writeDevMode(dir, "docker");
|
|
46
|
+
expect(await resolveDevSqlEnv(dir)).toBe("docker");
|
|
47
|
+
});
|
|
48
|
+
});
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resolve the active SQL env for schema tooling (`oke db`, `oke mode`,
|
|
3
|
+
* `oke dev` one-shot sync) from session overrides and the saved dev mode.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { ConfigEnv } from "../config/index.ts";
|
|
7
|
+
import { readDevMode } from "./dev-mode.ts";
|
|
8
|
+
|
|
9
|
+
/** Options for {@link resolveDevSqlEnv}. */
|
|
10
|
+
export interface ResolveDevSqlEnvOptions {
|
|
11
|
+
/**
|
|
12
|
+
* Session override — `true` means docker (e.g. `OKE_DOCKER=1` or
|
|
13
|
+
* `oke dev --docker`). `false` means local. `undefined` → auto-resolve.
|
|
14
|
+
*/
|
|
15
|
+
readonly docker?: boolean;
|
|
16
|
+
/**
|
|
17
|
+
* Explicit env override (e.g. `oke db --env docker`). Wins over `.oke/mode`.
|
|
18
|
+
*/
|
|
19
|
+
readonly env?: ConfigEnv;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Resolve the SQL env (`local` | `docker` for dev tooling) without prompting.
|
|
24
|
+
*
|
|
25
|
+
* Order:
|
|
26
|
+
* 1. Explicit `options.env` (CLI flag)
|
|
27
|
+
* 2. Session docker flag (`options.docker`)
|
|
28
|
+
* 3. `.oke/mode` persisted preference
|
|
29
|
+
* 4. Default `local`
|
|
30
|
+
*
|
|
31
|
+
* @param cwd - Project root
|
|
32
|
+
* @param options - Session / CLI overrides
|
|
33
|
+
*/
|
|
34
|
+
export async function resolveDevSqlEnv(
|
|
35
|
+
cwd: string,
|
|
36
|
+
options: ResolveDevSqlEnvOptions = {},
|
|
37
|
+
): Promise<ConfigEnv> {
|
|
38
|
+
if (options.env !== undefined) return options.env;
|
|
39
|
+
if (options.docker !== undefined) return options.docker ? "docker" : "local";
|
|
40
|
+
const saved = await readDevMode(cwd);
|
|
41
|
+
return saved ?? "local";
|
|
42
|
+
}
|
package/src/cli/stack.ts
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* `oke stack` — preview resolved images/tags/ports (writes nothing).
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
-
import { formatStackPreview, resolveStack } from "../docker/index.ts";
|
|
5
|
+
import { formatStackPreview, resolveStack, stackInstanceId } from "../docker/index.ts";
|
|
6
6
|
import { wantsJson } from "./args.ts";
|
|
7
7
|
import { EXIT_OK, EXIT_RUNTIME } from "./exit.ts";
|
|
8
8
|
import { loadOkeConfig, resolveImages } from "./load-config.ts";
|
|
@@ -26,10 +26,11 @@ export async function runStackPreview(options: StackCliOptions = {}): Promise<nu
|
|
|
26
26
|
const write = options.write ?? ((t) => process.stdout.write(t));
|
|
27
27
|
const writeErr = options.writeErr ?? ((t) => process.stderr.write(t));
|
|
28
28
|
const json = options.json ?? false;
|
|
29
|
+
const cwd = options.cwd ?? process.cwd();
|
|
29
30
|
let images = options.images;
|
|
30
31
|
if (!images) {
|
|
31
32
|
try {
|
|
32
|
-
const loaded = await loadOkeConfig(
|
|
33
|
+
const loaded = await loadOkeConfig(cwd, options.configPath);
|
|
33
34
|
images = resolveImages(loaded.config);
|
|
34
35
|
} catch (err) {
|
|
35
36
|
const msg = err instanceof Error ? err.message : String(err);
|
|
@@ -41,7 +42,8 @@ export async function runStackPreview(options: StackCliOptions = {}): Promise<nu
|
|
|
41
42
|
return EXIT_RUNTIME;
|
|
42
43
|
}
|
|
43
44
|
}
|
|
44
|
-
|
|
45
|
+
// Match `oke dev --docker` host ports (stable per-project offsets).
|
|
46
|
+
const rows = resolveStack({ images, instanceId: stackInstanceId(cwd) });
|
|
45
47
|
if (json) {
|
|
46
48
|
write(`${JSON.stringify({ ok: true, rows }, null, 2)}\n`);
|
|
47
49
|
if (rows.length === 0) {
|
|
@@ -67,7 +69,8 @@ export async function stackCli(args: readonly string[]): Promise<number> {
|
|
|
67
69
|
else if (a === "--help" || a === "-h") {
|
|
68
70
|
console.log(`oke stack [--config|-c oke.config.ts] [--json|-j]
|
|
69
71
|
|
|
70
|
-
Preview resolved images, recipes, and ports
|
|
72
|
+
Preview resolved images, recipes, and ports (same per-project host ports as
|
|
73
|
+
oke dev --docker). Writes nothing.
|
|
71
74
|
--json Machine-parseable JSON on stdout; hints on stderr.
|
|
72
75
|
`);
|
|
73
76
|
return EXIT_OK;
|
package/src/cli/vault-cmd.ts
CHANGED
|
@@ -2,9 +2,43 @@
|
|
|
2
2
|
* `oke vault` — set · list · import · key rotate.
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
5
6
|
import { resolve } from "node:path";
|
|
6
7
|
import { parseDotenv, formatDotenv } from "../drivers/vault-dotenv-parse.ts";
|
|
7
8
|
|
|
9
|
+
/** Resolved OpenBao target for direct API writes (docker / prod vault). */
|
|
10
|
+
interface OpenBaoTarget {
|
|
11
|
+
readonly url: string;
|
|
12
|
+
readonly token: string;
|
|
13
|
+
readonly mount: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Resolve a live OpenBao target when the active vault is OpenBao.
|
|
18
|
+
*
|
|
19
|
+
* Sources, in order: `OKE_VAULT_*` env · persisted `.oke/openbao/` host
|
|
20
|
+
* material + `docker/.env.docker` URL. Returns `null` when neither resolves
|
|
21
|
+
* (local dotenv loop) so `oke vault set` keeps the file path.
|
|
22
|
+
*
|
|
23
|
+
* @param cwd - Project root
|
|
24
|
+
*/
|
|
25
|
+
export async function resolveOpenBaoTarget(cwd: string): Promise<OpenBaoTarget | null> {
|
|
26
|
+
const mount = process.env.OKE_VAULT_MOUNT ?? "secret";
|
|
27
|
+
const envUrl = process.env.OKE_VAULT_URL;
|
|
28
|
+
const envToken = process.env.OKE_VAULT_TOKEN;
|
|
29
|
+
if (envUrl && envToken) {
|
|
30
|
+
return { url: envUrl.replace(/\/$/, ""), token: envToken, mount };
|
|
31
|
+
}
|
|
32
|
+
const appTokenPath = resolve(cwd, ".oke/openbao/app.token");
|
|
33
|
+
const composeEnvPath = resolve(cwd, "docker/.env.docker");
|
|
34
|
+
if (!existsSync(appTokenPath) || !existsSync(composeEnvPath)) return null;
|
|
35
|
+
const envFile = parseDotenv(readFileSync(composeEnvPath, "utf8"));
|
|
36
|
+
const url = envFile.get("OKE_VAULT_URL");
|
|
37
|
+
const token = readFileSync(appTokenPath, "utf8").trim();
|
|
38
|
+
if (!url || !token) return null;
|
|
39
|
+
return { url: url.replace(/\/$/, ""), token, mount };
|
|
40
|
+
}
|
|
41
|
+
|
|
8
42
|
/** In-memory / file-backed vault bag for CLI (dotenv-shaped). */
|
|
9
43
|
export interface VaultCliStore {
|
|
10
44
|
get(name: string): string | undefined;
|
|
@@ -78,6 +112,35 @@ oke vault key rotate
|
|
|
78
112
|
console.error("Usage: oke vault set <NAME> [value]");
|
|
79
113
|
return 1;
|
|
80
114
|
}
|
|
115
|
+
const ob = await resolveOpenBaoTarget(cwd);
|
|
116
|
+
if (ob && !options.store && !options.envFile) {
|
|
117
|
+
let value = rest[1];
|
|
118
|
+
if (value === undefined) {
|
|
119
|
+
const read =
|
|
120
|
+
options.readLine ??
|
|
121
|
+
(async () => {
|
|
122
|
+
write(`Enter value for ${name}: `);
|
|
123
|
+
const chunks: Buffer[] = [];
|
|
124
|
+
for await (const chunk of Bun.stdin.stream()) {
|
|
125
|
+
chunks.push(Buffer.from(chunk));
|
|
126
|
+
if (Buffer.concat(chunks).includes(0x0a)) break;
|
|
127
|
+
}
|
|
128
|
+
return Buffer.concat(chunks).toString("utf8").trim();
|
|
129
|
+
});
|
|
130
|
+
value = await read(`Enter value for ${name}: `);
|
|
131
|
+
}
|
|
132
|
+
const res = await fetch(`${ob.url}/v1/${ob.mount}/data/${encodeURIComponent(name)}`, {
|
|
133
|
+
method: "POST",
|
|
134
|
+
headers: { "X-Vault-Token": ob.token, "content-type": "application/json" },
|
|
135
|
+
body: JSON.stringify({ data: { value } }),
|
|
136
|
+
});
|
|
137
|
+
if (!res.ok) {
|
|
138
|
+
console.error(`oke vault: OpenBao write failed (${res.status}) — sealed / unauthorized?`);
|
|
139
|
+
return 1;
|
|
140
|
+
}
|
|
141
|
+
write(`oke vault: set ${name} (openbao)\n`);
|
|
142
|
+
return 0;
|
|
143
|
+
}
|
|
81
144
|
let value = rest[1];
|
|
82
145
|
if (value === undefined) {
|
|
83
146
|
const read =
|
package/src/client/types.ts
CHANGED
|
@@ -122,6 +122,8 @@ export interface TransportError {
|
|
|
122
122
|
|
|
123
123
|
/**
|
|
124
124
|
* Client call result — errors are values, never thrown for flow failures.
|
|
125
|
+
* Success may carry an optional top-level `meta` (Stripe-style envelope,
|
|
126
|
+
* e.g. paginated lists).
|
|
125
127
|
*
|
|
126
128
|
* @typeParam O - Success data
|
|
127
129
|
* @typeParam E - Declared error map
|
|
@@ -130,7 +132,11 @@ export type ClientResult<
|
|
|
130
132
|
O = unknown,
|
|
131
133
|
E extends Record<string, unknown> = Record<string, unknown>,
|
|
132
134
|
> =
|
|
133
|
-
| {
|
|
135
|
+
| {
|
|
136
|
+
readonly data: O;
|
|
137
|
+
readonly error: null;
|
|
138
|
+
readonly meta?: Record<string, unknown>;
|
|
139
|
+
}
|
|
134
140
|
| { readonly data: null; readonly error: ClientError<E> | TransportError };
|
|
135
141
|
|
|
136
142
|
/** Minimal fetch signature (avoids DOM `HeadersInit` / `preconnect` coupling). */
|
|
@@ -241,3 +241,43 @@ export const db = store.sql("app", { schema: { notes } });
|
|
|
241
241
|
});
|
|
242
242
|
});
|
|
243
243
|
});
|
|
244
|
+
|
|
245
|
+
describe("extractManifest — on(http.resource(...))", () => {
|
|
246
|
+
test("expands the mount into five CRUD bindings with store effects", async () => {
|
|
247
|
+
const source = `
|
|
248
|
+
import { on, http, store } from "okengine";
|
|
249
|
+
|
|
250
|
+
export const db = store.sql("notes", { schema: {} });
|
|
251
|
+
|
|
252
|
+
const notesR = store.resource(db, {}, { unit: "notes", breaking: true });
|
|
253
|
+
|
|
254
|
+
const mounted = on(http.resource("/notes", notesR.all()));
|
|
255
|
+
|
|
256
|
+
export const list = mounted.list;
|
|
257
|
+
export const create = mounted.create;
|
|
258
|
+
export const get = mounted.get;
|
|
259
|
+
export const update = mounted.update;
|
|
260
|
+
export const remove = mounted.remove;
|
|
261
|
+
`;
|
|
262
|
+
const manifest = await extractFromSources({ "src/flows/notes.ts": source });
|
|
263
|
+
|
|
264
|
+
expect(manifest.flows?.list?.trigger).toEqual({
|
|
265
|
+
http: { method: "GET", path: "/notes" },
|
|
266
|
+
});
|
|
267
|
+
expect(manifest.flows?.create?.trigger).toEqual({
|
|
268
|
+
http: { method: "POST", path: "/notes" },
|
|
269
|
+
});
|
|
270
|
+
expect(manifest.flows?.get?.trigger).toEqual({
|
|
271
|
+
http: { method: "GET", path: "/notes/:id" },
|
|
272
|
+
});
|
|
273
|
+
expect(manifest.flows?.update?.trigger).toEqual({
|
|
274
|
+
http: { method: "PATCH", path: "/notes/:id" },
|
|
275
|
+
});
|
|
276
|
+
expect(manifest.flows?.remove?.trigger).toEqual({
|
|
277
|
+
http: { method: "DELETE", path: "/notes/:id" },
|
|
278
|
+
});
|
|
279
|
+
expect(manifest.flows?.list?.effects?.reads).toEqual(["sql:notes"]);
|
|
280
|
+
expect(manifest.flows?.create?.effects?.writes).toEqual(["sql:notes"]);
|
|
281
|
+
expect(manifest.flows?.list?.breaking).toBe(true);
|
|
282
|
+
});
|
|
283
|
+
});
|
package/src/compiler/extract.ts
CHANGED
|
@@ -88,6 +88,11 @@ interface ProjectScope {
|
|
|
88
88
|
flows: Record<string, Flow>;
|
|
89
89
|
/** Export name → flow id (for agent tools). */
|
|
90
90
|
flowExports: Map<string, string>;
|
|
91
|
+
/** Local binding name → store.resource declaration (for on(http.resource)). */
|
|
92
|
+
resources: Map<
|
|
93
|
+
string,
|
|
94
|
+
{ storeName: string; storeRef: string; unit?: string; breaking?: boolean }
|
|
95
|
+
>;
|
|
91
96
|
}
|
|
92
97
|
|
|
93
98
|
/**
|
|
@@ -116,6 +121,7 @@ export async function extractManifest(options: ExtractManifestOptions = {}): Pro
|
|
|
116
121
|
drivers: {},
|
|
117
122
|
flows: {},
|
|
118
123
|
flowExports: new Map(),
|
|
124
|
+
resources: new Map(),
|
|
119
125
|
};
|
|
120
126
|
|
|
121
127
|
const parsed = files.map((file) => {
|
|
@@ -603,6 +609,27 @@ function visitDeclarationCall(call: CallExpression, program: AstNode, scope: Pro
|
|
|
603
609
|
});
|
|
604
610
|
}
|
|
605
611
|
}
|
|
612
|
+
|
|
613
|
+
// store.resource(db, table, { … }) — remember for on(http.resource(…))
|
|
614
|
+
if (prop === "resource") {
|
|
615
|
+
const bindingName = enclosingConstName(call, program);
|
|
616
|
+
const dbName = identifierName(call.arguments[0]);
|
|
617
|
+
const storeBinding = dbName ? scope.bindings.get(dbName) : undefined;
|
|
618
|
+
const ref =
|
|
619
|
+
storeBinding?.kind === "store" ? storeBinding.ref : (`sql:${dbName ?? "store"}` as const);
|
|
620
|
+
const storeName = ref.split(":")[1] ?? dbName ?? "store";
|
|
621
|
+
const unit = stringProp(objectArg(call.arguments[2]) ?? ({} as never), "unit");
|
|
622
|
+
const opts = objectArg(call.arguments[2]);
|
|
623
|
+
const breaking = opts ? boolProp(opts, "breaking") === true : false;
|
|
624
|
+
if (bindingName) {
|
|
625
|
+
scope.resources.set(bindingName, {
|
|
626
|
+
storeName,
|
|
627
|
+
storeRef: ref,
|
|
628
|
+
unit,
|
|
629
|
+
...(breaking ? { breaking: true } : {}),
|
|
630
|
+
});
|
|
631
|
+
}
|
|
632
|
+
}
|
|
606
633
|
}
|
|
607
634
|
|
|
608
635
|
// store.schema.table("notes", { … })
|
|
@@ -996,7 +1023,14 @@ function collectFlows(file: SourceFile, program: AstNode, scope: ProjectScope):
|
|
|
996
1023
|
if (callee === "on") {
|
|
997
1024
|
const triggerNode = call.arguments[0];
|
|
998
1025
|
const flowNode = call.arguments[1];
|
|
999
|
-
if (!triggerNode
|
|
1026
|
+
if (!triggerNode) return;
|
|
1027
|
+
|
|
1028
|
+
// on(http.resource(path, bag)) — expand the mount into five bindings.
|
|
1029
|
+
if (!flowNode) {
|
|
1030
|
+
registerResourceMount(call, triggerNode, file, program, scope);
|
|
1031
|
+
return;
|
|
1032
|
+
}
|
|
1033
|
+
|
|
1000
1034
|
const flowCall = unwrapFlowCall(flowNode);
|
|
1001
1035
|
if (!flowCall) return;
|
|
1002
1036
|
const exportName = enclosingConstName(call, program);
|
|
@@ -1139,6 +1173,7 @@ function registerFlow(args: {
|
|
|
1139
1173
|
|
|
1140
1174
|
if (boolProp(opts, "durable")) flow.durable = true;
|
|
1141
1175
|
if (boolProp(opts, "live") || liveFromTrigger) flow.live = true;
|
|
1176
|
+
if (boolProp(opts, "breaking")) flow.breaking = true;
|
|
1142
1177
|
|
|
1143
1178
|
const slo = parseSlo(objectProp(opts, "slo"));
|
|
1144
1179
|
if (slo) flow.slo = slo;
|
|
@@ -1213,6 +1248,93 @@ function registerFlow(args: {
|
|
|
1213
1248
|
args.scope.flows[name] = flow;
|
|
1214
1249
|
}
|
|
1215
1250
|
|
|
1251
|
+
/**
|
|
1252
|
+
* Expand `on(http.resource(path, bag))` into the five CRUD flows.
|
|
1253
|
+
*
|
|
1254
|
+
* The flows are synthesized at runtime by `store.resource(…)`; statically we
|
|
1255
|
+
* register list/create/get/update/remove with their HTTP triggers and the
|
|
1256
|
+
* resource's store effects so the Manifest stays complete.
|
|
1257
|
+
*
|
|
1258
|
+
* @param onCall - The outer `on(…)` call
|
|
1259
|
+
* @param triggerNode - `http.resource(…)` expression
|
|
1260
|
+
* @param file - Source file
|
|
1261
|
+
* @param program - Program root (for export-name lookup)
|
|
1262
|
+
* @param scope - Project scope
|
|
1263
|
+
*/
|
|
1264
|
+
function registerResourceMount(
|
|
1265
|
+
onCall: CallExpression,
|
|
1266
|
+
triggerNode: AstNode,
|
|
1267
|
+
file: SourceFile,
|
|
1268
|
+
program: AstNode,
|
|
1269
|
+
scope: ProjectScope,
|
|
1270
|
+
): void {
|
|
1271
|
+
if (triggerNode.type !== "CallExpression") return;
|
|
1272
|
+
const httpCall = triggerNode as CallExpression;
|
|
1273
|
+
const callee = httpCall.callee;
|
|
1274
|
+
if (callee.type !== "MemberExpression") return;
|
|
1275
|
+
const member = callee as AstNode & { object: AstNode; property: AstNode };
|
|
1276
|
+
if (identifierName(member.object) !== "http") return;
|
|
1277
|
+
if (identifierName(member.property) !== "resource") return;
|
|
1278
|
+
|
|
1279
|
+
const path = stringArg(httpCall.arguments[0]);
|
|
1280
|
+
if (!path) return;
|
|
1281
|
+
|
|
1282
|
+
// bag: `notesR.all()` → resolve `notesR` to its store.resource declaration.
|
|
1283
|
+
const bagNode = httpCall.arguments[1];
|
|
1284
|
+
let baseName: string | undefined;
|
|
1285
|
+
if (bagNode?.type === "CallExpression") {
|
|
1286
|
+
const bagCallee = (bagNode as CallExpression).callee;
|
|
1287
|
+
if (bagCallee.type === "MemberExpression") {
|
|
1288
|
+
baseName = identifierName((bagCallee as AstNode & { object: AstNode }).object);
|
|
1289
|
+
}
|
|
1290
|
+
} else if (bagNode?.type === "Identifier") {
|
|
1291
|
+
baseName = identifierName(bagNode);
|
|
1292
|
+
}
|
|
1293
|
+
const resource = baseName ? scope.resources.get(baseName) : undefined;
|
|
1294
|
+
|
|
1295
|
+
const unit = resource?.unit;
|
|
1296
|
+
const storeRef = resource?.storeRef as Effects["reads"] extends readonly (infer R)[] | undefined
|
|
1297
|
+
? R
|
|
1298
|
+
: never;
|
|
1299
|
+
const effects: Effects | undefined = storeRef
|
|
1300
|
+
? { reads: [storeRef], writes: [storeRef] }
|
|
1301
|
+
: undefined;
|
|
1302
|
+
const idPath = `${path}/:id`;
|
|
1303
|
+
const line = lineAt(file.source, onCall.start ?? 0);
|
|
1304
|
+
|
|
1305
|
+
const verbs = [
|
|
1306
|
+
{ op: "list", method: "GET", p: path, eff: storeRef ? { reads: [storeRef] } : undefined },
|
|
1307
|
+
{ op: "create", method: "POST", p: path, eff: storeRef ? { writes: [storeRef] } : undefined },
|
|
1308
|
+
{ op: "get", method: "GET", p: idPath, eff: storeRef ? { reads: [storeRef] } : undefined },
|
|
1309
|
+
{ op: "update", method: "PATCH", p: idPath, eff: effects },
|
|
1310
|
+
{
|
|
1311
|
+
op: "remove",
|
|
1312
|
+
method: "DELETE",
|
|
1313
|
+
p: idPath,
|
|
1314
|
+
eff: storeRef ? { writes: [storeRef] } : undefined,
|
|
1315
|
+
},
|
|
1316
|
+
] as const;
|
|
1317
|
+
|
|
1318
|
+
for (const v of verbs) {
|
|
1319
|
+
// Short op names match `export const list = mounted.list` so Manifest Diff
|
|
1320
|
+
// keeps flow identity across a handwritten → store.resource migration.
|
|
1321
|
+
const name = v.op;
|
|
1322
|
+
const flow: Flow = {
|
|
1323
|
+
trigger: { http: { method: v.method as never, path: v.p } },
|
|
1324
|
+
...(v.eff ? { effects: v.eff as Effects } : {}),
|
|
1325
|
+
...(resource?.breaking ? { breaking: true } : {}),
|
|
1326
|
+
source: `${file.path}:${line}`,
|
|
1327
|
+
};
|
|
1328
|
+
scope.flows[name] = flow;
|
|
1329
|
+
scope.bindings.set(name, { kind: "flow", ref: name });
|
|
1330
|
+
}
|
|
1331
|
+
|
|
1332
|
+
const exportName = enclosingConstName(onCall, program);
|
|
1333
|
+
if (exportName) {
|
|
1334
|
+
scope.bindings.set(exportName, { kind: "flow", ref: unit ?? baseName ?? exportName });
|
|
1335
|
+
}
|
|
1336
|
+
}
|
|
1337
|
+
|
|
1216
1338
|
interface ParsedTrigger {
|
|
1217
1339
|
trigger: Trigger;
|
|
1218
1340
|
gates?: string[];
|
|
@@ -2,7 +2,7 @@ import { defineConfig } from "okengine/config";
|
|
|
2
2
|
|
|
3
3
|
export default defineConfig({
|
|
4
4
|
drivers: {
|
|
5
|
-
prod: ["postgres", "redis", "s3", "smtp", "
|
|
5
|
+
prod: ["postgres", "redis", "s3", "smtp", "openbao", "anthropic", "pgvector"],
|
|
6
6
|
store: {
|
|
7
7
|
sql: { local: "sqlite", prod: { driver: "postgres" } },
|
|
8
8
|
kv: { prod: "redis" },
|
|
@@ -10,7 +10,7 @@ export default defineConfig({
|
|
|
10
10
|
index: { prod: "pgvector" },
|
|
11
11
|
},
|
|
12
12
|
signal: { prod: "postgres" },
|
|
13
|
-
vault: { prod: "
|
|
13
|
+
vault: { prod: "openbao" },
|
|
14
14
|
channel: { email: { prod: "smtp" } },
|
|
15
15
|
ai: { prod: { driver: "anthropic" } },
|
|
16
16
|
},
|
package/src/compiler/response.ts
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
import type { FlowFailure } from "../kernel/errors.ts";
|
|
8
|
+
import { isJsonResult } from "../kernel/fx.ts";
|
|
8
9
|
import { isFlowFailure } from "../kernel/hooks.ts";
|
|
9
10
|
import { VALIDATION_ERROR_CODE } from "../validation/standard-schema.ts";
|
|
10
11
|
|
|
@@ -12,6 +13,7 @@ import { VALIDATION_ERROR_CODE } from "../validation/standard-schema.ts";
|
|
|
12
13
|
export interface SuccessEnvelope {
|
|
13
14
|
readonly data: unknown;
|
|
14
15
|
readonly error: null;
|
|
16
|
+
readonly meta?: Record<string, unknown>;
|
|
15
17
|
}
|
|
16
18
|
|
|
17
19
|
/** Failure envelope. */
|
|
@@ -45,10 +47,20 @@ export function statusForFailure(failure: FlowFailure): number {
|
|
|
45
47
|
|
|
46
48
|
/**
|
|
47
49
|
* Encode a successful output as JSON `{ data, error: null }`.
|
|
50
|
+
* An `fx.json` carrier overrides status (`create` → 201) and attaches
|
|
51
|
+
* top-level `meta` (Stripe-style envelope).
|
|
48
52
|
*
|
|
49
53
|
* @param output - Handler output (`undefined` → 204)
|
|
50
54
|
*/
|
|
51
55
|
export function encodeSuccess(output: unknown): Response {
|
|
56
|
+
if (isJsonResult(output)) {
|
|
57
|
+
if (output.status === 204) {
|
|
58
|
+
return new Response(null, { status: 204 });
|
|
59
|
+
}
|
|
60
|
+
const body: SuccessEnvelope = { data: output.value, error: null };
|
|
61
|
+
const envelope = output.meta === undefined ? body : { ...body, meta: output.meta };
|
|
62
|
+
return Response.json(envelope, { status: output.status });
|
|
63
|
+
}
|
|
52
64
|
if (output === undefined) {
|
|
53
65
|
return new Response(null, { status: 204 });
|
|
54
66
|
}
|
|
@@ -25,10 +25,8 @@ describe("fillDockerFromProd / defineConfig", () => {
|
|
|
25
25
|
).toBe("memory");
|
|
26
26
|
});
|
|
27
27
|
|
|
28
|
-
test("vault
|
|
29
|
-
expect(fillDockerFromProd({ local: "dotenv", prod: "
|
|
30
|
-
"dotenv",
|
|
31
|
-
);
|
|
28
|
+
test("vault prod → docker copies the prod driver", () => {
|
|
29
|
+
expect(fillDockerFromProd({ local: "dotenv", prod: "openbao" })?.docker).toBe("openbao");
|
|
32
30
|
});
|
|
33
31
|
|
|
34
32
|
test("defineConfig fills every element from prod", () => {
|
|
@@ -41,7 +39,7 @@ describe("fillDockerFromProd / defineConfig", () => {
|
|
|
41
39
|
},
|
|
42
40
|
signal: { local: "memory", prod: "postgres" },
|
|
43
41
|
clock: { local: "memory", prod: "postgres" },
|
|
44
|
-
vault: { local: "dotenv", prod: "
|
|
42
|
+
vault: { local: "dotenv", prod: "openbao" },
|
|
45
43
|
channel: {
|
|
46
44
|
email: { local: "console", prod: "smtp" },
|
|
47
45
|
},
|
|
@@ -53,7 +51,7 @@ describe("fillDockerFromProd / defineConfig", () => {
|
|
|
53
51
|
expect(cfg.drivers?.store?.files?.docker).toBe("s3");
|
|
54
52
|
expect(cfg.drivers?.signal?.docker).toBe("postgres");
|
|
55
53
|
expect(cfg.drivers?.clock?.docker).toBe("postgres");
|
|
56
|
-
expect(cfg.drivers?.vault?.docker).toBe("
|
|
54
|
+
expect(cfg.drivers?.vault?.docker).toBe("openbao");
|
|
57
55
|
expect(cfg.drivers?.channel?.email?.docker).toBe("smtp");
|
|
58
56
|
expect(cfg.drivers?.ai?.docker).toBe("anthropic");
|
|
59
57
|
});
|
package/src/config/index.ts
CHANGED
|
@@ -293,24 +293,11 @@ export function normalizeDriversConfig(
|
|
|
293
293
|
/**
|
|
294
294
|
* Fill missing `docker` pins from `prod` (compose infra ≈ production protocols).
|
|
295
295
|
*
|
|
296
|
-
* Vault is the exception: `prod: "sops"` becomes `docker: "dotenv"` so
|
|
297
|
-
* `oke dev -d` can read `docker/.env.docker` without age keys.
|
|
298
|
-
*
|
|
299
296
|
* @param map - Env → driver map
|
|
300
|
-
* @param options - Element-specific defaults
|
|
301
297
|
*/
|
|
302
|
-
export function fillDockerFromProd(
|
|
303
|
-
map: EnvDriverMap | undefined,
|
|
304
|
-
options: { readonly vault?: boolean } = {},
|
|
305
|
-
): EnvDriverMap | undefined {
|
|
298
|
+
export function fillDockerFromProd(map: EnvDriverMap | undefined): EnvDriverMap | undefined {
|
|
306
299
|
if (!map) return undefined;
|
|
307
300
|
if (map.docker !== undefined || map.prod === undefined) return map;
|
|
308
|
-
if (options.vault) {
|
|
309
|
-
const prodId = typeof map.prod === "string" ? map.prod : map.prod.driver;
|
|
310
|
-
if (prodId === "sops") {
|
|
311
|
-
return { ...map, docker: "dotenv" };
|
|
312
|
-
}
|
|
313
|
-
}
|
|
314
301
|
return { ...map, docker: map.prod };
|
|
315
302
|
}
|
|
316
303
|
|
|
@@ -346,7 +333,7 @@ export function fillDriversDockerFromProd(
|
|
|
346
333
|
...(store !== undefined ? { store } : {}),
|
|
347
334
|
signal: fillDockerFromProd(drivers.signal),
|
|
348
335
|
clock: fillDockerFromProd(drivers.clock),
|
|
349
|
-
vault: fillDockerFromProd(drivers.vault
|
|
336
|
+
vault: fillDockerFromProd(drivers.vault),
|
|
350
337
|
...(channel !== undefined ? { channel } : {}),
|
|
351
338
|
ai: fillDockerFromProd(drivers.ai),
|
|
352
339
|
runs: fillDockerFromProd(drivers.runs),
|
|
@@ -56,6 +56,8 @@ export function createConsoleApp(options: CreateConsoleAppOptions = {}): Console
|
|
|
56
56
|
|
|
57
57
|
const app = oke({
|
|
58
58
|
name: options.name ?? "console",
|
|
59
|
+
// Isolated source — leftover host `on()` registrations never enter Console.
|
|
60
|
+
registry: "ignore",
|
|
59
61
|
bindings,
|
|
60
62
|
autoBoot: false,
|
|
61
63
|
fx: { now: state.now },
|
|
@@ -6,12 +6,15 @@
|
|
|
6
6
|
* rotation blast radius are derived — never reimplemented in the UI.
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
+
import { readFileSync } from "node:fs";
|
|
9
10
|
import { resolve } from "node:path";
|
|
10
11
|
import type { ConfigEnv } from "../../config/index.ts";
|
|
11
|
-
import { envVaultDriver, memoryVaultDriver } from "../../drivers/index.ts";
|
|
12
|
+
import { envVaultDriver, memoryVaultDriver, openbaoVaultDriver } from "../../drivers/index.ts";
|
|
13
|
+
import { parseDotenv } from "../../drivers/vault-dotenv-parse.ts";
|
|
12
14
|
import {
|
|
13
15
|
createVaultRuntime,
|
|
14
16
|
vault as declareVault,
|
|
17
|
+
type VaultChainLayer,
|
|
15
18
|
type VaultResolutionSource,
|
|
16
19
|
type VaultResolutionStep,
|
|
17
20
|
type VaultRuntime,
|
|
@@ -234,6 +237,22 @@ export async function createManifestVaultRuntime(
|
|
|
234
237
|
|
|
235
238
|
const seed = options.seed ?? {};
|
|
236
239
|
const composeEnv = resolveComposeEnvPath(cwd);
|
|
240
|
+
const envMap = parseDotenv(readFileSync(composeEnv.path, "utf8").toString());
|
|
241
|
+
const openbaoUrl = process.env.OKE_VAULT_URL ?? envMap.get("OKE_VAULT_URL");
|
|
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
|
+
};
|
|
237
256
|
const runtime = createVaultRuntime({
|
|
238
257
|
secrets,
|
|
239
258
|
allowDevFallbacks: options.allowDevFallbacks ?? (options.env ?? "local") !== "prod",
|
|
@@ -250,11 +269,7 @@ export async function createManifestVaultRuntime(
|
|
|
250
269
|
source: composeEnv.source,
|
|
251
270
|
options: { path: composeEnv.path },
|
|
252
271
|
},
|
|
253
|
-
|
|
254
|
-
driver: memoryVaultDriver,
|
|
255
|
-
source: "driver",
|
|
256
|
-
options: { secrets: seed },
|
|
257
|
-
},
|
|
272
|
+
driverLayer,
|
|
258
273
|
],
|
|
259
274
|
});
|
|
260
275
|
await runtime.boot();
|