okengine 0.13.0 → 0.15.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 (277) hide show
  1. package/manifest.v1.schema.json +7 -1
  2. package/package.json +4 -5
  3. package/site/content/docs/ai/mcp.mdx +27 -2
  4. package/site/content/docs/elements/ai.mdx +59 -11
  5. package/site/content/docs/elements/channel.mdx +1 -0
  6. package/site/content/docs/elements/clock.mdx +30 -12
  7. package/site/content/docs/elements/flow.mdx +9 -3
  8. package/site/content/docs/elements/gate.mdx +25 -22
  9. package/site/content/docs/elements/signal.mdx +71 -25
  10. package/site/content/docs/elements/store.mdx +87 -28
  11. package/site/content/docs/elements/vault.mdx +1 -0
  12. package/site/content/docs/get-started/basic-usage.mdx +22 -13
  13. package/site/content/docs/get-started/index.mdx +5 -0
  14. package/site/content/docs/get-started/installation.mdx +2 -2
  15. package/site/content/docs/get-started/introduction.mdx +11 -3
  16. package/site/content/docs/get-started/meta.json +1 -1
  17. package/site/content/docs/get-started/project-structure.mdx +932 -0
  18. package/site/content/docs/reference/cli.md +13 -10
  19. package/site/content/docs/reference/client.mdx +25 -21
  20. package/site/content/docs/reference/fx.mdx +27 -22
  21. package/site/content/docs/reference/security.md +1 -1
  22. package/src/auth/bindings.ts +14 -5
  23. package/src/auth/cross-plane.ts +10 -3
  24. package/src/cli/ai-setup/ai-setup.test.ts +62 -0
  25. package/src/cli/ai-setup/apply.ts +32 -1
  26. package/src/cli/ai-setup/recommend.test.ts +25 -0
  27. package/src/cli/ai-setup/recommend.ts +8 -3
  28. package/src/cli/attach-host-console.test.ts +3 -2
  29. package/src/cli/attach-host-console.ts +3 -0
  30. package/src/cli/build.test.ts +4 -2
  31. package/src/cli/db-seed.ts +48 -15
  32. package/src/cli/db.test.ts +104 -0
  33. package/src/cli/db.ts +122 -10
  34. package/src/cli/dev-controls.test.ts +20 -0
  35. package/src/cli/dev-controls.ts +13 -0
  36. package/src/cli/dev.test.ts +15 -4
  37. package/src/cli/dev.ts +102 -29
  38. package/src/cli/ensure-drizzle-config.ts +2 -1
  39. package/src/cli/tui/DevLive.tsx +12 -2
  40. package/src/cli/tui/keys.test.ts +2 -1
  41. package/src/cli/tui/keys.ts +3 -2
  42. package/src/compiler/aot.test.ts +1 -1
  43. package/src/compiler/effects-infer.ts +58 -3
  44. package/src/compiler/extract.test.ts +458 -29
  45. package/src/compiler/extract.ts +387 -69
  46. package/src/compiler/flow-path.test.ts +96 -0
  47. package/src/compiler/flow-path.ts +173 -0
  48. package/src/compiler/generate-adopt.test.ts +112 -3
  49. package/src/compiler/generate-adopt.ts +280 -26
  50. package/src/compiler/response.ts +45 -1
  51. package/src/compiler/schema-from-ast.ts +609 -0
  52. package/src/console/server/ai.ts +5 -2
  53. package/src/console/server/bind.ts +2 -2
  54. package/src/console/server/console.test.ts +3 -0
  55. package/src/console/server/flows.ts +121 -40
  56. package/src/console/server/invoke-user-flow.test.ts +3 -1
  57. package/src/console/server/invoke-user-flow.ts +17 -2
  58. package/src/console/server/iso-at.test.ts +9 -0
  59. package/src/console/server/iso-at.ts +11 -0
  60. package/src/console/server/security-headers.ts +6 -3
  61. package/src/console/server/serve.ts +17 -2
  62. package/src/console/server/spa-proxy.test.ts +1 -0
  63. package/src/console/server/sql-catalog.ts +147 -7
  64. package/src/console/server/sql-rls.test.ts +16 -0
  65. package/src/console/server/state.ts +8 -0
  66. package/src/console/server/store.test.ts +7 -4
  67. package/src/console/server/store.ts +142 -58
  68. package/src/console/ui-next/dist/assets/agent-disclosure-C0X1fbWF.js +1 -0
  69. package/src/console/ui-next/dist/assets/cache-glyph-F1FI122b.js +1 -0
  70. package/src/console/ui-next/dist/assets/{call-pii-button-bqkxMrJH.js → call-pii-button-DEDCl_j3.js} +1 -1
  71. package/src/console/ui-next/dist/assets/collapsible-LPqGvfoz.js +1 -0
  72. package/src/console/ui-next/dist/assets/{confirm-sheet-DDCRmG62.js → confirm-sheet-3ptDzXbz.js} +1 -1
  73. package/src/console/ui-next/dist/assets/{duration-tone-oiRxPVsZ.js → duration-tone-sC3lGABz.js} +1 -1
  74. package/src/console/ui-next/dist/assets/{explorer-empty-HjTnVQoR.js → explorer-empty-BvYrygyO.js} +1 -1
  75. package/src/console/ui-next/dist/assets/flows-page-Dluo1Bd7.js +1 -0
  76. package/src/console/ui-next/dist/assets/{highlighted-json-CvDPvveV.js → highlighted-json-CS_O8L-r.js} +1 -1
  77. package/src/console/ui-next/dist/assets/http-method--sWDdXSB.js +1 -0
  78. package/src/console/ui-next/dist/assets/index-Ca3HZMVq.js +66 -0
  79. package/src/console/ui-next/dist/assets/index-UtTDRQpS.css +2 -0
  80. package/src/console/ui-next/dist/assets/{link-DF7SZ9Ek.js → link-COeyggt-.js} +1 -1
  81. package/src/console/ui-next/dist/assets/observability-page-HK9-BO8z.js +4 -0
  82. package/src/console/ui-next/dist/assets/react-D8E3mtu1.js +1 -0
  83. package/src/console/ui-next/dist/assets/replica-lag-DBIFFf7d.js +18 -0
  84. package/src/console/ui-next/dist/assets/sheet-form-Dnwa5oOR.js +1 -0
  85. package/src/console/ui-next/dist/assets/shortcut-keys-CoqwEIi0.js +1 -0
  86. package/src/console/ui-next/dist/assets/skeleton-BHcfDYcb.js +1 -0
  87. package/src/console/ui-next/dist/assets/store-page-BSohH3wM.js +41 -0
  88. package/src/console/ui-next/dist/assets/trace-detail-sheet-CFIGRnnA.js +2 -0
  89. package/src/console/ui-next/dist/assets/tree-expand-toggle-BgLl34w7.js +54 -0
  90. package/src/console/ui-next/dist/assets/units-page-B0cFE76A.js +1 -0
  91. package/src/console/ui-next/dist/assets/{use-vault-list-Cl79j_ku.js → use-vault-list-BLbZhzyF.js} +1 -1
  92. package/src/console/ui-next/dist/assets/vault-page-B1SbYe10.js +2 -0
  93. package/src/console/ui-next/dist/index.html +6 -6
  94. package/src/console/ui-next/seed-invoke-host.ts +2 -0
  95. package/src/console/ui-next/src/client.ts +35 -7
  96. package/src/console/ui-next/src/components/motion/table/index.tsx +16 -0
  97. package/src/console/ui-next/src/components/motion/table/types.ts +4 -0
  98. package/src/console/ui-next/src/components/ui/kbd.tsx +2 -2
  99. package/src/console/ui-next/src/features/flows/graph/build-flow-graph.test.ts +18 -0
  100. package/src/console/ui-next/src/features/flows/graph/build-flow-graph.ts +14 -1
  101. package/src/console/ui-next/src/features/flows/graph/neighborhood.test.ts +17 -0
  102. package/src/console/ui-next/src/features/flows/graph/neighborhood.ts +16 -3
  103. package/src/console/ui-next/src/features/flows/traces/effect-kind.ts +3 -1
  104. package/src/console/ui-next/src/features/flows/traces/effect-summary.ts +24 -0
  105. package/src/console/ui-next/src/features/flows/traces/http-method.ts +19 -0
  106. package/src/console/ui-next/src/features/flows/traces/trace-detail-sheet.tsx +9 -3
  107. package/src/console/ui-next/src/features/flows/traces/trace-detail.test.ts +29 -2
  108. package/src/console/ui-next/src/features/flows/traces/traces-pane.tsx +10 -3
  109. package/src/console/ui-next/src/features/observability/lib/ask-count.test.ts +25 -0
  110. package/src/console/ui-next/src/features/observability/lib/ask-count.ts +4 -1
  111. package/src/console/ui-next/src/features/store/detail/browse-section.tsx +45 -1
  112. package/src/console/ui-next/src/features/store/detail/resource-panel.tsx +78 -16
  113. package/src/console/ui-next/src/features/store/detail/store-row-detail-sheet.tsx +155 -14
  114. package/src/console/ui-next/src/features/store/explorer/store-tree.tsx +33 -133
  115. package/src/console/ui-next/src/features/store/files/file-preview.tsx +4 -1
  116. package/src/console/ui-next/src/features/store/grid/rls-policy-sheet.tsx +326 -165
  117. package/src/console/ui-next/src/features/store/grid/sql-insert-sheet.tsx +6 -0
  118. package/src/console/ui-next/src/features/store/grid/store-data-grid.tsx +50 -3
  119. package/src/console/ui-next/src/features/store/lib/files-meta.test.ts +3 -0
  120. package/src/console/ui-next/src/features/store/lib/files-meta.ts +12 -0
  121. package/src/console/ui-next/src/features/store/lib/query-defaults.test.ts +25 -0
  122. package/src/console/ui-next/src/features/store/lib/query-defaults.ts +85 -3
  123. package/src/console/ui-next/src/features/store/lib/query-gate.ts +30 -4
  124. package/src/console/ui-next/src/features/store/lib/rls-policy.test.ts +139 -0
  125. package/src/console/ui-next/src/features/store/lib/rls-policy.ts +223 -21
  126. package/src/console/ui-next/src/features/store/lib/schema-graph.ts +4 -0
  127. package/src/console/ui-next/src/features/store/lib/sql-catalog.test.ts +24 -0
  128. package/src/console/ui-next/src/features/store/lib/sql-catalog.ts +19 -0
  129. package/src/console/ui-next/src/features/store/lib/store-tree.test.ts +6 -0
  130. package/src/console/ui-next/src/features/store/lib/store-tree.ts +13 -0
  131. package/src/console/ui-next/src/features/store/query/query-console.tsx +48 -14
  132. package/src/console/ui-next/src/features/store/query/query-gate-menu.tsx +19 -20
  133. package/src/console/ui-next/src/features/store/query/query-gate-parts.tsx +2 -1
  134. package/src/console/ui-next/src/features/store/query/query-results.tsx +110 -83
  135. package/src/console/ui-next/src/features/store/schema/schema-table-node.tsx +17 -0
  136. package/src/console/ui-next/src/features/units/call/call-api-panel.tsx +112 -72
  137. package/src/console/ui-next/src/features/units/detail/effects-summary.tsx +12 -4
  138. package/src/console/ui-next/src/features/units/lib/contract-input.test.ts +206 -1
  139. package/src/console/ui-next/src/features/units/lib/contract-input.ts +55 -0
  140. package/src/console/ui-next/src/features/vault/lib/types.ts +1 -1
  141. package/src/docker/compose-health.test.ts +27 -0
  142. package/src/docker/compose-health.ts +19 -3
  143. package/src/docker/docker.test.ts +4 -1
  144. package/src/docker/dockerfile.ts +1 -1
  145. package/src/docker/recipes/pgdog.ts +2 -0
  146. package/src/drivers/ai-anthropic.ts +5 -0
  147. package/src/drivers/ai-ollama.ts +49 -30
  148. package/src/drivers/ai-openai-compatible.ts +57 -46
  149. package/src/drivers/ai-providers.test.ts +3 -0
  150. package/src/drivers/bun-native-completeness.test.ts +7 -9
  151. package/src/drivers/clock-postgres.ts +13 -9
  152. package/src/drivers/instances-postgres.ts +4 -6
  153. package/src/drivers/journal-postgres.ts +13 -9
  154. package/src/drivers/pg-rls.test.ts +87 -0
  155. package/src/drivers/pg-rls.ts +233 -7
  156. package/src/drivers/pglite.ts +26 -3
  157. package/src/drivers/postgres.test.ts +23 -0
  158. package/src/drivers/postgres.ts +133 -9
  159. package/src/drivers/redis.ts +11 -4
  160. package/src/drivers/signal-redis.ts +24 -14
  161. package/src/drivers/signal-types.ts +2 -1
  162. package/src/drivers/types.ts +10 -0
  163. package/src/elements/ai/declare.ts +109 -0
  164. package/src/elements/ai/errors.test.ts +5 -1
  165. package/src/elements/ai/errors.ts +30 -2
  166. package/src/elements/ai/eval.ts +4 -6
  167. package/src/elements/ai/mcp-client.test.ts +206 -0
  168. package/src/elements/ai/mcp-client.ts +362 -0
  169. package/src/elements/ai/mcp-http.ts +159 -0
  170. package/src/elements/ai/mcp-mock.ts +134 -0
  171. package/src/elements/ai/mcp-protocol.ts +234 -0
  172. package/src/elements/ai/mcp-stdio.test.ts +50 -0
  173. package/src/elements/ai/mcp-stdio.ts +212 -0
  174. package/src/elements/ai/mcp-transport.ts +70 -0
  175. package/src/elements/ai/runtime.ts +159 -29
  176. package/src/elements/ai.test.ts +139 -0
  177. package/src/elements/ai.ts +16 -0
  178. package/src/elements/clock/declare.ts +26 -1
  179. package/src/elements/clock/health.test.ts +43 -0
  180. package/src/elements/clock/runtime.ts +55 -2
  181. package/src/elements/clock/schedule.ts +71 -142
  182. package/src/elements/clock.test.ts +9 -40
  183. package/src/elements/clock.ts +6 -1
  184. package/src/elements/gate/boot.ts +5 -5
  185. package/src/elements/gate/declare.ts +36 -4
  186. package/src/elements/gate/flatten.ts +1 -1
  187. package/src/elements/gate/runtime.ts +1 -1
  188. package/src/elements/index.ts +2 -0
  189. package/src/elements/signal/declare.ts +2 -1
  190. package/src/elements/signal/runtime.ts +16 -1
  191. package/src/elements/signal.ts +1 -0
  192. package/src/elements/store/cache.test.ts +2 -0
  193. package/src/elements/store/cache.ts +3 -3
  194. package/src/elements/store/declare.ts +9 -6
  195. package/src/elements/store/emit-drizzle.ts +44 -8
  196. package/src/elements/store/resource.test.ts +1 -1
  197. package/src/elements/store/rls-identity.test.ts +95 -0
  198. package/src/elements/store/rls-identity.ts +116 -0
  199. package/src/elements/store/runtime.ts +4 -0
  200. package/src/elements/store/schema-decl.test.ts +37 -0
  201. package/src/elements/store/schema-decl.ts +161 -0
  202. package/src/elements/store/seed.ts +1 -1
  203. package/src/elements/store/sql-condition.test.ts +12 -0
  204. package/src/elements/store/sql-condition.ts +26 -2
  205. package/src/elements/store/sql-rls-isolation.test.ts +165 -0
  206. package/src/elements/store/sql-rls-stamp.test.ts +117 -0
  207. package/src/elements/store/sql-session.ts +81 -3
  208. package/src/elements/store/upsert-app.test.ts +1 -1
  209. package/src/elements/store.ts +14 -0
  210. package/src/elements/vault/builtin-adapter.ts +17 -0
  211. package/src/full.ts +2 -0
  212. package/src/http.ts +20 -3
  213. package/src/index.ts +14 -1
  214. package/src/kernel/adopt-barrel-fresh.test.ts +1 -1
  215. package/src/kernel/adopt-routes.ts +30 -2
  216. package/src/kernel/app.ts +227 -77
  217. package/src/kernel/auto-cache.test.ts +8 -8
  218. package/src/kernel/auto-registry.test.ts +54 -9
  219. package/src/kernel/boot-bind/ai.ts +24 -0
  220. package/src/kernel/boot-bind/clock.ts +2 -0
  221. package/src/kernel/boot.test.ts +4 -4
  222. package/src/kernel/boot.ts +5 -3
  223. package/src/kernel/effects-stamping.test.ts +3 -3
  224. package/src/kernel/element-registries.ts +15 -5
  225. package/src/kernel/errors.ts +24 -0
  226. package/src/kernel/flow-units.ts +64 -0
  227. package/src/kernel/flow.ts +29 -17
  228. package/src/kernel/fx-dead-letters.test.ts +77 -0
  229. package/src/kernel/fx.test.ts +22 -0
  230. package/src/kernel/fx.ts +135 -6
  231. package/src/kernel/horizontal-child.ts +4 -4
  232. package/src/kernel/http-path-pending.ts +20 -0
  233. package/src/kernel/http-resource.ts +4 -0
  234. package/src/kernel/http-stream.test.ts +174 -0
  235. package/src/kernel/index.ts +2 -0
  236. package/src/kernel/pipeline.test.ts +2 -2
  237. package/src/kernel/plugin-elements.test.ts +1 -1
  238. package/src/kernel/plugin-needs.test.ts +1 -1
  239. package/src/kernel/ready.test.ts +2 -2
  240. package/src/kernel/resource-mount.test.ts +14 -0
  241. package/src/kernel/run-duration.test.ts +2 -2
  242. package/src/kernel/stamp-http.test.ts +79 -0
  243. package/src/kernel/stamp-http.ts +75 -0
  244. package/src/kernel/triggers.ts +52 -27
  245. package/src/manifest/mcp-ref.ts +88 -0
  246. package/src/manifest/types.ts +41 -2
  247. package/src/manifest/validate.test.ts +20 -0
  248. package/src/mcp/docs-server.ts +1 -1
  249. package/src/mcp/server.ts +1 -1
  250. package/src/plugins/compression.test.ts +21 -0
  251. package/src/plugins/compression.ts +1 -0
  252. package/src/release/measure.ts +2 -2
  253. package/src/runtime/bun.ts +41 -4
  254. package/src/runtime/json-code-block.test.ts +2 -2
  255. package/src/runtime/serve.test.ts +4 -4
  256. package/src/term.test.ts +52 -4
  257. package/src/term.ts +125 -17
  258. package/src/test/reset-element-registries.ts +10 -3
  259. package/src/cli/ask-seed.test.ts +0 -96
  260. package/src/cli/ask-seed.ts +0 -82
  261. package/src/console/ui-next/dist/assets/cache-glyph-CLPBqZeb.js +0 -1
  262. package/src/console/ui-next/dist/assets/collapsible-DYb0xU8C.js +0 -1
  263. package/src/console/ui-next/dist/assets/flows-page-Dg8CTE29.js +0 -1
  264. package/src/console/ui-next/dist/assets/http-method-BJ92Z_ke.js +0 -1
  265. package/src/console/ui-next/dist/assets/index-Bp-R7jtM.js +0 -66
  266. package/src/console/ui-next/dist/assets/index-D4Ldtj79.css +0 -2
  267. package/src/console/ui-next/dist/assets/observability-page-DAnpEaq1.js +0 -4
  268. package/src/console/ui-next/dist/assets/query-gate-parts-1m8m1iNp.js +0 -2
  269. package/src/console/ui-next/dist/assets/react-B1ML8gxg.js +0 -1
  270. package/src/console/ui-next/dist/assets/replica-lag-C8_BRt2x.js +0 -18
  271. package/src/console/ui-next/dist/assets/sheet-form-D-ata7jy.js +0 -1
  272. package/src/console/ui-next/dist/assets/shortcut-keys-DO4IsVqv.js +0 -1
  273. package/src/console/ui-next/dist/assets/skeleton-CL_X0GCj.js +0 -1
  274. package/src/console/ui-next/dist/assets/store-page-v3LXdYpr.js +0 -45
  275. package/src/console/ui-next/dist/assets/trace-detail-sheet-D16lWQMt.js +0 -2
  276. package/src/console/ui-next/dist/assets/units-page-BtQ0bqMe.js +0 -1
  277. package/src/console/ui-next/dist/assets/vault-page-Ca-MvcmJ.js +0 -2
