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
@@ -60,6 +60,7 @@ import { modChord, modShiftChord } from "@/lib/shortcut.ts";
60
60
  import { cn } from "@/lib/utils.ts";
61
61
  import { useStoreEdit } from "../data/use-store-edit.ts";
62
62
  import { JsonValueSheet } from "../detail/json-value-sheet.tsx";
63
+ import { StoreRowDetailSheet } from "../detail/store-row-detail-sheet.tsx";
63
64
  import { RevealCell } from "../detail/reveal-cell.tsx";
64
65
  import { asInspectableJson, jsonValueEqual } from "../lib/json-value.ts";
65
66
  import { parseKvTtlDraft } from "../lib/kv-meta.ts";
@@ -132,12 +133,22 @@ export interface StoreDataGridProps {
132
133
  readonly insertOpen?: boolean;
133
134
  readonly onInsertOpenChange?: (open: boolean) => void;
134
135
  readonly onDeleteRows?: (rows: readonly StoreGridRow[]) => void;
136
+ /** Gate identity for row DML (`oke.*` stamp). Catalog DDL ignores this. */
137
+ readonly asGate?: string | null;
138
+ readonly asUserId?: string | null;
139
+ /** Two-finger / right-click empty grid (Create policy / insert). */
140
+ readonly onViewportContextMenu?: () => void;
135
141
  }
136
142
 
137
143
  /** Row height presets (px) for the height menu. */
138
144
  const ROW_HEIGHTS = { short: 26, medium: 34, tall: 44, "extra-tall": 56 } as const;
139
145
  type RowHeight = keyof typeof ROW_HEIGHTS;
140
146
 
147
+ /** Payload columns absorb leftover table width (SQL `comment`, KV `value`). */
148
+ function isFillColumn(key: string): boolean {
149
+ return key === "comment" || key === "value";
150
+ }
151
+
141
152
  /** Fixed column width (px) by cell variant. */
