@homeflare/alchemy 0.42.0 → 0.44.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 (648) 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/budget-errors.d.ts +36 -0
  17. package/dist/litellm/budget-errors.d.ts.map +1 -0
  18. package/dist/litellm/budget-form.d.ts +50 -0
  19. package/dist/litellm/budget-form.d.ts.map +1 -0
  20. package/dist/litellm/budget-operations.d.ts +20 -0
  21. package/dist/litellm/budget-operations.d.ts.map +1 -0
  22. package/dist/litellm/budget.d.ts +61 -0
  23. package/dist/litellm/budget.d.ts.map +1 -0
  24. package/dist/litellm/credential-errors.d.ts +131 -0
  25. package/dist/litellm/credential-errors.d.ts.map +1 -0
  26. package/dist/litellm/credential-form.d.ts +79 -0
  27. package/dist/litellm/credential-form.d.ts.map +1 -0
  28. package/dist/litellm/credential-operations.d.ts +84 -0
  29. package/dist/litellm/credential-operations.d.ts.map +1 -0
  30. package/dist/litellm/credential-rewrite.d.ts +6 -0
  31. package/dist/litellm/credential-rewrite.d.ts.map +1 -0
  32. package/dist/litellm/credential-types.d.ts +87 -0
  33. package/dist/litellm/credential-types.d.ts.map +1 -0
  34. package/dist/litellm/credential-values.d.ts +53 -0
  35. package/dist/litellm/credential-values.d.ts.map +1 -0
  36. package/dist/litellm/credential.d.ts +83 -0
  37. package/dist/litellm/credential.d.ts.map +1 -0
  38. package/dist/litellm/fake-access-group-litellm.d.ts +34 -0
  39. package/dist/litellm/fake-access-group-litellm.d.ts.map +1 -0
  40. package/dist/litellm/fake-credential-litellm.d.ts +54 -0
  41. package/dist/litellm/fake-credential-litellm.d.ts.map +1 -0
  42. package/dist/litellm/fake-keys.d.ts +53 -0
  43. package/dist/litellm/fake-keys.d.ts.map +1 -0
  44. package/dist/litellm/fake-litellm.d.ts +9 -1
  45. package/dist/litellm/fake-litellm.d.ts.map +1 -1
  46. package/dist/litellm/fake-mcp-litellm.d.ts +67 -0
  47. package/dist/litellm/fake-mcp-litellm.d.ts.map +1 -0
  48. package/dist/litellm/fake-model-litellm-post-update.d.ts +49 -0
  49. package/dist/litellm/fake-model-litellm-post-update.d.ts.map +1 -0
  50. package/dist/litellm/fake-model-litellm.d.ts +48 -0
  51. package/dist/litellm/fake-model-litellm.d.ts.map +1 -0
  52. package/dist/litellm/fake-policy-attachment-litellm.d.ts +33 -0
  53. package/dist/litellm/fake-policy-attachment-litellm.d.ts.map +1 -0
  54. package/dist/litellm/fake-policy-litellm.d.ts +32 -0
  55. package/dist/litellm/fake-policy-litellm.d.ts.map +1 -0
  56. package/dist/litellm/fake-registry-base.d.ts +42 -0
  57. package/dist/litellm/fake-registry-base.d.ts.map +1 -0
  58. package/dist/litellm/fake-stack.d.ts +8 -1
  59. package/dist/litellm/fake-stack.d.ts.map +1 -1
  60. package/dist/litellm/fake-team-litellm.d.ts +47 -0
  61. package/dist/litellm/fake-team-litellm.d.ts.map +1 -0
  62. package/dist/litellm/fake-tool-litellm.d.ts +28 -0
  63. package/dist/litellm/fake-tool-litellm.d.ts.map +1 -0
  64. package/dist/litellm/fake-toolset-litellm.d.ts +29 -0
  65. package/dist/litellm/fake-toolset-litellm.d.ts.map +1 -0
  66. package/dist/litellm/index.d.ts +20 -0
  67. package/dist/litellm/index.d.ts.map +1 -1
  68. package/dist/litellm/index.js +3201 -2
  69. package/dist/litellm/index.js.map +70 -5
  70. package/dist/litellm/key-errors.d.ts +169 -0
  71. package/dist/litellm/key-errors.d.ts.map +1 -0
  72. package/dist/litellm/key-form.d.ts +119 -0
  73. package/dist/litellm/key-form.d.ts.map +1 -0
  74. package/dist/litellm/key-harness.d.ts +41 -0
  75. package/dist/litellm/key-harness.d.ts.map +1 -0
  76. package/dist/litellm/key-metadata-errors.d.ts +30 -0
  77. package/dist/litellm/key-metadata-errors.d.ts.map +1 -0
  78. package/dist/litellm/key-metadata.d.ts +22 -0
  79. package/dist/litellm/key-metadata.d.ts.map +1 -0
  80. package/dist/litellm/key-operations.d.ts +63 -0
  81. package/dist/litellm/key-operations.d.ts.map +1 -0
  82. package/dist/litellm/key-secret.d.ts +33 -0
  83. package/dist/litellm/key-secret.d.ts.map +1 -0
  84. package/dist/litellm/key.d.ts +78 -0
  85. package/dist/litellm/key.d.ts.map +1 -0
  86. package/dist/litellm/mcp-server-credential.d.ts +50 -0
  87. package/dist/litellm/mcp-server-credential.d.ts.map +1 -0
  88. package/dist/litellm/mcp-server-errors.d.ts +101 -0
  89. package/dist/litellm/mcp-server-errors.d.ts.map +1 -0
  90. package/dist/litellm/mcp-server-form.d.ts +56 -0
  91. package/dist/litellm/mcp-server-form.d.ts.map +1 -0
  92. package/dist/litellm/mcp-server-locate.d.ts +26 -0
  93. package/dist/litellm/mcp-server-locate.d.ts.map +1 -0
  94. package/dist/litellm/mcp-server-operations.d.ts +63 -0
  95. package/dist/litellm/mcp-server-operations.d.ts.map +1 -0
  96. package/dist/litellm/mcp-server-shadow.d.ts +24 -0
  97. package/dist/litellm/mcp-server-shadow.d.ts.map +1 -0
  98. package/dist/litellm/mcp-server-types.d.ts +118 -0
  99. package/dist/litellm/mcp-server-types.d.ts.map +1 -0
  100. package/dist/litellm/mcp-server-url.d.ts +11 -0
  101. package/dist/litellm/mcp-server-url.d.ts.map +1 -0
  102. package/dist/litellm/mcp-server.d.ts +95 -0
  103. package/dist/litellm/mcp-server.d.ts.map +1 -0
  104. package/dist/litellm/model-claim.d.ts +24 -0
  105. package/dist/litellm/model-claim.d.ts.map +1 -0
  106. package/dist/litellm/model-credential.d.ts +12 -0
  107. package/dist/litellm/model-credential.d.ts.map +1 -0
  108. package/dist/litellm/model-errors.d.ts +119 -0
  109. package/dist/litellm/model-errors.d.ts.map +1 -0
  110. package/dist/litellm/model-form.d.ts +78 -0
  111. package/dist/litellm/model-form.d.ts.map +1 -0
  112. package/dist/litellm/model-locate.d.ts +13 -0
  113. package/dist/litellm/model-locate.d.ts.map +1 -0
  114. package/dist/litellm/model-operations.d.ts +59 -0
  115. package/dist/litellm/model-operations.d.ts.map +1 -0
  116. package/dist/litellm/model-types.d.ts +91 -0
  117. package/dist/litellm/model-types.d.ts.map +1 -0
  118. package/dist/litellm/model.d.ts +88 -0
  119. package/dist/litellm/model.d.ts.map +1 -0
  120. package/dist/litellm/operations.d.ts +22 -0
  121. package/dist/litellm/operations.d.ts.map +1 -1
  122. package/dist/litellm/policy-attachment-form.d.ts +19 -0
  123. package/dist/litellm/policy-attachment-form.d.ts.map +1 -0
  124. package/dist/litellm/policy-attachment-operations.d.ts +31 -0
  125. package/dist/litellm/policy-attachment-operations.d.ts.map +1 -0
  126. package/dist/litellm/policy-attachment-types.d.ts +48 -0
  127. package/dist/litellm/policy-attachment-types.d.ts.map +1 -0
  128. package/dist/litellm/policy-attachment.d.ts +61 -0
  129. package/dist/litellm/policy-attachment.d.ts.map +1 -0
  130. package/dist/litellm/policy-form.d.ts +22 -0
  131. package/dist/litellm/policy-form.d.ts.map +1 -0
  132. package/dist/litellm/policy-operations.d.ts +42 -0
  133. package/dist/litellm/policy-operations.d.ts.map +1 -0
  134. package/dist/litellm/policy-types.d.ts +57 -0
  135. package/dist/litellm/policy-types.d.ts.map +1 -0
  136. package/dist/litellm/policy.d.ts +65 -0
  137. package/dist/litellm/policy.d.ts.map +1 -0
  138. package/dist/litellm/providers.d.ts +1 -1
  139. package/dist/litellm/providers.d.ts.map +1 -1
  140. package/dist/litellm/registry-errors.d.ts +62 -0
  141. package/dist/litellm/registry-errors.d.ts.map +1 -0
  142. package/dist/litellm/registry-support.d.ts +42 -0
  143. package/dist/litellm/registry-support.d.ts.map +1 -0
  144. package/dist/litellm/team-form.d.ts +26 -0
  145. package/dist/litellm/team-form.d.ts.map +1 -0
  146. package/dist/litellm/team-members.d.ts +10 -0
  147. package/dist/litellm/team-members.d.ts.map +1 -0
  148. package/dist/litellm/team-operations.d.ts +38 -0
  149. package/dist/litellm/team-operations.d.ts.map +1 -0
  150. package/dist/litellm/team-permission.d.ts +35 -0
  151. package/dist/litellm/team-permission.d.ts.map +1 -0
  152. package/dist/litellm/team-types.d.ts +101 -0
  153. package/dist/litellm/team-types.d.ts.map +1 -0
  154. package/dist/litellm/team.d.ts +67 -0
  155. package/dist/litellm/team.d.ts.map +1 -0
  156. package/dist/litellm/tool-policy-form.d.ts +21 -0
  157. package/dist/litellm/tool-policy-form.d.ts.map +1 -0
  158. package/dist/litellm/tool-policy-operations.d.ts +30 -0
  159. package/dist/litellm/tool-policy-operations.d.ts.map +1 -0
  160. package/dist/litellm/tool-policy-types.d.ts +46 -0
  161. package/dist/litellm/tool-policy-types.d.ts.map +1 -0
  162. package/dist/litellm/tool-policy.d.ts +62 -0
  163. package/dist/litellm/tool-policy.d.ts.map +1 -0
  164. package/dist/litellm/toolset-form.d.ts +20 -0
  165. package/dist/litellm/toolset-form.d.ts.map +1 -0
  166. package/dist/litellm/toolset-operations.d.ts +39 -0
  167. package/dist/litellm/toolset-operations.d.ts.map +1 -0
  168. package/dist/litellm/toolset-types.d.ts +46 -0
  169. package/dist/litellm/toolset-types.d.ts.map +1 -0
  170. package/dist/litellm/toolset.d.ts +55 -0
  171. package/dist/litellm/toolset.d.ts.map +1 -0
  172. package/dist/openbao/auth-role-form.d.ts +9 -0
  173. package/dist/openbao/auth-role-form.d.ts.map +1 -1
  174. package/dist/openbao/auth-role-wire.d.ts.map +1 -1
  175. package/dist/openbao/fake-engines-roles.d.ts.map +1 -1
  176. package/dist/openbao/index.js +43 -6
  177. package/dist/openbao/index.js.map +5 -5
  178. package/dist/ownership/rows.d.ts +11 -0
  179. package/dist/ownership/rows.d.ts.map +1 -1
  180. package/dist/postgres/connection.d.ts +32 -2
  181. package/dist/postgres/connection.d.ts.map +1 -1
  182. package/dist/postgres/database-sql.d.ts +28 -9
  183. package/dist/postgres/database-sql.d.ts.map +1 -1
  184. package/dist/postgres/database.d.ts +1 -1
  185. package/dist/postgres/database.d.ts.map +1 -1
  186. package/dist/postgres/fake-grants-parse.d.ts +37 -0
  187. package/dist/postgres/fake-grants-parse.d.ts.map +1 -0
  188. package/dist/postgres/fake-grants-read.d.ts +61 -0
  189. package/dist/postgres/fake-grants-read.d.ts.map +1 -0
  190. package/dist/postgres/fake-grants-sql.d.ts +46 -0
  191. package/dist/postgres/fake-grants-sql.d.ts.map +1 -0
  192. package/dist/postgres/fake-grants-write.d.ts +29 -0
  193. package/dist/postgres/fake-grants-write.d.ts.map +1 -0
  194. package/dist/postgres/fake-role-sql.d.ts +40 -0
  195. package/dist/postgres/fake-role-sql.d.ts.map +1 -0
  196. package/dist/postgres/fake-schema-sql.d.ts +42 -0
  197. package/dist/postgres/fake-schema-sql.d.ts.map +1 -0
  198. package/dist/postgres/fake-sql-parse.d.ts +23 -0
  199. package/dist/postgres/fake-sql-parse.d.ts.map +1 -0
  200. package/dist/postgres/fake-sql-quote.d.ts +12 -0
  201. package/dist/postgres/fake-sql-quote.d.ts.map +1 -0
  202. package/dist/postgres/fake-sql.d.ts +42 -0
  203. package/dist/postgres/fake-sql.d.ts.map +1 -1
  204. package/dist/postgres/grants-attrs.d.ts +115 -0
  205. package/dist/postgres/grants-attrs.d.ts.map +1 -0
  206. package/dist/postgres/grants-declare.d.ts +88 -0
  207. package/dist/postgres/grants-declare.d.ts.map +1 -0
  208. package/dist/postgres/grants-diff.d.ts +25 -0
  209. package/dist/postgres/grants-diff.d.ts.map +1 -0
  210. package/dist/postgres/grants-errors.d.ts +137 -0
  211. package/dist/postgres/grants-errors.d.ts.map +1 -0
  212. package/dist/postgres/grants-existence.d.ts +14 -0
  213. package/dist/postgres/grants-existence.d.ts.map +1 -0
  214. package/dist/postgres/grants-ops.d.ts +65 -0
  215. package/dist/postgres/grants-ops.d.ts.map +1 -0
  216. package/dist/postgres/grants-ownership.d.ts +18 -0
  217. package/dist/postgres/grants-ownership.d.ts.map +1 -0
  218. package/dist/postgres/grants-plan-restore.d.ts +22 -0
  219. package/dist/postgres/grants-plan-restore.d.ts.map +1 -0
  220. package/dist/postgres/grants-plan.d.ts +65 -0
  221. package/dist/postgres/grants-plan.d.ts.map +1 -0
  222. package/dist/postgres/grants-read-defaults.d.ts +17 -0
  223. package/dist/postgres/grants-read-defaults.d.ts.map +1 -0
  224. package/dist/postgres/grants-read.d.ts +77 -0
  225. package/dist/postgres/grants-read.d.ts.map +1 -0
  226. package/dist/postgres/grants-refuse.d.ts +25 -0
  227. package/dist/postgres/grants-refuse.d.ts.map +1 -0
  228. package/dist/postgres/grants-sql.d.ts +42 -0
  229. package/dist/postgres/grants-sql.d.ts.map +1 -0
  230. package/dist/postgres/grants-words.d.ts +29 -0
  231. package/dist/postgres/grants-words.d.ts.map +1 -0
  232. package/dist/postgres/grants.d.ts +37 -0
  233. package/dist/postgres/grants.d.ts.map +1 -0
  234. package/dist/postgres/index.d.ts +35 -8
  235. package/dist/postgres/index.d.ts.map +1 -1
  236. package/dist/postgres/index.js +2063 -39
  237. package/dist/postgres/index.js.map +47 -7
  238. package/dist/postgres/installed-classifier.d.ts +13 -0
  239. package/dist/postgres/installed-classifier.d.ts.map +1 -0
  240. package/dist/postgres/providers.d.ts +17 -7
  241. package/dist/postgres/providers.d.ts.map +1 -1
  242. package/dist/postgres/psql-executor.d.ts +25 -0
  243. package/dist/postgres/psql-executor.d.ts.map +1 -0
  244. package/dist/postgres/role-attrs.d.ts +109 -0
  245. package/dist/postgres/role-attrs.d.ts.map +1 -0
  246. package/dist/postgres/role-diff.d.ts +22 -0
  247. package/dist/postgres/role-diff.d.ts.map +1 -0
  248. package/dist/postgres/role-errors.d.ts +126 -0
  249. package/dist/postgres/role-errors.d.ts.map +1 -0
  250. package/dist/postgres/role-membership-sql.d.ts +75 -0
  251. package/dist/postgres/role-membership-sql.d.ts.map +1 -0
  252. package/dist/postgres/role-provider.d.ts +11 -0
  253. package/dist/postgres/role-provider.d.ts.map +1 -0
  254. package/dist/postgres/role-scram.d.ts +6 -0
  255. package/dist/postgres/role-scram.d.ts.map +1 -0
  256. package/dist/postgres/role-secrets.d.ts +30 -0
  257. package/dist/postgres/role-secrets.d.ts.map +1 -0
  258. package/dist/postgres/role-sql.d.ts +68 -0
  259. package/dist/postgres/role-sql.d.ts.map +1 -0
  260. package/dist/postgres/role.d.ts +57 -0
  261. package/dist/postgres/role.d.ts.map +1 -0
  262. package/dist/postgres/schema-assert.d.ts +50 -0
  263. package/dist/postgres/schema-assert.d.ts.map +1 -0
  264. package/dist/postgres/schema-attrs.d.ts +51 -0
  265. package/dist/postgres/schema-attrs.d.ts.map +1 -0
  266. package/dist/postgres/schema-delete.d.ts +28 -0
  267. package/dist/postgres/schema-delete.d.ts.map +1 -0
  268. package/dist/postgres/schema-diff.d.ts +14 -0
  269. package/dist/postgres/schema-diff.d.ts.map +1 -0
  270. package/dist/postgres/schema-errors.d.ts +139 -0
  271. package/dist/postgres/schema-errors.d.ts.map +1 -0
  272. package/dist/postgres/schema-identity.d.ts +8 -0
  273. package/dist/postgres/schema-identity.d.ts.map +1 -0
  274. package/dist/postgres/schema-reconcile.d.ts +12 -0
  275. package/dist/postgres/schema-reconcile.d.ts.map +1 -0
  276. package/dist/postgres/schema-sql.d.ts +45 -0
  277. package/dist/postgres/schema-sql.d.ts.map +1 -0
  278. package/dist/postgres/schema-test-kit.d.ts +67 -0
  279. package/dist/postgres/schema-test-kit.d.ts.map +1 -0
  280. package/dist/postgres/schema.d.ts +54 -0
  281. package/dist/postgres/schema.d.ts.map +1 -0
  282. package/dist/proxmox/ceph-flag-types.d.ts +57 -0
  283. package/dist/proxmox/ceph-flag-types.d.ts.map +1 -0
  284. package/dist/proxmox/ceph-flag-wire.d.ts +77 -0
  285. package/dist/proxmox/ceph-flag-wire.d.ts.map +1 -0
  286. package/dist/proxmox/ceph-flag.d.ts +15 -50
  287. package/dist/proxmox/ceph-flag.d.ts.map +1 -1
  288. package/dist/proxmox/ceph-osd-write.d.ts +10 -13
  289. package/dist/proxmox/ceph-osd-write.d.ts.map +1 -1
  290. package/dist/proxmox/index.js +806 -740
  291. package/dist/proxmox/index.js.map +9 -8
  292. package/dist/release/fake-engine.d.ts +10 -0
  293. package/dist/release/fake-engine.d.ts.map +1 -1
  294. package/dist/release/index.js +35 -1
  295. package/dist/release/index.js.map +3 -3
  296. package/dist/valkey/acl-attrs.d.ts +94 -0
  297. package/dist/valkey/acl-attrs.d.ts.map +1 -0
  298. package/dist/valkey/acl-compare.d.ts +28 -0
  299. package/dist/valkey/acl-compare.d.ts.map +1 -0
  300. package/dist/valkey/acl-connection.d.ts +6 -0
  301. package/dist/valkey/acl-connection.d.ts.map +1 -0
  302. package/dist/valkey/acl-form.d.ts +62 -0
  303. package/dist/valkey/acl-form.d.ts.map +1 -0
  304. package/dist/valkey/acl-managed.d.ts +4 -0
  305. package/dist/valkey/acl-managed.d.ts.map +1 -0
  306. package/dist/valkey/acl-ops.d.ts +37 -0
  307. package/dist/valkey/acl-ops.d.ts.map +1 -0
  308. package/dist/valkey/acl-profiles.d.ts +53 -0
  309. package/dist/valkey/acl-profiles.d.ts.map +1 -0
  310. package/dist/valkey/acl-reconcile.d.ts +26 -0
  311. package/dist/valkey/acl-reconcile.d.ts.map +1 -0
  312. package/dist/valkey/acl.d.ts +24 -0
  313. package/dist/valkey/acl.d.ts.map +1 -0
  314. package/dist/valkey/connection-service.d.ts +22 -0
  315. package/dist/valkey/connection-service.d.ts.map +1 -0
  316. package/dist/valkey/connection.d.ts +59 -0
  317. package/dist/valkey/connection.d.ts.map +1 -0
  318. package/dist/valkey/errors.d.ts +165 -0
  319. package/dist/valkey/errors.d.ts.map +1 -0
  320. package/dist/valkey/fake-acl.d.ts +29 -0
  321. package/dist/valkey/fake-acl.d.ts.map +1 -0
  322. package/dist/valkey/fake-valkey.d.ts +35 -0
  323. package/dist/valkey/fake-valkey.d.ts.map +1 -0
  324. package/dist/valkey/handler-test-fixture.d.ts +20 -0
  325. package/dist/valkey/handler-test-fixture.d.ts.map +1 -0
  326. package/dist/valkey/index.d.ts +18 -0
  327. package/dist/valkey/index.d.ts.map +1 -0
  328. package/dist/valkey/index.js +964 -0
  329. package/dist/valkey/index.js.map +27 -0
  330. package/dist/valkey/instance-attrs.d.ts +46 -0
  331. package/dist/valkey/instance-attrs.d.ts.map +1 -0
  332. package/dist/valkey/instance-form.d.ts +22 -0
  333. package/dist/valkey/instance-form.d.ts.map +1 -0
  334. package/dist/valkey/instance.d.ts +48 -0
  335. package/dist/valkey/instance.d.ts.map +1 -0
  336. package/dist/valkey/provider-socket-fixture.d.ts +8 -0
  337. package/dist/valkey/provider-socket-fixture.d.ts.map +1 -0
  338. package/dist/valkey/providers.d.ts +16 -0
  339. package/dist/valkey/providers.d.ts.map +1 -0
  340. package/dist/valkey/resp.d.ts +38 -0
  341. package/dist/valkey/resp.d.ts.map +1 -0
  342. package/dist/valkey/transport-error.d.ts +36 -0
  343. package/dist/valkey/transport-error.d.ts.map +1 -0
  344. package/dist/valkey/transport.d.ts +50 -0
  345. package/dist/valkey/transport.d.ts.map +1 -0
  346. package/docs/litellm-access-group.md +105 -0
  347. package/docs/litellm-budget.md +55 -0
  348. package/docs/litellm-credential.md +60 -0
  349. package/docs/litellm-key-metadata.md +56 -0
  350. package/docs/litellm-key.md +194 -0
  351. package/docs/litellm-mcp.md +187 -0
  352. package/docs/litellm-model.md +111 -0
  353. package/docs/litellm-policy-attachment.md +86 -0
  354. package/docs/litellm-policy.md +103 -0
  355. package/docs/litellm-team.md +117 -0
  356. package/docs/litellm-tool-policy.md +90 -0
  357. package/docs/litellm-toolset.md +92 -0
  358. package/docs/litellm.md +16 -8
  359. package/docs/peers.md +15 -6
  360. package/docs/postgres-grants-example.md +55 -0
  361. package/docs/postgres-grants.md +157 -0
  362. package/docs/postgres-role.md +114 -0
  363. package/docs/postgres-schema.md +139 -0
  364. package/docs/postgres.md +39 -0
  365. package/docs/proxmox-sdk-migration.md +45 -0
  366. package/docs/valkey-transport.md +29 -0
  367. package/docs/valkey.md +187 -0
  368. package/package.json +7 -3
  369. package/src/litellm/access-group-form.ts +105 -0
  370. package/src/litellm/access-group-operations.ts +92 -0
  371. package/src/litellm/access-group-types.ts +49 -0
  372. package/src/litellm/access-group.test.ts +197 -0
  373. package/src/litellm/access-group.ts +151 -0
  374. package/src/litellm/budget-errors.ts +38 -0
  375. package/src/litellm/budget-form.ts +94 -0
  376. package/src/litellm/budget-operations.ts +51 -0
  377. package/src/litellm/budget.test.ts +105 -0
  378. package/src/litellm/budget.ts +127 -0
  379. package/src/litellm/credential-errors.ts +183 -0
  380. package/src/litellm/credential-form.test.ts +151 -0
  381. package/src/litellm/credential-form.ts +211 -0
  382. package/src/litellm/credential-guards.test.ts +241 -0
  383. package/src/litellm/credential-operations.ts +208 -0
  384. package/src/litellm/credential-recovery.test.ts +93 -0
  385. package/src/litellm/credential-rewrite.test.ts +164 -0
  386. package/src/litellm/credential-rewrite.ts +50 -0
  387. package/src/litellm/credential-types.ts +117 -0
  388. package/src/litellm/credential-values.ts +94 -0
  389. package/src/litellm/credential.test.ts +218 -0
  390. package/src/litellm/credential.ts +227 -0
  391. package/src/litellm/fake-access-group-litellm.ts +127 -0
  392. package/src/litellm/fake-credential-litellm.ts +216 -0
  393. package/src/litellm/fake-keys.ts +190 -0
  394. package/src/litellm/fake-litellm.ts +66 -7
  395. package/src/litellm/fake-mcp-litellm.ts +214 -0
  396. package/src/litellm/fake-model-litellm-post-update.ts +103 -0
  397. package/src/litellm/fake-model-litellm.ts +233 -0
  398. package/src/litellm/fake-policy-attachment-litellm.ts +97 -0
  399. package/src/litellm/fake-policy-litellm.ts +186 -0
  400. package/src/litellm/fake-registry-base.ts +69 -0
  401. package/src/litellm/fake-stack.ts +17 -1
  402. package/src/litellm/fake-team-litellm.ts +229 -0
  403. package/src/litellm/fake-tool-litellm.ts +88 -0
  404. package/src/litellm/fake-toolset-litellm.ts +110 -0
  405. package/src/litellm/index.ts +153 -0
  406. package/src/litellm/key-delete.test.ts +212 -0
  407. package/src/litellm/key-errors.ts +241 -0
  408. package/src/litellm/key-form.ts +200 -0
  409. package/src/litellm/key-harness.ts +121 -0
  410. package/src/litellm/key-metadata-errors.ts +49 -0
  411. package/src/litellm/key-metadata.test.ts +156 -0
  412. package/src/litellm/key-metadata.ts +74 -0
  413. package/src/litellm/key-operations.ts +184 -0
  414. package/src/litellm/key-rotation.test.ts +110 -0
  415. package/src/litellm/key-scope.test.ts +135 -0
  416. package/src/litellm/key-secret.test.ts +248 -0
  417. package/src/litellm/key-secret.ts +137 -0
  418. package/src/litellm/key-transport.test.ts +166 -0
  419. package/src/litellm/key.test.ts +231 -0
  420. package/src/litellm/key.ts +213 -0
  421. package/src/litellm/mcp-server-adopt.test.ts +86 -0
  422. package/src/litellm/mcp-server-alias.test.ts +184 -0
  423. package/src/litellm/mcp-server-credential.ts +85 -0
  424. package/src/litellm/mcp-server-errors.ts +141 -0
  425. package/src/litellm/mcp-server-exports.test.ts +15 -0
  426. package/src/litellm/mcp-server-form.test.ts +241 -0
  427. package/src/litellm/mcp-server-form.ts +233 -0
  428. package/src/litellm/mcp-server-handlers.test.ts +150 -0
  429. package/src/litellm/mcp-server-locate.ts +66 -0
  430. package/src/litellm/mcp-server-operations.ts +111 -0
  431. package/src/litellm/mcp-server-registry.test.ts +146 -0
  432. package/src/litellm/mcp-server-retype.test.ts +114 -0
  433. package/src/litellm/mcp-server-shadow.test.ts +91 -0
  434. package/src/litellm/mcp-server-shadow.ts +45 -0
  435. package/src/litellm/mcp-server-tools.test.ts +85 -0
  436. package/src/litellm/mcp-server-types.ts +143 -0
  437. package/src/litellm/mcp-server-url.ts +66 -0
  438. package/src/litellm/mcp-server.test.ts +213 -0
  439. package/src/litellm/mcp-server.ts +225 -0
  440. package/src/litellm/model-claim.ts +69 -0
  441. package/src/litellm/model-credential.ts +45 -0
  442. package/src/litellm/model-errors.ts +168 -0
  443. package/src/litellm/model-foreign-row.test.ts +173 -0
  444. package/src/litellm/model-form.test.ts +228 -0
  445. package/src/litellm/model-form.ts +223 -0
  446. package/src/litellm/model-locate.test.ts +128 -0
  447. package/src/litellm/model-locate.ts +67 -0
  448. package/src/litellm/model-operations.test.ts +93 -0
  449. package/src/litellm/model-operations.ts +146 -0
  450. package/src/litellm/model-params-preserve.test.ts +119 -0
  451. package/src/litellm/model-recovery.test.ts +57 -0
  452. package/src/litellm/model-types.ts +112 -0
  453. package/src/litellm/model.test.ts +222 -0
  454. package/src/litellm/model.ts +237 -0
  455. package/src/litellm/operations.ts +1 -1
  456. package/src/litellm/policy-attachment-form.ts +103 -0
  457. package/src/litellm/policy-attachment-operations.ts +74 -0
  458. package/src/litellm/policy-attachment-types.ts +48 -0
  459. package/src/litellm/policy-attachment.test.ts +198 -0
  460. package/src/litellm/policy-attachment.ts +178 -0
  461. package/src/litellm/policy-form.ts +122 -0
  462. package/src/litellm/policy-operations.ts +112 -0
  463. package/src/litellm/policy-types.ts +58 -0
  464. package/src/litellm/policy.test.ts +214 -0
  465. package/src/litellm/policy.ts +146 -0
  466. package/src/litellm/providers.ts +25 -1
  467. package/src/litellm/registry-errors.ts +88 -0
  468. package/src/litellm/registry-exports.test.ts +66 -0
  469. package/src/litellm/registry-support.ts +109 -0
  470. package/src/litellm/team-form.ts +140 -0
  471. package/src/litellm/team-members.test.ts +150 -0
  472. package/src/litellm/team-members.ts +58 -0
  473. package/src/litellm/team-operations.ts +90 -0
  474. package/src/litellm/team-permission.ts +159 -0
  475. package/src/litellm/team-read.test.ts +89 -0
  476. package/src/litellm/team-types.ts +106 -0
  477. package/src/litellm/team.test.ts +206 -0
  478. package/src/litellm/team.ts +159 -0
  479. package/src/litellm/tool-policy-form.ts +94 -0
  480. package/src/litellm/tool-policy-operations.ts +58 -0
  481. package/src/litellm/tool-policy-types.ts +48 -0
  482. package/src/litellm/tool-policy.test.ts +172 -0
  483. package/src/litellm/tool-policy.ts +125 -0
  484. package/src/litellm/toolset-form.ts +104 -0
  485. package/src/litellm/toolset-operations.ts +90 -0
  486. package/src/litellm/toolset-types.ts +47 -0
  487. package/src/litellm/toolset.test.ts +198 -0
  488. package/src/litellm/toolset.ts +151 -0
  489. package/src/openbao/auth-role-form.test.ts +37 -0
  490. package/src/openbao/auth-role-form.ts +12 -1
  491. package/src/openbao/auth-role-wire.test.ts +1 -0
  492. package/src/openbao/auth-role-wire.ts +13 -1
  493. package/src/openbao/distilled-retry.test.ts +1 -0
  494. package/src/openbao/fake-engines-roles.ts +7 -1
  495. package/src/ownership/ownership.test.ts +33 -1
  496. package/src/ownership/rows.ts +53 -0
  497. package/src/postgres/connection.ts +72 -3
  498. package/src/postgres/database-bigint-serialization.test.ts +1 -0
  499. package/src/postgres/database-refusals.test.ts +2 -2
  500. package/src/postgres/database-sql.ts +40 -12
  501. package/src/postgres/database.ts +3 -2
  502. package/src/postgres/fake-grants-parse.ts +189 -0
  503. package/src/postgres/fake-grants-read.ts +250 -0
  504. package/src/postgres/fake-grants-sql.ts +207 -0
  505. package/src/postgres/fake-grants-write.ts +90 -0
  506. package/src/postgres/fake-role-sql.ts +220 -0
  507. package/src/postgres/fake-schema-semantics.test.ts +36 -0
  508. package/src/postgres/fake-schema-sql.ts +146 -0
  509. package/src/postgres/fake-sql-parse.ts +86 -0
  510. package/src/postgres/fake-sql-quote.ts +36 -0
  511. package/src/postgres/fake-sql.ts +161 -46
  512. package/src/postgres/fixtures/pg-REL_18_6-acl.h.txt +147 -0
  513. package/src/postgres/fixtures/pg-REL_18_6-aclexplode-func.txt +31 -0
  514. package/src/postgres/fixtures/pg-REL_18_6-alter-default-privileges-synopsis.txt +82 -0
  515. package/src/postgres/fixtures/pg-REL_18_6-alter_role.sgml.txt +44 -0
  516. package/src/postgres/fixtures/pg-REL_18_6-create_role.sgml.txt +26 -0
  517. package/src/postgres/fixtures/pg-REL_18_6-grant-synopsis.txt +95 -0
  518. package/src/postgres/fixtures/pg-REL_18_6-pg_attribute.h.txt +24 -0
  519. package/src/postgres/fixtures/pg-REL_18_6-pg_auth_members.h.txt +62 -0
  520. package/src/postgres/fixtures/pg-REL_18_6-pg_authid.h.txt +70 -0
  521. package/src/postgres/fixtures/pg-REL_18_6-pg_class.h.txt +33 -0
  522. package/src/postgres/fixtures/pg-REL_18_6-pg_default_acl.h.txt +81 -0
  523. package/src/postgres/fixtures/pg-REL_18_6-pg_namespace.h.txt +28 -0
  524. package/src/postgres/fixtures/pg-REL_18_6-revoke-synopsis.txt +124 -0
  525. package/src/postgres/grants-acl.test.ts +61 -0
  526. package/src/postgres/grants-attrs.ts +185 -0
  527. package/src/postgres/grants-database.test.ts +158 -0
  528. package/src/postgres/grants-declare.ts +172 -0
  529. package/src/postgres/grants-delete-proof.test.ts +154 -0
  530. package/src/postgres/grants-diff.ts +101 -0
  531. package/src/postgres/grants-errors.ts +215 -0
  532. package/src/postgres/grants-existence-guards.test.ts +113 -0
  533. package/src/postgres/grants-existence.ts +101 -0
  534. package/src/postgres/grants-lifecycle-guards.test.ts +208 -0
  535. package/src/postgres/grants-lifecycle-repair.test.ts +208 -0
  536. package/src/postgres/grants-lifecycle.test.ts +181 -0
  537. package/src/postgres/grants-ops.ts +229 -0
  538. package/src/postgres/grants-owner-revoke.test.ts +121 -0
  539. package/src/postgres/grants-ownership.ts +49 -0
  540. package/src/postgres/grants-plan-collateral.test.ts +202 -0
  541. package/src/postgres/grants-plan-diff.test.ts +204 -0
  542. package/src/postgres/grants-plan-restore.ts +43 -0
  543. package/src/postgres/grants-plan-revocations.test.ts +69 -0
  544. package/src/postgres/grants-plan.test.ts +148 -0
  545. package/src/postgres/grants-plan.ts +245 -0
  546. package/src/postgres/grants-provenance.test.ts +134 -0
  547. package/src/postgres/grants-psql-lifecycle.test.ts +105 -0
  548. package/src/postgres/grants-read-absent.test.ts +91 -0
  549. package/src/postgres/grants-read-defaults.ts +51 -0
  550. package/src/postgres/grants-read.ts +250 -0
  551. package/src/postgres/grants-refusals.test.ts +136 -0
  552. package/src/postgres/grants-refuse.ts +119 -0
  553. package/src/postgres/grants-relkind-wire.test.ts +111 -0
  554. package/src/postgres/grants-repair-plan.test.ts +201 -0
  555. package/src/postgres/grants-sql.test.ts +128 -0
  556. package/src/postgres/grants-sql.ts +144 -0
  557. package/src/postgres/grants-words.ts +34 -0
  558. package/src/postgres/grants.ts +110 -0
  559. package/src/postgres/index.ts +137 -7
  560. package/src/postgres/installed-classifier.ts +36 -0
  561. package/src/postgres/providers.ts +44 -7
  562. package/src/postgres/psql-executor-escaping.test.ts +67 -0
  563. package/src/postgres/psql-executor.test.ts +167 -0
  564. package/src/postgres/psql-executor.ts +170 -0
  565. package/src/postgres/role-adopt.test.ts +98 -0
  566. package/src/postgres/role-attrs.ts +134 -0
  567. package/src/postgres/role-diff.ts +89 -0
  568. package/src/postgres/role-errors.ts +198 -0
  569. package/src/postgres/role-guard.test.ts +60 -0
  570. package/src/postgres/role-identity.test.ts +185 -0
  571. package/src/postgres/role-lifecycle.test.ts +228 -0
  572. package/src/postgres/role-membership-sql.ts +198 -0
  573. package/src/postgres/role-password.test.ts +158 -0
  574. package/src/postgres/role-privilege.test.ts +80 -0
  575. package/src/postgres/role-provenance.test.ts +200 -0
  576. package/src/postgres/role-provider.ts +104 -0
  577. package/src/postgres/role-refusals.test.ts +250 -0
  578. package/src/postgres/role-scram.test.ts +38 -0
  579. package/src/postgres/role-scram.ts +38 -0
  580. package/src/postgres/role-secrets.ts +46 -0
  581. package/src/postgres/role-sql.ts +211 -0
  582. package/src/postgres/role-valid-until.test.ts +68 -0
  583. package/src/postgres/role.ts +218 -0
  584. package/src/postgres/schema-assert.ts +139 -0
  585. package/src/postgres/schema-attrs.ts +71 -0
  586. package/src/postgres/schema-delete-proofs.test.ts +192 -0
  587. package/src/postgres/schema-delete.ts +96 -0
  588. package/src/postgres/schema-diff.ts +44 -0
  589. package/src/postgres/schema-drop-2bp01.test.ts +146 -0
  590. package/src/postgres/schema-errors.ts +209 -0
  591. package/src/postgres/schema-handlers.test.ts +193 -0
  592. package/src/postgres/schema-identity.ts +39 -0
  593. package/src/postgres/schema-lifecycle.test.ts +246 -0
  594. package/src/postgres/schema-race.test.ts +70 -0
  595. package/src/postgres/schema-reconcile.ts +100 -0
  596. package/src/postgres/schema-refusals.test.ts +144 -0
  597. package/src/postgres/schema-socket.test.ts +161 -0
  598. package/src/postgres/schema-sql.ts +130 -0
  599. package/src/postgres/schema-takeover.test.ts +149 -0
  600. package/src/postgres/schema-test-kit.ts +85 -0
  601. package/src/postgres/schema.ts +130 -0
  602. package/src/proxmox/ceph-flag-types.ts +70 -0
  603. package/src/proxmox/ceph-flag-wire.test.ts +72 -0
  604. package/src/proxmox/ceph-flag-wire.ts +123 -0
  605. package/src/proxmox/ceph-flag.ts +77 -130
  606. package/src/proxmox/ceph-osd-write.ts +10 -13
  607. package/src/release/binary-alias.test.ts +3 -3
  608. package/src/release/binary-claims.test.ts +3 -3
  609. package/src/release/binary-directory-umask.test.ts +65 -0
  610. package/src/release/fake-engine.ts +15 -0
  611. package/src/valkey/acl-attrs.ts +98 -0
  612. package/src/valkey/acl-compare.ts +57 -0
  613. package/src/valkey/acl-connection.ts +13 -0
  614. package/src/valkey/acl-delete.test.ts +127 -0
  615. package/src/valkey/acl-form.ts +239 -0
  616. package/src/valkey/acl-managed.ts +13 -0
  617. package/src/valkey/acl-ops.ts +122 -0
  618. package/src/valkey/acl-ownership.test.ts +118 -0
  619. package/src/valkey/acl-prefix.test.ts +38 -0
  620. package/src/valkey/acl-profiles.ts +81 -0
  621. package/src/valkey/acl-reconcile.ts +180 -0
  622. package/src/valkey/acl-reserved.test.ts +69 -0
  623. package/src/valkey/acl-revocation.test.ts +94 -0
  624. package/src/valkey/acl-seat-prefix.test.ts +111 -0
  625. package/src/valkey/acl-validation.test.ts +54 -0
  626. package/src/valkey/acl.test.ts +215 -0
  627. package/src/valkey/acl.ts +169 -0
  628. package/src/valkey/connection-service.ts +46 -0
  629. package/src/valkey/connection.ts +180 -0
  630. package/src/valkey/errors.ts +239 -0
  631. package/src/valkey/fake-acl.ts +128 -0
  632. package/src/valkey/fake-valkey.ts +224 -0
  633. package/src/valkey/handler-test-fixture.ts +43 -0
  634. package/src/valkey/handlers.test.ts +205 -0
  635. package/src/valkey/index.ts +54 -0
  636. package/src/valkey/instance-attrs.ts +48 -0
  637. package/src/valkey/instance-form.ts +104 -0
  638. package/src/valkey/instance.test.ts +108 -0
  639. package/src/valkey/instance.ts +160 -0
  640. package/src/valkey/provider-socket-fixture.ts +71 -0
  641. package/src/valkey/providers.test.ts +129 -0
  642. package/src/valkey/providers.ts +21 -0
  643. package/src/valkey/resp.ts +212 -0
  644. package/src/valkey/transport-bounds.test.ts +150 -0
  645. package/src/valkey/transport-error.ts +37 -0
  646. package/src/valkey/transport.test.ts +158 -0
  647. package/src/valkey/transport.ts +109 -0
  648. package/src/valkey/upstream-bounds.test.ts +14 -0