@@ -105,22 +105,24 @@ describe("extractManifest — fx.raw", () => {
105
105
  });
106
106
 
107
107
  describe("extractManifest — performance", () => {
108
- test("200-flow synthetic app extracts in under 2s", async () => {
109
- const sources: Record<string, string> = {};
110
- const parts: string[] = [
111
- `import { on, flow, http, store } from "okengine";`,
112
- `export const db = store.sql("db");`,
113
- `export const items = { name: "items" };`,
114
- `export const app = { name: "synth" };`,
115
- ];
116
- // Declare oke name via a tiny app file pattern
117
- sources["src/app.ts"] = `
108
+ test(
109
+ "200-flow synthetic app extracts in under 2s",
110
+ async () => {
111
+ const sources: Record<string, string> = {};
112
+ const parts: string[] = [
113
+ `import { on, flow, http, store } from "okengine";`,
114
+ `export const db = store.sql("db");`,
115
+ `export const items = { name: "items" };`,
116
+ `export const app = { name: "synth" };`,
117
+ ];
118
+ // Declare oke name via a tiny app file pattern
119
+ sources["src/app.ts"] = `
118
120
  import { oke } from "okengine";
119
121
  export const app = oke({ name: "synth200" });
120
122
  `;
121
123
 
122
- for (let i = 0; i < 200; i++) {
123
- parts.push(`
124
+ for (let i = 0; i < 200; i++) {
125
+ parts.push(`
124
126
  export const flow_${i} = on(
125
127
  http.get("/f/${i}"),
126
128
  flow("synth.flow_${i}", {
@@ -132,16 +134,22 @@ export const flow_${i} = on(
132
134
  }),
133
135
  );
134
136
  `);
135
- }
136
- sources["src/flows/all.ts"] = parts.join("\n");
137
-
138
- const start = performance.now();
139
- const manifest = await extractFromSources(sources);
140
- const elapsed = performance.now() - start;
141
-
142
- expect(Object.keys(manifest.flows ?? {}).length).toBe(200);
143
- expect(elapsed).toBeLessThan(2000);
144
- });
137
+ }
138
+ sources["src/flows/all.ts"] = parts.join("\n");
139
+
140
+ await extractFromSources(sources);
141
+ const start = performance.now();
142
+ const manifest = await extractFromSources(sources);
143
+ const elapsed = performance.now() - start;
144
+
145
+ expect(Object.keys(manifest.flows ?? {}).length).toBe(200);
146
+ // Local ~400ms after warmup. GHA shared runners measured ~2.9s cold.
147
+ // Warmup + extract exceeds Bun's 5s default (v0.15.0 tag timed out at 5.6s).
148
+ const budgetMs = process.env.CI ? 4_000 : 2_000;
149
+ expect(elapsed).toBeLessThan(budgetMs);
150
+ },
151
+ { timeout: 15_000 },
152
+ );
145
153
  });
