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.
Files changed (180) hide show
  1. package/AGENTS.md +17 -15
  2. package/README.md +57 -29
  3. package/package.json +11 -16
  4. package/site/content/docs/ai/llms-txt.mdx +54 -0
  5. package/site/content/docs/ai/mcp.mdx +123 -0
  6. package/site/content/docs/ai/meta.json +5 -0
  7. package/site/content/docs/ai/skills.mdx +53 -0
  8. package/site/content/docs/console/access.mdx +29 -0
  9. package/site/content/docs/console/ai.mdx +35 -0
  10. package/site/content/docs/console/architecture.mdx +35 -0
  11. package/site/content/docs/console/channels.mdx +37 -0
  12. package/site/content/docs/console/clock.mdx +31 -0
  13. package/site/content/docs/console/flows.mdx +31 -0
  14. package/site/content/docs/console/gates.mdx +35 -0
  15. package/site/content/docs/console/manifest-diff.mdx +34 -0
  16. package/site/content/docs/console/meta.json +23 -0
  17. package/site/content/docs/console/overview.mdx +40 -0
  18. package/site/content/docs/console/plugins.mdx +41 -0
  19. package/site/content/docs/console/privacy.mdx +32 -0
  20. package/site/content/docs/console/runs.mdx +40 -0
  21. package/site/content/docs/console/signals.mdx +31 -0
  22. package/site/content/docs/console/store.mdx +32 -0
  23. package/site/content/docs/console/tenancy.mdx +32 -0
  24. package/site/content/docs/console/traces.mdx +34 -0
  25. package/site/content/docs/console/vault.mdx +37 -0
  26. package/site/content/docs/elements/ai.mdx +180 -0
  27. package/site/content/docs/elements/channel.mdx +167 -0
  28. package/site/content/docs/elements/clock.mdx +182 -0
  29. package/site/content/docs/elements/flow.mdx +288 -0
  30. package/site/content/docs/elements/gate.mdx +171 -0
  31. package/site/content/docs/elements/meta.json +5 -0
  32. package/site/content/docs/elements/signal.mdx +171 -0
  33. package/site/content/docs/elements/store.mdx +320 -0
  34. package/site/content/docs/elements/vault.mdx +263 -0
  35. package/site/content/docs/get-started/basic-usage.mdx +124 -0
  36. package/site/content/docs/get-started/comparison.mdx +65 -0
  37. package/site/content/docs/get-started/installation.mdx +113 -0
  38. package/site/content/docs/get-started/introduction.mdx +123 -0
  39. package/site/content/docs/get-started/meta.json +5 -0
  40. package/site/content/docs/index.mdx +63 -0
  41. package/site/content/docs/meta.json +5 -0
  42. package/site/content/docs/plugins/compression.mdx +60 -0
  43. package/site/content/docs/plugins/cors.mdx +92 -0
  44. package/site/content/docs/plugins/csrf.mdx +96 -0
  45. package/site/content/docs/plugins/ip-allowlist.mdx +92 -0
  46. package/site/content/docs/plugins/maintenance-mode.mdx +101 -0
  47. package/site/content/docs/plugins/meta.json +15 -0
  48. package/site/content/docs/plugins/security-headers.mdx +136 -0
  49. package/site/content/docs/reference/cli.md +101 -0
  50. package/site/content/docs/reference/configuration.mdx +159 -0
  51. package/site/content/docs/reference/environment-variables.mdx +87 -0
  52. package/site/content/docs/reference/errors.mdx +80 -0
  53. package/site/content/docs/reference/fx.mdx +117 -0
  54. package/site/content/docs/reference/meta.json +5 -0
  55. package/site/content/docs/reference/plugins.mdx +249 -0
  56. package/site/content/docs/reference/security.md +63 -0
  57. package/src/auth/auth.test.ts +3 -0
  58. package/src/cli/ask-dev-mode.ts +1 -1
  59. package/src/cli/db.ts +87 -17
  60. package/src/cli/dev-db-push.test.ts +32 -2
  61. package/src/cli/dev-schema-sync.test.ts +66 -0
  62. package/src/cli/dev-schema-sync.ts +139 -0
  63. package/src/cli/dev.test.ts +123 -1
  64. package/src/cli/dev.ts +150 -8
  65. package/src/cli/doc-staleness.test.ts +4 -4
  66. package/src/cli/docker-cli.test.ts +20 -0
  67. package/src/cli/docker.ts +10 -0
  68. package/src/cli/drizzle-env.test.ts +67 -0
  69. package/src/cli/drizzle-env.ts +78 -0
  70. package/src/cli/ensure-drizzle-config.ts +50 -0
  71. package/src/cli/hero-meta.test.ts +1 -1
  72. package/src/cli/load-config.ts +6 -0
  73. package/src/cli/mode.ts +24 -4
  74. package/src/cli/openbao-bootstrap.test.ts +147 -0
  75. package/src/cli/openbao-bootstrap.ts +280 -0
  76. package/src/cli/openbao-restart.integration.test.ts +136 -0
  77. package/src/cli/ports.test.ts +7 -5
  78. package/src/cli/ports.ts +6 -2
  79. package/src/cli/resolve-dev-sql-env.test.ts +48 -0
  80. package/src/cli/resolve-dev-sql-env.ts +42 -0
  81. package/src/cli/stack.ts +7 -4
  82. package/src/cli/vault-cmd.ts +63 -0
  83. package/src/client/types.ts +7 -1
  84. package/src/compiler/extract.test.ts +40 -0
  85. package/src/compiler/extract.ts +123 -1
  86. package/src/compiler/fixtures/skyport/oke.config.ts +2 -2
  87. package/src/compiler/fixtures/skyport.expected.json +1 -1
  88. package/src/compiler/response.ts +12 -0
  89. package/src/config/define-config.test.ts +4 -6
  90. package/src/config/index.ts +2 -15
  91. package/src/console/server/app.ts +2 -0
  92. package/src/console/server/vault.ts +21 -6
  93. package/src/docker/compose.ts +112 -16
  94. package/src/docker/derive.ts +7 -1
  95. package/src/docker/docker.test.ts +103 -0
  96. package/src/docker/index.ts +11 -1
  97. package/src/docker/recipes/index.ts +3 -2
  98. package/src/docker/recipes/openbao.ts +47 -0
  99. package/src/docker/recipes/redis.ts +5 -1
  100. package/src/docker/recipes/rustfs.ts +2 -3
  101. package/src/docker/stack-id.test.ts +43 -8
  102. package/src/docker/stack-id.ts +99 -20
  103. package/src/docker/stack.ts +36 -4
  104. package/src/docker/types.ts +3 -0
  105. package/src/docs-origin.ts +4 -4
  106. package/src/drivers/drizzle-dialect.test.ts +20 -0
  107. package/src/drivers/drizzle-dialect.ts +37 -0
  108. package/src/drivers/index.ts +1 -2
  109. package/src/drivers/memory.ts +278 -39
  110. package/src/drivers/s3.ts +10 -1
  111. package/src/drivers/vault-driver-removal.test.ts +55 -0
  112. package/src/drivers/vault-openbao.test.ts +97 -0
  113. package/src/drivers/vault-openbao.ts +102 -35
  114. package/src/drivers/vault-types.ts +3 -10
  115. package/src/elements/store/declare.ts +4 -1
  116. package/src/elements/store/resource-list-docs.fixture.ts +56 -0
  117. package/src/elements/store/resource-list-docs.test.ts +79 -0
  118. package/src/elements/store/resource.test.ts +253 -0
  119. package/src/elements/store/resource.ts +786 -0
  120. package/src/elements/store/sql-condition.test.ts +132 -0
  121. package/src/elements/store/sql-condition.ts +284 -46
  122. package/src/elements/store/sql-session.test.ts +86 -1
  123. package/src/elements/store/sql-session.ts +187 -27
  124. package/src/elements/store/table.ts +34 -4
  125. package/src/elements/store.ts +16 -0
  126. package/src/elements/vault/runtime.ts +1 -1
  127. package/src/elements/vault.test.ts +1 -28
  128. package/src/elements/vault.ts +1 -1
  129. package/src/kernel/app.ts +59 -25
  130. package/src/kernel/boot-bind/channel.test.ts +60 -0
  131. package/src/kernel/boot-bind/channel.ts +64 -2
  132. package/src/kernel/boot-bind/store.test.ts +10 -1
  133. package/src/kernel/boot-bind/store.ts +49 -2
  134. package/src/kernel/boot.test.ts +0 -1
  135. package/src/kernel/boot.ts +1 -1
  136. package/src/kernel/edge.test.ts +68 -0
  137. package/src/kernel/errors.registry.test.ts +1 -1
  138. package/src/kernel/flow.ts +8 -0
  139. package/src/kernel/fx.test.ts +23 -3
  140. package/src/kernel/fx.ts +115 -18
  141. package/src/kernel/hooks.test.ts +33 -0
  142. package/src/kernel/hooks.ts +22 -0
  143. package/src/kernel/index.ts +7 -0
  144. package/src/kernel/on.ts +44 -3
  145. package/src/kernel/plugin.ts +33 -3
  146. package/src/kernel/registry-isolation.test.ts +74 -0
  147. package/src/kernel/registry.ts +22 -1
  148. package/src/kernel/triggers.ts +59 -0
  149. package/src/manifest/fixtures/skyport.excerpt.json +1 -1
  150. package/src/manifest/fixtures/skyport.manifest.json +1 -1
  151. package/src/manifest/index.ts +1 -1
  152. package/src/manifest/types.ts +1 -1
  153. package/src/manifest/validate.ts +2 -2
  154. package/src/plugins/compression.test.ts +127 -0
  155. package/src/plugins/compression.ts +94 -0
  156. package/src/plugins/config-source.test.ts +204 -0
  157. package/src/plugins/config-source.ts +209 -0
  158. package/src/plugins/cors.test.ts +138 -0
  159. package/src/plugins/cors.ts +129 -0
  160. package/src/plugins/csrf.test.ts +102 -0
  161. package/src/plugins/csrf.ts +86 -0
  162. package/src/plugins/headers.ts +54 -0
  163. package/src/plugins/index.ts +26 -0
  164. package/src/plugins/ip-allowlist.test.ts +105 -0
  165. package/src/plugins/ip-allowlist.ts +76 -0
  166. package/src/plugins/maintenance-mode.test.ts +91 -0
  167. package/src/plugins/maintenance-mode.ts +85 -0
  168. package/src/plugins/security-headers.test.ts +243 -0
  169. package/src/plugins/security-headers.ts +255 -0
  170. package/src/release/measure.ts +1 -2
  171. package/src/test/create-test-app.ts +14 -2
  172. package/docs/spec/console.md +0 -762
  173. package/docs/spec/example.md +0 -1374
  174. package/docs/spec/four-applications.md +0 -1376
  175. package/docs/spec/unified-theory.md +0 -498
  176. package/src/cli/doc-drift.test.ts +0 -147
  177. package/src/cli/doc-drift.ts +0 -401
  178. package/src/cli/doctor-diff-examples.ts +0 -90
  179. package/src/drivers/vault-sops.ts +0 -246
  180. /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(options.cwd ?? process.cwd(), options.configPath);
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
- const rows = resolveStack({ images });
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. Writes nothing.
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;
@@ -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 =
@@ -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
- | { readonly data: O; readonly error: null }
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
+ });
@@ -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 || !flowNode) return;
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", "sops", "anthropic", "pgvector"],
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: "sops" },
13
+ vault: { prod: "openbao" },
14
14
  channel: { email: { prod: "smtp" } },
15
15
  ai: { prod: { driver: "anthropic" } },
16
16
  },
@@ -162,7 +162,7 @@
162
162
  }
163
163
  },
164
164
  "drivers": {
165
- "prod": ["postgres", "redis", "s3", "smtp", "sops", "anthropic", "pgvector"]
165
+ "prod": ["postgres", "redis", "s3", "smtp", "openbao", "anthropic", "pgvector"]
166
166
  },
167
167
  "tenancy": {
168
168
  "isolation": "row"
@@ -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 sops → docker dotenv", () => {
29
- expect(fillDockerFromProd({ local: "dotenv", prod: "sops" }, { vault: true })?.docker).toBe(
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: "sops" },
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("dotenv");
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
  });
@@ -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, { vault: true }),
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();