@homeflare/alchemy 0.43.0 → 0.45.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 (612) hide show
  1. package/README.md +10 -1
  2. package/dist/caddy/index.js +35 -1
  3. package/dist/caddy/index.js.map +3 -3
  4. package/dist/launchd/index.js +35 -1
  5. package/dist/launchd/index.js.map +3 -3
  6. package/dist/linux/index.js +35 -1
  7. package/dist/linux/index.js.map +3 -3
  8. package/dist/litellm/access-group-form.d.ts +26 -0
  9. package/dist/litellm/access-group-form.d.ts.map +1 -0
  10. package/dist/litellm/access-group-operations.d.ts +37 -0
  11. package/dist/litellm/access-group-operations.d.ts.map +1 -0
  12. package/dist/litellm/access-group-types.d.ts +49 -0
  13. package/dist/litellm/access-group-types.d.ts.map +1 -0
  14. package/dist/litellm/access-group.d.ts +55 -0
  15. package/dist/litellm/access-group.d.ts.map +1 -0
  16. package/dist/litellm/credential-errors.d.ts +131 -0
  17. package/dist/litellm/credential-errors.d.ts.map +1 -0
  18. package/dist/litellm/credential-form.d.ts +79 -0
  19. package/dist/litellm/credential-form.d.ts.map +1 -0
  20. package/dist/litellm/credential-operations.d.ts +84 -0
  21. package/dist/litellm/credential-operations.d.ts.map +1 -0
  22. package/dist/litellm/credential-rewrite.d.ts +6 -0
  23. package/dist/litellm/credential-rewrite.d.ts.map +1 -0
  24. package/dist/litellm/credential-types.d.ts +87 -0
  25. package/dist/litellm/credential-types.d.ts.map +1 -0
  26. package/dist/litellm/credential-values.d.ts +53 -0
  27. package/dist/litellm/credential-values.d.ts.map +1 -0
  28. package/dist/litellm/credential.d.ts +83 -0
  29. package/dist/litellm/credential.d.ts.map +1 -0
  30. package/dist/litellm/fake-access-group-litellm.d.ts +34 -0
  31. package/dist/litellm/fake-access-group-litellm.d.ts.map +1 -0
  32. package/dist/litellm/fake-credential-litellm.d.ts +54 -0
  33. package/dist/litellm/fake-credential-litellm.d.ts.map +1 -0
  34. package/dist/litellm/fake-keys.d.ts +53 -0
  35. package/dist/litellm/fake-keys.d.ts.map +1 -0
  36. package/dist/litellm/fake-litellm.d.ts +4 -1
  37. package/dist/litellm/fake-litellm.d.ts.map +1 -1
  38. package/dist/litellm/fake-mcp-litellm.d.ts +67 -0
  39. package/dist/litellm/fake-mcp-litellm.d.ts.map +1 -0
  40. package/dist/litellm/fake-model-litellm-post-update.d.ts +49 -0
  41. package/dist/litellm/fake-model-litellm-post-update.d.ts.map +1 -0
  42. package/dist/litellm/fake-model-litellm.d.ts +48 -0
  43. package/dist/litellm/fake-model-litellm.d.ts.map +1 -0
  44. package/dist/litellm/fake-policy-attachment-litellm.d.ts +33 -0
  45. package/dist/litellm/fake-policy-attachment-litellm.d.ts.map +1 -0
  46. package/dist/litellm/fake-policy-litellm.d.ts +32 -0
  47. package/dist/litellm/fake-policy-litellm.d.ts.map +1 -0
  48. package/dist/litellm/fake-registry-base.d.ts +42 -0
  49. package/dist/litellm/fake-registry-base.d.ts.map +1 -0
  50. package/dist/litellm/fake-stack.d.ts +8 -1
  51. package/dist/litellm/fake-stack.d.ts.map +1 -1
  52. package/dist/litellm/fake-team-litellm.d.ts +47 -0
  53. package/dist/litellm/fake-team-litellm.d.ts.map +1 -0
  54. package/dist/litellm/fake-tool-litellm.d.ts +28 -0
  55. package/dist/litellm/fake-tool-litellm.d.ts.map +1 -0
  56. package/dist/litellm/fake-toolset-litellm.d.ts +29 -0
  57. package/dist/litellm/fake-toolset-litellm.d.ts.map +1 -0
  58. package/dist/litellm/index.d.ts +18 -0
  59. package/dist/litellm/index.d.ts.map +1 -1
  60. package/dist/litellm/index.js +3078 -2
  61. package/dist/litellm/index.js.map +65 -4
  62. package/dist/litellm/key-errors.d.ts +169 -0
  63. package/dist/litellm/key-errors.d.ts.map +1 -0
  64. package/dist/litellm/key-form.d.ts +119 -0
  65. package/dist/litellm/key-form.d.ts.map +1 -0
  66. package/dist/litellm/key-harness.d.ts +41 -0
  67. package/dist/litellm/key-harness.d.ts.map +1 -0
  68. package/dist/litellm/key-metadata-errors.d.ts +30 -0
  69. package/dist/litellm/key-metadata-errors.d.ts.map +1 -0
  70. package/dist/litellm/key-metadata.d.ts +22 -0
  71. package/dist/litellm/key-metadata.d.ts.map +1 -0
  72. package/dist/litellm/key-operations.d.ts +63 -0
  73. package/dist/litellm/key-operations.d.ts.map +1 -0
  74. package/dist/litellm/key-secret.d.ts +33 -0
  75. package/dist/litellm/key-secret.d.ts.map +1 -0
  76. package/dist/litellm/key.d.ts +78 -0
  77. package/dist/litellm/key.d.ts.map +1 -0
  78. package/dist/litellm/mcp-server-credential.d.ts +50 -0
  79. package/dist/litellm/mcp-server-credential.d.ts.map +1 -0
  80. package/dist/litellm/mcp-server-errors.d.ts +101 -0
  81. package/dist/litellm/mcp-server-errors.d.ts.map +1 -0
  82. package/dist/litellm/mcp-server-form.d.ts +56 -0
  83. package/dist/litellm/mcp-server-form.d.ts.map +1 -0
  84. package/dist/litellm/mcp-server-locate.d.ts +26 -0
  85. package/dist/litellm/mcp-server-locate.d.ts.map +1 -0
  86. package/dist/litellm/mcp-server-operations.d.ts +63 -0
  87. package/dist/litellm/mcp-server-operations.d.ts.map +1 -0
  88. package/dist/litellm/mcp-server-shadow.d.ts +24 -0
  89. package/dist/litellm/mcp-server-shadow.d.ts.map +1 -0
  90. package/dist/litellm/mcp-server-types.d.ts +118 -0
  91. package/dist/litellm/mcp-server-types.d.ts.map +1 -0
  92. package/dist/litellm/mcp-server-url.d.ts +11 -0
  93. package/dist/litellm/mcp-server-url.d.ts.map +1 -0
  94. package/dist/litellm/mcp-server.d.ts +95 -0
  95. package/dist/litellm/mcp-server.d.ts.map +1 -0
  96. package/dist/litellm/model-claim.d.ts +24 -0
  97. package/dist/litellm/model-claim.d.ts.map +1 -0
  98. package/dist/litellm/model-credential.d.ts +12 -0
  99. package/dist/litellm/model-credential.d.ts.map +1 -0
  100. package/dist/litellm/model-errors.d.ts +119 -0
  101. package/dist/litellm/model-errors.d.ts.map +1 -0
  102. package/dist/litellm/model-form.d.ts +78 -0
  103. package/dist/litellm/model-form.d.ts.map +1 -0
  104. package/dist/litellm/model-locate.d.ts +13 -0
  105. package/dist/litellm/model-locate.d.ts.map +1 -0
  106. package/dist/litellm/model-operations.d.ts +59 -0
  107. package/dist/litellm/model-operations.d.ts.map +1 -0
  108. package/dist/litellm/model-types.d.ts +91 -0
  109. package/dist/litellm/model-types.d.ts.map +1 -0
  110. package/dist/litellm/model.d.ts +88 -0
  111. package/dist/litellm/model.d.ts.map +1 -0
  112. package/dist/litellm/policy-attachment-form.d.ts +19 -0
  113. package/dist/litellm/policy-attachment-form.d.ts.map +1 -0
  114. package/dist/litellm/policy-attachment-operations.d.ts +31 -0
  115. package/dist/litellm/policy-attachment-operations.d.ts.map +1 -0
  116. package/dist/litellm/policy-attachment-types.d.ts +48 -0
  117. package/dist/litellm/policy-attachment-types.d.ts.map +1 -0
  118. package/dist/litellm/policy-attachment.d.ts +61 -0
  119. package/dist/litellm/policy-attachment.d.ts.map +1 -0
  120. package/dist/litellm/policy-form.d.ts +22 -0
  121. package/dist/litellm/policy-form.d.ts.map +1 -0
  122. package/dist/litellm/policy-operations.d.ts +42 -0
  123. package/dist/litellm/policy-operations.d.ts.map +1 -0
  124. package/dist/litellm/policy-types.d.ts +57 -0
  125. package/dist/litellm/policy-types.d.ts.map +1 -0
  126. package/dist/litellm/policy.d.ts +65 -0
  127. package/dist/litellm/policy.d.ts.map +1 -0
  128. package/dist/litellm/providers.d.ts +1 -1
  129. package/dist/litellm/providers.d.ts.map +1 -1
  130. package/dist/litellm/registry-errors.d.ts +62 -0
  131. package/dist/litellm/registry-errors.d.ts.map +1 -0
  132. package/dist/litellm/registry-support.d.ts +42 -0
  133. package/dist/litellm/registry-support.d.ts.map +1 -0
  134. package/dist/litellm/team-form.d.ts +26 -0
  135. package/dist/litellm/team-form.d.ts.map +1 -0
  136. package/dist/litellm/team-members.d.ts +10 -0
  137. package/dist/litellm/team-members.d.ts.map +1 -0
  138. package/dist/litellm/team-operations.d.ts +38 -0
  139. package/dist/litellm/team-operations.d.ts.map +1 -0
  140. package/dist/litellm/team-permission.d.ts +35 -0
  141. package/dist/litellm/team-permission.d.ts.map +1 -0
  142. package/dist/litellm/team-types.d.ts +101 -0
  143. package/dist/litellm/team-types.d.ts.map +1 -0
  144. package/dist/litellm/team.d.ts +67 -0
  145. package/dist/litellm/team.d.ts.map +1 -0
  146. package/dist/litellm/tool-policy-form.d.ts +21 -0
  147. package/dist/litellm/tool-policy-form.d.ts.map +1 -0
  148. package/dist/litellm/tool-policy-operations.d.ts +30 -0
  149. package/dist/litellm/tool-policy-operations.d.ts.map +1 -0
  150. package/dist/litellm/tool-policy-types.d.ts +46 -0
  151. package/dist/litellm/tool-policy-types.d.ts.map +1 -0
  152. package/dist/litellm/tool-policy.d.ts +62 -0
  153. package/dist/litellm/tool-policy.d.ts.map +1 -0
  154. package/dist/litellm/toolset-form.d.ts +20 -0
  155. package/dist/litellm/toolset-form.d.ts.map +1 -0
  156. package/dist/litellm/toolset-operations.d.ts +39 -0
  157. package/dist/litellm/toolset-operations.d.ts.map +1 -0
  158. package/dist/litellm/toolset-types.d.ts +46 -0
  159. package/dist/litellm/toolset-types.d.ts.map +1 -0
  160. package/dist/litellm/toolset.d.ts +55 -0
  161. package/dist/litellm/toolset.d.ts.map +1 -0
  162. package/dist/openbao/index.js +35 -1
  163. package/dist/openbao/index.js.map +3 -3
  164. package/dist/ownership/rows.d.ts +11 -0
  165. package/dist/ownership/rows.d.ts.map +1 -1
  166. package/dist/postgres/connection.d.ts +14 -2
  167. package/dist/postgres/connection.d.ts.map +1 -1
  168. package/dist/postgres/database-sql.d.ts +27 -8
  169. package/dist/postgres/database-sql.d.ts.map +1 -1
  170. package/dist/postgres/fake-grants-parse.d.ts +37 -0
  171. package/dist/postgres/fake-grants-parse.d.ts.map +1 -0
  172. package/dist/postgres/fake-grants-read.d.ts +61 -0
  173. package/dist/postgres/fake-grants-read.d.ts.map +1 -0
  174. package/dist/postgres/fake-grants-sql.d.ts +46 -0
  175. package/dist/postgres/fake-grants-sql.d.ts.map +1 -0
  176. package/dist/postgres/fake-grants-write.d.ts +29 -0
  177. package/dist/postgres/fake-grants-write.d.ts.map +1 -0
  178. package/dist/postgres/fake-role-sql.d.ts +41 -0
  179. package/dist/postgres/fake-role-sql.d.ts.map +1 -0
  180. package/dist/postgres/fake-schema-sql.d.ts +42 -0
  181. package/dist/postgres/fake-schema-sql.d.ts.map +1 -0
  182. package/dist/postgres/fake-sql-parse.d.ts +23 -0
  183. package/dist/postgres/fake-sql-parse.d.ts.map +1 -0
  184. package/dist/postgres/fake-sql-quote.d.ts +12 -0
  185. package/dist/postgres/fake-sql-quote.d.ts.map +1 -0
  186. package/dist/postgres/fake-sql.d.ts +42 -0
  187. package/dist/postgres/fake-sql.d.ts.map +1 -1
  188. package/dist/postgres/grants-attrs.d.ts +115 -0
  189. package/dist/postgres/grants-attrs.d.ts.map +1 -0
  190. package/dist/postgres/grants-declare.d.ts +88 -0
  191. package/dist/postgres/grants-declare.d.ts.map +1 -0
  192. package/dist/postgres/grants-diff.d.ts +25 -0
  193. package/dist/postgres/grants-diff.d.ts.map +1 -0
  194. package/dist/postgres/grants-errors.d.ts +137 -0
  195. package/dist/postgres/grants-errors.d.ts.map +1 -0
  196. package/dist/postgres/grants-existence.d.ts +14 -0
  197. package/dist/postgres/grants-existence.d.ts.map +1 -0
  198. package/dist/postgres/grants-ops.d.ts +65 -0
  199. package/dist/postgres/grants-ops.d.ts.map +1 -0
  200. package/dist/postgres/grants-ownership.d.ts +18 -0
  201. package/dist/postgres/grants-ownership.d.ts.map +1 -0
  202. package/dist/postgres/grants-plan-restore.d.ts +22 -0
  203. package/dist/postgres/grants-plan-restore.d.ts.map +1 -0
  204. package/dist/postgres/grants-plan.d.ts +65 -0
  205. package/dist/postgres/grants-plan.d.ts.map +1 -0
  206. package/dist/postgres/grants-read-defaults.d.ts +17 -0
  207. package/dist/postgres/grants-read-defaults.d.ts.map +1 -0
  208. package/dist/postgres/grants-read.d.ts +77 -0
  209. package/dist/postgres/grants-read.d.ts.map +1 -0
  210. package/dist/postgres/grants-refuse.d.ts +25 -0
  211. package/dist/postgres/grants-refuse.d.ts.map +1 -0
  212. package/dist/postgres/grants-sql.d.ts +42 -0
  213. package/dist/postgres/grants-sql.d.ts.map +1 -0
  214. package/dist/postgres/grants-words.d.ts +29 -0
  215. package/dist/postgres/grants-words.d.ts.map +1 -0
  216. package/dist/postgres/grants.d.ts +37 -0
  217. package/dist/postgres/grants.d.ts.map +1 -0
  218. package/dist/postgres/index.d.ts +31 -6
  219. package/dist/postgres/index.d.ts.map +1 -1
  220. package/dist/postgres/index.js +1998 -37
  221. package/dist/postgres/index.js.map +46 -7
  222. package/dist/postgres/installed-classifier.d.ts +13 -0
  223. package/dist/postgres/installed-classifier.d.ts.map +1 -0
  224. package/dist/postgres/providers.d.ts +16 -8
  225. package/dist/postgres/providers.d.ts.map +1 -1
  226. package/dist/postgres/psql-executor.d.ts +4 -0
  227. package/dist/postgres/psql-executor.d.ts.map +1 -1
  228. package/dist/postgres/role-attrs.d.ts +112 -0
  229. package/dist/postgres/role-attrs.d.ts.map +1 -0
  230. package/dist/postgres/role-diff.d.ts +22 -0
  231. package/dist/postgres/role-diff.d.ts.map +1 -0
  232. package/dist/postgres/role-errors.d.ts +132 -0
  233. package/dist/postgres/role-errors.d.ts.map +1 -0
  234. package/dist/postgres/role-membership-sql.d.ts +78 -0
  235. package/dist/postgres/role-membership-sql.d.ts.map +1 -0
  236. package/dist/postgres/role-provider.d.ts +11 -0
  237. package/dist/postgres/role-provider.d.ts.map +1 -0
  238. package/dist/postgres/role-scram.d.ts +8 -0
  239. package/dist/postgres/role-scram.d.ts.map +1 -0
  240. package/dist/postgres/role-secrets.d.ts +38 -0
  241. package/dist/postgres/role-secrets.d.ts.map +1 -0
  242. package/dist/postgres/role-sql.d.ts +68 -0
  243. package/dist/postgres/role-sql.d.ts.map +1 -0
  244. package/dist/postgres/role.d.ts +57 -0
  245. package/dist/postgres/role.d.ts.map +1 -0
  246. package/dist/postgres/schema-assert.d.ts +50 -0
  247. package/dist/postgres/schema-assert.d.ts.map +1 -0
  248. package/dist/postgres/schema-attrs.d.ts +51 -0
  249. package/dist/postgres/schema-attrs.d.ts.map +1 -0
  250. package/dist/postgres/schema-delete.d.ts +28 -0
  251. package/dist/postgres/schema-delete.d.ts.map +1 -0
  252. package/dist/postgres/schema-diff.d.ts +14 -0
  253. package/dist/postgres/schema-diff.d.ts.map +1 -0
  254. package/dist/postgres/schema-errors.d.ts +139 -0
  255. package/dist/postgres/schema-errors.d.ts.map +1 -0
  256. package/dist/postgres/schema-identity.d.ts +8 -0
  257. package/dist/postgres/schema-identity.d.ts.map +1 -0
  258. package/dist/postgres/schema-reconcile.d.ts +12 -0
  259. package/dist/postgres/schema-reconcile.d.ts.map +1 -0
  260. package/dist/postgres/schema-sql.d.ts +45 -0
  261. package/dist/postgres/schema-sql.d.ts.map +1 -0
  262. package/dist/postgres/schema-test-kit.d.ts +67 -0
  263. package/dist/postgres/schema-test-kit.d.ts.map +1 -0
  264. package/dist/postgres/schema.d.ts +54 -0
  265. package/dist/postgres/schema.d.ts.map +1 -0
  266. package/dist/proxmox/index.js +35 -1
  267. package/dist/proxmox/index.js.map +3 -3
  268. package/dist/release/fake-engine.d.ts +10 -0
  269. package/dist/release/fake-engine.d.ts.map +1 -1
  270. package/dist/release/index.js +35 -1
  271. package/dist/release/index.js.map +3 -3
  272. package/dist/valkey/acl-attrs.d.ts +94 -0
  273. package/dist/valkey/acl-attrs.d.ts.map +1 -0
  274. package/dist/valkey/acl-compare.d.ts +28 -0
  275. package/dist/valkey/acl-compare.d.ts.map +1 -0
  276. package/dist/valkey/acl-connection.d.ts +6 -0
  277. package/dist/valkey/acl-connection.d.ts.map +1 -0
  278. package/dist/valkey/acl-form.d.ts +62 -0
  279. package/dist/valkey/acl-form.d.ts.map +1 -0
  280. package/dist/valkey/acl-managed.d.ts +4 -0
  281. package/dist/valkey/acl-managed.d.ts.map +1 -0
  282. package/dist/valkey/acl-ops.d.ts +37 -0
  283. package/dist/valkey/acl-ops.d.ts.map +1 -0
  284. package/dist/valkey/acl-profiles.d.ts +53 -0
  285. package/dist/valkey/acl-profiles.d.ts.map +1 -0
  286. package/dist/valkey/acl-reconcile.d.ts +26 -0
  287. package/dist/valkey/acl-reconcile.d.ts.map +1 -0
  288. package/dist/valkey/acl.d.ts +24 -0
  289. package/dist/valkey/acl.d.ts.map +1 -0
  290. package/dist/valkey/connection-service.d.ts +22 -0
  291. package/dist/valkey/connection-service.d.ts.map +1 -0
  292. package/dist/valkey/connection.d.ts +59 -0
  293. package/dist/valkey/connection.d.ts.map +1 -0
  294. package/dist/valkey/errors.d.ts +165 -0
  295. package/dist/valkey/errors.d.ts.map +1 -0
  296. package/dist/valkey/fake-acl.d.ts +29 -0
  297. package/dist/valkey/fake-acl.d.ts.map +1 -0
  298. package/dist/valkey/fake-valkey.d.ts +35 -0
  299. package/dist/valkey/fake-valkey.d.ts.map +1 -0
  300. package/dist/valkey/handler-test-fixture.d.ts +20 -0
  301. package/dist/valkey/handler-test-fixture.d.ts.map +1 -0
  302. package/dist/valkey/index.d.ts +18 -0
  303. package/dist/valkey/index.d.ts.map +1 -0
  304. package/dist/valkey/index.js +964 -0
  305. package/dist/valkey/index.js.map +27 -0
  306. package/dist/valkey/instance-attrs.d.ts +46 -0
  307. package/dist/valkey/instance-attrs.d.ts.map +1 -0
  308. package/dist/valkey/instance-form.d.ts +22 -0
  309. package/dist/valkey/instance-form.d.ts.map +1 -0
  310. package/dist/valkey/instance.d.ts +48 -0
  311. package/dist/valkey/instance.d.ts.map +1 -0
  312. package/dist/valkey/provider-socket-fixture.d.ts +8 -0
  313. package/dist/valkey/provider-socket-fixture.d.ts.map +1 -0
  314. package/dist/valkey/providers.d.ts +16 -0
  315. package/dist/valkey/providers.d.ts.map +1 -0
  316. package/dist/valkey/resp.d.ts +38 -0
  317. package/dist/valkey/resp.d.ts.map +1 -0
  318. package/dist/valkey/transport-error.d.ts +36 -0
  319. package/dist/valkey/transport-error.d.ts.map +1 -0
  320. package/dist/valkey/transport.d.ts +50 -0
  321. package/dist/valkey/transport.d.ts.map +1 -0
  322. package/docs/litellm-access-group.md +105 -0
  323. package/docs/litellm-credential.md +60 -0
  324. package/docs/litellm-key-metadata.md +56 -0
  325. package/docs/litellm-key.md +194 -0
  326. package/docs/litellm-mcp.md +187 -0
  327. package/docs/litellm-model.md +111 -0
  328. package/docs/litellm-policy-attachment.md +86 -0
  329. package/docs/litellm-policy.md +103 -0
  330. package/docs/litellm-team.md +117 -0
  331. package/docs/litellm-tool-policy.md +90 -0
  332. package/docs/litellm-toolset.md +92 -0
  333. package/docs/litellm.md +16 -9
  334. package/docs/peers.md +15 -6
  335. package/docs/postgres-grants-example.md +55 -0
  336. package/docs/postgres-grants.md +157 -0
  337. package/docs/postgres-role.md +126 -0
  338. package/docs/postgres-schema.md +139 -0
  339. package/docs/postgres.md +20 -3
  340. package/docs/valkey-transport.md +29 -0
  341. package/docs/valkey.md +187 -0
  342. package/package.json +6 -2
  343. package/src/ceph/ceph-argv.test.ts +1 -1
  344. package/src/launchd/sudo-allowlist.test.ts +2 -2
  345. package/src/linux/sudo-allowlist.test.ts +3 -3
  346. package/src/linux/sudo-runner.test.ts +2 -2
  347. package/src/litellm/access-group-form.ts +105 -0
  348. package/src/litellm/access-group-operations.ts +92 -0
  349. package/src/litellm/access-group-types.ts +49 -0
  350. package/src/litellm/access-group.test.ts +197 -0
  351. package/src/litellm/access-group.ts +151 -0
  352. package/src/litellm/credential-errors.ts +183 -0
  353. package/src/litellm/credential-form.test.ts +151 -0
  354. package/src/litellm/credential-form.ts +211 -0
  355. package/src/litellm/credential-guards.test.ts +241 -0
  356. package/src/litellm/credential-operations.ts +208 -0
  357. package/src/litellm/credential-recovery.test.ts +93 -0
  358. package/src/litellm/credential-rewrite.test.ts +164 -0
  359. package/src/litellm/credential-rewrite.ts +50 -0
  360. package/src/litellm/credential-types.ts +117 -0
  361. package/src/litellm/credential-values.ts +94 -0
  362. package/src/litellm/credential.test.ts +218 -0
  363. package/src/litellm/credential.ts +227 -0
  364. package/src/litellm/fake-access-group-litellm.ts +127 -0
  365. package/src/litellm/fake-credential-litellm.ts +216 -0
  366. package/src/litellm/fake-keys.ts +190 -0
  367. package/src/litellm/fake-litellm.ts +19 -9
  368. package/src/litellm/fake-mcp-litellm.ts +214 -0
  369. package/src/litellm/fake-model-litellm-post-update.ts +103 -0
  370. package/src/litellm/fake-model-litellm.ts +233 -0
  371. package/src/litellm/fake-policy-attachment-litellm.ts +97 -0
  372. package/src/litellm/fake-policy-litellm.ts +186 -0
  373. package/src/litellm/fake-registry-base.ts +69 -0
  374. package/src/litellm/fake-stack.ts +17 -1
  375. package/src/litellm/fake-team-litellm.ts +229 -0
  376. package/src/litellm/fake-tool-litellm.ts +88 -0
  377. package/src/litellm/fake-toolset-litellm.ts +110 -0
  378. package/src/litellm/index.ts +140 -0
  379. package/src/litellm/key-delete.test.ts +212 -0
  380. package/src/litellm/key-errors.ts +241 -0
  381. package/src/litellm/key-form.ts +200 -0
  382. package/src/litellm/key-harness.ts +121 -0
  383. package/src/litellm/key-metadata-errors.ts +49 -0
  384. package/src/litellm/key-metadata.test.ts +156 -0
  385. package/src/litellm/key-metadata.ts +74 -0
  386. package/src/litellm/key-operations.ts +184 -0
  387. package/src/litellm/key-rotation.test.ts +110 -0
  388. package/src/litellm/key-scope.test.ts +135 -0
  389. package/src/litellm/key-secret.test.ts +248 -0
  390. package/src/litellm/key-secret.ts +137 -0
  391. package/src/litellm/key-transport.test.ts +166 -0
  392. package/src/litellm/key.test.ts +231 -0
  393. package/src/litellm/key.ts +213 -0
  394. package/src/litellm/mcp-server-adopt.test.ts +86 -0
  395. package/src/litellm/mcp-server-alias.test.ts +184 -0
  396. package/src/litellm/mcp-server-credential.ts +85 -0
  397. package/src/litellm/mcp-server-errors.ts +141 -0
  398. package/src/litellm/mcp-server-exports.test.ts +15 -0
  399. package/src/litellm/mcp-server-form.test.ts +241 -0
  400. package/src/litellm/mcp-server-form.ts +233 -0
  401. package/src/litellm/mcp-server-handlers.test.ts +150 -0
  402. package/src/litellm/mcp-server-locate.ts +66 -0
  403. package/src/litellm/mcp-server-operations.ts +111 -0
  404. package/src/litellm/mcp-server-registry.test.ts +146 -0
  405. package/src/litellm/mcp-server-retype.test.ts +114 -0
  406. package/src/litellm/mcp-server-shadow.test.ts +91 -0
  407. package/src/litellm/mcp-server-shadow.ts +45 -0
  408. package/src/litellm/mcp-server-tools.test.ts +85 -0
  409. package/src/litellm/mcp-server-types.ts +143 -0
  410. package/src/litellm/mcp-server-url.ts +66 -0
  411. package/src/litellm/mcp-server.test.ts +213 -0
  412. package/src/litellm/mcp-server.ts +225 -0
  413. package/src/litellm/model-claim.ts +69 -0
  414. package/src/litellm/model-credential.ts +45 -0
  415. package/src/litellm/model-errors.ts +168 -0
  416. package/src/litellm/model-foreign-row.test.ts +173 -0
  417. package/src/litellm/model-form.test.ts +228 -0
  418. package/src/litellm/model-form.ts +223 -0
  419. package/src/litellm/model-locate.test.ts +128 -0
  420. package/src/litellm/model-locate.ts +67 -0
  421. package/src/litellm/model-operations.test.ts +93 -0
  422. package/src/litellm/model-operations.ts +146 -0
  423. package/src/litellm/model-params-preserve.test.ts +119 -0
  424. package/src/litellm/model-recovery.test.ts +57 -0
  425. package/src/litellm/model-types.ts +112 -0
  426. package/src/litellm/model.test.ts +222 -0
  427. package/src/litellm/model.ts +237 -0
  428. package/src/litellm/policy-attachment-form.ts +103 -0
  429. package/src/litellm/policy-attachment-operations.ts +74 -0
  430. package/src/litellm/policy-attachment-types.ts +48 -0
  431. package/src/litellm/policy-attachment.test.ts +198 -0
  432. package/src/litellm/policy-attachment.ts +178 -0
  433. package/src/litellm/policy-form.ts +122 -0
  434. package/src/litellm/policy-operations.ts +112 -0
  435. package/src/litellm/policy-types.ts +58 -0
  436. package/src/litellm/policy.test.ts +214 -0
  437. package/src/litellm/policy.ts +146 -0
  438. package/src/litellm/providers.ts +24 -3
  439. package/src/litellm/registry-errors.ts +88 -0
  440. package/src/litellm/registry-exports.test.ts +66 -0
  441. package/src/litellm/registry-support.ts +109 -0
  442. package/src/litellm/team-form.ts +140 -0
  443. package/src/litellm/team-members.test.ts +150 -0
  444. package/src/litellm/team-members.ts +58 -0
  445. package/src/litellm/team-operations.ts +90 -0
  446. package/src/litellm/team-permission.ts +159 -0
  447. package/src/litellm/team-read.test.ts +89 -0
  448. package/src/litellm/team-types.ts +106 -0
  449. package/src/litellm/team.test.ts +206 -0
  450. package/src/litellm/team.ts +159 -0
  451. package/src/litellm/tool-policy-form.ts +94 -0
  452. package/src/litellm/tool-policy-operations.ts +58 -0
  453. package/src/litellm/tool-policy-types.ts +48 -0
  454. package/src/litellm/tool-policy.test.ts +172 -0
  455. package/src/litellm/tool-policy.ts +125 -0
  456. package/src/litellm/toolset-form.ts +104 -0
  457. package/src/litellm/toolset-operations.ts +90 -0
  458. package/src/litellm/toolset-types.ts +47 -0
  459. package/src/litellm/toolset.test.ts +198 -0
  460. package/src/litellm/toolset.ts +151 -0
  461. package/src/ownership/ownership.test.ts +33 -1
  462. package/src/ownership/rows.ts +53 -0
  463. package/src/postgres/connection.ts +42 -7
  464. package/src/postgres/database-bigint-serialization.test.ts +1 -0
  465. package/src/postgres/database-refusals.test.ts +2 -2
  466. package/src/postgres/database-sql.ts +36 -9
  467. package/src/postgres/fake-grants-parse.ts +189 -0
  468. package/src/postgres/fake-grants-read.ts +250 -0
  469. package/src/postgres/fake-grants-sql.ts +207 -0
  470. package/src/postgres/fake-grants-write.ts +90 -0
  471. package/src/postgres/fake-role-sql.ts +240 -0
  472. package/src/postgres/fake-schema-semantics.test.ts +36 -0
  473. package/src/postgres/fake-schema-sql.ts +146 -0
  474. package/src/postgres/fake-sql-parse.ts +86 -0
  475. package/src/postgres/fake-sql-quote.ts +36 -0
  476. package/src/postgres/fake-sql.ts +166 -46
  477. package/src/postgres/fixtures/pg-REL_18_6-acl.h.txt +147 -0
  478. package/src/postgres/fixtures/pg-REL_18_6-aclexplode-func.txt +31 -0
  479. package/src/postgres/fixtures/pg-REL_18_6-alter-default-privileges-synopsis.txt +82 -0
  480. package/src/postgres/fixtures/pg-REL_18_6-alter_role.sgml.txt +44 -0
  481. package/src/postgres/fixtures/pg-REL_18_6-create_role.sgml.txt +26 -0
  482. package/src/postgres/fixtures/pg-REL_18_6-grant-synopsis.txt +95 -0
  483. package/src/postgres/fixtures/pg-REL_18_6-pg_attribute.h.txt +24 -0
  484. package/src/postgres/fixtures/pg-REL_18_6-pg_auth_members.h.txt +62 -0
  485. package/src/postgres/fixtures/pg-REL_18_6-pg_authid.h.txt +70 -0
  486. package/src/postgres/fixtures/pg-REL_18_6-pg_class.h.txt +33 -0
  487. package/src/postgres/fixtures/pg-REL_18_6-pg_default_acl.h.txt +81 -0
  488. package/src/postgres/fixtures/pg-REL_18_6-pg_namespace.h.txt +28 -0
  489. package/src/postgres/fixtures/pg-REL_18_6-revoke-synopsis.txt +124 -0
  490. package/src/postgres/grants-acl.test.ts +61 -0
  491. package/src/postgres/grants-attrs.ts +185 -0
  492. package/src/postgres/grants-database.test.ts +158 -0
  493. package/src/postgres/grants-declare.ts +172 -0
  494. package/src/postgres/grants-delete-proof.test.ts +154 -0
  495. package/src/postgres/grants-diff.ts +101 -0
  496. package/src/postgres/grants-errors.ts +215 -0
  497. package/src/postgres/grants-existence-guards.test.ts +113 -0
  498. package/src/postgres/grants-existence.ts +101 -0
  499. package/src/postgres/grants-lifecycle-guards.test.ts +208 -0
  500. package/src/postgres/grants-lifecycle-repair.test.ts +208 -0
  501. package/src/postgres/grants-lifecycle.test.ts +181 -0
  502. package/src/postgres/grants-ops.ts +229 -0
  503. package/src/postgres/grants-owner-revoke.test.ts +121 -0
  504. package/src/postgres/grants-ownership.ts +49 -0
  505. package/src/postgres/grants-plan-collateral.test.ts +202 -0
  506. package/src/postgres/grants-plan-diff.test.ts +204 -0
  507. package/src/postgres/grants-plan-restore.ts +43 -0
  508. package/src/postgres/grants-plan-revocations.test.ts +69 -0
  509. package/src/postgres/grants-plan.test.ts +148 -0
  510. package/src/postgres/grants-plan.ts +245 -0
  511. package/src/postgres/grants-provenance.test.ts +134 -0
  512. package/src/postgres/grants-psql-lifecycle.test.ts +105 -0
  513. package/src/postgres/grants-read-absent.test.ts +91 -0
  514. package/src/postgres/grants-read-defaults.ts +51 -0
  515. package/src/postgres/grants-read.ts +250 -0
  516. package/src/postgres/grants-refusals.test.ts +136 -0
  517. package/src/postgres/grants-refuse.ts +119 -0
  518. package/src/postgres/grants-relkind-wire.test.ts +111 -0
  519. package/src/postgres/grants-repair-plan.test.ts +201 -0
  520. package/src/postgres/grants-sql.test.ts +128 -0
  521. package/src/postgres/grants-sql.ts +144 -0
  522. package/src/postgres/grants-words.ts +34 -0
  523. package/src/postgres/grants.ts +110 -0
  524. package/src/postgres/index.ts +129 -5
  525. package/src/postgres/installed-classifier.ts +36 -0
  526. package/src/postgres/providers.ts +33 -9
  527. package/src/postgres/psql-executor-escaping.test.ts +67 -0
  528. package/src/postgres/psql-executor.test.ts +63 -2
  529. package/src/postgres/psql-executor.ts +74 -34
  530. package/src/postgres/role-adopt.test.ts +98 -0
  531. package/src/postgres/role-attrs.ts +137 -0
  532. package/src/postgres/role-diff.ts +93 -0
  533. package/src/postgres/role-errors.ts +206 -0
  534. package/src/postgres/role-guard.test.ts +60 -0
  535. package/src/postgres/role-identity.test.ts +197 -0
  536. package/src/postgres/role-lifecycle.test.ts +228 -0
  537. package/src/postgres/role-membership-options.test.ts +73 -0
  538. package/src/postgres/role-membership-repair.test.ts +153 -0
  539. package/src/postgres/role-membership-sql.ts +223 -0
  540. package/src/postgres/role-password-ascii.test.ts +53 -0
  541. package/src/postgres/role-password-transports.test.ts +123 -0
  542. package/src/postgres/role-password.test.ts +158 -0
  543. package/src/postgres/role-privilege.test.ts +107 -0
  544. package/src/postgres/role-provenance.test.ts +200 -0
  545. package/src/postgres/role-provider.ts +105 -0
  546. package/src/postgres/role-refusals.test.ts +225 -0
  547. package/src/postgres/role-scram.test.ts +38 -0
  548. package/src/postgres/role-scram.ts +40 -0
  549. package/src/postgres/role-secrets.ts +61 -0
  550. package/src/postgres/role-sql.ts +213 -0
  551. package/src/postgres/role-valid-until.test.ts +68 -0
  552. package/src/postgres/role.ts +226 -0
  553. package/src/postgres/schema-assert.ts +139 -0
  554. package/src/postgres/schema-attrs.ts +71 -0
  555. package/src/postgres/schema-delete-proofs.test.ts +192 -0
  556. package/src/postgres/schema-delete.ts +96 -0
  557. package/src/postgres/schema-diff.ts +44 -0
  558. package/src/postgres/schema-drop-2bp01.test.ts +146 -0
  559. package/src/postgres/schema-errors.ts +209 -0
  560. package/src/postgres/schema-handlers.test.ts +193 -0
  561. package/src/postgres/schema-identity.ts +39 -0
  562. package/src/postgres/schema-lifecycle.test.ts +246 -0
  563. package/src/postgres/schema-race.test.ts +70 -0
  564. package/src/postgres/schema-reconcile.ts +100 -0
  565. package/src/postgres/schema-refusals.test.ts +144 -0
  566. package/src/postgres/schema-socket.test.ts +162 -0
  567. package/src/postgres/schema-sql.ts +130 -0
  568. package/src/postgres/schema-takeover.test.ts +149 -0
  569. package/src/postgres/schema-test-kit.ts +85 -0
  570. package/src/postgres/schema.ts +130 -0
  571. package/src/release/binary-alias.test.ts +3 -3
  572. package/src/release/binary-claims.test.ts +3 -3
  573. package/src/release/binary-directory-umask.test.ts +65 -0
  574. package/src/release/fake-engine.ts +15 -0
  575. package/src/valkey/acl-attrs.ts +98 -0
  576. package/src/valkey/acl-compare.ts +57 -0
  577. package/src/valkey/acl-connection.ts +13 -0
  578. package/src/valkey/acl-delete.test.ts +127 -0
  579. package/src/valkey/acl-form.ts +239 -0
  580. package/src/valkey/acl-managed.ts +13 -0
  581. package/src/valkey/acl-ops.ts +122 -0
  582. package/src/valkey/acl-ownership.test.ts +118 -0
  583. package/src/valkey/acl-prefix.test.ts +38 -0
  584. package/src/valkey/acl-profiles.ts +81 -0
  585. package/src/valkey/acl-reconcile.ts +180 -0
  586. package/src/valkey/acl-reserved.test.ts +69 -0
  587. package/src/valkey/acl-revocation.test.ts +94 -0
  588. package/src/valkey/acl-seat-prefix.test.ts +111 -0
  589. package/src/valkey/acl-validation.test.ts +54 -0
  590. package/src/valkey/acl.test.ts +215 -0
  591. package/src/valkey/acl.ts +169 -0
  592. package/src/valkey/connection-service.ts +46 -0
  593. package/src/valkey/connection.ts +180 -0
  594. package/src/valkey/errors.ts +239 -0
  595. package/src/valkey/fake-acl.ts +128 -0
  596. package/src/valkey/fake-valkey.ts +224 -0
  597. package/src/valkey/handler-test-fixture.ts +43 -0
  598. package/src/valkey/handlers.test.ts +205 -0
  599. package/src/valkey/index.ts +54 -0
  600. package/src/valkey/instance-attrs.ts +48 -0
  601. package/src/valkey/instance-form.ts +104 -0
  602. package/src/valkey/instance.test.ts +108 -0
  603. package/src/valkey/instance.ts +160 -0
  604. package/src/valkey/provider-socket-fixture.ts +71 -0
  605. package/src/valkey/providers.test.ts +129 -0
  606. package/src/valkey/providers.ts +21 -0
  607. package/src/valkey/resp.ts +212 -0
  608. package/src/valkey/transport-bounds.test.ts +150 -0
  609. package/src/valkey/transport-error.ts +37 -0
  610. package/src/valkey/transport.test.ts +158 -0
  611. package/src/valkey/transport.ts +109 -0
  612. package/src/valkey/upstream-bounds.test.ts +14 -0
