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
@@ -27,12 +27,16 @@ import { maskPiiValue, maskWideEventForConsole, piiFieldNamesFromManifest } from
27
27
  import { ConsoleRunsQueryError, RUNS_QUERY_PII_GAP, RUNS_QUERY_TIMEOUT_MS } from "./runs-query.ts";
28
28
  import { DuckQueryTimeoutError } from "../../runs/duckdb.ts";
29
29
  import { ClockResourceNotFoundError, ScheduleNotOverridableError } from "./clock.ts";
30
+ import { isoAt } from "./iso-at.ts";
30
31
  import { createFileDiff, emitStructuralDiff } from "./structural.ts";
31
32
  import type { ConsoleState } from "./state.ts";
32
33
  import { consoleFailureMessage } from "./i18n.ts";
33
34
  import { CONSOLE_PASSWORD_POLICY } from "../password-policy.ts";
34
35
  import { PUBLIC_CONSOLE_FLOWS } from "./public-flows.ts";
35
36
  import { resolveInvokeAs } from "./invoke-user-flow.ts";
37
+ import { resolveRlsIdentity } from "../../elements/store.ts";
38
+ import type { RlsIdentity } from "../../drivers/pg-rls.ts";
39
+ import { accessUserCeiling } from "./access.ts";
36
40
  import { isKnownConsoleSqlGate, StoreFileNotFoundError, tenancyDeclared } from "./store.ts";
37
41
  import {
38
42
  PG_STAT_STATEMENTS_NOT_CREATED,
@@ -152,7 +156,7 @@ const TracesReplayOut = z.object({
152
156
  ok: z.literal(true),
153
157
  rootId: z.string(),
154
158
  dryRun: z.boolean(),
155
- at: z.number(),
159
+ at: z.string(),
156
160
  flow: z.string(),
157
161
  });
158
162
 
@@ -260,7 +264,7 @@ const SignalsReplayOut = z.object({
260
264
  messageId: z.string().optional(),
261
265
  }),
262
266
  ),
263
- at: z.number(),
267
+ at: z.string(),
264
268
  });
265
269
 