146
154
 
147
155
  describe("serializeManifest stability", () => {
@@ -238,6 +246,32 @@ export const db = store.sql("app", { schema: { notes } });
238
246
  });
239
247
  });
240
248
 
249
+ test("extracts RLS helpers onto store.tables", async () => {
250
+ const source = `
251
+ import { store, field } from "okengine";
252
+
253
+ export const bookings = store.schema.table("bookings", {
254
+ id: field.text().primaryKey(),
255
+ owner: field.text().notNull(),
256
+ }, [
257
+ store.schema.policy.gate("member", { for: "select" }),
258
+ store.schema.policy.owner("owner"),
259
+ ]);
260
+
261
+ export const db = store.sql("app", { schema: { bookings } });
262
+ `;
263
+ const manifest = await extractFromSources({
264
+ "src/schema.decl.ts": source,
265
+ });
266
+ const table = manifest.stores?.app?.tables?.bookings;
267
+ expect(table?.rls).toBe(true);
268
+ expect(table?.policies?.gate_member_select).toMatchObject({
269
+ for: "select",
270
+ using: "oke.gate() = 'member'",
271
+ });
272
+ expect(table?.policies?.owner_owner_all?.using).toContain("oke.user()");
273
+ });
274
+
241
275
  test("extracts optional description fields additively", async () => {
242
276
  const source = `
243
277
  import { store, field, signal, channel, clock, gate, vault } from "okengine";
@@ -465,6 +499,27 @@ export const remove = mounted.remove;
465
499
  expect(manifest.flows?.update?.live).toBeUndefined();
466
500
  expect(manifest.flows?.remove?.live).toBeUndefined();
467
501
  });
502
+
503
+ test("chains .public().live() onto the five verbs", async () => {
504
+ const source = `
505
+ import { on, http, store } from "okengine";
506
+
507
+ export const db = store.sql("notes", { schema: {} });
508
+ const notesR = store.resource(db, {}, {});
509
+ const mounted = on(http.resource("/notes", notesR.all()).public().live());
510
+
511
+ export const list = mounted.list;
512
+ export const create = mounted.create;
513
+ export const get = mounted.get;
514
+ export const update = mounted.update;
515
+ export const remove = mounted.remove;
516
+ `;
517
+ const manifest = await extractFromSources({ "src/flows/notes.ts": source });
518
+ expect(manifest.flows?.list?.gates).toEqual(["public"]);
519
+ expect(manifest.flows?.create?.gates).toEqual(["public"]);
520
+ expect(manifest.flows?.get?.live).toBe(true);
521
+ expect(manifest.flows?.create?.live).toBeUndefined();
522
+ });
468
523
  });
469
524
 
470
525
  describe("extractManifest — vault.config", () => {
@@ -509,7 +564,7 @@ import { on, flow, http, gate, store } from "okengine";
509
564
  export const files = store.files("uploads");
510
565
 
511
566
  export const attach = on(
512
- http.post("/attach").gate.public,
567
+ http.post("/attach").public(),
513
568
  flow("notes.attach", {
514
569
  do: async (input, fx) => {
515
570
  await fx.store(files).put("key", input.text);
@@ -530,7 +585,7 @@ import { on, flow, http, gate, store } from "okengine";
530
585
  export const files = store.files("uploads");
531
586
 
532
587
  export const attach = on(
533
- http.post("/attach").gate.public,
588
+ http.post("/attach").public(),
534
589
  flow("notes.attachImage", {
535
590
  do: async (input, fx) => {
536
591
  await fx.store(files).putImage("key", input.bytes);
@@ -598,6 +653,25 @@ export const expire = on(
598
653
  });
599
654
  });
600
655
 
656
+ describe("extractManifest — fx.deadLetters", () => {
657
+ test("fx.deadLetters(signal) infers reads: [signal:<name>]", async () => {
658
+ const source = `
659
+ import { on, flow, http, signal } from "okengine";
660
+
661
+ export const notify = signal("notify", { delivery: "once" });
662
+
663
+ export const failed = on(
664
+ http.get("/notifications/failed").public(),
665
+ flow("notifications.failed", {
666
+ do: async (input, fx) => fx.json.withQuery(await fx.deadLetters(notify), input),
667
+ }),
668
+ );
669
+ `;
670
+ const manifest = await extractFromSources({ "src/flows/failed.ts": source });
671
+ expect(manifest.flows?.["notifications.failed"]?.effects?.reads).toEqual(["signal:notify"]);
672
+ });
673
+ });
674
+
601
675
  describe("extractManifest — channel medium binder aliasing", () => {
602
676
  test("mail.template(...) resolves through `const mail = channel.email(...)`", async () => {
603
677
  const source = `
@@ -610,7 +684,7 @@ export const noteCreatedMail = mail.template("note-created", {
610
684
  });
611
685
 
612
686
  export const onCreated = on(
613
- http.post("/hook").gate.public,
687
+ http.post("/hook").public(),
614
688
  flow("notes.onCreated", {
615
689
  do: async (payload, fx) => {
616
690
  await fx.send(noteCreatedMail, { to: "you@localhost", data: payload });
@@ -633,7 +707,7 @@ const otp = channel.sms({});
633
707
  export const otpTemplate = otp.template("otp-code", {});
634
708
 
635
709
  export const send = on(
636
- http.post("/otp").gate.public,
710
+ http.post("/otp").public(),
637
711
  flow("notes.sendOtp", {
638
712
  do: async (payload, fx) => {
639
713
  await fx.send(otpTemplate, { to: "+10000000000", data: payload });
@@ -716,19 +790,32 @@ export const decoy = flow("checkout.realName", {
716
790
  });
717
791
  });
718
792
 
719
- describe("extractManifest — .gate.public", () => {
720
- test("reads the public sentinel from a member chain", async () => {
793
+ describe("extractManifest — .public()", () => {
794
+ test("reads the public sentinel from .public()", async () => {
721
795
  const source = `
722
796
  import { on, flow, http } from "okengine";
723
797
 
724
798
  export const health = on(
725
- http.get("/health").gate.public,
799
+ http.get("/health").public(),
726
800
  flow("health.check", { do: () => ({ ok: true }) }),
727
801
  );
728
802
  `;
729
803
  const manifest = await extractFromSources({ "src/flows/health.ts": source });
730
804
  expect(manifest.flows?.["health.check"]?.gates).toEqual(["public"]);
731
805
  });
806
+
807
+ test("does not treat the removed .gate.public property as attach", async () => {
808
+ const source = `
809
+ import { on, flow, http } from "okengine";
810
+
811
+ export const health = on(
812
+ http.get("/health").gate.public,
813
+ flow("health.check", { do: () => ({ ok: true }) }),
814
+ );
815
+ `;
816
+ const manifest = await extractFromSources({ "src/flows/health.ts": source });
817
+ expect(manifest.flows?.["health.check"]?.gates).toBeUndefined();
818
+ });
732
819
  });
733
820
 
734
821
  describe("extractManifest — gate.all", () => {
@@ -776,3 +863,345 @@ export const create = on(
776
863
  expect(manifest.gates?.WRITE).toBeUndefined();
777
864
  });
778
865
  });
866
+
867
+ describe("extractManifest — ai.mcpServer", () => {
868
+ test("stamps mcpServers and infers mcp: calls from .tool()", async () => {
869
+ const source = `
870
+ import { ai, flow, on, http, vault } from "okengine";
871
+
872
+ export const token = vault.secret("GITHUB_TOKEN");
873
+ export const github = ai.mcpServer("github", {
874
+ url: "https://mcp.example/github",
875
+ auth: { bearer: token },
876
+ tools: ["create_issue"],
877
+ });
878
+
879
+ export const triage = on(
880
+ http.post("/triage"),
881
+ flow("support.triage", {
882
+ do: async (input, fx) => {
883
+ await fx.ask("ticket-triage", input, { tools: [github.tool("create_issue")] });
884
+ },
885
+ }),
886
+ );
887
+ `;
888
+ const manifest = await extractFromSources({ "src/ai.ts": source });
889
+ expect(manifest.ai?.mcpServers?.github).toEqual({
890
+ url: "https://mcp.example/github",
891
+ auth: "GITHUB_TOKEN",
892
+ tools: ["create_issue"],
893
+ });
894
+ expect(manifest.flows?.["support.triage"]?.effects?.calls).toEqual(["mcp:github/create_issue"]);
895
+ });
896
+ });
897
+
898
+ describe("extractManifest — file-tree stamps", () => {
899
+ test("pathless http.get() in notes/[id]/get.ts infers /notes/:id and notes.get", async () => {
900
+ const source = `
901
+ import { on, flow, http } from "okengine";
902
+ export const get = on(
903
+ http.get().public(),
904
+ flow({ do: (input) => input }),
905
+ );
906
+ `;
907
+ const manifest = await extractFromSources({ "src/flows/notes/[id]/get.ts": source });
908
+ expect(manifest.flows?.["notes.get"]?.trigger).toEqual({
909
+ http: { method: "GET", path: "/notes/:id" },
910
+ });
911
+ });
912
+
913
+ test("nameless flow({ do }) still appears on the Manifest", async () => {
914
+ const source = `
915
+ import { flow } from "okengine";
916
+ export const ping = flow({ do: () => ({ ok: true }) });
917
+ `;
918
+ const manifest = await extractFromSources({ "src/flows/notes/ping.ts": source });
919
+ expect(manifest.flows?.["notes.ping"]).toBeDefined();
920
+ });
921
+
922
+ test("effects are identical for the same do body in a tree file vs a barrel", async () => {
923
+ const doBody = `
924
+ const rows = await fx.store(db).select().from(notes);
925
+ await fx.emit(noteCreated, { id: "1" });
926
+ await fx.call("notes.ping");
927
+ return rows;
928
+ `;
929
+ const barrel = `
930
+ import { on, flow, http, store, signal } from "okengine";
931
+ export const db = store.sql("db");
932
+ export const notes = { name: "notes" };
933
+ export const noteCreated = signal("note-created");
934
+ export const get = on(
935
+ http.get("/notes/:id").public(),
936
+ flow("notes.get", {
937
+ do: async (_input, fx) => {${doBody}},
938
+ }),
939
+ );
940
+ `;
941
+ const tree = `
942
+ import { on, flow, http, store, signal } from "okengine";
943
+ export const db = store.sql("db");
944
+ export const notes = { name: "notes" };
945
+ export const noteCreated = signal("note-created");
946
+ export const get = on(
947
+ http.get().public(),
948
+ flow({
949
+ do: async (_input, fx) => {${doBody}},
950
+ }),
951
+ );
952
+ `;
953
+ const barrelManifest = await extractFromSources({ "src/flows/notes/index.ts": barrel });
954
+ const treeManifest = await extractFromSources({ "src/flows/notes/[id]/get.ts": tree });
955
+ const barrelFx = barrelManifest.flows?.["notes.get"]?.effects;
956
+ const treeFx = treeManifest.flows?.["notes.get"]?.effects;
957
+ expect(treeFx).toEqual(barrelFx);
958
+ expect(treeFx?.reads).toBeDefined();
959
+ expect(treeFx?.emits).toBeDefined();
960
+ expect(treeFx?.calls).toBeDefined();
961
+ expect(treeFx?.reads?.length).toBeGreaterThan(0);
962
+ expect(treeFx?.emits?.length).toBeGreaterThan(0);
963
+ expect(treeFx?.calls?.length).toBeGreaterThan(0);
964
+ });
965
+ });
966
+
967
+ describe("extractManifest — flow in/out schema expansion", () => {
968
+ test("expands z.object identifiers into JSON Schema fields", async () => {
969
+ const source = `
970
+ import { flow } from "okengine";
971
+ import { z } from "zod";
972
+
973
+ export const TaskCreateIn = z.object({
974
+ title: z.string().min(1).max(500),
975
+ spaceKey: z.string().min(1),
976
+ priority: z.number().int().min(0).max(4).optional(),
977
+ });
978
+
979
+ export const TaskCreateOut = z.object({
980
+ id: z.string(),
981
+ identifier: z.string(),
982
+ });
983
+
984
+ export const create = flow("tasks.create", {
985
+ in: TaskCreateIn,
986
+ out: TaskCreateOut,
987
+ do: async () => ({ id: "1", identifier: "ENG-1" }),
988
+ });
989
+ `;
990
+ const manifest = await extractFromSources({ "src/flows/tasks/shapes.ts": source });
991
+ expect(manifest.flows?.["tasks.create"]?.in).toEqual({
992
+ type: "object",
993
+ properties: {
994
+ title: { type: "string", minLength: 1, maxLength: 500 },
995
+ spaceKey: { type: "string", minLength: 1 },
996
+ priority: { type: "integer", minimum: 0, maximum: 4 },
997
+ },
998
+ required: ["title", "spaceKey"],
999
+ });
1000
+ expect(manifest.flows?.["tasks.create"]?.out).toEqual({
1001
+ type: "object",
1002
+ properties: {
1003
+ id: { type: "string" },
1004
+ identifier: { type: "string" },
1005
+ },
1006
+ required: ["id", "identifier"],
1007
+ });
1008
+ });
1009
+
1010
+ test("expands drizzle-orm/zod createSelect/Insert/UpdateSchema from table columns", async () => {
1011
+ const schema = `
1012
+ import { store, field, id, now } from "okengine";
1013
+
1014
+ export const users = store.schema.table("users", {
1015
+ id: field.text().primaryKey().defaultFn(id),
1016
+ name: field.text().notNull(),
1017
+ age: field.integer().notNull(),
1018
+ bio: field.text(),
1019
+ createdAt: field.integer().notNull().defaultFn(now),
1020
+ });
1021
+ `;
1022
+ const flows = `
1023
+ import { flow } from "okengine";
1024
+ import { createInsertSchema, createSelectSchema, createUpdateSchema } from "drizzle-orm/zod";
1025
+ import { users } from "../schema.decl.ts";
1026
+
1027
+ export const userSelect = createSelectSchema(users);
1028
+ export const userInsert = createInsertSchema(users);
1029
+ export const userUpdate = createUpdateSchema(users);
1030
+
1031
+ export const create = flow("users.create", {
1032
+ in: userInsert,
1033
+ out: userSelect,
1034
+ do: async () => ({}),
1035
+ });
1036
+
1037
+ export const patch = flow("users.patch", {
1038
+ in: userUpdate,
1039
+ do: async () => ({}),
1040
+ });
1041
+ `;
1042
+ const manifest = await extractFromSources({
1043
+ "src/schema.decl.ts": schema,
1044
+ "src/flows/users.ts": flows,
1045
+ });
1046
+ const insert = manifest.flows?.["users.create"]?.in as {
1047
+ properties?: Record<string, unknown>;
1048
+ required?: string[];
1049
+ };
1050
+ expect(insert?.properties).toMatchObject({
1051
+ name: { type: "string" },
1052
+ age: { type: "integer" },
1053
+ bio: { type: "string" },
1054
+ createdAt: { type: "integer" },
1055
+ });
1056
+ expect(insert?.properties?.id).toBeUndefined();
1057
+ expect(insert?.required).toEqual(["name", "age"]);
1058
+
1059
+ const select = manifest.flows?.["users.create"]?.out as {
1060
+ properties?: Record<string, unknown>;
1061
+ required?: string[];
1062
+ };
1063
+ expect(select?.properties?.id).toMatchObject({ type: "string", primaryKey: true });
1064
+ expect(select?.required).toEqual(expect.arrayContaining(["id", "name", "age", "createdAt"]));
1065
+ expect(select?.required).not.toContain("bio");
1066
+
1067
+ const update = manifest.flows?.["users.patch"]?.in as {
1068
+ properties?: Record<string, unknown>;
1069
+ required?: string[];
1070
+ };
1071
+ expect(update?.properties?.name).toMatchObject({ type: "string" });
1072
+ expect(update?.properties?.id).toBeUndefined();
1073
+ expect(update?.required).toBeUndefined();
1074
+ });
1075
+
1076
+ test("expands tableZod(table).select / .insert and .pick()", async () => {
1077
+ const schema = `
1078
+ import { store, field, id } from "okengine";
1079
+ export const spaces = store.schema.table("spaces", {
1080
+ id: field.text().primaryKey().defaultFn(id),
1081
+ key: field.text().notNull().unique(),
1082
+ name: field.text().notNull(),
1083
+ color: field.text(),
1084
+ });
1085
+ `;
1086
+ const zod = `
1087
+ import { createInsertSchema, createSelectSchema, createUpdateSchema } from "drizzle-orm/zod";
1088
+ export function tableZod(table) {
1089
+ return {
1090
+ select: createSelectSchema(table),
1091
+ insert: createInsertSchema(table),
1092
+ update: createUpdateSchema(table),
1093
+ };
1094
+ }
1095
+ export const spacesZod = tableZod(spaces);
1096
+ `;
1097
+ const flows = `
1098
+ import { flow } from "okengine";
1099
+ export const create = flow("spaces.create", {
1100
+ in: spacesZod.insert,
1101
+ out: spacesZod.select.pick({ id: true, key: true }),
1102
+ do: async () => ({}),
1103
+ });
1104
+ `;
1105
+ const manifest = await extractFromSources({
1106
+ "src/schema.decl.ts": schema,
1107
+ "src/db/zod.ts": zod,
1108
+ "src/flows/spaces.ts": flows,
1109
+ });
1110
+ expect(manifest.flows?.["spaces.create"]?.in).toMatchObject({
1111
+ type: "object",
1112
+ properties: {
1113
+ key: { type: "string", unique: true },
1114
+ name: { type: "string" },
1115
+ color: { type: "string" },
1116
+ },
1117
+ required: ["key", "name"],
1118
+ });
1119
+ expect(manifest.flows?.["spaces.create"]?.out).toEqual({
1120
+ type: "object",
1121
+ properties: {
1122
+ id: { type: "string", primaryKey: true },
1123
+ key: { type: "string", unique: true },
1124
+ },
1125
+ required: ["id", "key"],
1126
+ });
1127
+ });
1128
+
1129
+ test("bindCrud stamps createIn / out onto CRUD flows", async () => {
1130
+ const source = `
1131
+ import { store, field, id } from "okengine";
1132
+ import { z } from "zod";
1133
+
1134
+ export const notes = store.schema.table("notes", {
1135
+ id: field.text().primaryKey().defaultFn(id),
1136
+ title: field.text().notNull(),
1137
+ });
1138
+
1139
+ const createIn = z.object({
1140
+ title: z.string().min(1),
1141
+ });
1142
+
1143
+ export const { list, create, get, update, remove } = bindCrud({
1144
+ unit: "notes",
1145
+ path: "/notes",
1146
+ table: notes,
1147
+ createIn,
1148
+ out: createSelectSchema(notes),
1149
+ });
1150
+ `;
1151
+ const manifest = await extractFromSources({ "src/flows/notes/index.ts": source });
1152
+ expect(manifest.flows?.["notes.create"]?.in).toEqual({
1153
+ type: "object",
1154
+ properties: { title: { type: "string", minLength: 1 } },
1155
+ required: ["title"],
1156
+ });
1157
+ expect(manifest.flows?.["notes.get"]?.out).toMatchObject({
1158
+ type: "object",
1159
+ properties: {
1160
+ id: { type: "string", primaryKey: true },
1161
+ title: { type: "string" },
1162
+ },
1163
+ });
1164
+ expect(manifest.flows?.["notes.list"]?.in).toMatchObject({
1165
+ type: "object",
1166
+ properties: { q: { type: "string" }, limit: { type: "integer" } },
1167
+ });
1168
+ expect((manifest.flows?.["notes.update"]?.in as { required?: string[] }).required).toContain(
1169
+ "id",
1170
+ );
1171
+ });
1172
+
1173
+ test("same-named createIn stays file-local (does not leak across units)", async () => {
1174
+ const spaces = `
1175
+ import { flow } from "okengine";
1176
+ import { z } from "zod";
1177
+ const createIn = z.object({ key: z.string(), name: z.string() });
1178
+ export const create = flow("spaces.create", {
1179
+ in: createIn,
1180
+ do: async () => ({}),
1181
+ });
1182
+ `;
1183
+ const views = `
1184
+ import { flow } from "okengine";
1185
+ import { z } from "zod";
1186
+ const createIn = z.object({ kind: z.enum(["list", "board"]) });
1187
+ export const create = flow("views.create", {
1188
+ in: createIn,
1189
+ do: async () => ({}),
1190
+ });
1191
+ `;
1192
+ const manifest = await extractFromSources({
1193
+ "src/flows/spaces/index.ts": spaces,
1194
+ "src/flows/views/index.ts": views,
1195
+ });
1196
+ expect(manifest.flows?.["spaces.create"]?.in).toEqual({
1197
+ type: "object",
1198
+ properties: { key: { type: "string" }, name: { type: "string" } },
1199
+ required: ["key", "name"],
1200
+ });
1201
+ expect(manifest.flows?.["views.create"]?.in).toEqual({
1202
+ type: "object",
1203
+ properties: { kind: { type: "string", enum: ["list", "board"] } },
1204
+ required: ["kind"],
1205
+ });
1206
+ });
1207
+ });