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
@@ -17,6 +17,7 @@ oke --help # command list (also what bare `oke` prints whe
17
17
 
18
18
  oke dev # watch · hot reload · Console :6533 · app :6530 · MCP :6535 · docs MCP :6536
19
19
  # → always Docker Compose under docker/ + host Bun
20
+ # → regenerates flows/generated.ts when the flows tree changes
20
21
  # → client types + domain schema push on save
21
22
  # → Console UI hot-reloads from source (okengine checkout; no bun run build)
22
23
  oke dev --no-db-push # opt out of auto oke db push on schema input save
@@ -48,7 +49,7 @@ oke schema generate # core + plugin stubs → .oke/schema/oke.ts (-
48
49
  oke db push # domain schema.ts → live DB (dev; drizzle-kit)
49
50
  oke db generate # versioned SQL under drizzle/ (review)
50
51
  oke db migrate # apply migrations (explicit; never auto in prod)
51
- oke db seed # this app's defineSeed (name + essential + env); never at boot
52
+ oke db seed # this app's defineSeed (name + essential + env) standard CLI path
52
53
  oke db seed --env prod --force # CI: skip prod confirmation prompt
53
54
  oke db studio # drizzle-kit Studio (long-running)
54
55
  oke vault set STRIPE_KEY # also: list · import .env · key rotate
@@ -112,24 +113,26 @@ not spawn a second stack. Non-TTY / CI keeps the classic help listing
112
113
  `oke dev` always starts Compose (requires a running Docker daemon). Boot chrome
113
114
  prints immediately (wordmark + Starting + profile), then streams background work
114
115
  (compose up, vault, per-service health, AI probe). Status ● is green ready ·
115
- yellow pending/loading · red error · dim idle. Compose health keeps polling
116
+ yellow pending/loading · red error · dim idle. After bind, Backend · Console ·
117
+ MCP · Docs MCP each get a box (URL + one-line purpose). Compose health keeps polling
116
118
  (`docker compose ps -a`); AI ● tracks model phase while the AI container is up.
117
119
  Boot does not wait for the model to become ready. A successful session writes
118
120
  `.oke/dev.json` (pid · ports · startedAt) and clears it on stop. Durable local