266
270
  const SignalsDiscardIn = z.object({
@@ -273,7 +277,7 @@ const SignalsDiscardIn = z.object({
273
277
  const SignalsDiscardOut = z.object({
274
278
  ok: z.literal(true),
275
279
  discarded: z.number(),
276
- at: z.number(),
280
+ at: z.string(),
277
281
  });
278
282
 
279
283
  const ActionPingIn = z.object({
@@ -283,7 +287,7 @@ const ActionPingIn = z.object({
283
287
  const ActionPingOut = z.object({
284
288
  ok: z.literal(true),
285
289
  note: z.string().optional(),
286
- at: z.number(),
290
+ at: z.string(),
287
291
  });
288
292
 
289
293
  const StructuralIn = z.object({
@@ -354,6 +358,13 @@ const InvokeOut = z.object({
354
358
  durationMs: z.number().optional(),
355
359
  peakTier: z.enum(["none", "reads", "writes", "emits", "external", "capabilities"]),
356
360
  auditedAt: z.number(),
361
+ rls: z
362
+ .object({
363
+ gate: z.string().nullable(),
364
+ userId: z.string(),
365
+ applied: z.boolean(),
366
+ })
367
+ .optional(),
357
368
  });
358
369
 
359
370
  const SetupClosed = z.object({ reason: z.string() });
@@ -504,7 +515,7 @@ const ChannelRevealOut = z.object({
504
515
  ok: z.literal(true),
505
516
  id: z.string(),
506
517
  to: z.string(),
507
- at: z.number(),
518
+ at: z.string(),
508
519
  });
509
520
 
510
521
  const ChannelSendTestIn = z.object({
@@ -521,7 +532,7 @@ const ChannelSendTestOut = z.object({
521
532
  messageId: z.string(),
522
533
  status: z.string(),
523
534
  chain: z.string(),
524
- at: z.number(),
535
+ at: z.string(),
525
536
  });
526
537
 
527
538
  const VaultNotFound = z.object({
@@ -634,7 +645,7 @@ const ClockRunNowOut = z.object({
634
645
  ok: z.literal(true),
635
646
  name: z.string(),
636
647
  ran: z.boolean(),
637
- at: z.number(),
648
+ at: z.string(),
638
649
  });
639
650
 
640
651
  const ClockPauseIn = z.object({ name: z.string().min(1) });
@@ -642,7 +653,7 @@ const ClockPauseOut = z.object({
642
653
  ok: z.literal(true),
643
654
  name: z.string(),
644
655
  status: z.string(),
645
- at: z.number(),
656
+ at: z.string(),
646
657
  });
647
658
 
648
659
  const ClockEditIn = z.object({
@@ -656,7 +667,7 @@ const ClockEditOut = z.object({
656
667
  name: z.string(),
657
668
  effectiveCron: z.string().optional(),
658
669
  effectiveEvery: z.string().optional(),
659
- at: z.number(),
670
+ at: z.string(),
660
671
  });
661
672
 
662
673
  const ClockWakeEarlyIn = z.object({ runId: z.string().min(1) });
@@ -665,7 +676,7 @@ const ClockWakeEarlyOut = z.object({
665
676
  runId: z.string(),
666
677
  wakeAt: z.number(),
667
678
  resumed: z.boolean(),
668
- at: z.number(),
679
+ at: z.string(),
669
680
  });
670
681
 
671
682
  const VaultResolutionStepOut = z.object({
@@ -746,7 +757,7 @@ const VaultWriteOut = z.object({
746
757
  ok: z.literal(true),
747
758
  name: z.string(),
748
759
  fingerprint: z.string().nullable(),
749
- at: z.number(),
760
+ at: z.string(),
750
761
  });
751
762
 
752
763
  const VaultAuditRowOut = z.object({
@@ -780,7 +791,7 @@ const VaultRotateMasterOut = z.object({
780
791
  kekVersion: z.number(),
781
792
  remaining: z.number(),
782
793
  masterKey: z.string().nullable(),
783
- at: z.number(),
794
+ at: z.string(),
784
795
  });
785
796
 
786
797
  const AiMetricOut = z.object({
@@ -1154,6 +1165,22 @@ const GatesPowersOut = z.object({
1154
1165
  deniedFlowIds: z.array(z.string()),
1155
1166
  });
1156
1167
 
1168
+ const AccessKeyRowOut = z.object({
1169
+ id: z.string(),
1170
+ name: z.string(),
1171
+ plane: z.enum(["user", "operator"]),
1172
+ creatorId: z.string(),
1173
+ creatorScopes: z.array(z.string()),
1174
+ scopes: z.array(z.string()),
1175
+ createdAt: z.number(),
1176
+ lastUsedAt: z.number().nullable(),
1177
+ expiresAt: z.number().nullable(),
1178
+ revokedAt: z.number().nullable(),
1179
+ rateLimit: z.object({ max: z.number(), per: z.string() }).nullable(),
1180
+ ipAllowlist: z.array(z.string()),
1181
+ unused90d: z.boolean(),
1182
+ });
1183
+
1157
1184
  const AccessPlaneSection = z.object({
1158
1185
  plane: z.enum(["user", "operator"]),
1159
1186
  operators: z
@@ -1192,21 +1219,7 @@ const AccessPlaneSection = z.object({
1192
1219
  memberCount: z.number(),
1193
1220
  }),
1194
1221
  ),
1195
- keys: z.array(
1196
- z.object({
1197
- id: z.string(),
1198
- name: z.string(),
1199
- plane: z.enum(["user", "operator"]),
1200
- scopes: z.array(z.string()),
1201
- createdAt: z.number(),
1202
- lastUsedAt: z.number().nullable(),
1203
- expiresAt: z.number().nullable(),
1204
- revokedAt: z.number().nullable(),
1205
- rateLimit: z.object({ max: z.number(), per: z.string() }).nullable(),
1206
- ipAllowlist: z.array(z.string()),
1207
- unused90d: z.boolean(),
1208
- }),
1209
- ),
1222
+ keys: z.array(AccessKeyRowOut),
1210
1223
  invites: z
1211
1224
  .array(
1212
1225
  z.object({
@@ -1226,21 +1239,7 @@ const AccessListOut = z.object({
1226
1239
  operatorPlane: AccessPlaneSection,
1227
1240
  userPlane: AccessPlaneSection,
1228
1241
  hygiene: z.object({
1229
- unusedKeys: z.array(
1230
- z.object({
1231
- id: z.string(),
1232
- name: z.string(),
1233
- plane: z.enum(["user", "operator"]),
1234
- scopes: z.array(z.string()),
1235
- createdAt: z.number(),
1236
- lastUsedAt: z.number().nullable(),
1237
- expiresAt: z.number().nullable(),
1238
- revokedAt: z.number().nullable(),
1239
- rateLimit: z.object({ max: z.number(), per: z.string() }).nullable(),
1240
- ipAllowlist: z.array(z.string()),
1241
- unused90d: z.boolean(),
1242
- }),
1243
- ),
1242
+ unusedKeys: z.array(AccessKeyRowOut),
1244
1243
  neverSignedInOperators: z.array(
1245
1244
  z.object({
1246
1245
  id: z.string(),
@@ -1307,25 +1306,28 @@ const AccessCreateKeyIn = z.object({
1307
1306
  plane: z.enum(["user", "operator"]),
1308
1307
  name: z.string().min(1),
1309
1308
  scopes: z.array(z.string()),
1309
+ /** Required for user-plane keys — issuer whose grants are the ceiling. */
1310
+ creatorUserId: z.string().min(1).optional(),
1311
+ expiresAt: z.number().nullable().optional(),
1312
+ rateLimit: z.object({ max: z.number(), per: z.string() }).nullable().optional(),
1313
+ ipAllowlist: z.array(z.string()).optional(),
1314
+ });
1315
+
1316
+ const AccessUpdateKeyIn = z.object({
1317
+ keyId: z.string().min(1),
1318
+ name: z.string().min(1).optional(),
1319
+ scopes: z.array(z.string()).optional(),
1310
1320
  expiresAt: z.number().nullable().optional(),
1311
1321
  rateLimit: z.object({ max: z.number(), per: z.string() }).nullable().optional(),
1312
1322
  ipAllowlist: z.array(z.string()).optional(),
1313
1323
  });
1314
1324
 
1325
+ const AccessUpdateKeyOut = z.object({
1326
+ key: AccessKeyRowOut,
1327
+ });
1328
+
1315
1329
  const AccessCreateKeyOut = z.object({
1316
- key: z.object({
1317
- id: z.string(),
1318
- name: z.string(),
1319
- plane: z.enum(["user", "operator"]),
1320
- scopes: z.array(z.string()),
1321
- createdAt: z.number(),
1322
- lastUsedAt: z.number().nullable(),
1323
- expiresAt: z.number().nullable(),
1324
- revokedAt: z.number().nullable(),
1325
- rateLimit: z.object({ max: z.number(), per: z.string() }).nullable(),
1326
- ipAllowlist: z.array(z.string()),
1327
- unused90d: z.boolean(),
1328
- }),
1330
+ key: AccessKeyRowOut,
1329
1331
  /** Raw secret — returned exactly once. */
1330
1332
  secret: z.string(),
1331
1333
  });
@@ -1337,19 +1339,7 @@ const AccessRevokeKeyIn = z.object({
1337
1339
  });
1338
1340
 
1339
1341
  const AccessRevokeKeyOut = z.object({
1340
- key: z.object({
1341
- id: z.string(),
1342
- name: z.string(),
1343
- plane: z.enum(["user", "operator"]),
1344
- scopes: z.array(z.string()),
1345
- createdAt: z.number(),
1346
- lastUsedAt: z.number().nullable(),
1347
- expiresAt: z.number().nullable(),
1348
- revokedAt: z.number().nullable(),
1349
- rateLimit: z.object({ max: z.number(), per: z.string() }).nullable(),
1350
- ipAllowlist: z.array(z.string()),
1351
- unused90d: z.boolean(),
1352
- }),
1342
+ key: AccessKeyRowOut,
1353
1343
  blastRadius: AccessKeyBlastOut,
1354
1344
  });
1355
1345
 
@@ -1422,6 +1412,7 @@ const StoreListOut = z.object({
1422
1412
  piiColumns: z.array(z.string()),
1423
1413
  columnDescriptions: z.record(z.string(), z.string()),
1424
1414
  rls: z.boolean().optional(),
1415
+ rlsPolicyCount: z.number().int().nonnegative().optional(),
1425
1416
  }),
1426
1417
  ),
1427
1418
  replicaLagMs: z.number().nullable(),
@@ -1445,6 +1436,17 @@ const StoreListOut = z.object({
1445
1436
  ),
1446
1437
  });
1447
1438
 
1439
+ const StoreRlsAs = {
1440
+ asGate: z.string().min(1).optional(),
1441
+ asUserId: z.string().min(1).optional(),
1442
+ };
1443
+
1444
+ const StoreRlsOut = z.object({
1445
+ gate: z.string().nullable(),
1446
+ userId: z.string(),
1447
+ applied: z.boolean(),
1448
+ });
1449
+
1448
1450
  const StoreQueryIn = z.object({
1449
1451
  ref: z.string().min(1),
1450
1452
  child: z.string().optional(),
@@ -1455,6 +1457,7 @@ const StoreQueryIn = z.object({
1455
1457
  q: z.string().optional(),
1456
1458
  topK: z.number().min(1).max(100).optional(),
1457
1459
  revealPii: z.boolean().optional(),
1460
+ ...StoreRlsAs,
1458
1461
  });
1459
1462
 
1460
1463
  const StoreQueryOut = z.object({
@@ -1483,6 +1486,7 @@ const StoreQueryOut = z.object({
1483
1486
  .optional(),
1484
1487
  masked: z.boolean(),
1485
1488
  routedRole: z.enum(["primary", "replica"]).optional(),
1489
+ rls: StoreRlsOut.optional(),
1486
1490
  });
1487
1491
 
1488
1492
  const StoreRevealIn = z.object({
@@ -1496,7 +1500,7 @@ const StoreRevealIn = z.object({
1496
1500
  const StoreRevealOut = z.object({
1497
1501
  ok: z.literal(true),
1498
1502
  value: z.unknown(),
1499
- at: z.number(),
1503
+ at: z.string(),
1500
1504
  });
1501
1505
 
1502
1506
  const StoreFileGetIn = z.object({
@@ -1527,6 +1531,7 @@ const StoreEditIn = z.object({
1527
1531
  reason: z.string().optional(),
1528
1532
  /** When false/omitted, returns willNotFire without applying. */
1529
1533
  commit: z.boolean().optional(),
1534
+ ...StoreRlsAs,
1530
1535
  });
1531
1536
 
1532
1537
  const StoreEditOut = z.object({
@@ -1540,7 +1545,8 @@ const StoreEditOut = z.object({
1540
1545
  resource: z.string(),
1541
1546
  }),
1542
1547
  ),
1543
- at: z.number(),
1548
+ at: z.string(),
1549
+ rls: StoreRlsOut.optional(),
1544
1550
  });
1545
1551
 
1546
1552
  const StoreDeleteIn = z.object({
@@ -1556,7 +1562,7 @@ const StoreDeleteIn = z.object({
1556
1562
  const StoreDeleteOut = z.object({
1557
1563
  ok: z.literal(true),
1558
1564
  deleted: z.number(),
1559
- at: z.number(),
1565
+ at: z.string(),
1560
1566
  });
1561
1567
 
1562
1568
  const StorePurgeIn = z.object({
@@ -1568,7 +1574,7 @@ const StorePurgeIn = z.object({
1568
1574
  const StorePurgeOut = z.object({
1569
1575
  ok: z.literal(true),
1570
1576
  keys: z.array(z.string()),
1571
- at: z.number(),
1577
+ at: z.string(),
1572
1578
  });
1573
1579
 
1574
1580
  const StoreSqlIn = z.object({
@@ -1578,6 +1584,7 @@ const StoreSqlIn = z.object({
1578
1584
  allowWrite: z.boolean().optional(),
1579
1585
  revealPii: z.boolean().optional(),
1580
1586
  asGate: z.string().min(1).optional(),
1587
+ asUserId: z.string().min(1).optional(),
1581
1588
  });
1582
1589
 
1583
1590
  const StoreSqlOut = z.object({
@@ -1585,7 +1592,9 @@ const StoreSqlOut = z.object({
1585
1592
  masked: z.boolean(),
1586
1593
  routedRole: z.enum(["primary", "replica"]),
1587
1594
  asGate: z.string().nullable(),
1595
+ asUserId: z.string().nullable(),
1588
1596
  gateApplied: z.boolean(),
1597
+ rls: StoreRlsOut.optional(),
1589
1598
  });
1590
1599
 
1591
1600
  const StoreSqlStatsIn = z.object({
@@ -1749,7 +1758,7 @@ const StorePreviewOut = z.object({
1749
1758
  resource: z.string(),
1750
1759
  }),
1751
1760
  ),
1752
- at: z.number(),
1761
+ at: z.string(),
1753
1762
  });
1754
1763
 
1755
1764
  /**
@@ -1826,6 +1835,7 @@ export function createConsoleBindings(state: ConsoleState): {
1826
1835
  readonly createKey: AnyFlowDef;
1827
1836
  readonly revokeKey: AnyFlowDef;
1828
1837
  readonly rotateKey: AnyFlowDef;
1838
+ readonly updateKey: AnyFlowDef;
1829
1839
  readonly setRoleGrants: AnyFlowDef;
1830
1840
  };
1831
1841
  readonly diff: {
@@ -1899,6 +1909,7 @@ export function createConsoleBindings(state: ConsoleState): {
1899
1909
  const accessCreateKeyFlow = createAccessCreateKey(state);
1900
1910
  const accessRevokeKeyFlow = createAccessRevokeKey(state);
1901
1911
  const accessRotateKeyFlow = createAccessRotateKey(state);
1912
+ const accessUpdateKeyFlow = createAccessUpdateKey(state);
1902
1913
  const accessSetRoleGrantsFlow = createAccessSetRoleGrants(state);
1903
1914
  const diffList = createDiffList(state);
1904
1915
  const pluginsList = createPluginsList(state);
@@ -1961,6 +1972,7 @@ export function createConsoleBindings(state: ConsoleState): {
1961
1972
  bindHttp(http.post("/console/access/keys"), accessCreateKeyFlow),
1962
1973
  bindHttp(http.post("/console/access/keys/revoke"), accessRevokeKeyFlow),
1963
1974
  bindHttp(http.post("/console/access/keys/rotate"), accessRotateKeyFlow),
1975
+ bindHttp(http.post("/console/access/keys/update"), accessUpdateKeyFlow),
1964
1976
  bindHttp(http.post("/console/access/roles/grants"), accessSetRoleGrantsFlow),
1965
1977
  bindHttp(http.get("/console/diff"), diffList),
1966
1978
  bindHttp(http.get("/console/plugins"), pluginsList),
@@ -2030,6 +2042,7 @@ export function createConsoleBindings(state: ConsoleState): {
2030
2042
  createKey: accessCreateKeyFlow,
2031
2043
  revokeKey: accessRevokeKeyFlow,
2032
2044
  rotateKey: accessRotateKeyFlow,
2045
+ updateKey: accessUpdateKeyFlow,
2033
2046
  setRoleGrants: accessSetRoleGrantsFlow,
2034
2047
  },
2035
2048
  diff: { list: diffList },
@@ -2362,7 +2375,7 @@ function createTracesReplay(state: ConsoleState) {
2362
2375
  ok: true as const,
2363
2376
  rootId: input.rootId,
2364
2377
  dryRun,
2365
- at: Date.now(),
2378
+ at: isoAt(Date.now()),
2366
2379
  flow: root.flow,
2367
2380
  };
2368
2381
  },
@@ -2443,7 +2456,7 @@ function createSignalsDiscard(state: ConsoleState) {
2443
2456
  return {
2444
2457
  ok: true as const,
2445
2458
  discarded: result.discarded,
2446
- at: state.now(),
2459
+ at: isoAt(state.now()),
2447
2460
  };
2448
2461
  },
2449
2462
  });
@@ -2517,7 +2530,7 @@ async function runSignalReplay(
2517
2530
  dryRun: result.dryRun,
2518
2531
  results: [...result.results],
2519
2532
  wouldHaveFired: [...result.wouldHaveFired],
2520
- at: state.now(),
2533
+ at: isoAt(state.now()),
2521
2534
  };
2522
2535
  }
2523
2536
 
@@ -2562,7 +2575,7 @@ function createActionPing(state: ConsoleState) {
2562
2575
  operatorId: fx.operator.id,
2563
2576
  note: input.note,
2564
2577
  });
2565
- return { ok: true as const, note: input.note, at: state.now() };
2578
+ return { ok: true as const, note: input.note, at: isoAt(state.now()) };
2566
2579
  },
2567
2580
  });
2568
2581
  }
@@ -2664,6 +2677,7 @@ function createFlowsInvoke(state: ConsoleState) {
2664
2677
  ...(assumed.bypassGates ? { bypassGates: true } : {}),
2665
2678
  ...(input.reason !== undefined ? { reason: input.reason } : {}),
2666
2679
  ...(input.revealPii === true ? { revealPii: true } : {}),
2680
+ ...(assumed.rls ? { rls: assumed.rls } : {}),
2667
2681
  });
2668
2682
 
2669
2683
  const reveal = input.revealPii === true;
@@ -2716,6 +2730,11 @@ function createFlowsInvoke(state: ConsoleState) {
2716
2730
  ...(host.durationMs !== undefined ? { durationMs: host.durationMs } : {}),
2717
2731
  peakTier,
2718
2732
  auditedAt: state.now(),
2733
+ rls: {
2734
+ gate: assumed.rls?.gate ?? null,
2735
+ userId: assumed.rls?.userId ?? "",
2736
+ applied: assumed.rls !== null,
2737
+ },
2719
2738
  };
2720
2739
  },
2721
2740
  });
@@ -2806,6 +2825,32 @@ function createStoreList(state: ConsoleState) {
2806
2825
  });
2807
2826
  }
2808
2827
 
2828
+ function consoleStoreRls(
2829
+ state: ConsoleState,
2830
+ input: { readonly asGate?: string; readonly asUserId?: string },
2831
+ ):
2832
+ | { readonly ok: true; readonly rls: RlsIdentity | null }
2833
+ | { readonly ok: false; readonly reason: string } {
2834
+ if (input.asGate !== undefined && !isKnownConsoleSqlGate(state.manifest, input.asGate)) {
2835
+ return { ok: false, reason: `unknown gate: ${input.asGate}` };
2836
+ }
2837
+ if (input.asUserId !== undefined) {
2838
+ const identity = state.identities.find((row) => row.id === input.asUserId);
2839
+ if (!identity || identity.status === "disabled") {
2840
+ return { ok: false, reason: `unknown identity: ${input.asUserId}` };
2841
+ }
2842
+ }
2843
+ return {
2844
+ ok: true,
2845
+ rls: resolveRlsIdentity({
2846
+ asGate: input.asGate,
2847
+ asUserId: input.asUserId,
2848
+ identities: state.identities,
2849
+ manifest: state.manifest,
2850
+ }),
2851
+ };
2852
+ }
2853
+
2809
2854
  function createStoreQuery(state: ConsoleState) {
2810
2855
  return flow("console.store.query", {
2811
2856
  plane: "operator",
@@ -2824,6 +2869,8 @@ function createStoreQuery(state: ConsoleState) {
2824
2869
  tenant: input.tenant,
2825
2870
  });
2826
2871
  }
2872
+ const assumed = consoleStoreRls(state, input);
2873
+ if (!assumed.ok) return fail("StoreNotFound", { ref: assumed.reason });
2827
2874
  return state.queryStore({
2828
2875
  ref: input.ref as ResourceRef,
2829
2876
  child: input.child,
@@ -2834,6 +2881,9 @@ function createStoreQuery(state: ConsoleState) {
2834
2881
  q: input.q,
2835
2882
  topK: input.topK,
2836
2883
  revealPii: input.revealPii === true,
2884
+ ...(input.asGate !== undefined ? { asGate: input.asGate } : {}),
2885
+ ...(input.asUserId !== undefined ? { asUserId: input.asUserId } : {}),
2886
+ ...(assumed.rls ? { rls: assumed.rls } : {}),
2837
2887
  });
2838
2888
  },
2839
2889
  });
@@ -2869,7 +2919,7 @@ function createStoreReveal(state: ConsoleState) {
2869
2919
  return {
2870
2920
  ok: true as const,
2871
2921
  value: row[input.column],
2872
- at: state.now(),
2922
+ at: isoAt(state.now()),
2873
2923
  };
2874
2924
  },
2875
2925
  });
@@ -2922,6 +2972,14 @@ function createStoreEdit(state: ConsoleState) {
2922
2972
  const tenantFail = requireTenantIfDeclared(state, input.tenant);
2923
2973
  if (tenantFail) return tenantFail;
2924
2974
 
2975
+ const assumed = consoleStoreRls(state, input);
2976
+ if (!assumed.ok) return fail("StoreNotFound", { ref: assumed.reason });
2977
+ const rlsFields = {
2978
+ ...(input.asGate !== undefined ? { asGate: input.asGate } : {}),
2979
+ ...(input.asUserId !== undefined ? { asUserId: input.asUserId } : {}),
2980
+ ...(assumed.rls ? { rls: assumed.rls } : {}),
2981
+ };
2982
+
2925
2983
  if (!input.commit) {
2926
2984
  // Return will-not-fire payload without applying — informational confirm.
2927
2985
  const preview = await state.editStore(
@@ -2932,6 +2990,7 @@ function createStoreEdit(state: ConsoleState) {
2932
2990
  id: input.id,
2933
2991
  key: input.key,
2934
2992
  patch: input.patch,
2993
+ ...rlsFields,
2935
2994
  },
2936
2995
  { dryRun: true },
2937
2996
  );
@@ -2941,7 +3000,8 @@ function createStoreEdit(state: ConsoleState) {
2941
3000
  applied: false,
2942
3001
  willNotFire: preview.willNotFire,
2943
3002
  wouldHaveFired: [...preview.wouldHaveFired],
2944
- at: state.now(),
3003
+ at: isoAt(state.now()),
3004
+ ...(preview.rls ? { rls: preview.rls } : {}),
2945
3005
  };
2946
3006
  }
2947
3007
 
@@ -2962,6 +3022,7 @@ function createStoreEdit(state: ConsoleState) {
2962
3022
  id: input.id,
2963
3023
  key: input.key,
2964
3024
  patch: input.patch,
3025
+ ...rlsFields,
2965
3026
  },
2966
3027
  { dryRun: false },
2967
3028
  );
@@ -2978,7 +3039,8 @@ function createStoreEdit(state: ConsoleState) {
2978
3039
  applied: result.applied,
2979
3040
  willNotFire: result.willNotFire,
2980
3041
  wouldHaveFired: [...result.wouldHaveFired],
2981
- at: state.now(),
3042
+ at: isoAt(state.now()),
3043
+ ...(result.rls ? { rls: result.rls } : {}),
2982
3044
  };
2983
3045
  },
2984
3046
  });
@@ -3018,7 +3080,7 @@ function createStoreDelete(state: ConsoleState) {
3018
3080
  return {
3019
3081
  ok: true as const,
3020
3082
  deleted: result.deleted,
3021
- at: state.now(),
3083
+ at: isoAt(state.now()),
3022
3084
  };
3023
3085
  },
3024
3086
  });
@@ -3050,7 +3112,7 @@ function createStorePurgeCache(state: ConsoleState) {
3050
3112
  return {
3051
3113
  ok: true as const,
3052
3114
  keys: [...result.keys],
3053
- at: state.now(),
3115
+ at: isoAt(state.now()),
3054
3116
  };
3055
3117
  },
3056
3118
  });
@@ -3074,9 +3136,8 @@ function createStoreSql(state: ConsoleState) {
3074
3136
  tenant: input.tenant,
3075
3137
  });
3076
3138
  }
3077
- if (input.asGate !== undefined && !isKnownConsoleSqlGate(state.manifest, input.asGate)) {
3078
- return fail("StoreNotFound", { ref: `unknown gate: ${input.asGate}` });
3079
- }
3139
+ const assumed = consoleStoreRls(state, input);
3140
+ if (!assumed.ok) return fail("StoreNotFound", { ref: assumed.reason });
3080
3141
  if (input.asGate !== undefined) {
3081
3142
  fx.log.info("console.store.sql.asGate", {
3082
3143
  operatorId: fx.operator.id,
@@ -3090,6 +3151,8 @@ function createStoreSql(state: ConsoleState) {
3090
3151
  revealPii: input.revealPii === true,
3091
3152
  tenant: input.tenant,
3092
3153
  ...(input.asGate !== undefined ? { asGate: input.asGate } : {}),
3154
+ ...(input.asUserId !== undefined ? { asUserId: input.asUserId } : {}),
3155
+ ...(assumed.rls ? { rls: assumed.rls } : {}),
3093
3156
  });
3094
3157
  fx.log.info("console.store.sql", {
3095
3158
  operatorId: fx.operator.id,
@@ -3097,6 +3160,7 @@ function createStoreSql(state: ConsoleState) {
3097
3160
  allowWrite: input.allowWrite === true,
3098
3161
  revealPii: input.revealPii === true,
3099
3162
  asGate: input.asGate ?? null,
3163
+ asUserId: input.asUserId ?? null,
3100
3164
  rowCount: result.rows.length,
3101
3165
  });
3102
3166
  return result;
@@ -3312,7 +3376,7 @@ function createStorePreview(state: ConsoleState) {
3312
3376
  dryRun: true as const,
3313
3377
  willNotFire: preview.willNotFire,
3314
3378
  wouldHaveFired: [...preview.wouldHaveFired],
3315
- at: state.now(),
3379
+ at: isoAt(state.now()),
3316
3380
  };
3317
3381
  } catch (err) {
3318
3382
  if (err instanceof DryRunWriteIsolationError) {
@@ -3368,7 +3432,7 @@ function createVaultCreate(state: ConsoleState) {
3368
3432
  ok: true as const,
3369
3433
  name: result.name,
3370
3434
  fingerprint: result.fingerprint,
3371
- at: state.now(),
3435
+ at: isoAt(state.now()),
3372
3436
  };
3373
3437
  } catch (err) {
3374
3438
  const message = err instanceof Error ? err.message : String(err);
@@ -3408,7 +3472,7 @@ function createVaultSet(state: ConsoleState) {
3408
3472
  ok: true as const,
3409
3473
  name: result.name,
3410
3474
  fingerprint: result.fingerprint,
3411
- at: state.now(),
3475
+ at: isoAt(state.now()),
3412
3476
  };
3413
3477
  } catch (err) {
3414
3478
  return fail("VaultNotFound", {
@@ -3446,7 +3510,7 @@ function createVaultRotate(state: ConsoleState) {
3446
3510
  ok: true as const,
3447
3511
  name: result.name,
3448
3512
  fingerprint: result.fingerprint,
3449
- at: state.now(),
3513
+ at: isoAt(state.now()),
3450
3514
  };
3451
3515
  } catch (err) {
3452
3516
  return fail("VaultNotFound", {
@@ -3492,7 +3556,7 @@ function createVaultRotateMaster(state: ConsoleState) {
3492
3556
  kekVersion: result.kekVersion,
3493
3557
  remaining: result.remaining,
3494
3558
  masterKey: result.masterKey,
3495
- at: state.now(),
3559
+ at: isoAt(state.now()),
3496
3560
  };
3497
3561
  } catch (err) {
3498
3562
  return mapVaultOperatorError(err);
@@ -3774,6 +3838,46 @@ function actorScopesOf(state: ConsoleState, operatorId: string): string[] {
3774
3838
  return [...new Set([...fromRoles, "console:*"])];
3775
3839
  }
3776
3840
 
3841
+ /**
3842
+ * Derive the issuer for a Console-minted key.
3843
+ * Operator-plane: the acting operator. User-plane: required `creatorUserId`.
3844
+ *
3845
+ * @param state - Console state
3846
+ * @param operatorId - Acting operator
3847
+ * @param input - Create payload
3848
+ */
3849
+ export function resolveAccessKeyIssuer(
3850
+ state: ConsoleState,
3851
+ operatorId: string,
3852
+ input: { readonly plane: "user" | "operator"; readonly creatorUserId?: string },
3853
+ ):
3854
+ | { readonly creatorId: string; readonly creatorScopes: readonly string[] }
3855
+ | { readonly error: string } {
3856
+ if (input.plane === "operator") {
3857
+ return { creatorId: operatorId, creatorScopes: actorScopesOf(state, operatorId) };
3858
+ }
3859
+ const userId = input.creatorUserId;
3860
+ if (!userId) {
3861
+ return { error: "creatorUserId is required for user-plane keys" };
3862
+ }
3863
+ const identity = state.identities.find((row) => row.id === userId);
3864
+ const roleIds = [...state.roleMembers.entries()]
3865
+ .filter(([, members]) => members.includes(userId))
3866
+ .map(([id]) => id);
3867
+ if (!identity && roleIds.length === 0) {
3868
+ return { error: `unknown user: ${userId}` };
3869
+ }
3870
+ return {
3871
+ creatorId: userId,
3872
+ creatorScopes: accessUserCeiling({
3873
+ userId,
3874
+ identityScopes: identity?.scopes,
3875
+ roles: state.roles,
3876
+ roleMembers: state.roleMembers,
3877
+ }),
3878
+ };
3879
+ }
3880
+
3777
3881
  function createAccessList(state: ConsoleState) {
3778
3882
  return flow("console.access.list", {
3779
3883
  plane: "operator",
@@ -3836,12 +3940,16 @@ function createAccessCreateKey(state: ConsoleState) {
3836
3940
  do: async (input: z.infer<typeof AccessCreateKeyIn>, fx) => {
3837
3941
  if (!fx.operator.id) return fail("AuthFailed", {});
3838
3942
  try {
3943
+ const issued = resolveAccessKeyIssuer(state, fx.operator.id, input);
3944
+ if ("error" in issued) {
3945
+ return fail("AccessGrantDenied", { reason: issued.error });
3946
+ }
3839
3947
  const created = await state.accessCreateKey({
3840
3948
  plane: input.plane,
3841
3949
  name: input.name,
3842
3950
  scopes: input.scopes,
3843
- creatorId: fx.operator.id,
3844
- creatorScopes: actorScopesOf(state, fx.operator.id),
3951
+ creatorId: issued.creatorId,
3952
+ creatorScopes: issued.creatorScopes,
3845
3953
  expiresAt: input.expiresAt,
3846
3954
  rateLimit: input.rateLimit,
3847
3955
  ipAllowlist: input.ipAllowlist,
@@ -3924,6 +4032,33 @@ function createAccessRotateKey(state: ConsoleState) {
3924
4032
  });
3925
4033
  }
3926
4034
 
4035
+ function createAccessUpdateKey(state: ConsoleState) {
4036
+ return flow("console.access.updateKey", {
4037
+ plane: "operator",
4038
+ in: AccessUpdateKeyIn,
4039
+ out: AccessUpdateKeyOut,
4040
+ errors: { AuthFailed, AccessKeyNotFound, AccessGrantDenied },
4041
+ do: async (input: z.infer<typeof AccessUpdateKeyIn>, fx) => {
4042
+ if (!fx.operator.id) return fail("AuthFailed", {});
4043
+ try {
4044
+ const key = await state.accessUpdateKey(input);
4045
+ if (!key) {
4046
+ return fail("AccessKeyNotFound", { keyId: input.keyId });
4047
+ }
4048
+ fx.log.info("console.access.updateKey", {
4049
+ keyId: key.id,
4050
+ operatorId: fx.operator.id,
4051
+ });
4052
+ return { key: { ...key } };
4053
+ } catch (err) {
4054
+ return fail("AccessGrantDenied", {
4055
+ reason: err instanceof Error ? err.message : "grant denied",
4056
+ });
4057
+ }
4058
+ },
4059
+ });
4060
+ }
4061
+
3927
4062
  function createAccessSetRoleGrants(state: ConsoleState) {
3928
4063
  return flow("console.access.setRoleGrants", {
3929
4064
  plane: "operator",
@@ -4010,7 +4145,7 @@ function createClockRunNow(state: ConsoleState) {
4010
4145
  ok: true as const,
4011
4146
  name: input.name,
4012
4147
  ran: result.ran,
4013
- at: state.now(),
4148
+ at: isoAt(state.now()),
4014
4149
  };
4015
4150
  } catch (err) {
4016
4151
  if (err instanceof ClockResourceNotFoundError) {
@@ -4040,7 +4175,7 @@ function createClockPause(state: ConsoleState) {
4040
4175
  ok: true as const,
4041
4176
  name: result.name,
4042
4177
  status: result.status,
4043
- at: state.now(),
4178
+ at: isoAt(state.now()),
4044
4179
  };
4045
4180
  } catch (err) {
4046
4181
  if (err instanceof ClockResourceNotFoundError) {
@@ -4075,7 +4210,7 @@ function createClockEditSchedule(state: ConsoleState) {
4075
4210
  name: result.name,
4076
4211
  effectiveCron: result.effectiveCron,
4077
4212
  effectiveEvery: result.effectiveEvery,
4078
- at: state.now(),
4213
+ at: isoAt(state.now()),
4079
4214
  };
4080
4215
  } catch (err) {
4081
4216
  if (err instanceof ScheduleNotOverridableError) {
@@ -4110,7 +4245,7 @@ function createClockWakeEarly(state: ConsoleState) {
4110
4245
  runId: result.runId,
4111
4246
  wakeAt: result.wakeAt,
4112
4247
  resumed: result.resumed,
4113
- at: state.now(),
4248
+ at: isoAt(state.now()),
4114
4249
  };
4115
4250
  } catch (err) {
4116
4251
  if (err instanceof ClockResourceNotFoundError) {
@@ -4187,7 +4322,7 @@ function createChannelReveal(state: ConsoleState) {
4187
4322
  ok: true as const,
4188
4323
  id: revealed.id,
4189
4324
  to: revealed.to,
4190
- at: state.now(),
4325
+ at: isoAt(state.now()),
4191
4326
  };
4192
4327
  },
4193
4328
  });
@@ -4234,7 +4369,7 @@ function createChannelSendTest(state: ConsoleState) {
4234
4369
  });
4235
4370
  return {
4236
4371
  ...result,
4237
- at: state.now(),
4372
+ at: isoAt(state.now()),
4238
4373
  };
4239
4374
  } catch (err) {
4240
4375
  return fail("ChannelNotFound", {