142
153
  function columnWidthPx(type: StoreGridColumn["type"], key: string): number {
143
154
  if (key === "id" || key === "key" || key === "name") return 192;
@@ -231,6 +242,9 @@ export function StoreDataGrid({
231
242
  insertOpen = false,
232
243
  onInsertOpenChange,
233
244
  onDeleteRows,
245
+ asGate = null,
246
+ asUserId = null,
247
+ onViewportContextMenu,
234
248
  }: StoreDataGridProps): JSX.Element {
235
249
  const [findText, setFindText] = useState("");
236
250
  const [selected, setSelected] = useState<readonly string[]>([]);
@@ -250,6 +264,8 @@ export function StoreDataGrid({
250
264
  readonly rowId: string;
251
265
  readonly column: string;
252
266
  } | null>(null);
267
+ const [detailRow, setDetailRow] = useState<StoreGridRow | null>(null);
268
+ const [detailOpen, setDetailOpen] = useState(false);
253
269
  const [addOpen, setAddOpen] = useState(false);
254
270
  const [saving, setSaving] = useState(false);
255
271
  const [viewportHeight, setViewportHeight] = useState(440);
@@ -533,6 +549,8 @@ export function StoreDataGrid({
533
549
  ref: storeRef,
534
550
  ...(facet === "sql" ? { child: childName, id: rowId } : { key: rowId }),
535
551
  ...(tenant ? { tenant } : {}),
552
+ ...(asGate ? { asGate } : {}),
553
+ ...(asUserId ? { asUserId } : {}),
536
554
  patch: sanitizeStorePatch(patch),
537
555
  commit: true,
538
556
  }),
@@ -758,8 +776,8 @@ export function StoreDataGrid({
758
776
  header: <StoreColumnHeader col={col} />,
759
777
  sortable: true,
760
778
  align: isNumericType(col.type) ? "right" : "left",
761
- width: col.key === "comment" ? undefined : `${columnWidthPx(col.type, col.key)}px`,
762
- fill: col.key === "comment",
779
+ width: isFillColumn(col.key) ? undefined : `${columnWidthPx(col.type, col.key)}px`,
780
+ fill: isFillColumn(col.key),
763
781
  sortValue: (row) => {
764
782
  const raw = overlayValue(row, col.key);
765
783
  if (typeof raw === "number") return raw;
@@ -1260,6 +1278,11 @@ export function StoreDataGrid({
1260
1278
  }
1261
1279
  : undefined
1262
1280
  }
1281
+ onRowContextMenu={(row) => {
1282
+ setDetailRow(row);
1283
+ setDetailOpen(true);
1284
+ }}
1285
+ onViewportContextMenu={onViewportContextMenu}
1263
1286
  cellRange={cellRange}
1264
1287
  onCellRangeChange={
1265
1288
  canInlineEdit
@@ -1324,6 +1347,8 @@ export function StoreDataGrid({
1324
1347
  childName={childName}
1325
1348
  tenant={tenant}
1326
1349
  columns={model.columns}
1350
+ asGate={asGate}
1351
+ asUserId={asUserId}
1327
1352
  />
1328
1353
  ) : null}
1329
1354
 
@@ -1338,6 +1363,28 @@ export function StoreDataGrid({
1338
1363
  />
1339
1364
  ) : null}
1340
1365
 
1366
+ <StoreRowDetailSheet
1367
+ open={detailOpen}
1368
+ onOpenChange={setDetailOpen}
1369
+ model={model}
1370
+ row={detailRow ? (model.rows.find((row) => row.id === detailRow.id) ?? detailRow) : null}
1371
+ facet={facet}
1372
+ storeRef={storeRef}
1373
+ childName={childName}
1374
+ tenant={tenant}
1375
+ masked={masked}
1376
+ asGate={asGate}
1377
+ asUserId={asUserId}
1378
+ onDeleteRow={
1379
+ onDeleteRows
1380
+ ? (row) => {
1381
+ setDetailOpen(false);
1382
+ onDeleteRows([row]);
1383
+ }
1384
+ : undefined
1385
+ }
1386
+ />
1387
+
1341
1388
  {inspect && inspectRow && inspectCol ? (
1342
1389
  <JsonValueSheet
1343
1390
  open
@@ -1443,7 +1490,7 @@ export function StoreDataGrid({
1443
1490
  </span>
1444
1491
  ) : (
1445
1492
  <span className="ml-auto opacity-60">
1446
- Click to select · drag a range · click again or type to edit · Changes to review
1493
+ Click to select · two-finger click opens sheet · drag a range · type to edit
1447
1494
  </span>
1448
1495
  )}
1449
1496
  </div>
@@ -8,6 +8,7 @@ import {
8
8
  fileNameFromKey,
9
9
  filePreviewMode,
10
10
  formatFilePreviewText,
11
+ previewBlobType,
11
12
  inferContentType,
12
13
  isAsciiObjectKey,
13
14
  safeFileObjectKey,
@@ -66,6 +67,8 @@ describe("fileKind helpers", () => {
66
67
  expect(fileKindIsText("code")).toBe(true);
67
68
  expect(fileKindIsText("image")).toBe(false);
68
69
  expect(fileKindIsImage("image")).toBe(true);
70
+ expect(previewBlobType("pdf", "application/octet-stream")).toBe("application/pdf");
71
+ expect(previewBlobType("image", "image/png")).toBe("image/png");
69
72
  expect(filePreviewMode("pdf")).toBe("pdf");
70
73
  expect(filePreviewMode("video")).toBe("video");
71
74
  expect(filePreviewMode("archive")).toBe("none");
@@ -219,6 +219,18 @@ export function fileKindIsImage(kind: FileKind): boolean {
219
219
  return kind === "image";
220
220
  }
221
221
 
222
+ /**
223
+ * MIME type for a files-preview blob.
224
+ * PDF is always `application/pdf` so `nosniff` does not hide the viewer.
225
+ *
226
+ * @param mode - Inspector surface
227
+ * @param contentType - Catalog / inferred type
228
+ */
229
+ export function previewBlobType(mode: FilePreviewMode, contentType: string): string {
230
+ if (mode === "pdf") return "application/pdf";
231
+ return contentType;
232
+ }
233
+
222
234
  /**
223
235
  * Inspector surface for a kind (`none` → download fallback).
224
236
  *
@@ -5,7 +5,10 @@ import {
5
5
  defaultKvQuery,
6
6
  defaultSqlQuery,
7
7
  isConsoleAuthStore,
8
+ pickChildName,
8
9
  pickQueryStore,
10
+ reconcileDefaultKv,
11
+ reconcileDefaultSql,
9
12
  } from "./query-defaults.ts";
10
13
 
11
14
  const emptyWillNot = { writerFlowIds: [], signals: [], channels: [] };
@@ -53,6 +56,28 @@ describe("default queries", () => {
53
56
  expect(defaultKvQuery("holds:")).toContain("list holds:");
54
57
  expect(defaultKvQuery()).toContain("list");
55
58
  });
59
+
60
+ test("picks a random live child", () => {
61
+ expect(pickChildName(["activity", "tasks"], () => 0)).toBe("activity");
62
+ expect(pickChildName(["activity", "tasks"], () => 0.99)).toBe("tasks");
63
+ expect(pickChildName([])).toBeUndefined();
64
+ });
65
+
66
+ test("rewrites a leftover default select when the table is gone", () => {
67
+ const stale = defaultSqlQuery("issues");
68
+ expect(reconcileDefaultSql(stale, ["activity", "tasks"], () => 0.99)).toBe(
69
+ defaultSqlQuery("tasks"),
70
+ );
71
+ expect(reconcileDefaultSql(stale, ["issues"])).toBe(stale);
72
+ expect(reconcileDefaultSql("SELECT 1 FROM issues", ["activity"])).toBe("SELECT 1 FROM issues");
73
+ });
74
+
75
+ test("rewrites a leftover default kv list when the namespace is gone", () => {
76
+ const stale = defaultKvQuery("sessions:");
77
+ expect(reconcileDefaultKv(stale, ["holds", "locks"], () => 0)).toBe(defaultKvQuery("holds:"));
78
+ expect(reconcileDefaultKv(stale, ["sessions"])).toBe(stale);
79
+ expect(reconcileDefaultKv("get sessions:a", ["holds"])).toBe("get sessions:a");
80
+ });
56
81
  });
57
82
 
58
83
  describe("pickQueryStore", () => {
@@ -17,6 +17,66 @@ export function defaultSqlQuery(table?: string): string {
17
17
  return `SELECT *\nFROM "${table}"\nLIMIT 50;`;
18
18
  }
19
19
 
20
+ const DEFAULT_SQL = /^SELECT \*\nFROM "([^"]+)"\nLIMIT 50;$/;
21
+ const DEFAULT_KV = /^\/\/ list · get · set · delete · ttl\nlist(?: (.+))?$/;
22
+
23
+ /**
24
+ * Replace a leftover default `SELECT` when that table is gone.
25
+ * Custom SQL is left alone.
26
+ *
27
+ * @param text - Editor buffer
28
+ * @param tables - Live table names
29
+ * @param random - Unit interval used to pick a replacement table
30
+ */
31
+ export function reconcileDefaultSql(
32
+ text: string,
33
+ tables: readonly string[],
34
+ random: () => number = Math.random,
35
+ ): string {
36
+ const match = DEFAULT_SQL.exec(text);
37
+ if (!match) return text;
38
+ const table = match[1];
39
+ if (table && tables.includes(table)) return text;
40
+ return defaultSqlQuery(pickChildName(tables, random));
41
+ }
42
+
43
+ /**
44
+ * Replace a leftover default `list ns:` when that namespace is gone.
45
+ *
46
+ * @param text - Editor buffer
47
+ * @param namespaces - Live namespace names
48
+ * @param random - Unit interval used to pick a replacement namespace
49
+ */
50
+ export function reconcileDefaultKv(
51
+ text: string,
52
+ namespaces: readonly string[],
53
+ random: () => number = Math.random,
54
+ ): string {
55
+ const match = DEFAULT_KV.exec(text);
56
+ if (!match) return text;
57
+ const prefix = match[1];
58
+ if (!prefix) return text;
59
+ const name = prefix.endsWith(":") ? prefix.slice(0, -1) : prefix;
60
+ if (namespaces.includes(name)) return text;
61
+ const next = pickChildName(namespaces, random);
62
+ return defaultKvQuery(next ? `${next}:` : undefined);
63
+ }
64
+
65
+ /**
66
+ * Re-seed a default buffer against the live store children.
67
+ *
68
+ * @param text - Editor buffer
69
+ * @param names - Table or namespace names
70
+ * @param facet - SQL or KV
71
+ */
72
+ export function reconcileDefaultQuery(
73
+ text: string,
74
+ names: readonly string[],
75
+ facet: Extract<StoreFacet, "sql" | "kv">,
76
+ ): string {
77
+ return facet === "sql" ? reconcileDefaultSql(text, names) : reconcileDefaultKv(text, names);
78
+ }
79
+
20
80
  /**
21
81
  * Seed KV command for a namespace prefix.
22
82
  *
@@ -59,12 +119,34 @@ export function pickQueryStore(
59
119
  }
60
120
 
61
121
  /**
62
- * Default child name under a store (first table / namespace).
122
+ * One live child name, picked at random.
123
+ *
124
+ * @param names - Table or namespace names
125
+ * @param random - Unit interval in `[0, 1)`
126
+ */
127
+ export function pickChildName(
128
+ names: readonly string[],
129
+ random: () => number = Math.random,
130
+ ): string | undefined {
131
+ if (names.length === 0) return undefined;
132
+ const index = Math.min(names.length - 1, Math.floor(random() * names.length));
133
+ return names[index];
134
+ }
135
+
136
+ /**
137
+ * Default child name under a store (random table / namespace).
63
138
  *
64
139
  * @param store - Store row
140
+ * @param random - Unit interval used to pick
65
141
  */
66
- export function firstChildName(store: StoreListStore): string | undefined {
67
- return store.children[0]?.name;
142
+ export function randomChildName(
143
+ store: StoreListStore,
144
+ random: () => number = Math.random,
145
+ ): string | undefined {
146
+ return pickChildName(
147
+ store.children.map((child) => child.name),
148
+ random,
149
+ );
68
150
  }
69
151
 
70
152
  /**
@@ -1,9 +1,8 @@
1
1
  /**
2
2
  * Query-console "view as Gate" choices — Operator, public, user, or policy.
3
3
  *
4
- * Gates are OKE authorization, not Postgres `TO` roles. Selecting one sets
5
- * `oke.gate` on postgres / pglite so RLS policies can read
6
- * `current_setting('oke.gate', true)`.
4
+ * Gates are OKE authorization, not Postgres `TO` roles. Selecting one stamps
5
+ * `oke.gate()` / `oke.user()` / `oke.has_scope()` on postgres / pglite.
7
6
  */
8
7
 
9
8
  import type { RlsCatalogGate, RlsGateCatalog } from "./rls-gate-catalog.ts";
@@ -36,6 +35,12 @@ export type QueryGateUserChoice = {
36
35
  readonly gate: string;
37
36
  };
38
37
 
38
+ /** Picker value shared by SQL console, browse, and grid edit. */
39
+ export type RlsPickerValue = {
40
+ readonly asGate: string | null;
41
+ readonly asUserId: string | null;
42
+ };
43
+
39
44
  /** Identity slice used to build As-card user rows. */
40
45
  export type QueryGateIdentity = {
41
46
  readonly id: string;
@@ -175,10 +180,31 @@ export function filterQueryGateAsChoices<
175
180
  *
176
181
  * @param asGate - Selected gate name, or null for Operator
177
182
  */
178
- export function queryGateToolbarLabel(asGate: string | null): string {
183
+ export function queryGateToolbarLabel(
184
+ asGate: string | null,
185
+ asUserId?: string | null,
186
+ userLabel?: string | null,
187
+ ): string {
188
+ if (asUserId && userLabel) return `User · ${userLabel}`;
189
+ if (asUserId) return `User · ${asUserId}`;
179
190
  return asGate && asGate.length > 0 ? `Gate · ${asGate}` : "Gate";
180
191
  }
181
192
 
193
+ /**
194
+ * Request fields for store query / edit / SQL.
195
+ *
196
+ * @param value - Picker bag
197
+ */
198
+ export function rlsPickerPayload(value: RlsPickerValue): {
199
+ readonly asGate?: string;
200
+ readonly asUserId?: string;
201
+ } {
202
+ return {
203
+ ...(value.asGate ? { asGate: value.asGate } : {}),
204
+ ...(value.asUserId ? { asUserId: value.asUserId } : {}),
205
+ };
206
+ }
207
+
182
208
  function gateChoiceDetail(gate: RlsCatalogGate): string {
183
209
  if (gate.description) return gate.description;
184
210
  if (gate.roles.length > 0) return `Roles ${gate.roles.join(", ")}`;
@@ -9,7 +9,17 @@ import {
9
9
  rlsGateModeFromCommand,
10
10
  rlsPolicyPredicates,
11
11
  rlsPolicyPreviewSql,
12
+ rlsPolicyCodeSource,
13
+ rlsGatePredicateSql,
12
14
  rlsStoreGateActions,
15
+ rlsBindOwnerExpr,
16
+ rlsExprNeedsOwnerColumn,
17
+ rlsExprUsesUserIdentity,
18
+ rlsOwnerColumn,
19
+ rlsRewriteIdentityColumn,
20
+ rlsTableColumns,
21
+ rlsTableSqlColumns,
22
+ rlsTemplateUsesOwner,
13
23
  } from "./rls-policy.ts";
14
24
 
15
25
  describe("rlsPolicyPreviewSql", () => {
@@ -62,6 +72,35 @@ describe("RLS_POLICY_TEMPLATES", () => {
62
72
  expect(filterRlsPolicyTemplates(RLS_POLICY_TEMPLATES, "oke.gate").length).toBeGreaterThan(0);
63
73
  expect(filterRlsPolicyTemplates(RLS_POLICY_TEMPLATES, "owner").length).toBeGreaterThan(0);
64
74
  expect(filterRlsPolicyTemplates(RLS_POLICY_TEMPLATES, "missing")).toEqual([]);
75
+ expect(
76
+ RLS_POLICY_TEMPLATES.some((tpl) => /\bcurrent_user\b/.test(`${tpl.using} ${tpl.withCheck}`)),
77
+ ).toBe(false);
78
+ expect(
79
+ RLS_POLICY_TEMPLATES.some((tpl) => /oke\.user\(\)/.test(`${tpl.using} ${tpl.withCheck}`)),
80
+ ).toBe(true);
81
+ });
82
+ });
83
+
84
+ describe("rlsGatePredicateSql", () => {
85
+ test("fills oke.gate / oke.has_scope — not Postgres TO", () => {
86
+ expect(rlsGatePredicateSql([])).toBe("true");
87
+ expect(rlsGatePredicateSql(["member"])).toBe("oke.gate() = 'member'");
88
+ expect(rlsGatePredicateSql(["booking:create"])).toBe("oke.has_scope('booking:create')");
89
+ });
90
+ });
91
+
92
+ describe("rlsPolicyCodeSource", () => {
93
+ test("emits store.schema.policy and pgPolicy", () => {
94
+ const spec = parseSqlPolicySpec({
95
+ name: "gate_member_select",
96
+ table: "bookings",
97
+ command: "SELECT",
98
+ using: "oke.gate() = 'member'",
99
+ });
100
+ const code = rlsPolicyCodeSource(spec);
101
+ expect(code).toContain("store.schema.policy");
102
+ expect(code).toContain("pgPolicy");
103
+ expect(code).toContain("oke.gate()");
65
104
  });
66
105
  });
67
106
 
@@ -75,6 +114,106 @@ describe("rlsPolicyPredicates", () => {
75
114
  });
76
115
  });
77
116
 
117
+ describe("rls owner column bind", () => {
118
+ test("picks creator_email when owner is missing", () => {
119
+ expect(rlsOwnerColumn(["id", "title", "creator_email"])).toBe("creator_email");
120
+ expect(rlsOwnerColumn(["id", "owner_email"])).toBe("owner_email");
121
+ expect(rlsOwnerColumn(["id", "title"])).toBeNull();
122
+ expect(rlsBindOwnerExpr("owner = oke.user()", "creator_email")).toBe(
123
+ "creator_email = oke.user()",
124
+ );
125
+ expect(rlsExprNeedsOwnerColumn("owner = oke.user()")).toBe(true);
126
+ expect(rlsExprNeedsOwnerColumn("creator_email = oke.user()")).toBe(false);
127
+ });
128
+
129
+ test("reads Manifest sqlName / snake_case keys", () => {
130
+ const cols = rlsTableSqlColumns(
131
+ {
132
+ oke: "1.0",
133
+ app: "keel",
134
+ stores: {
135
+ db: {
136
+ facet: "sql",
137
+ tables: {
138
+ tasks: {
139
+ columns: {
140
+ creatorEmail: { type: "text", sqlName: "creator_email" },
141
+ title: { type: "text" },
142
+ },
143
+ },
144
+ },
145
+ },
146
+ },
147
+ },
148
+ "sql:db",
149
+ "tasks",
150
+ );
151
+ expect(cols).toEqual(["creator_email", "title"]);
152
+ expect(rlsOwnerColumn(cols)).toBe("creator_email");
153
+ });
154
+
155
+ test("marks PK / declared FK / inferred *_id", () => {
156
+ const cols = rlsTableColumns(
157
+ {
158
+ oke: "1.0",
159
+ app: "keel",
160
+ stores: {
161
+ db: {
162
+ facet: "sql",
163
+ tables: {
164
+ tasks: {
165
+ columns: {
166
+ id: { type: "text", primaryKey: true, sqlName: "id" },
167
+ identifier: { type: "text", unique: true, sqlName: "identifier" },
168
+ spaceId: {
169
+ type: "text",
170
+ sqlName: "space_id",
171
+ references: { table: "spaces", column: "id" },
172
+ },
173
+ parentId: { type: "text", sqlName: "parent_id" },
174
+ title: { type: "text", sqlName: "title" },
175
+ },
176
+ },
177
+ },
178
+ },
179
+ },
180
+ },
181
+ "sql:db",
182
+ "tasks",
183
+ );
184
+ expect(cols.find((c) => c.sqlName === "id")).toMatchObject({
185
+ primaryKey: true,
186
+ foreignKey: false,
187
+ });
188
+ expect(cols.find((c) => c.sqlName === "identifier")).toMatchObject({ unique: true });
189
+ expect(cols.find((c) => c.sqlName === "space_id")).toMatchObject({
190
+ foreignKey: true,
191
+ inferred: false,
192
+ });
193
+ expect(cols.find((c) => c.sqlName === "parent_id")).toMatchObject({
194
+ foreignKey: true,
195
+ inferred: true,
196
+ });
197
+ expect(cols.find((c) => c.sqlName === "title")).toMatchObject({
198
+ foreignKey: false,
199
+ primaryKey: false,
200
+ });
201
+ });
202
+
203
+ test("rewrites owner or a previous identity column", () => {
204
+ expect(rlsRewriteIdentityColumn("owner = oke.user()", "owner", "creator_email")).toBe(
205
+ "creator_email = oke.user()",
206
+ );
207
+ expect(
208
+ rlsRewriteIdentityColumn("creator_email = oke.user()", "creator_email", "owner_email"),
209
+ ).toBe("owner_email = oke.user()");
210
+ expect(rlsExprUsesUserIdentity("creator_email = oke.user()")).toBe(true);
211
+ expect(rlsExprUsesUserIdentity("true")).toBe(false);
212
+ expect(rlsTemplateUsesOwner({ using: "owner = oke.user()" })).toBe(true);
213
+ expect(rlsTemplateUsesOwner({ withCheck: "true" })).toBe(false);
214
+ });
215
+ });
216
+
78
217
  describe("rls gate posture", () => {
79
218
  test("maps commands to Module:Action pairs", () => {
80
219
  expect(rlsStoreGateActions("SELECT")).toEqual(["store.sql:read"]);