119
- markers live in `.oke/state.json` (`seededAt` + `seed` after the one-shot
120
- prompt for this app's `defineSeed({ name })`) — not in the session lock. Console session signing uses
121
+ markers live in `.oke/state.json` not in the session lock. Console session signing uses
121
122
  `.oke/console.secret` (or `OKE_CONSOLE_SECRET`); that is not a Vault secret.
122
123
 
123
124
  On a TTY, Ink keyboard controls stay active after boot (`useInput` — same
124
125
  shortcuts as before). Press `r` to clear the log pane and reprint the latest
125
126
  board.
126
127
 
127
- | Key | Action |
128
- | --------- | ----------------------------------- |
129
- | `?` | help |
130
- | `r` | refresh — clear logs, show latest ● |
131
- | `q` | quit `oke dev` |
132
- | `u` / `x` | compose up / stop (whole stack) |
128
+ | Key | Action |
129
+ | --- | ----------------------------------- |
130
+ | `?` | help |
131
+ | `r` | refresh — clear logs, show latest ● |
132
+ | `s` | `oke db seed` |
133
+ | `q` | quit `oke dev` |
134
+ | `u` | compose up (whole stack) |
135
+ | `x` | compose stop (whole stack) |
133
136
 
134
137
  Compose stays quiet unless it fails. Process-local boot notices print once under
135
138
  the hero (not once per process).
@@ -5,7 +5,7 @@ icon: "MonitorSmartphone"
5
5
  source: "docs/spec/unified-theory.md"
6
6
  ---
7
7
 
8
- `okengine/client` is how a browser, CLI, or another service calls your app's flows. Adopt the unit, take `typeof app`, and `api.notes.get({ id })` is fully typed — same contracts the server already has, no separate schema project.
8
+ `okengine/client` is how a browser, CLI, or another service calls your app's flows. Import the generated barrel, take `typeof app`, and `api.notes.get({ id })` is fully typed — same contracts the server already has, no separate schema project.
9
9
 
10
10
  <Callout title="The one rule">
11
11
  Treat every call as a result envelope: `{ data, error }`. Flow failures are values you switch on
@@ -21,19 +21,22 @@ source: "docs/spec/unified-theory.md"
21
21
  ### Adopt flows and export `App`
22
22
 
23
23
  ```typescript title="src/app.ts"
24
- import { oke } from "okengine";
25
- import * as main from "./flows/main";
24
+ import "@/core";
25
+ import "@/flows/generated";
26
+ import { oke } from "okengine/http";
26
27
 
27
- export const app = oke({ name: "standard" }).adopt({ main });
28
+ export const app = oke({ name: "notes" });
28
29
  export type App = typeof app;
29
30
  ```
30
31
 
32
+ `.adopt({ notes })` is optional and additive.
33
+
31
34
  </Step>
32
35
 
33
36
  <Step>
34
37
  ### Create the client
35
38
 
36
- Same repo — pass the app value so HTTP triggers hit REST (method + path from adopt):
39
+ Same repo — pass the app value so HTTP triggers hit REST (method + path from `$routes`):
37
40
 
38
41
  ```typescript title="client"
39
42
  import { createClient } from "okengine/client";
@@ -114,7 +117,7 @@ oke client add https://api.example.com --out ./types/oke-client.d.ts
114
117
  | Incomplete proxy path (`api.notes()` with no flow) | Result error: `Incomplete path: api.notes(…)` |
115
118
 
116
119
  ```typescript
117
- // REST — createClient(app, url) saw method/path from on(http.get("/notes/:id"), )
120
+ // REST — method/path from the bound HTTP trigger (file tree or explicit)
118
121
  await api.notes.get({ id: "n_1" }); // GET /notes/n_1
119
122
 
120
123
  // RPC — untriggered flow named notes.stats
@@ -177,6 +180,9 @@ React: `useSession(api, session)` from `okengine/client-react`.
177
180
  | HTTP **401** | `refresh()` runs **once**, then the same call retries |
178
181
  | HTTP **403** / **429** | No refresh — decode the failure envelope as usual |
179
182
 
183
+ `getToken` may return a session access token or an API key secret — both are Bearer.
184
+ See [Gate](/docs/elements/gate#api-keys). `refresh` applies to sessions only.
185
+
180
186
  **Consequence:** `refresh` must mutate whatever `getToken` reads. Returning a new string alone does
181
187
  nothing if storage was not updated.
182
188
 
@@ -210,16 +216,16 @@ core today: cookie jars or plugin `.client()` decorations. Browser apps: also se
210
216
  outcome by calling a Flow that uses it — or by handling a gate denial on that call.
211
217
  </Callout>
212
218
 
213
- | Element | On the client | How |
214
- | --------------------------------- | ------------- | --------------------------------------------------------------------------------------- |
215
- | [Flow](/docs/elements/flow) | Direct | `api.unit.flow(input)` — the only public surface |
216
- | [Gate](/docs/elements/gate) | Indirect | Bearer via `auth`; denials as `Unauthorized` / `Forbidden` / `RateLimited` |
217
- | [Store](/docs/elements/store) | Via Flows | `fx.store` inside Flows; `store.resource` + `on(http.resource…)` → five Flows you adopt |
218
- | [Signal](/docs/elements/signal) | Via Flows | Emit/consume server-side; no subscribe API on `okengine/client` yet |
219
- | [Clock](/docs/elements/clock) | Via Flows | Schedules fire on the server — the client never ticks a clock |
220
- | [Vault](/docs/elements/vault) | Via Flows | Secrets stay server-side; never ship them to the browser package |
221
- | [Channel](/docs/elements/channel) | Via Flows | `fx.send` in a Flow — the client does not send email/SMS/push |
222
- | [AI](/docs/elements/ai) | Via Flows | `fx.ask` / `fx.run` inside a Flow; the client gets that Flow’s `out` |
219
+ | Element | On the client | How |
220
+ | --------------------------------- | ------------- | ------------------------------------------------------------------------------------------ |
221
+ | [Flow](/docs/elements/flow) | Direct | `api.unit.flow(input)` — the only public surface |
222
+ | [Gate](/docs/elements/gate) | Indirect | Bearer via `auth`; denials as `Unauthorized` / `Forbidden` / `RateLimited` |
223
+ | [Store](/docs/elements/store) | Via Flows | `fx.store` inside Flows; `store.resource` + `on(http.resource…)` → five Flows on `$routes` |
224
+ | [Signal](/docs/elements/signal) | Via Flows | Emit/consume server-side; no subscribe API on `okengine/client` yet |
225
+ | [Clock](/docs/elements/clock) | Via Flows | Schedules fire on the server — the client never ticks a clock |
226
+ | [Vault](/docs/elements/vault) | Via Flows | Secrets stay server-side; never ship them to the browser package |
227
+ | [Channel](/docs/elements/channel) | Via Flows | `fx.send` in a Flow — the client does not send email/SMS/push |
228
+ | [AI](/docs/elements/ai) | Via Flows | `fx.ask` / `fx.run` inside a Flow; the client gets that Flow’s `out` |
223
229
 
224
230
  ### Store resources
225
231
 
@@ -227,7 +233,7 @@ Mount a resource, adopt the returned ops, then call the five Flows like any othe
227
233
 
228
234
  ```typescript
229
235
  const notesR = store.resource(db, notes, {/* in, out, list */});
230
- const mounted = on(http.resource("/notes", notesR.all()).gate.public.live());
236
+ const mounted = on(http.resource("/notes", notesR.all()).public().live());
231
237
  // .adopt({ notes: mounted }) →
232
238
  const page = await api.notes.list({ limit: 20 });
233
239
  const more = await page.next();
@@ -280,9 +286,9 @@ Budget: the `./client` export stays under the measured client-runtime cap (hard
280
286
 
281
287
  <Accordion title="api.main.health is not a function / type error">
282
288
 
283
- Confirm the flow is `export`ed from the module you `.adopt({ main })`, and that `createClient` is
284
- typed with that `App` (or ambient `Register` after `oke-client.d.ts` regenerates). Restart
285
- `oke dev` after renaming exports.
289
+ Confirm the Flow is `export`ed from a generated unit (`import "@/flows/generated"` then `oke({ name })`), or from a module you still `.adopt({ main })`.
290
+
291
+ Type `createClient` with that `App` (or ambient `Register` after `oke-client.d.ts` regenerates). Restart `oke dev` after renaming exports.
286
292
 
287
293
  </Accordion>
288
294
 
@@ -321,7 +327,8 @@ calls `api.auth.refresh({ refreshToken })`. Re-login when rotation fails or no r
321
327
 
322
328
  ## Learn more
323
329
 
324
- - [Basic usage](/docs/get-started/basic-usage) — adopt client test loop
330
+ - [Project structure](/docs/get-started/project-structure) — folders are the URL; `$routes` without `.adopt()`
331
+ - [Basic usage](/docs/get-started/basic-usage) — generated barrel → client → test loop
325
332
  - [Gate](/docs/elements/gate) — policies, `gate.public`, denials
326
333
  - [Store](/docs/elements/store) — `store.resource` and list query language
327
334
  - [Flow](/docs/elements/flow) — `in` / `out` / `errors` and `fx.fail`
@@ -14,11 +14,16 @@ flow("orders.example", {
14
14
  });
15
15
  ```
16
16
 
17
+ <Callout title="Extracted helpers">
18
+ Type a helper's `fx` parameter as `Fx` (`import type {Fx} from "okengine"`). A narrower structural
19
+ type will not match `store()` overloads. See [Flow](/docs/elements/flow).
20
+ </Callout>
21
+
17
22
  ## Stores
18
23
 
19
24
  | Signature | Records | Returns |
20
25
  | ------------------------------------------------------------------- | ------------ | ------------------------------------------------------------------------------------------- |
21
- | `fx.store(sqlDecl).select().from(t)…` | `read` | rows (`where` · `orderBy` · `limit` · `offset` chainable) |
26
+ | `fx.store(sqlDecl).select().from(t)…` | `read` | inferred rows (`where` · `orderBy` · `limit` · `offset` chainable) |
22
27
  | `fx.store(sqlDecl).insert(t).values(v)` | `write` | `Promise<void>` |
23
28
  | `fx.store(sqlDecl).update(t).set(v).where(…)` | `write` | `Promise<void>` |
24
29
  | `fx.store(sqlDecl).delete(t).where(…)` | `write` | `Promise<void>` |
@@ -72,16 +77,21 @@ on(
72
77
 
73
78
  ## Flows
74
79
 
75
- | Signature | Records | Returns / notes |
76
- | ---------------------------- | ------- | ----------------------------------------------------- |
77
- | `fx.call(flow, input?)` | `call` | The callee's `out` — runs through the same pipeline |
78
- | `fx.step(name, fn)` | — | Durable step: replays from the journal, never re-runs |
79
- | `fx.all([...thunks])` | — | Parallel; first rejection aborts siblings |
80
- | `fx.race([...thunks])` | — | First settle wins; losers aborted |
81
- | `fx.retry(fn, opts?)` | — | Exponential backoff + jitter (plain Promise) |
82
- | `fx.using(acq, rel, use)` | — | `release` runs once on settle or ambient abort |
83
- | `fx.signal` | — | Ambient `AbortSignal` for the current branch |
84
- | `fx.fail(code, data, opts?)` | — | Typed failure value (`opts.message` overrides) |
80
+ | Signature | Records | Returns / notes |
81
+ | ------------------------------------------------------------------------------ | ----------------------- | -------------------------------------------------------------------------------------------------- |
82
+ | `fx.call(flow, input?)` | `call` | The callee's `out` — runs through the same pipeline |
83
+ | `fx.step(name, fn)` | — | Durable step: replays from the journal, never re-runs |
84
+ | `fx.all([...thunks])` | — | Parallel; first rejection aborts siblings |
85
+ | `fx.race([...thunks])` | — | First settle wins; losers aborted |
86
+ | `fx.retry(fn, opts?)` | — | Exponential backoff + jitter (plain Promise) |
87
+ | `fx.using(acq, rel, use)` | — | `release` runs once on settle or ambient abort |
88
+ | `fx.signal` | — | Ambient `AbortSignal` for the current branch |
89
+ | `fx.fail(code, data, opts?)` | — | Typed failure value (`opts.message` overrides) |
90
+ | `fx.auth.createApiKey({ name, scopes, expiresIn?, ipAllowlist?, rateLimit? })` | `write` `auth:api-keys` | Secret once. Creator is live `userId` / `scopes`. Session only. `ipAllowlist` is IPs or hostnames. |
91
+ | `fx.auth.listApiKeys()` | `read` `auth:api-keys` | Keys this session minted |
92
+ | `fx.auth.revokeApiKey(id)` | `write` `auth:api-keys` | Owner only |
93
+ | `fx.auth.rotateApiKey(id)` | `write` `auth:api-keys` | New secret once. Owner only |
94
+ | `fx.auth.updateApiKey(id, …)` | `write` `auth:api-keys` | Name / scopes / expiry / allowlist / rate. Re-attenuates |
85
95
 
86
96
  `fx.call` starts the callee with an **empty** `fx.auth` (fail-closed for authorization). For
87
97
  audit/attribution only, read `fx.principal` — it propagates the originating identity without
@@ -265,15 +275,16 @@ Built-in `errors.*` / `oke.*` catalogs localize `fx.fail` messages and thrown
265
275
 
266
276
  ## Principals
267
277
 
268
- | Property | Shape |
269
- | -------------- | ------------------------------------------------------------------------- |
270
- | `fx.auth` | `{ userId: string \| null, scopes: Set<string>, verified?: boolean }` |
271
- | `fx.operator` | `{ id: string \| null }` — Console plane |
272
- | `fx.principal` | Read-only origin: `userId`, `operatorId`, `scopes`, `verified?`, `plane?` |
273
- | `fx.tenant` | `{ id: string \| null }` — active tenant |
278
+ | Property | Shape |
279
+ | -------------- | -------------------------------------------------------------------------- |
280
+ | `fx.auth` | `{ userId, scopes, verified?, apiKeyId? }` plus key methods (session only) |
281
+ | `fx.operator` | `{ id: string \| null }` — Console plane |
282
+ | `fx.principal` | Read-only origin: `userId`, `operatorId`, `scopes`, `verified?`, `plane?` |
283
+ | `fx.tenant` | `{ id: string \| null }` — active tenant |
274
284
 
275
285
  **Consequence:** use `fx.auth` / gates for authorization; use `fx.principal` only when a callee
276
- must log who started the call chain.
286
+ must log who started the call chain. A key Bearer sets `userId` to the issuer and `apiKeyId`
287
+ to the key — see [Gate](/docs/elements/gate#api-keys).
277
288
 
278
289
  ## Not on `fx`
279
290
 
@@ -41,13 +41,13 @@ Our path is concrete: a booking name containing "ignore previous instructions an
41
41
  #### 10.4 Remaining closures
42
42
 
43
43
  - **`invoke-as` is attenuated** exactly like an API key: an operator cannot assume a scope set they could not grant. Impersonating a real user is development-only. Console `POST /console/flows/invoke` runs the target flow on a bound host under the selected identity’s scopes (`trustedInvoke` is in-process only — never from public HTTP); when the host adapter is unbound, invoke fails closed.
44
- - **Store SQL browse** masks classified PII by default. `QUERY /console/store/query` with `revealPii: true` returns cleartext for the current page and writes an audited `console.store.query.reveal` log (operator, ref, child). The SQL query console (`POST /console/store/sql`) stays masked unless `revealPii: true`, which writes `console.store.sql.reveal` (operator, ref). Call API (`POST /console/flows/invoke`) masks the same classified field names on the handler response unless `revealPii: true`, which writes `console.flows.invoke.reveal` and opens the host store session with cleartext so columns the handler reads (for example `ownerEmail`) are not already `[redacted]`. `asGate` (a Manifest policy gate or `public`) sets `oke.gate` and `row_security` on postgres / pglite and writes `console.store.sql.asGate`; Operator (omit `asGate`) still bypasses RLS. Per-cell `POST /console/store/reveal` remains available. The Console auth schema (`sql:oke_console` — `oke_operators`, sessions, roles, keys) is hidden from Store browse unless `OKE_CONSOLE_AUTH_STORE=1`. When listed it is **read-only**; edit / delete / raw SQL are refused so operator-plane rows are never joined to `public`.
44
+ - **Store SQL browse** masks classified PII by default. `QUERY /console/store/query` with `revealPii: true` returns cleartext for the current page and writes an audited `console.store.query.reveal` log (operator, ref, child). The SQL query console (`POST /console/store/sql`) stays masked unless `revealPii: true`, which writes `console.store.sql.reveal` (operator, ref). Call API (`POST /console/flows/invoke`) masks the same classified field names on the handler response unless `revealPii: true`, which writes `console.flows.invoke.reveal` and opens the host store session with cleartext so columns the handler reads (for example `ownerEmail`) are not already `[redacted]`. `{ asGate, asUserId }` on SQL, browse, row edit, and Call API stamps `oke.gate()` / `oke.user()` / `oke.has_scope()` on postgres / pglite (one identity bag; policy-only leaves `oke.user()` empty — never `gate:${name}`); Operator / omit / `bypassGates` still bypasses RLS. The response may include `rls: { gate, userId, applied }`. Catalog DDL stays Operator. Per-cell `POST /console/store/reveal` remains available. The Console auth schema (`sql:oke_console` — `oke_operators`, sessions, roles, keys) is hidden from Store browse unless `OKE_CONSOLE_AUTH_STORE=1`. When listed it is **read-only**; edit / delete / raw SQL are refused so operator-plane rows are never joined to `public`.
45
45
  - **Host WideEvent ingest** (`POST /console/runs/ingest`, `oke dev` only) is secret-gated and never echoes the event body. Operators read runs only through `projectRun` / `maskWideEventForConsole` on `GET /console/runs` and `/console/live` — the same PII boundary as Console-local runs.
46
46
  - **Store SQL performance** (`QUERY /console/store/sql/stats`, `/locks`, `POST /console/store/sql/advise`) is engine-native query text — not Store browse masking. Statements show `pg_stat_statements` fingerprints (`$n`). Live `pg_stat_activity.query` is collapsed until `revealPii: true`, which writes `console.store.sql.stats.reveal` (operator, ref). Named limitation `StoreSqlStatsQueryTextGap`. This surface does not terminate sessions or call `pg_stat_statements_reset`.
47
47
  - **Store KV performance** (`QUERY /console/store/kv/stats`) is Redis-wire `INFO` / `COMMANDSTATS` / `SLOWLOG` / `LATENCY` — not Store browse. INFO is instance-wide (`StoreKvStatsServerWideGap`). SLOWLOG args are keys and values — collapsed until `revealPii: true`, which writes `console.store.kv.stats.reveal` (operator, ref). Named limitation `StoreKvStatsSlowlogArgsGap`. `memory` returns `KvStatsUnsupported`. This surface does not run `MONITOR` and does not invent a hot-key table.
48
48
  - **Runs SQL** (`POST /console/runs/query`, Observability SQL tab) is operator-session, read-only, 5s timeout, 1000-row cap. DuckDB filesystem access is disabled for the statement. Masking is column-key only (`dim_*` and JSON blobs `input` / `output` / `logs` / `dimensions`). Aliases and expressions can leak classified values — named limitation `RunsQueryPiiProjectionGap`. This is **not** the `projectRun` guarantee. `revealPii: true` writes `console.runs.query.reveal`. Flow `fx.runs.query` stays unrestricted app SQL.
49
49
  - **Plugin panels** run in a sandboxed iframe without `allow-same-origin`, communicating only over a `postMessage` bridge with their own CSP, no access to the operator session token, and exposure limited to that plugin's declared flows.
50
- - **Session and framing:** `frame-ancestors 'none'`, `SameSite=Strict`, step-up authentication before destructive actions. An expired operator session redirects to Sign in with `?next=` set to the current `/overview`, `/flows`, `/store`, `/vault`, or `/observability` href (search included); after login the operator returns there. `/monitoring` is rewritten to `/observability`. Any other `next` (including `/units`) is dropped so the gate cannot be used as an open redirect. Unknown Console paths are 404.
50
+ - **Session and framing:** `frame-ancestors 'none'`, `SameSite=Strict`, step-up authentication before destructive actions. An expired operator session redirects to Sign in with `?next=` set to the current `/overview`, `/flows`, `/store`, `/vault`, `/access`, or `/observability` href (search included); after login the operator returns there. `/monitoring` is rewritten to `/observability`. Any other `next` (including `/units`) is dropped so the gate cannot be used as an open redirect. Unknown Console paths are 404.
51
51
  - **Secret write path:** TLS required, no autocomplete, never echoed, never logged, not retained in browser memory after submission. Vault set / rotate / rotate-master use typed confirm (`SET` / `ROTATE` / `ROTATE_MASTER`). The Console never accepts a master key in the HTTP body (`OKE_VAULT_MASTER_KEY` only). `GET /console/vault/audit/verify` is read-only (no typed phrase).
52
52
  - **The setup claim code** is rate-limited and compared in constant time. It is
53
53
  printed on the `oke dev` board (TTY only — never on `GET /console/setup/status`)
@@ -0,0 +1,47 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import { createApiKey, createApiKeyStore } from "./api-keys.ts";
3
+ import { bindApiKeySqlPersist, hydrateApiKeyStore, type ApiKeySqlExec } from "./api-key-sql.ts";
4
+
5
+ describe("api-key SQL persist", () => {
6
+ test("hydrate + persist round-trip", async () => {
7
+ const rows = new Map<string, Record<string, unknown>>();
8
+ const sql: ApiKeySqlExec = {
9
+ async execute(_q, params) {
10
+ const id = String(params[0]);
11
+ rows.set(id, {
12
+ id,
13
+ plane: params[1],
14
+ hash: params[2],
15
+ name: params[3],
16
+ scopes: params[4],
17
+ expires_at: params[5],
18
+ rate_limit: params[6],
19
+ ip_allowlist: params[7],
20
+ creator_id: params[8],
21
+ creator_scopes: params[9],
22
+ created_at: params[10],
23
+ last_used_at: params[11],
24
+ revoked_at: params[12],
25
+ });
26
+ },
27
+ async all() {
28
+ return [...rows.values()];
29
+ },
30
+ };
31
+ const store = createApiKeyStore();
32
+ bindApiKeySqlPersist(store, sql);
33
+ const created = await createApiKey(store, {
34
+ plane: "user",
35
+ name: "sql",
36
+ scopes: ["member"],
37
+ creatorId: "u1",
38
+ creatorScopes: ["member"],
39
+ id: "key_sql",
40
+ });
41
+ const other = createApiKeyStore();
42
+ await hydrateApiKeyStore(sql, other);
43
+ expect(other.keys.get("key_sql")?.name).toBe("sql");
44
+ expect(other.keys.get("key_sql")?.creatorId).toBe("u1");
45
+ expect(other.keys.get("key_sql")?.hash).toBe(created.row.hash);
46
+ });
47
+ });
@@ -0,0 +1,254 @@
1
+ /**
2
+ * SQL persist / hydrate for {@link ApiKeyRow} on `oke_api_keys`.
3
+ *
4
+ * Host path: public schema on the app `store.sql()` connection
5
+ * (`sharedSqlConn` / `DATABASE_URL`). Console attaches that same
6
+ * {@link ApiKeyStore} — it does not own a second key table.
7
+ */
8
+
9
+ import type { SqlConnection } from "../drivers/types.ts";
10
+ import type { ApiKeyRow } from "./tables.ts";
11
+ import { AUTH_TABLES } from "./tables.ts";
12
+ import type { ApiKeyStore } from "./api-keys.ts";
13
+
14
+ /** Minimal SQL executor for the auth key table. */
15
+ export interface ApiKeySqlExec {
16
+ execute(sql: string, params: readonly unknown[]): Promise<void>;
17
+ all(sql: string): Promise<readonly Record<string, unknown>[]>;
18
+ }
19
+
20
+ /**
21
+ * Persist one key row to `oke_api_keys`.
22
+ *
23
+ * @param sql - Executor
24
+ * @param row - Key row
25
+ * @param table - Physical table (`oke_api_keys` or schema-qualified)
26
+ */
27
+ export async function persistApiKeyRow(
28
+ sql: ApiKeySqlExec,
29
+ row: ApiKeyRow,
30
+ table: string = AUTH_TABLES.apiKeys,
31
+ ): Promise<void> {
32
+ await sql.execute(
33
+ `INSERT INTO ${table} (
34
+ id, plane, hash, name, scopes, expires_at, rate_limit, ip_allowlist,
35
+ creator_id, creator_scopes, created_at, last_used_at, revoked_at
36
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
37
+ ON CONFLICT(id) DO UPDATE SET
38
+ hash = excluded.hash,
39
+ name = excluded.name,
40
+ scopes = excluded.scopes,
41
+ expires_at = excluded.expires_at,
42
+ rate_limit = excluded.rate_limit,
43
+ ip_allowlist = excluded.ip_allowlist,
44
+ creator_id = excluded.creator_id,
45
+ creator_scopes = excluded.creator_scopes,
46
+ last_used_at = excluded.last_used_at,
47
+ revoked_at = excluded.revoked_at`,
48
+ [
49
+ row.id,
50
+ row.plane,
51
+ row.hash,
52
+ row.name,
53
+ JSON.stringify(row.scopes),
54
+ row.expiresAt,
55
+ row.rateLimit ? JSON.stringify(row.rateLimit) : null,
56
+ JSON.stringify(row.ipAllowlist),
57
+ row.creatorId,
58
+ JSON.stringify(row.creatorScopes),
59
+ row.createdAt,
60
+ row.lastUsedAt,
61
+ row.revokedAt,
62
+ ],
63
+ );
64
+ }
65
+
66
+ /**
67
+ * Load every key row from `oke_api_keys` into a store.
68
+ *
69
+ * @param sql - Executor
70
+ * @param store - Destination store
71
+ * @param table - Physical table
72
+ */
73
+ export async function hydrateApiKeyStore(
74
+ sql: ApiKeySqlExec,
75
+ store: ApiKeyStore,
76
+ table: string = AUTH_TABLES.apiKeys,
77
+ ): Promise<void> {
78
+ const rows = await sql.all(`SELECT * FROM ${table}`);
79
+ for (const raw of rows) {
80
+ const row = rowFromSql(raw);
81
+ if (row) store.keys.set(row.id, row);
82
+ }
83
+ }
84
+
85
+ /**
86
+ * Bind write-through persist on a store.
87
+ *
88
+ * @param store - Key store
89
+ * @param sql - Executor
90
+ * @param table - Physical table
91
+ */
92
+ export function bindApiKeySqlPersist(
93
+ store: ApiKeyStore,
94
+ sql: ApiKeySqlExec,
95
+ table: string = AUTH_TABLES.apiKeys,
96
+ ): void {
97
+ store.persist = (row) => persistApiKeyRow(sql, row, table);
98
+ }
99
+
100
+ /** Runtime that can open the app's shared primary SQL connection. */
101
+ export interface HostApiKeySqlRuntime {
102
+ primarySql(): Promise<SqlConnection | undefined>;
103
+ }
104
+
105
+ /**
106
+ * Adapt a driver connection to {@link ApiKeySqlExec}.
107
+ *
108
+ * @param conn - Shared `store.sql()` connection
109
+ */
110
+ export function apiKeySqlExec(conn: SqlConnection): ApiKeySqlExec {
111
+ return {
112
+ execute: async (sql, params) => {
113
+ await conn.exec(sql, params);
114
+ },
115
+ all: async (sql) => conn.query(sql),
116
+ };
117
+ }
118
+
119
+ /**
120
+ * Create public `oke_api_keys` when missing (host schema, not `oke_console`).
121
+ *
122
+ * @param conn - Shared SQL connection
123
+ * @param table - Physical table
124
+ */
125
+ export async function ensureApiKeyTable(
126
+ conn: SqlConnection,
127
+ table: string = AUTH_TABLES.apiKeys,
128
+ ): Promise<void> {
129
+ await conn.exec(`
130
+ CREATE TABLE IF NOT EXISTS ${table} (
131
+ id TEXT PRIMARY KEY NOT NULL,
132
+ plane TEXT NOT NULL,
133
+ hash TEXT NOT NULL,
134
+ name TEXT NOT NULL,
135
+ scopes TEXT NOT NULL DEFAULT '[]',
136
+ expires_at BIGINT,
137
+ rate_limit TEXT,
138
+ ip_allowlist TEXT NOT NULL DEFAULT '[]',
139
+ creator_id TEXT NOT NULL,
140
+ creator_scopes TEXT NOT NULL DEFAULT '[]',
141
+ created_at BIGINT NOT NULL,
142
+ last_used_at BIGINT,
143
+ revoked_at BIGINT
144
+ )
145
+ `);
146
+ }
147
+
148
+ /**
149
+ * Hydrate and write-through-persist a host {@link ApiKeyStore} on `conn`.
150
+ *
151
+ * Leaves an already-bound `persist` hook in place (injected test stores).
152
+ *
153
+ * @param conn - App SQL connection (`sharedSqlConn`)
154
+ * @param store - `gate.auth.apiKeyStore`
155
+ * @param table - Physical table (`oke_api_keys` in `public`)
156
+ */
157
+ export async function bindHostApiKeySql(
158
+ conn: SqlConnection,
159
+ store: ApiKeyStore,
160
+ table: string = AUTH_TABLES.apiKeys,
161
+ ): Promise<void> {
162
+ await ensureApiKeyTable(conn, table);
163
+ const exec = apiKeySqlExec(conn);
164
+ await hydrateApiKeyStore(exec, store, table);
165
+ if (!store.persist) bindApiKeySqlPersist(store, exec, table);
166
+ }
167
+
168
+ /**
169
+ * Bind host key persist through {@link HostApiKeySqlRuntime.primarySql}.
170
+ *
171
+ * No-ops when the store runtime has no SQL driver (in-memory-only apps).
172
+ *
173
+ * @param runtime - Booted `store` element
174
+ * @param store - `gate.auth.apiKeyStore`
175
+ */
176
+ export async function bindHostApiKeySqlFromStore(
177
+ runtime: HostApiKeySqlRuntime,
178
+ store: ApiKeyStore,
179
+ ): Promise<void> {
180
+ const conn = await runtime.primarySql();
181
+ if (!conn) return;
182
+ await bindHostApiKeySql(conn, store);
183
+ }
184
+
185
+ function rowFromSql(raw: Record<string, unknown>): ApiKeyRow | null {
186
+ const id = asString(raw.id ?? raw.ID);
187
+ const plane = raw.plane === "operator" ? "operator" : "user";
188
+ const hash = asString(raw.hash);
189
+ const name = asString(raw.name);
190
+ const creatorId = asString(raw.creator_id ?? raw.creatorId);
191
+ if (!id || !hash || !name || !creatorId) return null;
192
+ return {
193
+ id,
194
+ plane,
195
+ hash,
196
+ name,
197
+ scopes: asStringArray(raw.scopes),
198
+ expiresAt: asNumberOrNull(raw.expires_at ?? raw.expiresAt),
199
+ rateLimit: asRateLimit(raw.rate_limit ?? raw.rateLimit),
200
+ ipAllowlist: asStringArray(raw.ip_allowlist ?? raw.ipAllowlist),
201
+ creatorId,
202
+ creatorScopes: asStringArray(raw.creator_scopes ?? raw.creatorScopes),
203
+ createdAt: asNumberOrNull(raw.created_at ?? raw.createdAt) ?? 0,
204
+ lastUsedAt: asNumberOrNull(raw.last_used_at ?? raw.lastUsedAt),
205
+ revokedAt: asNumberOrNull(raw.revoked_at ?? raw.revokedAt),
206
+ };
207
+ }
208
+
209
+ function asString(value: unknown): string | null {
210
+ return typeof value === "string" && value.length > 0 ? value : null;
211
+ }
212
+
213
+ function asNumberOrNull(value: unknown): number | null {
214
+ if (typeof value === "number" && Number.isFinite(value)) return value;
215
+ if (typeof value === "string" && value.length > 0) {
216
+ const n = Number(value);
217
+ return Number.isFinite(n) ? n : null;
218
+ }
219
+ return null;
220
+ }
221
+
222
+ function asStringArray(value: unknown): string[] {
223
+ if (Array.isArray(value)) return value.filter((item): item is string => typeof item === "string");
224
+ if (typeof value === "string" && value.length > 0) {
225
+ try {
226
+ const parsed: unknown = JSON.parse(value);
227
+ if (Array.isArray(parsed)) {
228
+ return parsed.filter((item): item is string => typeof item === "string");
229
+ }
230
+ } catch {
231
+ return [];
232
+ }
233
+ }
234
+ return [];
235
+ }
236
+
237
+ function asRateLimit(value: unknown): { max: number; per: string } | null {
238
+ const parsed: unknown =
239
+ typeof value === "string" && value.length > 0
240
+ ? (() => {
241
+ try {
242
+ return JSON.parse(value) as unknown;
243
+ } catch {
244
+ return null;
245
+ }
246
+ })()
247
+ : value;
248
+ if (!parsed || typeof parsed !== "object") return null;
249
+ const rec = parsed as { max?: unknown; per?: unknown };
250
+ if (typeof rec.max === "number" && typeof rec.per === "string") {
251
+ return { max: rec.max, per: rec.per };
252
+ }
253
+ return null;
254
+ }