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
@@ -86,6 +86,18 @@ describe("compileWhere", () => {
86
86
  expect(c.params).toEqual(["n1", "t"]);
87
87
  });
88
88
 
89
+ test("legacy array-valued StringChunk fragments still compile", () => {
90
+ const c = compileWhere({
91
+ queryChunks: [
92
+ notes.id,
93
+ { constructor: { name: "StringChunk" }, value: [" = "] },
94
+ { value: "n1" },
95
+ ],
96
+ });
97
+ expect(c.clause).toBe(`"id" = ?`);
98
+ expect(c.params).toEqual(["n1"]);
99
+ });
100
+
89
101
  test("inArray expands one placeholder per value", () => {
90
102
  const c = compileWhere(inArray(notes.id, ["a", "b", "c"]));
91
103
  expect(c.clause).toBe(`"id" in (?, ?, ?)`);
@@ -333,10 +333,33 @@ function chunksOf(node: unknown): readonly unknown[] {
333
333
  return (node as { queryChunks: unknown[] }).queryChunks;
334
334
  }
335
335
 
336
- /** Joined text of a Drizzle `StringChunk` (`{ value: string[] }`). */
336
+ /**
337
+ * True when `chunk` is a Drizzle `StringChunk` (or the synthetic array-valued
338
+ * shape `resource.ts` still emits). `Param` also has a `value` field — never
339
+ * treat those as SQL text.
340
+ */
341
+ function isStringChunk(chunk: object): boolean {
342
+ const ctor = (chunk as { constructor?: { name?: unknown } }).constructor?.name;
343
+ if (ctor === "StringChunk") return true;
344
+ const value = (chunk as { value?: unknown }).value;
345
+ return (
346
+ Array.isArray(value) &&
347
+ value.every((part) => typeof part === "string") &&
348
+ !("encoder" in chunk) &&
349
+ !("name" in chunk)
350
+ );
351
+ }
352
+
353
+ /**
354
+ * Joined text of a Drizzle `StringChunk`.
355
+ *
356
+ * drizzle-orm `1.0.0-rc.5-169397b` stores `value` as a `string`. Earlier
357
+ * snapshots (and the synthetic chunks in `resource.ts`) store `string[]`.
358
+ */
337
359
  function chunkText(chunk: unknown): string | undefined {
338
- if (!chunk || typeof chunk !== "object") return undefined;
360
+ if (!chunk || typeof chunk !== "object" || !isStringChunk(chunk)) return undefined;
339
361
  const value = (chunk as { value?: unknown }).value;
362
+ if (typeof value === "string") return value;
340
363
  return Array.isArray(value) ? value.join("") : undefined;
341
364
  }
342
365
 
@@ -394,6 +417,7 @@ function asParamValue(chunk: unknown): { readonly found: boolean; readonly value
394
417
  if (chunk === undefined) return { found: false };
395
418
  if (chunk === null || typeof chunk !== "object") return { found: true, value: chunk };
396
419
  if (Array.isArray(chunk)) return { found: false };
420
+ if (isStringChunk(chunk)) return { found: false };
397
421
  if (!("value" in (chunk as object))) return { found: false };
398
422
  if (Array.isArray((chunk as { value: unknown }).value)) return { found: false };
399
423
  if ((chunk as { name?: unknown }).name) return { found: false };
@@ -0,0 +1,165 @@
1
+ /**
2
+ * Real PGlite RLS isolation through {@link createSqlStoreHandle}.
3
+ *
4
+ * Covers the gap unit tests missed: a live policy, two concurrent identity
5
+ * bags on one shared connection, and row-level isolation. PGlite `query`
6
+ * rejects multi-command batches — the stamp is split statements pinned
7
+ * through `SqlConnection.transaction`.
8
+ */
9
+
10
+ import { afterAll, beforeAll, describe, expect, test } from "bun:test";
11
+ import { connectPglite } from "../../drivers/pglite.ts";
12
+ import { connectPostgres } from "../../drivers/postgres.ts";
13
+ import { installOkeRlsHelpers } from "../../drivers/pg-rls.ts";
14
+ import type { SqlConnection } from "../../drivers/types.ts";
15
+ import { createSqlStoreHandle, type SqlStoreHandle } from "./sql-session.ts";
16
+
17
+ const NOTES_DDL = `CREATE TABLE notes (
18
+ id text PRIMARY KEY,
19
+ owner text NOT NULL,
20
+ body text NOT NULL
21
+ )`;
22
+
23
+ const OWNER_POLICY = `CREATE POLICY owner_select ON notes
24
+ AS PERMISSIVE FOR SELECT TO public
25
+ USING (owner = oke.user())`;
26
+
27
+ /** File-scoped warmed PGlite. */
28
+ let conn: SqlConnection;
29
+ /** Stamped as alice. */
30
+ let asAlice: SqlStoreHandle;
31
+ /** Stamped as bob. */
32
+ let asBob: SqlStoreHandle;
33
+ /** No stamp — table-owner / superuser bypass. */
34
+ let asOwner: SqlStoreHandle;
35
+
36
+ beforeAll(async () => {
37
+ conn = await connectPglite({ url: "memory://sql-rls-isolation", role: "primary" });
38
+ await conn.exec(NOTES_DDL);
39
+ await conn.exec(`INSERT INTO notes VALUES ('a', 'alice', 'hi'), ('b', 'bob', 'yo')`);
40
+ await conn.exec(`ALTER TABLE notes ENABLE ROW LEVEL SECURITY`);
41
+ await installOkeRlsHelpers((sql) => conn.exec(sql));
42
+ await conn.exec(OWNER_POLICY);
43
+ asAlice = createSqlStoreHandle("sql:app", {
44
+ connection: conn,
45
+ classifications: new Map(),
46
+ routedRole: "primary",
47
+ domainDdl: "off",
48
+ rls: { gate: "member", userId: "alice", scopes: ["member"] },
49
+ });
50
+ asBob = createSqlStoreHandle("sql:app", {
51
+ connection: conn,
52
+ classifications: new Map(),
53
+ routedRole: "primary",
54
+ domainDdl: "off",
55
+ rls: { gate: "member", userId: "bob", scopes: ["member"] },
56
+ });
57
+ asOwner = createSqlStoreHandle("sql:app", {
58
+ connection: conn,
59
+ classifications: new Map(),
60
+ routedRole: "primary",
61
+ domainDdl: "off",
62
+ });
63
+ }, 15_000);
64
+
65
+ afterAll(async () => {
66
+ await conn.close();
67
+ });
68
+
69
+ describe("sql-session RLS isolation (pglite)", () => {
70
+ test("pglite query rejects a multi-command batch (the old prelude)", async () => {
71
+ await expect(
72
+ conn.query("BEGIN; SET LOCAL row_security = on; SELECT set_config('oke.user', $1, true)", [
73
+ "alice",
74
+ ]),
75
+ ).rejects.toThrow(/multiple commands/i);
76
+ });
77
+
78
+ test("sequential identities see only their rows", async () => {
79
+ const alice = await asAlice.raw(`SELECT id, owner FROM notes ORDER BY id`);
80
+ const bob = await asBob.raw(`SELECT id, owner FROM notes ORDER BY id`);
81
+ expect(alice).toEqual([{ id: "a", owner: "alice" }]);
82
+ expect(bob).toEqual([{ id: "b", owner: "bob" }]);
83
+ });
84
+
85
+ test("concurrent identities do not leak across the shared connection", async () => {
86
+ const [alice, bob, aliceAgain] = await Promise.all([
87
+ asAlice.raw(`SELECT id, owner FROM notes ORDER BY id`),
88
+ asBob.raw(`SELECT id, owner FROM notes ORDER BY id`),
89
+ asAlice.raw(`SELECT id, owner FROM notes ORDER BY id`),
90
+ ]);
91
+ expect(alice).toEqual([{ id: "a", owner: "alice" }]);
92
+ expect(bob).toEqual([{ id: "b", owner: "bob" }]);
93
+ expect(aliceAgain).toEqual([{ id: "a", owner: "alice" }]);
94
+ });
95
+
96
+ test("unstamped owner/superuser still sees every row", async () => {
97
+ const rows = await asOwner.raw(`SELECT id, owner FROM notes ORDER BY id`);
98
+ expect(rows).toEqual([
99
+ { id: "a", owner: "alice" },
100
+ { id: "b", owner: "bob" },
101
+ ]);
102
+ });
103
+ });
104
+
105
+ const LIVE_PG =
106
+ process.env.OKE_TEST_DOCKER === "1" &&
107
+ (process.env.DATABASE_URL ?? process.env.OKE_STORE_SQL_URL)?.startsWith("postgres");
108
+ if (!LIVE_PG) {
109
+ console.log("skip: live Postgres RLS isolation needs OKE_TEST_DOCKER=1 and DATABASE_URL");
110
+ }
111
+
112
+ describe("sql-session RLS isolation (live postgres)", () => {
113
+ const live = LIVE_PG ? test : test.skip;
114
+ const table = "oke_rls_isolation_notes";
115
+
116
+ live(
117
+ "split stamp isolates concurrent identities",
118
+ async () => {
119
+ const pg = await connectPostgres({
120
+ url: process.env.DATABASE_URL ?? process.env.OKE_STORE_SQL_URL,
121
+ role: "primary",
122
+ });
123
+ try {
124
+ await pg.exec(`DROP TABLE IF EXISTS ${table}`);
125
+ await pg.exec(
126
+ `CREATE TABLE ${table} (id text PRIMARY KEY, owner text NOT NULL, body text NOT NULL)`,
127
+ );
128
+ await pg.exec(`INSERT INTO ${table} VALUES ('a', 'alice', 'hi'), ('b', 'bob', 'yo')`);
129
+ await pg.exec(`ALTER TABLE ${table} ENABLE ROW LEVEL SECURITY`);
130
+ await installOkeRlsHelpers((sql) => pg.exec(sql));
131
+ await pg.exec(
132
+ `CREATE POLICY owner_select ON ${table} AS PERMISSIVE FOR SELECT TO public USING (owner = oke.user())`,
133
+ );
134
+ const alice = createSqlStoreHandle("sql:app", {
135
+ connection: pg,
136
+ classifications: new Map(),
137
+ routedRole: "primary",
138
+ domainDdl: "off",
139
+ rls: { gate: "member", userId: "alice", scopes: ["member"] },
140
+ });
141
+ const bob = createSqlStoreHandle("sql:app", {
142
+ connection: pg,
143
+ classifications: new Map(),
144
+ routedRole: "primary",
145
+ domainDdl: "off",
146
+ rls: { gate: "member", userId: "bob", scopes: ["member"] },
147
+ });
148
+ const [a, b] = await Promise.all([
149
+ alice.raw(`SELECT id, owner FROM ${table} ORDER BY id`),
150
+ bob.raw(`SELECT id, owner FROM ${table} ORDER BY id`),
151
+ ]);
152
+ expect(a).toEqual([{ id: "a", owner: "alice" }]);
153
+ expect(b).toEqual([{ id: "b", owner: "bob" }]);
154
+ } finally {
155
+ try {
156
+ await pg.exec(`DROP TABLE IF EXISTS ${table}`);
157
+ } catch {
158
+ // Best-effort cleanup.
159
+ }
160
+ await pg.close();
161
+ }
162
+ },
163
+ 20_000,
164
+ );
165
+ });
@@ -0,0 +1,117 @@
1
+ /**
2
+ * RLS stamps on pooled postgres must `reserve()` one slot, then BEGIN/SET
3
+ * on that slot. `begin()` + parent `unsafe()` checkouts a second slot and
4
+ * deadlocks a tiny pool (`checkout timeout` from PgDog).
5
+ */
6
+
7
+ import { describe, expect, test } from "bun:test";
8
+ import { connectPostgres, type PostgresClientLike } from "../../drivers/postgres.ts";
9
+ import { createSqlStoreHandle } from "./sql-session.ts";
10
+
11
+ /**
12
+ * Two-slot pool. `reserve()` holds a slot until `release()`. Unreserved
13
+ * `BEGIN` occupies a slot until COMMIT on that same checkout.
14
+ */
15
+ function createTinyPoolClient(size = 2): PostgresClientLike & {
16
+ readonly reserveCalls: { n: number };
17
+ readonly rootBegins: { n: number };
18
+ } {
19
+ const slots = Array.from({ length: size }, () => ({ held: false }));
20
+ const reserveCalls = { n: 0 };
21
+ const rootBegins = { n: 0 };
22
+
23
+ function checkout(): { held: boolean } {
24
+ const idle = slots.find((slot) => !slot.held);
25
+ if (!idle) {
26
+ throw new Error("checkout timeout");
27
+ }
28
+ return idle;
29
+ }
30
+
31
+ function rowsFor(sql: string): Record<string, unknown>[] {
32
+ return /select/i.test(sql) && !/set_config/i.test(sql) ? [{ id: "a", owner: "alice" }] : [];
33
+ }
34
+
35
+ const client: PostgresClientLike & {
36
+ readonly reserveCalls: { n: number };
37
+ readonly rootBegins: { n: number };
38
+ } = {
39
+ reserveCalls,
40
+ rootBegins,
41
+ async reserve() {
42
+ reserveCalls.n += 1;
43
+ const slot = checkout();
44
+ slot.held = true;
45
+ return {
46
+ async unsafe(sql) {
47
+ return rowsFor(sql);
48
+ },
49
+ release() {
50
+ slot.held = false;
51
+ },
52
+ };
53
+ },
54
+ async unsafe(sql) {
55
+ const slot = checkout();
56
+ const head = sql.trim().split(/\s+/)[0]?.toUpperCase();
57
+ if (head === "BEGIN") {
58
+ rootBegins.n += 1;
59
+ slot.held = true;
60
+ return [];
61
+ }
62
+ if (head === "COMMIT" || head === "ROLLBACK") {
63
+ slot.held = false;
64
+ return [];
65
+ }
66
+ return rowsFor(sql);
67
+ },
68
+ };
69
+ return client;
70
+ }
71
+
72
+ describe("sql-session RLS stamp on pooled postgres", () => {
73
+ test("pins the stamp through reserve — no leaked root BEGIN", async () => {
74
+ const pool = createTinyPoolClient(2);
75
+ const connection = await connectPostgres({ client: pool, role: "primary" });
76
+ const handle = createSqlStoreHandle("sql:app", {
77
+ connection,
78
+ classifications: new Map(),
79
+ routedRole: "primary",
80
+ domainDdl: "off",
81
+ rls: { gate: "member", userId: "alice", scopes: ["member"] },
82
+ });
83
+
84
+ const [a, b] = await Promise.all([
85
+ handle.raw(`SELECT id, owner FROM notes`),
86
+ handle.raw(`SELECT id, owner FROM notes`),
87
+ ]);
88
+
89
+ expect(a).toEqual([{ id: "a", owner: "alice" }]);
90
+ expect(b).toEqual([{ id: "a", owner: "alice" }]);
91
+ expect(pool.reserveCalls.n).toBe(2);
92
+ expect(pool.rootBegins.n).toBe(0);
93
+ });
94
+
95
+ test("table browse SELECT pins through reserve like any other stamped query", async () => {
96
+ const pool = createTinyPoolClient(2);
97
+ const connection = await connectPostgres({ client: pool, role: "primary" });
98
+ const handle = createSqlStoreHandle("sql:app", {
99
+ connection,
100
+ classifications: new Map(),
101
+ routedRole: "primary",
102
+ domainDdl: "off",
103
+ rls: { gate: "member", userId: "alice", scopes: ["member"] },
104
+ });
105
+ const rows = await handle.raw(`SELECT * FROM "notes" LIMIT 50`);
106
+ expect(rows).toEqual([{ id: "a", owner: "alice" }]);
107
+ expect(pool.reserveCalls.n).toBe(1);
108
+ expect(pool.rootBegins.n).toBe(0);
109
+ });
110
+
111
+ test("standalone BEGIN on a 2-slot pool reproduces checkout timeout", async () => {
112
+ const pool = createTinyPoolClient(2);
113
+ await expect(pool.unsafe("BEGIN")).resolves.toEqual([]);
114
+ await expect(pool.unsafe("BEGIN")).resolves.toEqual([]);
115
+ await expect(pool.unsafe("SET LOCAL ROLE oke_app")).rejects.toThrow("checkout timeout");
116
+ });
117
+ });
@@ -7,6 +7,12 @@
7
7
 
8
8
  import type { DomainDdlMode } from "../../config/index.ts";
9
9
  import type { ClassificationMap, SqlConnection, SqlRow } from "../../drivers/types.ts";
10
+ import {
11
+ buildRlsIdentityPreludeSql,
12
+ installOkeRlsHelpers,
13
+ RLS_CONTEXT_DRIVERS,
14
+ type RlsIdentity,
15
+ } from "../../drivers/pg-rls.ts";
10
16
  import { throwOke } from "../../kernel/errors.ts";
11
17
  import { maskRows, tableFromSql } from "./classify.ts";
12
18
  import { isMissingDomainRelationError } from "./missing-relation.ts";
@@ -28,6 +34,45 @@ import {
28
34
 
29
35
  export type { WhereMap } from "./sql-condition.ts";
30
36
 
37
+ /** Serialize stamp frames on a shared connection so concurrent identities cannot interleave. */
38
+ const rlsStampTails = new WeakMap<SqlConnection, Promise<unknown>>();
39
+
40
+ /**
41
+ * True when SQL must not enter an RLS identity frame (txn control or DDL).
42
+ *
43
+ * @param sql - Statement
44
+ */
45
+ export function isRlsStampExemptSql(sql: string): boolean {
46
+ return /^\s*(begin|commit|rollback|create|alter|drop|truncate|grant|revoke|comment)\b/i.test(sql);
47
+ }
48
+
49
+ /**
50
+ * Run `fn` after any in-flight stamp on `connection` finishes.
51
+ *
52
+ * @param connection - Shared SQL connection
53
+ * @param fn - Stamp frame
54
+ */
55
+ async function withRlsStampLock<T>(connection: SqlConnection, fn: () => Promise<T>): Promise<T> {
56
+ const prev = rlsStampTails.get(connection) ?? Promise.resolve();
57
+ let release!: () => void;
58
+ const gate = new Promise<void>((resolve) => {
59
+ release = resolve;
60
+ });
61
+ rlsStampTails.set(
62
+ connection,
63
+ prev.then(
64
+ () => gate,
65
+ () => gate,
66
+ ),
67
+ );
68
+ await prev.catch(() => undefined);
69
+ try {
70
+ return await fn();
71
+ } finally {
72
+ release();
73
+ }
74
+ }
75
+
31
76
  /** Options for a SQL session. */
32
77
  export interface SqlSessionOptions {
33
78
  /** Open connection (already role-routed). */
@@ -44,6 +89,8 @@ export interface SqlSessionOptions {
44
89
  * and local+autoPush). Default `ensure` for backward-compatible tests.
45
90
  */
46
91
  readonly domainDdl?: DomainDdlMode;
92
+ /** Gate identity — stamps `oke.*` on postgres / pglite. */
93
+ readonly rls?: RlsIdentity;
47
94
  }
48
95
 
49
96
  /**
@@ -339,8 +386,9 @@ export function createSqlStoreHandle(
339
386
  ref: `sql:${string}`,
340
387
  options: SqlSessionOptions,
341
388
  ): SqlStoreHandle {
342
- const { connection, classifications, revealPii } = options;
389
+ const { connection, classifications, revealPii, rls } = options;
343
390
  const domainDdl: DomainDdlMode = options.domainDdl ?? "ensure";
391
+ let helpersReady = false;
344
392
 
345
393
  function mask(rows: SqlRow[], table?: string): SqlRow[] {
346
394
  return maskRows(rows, { classifications, table, revealPii });
@@ -362,11 +410,41 @@ export function createSqlStoreHandle(
362
410
  }
363
411
 
364
412
  function query(sql: string, params: readonly unknown[] = []): Promise<SqlRow[]> {
365
- return withSchemaGuard(() => connection.query(sql, params));
413
+ return withSchemaGuard(() => withRlsStamp((conn) => conn.query(sql, params), sql));
366
414
  }
367
415
 
368
416
  function exec(sql: string, params: readonly unknown[] = []): Promise<{ changes: number }> {
369
- return withSchemaGuard(() => connection.exec(sql, params));
417
+ return withSchemaGuard(() => withRlsStamp((conn) => conn.exec(sql, params), sql));
418
+ }
419
+
420
+ async function withRlsStamp<T>(fn: (conn: SqlConnection) => Promise<T>, sql: string): Promise<T> {
421
+ if (!rls || !RLS_CONTEXT_DRIVERS.has(connection.driverId)) return fn(connection);
422
+ if (isRlsStampExemptSql(sql)) return fn(connection);
423
+ const run = (): Promise<T> => applyRlsStamp(fn);
424
+ // PGlite is one backend session — concurrent identities must not interleave.
425
+ // Pooled postgres pins each stamp via `transaction()` instead.
426
+ if (connection.driverId === "pglite") {
427
+ return withRlsStampLock(connection, run);
428
+ }
429
+ return run();
430
+ }
431
+
432
+ async function applyRlsStamp<T>(fn: (conn: SqlConnection) => Promise<T>): Promise<T> {
433
+ if (!rls) return fn(connection);
434
+ if (!helpersReady) {
435
+ await installOkeRlsHelpers((sql) => connection.exec(sql));
436
+ helpersReady = true;
437
+ }
438
+ const frame = async (tx: SqlConnection): Promise<T> => {
439
+ for (const stmt of buildRlsIdentityPreludeSql(rls)) {
440
+ await tx.exec(stmt.sql, stmt.params ?? []);
441
+ }
442
+ return fn(tx);
443
+ };
444
+ if (!connection.transaction) {
445
+ throw new Error("RLS stamp needs SqlConnection.transaction to pin SET LOCAL");
446
+ }
447
+ return connection.transaction(frame);
370
448
  }
371
449
 
372
450
  async function ensureFromMeta(table: TableHandle | unknown): Promise<void> {
@@ -45,7 +45,7 @@ describe("fx.store(db).upsert via app.fetch (real boot)", () => {
45
45
  const db = store.sql("app", { schema: { notes } });
46
46
 
47
47
  const seed = on(
48
- http.post("/seed").gate.public,
48
+ http.post("/seed").public(),
49
49
  flow("notes.seed", {
50
50
  in: UpsertIn,
51
51
  out: UpsertOut,
@@ -26,6 +26,11 @@ export {
26
26
  schema,
27
27
  schemaTable,
28
28
  schemaRelations,
29
+ schemaRls,
30
+ schemaPolicy,
31
+ schemaPolicyGate,
32
+ schemaPolicyOwner,
33
+ schemaPolicyScope,
29
34
  isSchemaTableDecl,
30
35
  isSchemaColumnDecl,
31
36
  isSchemaRelationsDecl,
@@ -43,6 +48,8 @@ export type {
43
48
  SchemaTableDecl,
44
49
  SchemaTableWithColumns,
45
50
  SchemaRelationsDecl,
51
+ SchemaPolicyDecl,
52
+ SchemaTableExtra,
46
53
  SchemaRelationEntry,
47
54
  SchemaRelationOne,
48
55
  SchemaRelationMany,
@@ -123,6 +130,13 @@ export type {
123
130
  WhereMap,
124
131
  } from "./store/sql-session.ts";
125
132
 
133
+ export {
134
+ resolveRlsIdentity,
135
+ rlsIdentityFromAuth,
136
+ firstPolicyOrPublic,
137
+ policyGateForScopes,
138
+ } from "./store/rls-identity.ts";
139
+ export type { RlsIdentityRow, ResolveRlsIdentityInput } from "./store/rls-identity.ts";
126
140
  export { createStoreRuntime, putTier2 } from "./store/runtime.ts";
127
141
  export type {
128
142
  StoreRuntime,
@@ -450,6 +450,23 @@ export function createBuiltinVaultAdapter(opts: CreateBuiltinVaultOptions): Buil
450
450
  * @param keyRow - That version's live wrapped DEK
451
451
  */
452
452
  async function decryptRow(row: SecretRow, keyRow: KeyRow): Promise<string> {
453
+ try {
454
+ return await decryptRowOnce(row, keyRow);
455
+ } catch (err) {
456
+ // Stale key snapshot: rotation committed and dropped `pending` after
457
+ // this read. Reload the wrap and retry once — never return torn bytes.
458
+ if (!(err instanceof VaultError) || err.code !== "INVALID_KEY") throw err;
459
+ return await decryptRowOnce(row, await readKeyRow(row.id));
460
+ }
461
+ }
462
+
463
+ /**
464
+ * Unwrap the DEK and decrypt the value ciphertext.
465
+ *
466
+ * @param row - Stored secret version
467
+ * @param keyRow - That version's wrapped DEK
468
+ */
469
+ async function decryptRowOnce(row: SecretRow, keyRow: KeyRow): Promise<string> {
453
470
  const path = row.path;
454
471
  const version = toInt(row.version);
455
472
  const algorithm = row.algorithm as VaultAlgorithm;
package/src/full.ts CHANGED
@@ -88,6 +88,8 @@ export {
88
88
  type SignalDecl,
89
89
  type SignalOptions,
90
90
  type SignalRuntime,
91
+ type DeadLetter,
92
+ type SignalFailureReason,
91
93
  } from "./elements/signal.ts";
92
94
 
93
95
  export {
package/src/http.ts CHANGED
@@ -8,7 +8,7 @@
8
8
  * ```ts
9
9
  * import { on, flow, http, gate, oke, createBunRuntime } from "okengine/http";
10
10
  *
11
- * on(http.get("/").gate.public, flow("ping", { do: () => "Hi" }));
11
+ * on(http.get("/").public(), flow("ping", { do: () => "Hi" }));
12
12
  * const app = oke({ name: "ping" });
13
13
  * createBunRuntime().serve(app);
14
14
  * ```
@@ -24,15 +24,32 @@ export { fail, type FlowFailure } from "./kernel/errors.ts";
24
24
  export { isFlowFailure } from "./kernel/hooks.ts";
25
25
  export { plugin, isPlugin, type PluginDef } from "./kernel/plugin.ts";
26
26
  export { createBunRuntime, APP_PORT, type Runtime, type ServeOptions } from "./runtime/index.ts";
27
- export type { OkeApp, OkeOptions, ReadyState } from "./kernel/app.ts";
27
+ export type {
28
+ OkeApp,
29
+ OkeOptions,
30
+ ReadyState,
31
+ RegisteredFlowUnits,
32
+ RoutesFromRegisteredUnits,
33
+ } from "./kernel/app.ts";
34
+ export { registerFlowUnits } from "./kernel/flow-units.ts";
35
+ export { stampFlowName, stampHttpPath } from "./kernel/stamp-http.ts";
28
36
 
29
- import { oke as okeCore, type OkeApp, type OkeOptions } from "./kernel/app.ts";
37
+ import {
38
+ oke as okeCore,
39
+ type OkeApp,
40
+ type OkeOptions,
41
+ type RoutesFromRegisteredUnits,
42
+ } from "./kernel/app.ts";
30
43
 
31
44
  /**
32
45
  * Create an HTTP-oriented app — defaults `router` to `"edge"` when omitted.
33
46
  *
34
47
  * @param options - Application options
35
48
  */
49
+ export function oke(
50
+ options: OkeOptions & { readonly registry: "ignore" },
51
+ ): OkeApp<Record<string, never>, Record<string, never>>;
52
+ export function oke(options: OkeOptions): OkeApp<{}, RoutesFromRegisteredUnits>;
36
53
  export function oke(options: OkeOptions): OkeApp {
37
54
  return okeCore({
38
55
  ...options,
package/src/index.ts CHANGED
@@ -14,10 +14,19 @@
14
14
  * @module
15
15
  */
16
16
 
17
- export { oke, type OkeApp, type OkeOptions, type ReadyState } from "./kernel/app.ts";
17
+ export {
18
+ oke,
19
+ type OkeApp,
20
+ type OkeOptions,
21
+ type ReadyState,
22
+ type RegisteredFlowUnits,
23
+ type RoutesFromRegisteredUnits,
24
+ } from "./kernel/app.ts";
18
25
  export { on, type Binding } from "./kernel/on.ts";
19
26
  export { flow, isFlow, type FlowDef } from "./kernel/flow.ts";
20
27
  export { http, every, internal, table } from "./kernel/triggers.ts";
28
+ export { registerFlowUnits } from "./kernel/flow-units.ts";
29
+ export { stampFlowName, stampHttpPath } from "./kernel/stamp-http.ts";
21
30
  export { fail, type FlowFailure, type FlowErrorValue } from "./kernel/errors.ts";
22
31
  export { isFlowFailure } from "./kernel/hooks.ts";
23
32
  export { plugin, isPlugin, type PluginDef, type PluginCapabilities } from "./kernel/plugin.ts";
@@ -71,6 +80,8 @@ export {
71
80
  type SignalDecl,
72
81
  type SignalOptions,
73
82
  type SignalRuntime,
83
+ type DeadLetter,
84
+ type SignalFailureReason,
74
85
  } from "./elements/signal.ts";
75
86
 
76
87
  export {
@@ -143,6 +154,8 @@ export {
143
154
  type AiModelDecl,
144
155
  type AiPromptDecl,
145
156
  type AiAgentDecl,
157
+ type AiMcpServerDecl,
158
+ type AiMcpServerOptions,
146
159
  type AiRuntime,
147
160
  } from "./elements/ai.ts";
148
161
 
@@ -89,7 +89,7 @@ describe("boot-level: stale barrel through a real oke() boot", () => {
89
89
  effects: {},
90
90
  do: () => ({ ok: true as const }),
91
91
  });
92
- on(http.get("/health").gate.public, mainFlow);
92
+ on(http.get("/health").public(), mainFlow);
93
93
  const app = oke({ name: "stale-barrel", gate: { unguardedHttp: "allow" } });
94
94
 
95
95
  await expect(