okengine 0.14.1 → 0.16.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (285) hide show
  1. package/package.json +3 -4
  2. package/site/content/docs/elements/ai.mdx +2 -1
  3. package/site/content/docs/elements/channel.mdx +1 -0
  4. package/site/content/docs/elements/clock.mdx +3 -0
  5. package/site/content/docs/elements/flow.mdx +9 -0
  6. package/site/content/docs/elements/gate.mdx +80 -22
  7. package/site/content/docs/elements/signal.mdx +4 -4
  8. package/site/content/docs/elements/store.mdx +84 -27
  9. package/site/content/docs/elements/vault.mdx +4 -0
  10. package/site/content/docs/get-started/basic-usage.mdx +22 -13
  11. package/site/content/docs/get-started/index.mdx +5 -0
  12. package/site/content/docs/get-started/introduction.mdx +11 -3
  13. package/site/content/docs/get-started/meta.json +1 -1
  14. package/site/content/docs/get-started/project-structure.mdx +932 -0
  15. package/site/content/docs/reference/cli.md +13 -10
  16. package/site/content/docs/reference/client.mdx +28 -21
  17. package/site/content/docs/reference/fx.mdx +29 -18
  18. package/site/content/docs/reference/security.md +2 -2
  19. package/src/auth/api-key-sql.test.ts +47 -0
  20. package/src/auth/api-key-sql.ts +254 -0
  21. package/src/auth/api-keys.ts +273 -14
  22. package/src/auth/auth.test.ts +56 -0
  23. package/src/auth/bindings.ts +14 -5
  24. package/src/auth/config.ts +5 -5
  25. package/src/auth/cross-plane.ts +10 -3
  26. package/src/auth/index.ts +25 -1
  27. package/src/auth/schema.ts +8 -0
  28. package/src/cli/ai-setup/ai-setup.test.ts +62 -0
  29. package/src/cli/ai-setup/apply.ts +32 -1
  30. package/src/cli/attach-host-console.test.ts +40 -2
  31. package/src/cli/attach-host-console.ts +9 -0
  32. package/src/cli/build.test.ts +4 -2
  33. package/src/cli/db-seed.ts +48 -15
  34. package/src/cli/db.test.ts +104 -0
  35. package/src/cli/db.ts +122 -10
  36. package/src/cli/dev-controls.test.ts +20 -0
  37. package/src/cli/dev-controls.ts +13 -0
  38. package/src/cli/dev.test.ts +15 -4
  39. package/src/cli/dev.ts +102 -29
  40. package/src/cli/ensure-drizzle-config.ts +2 -1
  41. package/src/cli/tui/DevLive.tsx +12 -2
  42. package/src/cli/tui/keys.test.ts +2 -1
  43. package/src/cli/tui/keys.ts +3 -2
  44. package/src/compiler/aot.test.ts +1 -1
  45. package/src/compiler/effects-infer.ts +17 -2
  46. package/src/compiler/extract.test.ts +409 -33
  47. package/src/compiler/extract.ts +318 -67
  48. package/src/compiler/flow-path.test.ts +96 -0
  49. package/src/compiler/flow-path.ts +173 -0
  50. package/src/compiler/generate-adopt.test.ts +112 -3
  51. package/src/compiler/generate-adopt.ts +280 -26
  52. package/src/compiler/schema-from-ast.ts +609 -0
  53. package/src/console/server/access.test.ts +116 -8
  54. package/src/console/server/access.ts +70 -23
  55. package/src/console/server/app.ts +1 -0
  56. package/src/console/server/bind.ts +2 -2
  57. package/src/console/server/console.test.ts +3 -0
  58. package/src/console/server/dev-identities.test.ts +127 -0
  59. package/src/console/server/dev-identities.ts +229 -0
  60. package/src/console/server/flows-invoke.test.ts +21 -4
  61. package/src/console/server/flows.ts +233 -98
  62. package/src/console/server/gates.test.ts +24 -0
  63. package/src/console/server/gates.ts +8 -21
  64. package/src/console/server/index.ts +1 -0
  65. package/src/console/server/invoke-user-flow.test.ts +3 -1
  66. package/src/console/server/invoke-user-flow.ts +17 -2
  67. package/src/console/server/iso-at.test.ts +9 -0
  68. package/src/console/server/iso-at.ts +11 -0
  69. package/src/console/server/live.test.ts +26 -1
  70. package/src/console/server/live.ts +4 -2
  71. package/src/console/server/operator-db.ts +2 -0
  72. package/src/console/server/security-headers.ts +6 -3
  73. package/src/console/server/spa-proxy.test.ts +1 -0
  74. package/src/console/server/sql-catalog.ts +147 -7
  75. package/src/console/server/sql-rls.test.ts +16 -0
  76. package/src/console/server/state.ts +37 -55
  77. package/src/console/server/store.test.ts +7 -4
  78. package/src/console/server/store.ts +142 -58
  79. package/src/console/ui-next/dist/assets/access-page-CAGHrA9H.js +4 -0
  80. package/src/console/ui-next/dist/assets/agent-disclosure-BP0Y0Sux.js +1 -0
  81. package/src/console/ui-next/dist/assets/cache-glyph-B5X-NM0-.js +1 -0
  82. package/src/console/ui-next/dist/assets/call-pii-button-ChAUiLo9.js +1 -0
  83. package/src/console/ui-next/dist/assets/collapsible-DGnOM2ph.js +1 -0
  84. package/src/console/ui-next/dist/assets/copy-inline-button-DBHhgHKP.js +1 -0
  85. package/src/console/ui-next/dist/assets/detail-header-DhHM1iaZ.js +1 -0
  86. package/src/console/ui-next/dist/assets/dropdown-menu-_NjTEo5_.js +1 -0
  87. package/src/console/ui-next/dist/assets/{duration-tone-oiRxPVsZ.js → duration-tone-sC3lGABz.js} +1 -1
  88. package/src/console/ui-next/dist/assets/element-icons-BVXtRyd3.js +1 -0
  89. package/src/console/ui-next/dist/assets/explorer-empty-2uIhBu0_.js +1 -0
  90. package/src/console/ui-next/dist/assets/flows-page-B-OUtiAu.js +1 -0
  91. package/src/console/ui-next/dist/assets/{highlighted-json-CvDPvveV.js → highlighted-json-Awq7gYdu.js} +1 -1
  92. package/src/console/ui-next/dist/assets/http-method-_UHM2ODJ.js +1 -0
  93. package/src/console/ui-next/dist/assets/index-CGoZkILK.js +66 -0
  94. package/src/console/ui-next/dist/assets/index-Ck88Jmv8.css +2 -0
  95. package/src/console/ui-next/dist/assets/link-BX6Vqztd.js +1 -0
  96. package/src/console/ui-next/dist/assets/observability-page-BDyXalNR.js +4 -0
  97. package/src/console/ui-next/dist/assets/react-D8E3mtu1.js +1 -0
  98. package/src/console/ui-next/dist/assets/replica-lag-DKRrbvdo.js +18 -0
  99. package/src/console/ui-next/dist/assets/request-meta-DV0ywz7t.js +1 -0
  100. package/src/console/ui-next/dist/assets/shortcut-JQIZlWfm.js +1 -0
  101. package/src/console/ui-next/dist/assets/shortcut-keys-DKxNTe_m.js +1 -0
  102. package/src/console/ui-next/dist/assets/{skeleton-CL_X0GCj.js → skeleton-D-czQJT6.js} +1 -1
  103. package/src/console/ui-next/dist/assets/store-page-Xh8Kn3rx.js +41 -0
  104. package/src/console/ui-next/dist/assets/trace-detail-sheet-B09O8rA6.js +2 -0
  105. package/src/console/ui-next/dist/assets/tree-expand-toggle-DkOXA12R.js +54 -0
  106. package/src/console/ui-next/dist/assets/units-page-Dpk40kOQ.js +1 -0
  107. package/src/console/ui-next/dist/assets/{use-vault-list-Cl79j_ku.js → use-vault-list-uk4WVboC.js} +1 -1
  108. package/src/console/ui-next/dist/assets/useMutation-B8EO02Ej.js +1 -0
  109. package/src/console/ui-next/dist/assets/useRender-BE2A9BWC.js +1 -0
  110. package/src/console/ui-next/dist/assets/vault-page-B1dB9Ft0.js +2 -0
  111. package/src/console/ui-next/dist/index.html +12 -10
  112. package/src/console/ui-next/index.html +3 -3
  113. package/src/console/ui-next/src/client.ts +202 -7
  114. package/src/console/ui-next/src/components/motion/table/index.tsx +16 -0
  115. package/src/console/ui-next/src/components/motion/table/types.ts +4 -0
  116. package/src/console/ui-next/src/components/shell/app-sidebar.tsx +2 -0
  117. package/src/console/ui-next/src/components/shell/command-items.test.ts +1 -0
  118. package/src/console/ui-next/src/components/shell/command-items.ts +8 -0
  119. package/src/console/ui-next/src/components/shell/shell-layout.tsx +2 -1
  120. package/src/console/ui-next/src/components/ui/kbd.tsx +2 -2
  121. package/src/console/ui-next/src/components/ui/sheet-form.tsx +8 -3
  122. package/src/console/ui-next/src/features/access/access-page.tsx +347 -0
  123. package/src/console/ui-next/src/features/access/data/use-access.ts +23 -0
  124. package/src/console/ui-next/src/features/access/detail/access-detail.tsx +552 -0
  125. package/src/console/ui-next/src/features/access/explorer/access-list.tsx +125 -0
  126. package/src/console/ui-next/src/features/access/lib/format-when.test.ts +139 -0
  127. package/src/console/ui-next/src/features/access/lib/format-when.ts +296 -0
  128. package/src/console/ui-next/src/features/access/lib/scope-groups.test.ts +34 -0
  129. package/src/console/ui-next/src/features/access/lib/scope-groups.ts +40 -0
  130. package/src/console/ui-next/src/features/access/sheets/access-allow-fields.tsx +236 -0
  131. package/src/console/ui-next/src/features/access/sheets/access-create-sheet.tsx +316 -0
  132. package/src/console/ui-next/src/features/access/sheets/access-edit-sheet.tsx +240 -0
  133. package/src/console/ui-next/src/features/access/sheets/access-expiry-fields.tsx +66 -0
  134. package/src/console/ui-next/src/features/access/sheets/access-refresh-sheet.tsx +104 -0
  135. package/src/console/ui-next/src/features/access/sheets/access-scope-field.tsx +115 -0
  136. package/src/console/ui-next/src/features/access/sheets/access-secret-sheet.tsx +89 -0
  137. package/src/console/ui-next/src/features/access/sheets/access-sheet-search.tsx +49 -0
  138. package/src/console/ui-next/src/features/access/state/access-selection.ts +84 -0
  139. package/src/console/ui-next/src/features/auth/auth-redirect.test.ts +5 -0
  140. package/src/console/ui-next/src/features/auth/auth-redirect.ts +18 -2
  141. package/src/console/ui-next/src/features/flows/traces/http-method.ts +19 -0
  142. package/src/console/ui-next/src/features/flows/traces/trace-detail.test.ts +19 -1
  143. package/src/console/ui-next/src/features/flows/traces/traces-pane.tsx +10 -3
  144. package/src/console/ui-next/src/features/not-found/not-found-page.tsx +1 -1
  145. package/src/console/ui-next/src/features/store/detail/browse-section.tsx +45 -1
  146. package/src/console/ui-next/src/features/store/detail/resource-panel.tsx +78 -16
  147. package/src/console/ui-next/src/features/store/detail/store-row-detail-sheet.tsx +155 -14
  148. package/src/console/ui-next/src/features/store/explorer/store-tree.tsx +33 -133
  149. package/src/console/ui-next/src/features/store/files/file-preview.tsx +4 -1
  150. package/src/console/ui-next/src/features/store/grid/rls-policy-sheet.tsx +326 -165
  151. package/src/console/ui-next/src/features/store/grid/sql-insert-sheet.tsx +6 -0
  152. package/src/console/ui-next/src/features/store/grid/store-data-grid.tsx +50 -3
  153. package/src/console/ui-next/src/features/store/lib/files-meta.test.ts +3 -0
  154. package/src/console/ui-next/src/features/store/lib/files-meta.ts +12 -0
  155. package/src/console/ui-next/src/features/store/lib/query-defaults.test.ts +25 -0
  156. package/src/console/ui-next/src/features/store/lib/query-defaults.ts +85 -3
  157. package/src/console/ui-next/src/features/store/lib/query-gate.ts +30 -4
  158. package/src/console/ui-next/src/features/store/lib/rls-policy.test.ts +139 -0
  159. package/src/console/ui-next/src/features/store/lib/rls-policy.ts +223 -21
  160. package/src/console/ui-next/src/features/store/lib/schema-graph.ts +4 -0
  161. package/src/console/ui-next/src/features/store/lib/sql-catalog.test.ts +24 -0
  162. package/src/console/ui-next/src/features/store/lib/sql-catalog.ts +19 -0
  163. package/src/console/ui-next/src/features/store/lib/store-tree.test.ts +6 -0
  164. package/src/console/ui-next/src/features/store/lib/store-tree.ts +13 -0
  165. package/src/console/ui-next/src/features/store/query/query-console.tsx +48 -14
  166. package/src/console/ui-next/src/features/store/query/query-gate-menu.tsx +19 -20
  167. package/src/console/ui-next/src/features/store/query/query-gate-parts.tsx +2 -1
  168. package/src/console/ui-next/src/features/store/query/query-results.tsx +110 -83
  169. package/src/console/ui-next/src/features/store/schema/schema-table-node.tsx +17 -0
  170. package/src/console/ui-next/src/features/units/call/call-api-panel.tsx +112 -72
  171. package/src/console/ui-next/src/features/units/lib/contract-input.test.ts +206 -1
  172. package/src/console/ui-next/src/features/units/lib/contract-input.ts +55 -0
  173. package/src/console/ui-next/src/features/vault/lib/types.ts +1 -1
  174. package/src/console/ui-next/src/lib/document-meta.test.ts +1 -0
  175. package/src/console/ui-next/src/lib/document-meta.ts +2 -1
  176. package/src/console/ui-next/src/lib/last-module-search.test.ts +1 -0
  177. package/src/console/ui-next/src/lib/last-module-search.ts +2 -1
  178. package/src/console/ui-next/src/lib/shortcut.test.ts +2 -0
  179. package/src/console/ui-next/src/lib/shortcut.ts +3 -0
  180. package/src/console/ui-next/src/router.tsx +10 -1
  181. package/src/console/ui-next/ui-next-seed-app-schema.ts +88 -37
  182. package/src/console/ui-next/ui-next-seed-store.ts +43 -15
  183. package/src/console/xss.gate.test.ts +1 -0
  184. package/src/docker/compose-health.test.ts +27 -0
  185. package/src/docker/compose-health.ts +19 -3
  186. package/src/docker/docker.test.ts +3 -0
  187. package/src/docker/recipes/pgdog.ts +2 -0
  188. package/src/drivers/clock-postgres.ts +13 -9
  189. package/src/drivers/instances-postgres.ts +4 -6
  190. package/src/drivers/journal-postgres.ts +13 -9
  191. package/src/drivers/pg-rls.test.ts +87 -0
  192. package/src/drivers/pg-rls.ts +233 -7
  193. package/src/drivers/pglite.ts +26 -3
  194. package/src/drivers/postgres.test.ts +23 -0
  195. package/src/drivers/postgres.ts +133 -9
  196. package/src/drivers/types.ts +10 -0
  197. package/src/elements/clock/declare.ts +26 -1
  198. package/src/elements/gate/boot.ts +5 -5
  199. package/src/elements/gate/declare.ts +38 -4
  200. package/src/elements/gate/flatten.ts +1 -1
  201. package/src/elements/gate/runtime.ts +1 -1
  202. package/src/elements/store/cache.ts +1 -1
  203. package/src/elements/store/declare.ts +9 -6
  204. package/src/elements/store/emit-drizzle.ts +44 -8
  205. package/src/elements/store/resource.test.ts +1 -1
  206. package/src/elements/store/rls-identity.test.ts +95 -0
  207. package/src/elements/store/rls-identity.ts +116 -0
  208. package/src/elements/store/runtime.ts +18 -0
  209. package/src/elements/store/schema-decl.test.ts +37 -0
  210. package/src/elements/store/schema-decl.ts +260 -41
  211. package/src/elements/store/seed.ts +1 -1
  212. package/src/elements/store/sql-condition.test.ts +12 -0
  213. package/src/elements/store/sql-condition.ts +26 -2
  214. package/src/elements/store/sql-rls-isolation.test.ts +165 -0
  215. package/src/elements/store/sql-rls-stamp.test.ts +117 -0
  216. package/src/elements/store/sql-select.types.test.ts +71 -0
  217. package/src/elements/store/sql-session.ts +135 -35
  218. package/src/elements/store/upsert-app.test.ts +1 -1
  219. package/src/elements/store.ts +17 -0
  220. package/src/http.ts +20 -3
  221. package/src/index.ts +11 -1
  222. package/src/kernel/adopt-barrel-fresh.test.ts +1 -1
  223. package/src/kernel/adopt-routes.ts +30 -2
  224. package/src/kernel/api-key-host-persist.test.ts +143 -0
  225. package/src/kernel/app.ts +145 -16
  226. package/src/kernel/auth-resolve.ts +10 -5
  227. package/src/kernel/auto-cache.test.ts +8 -8
  228. package/src/kernel/auto-registry.test.ts +49 -9
  229. package/src/kernel/boot.test.ts +5 -5
  230. package/src/kernel/boot.ts +2 -2
  231. package/src/kernel/effects-stamping.test.ts +3 -3
  232. package/src/kernel/element-registries.ts +12 -5
  233. package/src/kernel/errors.ts +24 -0
  234. package/src/kernel/flow-units.ts +64 -0
  235. package/src/kernel/flow.ts +29 -17
  236. package/src/kernel/fx-auth-keys.test.ts +88 -0
  237. package/src/kernel/fx-auth-keys.ts +198 -0
  238. package/src/kernel/fx.test.ts +17 -1
  239. package/src/kernel/fx.ts +59 -18
  240. package/src/kernel/horizontal-child.ts +4 -4
  241. package/src/kernel/http-path-pending.ts +20 -0
  242. package/src/kernel/http-resource.ts +4 -0
  243. package/src/kernel/http-stream.test.ts +4 -4
  244. package/src/kernel/pipeline-api-key.test.ts +192 -7
  245. package/src/kernel/pipeline.test.ts +2 -2
  246. package/src/kernel/pipeline.ts +4 -2
  247. package/src/kernel/plugin-elements.test.ts +1 -1
  248. package/src/kernel/plugin-needs.test.ts +1 -1
  249. package/src/kernel/ready.test.ts +2 -2
  250. package/src/kernel/resource-mount.test.ts +14 -0
  251. package/src/kernel/run-duration.test.ts +2 -2
  252. package/src/kernel/stamp-http.test.ts +79 -0
  253. package/src/kernel/stamp-http.ts +75 -0
  254. package/src/kernel/triggers.ts +52 -27
  255. package/src/manifest/types.ts +22 -3
  256. package/src/release/build-lib.ts +1 -0
  257. package/src/release/measure.ts +2 -2
  258. package/src/runtime/json-code-block.test.ts +2 -2
  259. package/src/runtime/serve.test.ts +4 -4
  260. package/src/term.test.ts +52 -4
  261. package/src/term.ts +125 -17
  262. package/src/test/reset-element-registries.ts +8 -3
  263. package/src/cli/ask-seed.test.ts +0 -96
  264. package/src/cli/ask-seed.ts +0 -82
  265. package/src/console/ui-next/dist/assets/cache-glyph-BanhLsEY.js +0 -1
  266. package/src/console/ui-next/dist/assets/call-pii-button-bqkxMrJH.js +0 -1
  267. package/src/console/ui-next/dist/assets/collapsible-DYb0xU8C.js +0 -1
  268. package/src/console/ui-next/dist/assets/confirm-sheet-DDCRmG62.js +0 -1
  269. package/src/console/ui-next/dist/assets/explorer-empty-HjTnVQoR.js +0 -1
  270. package/src/console/ui-next/dist/assets/flows-page-DxDsOd4f.js +0 -1
  271. package/src/console/ui-next/dist/assets/http-method-CJCBYL2j.js +0 -1
  272. package/src/console/ui-next/dist/assets/index-Ce6WKWKM.js +0 -66
  273. package/src/console/ui-next/dist/assets/index-D4Ldtj79.css +0 -2
  274. package/src/console/ui-next/dist/assets/link-DF7SZ9Ek.js +0 -1
  275. package/src/console/ui-next/dist/assets/observability-page-BEZDzyYh.js +0 -4
  276. package/src/console/ui-next/dist/assets/query-gate-parts-1m8m1iNp.js +0 -2
  277. package/src/console/ui-next/dist/assets/react-B1ML8gxg.js +0 -1
  278. package/src/console/ui-next/dist/assets/replica-lag-C8_BRt2x.js +0 -18
  279. package/src/console/ui-next/dist/assets/sheet-form-D-ata7jy.js +0 -1
  280. package/src/console/ui-next/dist/assets/shortcut-Cgzuv4k1.js +0 -1
  281. package/src/console/ui-next/dist/assets/shortcut-keys-DO4IsVqv.js +0 -1
  282. package/src/console/ui-next/dist/assets/store-page-v3LXdYpr.js +0 -45
  283. package/src/console/ui-next/dist/assets/trace-detail-sheet-DFLFfUUX.js +0 -2
  284. package/src/console/ui-next/dist/assets/units-page-C0gW6Kdo.js +0 -1
  285. package/src/console/ui-next/dist/assets/vault-page-DuKzqwzW.js +0 -2
