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
@@ -1,5 +1,6 @@
1
1
  /**
2
- * Boot-level: API-key Bearer stamps WideEvent.principal with the key id.
2
+ * Boot-level: API-key Bearer stamps WideEvent.principal with the issuer
3
+ * (`creatorId`) and `dimensions.api_key` with the key id.
3
4
  * Session JWT stamps the session subject — never a key id.
4
5
  */
5
6
 
@@ -22,7 +23,7 @@ async function lastEvent(runs: ReturnType<typeof createRunsRuntime>) {
22
23
  }
23
24
 
24
25
  describe("pipeline — API key Bearer identity", () => {
25
- test("API key Bearer stamps WideEvent.principal with the key id", async () => {
26
+ test("API key Bearer stamps WideEvent.principal with the issuer", async () => {
26
27
  resetBindings();
27
28
  resetFlowSeq();
28
29
 
@@ -70,9 +71,193 @@ describe("pipeline — API key Bearer identity", () => {
70
71
  expect(res.status).toBe(200);
71
72
 
72
73
  const event = await lastEvent(runs);
73
- expect(event?.principal).toBe("key_demo");
74
+ expect(event?.principal).toBe("u1");
74
75
  expect(event?.dimensions.api_key).toBe("key_demo");
75
- expect(event?.dimensions.principal).toBe("key_demo");
76
+ expect(event?.dimensions.principal).toBe("u1");
77
+
78
+ await app.stop();
79
+ });
80
+
81
+ test("key-authenticated request stamps fx.auth.userId as issuer and apiKeyId as the key", async () => {
82
+ resetBindings();
83
+ resetFlowSeq();
84
+
85
+ on(
86
+ http.get("/who").gate(member),
87
+ flow("secure.who", {
88
+ do: (_input, fx) => ({
89
+ userId: fx.auth.userId,
90
+ apiKeyId: fx.auth.apiKeyId ?? null,
91
+ }),
92
+ }),
93
+ );
94
+
95
+ const apiKeys = createApiKeyStore();
96
+ const created = await createApiKey(apiKeys, {
97
+ plane: "user",
98
+ name: "who",
99
+ scopes: ["member"],
100
+ creatorId: "u1",
101
+ creatorScopes: ["member"],
102
+ id: "key_who",
103
+ });
104
+ const app = oke({
105
+ name: "api-key-who",
106
+ gate: {
107
+ auth: {
108
+ secret: "hmac-secret-for-tests",
109
+ sessions: createSessionStore(),
110
+ apiKeyStore: apiKeys,
111
+ http: false,
112
+ },
113
+ policies: [member],
114
+ },
115
+ env: "test",
116
+ startScheduler: false,
117
+ });
118
+ await app.boot({ env: "test", gates: [member], startScheduler: false });
119
+
120
+ const res = await app.fetch(
121
+ new Request("http://localhost/who", {
122
+ method: "GET",
123
+ headers: { authorization: `Bearer ${created.secret}` },
124
+ }),
125
+ );
126
+ expect(res.status).toBe(200);
127
+ const body = (await res.json()) as { data: { userId: string; apiKeyId: string } };
128
+ expect(body.data.userId).toBe("u1");
129
+ expect(body.data.apiKeyId).toBe("key_who");
130
+
131
+ await app.stop();
132
+ });
133
+
134
+ test("scope A is Forbidden on gate.scope(B) and 200 on gate.scope(A)", async () => {
135
+ resetBindings();
136
+ resetFlowSeq();
137
+
138
+ const scopeA = gate.scope("scope:a");
139
+ const scopeB = gate.scope("scope:b");
140
+ on(http.get("/a").gate(scopeA), flow("secure.a", { do: () => ({ ok: true }) }));
141
+ on(http.get("/b").gate(scopeB), flow("secure.b", { do: () => ({ ok: true }) }));
142
+
143
+ const apiKeys = createApiKeyStore();
144
+ const created = await createApiKey(apiKeys, {
145
+ plane: "user",
146
+ name: "scoped",
147
+ scopes: ["scope:a"],
148
+ creatorId: "u1",
149
+ creatorScopes: ["scope:a", "scope:b"],
150
+ });
151
+ const app = oke({
152
+ name: "api-key-scopes",
153
+ gate: {
154
+ auth: {
155
+ secret: "hmac-secret-for-tests",
156
+ sessions: createSessionStore(),
157
+ apiKeyStore: apiKeys,
158
+ http: false,
159
+ },
160
+ policies: [scopeA, scopeB],
161
+ },
162
+ env: "test",
163
+ startScheduler: false,
164
+ });
165
+ await app.boot({ env: "test", gates: [scopeA, scopeB], startScheduler: false });
166
+
167
+ const a = await app.fetch(
168
+ new Request("http://localhost/a", {
169
+ method: "GET",
170
+ headers: { authorization: `Bearer ${created.secret}` },
171
+ }),
172
+ );
173
+ const b = await app.fetch(
174
+ new Request("http://localhost/b", {
175
+ method: "GET",
176
+ headers: { authorization: `Bearer ${created.secret}` },
177
+ }),
178
+ );
179
+ expect(a.status).toBe(200);
180
+ expect(b.status).toBe(403);
181
+
182
+ await app.stop();
183
+ });
184
+
185
+ test("allowlist miss and over-rate → 401, no api_key dimension", async () => {
186
+ resetBindings();
187
+ resetFlowSeq();
188
+
189
+ on(http.get("/secure").gate(member), flow("secure.limit", { do: () => ({ ok: true }) }));
190
+
191
+ const apiKeys = createApiKeyStore();
192
+ const allowlisted = await createApiKey(apiKeys, {
193
+ plane: "user",
194
+ name: "allow",
195
+ scopes: ["member"],
196
+ creatorId: "u1",
197
+ creatorScopes: ["member"],
198
+ id: "key_allow",
199
+ ipAllowlist: ["203.0.113.10"],
200
+ });
201
+ const rated = await createApiKey(apiKeys, {
202
+ plane: "user",
203
+ name: "rated",
204
+ scopes: ["member"],
205
+ creatorId: "u1",
206
+ creatorScopes: ["member"],
207
+ id: "key_rate",
208
+ rateLimit: { max: 1, per: "1m" },
209
+ });
210
+ const runs = createRunsRuntime({ driver: memoryRunsDriver });
211
+ await runs.open();
212
+ const app = oke({
213
+ name: "api-key-limits",
214
+ gate: {
215
+ auth: {
216
+ secret: "hmac-secret-for-tests",
217
+ sessions: createSessionStore(),
218
+ apiKeyStore: apiKeys,
219
+ http: false,
220
+ },
221
+ policies: [member],
222
+ },
223
+ runs,
224
+ env: "test",
225
+ startScheduler: false,
226
+ });
227
+ await app.boot({ env: "test", gates: [member], runs, startScheduler: false });
228
+
229
+ const miss = await app.fetch(
230
+ new Request("http://localhost/secure", {
231
+ method: "GET",
232
+ headers: {
233
+ authorization: `Bearer ${allowlisted.secret}`,
234
+ "x-forwarded-for": "198.51.100.7",
235
+ },
236
+ }),
237
+ );
238
+ expect(miss.status).toBe(401);
239
+
240
+ const first = await app.fetch(
241
+ new Request("http://localhost/secure", {
242
+ method: "GET",
243
+ headers: { authorization: `Bearer ${rated.secret}` },
244
+ }),
245
+ );
246
+ const second = await app.fetch(
247
+ new Request("http://localhost/secure", {
248
+ method: "GET",
249
+ headers: { authorization: `Bearer ${rated.secret}` },
250
+ }),
251
+ );
252
+ expect(first.status).toBe(200);
253
+ expect(second.status).toBe(401);
254
+
255
+ const events = await runs.all();
256
+ for (const event of events) {
257
+ if (event.dimensions.error_code) {
258
+ expect(event.dimensions.api_key).toBeUndefined();
259
+ }
260
+ }
76
261
 
77
262
  await app.stop();
78
263
  });
@@ -206,7 +391,7 @@ describe("pipeline — API key Bearer identity", () => {
206
391
  await app.stop();
207
392
  });
208
393
 
209
- test("operator-plane key stamps fx.operator.id as the key id", async () => {
394
+ test("operator-plane key stamps fx.operator.id as the issuer", async () => {
210
395
  resetBindings();
211
396
  resetFlowSeq();
212
397
 
@@ -254,10 +439,10 @@ describe("pipeline — API key Bearer identity", () => {
254
439
  );
255
440
  expect(res.status).toBe(200);
256
441
  const body = (await res.json()) as { data: { operatorId: string } };
257
- expect(body.data.operatorId).toBe("key_ops");
442
+ expect(body.data.operatorId).toBe("op1");
258
443
 
259
444
  const event = await lastEvent(runs);
260
- expect(event?.principal).toBe("key_ops");
445
+ expect(event?.principal).toBe("op1");
261
446
  expect(event?.plane).toBe("operator");
262
447
  expect(event?.dimensions.api_key).toBe("key_ops");
263
448
 
@@ -65,9 +65,9 @@ describe("gateDenialFailure", () => {
65
65
  });
66
66
  });
67
67
 
68
- describe("http trigger — .gate.public", () => {
68
+ describe("http trigger — .public()", () => {
69
69
  test("attaches the public sentinel without calling gate()", () => {
70
- const trigger = http.get("/health").gate.public;
70
+ const trigger = http.get("/health").public();
71
71
  expect(trigger.gates.map((g) => (typeof g === "string" ? g : g.name))).toEqual(["public"]);
72
72
  expect(trigger.live().isLive).toBe(true);
73
73
  });
@@ -24,6 +24,7 @@ export interface PrincipalBag {
24
24
  userId: string | null;
25
25
  scopes: Set<string>;
26
26
  verified?: boolean;
27
+ apiKeyId?: string | null;
27
28
  };
28
29
  readonly operator: {
29
30
  id: string | null;
@@ -53,7 +54,7 @@ export interface PipelineDeps {
53
54
  * @returns Principal on success
54
55
  * @throws On forge / expiry / revoke — pipeline maps to `Unauthorized`
55
56
  */
56
- readonly verifyBearer?: (token: string) => Promise<ResolvedPrincipal>;
57
+ readonly verifyBearer?: (token: string, request?: Request) => Promise<ResolvedPrincipal>;
57
58
  /**
58
59
  * Optional token extraction (e.g. cookie → Bearer when cookies enabled).
59
60
  * When omitted, only `Authorization: Bearer` is read.
@@ -155,6 +156,7 @@ export function applyPrincipal(bag: PrincipalBag, resolved: ResolvedPrincipal |
155
156
  for (const s of resolved.scopes) bag.auth.scopes.add(s);
156
157
  }
157
158
  if (resolved.verified !== undefined) bag.auth.verified = resolved.verified;
159
+ bag.auth.apiKeyId = resolved.apiKeyId ?? null;
158
160
  }
159
161
  }
160
162
 
@@ -192,7 +194,7 @@ export function createElementPipelineHooks(deps: PipelineDeps): {
192
194
  return fail("Unauthorized", {});
193
195
  }
194
196
  try {
195
- const principal = await deps.verifyBearer(token);
197
+ const principal = await deps.verifyBearer(token, ctx.request);
196
198
  applyPrincipal(deps.principals, principal);
197
199
  if (principal.apiKeyId) {
198
200
  deps.telemetry.dimensions.api_key = principal.apiKeyId;
@@ -32,7 +32,7 @@ describe("plugin element contributions", () => {
32
32
  .table("otp_codes")
33
33
  .needs("store.sql");
34
34
 
35
- on(http.get("/ok").gate.public, flow("ok", { do: () => ({ ok: true }) }));
35
+ on(http.get("/ok").public(), flow("ok", { do: () => ({ ok: true }) }));
36
36
 
37
37
  const app = oke({
38
38
  name: "plugin-elements",
@@ -75,7 +75,7 @@ describe("plugin .needs() resolution", () => {
75
75
  test("boot fails when plugin needs missing peer plugin", async () => {
76
76
  resetBindings();
77
77
  resetFlowSeq();
78
- on(http.get("/x").gate.public, flow("x", { do: () => ({ ok: true }) }));
78
+ on(http.get("/x").public(), flow("x", { do: () => ({ ok: true }) }));
79
79
  const dependent = plugin("needs-auth", { version: "0.0.1" }).needs("auth");
80
80
  const app = oke({ name: "needs-boot", gate: { policies: [gate.public] } }).plug(dependent);
81
81
  await expect(app.boot({ env: "test" })).rejects.toThrow(PluginNeedsError);
@@ -38,7 +38,7 @@ describe("GET /_/ready", () => {
38
38
  });
39
39
  const bindings: Binding[] = [
40
40
  {
41
- trigger: http.post("/charge").gate.public,
41
+ trigger: http.post("/charge").public(),
42
42
  flow: charge as AnyFlowDef,
43
43
  },
44
44
  ];
@@ -82,7 +82,7 @@ describe("GET /_oke/client.json", () => {
82
82
  startScheduler: false,
83
83
  env: "test",
84
84
  gate: { unguardedHttp: "allow" },
85
- bindings: [{ trigger: http.get("/ping").gate.public, flow: ping as AnyFlowDef }],
85
+ bindings: [{ trigger: http.get("/ping").public(), flow: ping as AnyFlowDef }],
86
86
  });
87
87
  await app.boot();
88
88
  const res = await app.fetch(new Request("http://127.0.0.1/_oke/client.json"));
@@ -63,6 +63,20 @@ describe("http.resource — gate / live", () => {
63
63
  expect(remove?.isLive).toBe(false);
64
64
  });
65
65
 
66
+ test(".public().live() stamps the sentinel on every verb", () => {
67
+ resetBindings();
68
+ resetFlowSeq();
69
+ on(http.resource("/notes", bag()).public().live());
70
+ expect(httpOf("/notes", "GET")?.gates.map((g) => (typeof g === "string" ? g : g.name))).toEqual(
71
+ ["public"],
72
+ );
73
+ expect(
74
+ httpOf("/notes", "POST")?.gates.map((g) => (typeof g === "string" ? g : g.name)),
75
+ ).toEqual(["public"]);
76
+ expect(httpOf("/notes", "GET")?.isLive).toBe(true);
77
+ expect(httpOf("/notes", "POST")?.isLive).toBe(false);
78
+ });
79
+
66
80
  test(".live().gate(member) order does not matter", () => {
67
81
  resetBindings();
68
82
  resetFlowSeq();
@@ -23,7 +23,7 @@ describe("execute run duration", () => {
23
23
  await runs.open();
24
24
 
25
25
  on(
26
- http.get("/ping").gate.public,
26
+ http.get("/ping").public(),
27
27
  flow("demo.ping", {
28
28
  do: () => ({ ok: true }),
29
29
  }),
@@ -68,7 +68,7 @@ describe("execute run duration", () => {
68
68
  await runs.open();
69
69
  try {
70
70
  on(
71
- http.get("/ping").gate.public,
71
+ http.get("/ping").public(),
72
72
  flow("demo.ping", {
73
73
  do: () => ({ ok: true }),
74
74
  }),
@@ -0,0 +1,79 @@
1
+ /**
2
+ * In-place path / name stamps and fail-loud HTTP boot.
3
+ */
4
+
5
+ import { describe, expect, test, beforeEach } from "bun:test";
6
+ import type { RuntimeRouteMap } from "./adopt-routes.ts";
7
+ import { oke } from "./app.ts";
8
+ import { flow, resetFlowSeq } from "./flow.ts";
9
+ import { resetRegisteredFlowUnits, registerFlowUnits } from "./flow-units.ts";
10
+ import { listBindings, on, resetBindings } from "./on.ts";
11
+ import { stampFlowName, stampHttpPath } from "./stamp-http.ts";
12
+ import { HTTP_PATH_PENDING } from "./http-path-pending.ts";
13
+ import { http } from "./triggers.ts";
14
+
15
+ beforeEach(() => {
16
+ resetBindings();
17
+ resetFlowSeq();
18
+ resetRegisteredFlowUnits();
19
+ });
20
+
21
+ describe("stampHttpPath", () => {
22
+ test("mutates the on() binding in place — listBindings sees the stamped path", () => {
23
+ const f = on(http.get().public(), flow({ do: () => ({ ok: true }) }));
24
+ expect(f.$trigger && f.$trigger.kind === "http" && f.$trigger.path).toBe(HTTP_PATH_PENDING);
25
+ stampHttpPath(f, "/notes/:id");
26
+ expect(f.$trigger && f.$trigger.kind === "http" && f.$trigger.path).toBe("/notes/:id");
27
+ const bound = listBindings()[0];
28
+ expect(bound?.trigger.kind === "http" && bound.trigger.path).toBe("/notes/:id");
29
+ });
30
+
31
+ test("explicit http.get('/x') wins — stamp does not overwrite", () => {
32
+ const f = on(http.get("/me/tasks").public(), flow("notes.get", { do: () => 1 }));
33
+ stampHttpPath(f, "/notes/:id");
34
+ expect(f.$trigger && f.$trigger.kind === "http" && f.$trigger.path).toBe("/me/tasks");
35
+ });
36
+ });
37
+
38
+ describe("stampFlowName", () => {
39
+ test("fills a nameless flow as unit.export", () => {
40
+ const f = flow({ do: () => 1 });
41
+ expect(f.name).toBe("");
42
+ stampFlowName(f, "notes.get");
43
+ expect(f.name).toBe("notes.get");
44
+ expect(f.unit).toBe("notes");
45
+ });
46
+
47
+ test("explicit flow('notes.get') wins", () => {
48
+ const f = flow("notes.get", { do: () => 1 });
49
+ stampFlowName(f, "notes.list");
50
+ expect(f.name).toBe("notes.get");
51
+ });
52
+ });
53
+
54
+ describe("oke — unresolved sentinel and duplicate routes", () => {
55
+ test("unresolved http.get() fails construction", () => {
56
+ on(http.get().public(), flow("notes.get", { do: () => 1 }));
57
+ expect(() => oke({ name: "t", autoBoot: false })).toThrow(/OKE1010/);
58
+ });
59
+
60
+ test("duplicate GET /notes fails construction", () => {
61
+ on(http.get("/notes").public(), flow("notes.list", { do: () => 1 }));
62
+ on(http.get("/notes").public(), flow("notes.also", { do: () => 1 }));
63
+ expect(() => oke({ name: "t", autoBoot: false })).toThrow(/OKE1011/);
64
+ });
65
+
66
+ test("nameless HTTP flow fails construction", () => {
67
+ on(http.get("/notes").public(), flow({ do: () => 1 }));
68
+ expect(() => oke({ name: "t", autoBoot: false })).toThrow(/OKE1012/);
69
+ });
70
+
71
+ test("registerFlowUnits drains into $routes; registry ignore does not", () => {
72
+ const get = on(http.get("/notes/:id").public(), flow("notes.get", { do: () => 1 }));
73
+ registerFlowUnits({ notes: { get } });
74
+ const ignored = oke({ name: "t", autoBoot: false, registry: "ignore" });
75
+ expect(ignored.$routes).toEqual({});
76
+ const app = oke({ name: "t", autoBoot: false });
77
+ expect((app.$routes as RuntimeRouteMap).notes?.get?.path).toBe("/notes/:id");
78
+ });
79
+ });
@@ -0,0 +1,75 @@
1
+ /**
2
+ * In-place HTTP path + Flow name stamps.
3
+ *
4
+ * `on()` already registered the trigger (possibly still on the pending-path
5
+ * sentinel). A wrapper that returned a new trigger would leave `listBindings()`
6
+ * on the sentinel — mutate the existing object.
7
+ */
8
+
9
+ import { isFlow, type AnyFlowDef, type FlowDef } from "./flow.ts";
10
+ import { isPendingHttpPath, type HttpPathPending } from "./http-path-pending.ts";
11
+ import type { HttpTrigger, Trigger } from "./triggers.ts";
12
+
13
+ /**
14
+ * Replace a pending HTTP path on {@link FlowDef} with the file-tree stamp.
15
+ *
16
+ * Explicit `http.get("/x")` wins — the pending sentinel is the only path
17
+ * this function overwrites. Returns the same object with a tighter trigger
18
+ * type when the original path was pending.
19
+ *
20
+ * @param flow - Bound flow (already passed through `on()`)
21
+ * @param path - Inferred URL (`/notes/:id`)
22
+ */
23
+ export function stampHttpPath<F extends AnyFlowDef, P extends string>(
24
+ flow: F,
25
+ path: P,
26
+ ): StampHttpPath<F, P> {
27
+ if (!isFlow(flow)) return flow as StampHttpPath<F, P>;
28
+ const apply = (trigger: Trigger): void => {
29
+ if (trigger.kind !== "http") return;
30
+ if (!isPendingHttpPath(trigger.path)) return;
31
+ (trigger as { path: string }).path = path;
32
+ };
33
+ if (flow.$trigger) apply(flow.$trigger);
34
+ for (const trigger of flow.triggers) apply(trigger);
35
+ return flow as StampHttpPath<F, P>;
36
+ }
37
+
38
+ /**
39
+ * Stamp `unit.export` onto a nameless (or `flow_*`) Flow.
40
+ *
41
+ * Explicit `flow("notes.get")` wins. Fills `unit` when missing.
42
+ *
43
+ * @param flow - Flow definition
44
+ * @param name - `notes.get`
45
+ */
46
+ export function stampFlowName<F extends AnyFlowDef>(flow: F, name: string): F {
47
+ if (!isFlow(flow)) return flow;
48
+ const f = flow as { name: string; unit: string | undefined };
49
+ if (!f.name || f.name.startsWith("flow_")) {
50
+ f.name = name;
51
+ }
52
+ if (!f.unit) {
53
+ const resolved = f.name || name;
54
+ const dot = resolved.indexOf(".");
55
+ f.unit = dot > 0 ? resolved.slice(0, dot) : resolved;
56
+ }
57
+ return flow;
58
+ }
59
+
60
+ /**
61
+ * Type-level path replacement — only when the bound trigger is still pending.
62
+ *
63
+ * @typeParam F - Flow definition
64
+ * @typeParam P - Stamped path
65
+ */
66
+ export type StampHttpPath<F, P extends string> =
67
+ F extends FlowDef<infer I, infer O, infer E, infer D, infer T>
68
+ ? T extends HttpTrigger<infer M, infer Cur>
69
+ ? [Cur] extends [HttpPathPending]
70
+ ? FlowDef<I, O, E, D, HttpTrigger<M, P>>
71
+ : F
72
+ : F
73
+ : F;
74
+
75
+ export { HTTP_PATH_PENDING, isPendingHttpPath } from "./http-path-pending.ts";