@@ -0,0 +1,213 @@
1
+ /**
2
+ * The statements `Postgres.Role` ever issues, and the one read that backs them.
3
+ *
4
+ * ⛔ `CREATE ROLE` / `ALTER ROLE` TAKE NO BIND PARAMETERS FOR ANY OF THEIR VALUES. Measured at
5
+ * `gram.y@REL_18_6`: every option value in `CreateOptRoleStmt` is a `RoleSpec`, a signed
6
+ * `Iconst` or an `Sconst`, and none of those productions reaches `PARAM` (`$n`). Only the
7
+ * catalog reads below bind a role name (`$1`).
8
+ * ⛔ THE PASSWORD NEVER CROSSES THE WIRE AS A PLAIN LITERAL. Postgres has no bind form for it in
9
+ * `CREATE`/`ALTER ROLE`, so the statement carries the SCRAM-SHA-256 verifier `role-scram.ts`
10
+ * computes client-side (what `psql \password` sends; stored as-is, never reversible): the
11
+ * statement text that reaches the span attribute `db.query.text`, `pg_stat_statements` and a
12
+ * failed `ALTER`'s server log holds no secret. It is quoted with `quoteStringLiteral` like every
13
+ * other string value and NEVER written to state — only its seal is (`role-attrs.ts`,
14
+ * `passwordSeal`).
15
+ * ★ MEMBERSHIP IS TWO ONE-PARENT STATEMENTS (`GRANT` / `REVOKE`), never a multi-role list, so a
16
+ * partially-failed membership write is observable in the state that remains. Those statements
17
+ * and the `pg_auth_members` read live in `role-membership-sql.ts` — this file stays the role
18
+ * row (`pg_roles`), its scalar statements and the compare between them.
19
+ */
20
+ import type { SqlError } from 'effect/unstable/sql/SqlError';
21
+ import * as Effect from 'effect/Effect';
22
+ import type { PgExecutor } from './database-sql.ts';
23
+ import { quoteIdent, quoteStringLiteral } from './database-sql.ts';
24
+ import { buildGrantMembershipSql, selectRoleMemberships } from './role-membership-sql.ts';
25
+ import {
26
+ type PostgresRoleAttributes,
27
+ type PostgresRoleProps,
28
+ sameValidUntil,
29
+ } from './role-attrs.ts';
30
+
31
+ /** The one read: the row from the `pg_roles` view, plus this role's memberships from
32
+ * `pg_auth_members`. `rolvaliduntil` is a `timestamptz`; it is serialised on the server side
33
+ * (`to_char(rolvaliduntil AT TIME ZONE 'UTC', …)`) so the value that reaches state is a plain
34
+ * ISO-8601 string, never a driver-specific Date that `encodeState` would persist differently
35
+ * across transports (the socket client decodes it as a JS Date; the runner transport would hand
36
+ * back a string). */
37
+ const SELECT_ROLE_SQL = `SELECT
38
+ r.oid AS oid,
39
+ r.rolname AS name,
40
+ r.rolcanlogin AS login,
41
+ r.rolconnlimit AS "connectionLimit",
42
+ r.rolinherit AS inherit,
43
+ r.rolsuper AS superuser,
44
+ r.rolcreaterole AS createrole,
45
+ r.rolcreatedb AS createdb,
46
+ r.rolreplication AS replication,
47
+ r.rolbypassrls AS bypassrls,
48
+ to_char(r.rolvaliduntil AT TIME ZONE 'UTC', 'YYYY-MM-DD"T"HH24:MI:SS.MS"Z"') AS "validUntil"
49
+ FROM pg_roles r
50
+ WHERE r.rolname = $1`;
51
+
52
+ export const selectRole = (
53
+ pg: PgExecutor,
54
+ name: string,
55
+ ): Effect.Effect<Omit<PostgresRoleAttributes, 'memberOf' | 'passwordSeal'> | undefined, SqlError> =>
56
+ Effect.map(
57
+ pg.unsafe<Omit<PostgresRoleAttributes, 'memberOf' | 'passwordSeal'>>(SELECT_ROLE_SQL, [name]),
58
+ (rows) => rows[0],
59
+ );
60
+
61
+ /** One combined read used by reconcile, so a no-op reconcile issues exactly one round trip.
62
+ * `passwordSeal` is state-only — `pg_roles` answers no password (S25) — so the caller merges it. */
63
+ export const readRoleWithClient = (
64
+ pg: PgExecutor,
65
+ name: string,
66
+ ): Effect.Effect<Omit<PostgresRoleAttributes, 'passwordSeal'> | undefined, SqlError> =>
67
+ Effect.gen(function* () {
68
+ const row = yield* selectRole(pg, name);
69
+ if (row === undefined) return undefined;
70
+ const memberships = yield* selectRoleMemberships(pg, name);
71
+ // Two grantors of one parent are two catalog rows. `memberOf` is the set of names.
72
+ return {
73
+ ...row,
74
+ memberOf: [...new Set(memberships.map((membership) => membership.parent))].sort(),
75
+ memberships,
76
+ };
77
+ });
78
+
79
+ /** The `WITH` options a create carries, in one place so `diff` and the fake agree on the text.
80
+ * A create carries no PASSWORD clause — the password travels through
81
+ * {@link buildSetPasswordSql} as its SCRAM verifier, so the one quoting path is exercised exactly
82
+ * once and a `CREATE ROLE` never carries a credential in its log line. */
83
+ const withOptions = (props: PostgresRoleProps): string => {
84
+ const parts: string[] = [];
85
+ parts.push(props.login ? 'LOGIN' : 'NOLOGIN');
86
+ parts.push(props.inherit ? 'INHERIT' : 'NOINHERIT');
87
+ parts.push(`CONNECTION LIMIT ${String(Math.trunc(props.connectionLimit))}`);
88
+ if (props.validUntil !== undefined) {
89
+ parts.push(`VALID UNTIL ${quoteStringLiteral(props.validUntil)}`);
90
+ }
91
+ return parts.join(' ');
92
+ };
93
+
94
+ export const buildCreateRoleSql = (props: PostgresRoleProps): string =>
95
+ `CREATE ROLE ${quoteIdent(props.name)} WITH ${withOptions(props)}`;
96
+
97
+ /** One `ALTER ROLE` per drifted scalar. Membership and password are separate statements. */
98
+ export const buildAlterRoleSql = (
99
+ name: string,
100
+ change:
101
+ | { prop: 'login'; value: boolean }
102
+ | { prop: 'inherit'; value: boolean }
103
+ | {
104
+ prop: 'connectionLimit';
105
+ value: number;
106
+ }
107
+ | { prop: 'validUntil'; value: string },
108
+ ): string => {
109
+ const who = quoteIdent(name);
110
+ switch (change.prop) {
111
+ case 'login':
112
+ return `ALTER ROLE ${who} WITH ${change.value ? 'LOGIN' : 'NOLOGIN'}`;
113
+ case 'inherit':
114
+ return `ALTER ROLE ${who} WITH ${change.value ? 'INHERIT' : 'NOINHERIT'}`;
115
+ case 'connectionLimit':
116
+ return `ALTER ROLE ${who} WITH CONNECTION LIMIT ${String(Math.trunc(change.value))}`;
117
+ case 'validUntil':
118
+ return `ALTER ROLE ${who} WITH VALID UNTIL ${quoteStringLiteral(change.value)}`;
119
+ }
120
+ };
121
+
122
+ /** One dedicated `ALTER ROLE … PASSWORD`. It carries the SCRAM-SHA-256 verifier
123
+ * `role-scram.ts` computes, never the plain password: Postgres takes no bind parameter for a role
124
+ * password, but a string already in SCRAM verifier format is stored as-is (`create_role.sgml`),
125
+ * so the statement text that reaches the span, `pg_stat_statements` and a failed `ALTER`'s server
126
+ * log holds no secret. */
127
+ export const buildSetPasswordSql = (name: string, verifier: string): string =>
128
+ `ALTER ROLE ${quoteIdent(name)} WITH PASSWORD ${quoteStringLiteral(verifier)}`;
129
+
130
+ export const buildDropRoleSql = (name: string): string => `DROP ROLE IF EXISTS ${quoteIdent(name)}`;
131
+
132
+ /** `CREATE`, the optional password `ALTER`, and one seat `GRANT` per declared parent, in the
133
+ * order a single transaction runs them. Grants are sorted so the script is stable. */
134
+ export const buildCreateStatements = (
135
+ props: PostgresRoleProps,
136
+ verifier: string | undefined,
137
+ ): readonly string[] => [
138
+ buildCreateRoleSql(props),
139
+ ...(verifier === undefined ? [] : [buildSetPasswordSql(props.name, verifier)]),
140
+ ...[...new Set(props.memberOf ?? [])]
141
+ .sort()
142
+ .map((parent) => buildGrantMembershipSql(props.name, parent)),
143
+ ];
144
+
145
+ /** Compare every declared scalar prop against the live row; answer the list of statements
146
+ * reconcile needs to run, empty when nothing drifted. `name` is compared by the caller. */
147
+ export const scalarDrift = (
148
+ props: PostgresRoleProps,
149
+ live: Omit<PostgresRoleAttributes, 'passwordSeal'>,
150
+ ): ReadonlyArray<Parameters<typeof buildAlterRoleSql>[1]> => {
151
+ const changes: Parameters<typeof buildAlterRoleSql>[1][] = [];
152
+ if (props.login !== live.login) changes.push({ prop: 'login', value: props.login });
153
+ if (props.inherit !== live.inherit) changes.push({ prop: 'inherit', value: props.inherit });
154
+ if (props.connectionLimit !== live.connectionLimit) {
155
+ changes.push({ prop: 'connectionLimit', value: props.connectionLimit });
156
+ }
157
+ // Instant comparison, never string equality: the server serialises `rolvaliduntil` with
158
+ // milliseconds, so `'2027-01-01T00:00:00Z'` (declared) and `'2027-01-01T00:00:00.000Z'`
159
+ // (read back) are the same value and must not churn an `ALTER` on every plan.
160
+ if (props.validUntil !== undefined && !sameValidUntil(props.validUntil, live.validUntil)) {
161
+ changes.push({ prop: 'validUntil', value: props.validUntil });
162
+ }
163
+ return changes;
164
+ };
165
+
166
+ /** Whether a live role is the one an interrupted create would have left: every declared scalar,
167
+ * the declared parent set, and no privilege flag or unsafe membership option. The password is
168
+ * not in `pg_roles`, so it is not part of the proof. */
169
+ export const declarationMatches = (
170
+ props: PostgresRoleProps,
171
+ live: Omit<PostgresRoleAttributes, 'passwordSeal'>,
172
+ ): boolean => {
173
+ const wanted =
174
+ props.memberOf === undefined ? undefined : [...new Set(props.memberOf)].sort().join('\0');
175
+ const found = [...(live.memberOf ?? [])].sort().join('\0');
176
+ return (
177
+ props.name === live.name &&
178
+ props.login === live.login &&
179
+ props.inherit === live.inherit &&
180
+ props.connectionLimit === live.connectionLimit &&
181
+ (props.validUntil === undefined || sameValidUntil(props.validUntil, live.validUntil)) &&
182
+ (wanted === undefined || wanted === found) &&
183
+ privilegedFlags(live).length === 0 &&
184
+ (live.memberships ?? []).every(
185
+ (row) => !row.admin && !row.set && (!row.inherit || props.inherit),
186
+ )
187
+ );
188
+ };
189
+
190
+ /** Drop the observe-only membership options before attributes reach state. Alchemy's plan diff
191
+ * receives the stored attributes (`Plan.ts` passes `oldState.attr`), so an `admin`/`set` row
192
+ * kept here would still not be what `diff` sees — the provider reads `pg_auth_members` live.
193
+ * What state stores is the parent-name set. */
194
+ export const storedAttributes = (
195
+ live: Omit<PostgresRoleAttributes, 'passwordSeal'>,
196
+ ): Omit<PostgresRoleAttributes, 'passwordSeal'> => {
197
+ const { memberships: _observeOnly, ...stored } = live;
198
+ return stored;
199
+ };
200
+
201
+ /** Catalog flags this family never declares. A create lands on the server default (all false);
202
+ * an absent field on a seeded test row is that default. */
203
+ export const privilegedFlags = (
204
+ live: Omit<PostgresRoleAttributes, 'passwordSeal'>,
205
+ ): readonly string[] => {
206
+ const flags: string[] = [];
207
+ if (live.superuser === true) flags.push('SUPERUSER');
208
+ if (live.createrole === true) flags.push('CREATEROLE');
209
+ if (live.createdb === true) flags.push('CREATEDB');
210
+ if (live.replication === true) flags.push('REPLICATION');
211
+ if (live.bypassrls === true) flags.push('BYPASSRLS');
212
+ return flags;
213
+ };
@@ -0,0 +1,68 @@
1
+ /**
2
+ * Plan-time `validUntil` refusals and the instant compare, extracted from
3
+ * `role-refusals.test.ts` so that file stays under the house 250-line cap.
4
+ *
5
+ * A zone suffix is not a timestamp. `2027-13-01T00:00:00Z` and `not-a-dateZ` match the offset
6
+ * regex and `Date.parse` to NaN; a plan that accepts them sends a `VALID UNTIL` PostgreSQL
7
+ * rejects, or one that never compares equal on read-back, so every later reconcile re-issues
8
+ * the same `ALTER`.
9
+ */
10
+ import { describe, expect, test } from 'bun:test';
11
+ import * as Effect from 'effect/Effect';
12
+ import { sameValidUntil, validUntilRefusal } from './role-attrs.ts';
13
+ import type { PostgresRoleAttributes, PostgresRoleProps } from './role-attrs.ts';
14
+ import { PostgresRoleValidUntilRefused } from './role-errors.ts';
15
+ import { diffPostgresRole } from './role-diff.ts';
16
+ import { refuseAtPlan } from './role.ts';
17
+
18
+ const fails = <A, E>(eff: Effect.Effect<A, E>): Promise<E> => Effect.runPromise(Effect.flip(eff));
19
+
20
+ const base: PostgresRoleProps = {
21
+ name: 'seat-observability',
22
+ login: false,
23
+ connectionLimit: 10,
24
+ inherit: true,
25
+ };
26
+
27
+ const output: PostgresRoleAttributes = {
28
+ name: 'seat-observability',
29
+ oid: 20000,
30
+ login: false,
31
+ connectionLimit: 10,
32
+ inherit: true,
33
+ validUntil: null,
34
+ memberOf: [],
35
+ passwordSeal: '',
36
+ };
37
+
38
+ describe('validUntil', () => {
39
+ test('a zone-carrying value passes; a zone-free or unparseable one is refused with its reason', () => {
40
+ expect(validUntilRefusal('2027-01-01T00:00:00Z')).toBeUndefined();
41
+ expect(validUntilRefusal('2027-01-01T02:00:00+02:00')).toBeUndefined();
42
+ expect(validUntilRefusal('2027-01-01T00:00:00')).toEqual({ reason: 'zone-free' });
43
+ expect(validUntilRefusal('2027-01-01')).toEqual({ reason: 'zone-free' });
44
+ expect(validUntilRefusal('not a timestamp')).toEqual({ reason: 'unparseable' });
45
+ // A zone suffix is not enough: Date.parse of these is NaN, and a plan that accepts them
46
+ // sends a VALID UNTIL PostgreSQL rejects or that never compares equal on read-back.
47
+ expect(validUntilRefusal('2027-13-01T00:00:00Z')).toEqual({ reason: 'unparseable' });
48
+ expect(validUntilRefusal('not-a-dateZ')).toEqual({ reason: 'unparseable' });
49
+ });
50
+
51
+ test('refuseAtPlan raises the typed tag for a zone-free value', async () => {
52
+ const error = await fails(refuseAtPlan({ ...base, validUntil: '2027-01-01' }));
53
+ expect(error).toBeInstanceOf(PostgresRoleValidUntilRefused);
54
+ expect((error as PostgresRoleValidUntilRefused).reason).toBe('zone-free');
55
+ });
56
+
57
+ test('diff refuses the same values, so reconcile and diff can never disagree', async () => {
58
+ const error = await fails(diffPostgresRole({ ...base, validUntil: '2027-01-01' }, output));
59
+ expect(error).toBeInstanceOf(PostgresRoleValidUntilRefused);
60
+ });
61
+
62
+ test('sameValidUntil compares instants, so the server’s milliseconds never churn a plan', () => {
63
+ expect(sameValidUntil('2027-01-01T00:00:00Z', '2027-01-01T00:00:00.000Z')).toBe(true);
64
+ expect(sameValidUntil('2027-01-01T02:00:00+02:00', '2027-01-01T00:00:00.000Z')).toBe(true);
65
+ expect(sameValidUntil('2027-01-01T00:00:00Z', null)).toBe(false);
66
+ expect(sameValidUntil('2027-01-01T00:00:00Z', '2028-01-01T00:00:00.000Z')).toBe(false);
67
+ });
68
+ });
@@ -0,0 +1,226 @@
1
+ /**
2
+ * `Postgres.Role` — create / adopt / alter / drop of a LOGIN or NOLOGIN role on the same
3
+ * self-hosted PostgreSQL 18 cluster `Postgres.Database` talks to, over the same transports
4
+ * (runner `psql` or socket). The seat-wiring lane B12a's shape: one group role per seat
5
+ * (NOLOGIN, member of `hf_agent`), plus the LOGIN seat users.
6
+ *
7
+ * ★ ALTER-CAPABLE, UNLIKE `Postgres.Database`. Every scalar this resource asserts has a safe
8
+ * atomic `ALTER ROLE` form (`LOGIN`/`NOLOGIN`, `INHERIT`/`NOINHERIT`, `CONNECTION LIMIT`,
9
+ * `VALID UNTIL` — `alter_role.sgml`'s first synopsis variant), so a drifted live row is
10
+ * brought back to the declaration, never refused. Memberships move through one-parent
11
+ * `GRANT`/`REVOKE` statements (`alter_role.sgml`: "there are no options for adding or
12
+ * removing memberships; use GRANT and REVOKE").
13
+ * ⛔ THE PASSWORD IS A REFERENCE, NEVER A VALUE (S25). It is declared as
14
+ * `{ fromEnv: 'PG_SEAT_WIDGET_PASSWORD' }`; the NAME lands in state, the value is resolved
15
+ * from the deploying process's environment at reconcile time, held as `Redacted`, and sent only
16
+ * as the SCRAM-SHA-256 verifier inside the one `ALTER ROLE … PASSWORD` statement that needs it
17
+ * (`role-scram.ts` — the statement text reaching spans, `pg_stat_statements` and a failed
18
+ * `ALTER`'s server log never holds the plain value). What state remembers is a
19
+ * `scrypt:<salt>:<digest>` seal, so a reconcile sends the password only when the environment
20
+ * value differs from the one last written — a plan never re-sends a secret that already
21
+ * matches, and never stores one.
22
+ * ★ MEMBERSHIP IS `pg_auth_members`, compared as a sorted set. `memberOf` omitted leaves live
23
+ * memberships alone; `[]` ensures none.
24
+ * ★ `defaultRemovalPolicy: 'retain'` — a seat group role may own objects and be granted across
25
+ * databases; a destroy is still implemented in full (`DROP ROLE IF EXISTS`), opt in with
26
+ * `.pipe(RemovalPolicy.destroy())`. A role that still owns objects fails the drop with the
27
+ * server's own `2BP01` (`dependent_objects_still_exist`), surfaced as the client's `SqlError`.
28
+ */
29
+ import { Resource } from 'alchemy';
30
+ import * as Effect from 'effect/Effect';
31
+ import * as Redacted from 'effect/Redacted';
32
+ import type { SqlError } from 'effect/unstable/sql/SqlError';
33
+ import { type Environment } from '../secrets/write-only.ts';
34
+ import type { PostgresRoleAttributes, PostgresRoleProps } from './role-attrs.ts';
35
+ import { nameByteRefusal, validUntilRefusal } from './role-attrs.ts';
36
+ import {
37
+ assertPasswordAscii,
38
+ passwordMatchesSeal,
39
+ resolvePassword,
40
+ sealPassword,
41
+ } from './role-secrets.ts';
42
+ import { scramSha256Verifier } from './role-scram.ts';
43
+ import {
44
+ buildAlterRoleSql,
45
+ buildCreateStatements,
46
+ buildSetPasswordSql,
47
+ privilegedFlags,
48
+ readRoleWithClient,
49
+ scalarDrift,
50
+ storedAttributes,
51
+ } from './role-sql.ts';
52
+ import { assertParentsExist, syncMemberships } from './role-membership-sql.ts';
53
+ import {
54
+ PostgresRoleCreateVanished,
55
+ PostgresRoleIdentityRefused,
56
+ type PostgresRoleMembershipUnrepaired,
57
+ PostgresRoleNameRefused,
58
+ type PostgresRoleParentMissing,
59
+ PostgresRolePasswordEnvUnsetError,
60
+ type PostgresRolePasswordNonAsciiRefused,
61
+ PostgresRolePrivilegedRefused,
62
+ PostgresRoleValidUntilRefused,
63
+ } from './role-errors.ts';
64
+ import { type PostgresConnection } from './connection.ts';
65
+ import type { PgExecutor } from './database-sql.ts';
66
+
67
+ export interface PostgresRole extends Resource<
68
+ 'Postgres.Role',
69
+ PostgresRoleProps,
70
+ PostgresRoleAttributes,
71
+ never,
72
+ PostgresConnection
73
+ > {}
74
+
75
+ export const PostgresRole = Resource<PostgresRole>('Postgres.Role', {
76
+ defaultRemovalPolicy: 'retain',
77
+ });
78
+
79
+ /** Mirrors `isPostgresDatabase`: a resource constructor is a callable `Object.assign`ed
80
+ * function, not a plain object, so `typeof` must accept both. */
81
+ export const isPostgresRole = (value: unknown): value is PostgresRole =>
82
+ (typeof value === 'object' || typeof value === 'function') &&
83
+ value !== null &&
84
+ (value as { Type?: unknown }).Type === 'Postgres.Role';
85
+
86
+ /**
87
+ * The core of `reconcile`, against any {@link PgExecutor} — the real client through `withPg`,
88
+ * or `fake-sql.ts`'s recording fake in tests. `env` defaults to `process.env`; tests pass a
89
+ * recording one. `previousSeal` is the seal in the engine's state — `''` when the role is
90
+ * being written for the first time — because `pg_roles` answers no password (S25), so the live
91
+ * row cannot say whether the environment value still matches the one last written.
92
+ */
93
+ export const reconcileWithClient = (
94
+ pg: PgExecutor,
95
+ props: PostgresRoleProps,
96
+ env: Environment = process.env,
97
+ previousSeal = '',
98
+ storedOid?: number,
99
+ ): Effect.Effect<
100
+ PostgresRoleAttributes,
101
+ | PostgresRolePasswordEnvUnsetError
102
+ | PostgresRolePasswordNonAsciiRefused
103
+ | PostgresRolePrivilegedRefused
104
+ | PostgresRoleCreateVanished
105
+ | PostgresRoleMembershipUnrepaired
106
+ | PostgresRoleParentMissing
107
+ | PostgresRoleIdentityRefused
108
+ | SqlError
109
+ > =>
110
+ Effect.gen(function* () {
111
+ const resolved = resolvePassword(props, env);
112
+ yield* assertPasswordAscii(props.name, resolved);
113
+ const observed = yield* readRoleWithClient(pg, props.name);
114
+ if (observed === undefined) {
115
+ // A create that declares a password must hold it: a LOGIN role with no password set, or
116
+ // one created against a guess, is exactly the unauthenticated path to refuse.
117
+ if (resolved.variable !== undefined && resolved.value === undefined) {
118
+ return yield* Effect.fail(
119
+ new PostgresRolePasswordEnvUnsetError({ role: props.name, variable: resolved.variable }),
120
+ );
121
+ }
122
+ // The create carries no PASSWORD clause. The verifier ALTER and the seat GRANTs share
123
+ // one transaction with the CREATE, so a failure rolls all of them back. A missing parent
124
+ // is refused first — `GRANT` would otherwise be `42704` after the CREATE committed.
125
+ yield* assertParentsExist(pg, props.name, props.memberOf);
126
+ const verifier =
127
+ resolved.value === undefined
128
+ ? undefined
129
+ : scramSha256Verifier(Redacted.value(resolved.value));
130
+ yield* pg.transaction(buildCreateStatements(props, verifier));
131
+ // Never trust the write's own report (S10): re-read what the server actually stored.
132
+ const created = yield* readRoleWithClient(pg, props.name);
133
+ if (created === undefined) {
134
+ return yield* Effect.fail(new PostgresRoleCreateVanished({ role: props.name }));
135
+ }
136
+ return {
137
+ ...storedAttributes(created),
138
+ passwordSeal: resolved.value === undefined ? '' : sealPassword(resolved.value),
139
+ };
140
+ }
141
+ // A recycled name (dropped and recreated out of band) is a different oid. Refuse before
142
+ // any ALTER, PASSWORD or DROP-by-name the caller might follow with.
143
+ if (storedOid !== undefined && observed.oid !== storedOid) {
144
+ return yield* Effect.fail(
145
+ new PostgresRoleIdentityRefused({
146
+ role: props.name,
147
+ storedOid,
148
+ liveOid: observed.oid,
149
+ }),
150
+ );
151
+ }
152
+ // A live role this stack did not create can carry flags CREATE ROLE's default leaves off.
153
+ // They are not inherited: SET ROLE to this role exercises them. Refuse before any write.
154
+ const flags = privilegedFlags(observed);
155
+ if (flags.length > 0) {
156
+ return yield* Effect.fail(new PostgresRolePrivilegedRefused({ role: props.name, flags }));
157
+ }
158
+ // A declared password this stack has never sealed is not "do not churn". CREATE ROLE and
159
+ // ALTER ROLE … PASSWORD are two statements; a retry after the second failed, or after the
160
+ // process died before state was saved, sees the role and an empty seal. An unset variable
161
+ // there must refuse — reporting success would leave a LOGIN role with a null password.
162
+ if (resolved.variable !== undefined && resolved.value === undefined && previousSeal === '') {
163
+ return yield* Effect.fail(
164
+ new PostgresRolePasswordEnvUnsetError({ role: props.name, variable: resolved.variable }),
165
+ );
166
+ }
167
+ // Parents before any ALTER, so a missing one does not leave a half-applied scalar update.
168
+ yield* assertParentsExist(pg, props.name, props.memberOf);
169
+ // Adopted or drifted: every scalar comes back to the declaration, one ALTER per field.
170
+ for (const change of scalarDrift(props, observed)) {
171
+ yield* pg.unsafe(buildAlterRoleSql(props.name, change)).pipe(Effect.asVoid);
172
+ }
173
+ // The password goes out only when it changed: the seal matches the last written value, or
174
+ // the variable is unset here (a plan-only run must not churn a secret it cannot read). The
175
+ // same match decides the seal that lands in state, so an unchanged password keeps the seal
176
+ // it already had instead of being re-salted into a new one on every plan.
177
+ const passwordMatched =
178
+ resolved.value !== undefined && passwordMatchesSeal(resolved.value, previousSeal);
179
+ if (resolved.value !== undefined && !passwordMatched) {
180
+ yield* pg
181
+ .unsafe(
182
+ buildSetPasswordSql(props.name, scramSha256Verifier(Redacted.value(resolved.value))),
183
+ )
184
+ .pipe(Effect.asVoid);
185
+ }
186
+ yield* syncMemberships(pg, props, observed.memberships ?? []);
187
+ const after = yield* readRoleWithClient(pg, props.name);
188
+ if (after === undefined) {
189
+ return yield* Effect.fail(new PostgresRoleCreateVanished({ role: props.name }));
190
+ }
191
+ return {
192
+ ...storedAttributes(after),
193
+ passwordSeal:
194
+ resolved.value === undefined || passwordMatched
195
+ ? previousSeal
196
+ : sealPassword(resolved.value),
197
+ };
198
+ });
199
+
200
+ /** The core of `read`. Answers `undefined` when absent; ownership branding and the state-only
201
+ * `passwordSeal` merge are the caller's. */
202
+ export const readRole = (pg: PgExecutor, name: string) => readRoleWithClient(pg, name);
203
+
204
+ /** Every plan-time refusal this family makes, in one place so `diff` and `reconcile` check the
205
+ * same facts. Nothing here touches a client. */
206
+ export const refuseAtPlan = (
207
+ props: PostgresRoleProps,
208
+ ): Effect.Effect<void, PostgresRoleNameRefused | PostgresRoleValidUntilRefused> =>
209
+ Effect.gen(function* () {
210
+ const nameRefusal = nameByteRefusal(props.name);
211
+ if (nameRefusal !== undefined) {
212
+ return yield* Effect.fail(new PostgresRoleNameRefused({ name: props.name, ...nameRefusal }));
213
+ }
214
+ if (props.validUntil !== undefined) {
215
+ const untilRefusal = validUntilRefusal(props.validUntil);
216
+ if (untilRefusal !== undefined) {
217
+ return yield* Effect.fail(
218
+ new PostgresRoleValidUntilRefused({
219
+ role: props.name,
220
+ value: props.validUntil,
221
+ reason: untilRefusal.reason,
222
+ }),
223
+ );
224
+ }
225
+ }
226
+ });
@@ -0,0 +1,139 @@
1
+ /**
2
+ * Assert a just-read `pg_namespace` row — and the connection that read it — against a
3
+ * `Postgres.Schema` declaration.
4
+ *
5
+ * ⛔ OWNER BEFORE COMMENT. `reconcile` calls `assertOwner` on the create re-read before any
6
+ * `COMMENT ON SCHEMA`. Measured before the 2026-10-01 fix: a concurrent creator could win
7
+ * `IF NOT EXISTS` (the `CREATE` did nothing). Plain CREATE now refuses that race. The kit role
8
+ * is superuser, so commenting first rewrites their schema, and the later drift failure does not roll that write back — the two statements are not one
9
+ * transaction.
10
+ * ⛔ AN OMITTED `owner` IS `current_user`, NOT "DON'T ASSERT". A fresh `CREATE SCHEMA` without
11
+ * `AUTHORIZATION` is owned by the role running it. Skipping the comparison when the prop is
12
+ * omitted returns that foreign row as reconciled, and a later `RemovalPolicy.destroy()` drops
13
+ * the other role's schema.
14
+ * ⛔ DELETE RE-READS AND PROVES OWNERSHIP (`deleteForeignRefusal`). `DROP SCHEMA` fires by
15
+ * name alone; the name is no proof the live schema is the one this resource created — the
16
+ * old row can be dropped out of band and the name recreated by another role, and the engine
17
+ * hands `delete` no re-read of its own once state exists. The persisted `oid` + `owner`
18
+ * (what the last reconcile asserted) are the proof: a live row that does not match them is
19
+ * refused, and nothing is dropped.
20
+ */
21
+ import * as Effect from 'effect/Effect';
22
+ import type { SqlError } from 'effect/unstable/sql/SqlError';
23
+ import type { PgExecutor } from './database-sql.ts';
24
+ import {
25
+ type PostgresSchemaAttributes,
26
+ type PostgresSchemaProps,
27
+ normalizedComment,
28
+ } from './schema-attrs.ts';
29
+ import {
30
+ PostgresSchemaDeleteForeignRefused,
31
+ PostgresSchemaDrift,
32
+ PostgresSchemaWrongDatabase,
33
+ } from './schema-errors.ts';
34
+ import { currentDatabase } from './schema-sql.ts';
35
+
36
+ /** The connected database, proven equal to the declaration or refused — the guard every write
37
+ * path in this family runs first. */
38
+ export const assertDatabase = (
39
+ props: PostgresSchemaProps,
40
+ pg: PgExecutor,
41
+ ): Effect.Effect<void, PostgresSchemaWrongDatabase | SqlError> =>
42
+ Effect.flatMap(currentDatabase(pg), (connected) =>
43
+ connected === props.database
44
+ ? Effect.void
45
+ : Effect.fail(
46
+ new PostgresSchemaWrongDatabase({
47
+ schema: props.name,
48
+ declared: props.database,
49
+ connected,
50
+ }),
51
+ ),
52
+ );
53
+
54
+ /** The role a fresh `CREATE SCHEMA` without `AUTHORIZATION` would be owned by. */
55
+ export const declaredOwner = (props: PostgresSchemaProps, executingRole: string): string =>
56
+ props.owner ?? executingRole;
57
+
58
+ const ownerDrift = (
59
+ props: PostgresSchemaProps,
60
+ live: PostgresSchemaAttributes,
61
+ executingRole: string,
62
+ ): { readonly prop: 'owner'; readonly declared: string; readonly live: string } | undefined => {
63
+ const declared = declaredOwner(props, executingRole);
64
+ return declared === live.owner ? undefined : { prop: 'owner', declared, live: live.owner };
65
+ };
66
+
67
+ /** Owner, then comment. An omitted owner is `executingRole` (`current_user`), so a race
68
+ * winner owned by someone else is drift on every return path, not only the create path. */
69
+ export const liveDrift = (
70
+ props: PostgresSchemaProps,
71
+ live: PostgresSchemaAttributes,
72
+ executingRole: string,
73
+ ): { readonly prop: string; readonly declared: unknown; readonly live: unknown } | undefined => {
74
+ const owner = ownerDrift(props, live, executingRole);
75
+ if (owner !== undefined) return owner;
76
+ const comment = normalizedComment(props.comment);
77
+ if (comment !== undefined && comment !== (live.comment ?? undefined)) {
78
+ return { prop: 'comment', declared: comment, live: live.comment };
79
+ }
80
+ return undefined;
81
+ };
82
+
83
+ const refuse = (
84
+ props: PostgresSchemaProps,
85
+ drift: { readonly prop: string; readonly declared: unknown; readonly live: unknown },
86
+ ): Effect.Effect<never, PostgresSchemaDrift> =>
87
+ Effect.fail(
88
+ new PostgresSchemaDrift({
89
+ schema: props.name,
90
+ prop: drift.prop,
91
+ declared: drift.declared,
92
+ live: drift.live,
93
+ }),
94
+ );
95
+
96
+ /** Owner only — the check that runs before `COMMENT ON`, so a race winner is never commented. */
97
+ export const assertOwner = (
98
+ props: PostgresSchemaProps,
99
+ row: PostgresSchemaAttributes,
100
+ executingRole: string,
101
+ ): Effect.Effect<void, PostgresSchemaDrift> => {
102
+ const drift = ownerDrift(props, row, executingRole);
103
+ return drift === undefined ? Effect.void : refuse(props, drift);
104
+ };
105
+
106
+ /** Owner and comment. Every return path of `reconcile` passes through this. */
107
+ export const assertLive = (
108
+ props: PostgresSchemaProps,
109
+ row: PostgresSchemaAttributes,
110
+ executingRole: string,
111
+ ): Effect.Effect<PostgresSchemaAttributes, PostgresSchemaDrift> => {
112
+ const drift = liveDrift(props, row, executingRole);
113
+ return drift === undefined ? Effect.succeed(row) : refuse(props, drift);
114
+ };
115
+
116
+ /**
117
+ * The delete-time ownership proof: `undefined` when the live row is provably the one this
118
+ * resource created (same `oid` and `owner` as the persisted state handed to `delete`), else
119
+ * the typed refusal — with `lastOid`/`lastOwner` `undefined` when no state was handed over,
120
+ * the fail-closed shape (the handler type allows `output === undefined` even though the
121
+ * engine passes state whenever it calls delete).
122
+ */
123
+ export const deleteForeignRefusal = (
124
+ props: PostgresSchemaProps,
125
+ live: PostgresSchemaAttributes,
126
+ output: PostgresSchemaAttributes | undefined,
127
+ ): PostgresSchemaDeleteForeignRefused | undefined => {
128
+ if (output !== undefined && output.oid === live.oid && output.owner === live.owner) {
129
+ return undefined;
130
+ }
131
+ return new PostgresSchemaDeleteForeignRefused({
132
+ schema: props.name,
133
+ database: props.database,
134
+ liveOid: live.oid,
135
+ liveOwner: live.owner,
136
+ lastOid: output?.oid,
137
+ lastOwner: output?.owner,
138
+ });
139
+ };