@@ -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(
@@ -39,10 +39,20 @@ export type AppFlowRoute<
39
39
  readonly path?: undefined;
40
40
  };
41
41
 
42
- /** Unit → flow → contract. */
42
+ /**
43
+ * Unit → flow → contract. Leaf is the HTTP / call-only union — do not
44
+ * index with `AppFlowRoute<any, any, any, any, any>` (`any extends string`
45
+ * makes `method`/`path` required and rejects cron / signal flows).
46
+ */
43
47
  export type AppRouteMap = {
44
48
  readonly [unit: string]: {
45
- readonly [flow: string]: AppFlowRoute<any, any, any, any, any>;
49
+ readonly [flow: string]: {
50
+ readonly in?: unknown;
51
+ readonly out?: unknown;
52
+ readonly errors?: unknown;
53
+ readonly method?: string;
54
+ readonly path?: string;
55
+ };
46
56
  };
47
57
  };
48
58
 
@@ -170,6 +180,7 @@ export function accumulateAdoptArgs(args: readonly unknown[], into: RuntimeRoute
170
180
  const unitBag = into[unit] ?? (into[unit] = {});
171
181
  for (const [flowName, flowDef] of Object.entries(value as Record<string, unknown>)) {
172
182
  if (!isFlow(flowDef)) continue;
183
+ stampAdoptedFlow(flowDef, unit, flowName);
173
184
  found.push(flowDef);
174
185
  unitBag[flowName] = runtimeRouteFromFlow(flowDef);
175
186
  }
@@ -178,3 +189,20 @@ export function accumulateAdoptArgs(args: readonly unknown[], into: RuntimeRoute
178
189
 
179
190
  return found;
180
191
  }
192
+
193
+ /**
194
+ * Stamp `unit.export` on nameless flows — same rule as {@link unit}.
195
+ *
196
+ * @param flowDef - Flow to stamp
197
+ * @param unit - Client unit (folder / adopt key)
198
+ * @param exportName - Namespace export
199
+ */
200
+ function stampAdoptedFlow(flowDef: AnyFlowDef, unit: string, exportName: string): void {
201
+ const f = flowDef as { name: string; unit: string | undefined };
202
+ if (!f.name || f.name.startsWith("flow_")) {
203
+ f.name = `${unit}.${exportName}`;
204
+ }
205
+ if (!f.unit) {
206
+ f.unit = unit;
207
+ }
208
+ }
@@ -0,0 +1,143 @@
1
+ /**
2
+ * Host API-key durability — standalone `oke()` (no Console).
3
+ *
4
+ * Creates a key via `fx.auth.createApiKey`, stops, boots a fresh app against
5
+ * the same SQL URL, and verifies Bearer on the new `gate.auth.apiKeyStore`.
6
+ * Production uses postgres + `DATABASE_URL`; this suite uses file-backed
7
+ * PGlite at that same URL so the restart is hermetic.
8
+ */
9
+
10
+ import { afterEach, describe, expect, test } from "bun:test";
11
+ import { mkdtemp, rm } from "node:fs/promises";
12
+ import { tmpdir } from "node:os";
13
+ import { join } from "node:path";
14
+ import { AUTH_TABLES } from "../auth/tables.ts";
15
+ import { gate } from "../elements/gate.ts";
16
+ import { resetStores, store } from "../elements/store/declare.ts";
17
+ import { oke } from "./app.ts";
18
+ import { flow, resetFlowSeq } from "./flow.ts";
19
+ import { on, resetBindings } from "./on.ts";
20
+ import { http } from "./triggers.ts";
21
+
22
+ const PEPPER = "oke-host-api-key-persist-pepper";
23
+ const member = gate.policy("member", ({ auth }) => !!auth.verified);
24
+
25
+ function resetIsolated(): void {
26
+ resetBindings();
27
+ resetFlowSeq();
28
+ resetStores();
29
+ }
30
+
31
+ afterEach(() => {
32
+ resetIsolated();
33
+ });
34
+
35
+ describe("host API key persist via store.sql()", () => {
36
+ test("standalone oke() persists keys across restart; Bearer verifies on a fresh store", async () => {
37
+ const dir = await mkdtemp(join(tmpdir(), "oke-host-keys-"));
38
+ const sqlUrl = join(dir, "pgdata");
39
+ const prevDb = process.env.DATABASE_URL;
40
+ const prevPglite = process.env.OKE_PGLITE_URL;
41
+ process.env.DATABASE_URL = sqlUrl;
42
+ process.env.OKE_PGLITE_URL = sqlUrl;
43
+
44
+ try {
45
+ resetIsolated();
46
+ const db1 = store.sql("app", { schema: {} });
47
+ on(
48
+ http.post("/keys").gate(member),
49
+ flow("keys.create", {
50
+ effects: { writes: ["auth:api-keys"] },
51
+ do: (_input, fx) => fx.auth.createApiKey({ name: "prod-ci", scopes: ["member"] }),
52
+ }),
53
+ );
54
+ on(
55
+ http.get("/secure").gate(member),
56
+ flow("secure.ping", {
57
+ effects: {},
58
+ do: () => ({ ok: true }),
59
+ }),
60
+ );
61
+
62
+ const first = oke({
63
+ name: "host-keys-first",
64
+ stores: [db1],
65
+ gate: {
66
+ auth: { secret: PEPPER, http: false },
67
+ policies: [member],
68
+ },
69
+ env: "test",
70
+ startScheduler: false,
71
+ });
72
+ await first.boot({ env: "test", startScheduler: false });
73
+ expect(first.apiKeys?.persist).toBeTypeOf("function");
74
+
75
+ const created = await first.execute(
76
+ first.flow("keys.create")!,
77
+ {},
78
+ http.post("/keys").gate(member),
79
+ {
80
+ principal: {
81
+ plane: "user",
82
+ userId: "u_issuer",
83
+ scopes: new Set(["member"]),
84
+ verified: true,
85
+ },
86
+ },
87
+ );
88
+ expect(created.failure).toBeUndefined();
89
+ const secret = (created.output as { secret?: string } | undefined)?.secret;
90
+ expect(secret?.startsWith("oke_")).toBe(true);
91
+
92
+ const firstStore = first.bootResult?.store;
93
+ await first.stop();
94
+ await firstStore?.close();
95
+
96
+ resetIsolated();
97
+ const db2 = store.sql("app", { schema: {} });
98
+ on(
99
+ http.get("/secure").gate(member),
100
+ flow("secure.ping", {
101
+ effects: {},
102
+ do: () => ({ ok: true }),
103
+ }),
104
+ );
105
+ const second = oke({
106
+ name: "host-keys-second",
107
+ stores: [db2],
108
+ gate: {
109
+ auth: { secret: PEPPER, http: false },
110
+ policies: [member],
111
+ },
112
+ env: "test",
113
+ startScheduler: false,
114
+ });
115
+ await second.boot({ env: "test", startScheduler: false });
116
+ expect(second.apiKeys).not.toBe(first.apiKeys);
117
+ expect(second.apiKeys?.keys.size).toBeGreaterThan(0);
118
+
119
+ const conn = await second.bootResult?.store?.primarySql();
120
+ const rows = await conn?.query(`SELECT id, name FROM ${AUTH_TABLES.apiKeys}`);
121
+ expect(rows?.some((r) => r.name === "prod-ci")).toBe(true);
122
+
123
+ const res = await second.fetch(
124
+ new Request("http://localhost/secure", {
125
+ method: "GET",
126
+ headers: { authorization: `Bearer ${secret}` },
127
+ }),
128
+ );
129
+ expect(res.status).toBe(200);
130
+ expect(await res.json()).toEqual({ data: { ok: true }, error: null });
131
+
132
+ const secondStore = second.bootResult?.store;
133
+ await second.stop();
134
+ await secondStore?.close();
135
+ } finally {
136
+ if (prevDb === undefined) delete process.env.DATABASE_URL;
137
+ else process.env.DATABASE_URL = prevDb;
138
+ if (prevPglite === undefined) delete process.env.OKE_PGLITE_URL;
139
+ else process.env.OKE_PGLITE_URL = prevPglite;
140
+ await rm(dir, { recursive: true, force: true });
141
+ }
142
+ }, 20_000);
143
+ });
package/src/kernel/app.ts CHANGED
@@ -14,6 +14,7 @@ import {
14
14
  accumulateAdoptArgs,
15
15
  type AppRouteMap,
16
16
  type RoutesFromAdoptArgs,
17
+ type RoutesFromNamespace,
17
18
  type RuntimeRouteMap,
18
19
  } from "./adopt-routes.ts";
19
20
  // `./boot.ts` pulls in every element + driver module (vault, store, signal,
@@ -23,6 +24,7 @@ import {
23
24
  import type { BootOptions, BootResult, ElementRuntimes } from "./boot.ts";
24
25
  import type { CapabilityToken } from "./capability.ts";
25
26
  import type { AppAuthBinding } from "./auth-resolve.ts";
27
+ import type { ApiKeyStore } from "../auth/api-keys.ts";
26
28
  import type { AuthHttpMaterialization } from "../auth/bindings.ts";
27
29
  import type { WiredGateAuth } from "./app-auth.ts";
28
30
  import {
@@ -59,7 +61,8 @@ import {
59
61
  } from "../runtime/dev-request-log.ts";
60
62
  import { asBrowserJsonCodeBlock, httpNavGroups } from "../runtime/json-code-block.ts";
61
63
  import { resolveDurationMs } from "./elapsed.ts";
62
- import { fail } from "./errors.ts";
64
+ import { fail, throwOke } from "./errors.ts";
65
+ import { consumeRegisteredFlowUnits, type FlowUnitBag } from "./flow-units.ts";
63
66
  import {
64
67
  isFlowFailure,
65
68
  mergeHooks,
@@ -91,6 +94,8 @@ import {
91
94
  aiModelRegistry,
92
95
  aiPromptRegistry,
93
96
  channelTemplateRegistry,
97
+ clockRegistry,
98
+ gateRegistry,
94
99
  requiredEnvRegistry,
95
100
  secretRegistry,
96
101
  signalRegistry,
@@ -99,6 +104,7 @@ import {
99
104
  import {
100
105
  applyPrincipal,
101
106
  createElementPipelineHooks,
107
+ gateNamesOf,
102
108
  type PrincipalBag,
103
109
  type ResolvedPrincipal,
104
110
  } from "./pipeline.ts";
@@ -118,6 +124,7 @@ import {
118
124
  type RouterPreset,
119
125
  type SmartRouter,
120
126
  } from "./router.ts";
127
+ import { isPendingHttpPath } from "./http-path-pending.ts";
121
128
  import type {
122
129
  CdcTrigger,
123
130
  EveryTrigger,
@@ -299,6 +306,21 @@ export interface UnitHooks<D extends Record<string, unknown> = {}> {
299
306
  plug<P extends PluginDef>(pluginDef: P): UnitHooks<AccumulateDecorations<D, P>>;
300
307
  }
301
308
 
309
+ /**
310
+ * Module-augmentation slot filled by `src/flows/generated.ts`.
311
+ * Kernel tests do not import an app generated file, so this stays `{}`.
312
+ */
313
+ export interface RegisteredFlowUnits {}
314
+
315
+ /**
316
+ * `$routes` derived from {@link RegisteredFlowUnits} after `import generated`.
317
+ *
318
+ * @typeParam U - Augmented unit map
319
+ */
320
+ export type RoutesFromRegisteredUnits<U = RegisteredFlowUnits> = {
321
+ [K in keyof U]: U[K] extends Record<string, unknown> ? RoutesFromNamespace<U[K]> : never;
322
+ };
323
+
302
324
  /**
303
325
  * Application instance — adopts flows, routes HTTP, runs the pipeline.
304
326
  *
@@ -350,6 +372,8 @@ export interface OkeApp<D extends Record<string, unknown> = {}, R extends AppRou
350
372
  * Builtin auth binding after boot (session store + secret), when configured.
351
373
  */
352
374
  readonly authBinding: AppAuthBinding | undefined;
375
+ /** Shared API key store when `gate.auth` is enabled. */
376
+ readonly apiKeys: ApiKeyStore | undefined;
353
377
  /**
354
378
  * Attach a plugin app-wide. Scope is the attachment point.
355
379
  * Types accumulate — decorations are visible on downstream handlers.
@@ -435,6 +459,8 @@ export interface OkeApp<D extends Record<string, unknown> = {}, R extends AppRou
435
459
  * Takes effect only with {@link trustedInvoke}.
436
460
  */
437
461
  readonly revealPii?: boolean;
462
+ /** Forced RLS bag (Console / Call API). Operator omit = no stamp. */
463
+ readonly rls?: import("../drivers/pg-rls.ts").RlsIdentity;
438
464
  /** Parent WideEvent id when this execution was caused by another. */
439
465
  readonly parentId?: string;
440
466
  /** Explicit run / WideEvent id (defaults to a new UUID). */
@@ -562,11 +588,57 @@ function mergeAiOptions(
562
588
  };
563
589
  }
564
590
 
591
+ /**
592
+ * Refuse an unresolved path sentinel or a nameless HTTP flow.
593
+ *
594
+ * @param binding - HTTP binding
595
+ */
596
+ function assertHttpBindingReady(binding: Binding): void {
597
+ const trigger = binding.trigger;
598
+ if (trigger.kind !== "http") return;
599
+ if (isPendingHttpPath(trigger.path)) {
600
+ throwOke("HTTP_PATH_UNRESOLVED", {
601
+ flow: binding.flow.name || "(unnamed)",
602
+ method: trigger.method,
603
+ });
604
+ }
605
+ if (!binding.flow.name || binding.flow.name.startsWith("flow_")) {
606
+ throwOke("HTTP_FLOW_UNNAMED", {
607
+ method: trigger.method,
608
+ path: trigger.path,
609
+ });
610
+ }
611
+ }
612
+
613
+ /**
614
+ * Fold `generated.ts` units into `$routes` and `flowsByName`.
615
+ *
616
+ * @param units - Drained {@link registerFlowUnits} bag
617
+ * @param routes - Runtime `$routes`
618
+ * @param registerFlow - App flow index
619
+ */
620
+ function drainGeneratedUnits(
621
+ units: Record<string, FlowUnitBag>,
622
+ routes: RuntimeRouteMap,
623
+ registerFlow: (flowDef: AnyFlowDef) => void,
624
+ ): void {
625
+ if (Object.keys(units).length === 0) return;
626
+ const flows = accumulateAdoptArgs([units], routes);
627
+ for (const flowDef of flows) {
628
+ registerFlow(flowDef);
629
+ }
630
+ }
631
+
565
632
  /**
566
633
  * Create an application. Adopts bindings registered via {@link on}.
634
+ * Drains {@link registerFlowUnits} into `$routes` unless `registry: "ignore"`.
567
635
  *
568
636
  * @param options - App name and router preset
569
637
  */
638
+ export function oke(
639
+ options: OkeOptions & { readonly registry: "ignore" },
640
+ ): OkeApp<Record<string, never>, Record<string, never>>;
641
+ export function oke(options: OkeOptions): OkeApp<{}, RoutesFromRegisteredUnits>;
570
642
  export function oke(options: OkeOptions): OkeApp {
571
643
  const aot = options.aot !== false;
572
644
  const registry = options.registry ?? "consume";
@@ -576,11 +648,12 @@ export function oke(options: OkeOptions): OkeApp {
576
648
  : [...listBindings(), ...(options.bindings ?? [])];
577
649
  if (registry === "consume") resetBindings();
578
650
 
579
- // Same registry mode drains store.sql/store.files, vault.secret, signal(),
580
- // channel.<medium>().template(), and ai.model/prompt/embed/agent/mcpServer —
581
- // module-evaluation registries that mirror `on`'s trigger drain
582
- // (`listBindings`/`resetBindings` above). Explicit `options.stores` /
583
- // `secrets` / `signals` / `channel.templates` / `ai` are additive, never
651
+ // Same registry mode drains store.*, vault.secret, signal(), clock(),
652
+ // gate.policy/scope/rate, channel.<medium>().template(), and
653
+ // ai.model/prompt/embed/agent/mcpServer — module-evaluation registries
654
+ // that mirror `on`'s trigger drain (`listBindings`/`resetBindings` above).
655
+ // Explicit `options.stores` / `secrets` / `signals` / `clocks` /
656
+ // `gate.policies` / `channel.templates` / `ai` are additive, never
584
657
  // silently ignored — deduped by reference so an explicitly-passed decl
585
658
  // that is also in the registry is not doubled.
586
659
  const registrySnapshot =
@@ -590,6 +663,8 @@ export function oke(options: OkeOptions): OkeApp {
590
663
  secrets: [],
591
664
  requiredEnv: [],
592
665
  signals: [],
666
+ clocks: [],
667
+ gates: [],
593
668
  channelTemplates: [],
594
669
  aiModels: [],
595
670
  aiPrompts: [],
@@ -602,6 +677,8 @@ export function oke(options: OkeOptions): OkeApp {
602
677
  secrets: secretRegistry.slice(),
603
678
  requiredEnv: requiredEnvRegistry.slice(),
604
679
  signals: signalRegistry.slice(),
680
+ clocks: clockRegistry.slice(),
681
+ gates: gateRegistry.slice(),
605
682
  channelTemplates: channelTemplateRegistry.slice(),
606
683
  aiModels: aiModelRegistry.slice(),
607
684
  aiPrompts: aiPromptRegistry.slice(),
@@ -614,6 +691,8 @@ export function oke(options: OkeOptions): OkeApp {
614
691
  secretRegistry.length = 0;
615
692
  requiredEnvRegistry.length = 0;
616
693
  signalRegistry.length = 0;
694
+ clockRegistry.length = 0;
695
+ gateRegistry.length = 0;
617
696
  channelTemplateRegistry.length = 0;
618
697
  aiModelRegistry.length = 0;
619
698
  aiPromptRegistry.length = 0;
@@ -624,6 +703,8 @@ export function oke(options: OkeOptions): OkeApp {
624
703
  const effectiveStores = mergeUnique(options.stores, registrySnapshot.stores);
625
704
  const effectiveSecrets = mergeUnique(options.secrets, registrySnapshot.secrets);
626
705
  const effectiveSignals = mergeUnique(options.signals, registrySnapshot.signals);
706
+ const effectiveClocks = mergeUnique(options.clocks, registrySnapshot.clocks);
707
+ const effectivePolicies = mergeUnique(options.gate?.policies, registrySnapshot.gates);
627
708
  /**
628
709
  * Fold registered `vault.env.required` names into the vault boot options so
629
710
  * boot reports missing env vars in the same gap list as missing secrets.
@@ -658,16 +739,23 @@ export function oke(options: OkeOptions): OkeApp {
658
739
 
659
740
  // Resolve Gate bag early so auth HTTP Bindings join `adopted` + the router
660
741
  // before posture audit (same ensureBoot → doBoot path — never a side channel).
742
+ const mergedGate: GateOptions | undefined =
743
+ effectivePolicies.length > 0 ? { ...options.gate, policies: effectivePolicies } : options.gate;
661
744
  const gateConfig: ResolvedGateConfig = resolveGateConfig({
662
- gate: options.gate,
745
+ gate: mergedGate,
663
746
  env: options.env,
664
747
  });
665
748
 
666
- /** Retained for test harness / boot merges (includes auto-drained AI decls). */
667
- const $options: OkeOptions =
668
- effectiveAi === undefined || options.ai === effectiveAi
669
- ? options
670
- : { ...options, ai: effectiveAi };
749
+ /** Retained for test harness / boot merges (includes auto-drained decls). */
750
+ const $options: OkeOptions = {
751
+ ...options,
752
+ ...(effectiveAi !== undefined ? { ai: effectiveAi } : {}),
753
+ ...(effectiveStores.length > 0 ? { stores: effectiveStores } : {}),
754
+ ...(effectiveSecrets.length > 0 ? { secrets: effectiveSecrets } : {}),
755
+ ...(effectiveSignals.length > 0 ? { signals: effectiveSignals } : {}),
756
+ ...(effectiveClocks.length > 0 ? { clocks: effectiveClocks } : {}),
757
+ ...(mergedGate !== undefined ? { gate: mergedGate } : {}),
758
+ };
671
759
 
672
760
  const appHooks: HookMap = {};
673
761
  const unitHooks = new Map<string, HookMap>();
@@ -701,8 +789,19 @@ export function oke(options: OkeOptions): OkeApp {
701
789
  }
702
790
 
703
791
  const smart = createRouter<Binding>(options.router ?? "default");
792
+ const seenHttpRoutes = new Set<string>();
704
793
  for (const b of adopted) {
705
794
  if (b.trigger.kind === "http") {
795
+ assertHttpBindingReady(b);
796
+ const key = `${b.trigger.method} ${b.trigger.path}`;
797
+ if (seenHttpRoutes.has(key)) {
798
+ throwOke("HTTP_ROUTE_DUPLICATE", {
799
+ method: b.trigger.method,
800
+ path: b.trigger.path,
801
+ flow: b.flow.name || "(unnamed)",
802
+ });
803
+ }
804
+ seenHttpRoutes.add(key);
706
805
  smart.add(b.trigger.method, b.trigger.path, b);
707
806
  compiled.set(b, compileHttpBinding(b, aot));
708
807
  }
@@ -715,6 +814,16 @@ export function oke(options: OkeOptions): OkeApp {
715
814
  adopted.push(b);
716
815
  registerFlow(b.flow);
717
816
  if (b.trigger.kind === "http") {
817
+ assertHttpBindingReady(b);
818
+ const key = `${b.trigger.method} ${b.trigger.path}`;
819
+ if (seenHttpRoutes.has(key)) {
820
+ throwOke("HTTP_ROUTE_DUPLICATE", {
821
+ method: b.trigger.method,
822
+ path: b.trigger.path,
823
+ flow: b.flow.name || "(unnamed)",
824
+ });
825
+ }
826
+ seenHttpRoutes.add(key);
718
827
  smart.add(b.trigger.method, b.trigger.path, b);
719
828
  compiled.set(b, compileHttpBinding(b, aot));
720
829
  }
@@ -788,6 +897,11 @@ export function oke(options: OkeOptions): OkeApp {
788
897
  registerFlow(b.flow);
789
898
  }
790
899
 
900
+ if (registry !== "ignore") {
901
+ const generatedUnits = consumeRegisteredFlowUnits();
902
+ drainGeneratedUnits(generatedUnits, routes, registerFlow);
903
+ }
904
+
791
905
  // --- boot (vault → store → signal → clock → channel → AI → runs → caps) ---
792
906
  let bootResult: BootResult | undefined;
793
907
  let bootPromise: Promise<BootResult> | undefined;
@@ -963,7 +1077,7 @@ export function oke(options: OkeOptions): OkeApp {
963
1077
  vault: withRequiredEnv(overrides?.vault ?? options.vault),
964
1078
  gates: [...baseGates, ...authGates],
965
1079
  signals: overrides?.signals ?? effectiveSignals,
966
- clocks: overrides?.clocks ?? options.clocks,
1080
+ clocks: overrides?.clocks ?? effectiveClocks,
967
1081
  stores: overrides?.stores ?? effectiveStores,
968
1082
  channel: overrides?.channel ?? effectiveChannel,
969
1083
  ai: overrides?.ai ?? effectiveAi,
@@ -1019,7 +1133,7 @@ export function oke(options: OkeOptions): OkeApp {
1019
1133
 
1020
1134
  const baseSecrets = overrides?.secrets ?? effectiveSecrets;
1021
1135
  const baseSignals = overrides?.signals ?? effectiveSignals;
1022
- const baseClocks = overrides?.clocks ?? options.clocks ?? [];
1136
+ const baseClocks = overrides?.clocks ?? effectiveClocks;
1023
1137
  const baseChannel = overrides?.channel ?? effectiveChannel;
1024
1138
  const mergedCatalog = mergeTemplateCatalogs(baseChannel?.catalog, ...pluginChannelCatalogs);
1025
1139
 
@@ -1065,6 +1179,10 @@ export function oke(options: OkeOptions): OkeApp {
1065
1179
  };
1066
1180
  const result = await bootApplication(merged);
1067
1181
  bootResult = result;
1182
+ if (gateConfig.auth?.apiKeyStore && result.store) {
1183
+ const { bindHostApiKeySqlFromStore } = await import("../auth/api-key-sql.ts");
1184
+ await bindHostApiKeySqlFromStore(result.store, gateConfig.auth.apiKeyStore);
1185
+ }
1068
1186
  // otp() provider / app mode capability — fail loud at boot, never silent downgrade.
1069
1187
  if (result.channel) {
1070
1188
  const { assertOtpPluginCapability } = await import("../auth/otp-capability.ts");
@@ -1138,6 +1256,8 @@ export function oke(options: OkeOptions): OkeApp {
1138
1256
  readonly bypassGates?: boolean;
1139
1257
  /** Console Call API — cleartext store PII. Requires {@link trustedInvoke}. */
1140
1258
  readonly revealPii?: boolean;
1259
+ /** Forced RLS bag (Console / Call API). Operator omit = no stamp. */
1260
+ readonly rls?: import("../drivers/pg-rls.ts").RlsIdentity;
1141
1261
  /** Frozen origin identity for {@link Fx.principal} across `fx.call`. */
1142
1262
  readonly originPrincipal?: FxPrincipal;
1143
1263
  /** Explicit locale override for {@link Fx.t} / channel sends. */
@@ -1189,6 +1309,7 @@ export function oke(options: OkeOptions): OkeApp {
1189
1309
  readonly trustedInvoke?: boolean;
1190
1310
  readonly bypassGates?: boolean;
1191
1311
  readonly revealPii?: boolean;
1312
+ readonly rls?: import("../drivers/pg-rls.ts").RlsIdentity;
1192
1313
  readonly originPrincipal?: FxPrincipal;
1193
1314
  readonly locale?: string;
1194
1315
  readonly parentId?: string;
@@ -1251,6 +1372,7 @@ export function oke(options: OkeOptions): OkeApp {
1251
1372
  userId: options.fx?.auth?.userId ?? null,
1252
1373
  scopes: new Set(options.fx?.auth?.scopes ?? []),
1253
1374
  verified: options.fx?.auth?.verified,
1375
+ apiKeyId: options.fx?.auth?.apiKeyId,
1254
1376
  },
1255
1377
  operator: { id: options.fx?.operator?.id ?? null },
1256
1378
  };
@@ -1279,7 +1401,8 @@ export function oke(options: OkeOptions): OkeApp {
1279
1401
  : {}),
1280
1402
  verifyBearer:
1281
1403
  binding && wiredAuth
1282
- ? async (token) => wiredAuth.verifyBearerOrApiKey(binding, token, apiKeyStore)
1404
+ ? (token, request) =>
1405
+ wiredAuth.verifyBearerOrApiKey(binding, token, apiKeyStore, request)
1283
1406
  : undefined,
1284
1407
  // Phase 1a: opt-in cookie → Bearer when Authorization is absent.
1285
1408
  resolveToken:
@@ -1336,9 +1459,13 @@ export function oke(options: OkeOptions): OkeApp {
1336
1459
  catalogs: loadMessages().getMessageCatalogs(),
1337
1460
  ...options.fx?.i18n,
1338
1461
  },
1339
- ...(principals ? { auth: principals.auth as FxAuth, operator: principals.operator } : {}),
1462
+ ...(principals ? { auth: principals.auth, operator: principals.operator } : {}),
1463
+ apiKeyStore: gateConfig.auth?.apiKeyStore,
1340
1464
  ...(extras?.originPrincipal ? { principal: extras.originPrincipal } : {}),
1341
1465
  ...(extras?.trustedInvoke === true && extras.revealPii === true ? { revealPii: true } : {}),
1466
+ rlsGateNames: gateNamesOf(trigger),
1467
+ rlsBypass: extras?.bypassGates === true || flowDef.plane === "operator",
1468
+ ...(extras?.rls ? { rls: extras.rls } : {}),
1342
1469
  ...(capability ? { capability } : {}),
1343
1470
  ...(booted
1344
1471
  ? {
@@ -1487,6 +1614,7 @@ export function oke(options: OkeOptions): OkeApp {
1487
1614
  }
1488
1615
  return await invoke(ctx.input);
1489
1616
  } catch (err) {
1617
+ if (isFlowFailure(err)) return err;
1490
1618
  // A durable sleep that has not yet elapsed suspends the run — this
1491
1619
  // is a park, not a pipeline failure, so it must not throw upward.
1492
1620
  if (flowDef.durable && journalSession && isJournalSuspend(err)) {
@@ -1663,6 +1791,7 @@ export function oke(options: OkeOptions): OkeApp {
1663
1791
  get authBinding() {
1664
1792
  return authBinding;
1665
1793
  },
1794
+ apiKeys: gateConfig.auth?.apiKeyStore,
1666
1795
  async resumeDurable(now) {
1667
1796
  const t = now ?? bootResult?.clock?.now() ?? options.fx?.now?.() ?? Date.now();
1668
1797
  const { store, instanceId, leaseMs } = activeJournal();
@@ -6,7 +6,7 @@
6
6
  * separately in the pipeline (`allowTestPrincipals`).
7
7
  */
8
8
 
9
- import { authenticateApiKey, type ApiKeyStore } from "../auth/api-keys.ts";
9
+ import { authenticateApiKey, clientIpFromRequest, type ApiKeyStore } from "../auth/api-keys.ts";
10
10
  import type { ApiKeyRow } from "../auth/tables.ts";
11
11
  import {
12
12
  createSessionStore,
@@ -89,7 +89,9 @@ export function claimsToPrincipal(claims: AccessClaims): ResolvedPrincipal {
89
89
 
90
90
  /**
91
91
  * Map an authenticated API key row to the pipeline principal shape.
92
- * Uses the bare key id so Access `runTouchesKey` matches `WideEvent.principal`.
92
+ *
93
+ * The key is the issuer with fewer gates: `userId` / `operatorId` is
94
+ * {@link ApiKeyRow.creatorId}. The credential id is {@link ResolvedPrincipal.apiKeyId}.
93
95
  *
94
96
  * @param row - Authenticated key
95
97
  */
@@ -97,7 +99,7 @@ export function apiKeyRowToPrincipal(row: ApiKeyRow): ResolvedPrincipal {
97
99
  if (row.plane === "operator") {
98
100
  return {
99
101
  plane: "operator",
100
- operatorId: row.id,
102
+ operatorId: row.creatorId,
101
103
  userId: null,
102
104
  scopes: row.scopes,
103
105
  verified: true,
@@ -106,7 +108,7 @@ export function apiKeyRowToPrincipal(row: ApiKeyRow): ResolvedPrincipal {
106
108
  }
107
109
  return {
108
110
  plane: "user",
109
- userId: row.id,
111
+ userId: row.creatorId,
110
112
  scopes: row.scopes,
111
113
  verified: true,
112
114
  apiKeyId: row.id,
@@ -119,17 +121,20 @@ export function apiKeyRowToPrincipal(row: ApiKeyRow): ResolvedPrincipal {
119
121
  * @param auth - Session binding
120
122
  * @param token - Raw Bearer token
121
123
  * @param apiKeys - Optional key store
124
+ * @param request - Incoming request (allowlist IP is read here, not in `oke()`)
122
125
  */
123
126
  export async function verifyBearerOrApiKey(
124
127
  auth: AppAuthBinding,
125
128
  token: string,
126
129
  apiKeys?: ApiKeyStore,
130
+ request?: Request,
127
131
  ): Promise<ResolvedPrincipal> {
128
132
  try {
129
133
  return await verifyBearerToken(auth, token);
130
134
  } catch (err) {
131
135
  if (apiKeys) {
132
- const row = await authenticateApiKey(apiKeys, token, auth.now);
136
+ const ip = request ? clientIpFromRequest(request) : undefined;
137
+ const row = await authenticateApiKey(apiKeys, token, { now: auth.now, ip });
133
138
  if (row) return apiKeyRowToPrincipal(row);
134
139
  }
135
140
  throw err;