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
@@ -38,8 +38,14 @@ export interface ColumnReference {
38
38
  * `getSQL` is a structural bridge so drizzle-orm operators (`eq`, `isNull`, …)
39
39
  * accept abstract columns in TypeScript. OKE’s SQL compiler never calls it —
40
40
  * it reads {@link SchemaColumnDecl.sqlName} / metadata directly.
41
+ *
42
+ * `TData` / `TNotNull` are type-level only (`$infer` is a phantom). They let
43
+ * {@link InferColumnJs} / table `$inferSelect` resolve real row shapes.
41
44
  */
42
- export interface SchemaColumnDecl extends ColumnDef {
45
+ export interface SchemaColumnDecl<
46
+ TData = unknown,
47
+ TNotNull extends boolean = boolean,
48
+ > extends ColumnDef {
43
49
  /** JS object key (e.g. `createdAt`). */
44
50
  readonly key: string;
45
51
  /** Database column name (e.g. `created_at`). */
@@ -47,7 +53,12 @@ export interface SchemaColumnDecl extends ColumnDef {
47
53
  /** Declared SQL type. */
48
54
  readonly sqlType: FieldSqlType;
49
55
  readonly primaryKey: boolean;
50
- readonly notNull: boolean;
56
+ readonly notNull: TNotNull;
57
+ /**
58
+ * Phantom JS value type — not present at runtime. Distinguishes `string`
59
+ * vs `number` so structural typing does not collapse column data types.
60
+ */
61
+ readonly $infer?: TData;
51
62
  readonly unique: boolean;
52
63
  /** Literal `.default(v)` when set. */
53
64
  readonly defaultValue?: string | number | boolean | null;
@@ -69,6 +80,42 @@ export interface SchemaColumnDecl extends ColumnDef {
69
80
  getSQL(): never;
70
81
  }
71
82
 
83
+ /** Policy command on a {@link SchemaPolicyDecl}. */
84
+ export type SchemaPolicyFor = "all" | "select" | "insert" | "update" | "delete";
85
+
86
+ /** Policy behavior on a {@link SchemaPolicyDecl}. */
87
+ export type SchemaPolicyAs = "permissive" | "restrictive";
88
+
89
+ /**
90
+ * Drizzle-shaped policy extra (`pgPolicy` emit).
91
+ */
92
+ export interface SchemaPolicyDecl {
93
+ readonly kind: "schema-policy";
94
+ readonly name: string;
95
+ readonly as?: SchemaPolicyAs;
96
+ readonly to?: string | readonly string[];
97
+ readonly for?: SchemaPolicyFor;
98
+ readonly using?: string;
99
+ readonly withCheck?: string;
100
+ }
101
+
102
+ /** Enable RLS with no policies (`pgTable.withRLS`). */
103
+ export interface SchemaRlsEnableDecl {
104
+ readonly kind: "schema-rls";
105
+ }
106
+
107
+ /** Third-arg extra for {@link schemaTable}. */
108
+ export type SchemaTableExtra = SchemaPolicyDecl | SchemaRlsEnableDecl;
109
+
110
+ /** Options for {@link schemaPolicy} / Gate helpers. */
111
+ export interface SchemaPolicyOptions {
112
+ readonly as?: SchemaPolicyAs;
113
+ readonly to?: string | readonly string[];
114
+ readonly for?: SchemaPolicyFor;
115
+ readonly using?: string;
116
+ readonly withCheck?: string;
117
+ }
118
+
72
119
  /** Table from {@link store.schema.table} — extends {@link TableHandle}. */
73
120
  export interface SchemaTableDecl extends TableHandle {
74
121
  readonly kind: "schema-table";
@@ -78,35 +125,47 @@ export interface SchemaTableDecl extends TableHandle {
78
125
  * (spreading columns would otherwise shadow {@link TableHandle.name}).
79
126
  */
80
127
  readonly tableName?: string;
128
+ /** `true` when {@link schemaRls} was declared or any policy is present. */
129
+ readonly rls?: boolean;
130
+ /** Declared policies (emit + Manifest). */
131
+ readonly policies?: readonly SchemaPolicyDecl[];
81
132
  }
82
133
 
83
- /** Fluent field builder before key finalization. */
84
- export interface FieldBuilder {
85
- primaryKey(): FieldBuilder;
86
- notNull(): FieldBuilder;
87
- unique(): FieldBuilder;
88
- default(value: string | number | boolean | null): FieldBuilder;
89
- defaultFn(fn: () => unknown): FieldBuilder;
90
- pii(): FieldBuilder;
91
- sensitive(): FieldBuilder;
92
- retain(duration: string): FieldBuilder;
134
+ /**
135
+ * Fluent field builder before key finalization.
136
+ *
137
+ * `TData` is the JS value type (`string` / `number`). `TNotNull` is `true`
138
+ * after `.notNull()` or `.primaryKey()` — matching runtime finalize.
139
+ */
140
+ export interface FieldBuilder<TData = unknown, TNotNull extends boolean = false> {
141
+ primaryKey(): FieldBuilder<TData, true>;
142
+ notNull(): FieldBuilder<TData, true>;
143
+ unique(): FieldBuilder<TData, TNotNull>;
144
+ default(value: string | number | boolean | null): FieldBuilder<TData, TNotNull>;
145
+ defaultFn(fn: () => unknown): FieldBuilder<TData, TNotNull>;
146
+ pii(): FieldBuilder<TData, TNotNull>;
147
+ sensitive(): FieldBuilder<TData, TNotNull>;
148
+ retain(duration: string): FieldBuilder<TData, TNotNull>;
93
149
  /** Override snake_case SQL name. */
94
- as(sqlName: string): FieldBuilder;
150
+ as(sqlName: string): FieldBuilder<TData, TNotNull>;
95
151
  /** Optional human description for Console / docs (falls back to the JS key). */
96
- describe(description: string): FieldBuilder;
152
+ describe(description: string): FieldBuilder<TData, TNotNull>;
97
153
  /**
98
154
  * Declare a foreign key to another column (dialect-agnostic).
99
155
  *
100
156
  * @param ref - Lazy target column (`() => links.code`)
101
157
  * @param actions - Optional ON DELETE / ON UPDATE
102
158
  */
103
- references(ref: () => SchemaColumnDecl, actions?: ReferenceActions): FieldBuilder;
159
+ references(
160
+ ref: () => SchemaColumnDecl,
161
+ actions?: ReferenceActions,
162
+ ): FieldBuilder<TData, TNotNull>;
104
163
  /**
105
164
  * Bind the JS key and produce a {@link SchemaColumnDecl}.
106
165
  *
107
166
  * @param key - Object key in the table column map
108
167
  */
109
- finalize(key: string): SchemaColumnDecl;
168
+ finalize(key: string): SchemaColumnDecl<TData, TNotNull>;
110
169
  }
111
170
 
112
171
  interface FieldState {
@@ -146,31 +205,39 @@ function mergeClassification(
146
205
  return { ...(current ?? {}), ...patch };
147
206
  }
148
207
 
149
- function createBuilder(state: FieldState): FieldBuilder {
150
- const next = (patch: Partial<FieldState>): FieldBuilder => createBuilder({ ...state, ...patch });
208
+ function createBuilder<TData, TNotNull extends boolean>(
209
+ state: FieldState,
210
+ ): FieldBuilder<TData, TNotNull> {
211
+ const next = <N extends boolean>(patch: Partial<FieldState>): FieldBuilder<TData, N> =>
212
+ createBuilder<TData, N>({ ...state, ...patch });
151
213
 
152
214
  return {
153
- primaryKey: () => next({ primaryKey: true, notNull: true }),
154
- notNull: () => next({ notNull: true }),
155
- unique: () => next({ unique: true }),
156
- default: (value) => next({ defaultValue: value }),
215
+ primaryKey: () => next<true>({ primaryKey: true, notNull: true }),
216
+ notNull: () => next<true>({ notNull: true }),
217
+ unique: () => next<TNotNull>({ unique: true }),
218
+ default: (value) => next<TNotNull>({ defaultValue: value }),
157
219
  defaultFn: (fn) =>
158
- next({
220
+ next<TNotNull>({
159
221
  defaultFn: fn,
160
222
  defaultFnKind: defaultFnKindOf(fn),
161
223
  }),
162
- pii: () => next({ classification: mergeClassification(state.classification, { pii: true }) }),
224
+ pii: () =>
225
+ next<TNotNull>({ classification: mergeClassification(state.classification, { pii: true }) }),
163
226
  sensitive: () =>
164
- next({ classification: mergeClassification(state.classification, { sensitive: true }) }),
227
+ next<TNotNull>({
228
+ classification: mergeClassification(state.classification, { sensitive: true }),
229
+ }),
165
230
  retain: (duration) =>
166
- next({ classification: mergeClassification(state.classification, { retain: duration }) }),
167
- as: (sqlName) => next({ sqlName }),
168
- describe: (description) => next({ description }),
231
+ next<TNotNull>({
232
+ classification: mergeClassification(state.classification, { retain: duration }),
233
+ }),
234
+ as: (sqlName) => next<TNotNull>({ sqlName }),
235
+ describe: (description) => next<TNotNull>({ description }),
169
236
  references: (ref, actions) =>
170
- next({
237
+ next<TNotNull>({
171
238
  references: actions ? { ref, actions } : { ref },
172
239
  }),
173
- finalize(key: string): SchemaColumnDecl {
240
+ finalize(key: string): SchemaColumnDecl<TData, TNotNull> {
174
241
  const sqlName = state.sqlName ?? camelToSnake(key);
175
242
  return {
176
243
  key,
@@ -178,7 +245,7 @@ function createBuilder(state: FieldState): FieldBuilder {
178
245
  sqlName,
179
246
  sqlType: state.sqlType,
180
247
  primaryKey: state.primaryKey,
181
- notNull: state.notNull || state.primaryKey,
248
+ notNull: (state.notNull || state.primaryKey) as TNotNull,
182
249
  unique: state.unique,
183
250
  ...(state.defaultValue !== undefined ? { defaultValue: state.defaultValue } : {}),
184
251
  ...(state.defaultFn ? { defaultFn: state.defaultFn } : {}),
@@ -200,15 +267,15 @@ function createBuilder(state: FieldState): FieldBuilder {
200
267
  * Field builders — `field.text()` / `field.integer()`.
201
268
  */
202
269
  export const field = {
203
- text: (): FieldBuilder =>
204
- createBuilder({
270
+ text: (): FieldBuilder<string, false> =>
271
+ createBuilder<string, false>({
205
272
  sqlType: "text",
206
273
  primaryKey: false,
207
274
  notNull: false,
208
275
  unique: false,
209
276
  }),
210
- integer: (): FieldBuilder =>
211
- createBuilder({
277
+ integer: (): FieldBuilder<number, false> =>
278
+ createBuilder<number, false>({
212
279
  sqlType: "integer",
213
280
  primaryKey: false,
214
281
  notNull: false,
@@ -216,8 +283,19 @@ export const field = {
216
283
  }),
217
284
  } as const;
218
285
 
219
- /** Column map input for {@link schemaTable}. */
220
- export type SchemaColumnInput = FieldBuilder | SchemaColumnDecl;
286
+ /**
287
+ * Column map input for {@link schemaTable}.
288
+ *
289
+ * Explicit `string` / `number` × nullability variants so `field.text()`
290
+ * (`FieldBuilder<string, false>`) remains assignable — methodful builders
291
+ * are invariant in their type parameters.
292
+ */
293
+ export type SchemaColumnInput =
294
+ | FieldBuilder<string, false>
295
+ | FieldBuilder<string, true>
296
+ | FieldBuilder<number, false>
297
+ | FieldBuilder<number, true>
298
+ | SchemaColumnDecl;
221
299
 
222
300
  /**
223
301
  * Whether a value is a finalized {@link SchemaColumnDecl}.
@@ -261,7 +339,7 @@ export function finalizeColumnMap(
261
339
  ): Record<string, SchemaColumnDecl> {
262
340
  const out: Record<string, SchemaColumnDecl> = {};
263
341
  for (const [key, value] of Object.entries(columns)) {
264
- out[key] = isFieldBuilder(value) ? value.finalize(key) : value;
342
+ out[key] = isFieldBuilder(value) ? value.finalize(key) : (value as SchemaColumnDecl);
265
343
  }
266
344
  return out;
267
345
  }
@@ -293,14 +371,34 @@ export function schemaTableSqlName(table: SchemaTableDecl): string | undefined {
293
371
  return undefined;
294
372
  }
295
373
 
374
+ /**
375
+ * JS value type for a finalized column (`string` / `number`, plus `| null`
376
+ * when the column is nullable).
377
+ */
378
+ export type InferColumnJs<C> =
379
+ C extends SchemaColumnDecl<infer D, infer N> ? (N extends true ? D : D | null) : unknown;
380
+
381
+ /**
382
+ * Finalize a {@link schemaTable} column input into a typed decl.
383
+ */
384
+ export type FinalizeColumn<T> =
385
+ T extends FieldBuilder<infer D, infer N>
386
+ ? SchemaColumnDecl<D, N>
387
+ : T extends SchemaColumnDecl<infer D, infer N>
388
+ ? SchemaColumnDecl<D, N>
389
+ : SchemaColumnDecl;
390
+
296
391
  /**
297
392
  * Table decl with columns as own properties (`links.code`) for FK / relations.
393
+ *
394
+ * `$inferSelect` is type-level only (Drizzle's own alias) — not set at runtime.
298
395
  */
299
396
  export type SchemaTableWithColumns<
300
397
  C extends Record<string, SchemaColumnDecl> = Record<string, SchemaColumnDecl>,
301
398
  > = Omit<SchemaTableDecl, "columns"> & {
302
399
  readonly columns: Readonly<C>;
303
- } & Omit<C, "kind" | "tableName">;
400
+ readonly $inferSelect: { [K in keyof C]: InferColumnJs<C[K]> };
401
+ } & Omit<C, "kind" | "tableName" | "$inferSelect">;
304
402
 
305
403
  /**
306
404
  * Declare an abstract schema table (ORM-agnostic).
@@ -310,28 +408,147 @@ export type SchemaTableWithColumns<
310
408
  *
311
409
  * @param name - SQL table name
312
410
  * @param columns - Column map using {@link field} builders
411
+ * @param extras - RLS enable + {@link schemaPolicy} extras (Drizzle third arg)
313
412
  */
314
413
  export function schemaTable<C extends Record<string, SchemaColumnInput>>(
315
414
  name: string,
316
415
  columns: C,
317
- ): SchemaTableWithColumns<{ [K in keyof C]: SchemaColumnDecl }> {
416
+ extras: readonly SchemaTableExtra[] = [],
417
+ ): SchemaTableWithColumns<{ [K in keyof C]: FinalizeColumn<C[K]> }> {
318
418
  const finalized = finalizeColumnMap(columns);
319
419
  const stamped: Record<string, SchemaColumnDecl> = {};
320
420
  for (const [key, col] of Object.entries(finalized)) {
321
421
  stamped[key] = { ...col, tableName: name };
322
422
  }
423
+ const policies = extras.filter(
424
+ (extra): extra is SchemaPolicyDecl => extra.kind === "schema-policy",
425
+ );
426
+ const rls = extras.some((extra) => extra.kind === "schema-rls") || policies.length > 0;
323
427
  const table = {
324
428
  name,
325
429
  columns: stamped,
326
430
  ...stamped,
431
+ ...(rls ? { rls: true } : {}),
432
+ ...(policies.length > 0 ? { policies } : {}),
327
433
  };
328
434
  // Survive columns named `name` or `kind` (they would otherwise shadow
329
435
  // the table discriminant / SQL name).
330
436
  Object.defineProperty(table, "kind", { value: "schema-table", enumerable: false });
331
437
  Object.defineProperty(table, "tableName", { value: name, enumerable: false });
332
- return table as SchemaTableWithColumns<{ [K in keyof C]: SchemaColumnDecl }>;
438
+ return table as SchemaTableWithColumns<{ [K in keyof C]: FinalizeColumn<C[K]> }>;
333
439
  }
334
440
 
441
+ /**
442
+ * Enable RLS with no policies (`pgTable.withRLS`).
443
+ */
444
+ export function schemaRls(): SchemaRlsEnableDecl {
445
+ return { kind: "schema-rls" };
446
+ }
447
+
448
+ /**
449
+ * Raw Drizzle-shaped policy extra.
450
+ *
451
+ * @param name - Policy name
452
+ * @param options - `as` / `to` / `for` / `using` / `withCheck`
453
+ */
454
+ export function schemaPolicy(name: string, options: SchemaPolicyOptions = {}): SchemaPolicyDecl {
455
+ return {
456
+ kind: "schema-policy",
457
+ name,
458
+ ...(options.as !== undefined ? { as: options.as } : {}),
459
+ ...(options.to !== undefined ? { to: options.to } : {}),
460
+ ...(options.for !== undefined ? { for: options.for } : {}),
461
+ ...(options.using !== undefined ? { using: options.using } : {}),
462
+ ...(options.withCheck !== undefined ? { withCheck: options.withCheck } : {}),
463
+ };
464
+ }
465
+
466
+ function sqlStringLiteral(value: string): string {
467
+ return `'${value.replaceAll("'", "''")}'`;
468
+ }
469
+
470
+ function helperPolicyName(prefix: string, key: string, command: SchemaPolicyFor): string {
471
+ const slug = key.replace(/[^A-Za-z0-9]+/g, "_").replace(/^_|_$/g, "");
472
+ return `${prefix}_${slug}_${command}`;
473
+ }
474
+
475
+ function policyPredicates(
476
+ command: SchemaPolicyFor,
477
+ expr: string,
478
+ ): { readonly using?: string; readonly withCheck?: string } {
479
+ if (command === "insert") return { withCheck: expr };
480
+ if (command === "update" || command === "all") return { using: expr, withCheck: expr };
481
+ return { using: expr };
482
+ }
483
+
484
+ /**
485
+ * Gate-name policy — `oke.gate() = 'member'`.
486
+ *
487
+ * @param gate - Policy / public Gate name
488
+ * @param options - Command (default `select`)
489
+ */
490
+ export function schemaPolicyGate(
491
+ gate: string,
492
+ options: Pick<SchemaPolicyOptions, "for" | "as" | "to"> = {},
493
+ ): SchemaPolicyDecl {
494
+ const command = options.for ?? "select";
495
+ return schemaPolicy(helperPolicyName("gate", gate, command), {
496
+ ...options,
497
+ for: command,
498
+ ...policyPredicates(command, `oke.gate() = ${sqlStringLiteral(gate)}`),
499
+ });
500
+ }
501
+
502
+ /**
503
+ * Owner-column policy — `owner = oke.user()`.
504
+ *
505
+ * @param column - SQL / JS column name
506
+ * @param options - Command (default `all`)
507
+ */
508
+ export function schemaPolicyOwner(
509
+ column: string,
510
+ options: Pick<SchemaPolicyOptions, "for" | "as" | "to"> = {},
511
+ ): SchemaPolicyDecl {
512
+ const command = options.for ?? "all";
513
+ return schemaPolicy(helperPolicyName("owner", column, command), {
514
+ ...options,
515
+ for: command,
516
+ ...policyPredicates(command, `${column} = oke.user()`),
517
+ });
518
+ }
519
+
520
+ /**
521
+ * Scope policy — `oke.has_scope('booking:create')`.
522
+ *
523
+ * @param scope - Scope string
524
+ * @param options - Command (default `insert`)
525
+ */
526
+ export function schemaPolicyScope(
527
+ scope: string,
528
+ options: Pick<SchemaPolicyOptions, "for" | "as" | "to"> = {},
529
+ ): SchemaPolicyDecl {
530
+ const command = options.for ?? "insert";
531
+ return schemaPolicy(helperPolicyName("scope", scope, command), {
532
+ ...options,
533
+ for: command,
534
+ ...policyPredicates(command, `oke.has_scope(${sqlStringLiteral(scope)})`),
535
+ });
536
+ }
537
+
538
+ /** Callable `store.schema.policy` plus Gate helpers. */
539
+ export type SchemaPolicyApi = typeof schemaPolicy & {
540
+ readonly gate: typeof schemaPolicyGate;
541
+ readonly owner: typeof schemaPolicyOwner;
542
+ readonly scope: typeof schemaPolicyScope;
543
+ };
544
+
545
+ /** `store.schema.policy` — raw + Gate helpers. */
546
+ export const schemaPolicyApi: SchemaPolicyApi = Object.assign(schemaPolicy, {
547
+ gate: schemaPolicyGate,
548
+ owner: schemaPolicyOwner,
549
+ scope: schemaPolicyScope,
550
+ });
551
+
335
552
  // ─── Relations (mirrors drizzle-orm `defineRelations`) ───────────────────────
336
553
 
337
554
  /** Column path recorded by relation helpers (`r.links.code`). */
@@ -527,6 +744,8 @@ export function schemaRelations<T extends Readonly<Record<string, SchemaTableDec
527
744
  export const schema = {
528
745
  table: schemaTable,
529
746
  relations: schemaRelations,
747
+ rls: schemaRls,
748
+ policy: schemaPolicyApi,
530
749
  } as const;
531
750
 
532
751
  /**
@@ -61,7 +61,7 @@ export function resolveSeedIdentity(
61
61
  }
62
62
 
63
63
  /**
64
- * Confirm copy for `oke dev` / `oke db seed` — names this app's seed.
64
+ * Confirm copy for `oke db seed` — names this app's seed.
65
65
  *
66
66
  * @param identity - Resolved {@link SeedIdentity}
67
67
  */
@@ -86,6 +86,18 @@ describe("compileWhere", () => {
86
86
  expect(c.params).toEqual(["n1", "t"]);
87
87
  });
88
88
 
89
+ test("legacy array-valued StringChunk fragments still compile", () => {
90
+ const c = compileWhere({
91
+ queryChunks: [
92
+ notes.id,
93
+ { constructor: { name: "StringChunk" }, value: [" = "] },
94
+ { value: "n1" },
95
+ ],
96
+ });
97
+ expect(c.clause).toBe(`"id" = ?`);
98
+ expect(c.params).toEqual(["n1"]);
99
+ });
100
+
89
101
  test("inArray expands one placeholder per value", () => {
90
102
  const c = compileWhere(inArray(notes.id, ["a", "b", "c"]));
91
103
  expect(c.clause).toBe(`"id" in (?, ?, ?)`);
@@ -333,10 +333,33 @@ function chunksOf(node: unknown): readonly unknown[] {
333
333
  return (node as { queryChunks: unknown[] }).queryChunks;
334
334
  }
335
335
 
336
- /** Joined text of a Drizzle `StringChunk` (`{ value: string[] }`). */
336
+ /**
337
+ * True when `chunk` is a Drizzle `StringChunk` (or the synthetic array-valued
338
+ * shape `resource.ts` still emits). `Param` also has a `value` field — never
339
+ * treat those as SQL text.
340
+ */
341
+ function isStringChunk(chunk: object): boolean {
342
+ const ctor = (chunk as { constructor?: { name?: unknown } }).constructor?.name;
343
+ if (ctor === "StringChunk") return true;
344
+ const value = (chunk as { value?: unknown }).value;
345
+ return (
346
+ Array.isArray(value) &&
347
+ value.every((part) => typeof part === "string") &&
348
+ !("encoder" in chunk) &&
349
+ !("name" in chunk)
350
+ );
351
+ }
352
+
353
+ /**
354
+ * Joined text of a Drizzle `StringChunk`.
355
+ *
356
+ * drizzle-orm `1.0.0-rc.5-169397b` stores `value` as a `string`. Earlier
357
+ * snapshots (and the synthetic chunks in `resource.ts`) store `string[]`.
358
+ */
337
359
  function chunkText(chunk: unknown): string | undefined {
338
- if (!chunk || typeof chunk !== "object") return undefined;
360
+ if (!chunk || typeof chunk !== "object" || !isStringChunk(chunk)) return undefined;
339
361
  const value = (chunk as { value?: unknown }).value;
362
+ if (typeof value === "string") return value;
340
363
  return Array.isArray(value) ? value.join("") : undefined;
341
364
  }
342
365
 
@@ -394,6 +417,7 @@ function asParamValue(chunk: unknown): { readonly found: boolean; readonly value
394
417
  if (chunk === undefined) return { found: false };
395
418
  if (chunk === null || typeof chunk !== "object") return { found: true, value: chunk };
396
419
  if (Array.isArray(chunk)) return { found: false };
420
+ if (isStringChunk(chunk)) return { found: false };
397
421
  if (!("value" in (chunk as object))) return { found: false };
398
422
  if (Array.isArray((chunk as { value: unknown }).value)) return { found: false };
399
423
  if ((chunk as { name?: unknown }).name) return { found: false };
@@ -0,0 +1,165 @@
1
+ /**
2
+ * Real PGlite RLS isolation through {@link createSqlStoreHandle}.
3
+ *
4
+ * Covers the gap unit tests missed: a live policy, two concurrent identity
5
+ * bags on one shared connection, and row-level isolation. PGlite `query`
6
+ * rejects multi-command batches — the stamp is split statements pinned
7
+ * through `SqlConnection.transaction`.
8
+ */
9
+
10
+ import { afterAll, beforeAll, describe, expect, test } from "bun:test";
11
+ import { connectPglite } from "../../drivers/pglite.ts";
12
+ import { connectPostgres } from "../../drivers/postgres.ts";
13
+ import { installOkeRlsHelpers } from "../../drivers/pg-rls.ts";
14
+ import type { SqlConnection } from "../../drivers/types.ts";
15
+ import { createSqlStoreHandle, type SqlStoreHandle } from "./sql-session.ts";
16
+
17
+ const NOTES_DDL = `CREATE TABLE notes (
18
+ id text PRIMARY KEY,
19
+ owner text NOT NULL,
20
+ body text NOT NULL
21
+ )`;
22
+
23
+ const OWNER_POLICY = `CREATE POLICY owner_select ON notes
24
+ AS PERMISSIVE FOR SELECT TO public
25
+ USING (owner = oke.user())`;
26
+
27
+ /** File-scoped warmed PGlite. */
28
+ let conn: SqlConnection;
29
+ /** Stamped as alice. */
30
+ let asAlice: SqlStoreHandle;
31
+ /** Stamped as bob. */
32
+ let asBob: SqlStoreHandle;
33
+ /** No stamp — table-owner / superuser bypass. */
34
+ let asOwner: SqlStoreHandle;
35
+
36
+ beforeAll(async () => {
37
+ conn = await connectPglite({ url: "memory://sql-rls-isolation", role: "primary" });
38
+ await conn.exec(NOTES_DDL);
39
+ await conn.exec(`INSERT INTO notes VALUES ('a', 'alice', 'hi'), ('b', 'bob', 'yo')`);
40
+ await conn.exec(`ALTER TABLE notes ENABLE ROW LEVEL SECURITY`);
41
+ await installOkeRlsHelpers((sql) => conn.exec(sql));
42
+ await conn.exec(OWNER_POLICY);
43
+ asAlice = createSqlStoreHandle("sql:app", {
44
+ connection: conn,
45
+ classifications: new Map(),
46
+ routedRole: "primary",
47
+ domainDdl: "off",
48
+ rls: { gate: "member", userId: "alice", scopes: ["member"] },
49
+ });
50
+ asBob = createSqlStoreHandle("sql:app", {
51
+ connection: conn,
52
+ classifications: new Map(),
53
+ routedRole: "primary",
54
+ domainDdl: "off",
55
+ rls: { gate: "member", userId: "bob", scopes: ["member"] },
56
+ });
57
+ asOwner = createSqlStoreHandle("sql:app", {
58
+ connection: conn,
59
+ classifications: new Map(),
60
+ routedRole: "primary",
61
+ domainDdl: "off",
62
+ });
63
+ }, 15_000);
64
+
65
+ afterAll(async () => {
66
+ await conn.close();
67
+ });
68
+
69
+ describe("sql-session RLS isolation (pglite)", () => {
70
+ test("pglite query rejects a multi-command batch (the old prelude)", async () => {
71
+ await expect(
72
+ conn.query("BEGIN; SET LOCAL row_security = on; SELECT set_config('oke.user', $1, true)", [
73
+ "alice",
74
+ ]),
75
+ ).rejects.toThrow(/multiple commands/i);
76
+ });
77
+
78
+ test("sequential identities see only their rows", async () => {
79
+ const alice = await asAlice.raw(`SELECT id, owner FROM notes ORDER BY id`);
80
+ const bob = await asBob.raw(`SELECT id, owner FROM notes ORDER BY id`);
81
+ expect(alice).toEqual([{ id: "a", owner: "alice" }]);
82
+ expect(bob).toEqual([{ id: "b", owner: "bob" }]);
83
+ });
84
+
85
+ test("concurrent identities do not leak across the shared connection", async () => {
86
+ const [alice, bob, aliceAgain] = await Promise.all([
87
+ asAlice.raw(`SELECT id, owner FROM notes ORDER BY id`),
88
+ asBob.raw(`SELECT id, owner FROM notes ORDER BY id`),
89
+ asAlice.raw(`SELECT id, owner FROM notes ORDER BY id`),
90
+ ]);
91
+ expect(alice).toEqual([{ id: "a", owner: "alice" }]);
92
+ expect(bob).toEqual([{ id: "b", owner: "bob" }]);
93
+ expect(aliceAgain).toEqual([{ id: "a", owner: "alice" }]);
94
+ });
95
+
96
+ test("unstamped owner/superuser still sees every row", async () => {
97
+ const rows = await asOwner.raw(`SELECT id, owner FROM notes ORDER BY id`);
98
+ expect(rows).toEqual([
99
+ { id: "a", owner: "alice" },
100
+ { id: "b", owner: "bob" },
101
+ ]);
102
+ });
103
+ });
104
+
105
+ const LIVE_PG =
106
+ process.env.OKE_TEST_DOCKER === "1" &&
107
+ (process.env.DATABASE_URL ?? process.env.OKE_STORE_SQL_URL)?.startsWith("postgres");
108
+ if (!LIVE_PG) {
109
+ console.log("skip: live Postgres RLS isolation needs OKE_TEST_DOCKER=1 and DATABASE_URL");
110
+ }
111
+
112
+ describe("sql-session RLS isolation (live postgres)", () => {
113
+ const live = LIVE_PG ? test : test.skip;
114
+ const table = "oke_rls_isolation_notes";
115
+
116
+ live(
117
+ "split stamp isolates concurrent identities",
118
+ async () => {
119
+ const pg = await connectPostgres({
120
+ url: process.env.DATABASE_URL ?? process.env.OKE_STORE_SQL_URL,
121
+ role: "primary",
122
+ });
123
+ try {
124
+ await pg.exec(`DROP TABLE IF EXISTS ${table}`);
125
+ await pg.exec(
126
+ `CREATE TABLE ${table} (id text PRIMARY KEY, owner text NOT NULL, body text NOT NULL)`,
127
+ );
128
+ await pg.exec(`INSERT INTO ${table} VALUES ('a', 'alice', 'hi'), ('b', 'bob', 'yo')`);
129
+ await pg.exec(`ALTER TABLE ${table} ENABLE ROW LEVEL SECURITY`);
130
+ await installOkeRlsHelpers((sql) => pg.exec(sql));
131
+ await pg.exec(
132
+ `CREATE POLICY owner_select ON ${table} AS PERMISSIVE FOR SELECT TO public USING (owner = oke.user())`,
133
+ );
134
+ const alice = createSqlStoreHandle("sql:app", {
135
+ connection: pg,
136
+ classifications: new Map(),
137
+ routedRole: "primary",
138
+ domainDdl: "off",
139
+ rls: { gate: "member", userId: "alice", scopes: ["member"] },
140
+ });
141
+ const bob = createSqlStoreHandle("sql:app", {
142
+ connection: pg,
143
+ classifications: new Map(),
144
+ routedRole: "primary",
145
+ domainDdl: "off",
146
+ rls: { gate: "member", userId: "bob", scopes: ["member"] },
147
+ });
148
+ const [a, b] = await Promise.all([
149
+ alice.raw(`SELECT id, owner FROM ${table} ORDER BY id`),
150
+ bob.raw(`SELECT id, owner FROM ${table} ORDER BY id`),
151
+ ]);
152
+ expect(a).toEqual([{ id: "a", owner: "alice" }]);
153
+ expect(b).toEqual([{ id: "b", owner: "bob" }]);
154
+ } finally {
155
+ try {
156
+ await pg.exec(`DROP TABLE IF EXISTS ${table}`);
157
+ } catch {
158
+ // Best-effort cleanup.
159
+ }
160
+ await pg.close();
161
+ }
162
+ },
163
+ 20_000,
164
+ );
165
+ });