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
@@ -78,6 +78,14 @@ async function liveRuntime(): Promise<{
78
78
  };
79
79
  }
80
80
 
81
+ describe("createDefaultGateAuthStores", () => {
82
+ test("user roles grant member only — no leftover app catalog", () => {
83
+ const auth = createDefaultGateAuthStores();
84
+ expect([...(auth.roles.grants.get("role_member") ?? [])]).toEqual(["member"]);
85
+ expect([...(auth.roles.grants.get("role_staff") ?? [])]).toEqual(["member"]);
86
+ });
87
+ });
88
+
81
89
  describe("isApplicationScope", () => {
82
90
  test("console:* is not application; everything else is", () => {
83
91
  expect(isApplicationScope("console:store.sql:write")).toBe(false);
@@ -91,6 +99,21 @@ describe("projectGatesPanel", () => {
91
99
  const auth = createDefaultGateAuthStores();
92
100
  // Poison an operator role with an application scope.
93
101
  setRoleGrants(auth.roles, "role_ops", ["console:store.sql:read", "booking:create"]);
102
+ auth.apiKeys.keys.set("key_fixture", {
103
+ id: "key_fixture",
104
+ plane: "user",
105
+ hash: "fixture",
106
+ name: "Fixture",
107
+ scopes: ["member"],
108
+ expiresAt: null,
109
+ rateLimit: null,
110
+ ipAllowlist: [],
111
+ creatorId: "user_demo",
112
+ creatorScopes: ["member"],
113
+ createdAt: 0,
114
+ lastUsedAt: null,
115
+ revokedAt: null,
116
+ });
94
117
 
95
118
  const projection = projectGatesPanel({
96
119
  manifest: MANIFEST,
@@ -318,6 +341,7 @@ describe("simulateGates — evaluate only", () => {
318
341
  test("role principal resolves scopes from RoleStore", async () => {
319
342
  const { runtime, close } = await liveRuntime();
320
343
  const auth = createDefaultGateAuthStores();
344
+ setRoleGrants(auth.roles, "role_member", ["member", "booking:create"]);
321
345
  try {
322
346
  const ok = await simulateGates({
323
347
  flowId: "bookings.create",
@@ -33,6 +33,7 @@ import { statusForFailure } from "../../compiler/response.ts";
33
33
  import { gateDenialFailure } from "../../kernel/index.ts";
34
34
  import { diffManifest } from "../../manifest/diff.ts";
35
35
  import type { Gate as ManifestGate, Manifest, ManifestChange } from "../../manifest/types.ts";
36
+ import { defaultMemberIdentityIds } from "./dev-identities.ts";
36
37
 
37
38
  /** Identity slice needed for Gates (avoids circular import with state). */
38
39
  export interface GatesIdentity {
@@ -413,8 +414,8 @@ export function createDefaultGateAuthStores(): {
413
414
  plane: "operator",
414
415
  description: "Console operators",
415
416
  });
416
- setRoleGrants(roles, "role_member", ["member", "booking:create"]);
417
- setRoleGrants(roles, "role_staff", ["member", "booking:create", "staff", "reports:export"]);
417
+ setRoleGrants(roles, "role_member", ["member"]);
418
+ setRoleGrants(roles, "role_staff", ["member"]);
418
419
  setRoleGrants(roles, "role_ops", [
419
420
  "console:store.sql:read",
420
421
  "console:store.sql:write",
@@ -422,24 +423,9 @@ export function createDefaultGateAuthStores(): {
422
423
  ]);
423
424
 
424
425
  const apiKeys = createApiKeyStore();
425
- apiKeys.keys.set("key_demo", {
426
- id: "key_demo",
427
- plane: "user",
428
- hash: "demo",
429
- name: "Demo key",
430
- scopes: ["member", "booking:create"],
431
- expiresAt: null,
432
- rateLimit: null,
433
- ipAllowlist: [],
434
- creatorId: "user_demo",
435
- creatorScopes: ["member", "booking:create"],
436
- createdAt: 0,
437
- lastUsedAt: null,
438
- revokedAt: null,
439
- });
440
426
 
441
427
  const roleMembers = new Map<string, string[]>([
442
- ["role_member", ["user_demo", "user_member"]],
428
+ ["role_member", defaultMemberIdentityIds()],
443
429
  ["role_staff", []],
444
430
  ["role_ops", []],
445
431
  ]);
@@ -706,7 +692,7 @@ function resolvePrincipalContext(options: SimulateGatesOptions): {
706
692
  return {
707
693
  ctx: {
708
694
  auth: { userId: null, scopes: new Set(), verified: false },
709
- operator: { id: `key:${id}` },
695
+ operator: { id: key.creatorId },
710
696
  meta: { ip: options.meta?.ip },
711
697
  },
712
698
  };
@@ -714,12 +700,13 @@ function resolvePrincipalContext(options: SimulateGatesOptions): {
714
700
  return {
715
701
  ctx: {
716
702
  auth: {
717
- userId: `key:${id}`,
703
+ userId: key.creatorId,
718
704
  scopes: new Set(key.scopes),
719
705
  verified: true,
706
+ apiKeyId: key.id,
720
707
  },
721
708
  operator: { id: null },
722
- meta: { ip: options.meta?.ip, userId: `key:${id}` },
709
+ meta: { ip: options.meta?.ip, userId: key.creatorId },
723
710
  },
724
711
  };
725
712
  }
@@ -6,6 +6,7 @@ export {
6
6
  accessCreateKey,
7
7
  accessRevokeKey,
8
8
  accessRotateKey,
9
+ accessUpdateKey,
9
10
  accessSetRoleGrants,
10
11
  effectivePermissions,
11
12
  expandAccessCeiling,
@@ -181,7 +181,9 @@ describe("resolveInvokeAs", () => {
181
181
  const resolved = resolveInvokeAs({ asGate: "member" }, IDENTITIES, MANIFEST);
182
182
  expect(resolved.ok).toBe(true);
183
183
  if (!resolved.ok) return;
184
- expect(resolved.asUserId).toBe("gate:member");
184
+ expect(resolved.asUserId).toBe("");
185
+ expect(resolved.principal.userId).toBe("");
186
+ expect(resolved.rls).toEqual({ gate: "member", userId: "", scopes: ["member"] });
185
187
  expect([...resolved.principal.scopes]).toEqual(["member"]);
186
188
  expect(resolved.principal.verified).toBe(true);
187
189
  });
@@ -14,6 +14,8 @@ import type { AnyFlowDef } from "../../kernel/flow.ts";
14
14
  import { isJsonResult } from "../../kernel/fx.ts";
15
15
  import { isFlowFailure } from "../../kernel/hooks.ts";
16
16
  import type { Trigger } from "../../kernel/triggers.ts";
17
+ import type { RlsIdentity } from "../../drivers/pg-rls.ts";
18
+ import { resolveRlsIdentity } from "../../elements/store.ts";
17
19
  import type { Manifest } from "../../manifest/types.ts";
18
20
  import type { ConsoleIdentity } from "./state.ts";
19
21
 
@@ -32,6 +34,8 @@ export interface InvokeUserFlowInput {
32
34
  * Envelope remask is skipped separately when this is true.
33
35
  */
34
36
  readonly revealPii?: boolean;
37
+ /** Same bag as SQL console / browse — stamped onto host `fx.store`. */
38
+ readonly rls?: RlsIdentity;
35
39
  }
36
40
 
37
41
  /** Resolved Call API invoke-as principal. */
@@ -42,6 +46,7 @@ export type ResolvedInvokeAs =
42
46
  readonly asUserId: string;
43
47
  readonly asGate: string | null;
44
48
  readonly bypassGates: boolean;
49
+ readonly rls: RlsIdentity | null;
45
50
  }
46
51
  | { readonly ok: false; readonly reason: string };
47
52
 
@@ -75,6 +80,12 @@ export function resolveInvokeAs(
75
80
  asUserId: identity.id,
76
81
  asGate: asGate ?? null,
77
82
  bypassGates: false,
83
+ rls: resolveRlsIdentity({
84
+ asUserId: identity.id,
85
+ asGate: asGate ?? null,
86
+ identities,
87
+ manifest,
88
+ }),
78
89
  };
79
90
  }
80
91
 
@@ -89,6 +100,7 @@ export function resolveInvokeAs(
89
100
  asUserId: "console:operator",
90
101
  asGate: null,
91
102
  bypassGates: true,
103
+ rls: null,
92
104
  };
93
105
  }
94
106
 
@@ -103,6 +115,7 @@ export function resolveInvokeAs(
103
115
  asUserId: "public",
104
116
  asGate: "public",
105
117
  bypassGates: false,
118
+ rls: resolveRlsIdentity({ asGate: "public", manifest }),
106
119
  };
107
120
  }
108
121
 
@@ -114,13 +127,14 @@ export function resolveInvokeAs(
114
127
  return {
115
128
  ok: true,
116
129
  principal: userPrincipal({
117
- userId: `gate:${asGate}`,
130
+ userId: "",
118
131
  scopes,
119
132
  verified: true,
120
133
  }),
121
- asUserId: `gate:${asGate}`,
134
+ asUserId: "",
122
135
  asGate,
123
136
  bypassGates: false,
137
+ rls: resolveRlsIdentity({ asGate, manifest }),
124
138
  };
125
139
  }
126
140
 
@@ -353,6 +367,7 @@ export function bindHostInvokeUserFlow(app: InvokeHostApp): ConsoleInvokeUserFlo
353
367
  runId,
354
368
  ...(input.bypassGates === true ? { bypassGates: true } : {}),
355
369
  ...(input.revealPii === true ? { revealPii: true } : {}),
370
+ ...(input.rls ? { rls: input.rls } : {}),
356
371
  principal: {
357
372
  plane: "user",
358
373
  userId: input.principal.userId.length > 0 ? input.principal.userId : null,
@@ -0,0 +1,9 @@
1
+ import { describe, expect, test } from "bun:test";
2
+
3
+ import { isoAt } from "./iso-at.ts";
4
+
5
+ describe("isoAt", () => {
6
+ test("formats epoch ms as UTC ISO-8601", () => {
7
+ expect(isoAt(1_787_305_011_525)).toBe("2026-08-21T09:36:51.525Z");
8
+ });
9
+ });
@@ -0,0 +1,11 @@
1
+ /**
2
+ * ISO-8601 UTC instant from a Console clock reading (epoch ms).
3
+ *
4
+ * Mutation receipts (`clock.run-now`, store edit, vault write, …) return
5
+ * this instead of a raw millisecond number so Call API JSON is readable.
6
+ *
7
+ * @param ms - Epoch milliseconds from `state.now()` / `Date.now()`
8
+ */
9
+ export function isoAt(ms: number): string {
10
+ return new Date(ms).toISOString();
11
+ }
@@ -6,7 +6,7 @@ import { describe, expect, test } from "bun:test";
6
6
  import type { Manifest } from "../../manifest/types.ts";
7
7
  import type { WideEvent } from "../../runs/types.ts";
8
8
  import { feedManifest, feedRun, subscribeLive } from "./live.ts";
9
- import { createConsoleState, type ConsoleLiveMessage } from "./state.ts";
9
+ import { createConsoleState, setManifest, type ConsoleLiveMessage } from "./state.ts";
10
10
 
11
11
  describe("console live Manifest feed", () => {
12
12
  test("pushes manifest and diff to subscribers", () => {
@@ -28,6 +28,31 @@ describe("console live Manifest feed", () => {
28
28
  expect(messages.some((m) => m.type === "manifest")).toBe(true);
29
29
  expect(messages.some((m) => m.type === "manifest.diff")).toBe(true);
30
30
  });
31
+
32
+ test("seeded identities follow the Manifest — no invented booking:create", () => {
33
+ const state = createConsoleState({ silentClaim: true, secret: "x" });
34
+ expect(state.identities).toHaveLength(10);
35
+ expect(state.identities[0]?.id).toBe("user_demo");
36
+ expect(state.identities.find((row) => row.id === "user_member")?.scopes).toEqual(["member"]);
37
+ expect(state.identities.find((row) => row.id === "user_guest")?.scopes).toEqual([]);
38
+ expect(state.identities.find((row) => row.id === "user_demo")?.scopes).toEqual(["member"]);
39
+
40
+ setManifest(state, {
41
+ oke: "1.0",
42
+ app: "keel-like",
43
+ gates: {
44
+ member: { kind: "policy", scopes: ["member"] },
45
+ "task:write": { kind: "policy", scopes: ["task:write"] },
46
+ },
47
+ });
48
+ expect(state.identities.find((row) => row.id === "user_demo")?.scopes).toEqual([
49
+ "member",
50
+ "task:write",
51
+ ]);
52
+ expect(state.identities.find((row) => row.id === "user_demo")?.scopes).not.toContain(
53
+ "booking:create",
54
+ );
55
+ });
31
56
  });
32
57
 
33
58
  describe("console live run feed", () => {
@@ -8,8 +8,8 @@
8
8
  import type { WideEvent } from "../../runs/types.ts";
9
9
  import { projectRun } from "./flows.ts";
10
10
  import { piiFieldNamesFromManifest } from "./runs-pii.ts";
11
- import type { ConsoleLiveMessage, ConsoleState } from "./state.ts";
12
- import { publishLive } from "./state.ts";
11
+ import { isKeelManifest, refreshSeededIdentities, seedKeelAccessRoles } from "./dev-identities.ts";
12
+ import { publishLive, type ConsoleLiveMessage, type ConsoleState } from "./state.ts";
13
13
 
14
14
  /** WebSocket data bag for Console live clients. */
15
15
  export interface ConsoleLiveData {
@@ -97,6 +97,8 @@ export function feedManifest(
97
97
  ): void {
98
98
  const before = state.manifest;
99
99
  state.manifest = manifest;
100
+ refreshSeededIdentities(state.identities, manifest);
101
+ if (isKeelManifest(manifest)) seedKeelAccessRoles(state.roles, state.roleMembers);
100
102
  publishLive(state, { type: "manifest", manifest });
101
103
  if (before) {
102
104
  publishLive(state, { type: "manifest.diff", before, after: manifest });
@@ -11,6 +11,8 @@
11
11
  * Spec: wizard closes permanently once the first operator exists (console §2.5).
12
12
  * Claim codes stay out of Postgres (optional local DX mirror: `.oke/claim-code`);
13
13
  * operators, sessions, and the signing secret must survive restarts.
14
+ * API keys persist on the host `store.sql()` connection (`public.oke_api_keys`),
15
+ * not in this schema — Console attaches that store via `attachHostToConsole`.
14
16
  */
15
17
 
16
18
  import { mkdirSync } from "node:fs";
@@ -7,14 +7,16 @@ export const CONSOLE_CSP: string = [
7
7
  "default-src 'self'",
8
8
  "script-src 'self'",
9
9
  "style-src 'self' 'unsafe-inline'",
10
- "img-src 'self' data:",
10
+ "img-src 'self' data: blob:",
11
+ "media-src 'self' blob:",
11
12
  "font-src 'self'",
12
13
  "connect-src 'self' ws: wss:",
13
14
  "object-src 'none'",
14
15
  "base-uri 'self'",
15
16
  "form-action 'self'",
16
17
  "frame-ancestors 'none'",
17
- "frame-src 'self'",
18
+ // Files preview mints blob: URLs for PDF iframes, images, and A/V.
19
+ "frame-src 'self' blob:",
18
20
  ].join("; ");
19
21
 
20
22
  /**
@@ -27,13 +29,14 @@ export const CONSOLE_VITE_DEV_CSP: string = [
27
29
  "script-src 'self' 'unsafe-inline' 'unsafe-eval'",
28
30
  "style-src 'self' 'unsafe-inline'",
29
31
  "img-src 'self' data: blob:",
32
+ "media-src 'self' blob:",
30
33
  "font-src 'self'",
31
34
  "connect-src 'self' ws: wss: http://127.0.0.1:* ws://127.0.0.1:*",
32
35
  "object-src 'none'",
33
36
  "base-uri 'self'",
34
37
  "form-action 'self'",
35
38
  "frame-ancestors 'none'",
36
- "frame-src 'self'",
39
+ "frame-src 'self' blob:",
37
40
  ].join("; ");
38
41
 
39
42
  /** CSP for sandboxed plugin panel iframes (no operator session reach). */
@@ -61,6 +61,7 @@ describe("serveConsole spaProxy", () => {
61
61
  expect(html.status).toBe(200);
62
62
  expect(html.headers.get("content-security-policy")).toBe(CONSOLE_VITE_DEV_CSP);
63
63
  expect(html.headers.get("content-security-policy")).toContain("unsafe-inline");
64
+ expect(html.headers.get("content-security-policy")).toContain("frame-src 'self' blob:");
64
65
  expect(await html.text()).toContain("vite-spa");
65
66
 
66
67
  const client = await server.fetch(
@@ -31,6 +31,8 @@ import {
31
31
  parseSqlPolicySpec,
32
32
  quotePgIdent,
33
33
  sqlPolicyRowId,
34
+ emitStoreSchemaPolicySource,
35
+ installOkeRlsHelpers,
34
36
  } from "../../drivers/pg-rls.ts";
35
37
  import type { ConsoleStoreChild, ConsoleWillNotFire } from "./store.ts";
36
38
 
@@ -117,18 +119,52 @@ export async function listSqlTableRls(sql: SqlStoreHandle): Promise<ReadonlyMap<
117
119
  }
118
120
 
119
121
  /**
120
- * Copy RLS flags onto SQL table children. Catalog folders are unchanged.
122
+ * Live RLS policy counts (`pg_policies`) keyed by table name.
123
+ *
124
+ * Counts in process — memory SQL has no `GROUP BY` / `COUNT(*)::int`.
125
+ *
126
+ * @param sql - Open SQL handle
127
+ */
128
+ export async function listSqlTablePolicyCounts(
129
+ sql: SqlStoreHandle,
130
+ ): Promise<ReadonlyMap<string, number>> {
131
+ try {
132
+ const rows = await sql.raw(`SELECT * FROM pg_policies`);
133
+ const out = new Map<string, number>();
134
+ for (const row of rows) {
135
+ if (!isAppCatalogSchema(row)) continue;
136
+ const name = stringOrNull(row.tablename ?? row.table);
137
+ if (name === null) continue;
138
+ out.set(name, (out.get(name) ?? 0) + 1);
139
+ }
140
+ return out;
141
+ } catch {
142
+ return new Map();
143
+ }
144
+ }
145
+
146
+ /**
147
+ * Copy RLS flags and policy counts onto SQL table children. Catalog folders
148
+ * are unchanged.
121
149
  *
122
150
  * @param children - Store list children
123
151
  * @param rlsByTable - Table name → RLS enabled
152
+ * @param declared - Manifest RLS when live catalog is empty
153
+ * @param policyCounts - Table name → live + declared policy count
124
154
  */
125
155
  export function applySqlTableRls(
126
156
  children: readonly ConsoleStoreChild[],
127
157
  rlsByTable: ReadonlyMap<string, boolean>,
158
+ declared?: ReadonlyMap<string, boolean>,
159
+ policyCounts?: ReadonlyMap<string, number>,
128
160
  ): ConsoleStoreChild[] {
129
161
  return children.map((child) => {
130
162
  if (child.kind !== "table") return child;
131
- return { ...child, rls: rlsByTable.get(child.name) ?? child.rls === true };
163
+ return {
164
+ ...child,
165
+ rls: rlsByTable.get(child.name) ?? declared?.get(child.name) ?? child.rls === true,
166
+ rlsPolicyCount: policyCounts?.get(child.name) ?? child.rlsPolicyCount ?? 0,
167
+ };
132
168
  });
133
169
  }
134
170
 
@@ -156,6 +192,9 @@ export async function listSqlCatalog(
156
192
  limit: number,
157
193
  ): Promise<readonly Record<string, unknown>[]> {
158
194
  const live = await tryLiveCatalog(sql, kind, limit);
195
+ if (kind === "policy") {
196
+ return overlayDeclaredPolicies(live ?? [], manifest, storeName).slice(0, limit);
197
+ }
159
198
  if (live) return live;
160
199
  if (kind === "index") return manifestIndexes(manifest, storeName).slice(0, limit);
161
200
  if (kind === "extension") return fallbackExtensions().slice(0, limit);
@@ -172,6 +211,11 @@ export async function createSqlPolicy(
172
211
  sql: SqlStoreHandle,
173
212
  patch: Readonly<Record<string, unknown>>,
174
213
  ): Promise<void> {
214
+ try {
215
+ await installOkeRlsHelpers((stmt) => sql.raw(stmt));
216
+ } catch {
217
+ // Memory / engines without CREATE FUNCTION — CREATE POLICY may still run.
218
+ }
175
219
  const spec = parseSqlPolicySpec(patch);
176
220
  if (patch.enableRls === true) {
177
221
  await sql.raw(buildRowSecuritySql(spec.table, true));
@@ -393,11 +437,13 @@ function catalogColumnDescriptions(kind: SqlCatalogKind): Readonly<Record<string
393
437
  return {
394
438
  name: "Policy",
395
439
  table: "Table",
440
+ origin: "Declared or live-only",
396
441
  command: "Command",
397
442
  roles: "Roles",
398
443
  permissive: "Permissive or restrictive",
399
444
  using: "USING expression",
400
445
  with_check: "WITH CHECK expression",
446
+ code: "Copy as code",
401
447
  };
402
448
  }
403
449
  return {
@@ -459,19 +505,113 @@ function isAppCatalogSchema(row: Record<string, unknown>): boolean {
459
505
  function projectPolicyRow(row: Record<string, unknown>): Record<string, unknown> {
460
506
  const name = stringOrNull(row.name ?? row.policyname) ?? "";
461
507
  const table = stringOrNull(row.table ?? row.tablename) ?? "";
508
+ const command = stringOrNull(row.command ?? row.cmd) ?? "ALL";
509
+ const roles = rolesText(row.roles);
510
+ const using = stringOrNull(row.using ?? row.qual);
511
+ const withCheck = stringOrNull(row.with_check);
512
+ const permissive = stringOrNull(row.permissive) ?? "PERMISSIVE";
462
513
  return {
463
514
  id: sqlPolicyRowId(table, name),
464
515
  name,
465
516
  table,
466
517
  schema: stringOrNull(row.schema ?? row.schemaname) ?? "public",
467
- command: stringOrNull(row.command ?? row.cmd) ?? "ALL",
468
- roles: rolesText(row.roles),
469
- permissive: stringOrNull(row.permissive) ?? "PERMISSIVE",
470
- using: stringOrNull(row.using ?? row.qual),
471
- with_check: stringOrNull(row.with_check),
518
+ command,
519
+ roles,
520
+ permissive,
521
+ using,
522
+ with_check: withCheck,
523
+ origin: stringOrNull(row.origin) ?? "live",
524
+ code: policyCopyAsCode({
525
+ name,
526
+ table,
527
+ command,
528
+ roles,
529
+ permissive,
530
+ using,
531
+ withCheck,
532
+ }),
472
533
  };
473
534
  }
474
535
 
536
+ function overlayDeclaredPolicies(
537
+ live: readonly Record<string, unknown>[],
538
+ manifest: Manifest | null,
539
+ storeName: string,
540
+ ): Record<string, unknown>[] {
541
+ const byId = new Map<string, Record<string, unknown>>();
542
+ for (const row of live) {
543
+ byId.set(String(row.id), { ...row, origin: "live" });
544
+ }
545
+ for (const row of declaredPoliciesFromManifest(manifest, storeName)) {
546
+ const existing = byId.get(row.id);
547
+ byId.set(row.id, existing ? { ...existing, ...row, origin: "declared" } : row);
548
+ }
549
+ return [...byId.values()];
550
+ }
551
+
552
+ function declaredPoliciesFromManifest(
553
+ manifest: Manifest | null,
554
+ storeName: string,
555
+ ): Array<Record<string, unknown> & { readonly id: string }> {
556
+ const tables = manifest?.stores?.[storeName]?.tables ?? {};
557
+ const out: Array<Record<string, unknown> & { readonly id: string }> = [];
558
+ for (const [table, spec] of Object.entries(tables)) {
559
+ for (const [name, policy] of Object.entries(spec.policies ?? {})) {
560
+ const command = (policy.for ?? "all").toUpperCase();
561
+ const roles = Array.isArray(policy.to) ? policy.to.join(", ") : (policy.to ?? "public");
562
+ const permissive = (policy.as ?? "permissive").toUpperCase();
563
+ out.push({
564
+ id: sqlPolicyRowId(table, name),
565
+ name,
566
+ table,
567
+ schema: "public",
568
+ command,
569
+ roles,
570
+ permissive,
571
+ using: policy.using ?? null,
572
+ with_check: policy.withCheck ?? null,
573
+ origin: "declared",
574
+ code: policyCopyAsCode({
575
+ name,
576
+ table,
577
+ command,
578
+ roles,
579
+ permissive,
580
+ using: policy.using,
581
+ withCheck: policy.withCheck,
582
+ }),
583
+ });
584
+ }
585
+ }
586
+ return out;
587
+ }
588
+
589
+ function policyCopyAsCode(input: {
590
+ readonly name: string;
591
+ readonly table: string;
592
+ readonly command: string;
593
+ readonly roles: string;
594
+ readonly permissive: string;
595
+ readonly using?: string | null;
596
+ readonly withCheck?: string | null;
597
+ }): string {
598
+ try {
599
+ return emitStoreSchemaPolicySource(
600
+ parseSqlPolicySpec({
601
+ name: input.name,
602
+ table: input.table,
603
+ command: input.command,
604
+ behavior: input.permissive,
605
+ roles: input.roles,
606
+ using: input.using ?? "",
607
+ withCheck: input.withCheck ?? "",
608
+ }),
609
+ );
610
+ } catch {
611
+ return "";
612
+ }
613
+ }
614
+
475
615
  function projectTriggerRow(row: Record<string, unknown>): Record<string, unknown> {
476
616
  const enabled = row.enabled;
477
617
  return {
@@ -41,4 +41,20 @@ describe("applySqlTableRls", () => {
41
41
  const rows = [{ ...child("comments"), rls: false }];
42
42
  expect(applySqlTableRls(rows, new Map())[0]?.rls).toBe(false);
43
43
  });
44
+
45
+ test("Manifest declared RLS wins when live catalog is empty", () => {
46
+ const out = applySqlTableRls([child("bookings")], new Map(), new Map([["bookings", true]]));
47
+ expect(out[0]?.rls).toBe(true);
48
+ });
49
+
50
+ test("stamps policy counts on tables and leaves catalog folders alone", () => {
51
+ const out = applySqlTableRls(
52
+ [child("comments"), child("policies", "policy")],
53
+ new Map([["comments", true]]),
54
+ undefined,
55
+ new Map([["comments", 2]]),
56
+ );
57
+ expect(out.find((row) => row.name === "comments")?.rlsPolicyCount).toBe(2);
58
+ expect(out.find((row) => row.name === "policies")?.rlsPolicyCount).toBeUndefined();
59
+ });
44
60
  });