@@ -0,0 +1,185 @@
1
+ /**
2
+ * Oid identity, live membership options, and the unfinished-create resume. The engine passes
3
+ * stored attributes to `diff` and calls `read` with `output: undefined` on a `creating` row;
4
+ * these tests drive `postgresRoleHandlers` the way that engine does, over the runner transport.
5
+ */
6
+ import { describe, expect, test } from 'bun:test';
7
+ import { Unowned } from 'alchemy/AdoptPolicy';
8
+ import { Artifacts, makeScopedArtifacts } from 'alchemy/Artifacts';
9
+ import { InMemoryService } from 'alchemy/State/InMemoryState';
10
+ import { State } from 'alchemy/State/State';
11
+ import { Stack } from 'alchemy/Stack';
12
+ import * as Effect from 'effect/Effect';
13
+ import { unfinished } from '../ownership/resume.ts';
14
+ import { postgresRunnerConnection } from './connection.ts';
15
+ import { parseLiteral } from './fake-sql-quote.ts';
16
+ import { makeFakeSql } from './fake-sql.ts';
17
+ import { type PsqlRunner } from './psql-executor.ts';
18
+ import { PostgresRoleIdentityRefused } from './role-errors.ts';
19
+ import { postgresRoleHandlers } from './role-provider.ts';
20
+ import type { PostgresRoleAttributes, PostgresRoleProps } from './role-attrs.ts';
21
+
22
+ const baseProps: PostgresRoleProps = {
23
+ name: 'seat-observability',
24
+ login: false,
25
+ connectionLimit: 10,
26
+ inherit: true,
27
+ };
28
+
29
+ const stored = (): PostgresRoleAttributes => ({
30
+ name: 'seat-observability',
31
+ oid: 20000,
32
+ login: false,
33
+ connectionLimit: 10,
34
+ inherit: true,
35
+ validUntil: null,
36
+ memberOf: ['hf_agent'],
37
+ passwordSeal: '',
38
+ });
39
+
40
+ /** Same envelope the adopt test uses: `withPg` wraps a SELECT in `json_agg` and inlines `$1`. */
41
+ const runnerFor =
42
+ (fake: ReturnType<typeof makeFakeSql>): PsqlRunner =>
43
+ async ({ stdin }) => {
44
+ const wrapped = /^SELECT coalesce\(json_agg\(t\), '\[\]'::json\)::text FROM \((.*)\) t;$/s.exec(
45
+ stdin,
46
+ );
47
+ const body = (wrapped?.[1] ?? stdin).replace(/;\s*$/, '');
48
+ const bound = /^([\s\S]*) = (E'[\s\S]*')$/.exec(body);
49
+ const sql = bound === null ? body : `${bound[1]} = $1`;
50
+ const params = bound === null ? [] : [parseLiteral(bound[2] as string)];
51
+ const rows = await Effect.runPromise(fake.unsafe(sql, params));
52
+ return { code: 0, stdout: JSON.stringify(rows), stderr: '' };
53
+ };
54
+
55
+ const layerFor = (fake: ReturnType<typeof makeFakeSql>) =>
56
+ postgresRunnerConnection({
57
+ run: runnerFor(fake),
58
+ database: 'postgres',
59
+ username: 'postgres',
60
+ });
61
+
62
+ const handlers = postgresRoleHandlers;
63
+ if (handlers.read === undefined || handlers.diff === undefined || handlers.delete === undefined) {
64
+ throw new Error('Postgres.Role read, diff and delete are part of the provider');
65
+ }
66
+ const readRoleHandler = handlers.read;
67
+ const diffRoleHandler = handlers.diff;
68
+ const deleteRoleHandler = handlers.delete;
69
+
70
+ const ids = {
71
+ id: 'seat-observability',
72
+ fqn: 'Postgres.Role/seat-observability',
73
+ instanceId: 'i',
74
+ };
75
+
76
+ describe('oid and live membership options', () => {
77
+ test('a recycled oid is refused on read, diff and delete, and delete issues no DROP', async () => {
78
+ const fake = makeFakeSql({ roleRows: [{ ...stored(), oid: 7 }] });
79
+ const output = stored();
80
+ const provided = <A, E, R>(effect: Effect.Effect<A, E, R>) =>
81
+ effect.pipe(Effect.provide(layerFor(fake)));
82
+ const readError = await Effect.runPromise(
83
+ Effect.flip(provided(readRoleHandler({ ...ids, olds: baseProps, output }))),
84
+ );
85
+ expect(readError).toBeInstanceOf(PostgresRoleIdentityRefused);
86
+ const diffError = await Effect.runPromise(
87
+ Effect.flip(
88
+ provided(
89
+ diffRoleHandler({
90
+ ...ids,
91
+ olds: baseProps,
92
+ news: { ...baseProps, memberOf: ['hf_agent'] },
93
+ output,
94
+ oldBindings: [],
95
+ newBindings: [],
96
+ }),
97
+ ),
98
+ ),
99
+ );
100
+ expect(diffError).toBeInstanceOf(PostgresRoleIdentityRefused);
101
+ await Effect.runPromise(
102
+ Effect.flip(
103
+ provided(
104
+ deleteRoleHandler({
105
+ ...ids,
106
+ olds: baseProps,
107
+ output,
108
+ session: {} as never,
109
+ bindings: [],
110
+ }),
111
+ ),
112
+ ),
113
+ );
114
+ expect(fake.statements.some((s) => s.text.startsWith('DROP'))).toBe(false);
115
+ });
116
+
117
+ test('diff reads live ADMIN even though stored output has no membership rows', async () => {
118
+ const fake = makeFakeSql({ roleRows: [stored()] });
119
+ fake.memberships.add('seat-observability\0hf_agent\0postgres');
120
+ fake.membershipOptions.set('seat-observability\0hf_agent\0postgres', {
121
+ admin: true,
122
+ set: false,
123
+ });
124
+ const plan = await Effect.runPromise(
125
+ diffRoleHandler({
126
+ ...ids,
127
+ olds: baseProps,
128
+ news: { ...baseProps, memberOf: ['hf_agent'] },
129
+ output: stored(),
130
+ oldBindings: [],
131
+ newBindings: [],
132
+ }).pipe(Effect.provide(layerFor(fake))),
133
+ );
134
+ expect(plan).toEqual({ action: 'update' });
135
+ });
136
+
137
+ test('a creating row with no attributes is noted unfinished', async () => {
138
+ const bag = makeScopedArtifacts(new Map(), ids.fqn);
139
+ const plan = await Effect.runPromise(
140
+ diffRoleHandler({
141
+ ...ids,
142
+ instanceId: 'creating-1',
143
+ olds: baseProps,
144
+ news: baseProps,
145
+ output: undefined,
146
+ oldBindings: [],
147
+ newBindings: [],
148
+ }).pipe(Effect.provideService(Artifacts, bag), Effect.provide(layerFor(makeFakeSql()))),
149
+ );
150
+ expect(plan).toBeUndefined();
151
+ expect(
152
+ await Effect.runPromise(unfinished('creating-1').pipe(Effect.provideService(Artifacts, bag))),
153
+ ).toBe(true);
154
+ });
155
+
156
+ test('a creating row whose live role matches the declaration is ours', async () => {
157
+ const fake = makeFakeSql({ roleRows: [{ ...stored(), memberOf: [] }] });
158
+ const row = {
159
+ fqn: ids.fqn,
160
+ instanceId: 'creating-1',
161
+ status: 'creating',
162
+ props: baseProps,
163
+ };
164
+ const found = await Effect.runPromise(
165
+ readRoleHandler({
166
+ ...ids,
167
+ instanceId: 'creating-1',
168
+ olds: baseProps,
169
+ output: undefined,
170
+ }).pipe(
171
+ Effect.provide(layerFor(fake)),
172
+ Effect.provideService(State, InMemoryService({ s: { test: { [ids.fqn]: row as never } } })),
173
+ Effect.provideService(Stack, {
174
+ actions: {},
175
+ bindings: {},
176
+ name: 's',
177
+ resources: { [ids.fqn]: { Props: baseProps } },
178
+ stage: 'test',
179
+ } as never),
180
+ ),
181
+ );
182
+ expect(Unowned.is(found)).toBe(false);
183
+ expect(found?.oid).toBe(20000);
184
+ });
185
+ });
@@ -0,0 +1,228 @@
1
+ /**
2
+ * `reconcile`, `read` and the drop statement against `fake-sql.ts`'s recording fake — greenfield
3
+ * create with exact statement text, membership `GRANT`/`REVOKE`, per-field scalar drift, a create
4
+ * whose re-read finds nothing, and zero statements when nothing drifted. The password lifecycle
5
+ * is `role-password.test.ts`; the plan-time refusals are `role-refusals.test.ts`. Every case
6
+ * reverts cleanly by reverting `role.ts`/`role-sql.ts`/`role-membership-sql.ts`/`fake-sql.ts`
7
+ * locally: these fail on `origin/main`, which has none of them.
8
+ */
9
+ import { describe, expect, test } from 'bun:test';
10
+ import * as Effect from 'effect/Effect';
11
+ import type { PgExecutor } from './database-sql.ts';
12
+ import { makeFakeSql } from './fake-sql.ts';
13
+ import { PostgresRoleCreateVanished } from './role-errors.ts';
14
+ import type { PostgresRoleAttributes, PostgresRoleProps } from './role-attrs.ts';
15
+ import { readRole, reconcileWithClient } from './role.ts';
16
+
17
+ const run = <A, E>(eff: Effect.Effect<A, E>): Promise<A> => Effect.runPromise(eff);
18
+ const fails = <A, E>(eff: Effect.Effect<A, E>): Promise<E> => Effect.runPromise(Effect.flip(eff));
19
+
20
+ const baseProps: PostgresRoleProps = {
21
+ name: 'seat-observability',
22
+ login: false,
23
+ connectionLimit: 10,
24
+ inherit: true,
25
+ };
26
+
27
+ const liveRole = (over: Partial<PostgresRoleAttributes> = {}): 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
+ ...over,
37
+ });
38
+
39
+ const startingWith = (fake: ReturnType<typeof makeFakeSql>, prefix: string) =>
40
+ fake.statements.filter((s) => s.text.startsWith(prefix));
41
+
42
+ const writeStatements = (fake: ReturnType<typeof makeFakeSql>) =>
43
+ fake.statements.filter(
44
+ (s) =>
45
+ s.text.startsWith('CREATE') ||
46
+ s.text.startsWith('ALTER') ||
47
+ s.text.startsWith('GRANT') ||
48
+ s.text.startsWith('REVOKE') ||
49
+ s.text.startsWith('DROP'),
50
+ );
51
+
52
+ describe('reconcile: greenfield', () => {
53
+ test('issues exactly one CREATE with the seat shape, and nothing else', async () => {
54
+ const fake = makeFakeSql();
55
+ const attrs = await run(reconcileWithClient(fake, baseProps));
56
+ expect(attrs.name).toBe('seat-observability');
57
+ expect(startingWith(fake, 'CREATE ROLE')).toEqual([
58
+ {
59
+ text: 'CREATE ROLE "seat-observability" WITH NOLOGIN INHERIT CONNECTION LIMIT 10',
60
+ params: [],
61
+ },
62
+ ]);
63
+ expect(writeStatements(fake).length).toBe(1);
64
+ });
65
+
66
+ test('a LOGIN role with an expiry carries VALID UNTIL last, quoted', async () => {
67
+ const fake = makeFakeSql();
68
+ const props = {
69
+ name: 'seat-alice',
70
+ login: true,
71
+ connectionLimit: 10,
72
+ inherit: true,
73
+ validUntil: '2027-01-01T00:00:00Z',
74
+ };
75
+ const attrs = await run(reconcileWithClient(fake, props));
76
+ expect(attrs.validUntil).toBe('2027-01-01T00:00:00Z');
77
+ expect(startingWith(fake, 'CREATE ROLE')[0]?.text).toBe(
78
+ 'CREATE ROLE "seat-alice" WITH LOGIN INHERIT CONNECTION LIMIT 10 VALID UNTIL E\'2027-01-01T00:00:00Z\'',
79
+ );
80
+ });
81
+
82
+ test('declared memberships become one-parent GRANTs after the CREATE, and the answer comes from the re-read, sorted', async () => {
83
+ const fake = makeFakeSql({ roles: ['hf_agent', 'seat-b'] });
84
+ const attrs = await run(
85
+ reconcileWithClient(fake, { ...baseProps, memberOf: ['seat-b', 'hf_agent'] }),
86
+ );
87
+ expect(startingWith(fake, 'GRANT')).toEqual([
88
+ { text: 'GRANT "hf_agent" TO "seat-observability" WITH SET FALSE', params: [] },
89
+ { text: 'GRANT "seat-b" TO "seat-observability" WITH SET FALSE', params: [] },
90
+ ]);
91
+ expect(attrs.memberOf).toEqual(['hf_agent', 'seat-b']);
92
+ });
93
+
94
+ test('a hostile role name survives the round trip through the fake\u2019s own catalog', async () => {
95
+ const fake = makeFakeSql();
96
+ const attrs = await run(reconcileWithClient(fake, { ...baseProps, name: 'a"b-c' }));
97
+ expect(attrs.name).toBe('a"b-c');
98
+ expect(startingWith(fake, 'CREATE ROLE')[0]?.text).toBe(
99
+ 'CREATE ROLE "a""b-c" WITH NOLOGIN INHERIT CONNECTION LIMIT 10',
100
+ );
101
+ });
102
+
103
+ test('a create whose re-read finds nothing fails with the typed tag, not a guess (S10/S20)', async () => {
104
+ const unsafe = <A extends object>(_sql?: string) => Effect.succeed([] as ReadonlyArray<A>);
105
+ const blankPg: PgExecutor = {
106
+ unsafe,
107
+ transaction: (statements) =>
108
+ Effect.gen(function* () {
109
+ for (const sql of statements) yield* unsafe(sql).pipe(Effect.asVoid);
110
+ }),
111
+ };
112
+ const error = await fails(reconcileWithClient(blankPg, baseProps));
113
+ expect(error).toBeInstanceOf(PostgresRoleCreateVanished);
114
+ });
115
+ });
116
+
117
+ describe('reconcile: already present', () => {
118
+ test('no drift issues zero write statements and returns the live row', async () => {
119
+ const fake = makeFakeSql({ roleRows: [liveRole()] });
120
+ const attrs = await run(reconcileWithClient(fake, baseProps));
121
+ expect(attrs).toEqual(liveRole());
122
+ expect(writeStatements(fake).length).toBe(0);
123
+ });
124
+
125
+ test('scalar drift issues exactly one ALTER per drifted field, in the compared order', async () => {
126
+ const fake = makeFakeSql({
127
+ roleRows: [
128
+ liveRole({
129
+ login: true,
130
+ inherit: false,
131
+ connectionLimit: 1,
132
+ validUntil: '2026-01-01T00:00:00.000Z',
133
+ }),
134
+ ],
135
+ });
136
+ const props = {
137
+ ...baseProps,
138
+ login: false,
139
+ inherit: true,
140
+ connectionLimit: 10,
141
+ validUntil: '2027-01-01T00:00:00Z',
142
+ };
143
+ const attrs = await run(reconcileWithClient(fake, props));
144
+ expect(startingWith(fake, 'ALTER ROLE').map((s) => s.text)).toEqual([
145
+ 'ALTER ROLE "seat-observability" WITH NOLOGIN',
146
+ 'ALTER ROLE "seat-observability" WITH INHERIT',
147
+ 'ALTER ROLE "seat-observability" WITH CONNECTION LIMIT 10',
148
+ 'ALTER ROLE "seat-observability" WITH VALID UNTIL E\'2027-01-01T00:00:00Z\'',
149
+ ]);
150
+ expect(attrs.connectionLimit).toBe(10);
151
+ });
152
+
153
+ test('a validUntil the server serialised with milliseconds is the same instant, so no ALTER', async () => {
154
+ const fake = makeFakeSql({
155
+ roleRows: [liveRole({ validUntil: '2027-01-01T00:00:00.000Z' })],
156
+ });
157
+ await run(reconcileWithClient(fake, { ...baseProps, validUntil: '2027-01-01T00:00:00Z' }));
158
+ expect(fake.statements.some((s) => s.text.startsWith('ALTER'))).toBe(false);
159
+ });
160
+
161
+ test('a membership the declaration adds is granted; a live one it drops is revoked by its grantor', async () => {
162
+ const fake = makeFakeSql({ roleRows: [liveRole()], roles: ['seat-owners'] });
163
+ fake.memberships.add('seat-observability\0hf_agent\0postgres');
164
+ const attrs = await run(reconcileWithClient(fake, { ...baseProps, memberOf: ['seat-owners'] }));
165
+ expect(startingWith(fake, 'GRANT').map((s) => s.text)).toEqual([
166
+ 'GRANT "seat-owners" TO "seat-observability" WITH SET FALSE',
167
+ ]);
168
+ expect(startingWith(fake, 'REVOKE').map((s) => s.text)).toEqual([
169
+ 'REVOKE "hf_agent" FROM "seat-observability" GRANTED BY "postgres"',
170
+ ]);
171
+ expect(attrs.memberOf).toEqual(['seat-owners']);
172
+ });
173
+
174
+ test('memberOf [] revokes everything; memberOf undefined touches nothing', async () => {
175
+ const revoking = makeFakeSql({ roleRows: [liveRole()] });
176
+ revoking.memberships.add('seat-observability\0hf_agent\0postgres');
177
+ const afterRevoke = await run(reconcileWithClient(revoking, { ...baseProps, memberOf: [] }));
178
+ expect(afterRevoke.memberOf).toEqual([]);
179
+
180
+ const untouched = makeFakeSql({ roleRows: [liveRole()] });
181
+ untouched.memberships.add('seat-observability\0hf_agent\0postgres');
182
+ const afterNoop = await run(reconcileWithClient(untouched, baseProps));
183
+ expect(afterNoop.memberOf).toEqual(['hf_agent']);
184
+ expect(
185
+ untouched.statements.some((s) => s.text.startsWith('GRANT') || s.text.startsWith('REVOKE')),
186
+ ).toBe(false);
187
+ });
188
+ });
189
+
190
+ describe('read', () => {
191
+ test('answers undefined when the role is absent', async () => {
192
+ const fake = makeFakeSql();
193
+ expect(await run(readRole(fake, 'seat-observability'))).toBeUndefined();
194
+ });
195
+
196
+ test('answers the live row with memberships merged in, sorted \u2014 no seal; branding and the seal merge are the caller\u2019s job', async () => {
197
+ const fake = makeFakeSql({ roleRows: [liveRole()] });
198
+ fake.memberships.add('seat-observability\0seat-b\0postgres');
199
+ fake.memberships.add('seat-observability\0hf_agent\0postgres');
200
+ expect(await run(readRole(fake, 'seat-observability'))).toEqual({
201
+ name: 'seat-observability',
202
+ oid: 20000,
203
+ login: false,
204
+ connectionLimit: 10,
205
+ inherit: true,
206
+ validUntil: null,
207
+ memberOf: ['hf_agent', 'seat-b'],
208
+ memberships: [
209
+ { parent: 'hf_agent', grantor: 'postgres', admin: false, set: false },
210
+ { parent: 'seat-b', grantor: 'postgres', admin: false, set: false },
211
+ ],
212
+ });
213
+ });
214
+ });
215
+
216
+ describe('delete statement', () => {
217
+ test('is the idempotent quoted DROP, and the fake applies it to the row and touching memberships', async () => {
218
+ const fake = makeFakeSql({ roleRows: [liveRole()] });
219
+ fake.memberships.add('seat-observability\0hf_agent\0postgres');
220
+ fake.memberships.add('other-role\0seat-observability\0postgres');
221
+ await run(fake.unsafe('DROP ROLE IF EXISTS "seat-observability"').pipe(Effect.asVoid));
222
+ expect(fake.roleRows.has('seat-observability')).toBe(false);
223
+ expect(fake.memberships.has('seat-observability\0hf_agent\0postgres')).toBe(false);
224
+ expect(fake.memberships.has('other-role\0seat-observability\0postgres')).toBe(false);
225
+ // Idempotent: the same statement against the now-absent role is not an error.
226
+ await run(fake.unsafe('DROP ROLE IF EXISTS "seat-observability"').pipe(Effect.asVoid));
227
+ });
228
+ });
@@ -0,0 +1,198 @@
1
+ /**
2
+ * Membership statements and the catalog read behind them, split out of `role-sql.ts`
3
+ * (file cap). One file so `diff`, reconcile and the fake agree on the exact texts.
4
+ *
5
+ * ★ ONE PARENT PER STATEMENT. `pg_auth_members` keys a membership on three columns —
6
+ * (parent, member, grantor) — so more than one row can exist for one parent name:
7
+ * another grantor's `ADMIN`-option grant rides beside the safe one. A multi-role
8
+ * `GRANT`/`REVOKE` list would make a partially-failed write harder to attribute.
9
+ * ★ SEAT GRANTS SAY `SET FALSE`. Upstream `GRANT` defaults `SET` to TRUE (`grant.sgml` at
10
+ * REL_18_6), and `SET ROLE` to the parent is how `SUPERUSER` / `CREATEROLE` / `CREATEDB`
11
+ * are exercised (`user-manag.sgml`). A safe seat membership is `ADMIN FALSE, SET FALSE`.
12
+ * ⛔ REVOKE BY GRANTOR. A plain `REVOKE parent FROM member` only touches rows the session's
13
+ * own grantor made; Postgres 18 warns and leaves another grantor's rows standing
14
+ * (`revoke.sgml`). Every revoke here names `GRANTED BY`, so the statement either removes
15
+ * the row it targets or fails loudly — never a silent no-op.
16
+ */
17
+ import type { SqlError } from 'effect/unstable/sql/SqlError';
18
+ import * as Effect from 'effect/Effect';
19
+ import type { PgExecutor } from './database-sql.ts';
20
+ import { quoteIdent, roleExists } from './database-sql.ts';
21
+ import { PostgresRoleMembershipUnrepaired, PostgresRoleParentMissing } from './role-errors.ts';
22
+ import type { PostgresRoleAttributes, PostgresRoleProps } from './role-attrs.ts';
23
+
24
+ /** One `pg_auth_members` row as this family reads it: the parent name, the grantor who made
25
+ * the grant, and the two options a name-only compare would hide. `admin` true is `WITH ADMIN`;
26
+ * `set` true (the `GRANT` default) lets the member `SET ROLE` to the parent. `grantor` is null
27
+ * when the granting role was later dropped — such a row can never be named in a `REVOKE …
28
+ * GRANTED BY`, so it can only be repaired by hand as a bootstrap superuser. */
29
+ export interface MembershipRow {
30
+ readonly parent: string;
31
+ readonly grantor: string | null;
32
+ readonly admin: boolean;
33
+ readonly set: boolean;
34
+ }
35
+
36
+ const MEMBERSHIP_SQL = `SELECT parent.rolname AS parent,
37
+ grantor.rolname AS grantor,
38
+ m.admin_option AS admin,
39
+ m.set_option AS set
40
+ FROM pg_auth_members m
41
+ JOIN pg_roles member ON member.oid = m.member
42
+ JOIN pg_roles parent ON parent.oid = m.roleid
43
+ LEFT JOIN pg_roles grantor ON grantor.oid = m.grantor
44
+ WHERE member.rolname = $1
45
+ ORDER BY parent.rolname, grantor.rolname NULLS FIRST`;
46
+
47
+ export const selectRoleMemberships = (
48
+ pg: PgExecutor,
49
+ name: string,
50
+ ): Effect.Effect<readonly MembershipRow[], SqlError> =>
51
+ Effect.map(pg.unsafe<MembershipRow>(MEMBERSHIP_SQL, [name]), (rows) => rows);
52
+
53
+ /** Seat grants say `SET FALSE`. Upstream `GRANT` defaults `SET` to TRUE, and `SET ROLE` to the
54
+ * parent is how `SUPERUSER` / `CREATEROLE` / `CREATEDB` are exercised (`user-manag.sgml`). */
55
+ export const buildGrantMembershipSql = (member: string, parent: string): string =>
56
+ `GRANT ${quoteIdent(parent)} TO ${quoteIdent(member)} WITH SET FALSE`;
57
+
58
+ /** Repair an existing membership in place: one atomic full-options `GRANT` per parent.
59
+ * Upstream keeps any `WITH` option the new `GRANT` omits (`grant.sgml`), so a repair must
60
+ * spell both bits — `ADMIN FALSE, SET FALSE` — rather than rely on `SET FALSE` alone. */
61
+ export const buildRepairMembershipSql = (member: string, parent: string): string =>
62
+ `GRANT ${quoteIdent(parent)} TO ${quoteIdent(member)} WITH ADMIN FALSE, SET FALSE`;
63
+
64
+ /** Remove one grantor's row for one parent. Naming the grantor is what makes the revoke
65
+ * bind: a plain `REVOKE` skips rows other grantors made (`revoke.sgml`). */
66
+ export const buildRevokeGrantorMembershipSql = (
67
+ member: string,
68
+ parent: string,
69
+ grantor: string,
70
+ ): string =>
71
+ `REVOKE ${quoteIdent(parent)} FROM ${quoteIdent(member)} GRANTED BY ${quoteIdent(grantor)}`;
72
+
73
+ /** Catalog flags this family never declares. A create lands on the server default (all false);
74
+ * an absent field on a seeded test row is that default. */
75
+ export const unsafeMemberships = (
76
+ live: Omit<PostgresRoleAttributes, 'passwordSeal'>,
77
+ ): ReadonlySet<string> =>
78
+ new Set((live.memberships ?? []).filter((row) => row.admin || row.set).map((row) => row.parent));
79
+
80
+ /** Membership drift: sorted set difference between declared and live. Duplicates in the
81
+ * declaration collapse to one; `undefined` means "not asserted" — nothing to grant or revoke.
82
+ * A parent already granted `WITH ADMIN`, or with `SET` still at the upstream default TRUE, is
83
+ * a grant again: the name matches, but the options do not, so reconcile repairs in place. */
84
+ export const membershipDrift = (
85
+ declared: readonly string[] | undefined,
86
+ live: readonly string[],
87
+ unsafe: ReadonlySet<string> = new Set(),
88
+ ): { readonly grants: readonly string[]; readonly revokes: readonly string[] } => {
89
+ if (declared === undefined) return { grants: [], revokes: [] };
90
+ const wanted = [...new Set(declared)].sort();
91
+ const current = [...new Set(live)].sort();
92
+ return {
93
+ grants: wanted.filter((parent) => !current.includes(parent) || unsafe.has(parent)),
94
+ revokes: [
95
+ ...current.filter((parent) => !wanted.includes(parent)),
96
+ ...wanted.filter((parent) => current.includes(parent) && unsafe.has(parent)),
97
+ ],
98
+ };
99
+ };
100
+
101
+ /** Refuse before any membership write when a declared parent is not in `pg_roles`. `GRANT` of a
102
+ * missing role is `42704` (`user.c@REL_18_6`), and on a create that error arrives only after
103
+ * `CREATE ROLE` has committed. */
104
+ export const assertParentsExist = (
105
+ pg: PgExecutor,
106
+ role: string,
107
+ parents: readonly string[] | undefined,
108
+ ): Effect.Effect<void, PostgresRoleParentMissing | SqlError> =>
109
+ Effect.gen(function* () {
110
+ if (parents === undefined) return;
111
+ for (const parent of [...new Set(parents)].sort()) {
112
+ if (!(yield* roleExists(pg, parent))) {
113
+ return yield* Effect.fail(new PostgresRoleParentMissing({ role, parent }));
114
+ }
115
+ }
116
+ });
117
+
118
+ /** Make live memberships equal the declaration, never dropping a membership the declaration
119
+ * keeps. A wanted parent that is already granted is repaired with one
120
+ * `GRANT … WITH ADMIN FALSE, SET FALSE` — `AddRoleMems` updates that grantor's row in place
121
+ * (`user.c@REL_18_6` `SearchSysCache3` on role, member, grantor) — and any other grantor's
122
+ * unsafe row is then revoked by name.
123
+ *
124
+ * 1. Rows the declaration does not want are revoked **by their own grantor** (dead-grantor
125
+ * rows are skipped and left for the backstop to report).
126
+ * 2. Unsafe-but-wanted rows get one full-options repair `GRANT` per parent — atomic, in place,
127
+ * so the membership never drops even when another grantor's row also exists.
128
+ * 3. A repair leaves other grantors' unsafe rows beside the safe one, so a mid re-read
129
+ * revokes every remaining unsafe row with a live grantor.
130
+ * 4. Absent-but-wanted parents get a fresh seat grant.
131
+ * 5. Backstop re-read: any row that is not wanted, or still unsafe, fails typed — including a
132
+ * dead-grantor row, which no statement this family may issue can repair. */
133
+ export const syncMemberships = (
134
+ pg: PgExecutor,
135
+ props: PostgresRoleProps,
136
+ rows: readonly MembershipRow[],
137
+ ): Effect.Effect<
138
+ readonly string[],
139
+ SqlError | PostgresRoleMembershipUnrepaired | PostgresRoleParentMissing
140
+ > =>
141
+ Effect.gen(function* () {
142
+ if (props.memberOf === undefined) {
143
+ return [...new Set(rows.map((row) => row.parent))].sort();
144
+ }
145
+ yield* assertParentsExist(pg, props.name, props.memberOf);
146
+ const wanted = [...new Set(props.memberOf)].sort();
147
+ // 1. Not-wanted rows: revoke each by its own grantor (dead-grantor rows skipped; the
148
+ // backstop catches them).
149
+ for (const row of rows) {
150
+ if (wanted.includes(row.parent) || row.grantor === null) continue;
151
+ yield* pg
152
+ .unsafe(buildRevokeGrantorMembershipSql(props.name, row.parent, row.grantor))
153
+ .pipe(Effect.asVoid);
154
+ }
155
+ // 2. Unsafe-but-wanted rows: one full-options repair `GRANT` per parent.
156
+ const repaired = [
157
+ ...new Set(
158
+ rows
159
+ .filter(
160
+ (row) => wanted.includes(row.parent) && (row.admin || row.set) && row.grantor !== null,
161
+ )
162
+ .map((row) => row.parent),
163
+ ),
164
+ ];
165
+ for (const parent of repaired) {
166
+ yield* pg.unsafe(buildRepairMembershipSql(props.name, parent)).pipe(Effect.asVoid);
167
+ }
168
+ // 3. If any repair ran, other grantors' unsafe rows may still stand: re-read and revoke
169
+ // them by grantor (the repair's own safe row remains).
170
+ if (repaired.length > 0) {
171
+ const midRead = yield* selectRoleMemberships(pg, props.name);
172
+ for (const row of midRead) {
173
+ if (wanted.includes(row.parent) && (row.admin || row.set) && row.grantor !== null) {
174
+ yield* pg
175
+ .unsafe(buildRevokeGrantorMembershipSql(props.name, row.parent, row.grantor))
176
+ .pipe(Effect.asVoid);
177
+ }
178
+ }
179
+ }
180
+ // 4. Absent-but-wanted parents: fresh seat grant.
181
+ for (const parent of wanted.filter((p) => !rows.some((row) => row.parent === p))) {
182
+ yield* pg.unsafe(buildGrantMembershipSql(props.name, parent)).pipe(Effect.asVoid);
183
+ }
184
+ // 5. Backstop: anything not wanted or still unsafe fails typed.
185
+ for (const row of yield* selectRoleMemberships(pg, props.name)) {
186
+ if (!wanted.includes(row.parent) || row.admin || row.set) {
187
+ return yield* Effect.fail(
188
+ new PostgresRoleMembershipUnrepaired({
189
+ role: props.name,
190
+ parent: row.parent,
191
+ grantor: row.grantor,
192
+ declared: wanted.includes(row.parent),
193
+ }),
194
+ );
195
+ }
196
+ }
197
+ return wanted;
198
+ });