@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,223 @@
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`. A mismatched grantor can still
15
+ * produce only a warning; the catalog re-read below catches surviving unsafe rows.
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 three options a name-only compare would hide. `admin` true is `WITH ADMIN`;
26
+ * `inherit` true inherits the parent's privileges even when the member is NOINHERIT;
27
+ * `set` true (the `GRANT` default) lets the member `SET ROLE` to the parent. `grantor` is null
28
+ * only in an inconsistent catalog or a synthetic test row, not after an ordinary DROP ROLE:
29
+ * PostgreSQL tracks grantor dependencies. Keep the null guard as a defensive refusal. */
30
+ export interface MembershipRow {
31
+ readonly parent: string;
32
+ readonly grantor: string | null;
33
+ readonly admin: boolean;
34
+ readonly set: boolean;
35
+ readonly inherit: boolean;
36
+ }
37
+
38
+ const MEMBERSHIP_SQL = `SELECT parent.rolname AS parent,
39
+ grantor.rolname AS grantor,
40
+ m.admin_option AS admin,
41
+ m.set_option AS set,
42
+ m.inherit_option AS inherit
43
+ FROM pg_auth_members m
44
+ JOIN pg_roles member ON member.oid = m.member
45
+ JOIN pg_roles parent ON parent.oid = m.roleid
46
+ LEFT JOIN pg_roles grantor ON grantor.oid = m.grantor
47
+ WHERE member.rolname = $1
48
+ ORDER BY parent.rolname, grantor.rolname NULLS FIRST`;
49
+
50
+ export const selectRoleMemberships = (
51
+ pg: PgExecutor,
52
+ name: string,
53
+ ): Effect.Effect<readonly MembershipRow[], SqlError> =>
54
+ Effect.map(pg.unsafe<MembershipRow>(MEMBERSHIP_SQL, [name]), (rows) => rows);
55
+
56
+ /** Seat grants say `SET FALSE`. Upstream `GRANT` defaults `SET` to TRUE, and `SET ROLE` to the
57
+ * parent is how `SUPERUSER` / `CREATEROLE` / `CREATEDB` are exercised (`user-manag.sgml`). */
58
+ export const buildGrantMembershipSql = (member: string, parent: string): string =>
59
+ `GRANT ${quoteIdent(parent)} TO ${quoteIdent(member)} WITH SET FALSE`;
60
+
61
+ /** ⛔ Do not repair with GRANT … ADMIN FALSE: measured in PR 336, that left grants made
62
+ * by the member standing after stripping its ADMIN. PG 16+ tracks dependent role grants:
63
+ * https://www.postgresql.org/docs/16/sql-grant.html#SQL-GRANT-DESCRIPTION-ROLES
64
+ * https://www.postgresql.org/docs/16/sql-revoke.html
65
+ * Measured 2026-10-02 on PG 17.11: REVOKE ADMIN OPTION … RESTRICT refuses dependents;
66
+ * without dependents, ADMIN/SET revocation keeps membership and its INHERIT bit intact.
67
+ * INHERIT revocation explicitly clears that bit for a declared NOINHERIT role.
68
+ * GRANTED BY targets the observed row, including a different grantor's row. */
69
+ export const buildRepairMembershipSql = (
70
+ member: string,
71
+ parent: string,
72
+ grantor: string,
73
+ option: 'ADMIN' | 'SET' | 'INHERIT',
74
+ ): string =>
75
+ `REVOKE ${option} OPTION FOR ${quoteIdent(parent)} FROM ${quoteIdent(member)} GRANTED BY ${quoteIdent(grantor)} RESTRICT`;
76
+
77
+ /** Remove one grantor's row for one parent. Naming the grantor is what makes the revoke
78
+ * bind: a plain `REVOKE` skips rows other grantors made (`revoke.sgml`). */
79
+ export const buildRevokeGrantorMembershipSql = (
80
+ member: string,
81
+ parent: string,
82
+ grantor: string,
83
+ ): string =>
84
+ `REVOKE ${quoteIdent(parent)} FROM ${quoteIdent(member)} GRANTED BY ${quoteIdent(grantor)}`;
85
+
86
+ /** ADMIN/SET are always unsafe; per-grant INHERIT is unsafe for declared NOINHERIT.
87
+ * PG 16–18 GRANT docs: the role attribute only defaults NEW membership grants. */
88
+ export const unsafeMemberships = (
89
+ live: Omit<PostgresRoleAttributes, 'passwordSeal'>,
90
+ /** The DECLARED inherit flag (never the live one): a declared NOINHERIT role must see live
91
+ * per-grant INHERIT rows as unsafe even when the live role flag says INHERIT. */
92
+ inherit: boolean,
93
+ ): ReadonlySet<string> =>
94
+ new Set(
95
+ (live.memberships ?? [])
96
+ .filter((row) => row.admin || row.set || (row.inherit && !inherit))
97
+ .map((row) => row.parent),
98
+ );
99
+
100
+ /** Membership drift: sorted set difference between declared and live. Duplicates in the
101
+ * declaration collapse to one; `undefined` means "not asserted" — nothing to grant or revoke.
102
+ * A parent already granted `WITH ADMIN`, or with `SET` still at the upstream default TRUE, is
103
+ * a grant again: the name matches, but the options do not, so reconcile repairs in place. */
104
+ export const membershipDrift = (
105
+ declared: readonly string[] | undefined,
106
+ live: readonly string[],
107
+ unsafe: ReadonlySet<string> = new Set(),
108
+ ): { readonly grants: readonly string[]; readonly revokes: readonly string[] } => {
109
+ if (declared === undefined) return { grants: [], revokes: [] };
110
+ const wanted = [...new Set(declared)].sort();
111
+ const current = [...new Set(live)].sort();
112
+ return {
113
+ grants: wanted.filter((parent) => !current.includes(parent) || unsafe.has(parent)),
114
+ revokes: [
115
+ ...current.filter((parent) => !wanted.includes(parent)),
116
+ ...wanted.filter((parent) => current.includes(parent) && unsafe.has(parent)),
117
+ ],
118
+ };
119
+ };
120
+
121
+ /** Refuse before any membership write when a declared parent is not in `pg_roles`. `GRANT` of a
122
+ * missing role is `42704` (`user.c@REL_18_6`), and on a create that error arrives only after
123
+ * `CREATE ROLE` has committed. */
124
+ export const assertParentsExist = (
125
+ pg: PgExecutor,
126
+ role: string,
127
+ parents: readonly string[] | undefined,
128
+ ): Effect.Effect<void, PostgresRoleParentMissing | SqlError> =>
129
+ Effect.gen(function* () {
130
+ if (parents === undefined) return;
131
+ for (const parent of [...new Set(parents)].sort()) {
132
+ if (!(yield* roleExists(pg, parent))) {
133
+ return yield* Effect.fail(new PostgresRoleParentMissing({ role, parent }));
134
+ }
135
+ }
136
+ });
137
+
138
+ /** Make live memberships equal the declaration, preserving wanted memberships in place.
139
+ * The previous full-options GRANT updated only its own grantor's row, then revoked other
140
+ * unsafe rows; it could silently strip ADMIN despite dependent grants (PR 336 round 2).
141
+ * Now each unsafe option is revoked by its observed grantor with RESTRICT. Unwanted rows
142
+ * commit first, so a blocked ADMIN repair cannot restore unwanted access (PR 349 review).
143
+ * Each retained row's options share a transaction, attributing a dependency/permission
144
+ * refusal to that parent and grantor. No CASCADE and no replacement grantor row.
145
+ * Unnameable grantors are defensive catalog-corruption cases, left to the typed backstop. */
146
+ export const syncMemberships = (
147
+ pg: PgExecutor,
148
+ props: PostgresRoleProps,
149
+ rows: readonly MembershipRow[],
150
+ ): Effect.Effect<
151
+ readonly string[],
152
+ SqlError | PostgresRoleMembershipUnrepaired | PostgresRoleParentMissing
153
+ > =>
154
+ Effect.gen(function* () {
155
+ if (props.memberOf === undefined) {
156
+ return [...new Set(rows.map((row) => row.parent))].sort();
157
+ }
158
+ yield* assertParentsExist(pg, props.name, props.memberOf);
159
+ const wanted = [...new Set(props.memberOf)].sort();
160
+ const revokes: string[] = [];
161
+ for (const row of rows) {
162
+ if (!wanted.includes(row.parent) && row.grantor !== null) {
163
+ revokes.push(buildRevokeGrantorMembershipSql(props.name, row.parent, row.grantor));
164
+ }
165
+ }
166
+ if (revokes.length > 0) yield* pg.transaction(revokes);
167
+ for (const row of rows) {
168
+ if (row.grantor === null || !wanted.includes(row.parent)) continue;
169
+ const statements: string[] = [];
170
+ for (const option of ['ADMIN', 'SET', 'INHERIT'] as const) {
171
+ const unsafe =
172
+ option === 'ADMIN'
173
+ ? row.admin
174
+ : option === 'SET'
175
+ ? row.set
176
+ : row.inherit && !props.inherit;
177
+ if (unsafe) {
178
+ statements.push(buildRepairMembershipSql(props.name, row.parent, row.grantor, option));
179
+ }
180
+ }
181
+ if (statements.length > 0)
182
+ yield* pg.transaction(statements).pipe(
183
+ Effect.catchTag('SqlError', (error) => {
184
+ // Both installed socket and psql transports preserve raw SQLSTATE here.
185
+ // Class 2B is UnknownError, class 42 is SqlSyntaxError; never match messages.
186
+ const cause = error.reason.cause;
187
+ const code =
188
+ typeof cause === 'object' && cause !== null
189
+ ? (cause as { code?: unknown }).code
190
+ : undefined;
191
+ return Effect.fail(
192
+ code === '2BP01' || code === '42501'
193
+ ? new PostgresRoleMembershipUnrepaired({
194
+ role: props.name,
195
+ parent: row.parent,
196
+ grantor: row.grantor,
197
+ declared: true,
198
+ })
199
+ : error,
200
+ );
201
+ }),
202
+ );
203
+ }
204
+ // Absent-but-wanted parents get a fresh seat grant. Existing rows keep their grantor.
205
+ const grants = wanted
206
+ .filter((p) => !rows.some((row) => row.parent === p))
207
+ .map((parent) => buildGrantMembershipSql(props.name, parent));
208
+ if (grants.length > 0) yield* pg.transaction(grants);
209
+ // Backstop: anything not wanted or still unsafe fails typed.
210
+ for (const row of yield* selectRoleMemberships(pg, props.name)) {
211
+ if (!wanted.includes(row.parent) || row.admin || row.set || (row.inherit && !props.inherit)) {
212
+ return yield* Effect.fail(
213
+ new PostgresRoleMembershipUnrepaired({
214
+ role: props.name,
215
+ parent: row.parent,
216
+ grantor: row.grantor,
217
+ declared: wanted.includes(row.parent),
218
+ }),
219
+ );
220
+ }
221
+ }
222
+ return wanted;
223
+ });
@@ -0,0 +1,53 @@
1
+ import { expect, test } from 'bun:test';
2
+ import * as Effect from 'effect/Effect';
3
+ import { makeFakeSql } from './fake-sql.ts';
4
+ import { diffPostgresRole } from './role-diff.ts';
5
+ import { reconcileWithClient } from './role.ts';
6
+ import { seal } from '../secrets/write-only.ts';
7
+ import type { PostgresRoleAttributes } from './role-attrs.ts';
8
+
9
+ const props = {
10
+ name: 'ascii-seat',
11
+ login: true,
12
+ inherit: true,
13
+ connectionLimit: -1,
14
+ password: { fromEnv: 'TEST_PASSWORD' },
15
+ };
16
+ const live: PostgresRoleAttributes = {
17
+ ...props,
18
+ oid: 42,
19
+ validUntil: null,
20
+ memberOf: [],
21
+ passwordSeal: '',
22
+ };
23
+
24
+ // Synthetic values cover SASLprep mapping, normalization and invalid Unicode; never credentials.
25
+ test.each(['soft\u00adhyphen', 'caf\u00e9', '\u2168', '\ud800'])(
26
+ 'non-ASCII case %# refuses create, adopt, sealed reconcile and diff without exposing input',
27
+ async (password) => {
28
+ const env = { TEST_PASSWORD: password };
29
+ const passwordSeal = seal({ password }, 'test-salt');
30
+ for (const exists of [false, true]) {
31
+ const fake = makeFakeSql({ roleRows: exists ? [live] : [] });
32
+ for (const previous of ['', passwordSeal]) {
33
+ const error = await Effect.runPromise(
34
+ reconcileWithClient(fake, props, env, previous).pipe(
35
+ Effect.catchTag('PostgresRolePasswordNonAsciiRefused', Effect.succeed),
36
+ ),
37
+ );
38
+ expect(error).toMatchObject({
39
+ _tag: 'PostgresRolePasswordNonAsciiRefused',
40
+ role: props.name,
41
+ });
42
+ expect(JSON.stringify(error)).not.toContain(password);
43
+ expect(fake.statements).toEqual([]);
44
+ }
45
+ }
46
+ const error = await Effect.runPromise(
47
+ diffPostgresRole(props, { ...live, passwordSeal }, env).pipe(
48
+ Effect.catchTag('PostgresRolePasswordNonAsciiRefused', Effect.succeed),
49
+ ),
50
+ );
51
+ expect(error).toHaveProperty('_tag', 'PostgresRolePasswordNonAsciiRefused');
52
+ },
53
+ );
@@ -0,0 +1,123 @@
1
+ /** Capture the actual Effect SQL spans and psql stdin, not just the builder's output. */
2
+ import { expect, test } from 'bun:test';
3
+ import * as PgClient from '@effect/sql-pg/PgClient';
4
+ import * as Effect from 'effect/Effect';
5
+ import * as Stream from 'effect/Stream';
6
+ import * as Tracer from 'effect/Tracer';
7
+ import * as Statement from 'effect/unstable/sql/Statement';
8
+ import type { Connection } from 'effect/unstable/sql/SqlConnection';
9
+ import type { PgExecutor } from './database-sql.ts';
10
+ import { makeFakeSql } from './fake-sql.ts';
11
+ import { type PsqlRunner, makePsqlExecutor } from './psql-executor.ts';
12
+ import { reconcileWithClient } from './role.ts';
13
+
14
+ const props = {
15
+ name: 'transport-seat',
16
+ login: true,
17
+ inherit: true,
18
+ connectionLimit: -1,
19
+ password: { fromEnv: 'TEST_PASSWORD' },
20
+ };
21
+ // Synthetic test input, deliberately recognizable if it escapes into captured telemetry.
22
+ const password = 'kit336-plaintext-canary:must-never-reach-transport!';
23
+ const env = { TEST_PASSWORD: password };
24
+ const live = { ...props, oid: 42, validUntil: null, memberOf: [], passwordSeal: '' };
25
+
26
+ test('SQL tracer captures only a verifier on create, rotate and failed ALTER', async () => {
27
+ for (const exists of [false, true]) {
28
+ for (const fail of [false, true]) {
29
+ const fake = makeFakeSql({
30
+ roleRows: exists ? [live] : [],
31
+ ...(fail ? { failNext: 'ALTER ROLE' } : {}),
32
+ });
33
+ const spans: Tracer.Span[] = [];
34
+ const tracer = Tracer.make({
35
+ span: (options) => {
36
+ const span = Tracer.nativeTracer.span(options);
37
+ spans.push(span);
38
+ return span;
39
+ },
40
+ });
41
+ const execute: Connection['execute'] = (sql, params) => fake.unsafe(sql, params);
42
+ const connection: Connection = {
43
+ execute,
44
+ executeUnprepared: execute,
45
+ executeRaw: (sql, params) => fake.unsafe(sql, params),
46
+ executeValues: () => Effect.succeed([]),
47
+ executeValuesUnprepared: () => Effect.succeed([]),
48
+ executeStream: () => Stream.empty,
49
+ };
50
+ // Use the installed SQL Statement and PG compiler: db.query.text is set by Effect,
51
+ // while only the connection beneath it is fake. A hand-written span would prove nothing.
52
+ const sql = Statement.make(
53
+ Effect.succeed(connection),
54
+ PgClient.makeCompiler(),
55
+ [],
56
+ undefined,
57
+ );
58
+ const pg: PgExecutor = {
59
+ unsafe: (text, params) => sql.unsafe(text, params),
60
+ transaction: (texts) =>
61
+ Effect.gen(function* () {
62
+ for (const text of texts) yield* sql.unsafe(text);
63
+ }),
64
+ };
65
+ const result = await Effect.runPromise(
66
+ Effect.exit(reconcileWithClient(pg, props, env, exists ? 'stale-seal' : '')).pipe(
67
+ Effect.provideService(Tracer.Tracer, tracer),
68
+ ),
69
+ );
70
+ expect(result._tag).toBe(fail ? 'Failure' : 'Success');
71
+ const attributes = spans.map((span) => Object.fromEntries(span.attributes));
72
+ const passwordQueries = attributes
73
+ .map((a) => a['db.query.text'])
74
+ .filter((text): text is string => typeof text === 'string' && text.includes('PASSWORD'));
75
+ expect(passwordQueries).toHaveLength(1);
76
+ expect(passwordQueries[0]).toContain("PASSWORD E'SCRAM-SHA-256$");
77
+ expect(JSON.stringify({ attributes, result })).not.toContain(password);
78
+ }
79
+ }
80
+ });
81
+
82
+ test('psql runner receives only a verifier; echoed failure and state hold no plain value', async () => {
83
+ for (const exists of [false, true]) {
84
+ for (const fail of [false, true]) {
85
+ const calls: Parameters<PsqlRunner>[0][] = [];
86
+ let created = exists;
87
+ const run: PsqlRunner = (call) => {
88
+ calls.push(call);
89
+ if (call.stdin.includes('PASSWORD') && fail) {
90
+ return Promise.resolve({
91
+ code: 3,
92
+ stdout: '',
93
+ stderr: `ERROR: 42501: refused\nSTATEMENT: ${call.stdin}`,
94
+ });
95
+ }
96
+ if (call.stdin.startsWith('BEGIN')) created = true;
97
+ return Promise.resolve({
98
+ code: 0,
99
+ stderr: '',
100
+ stdout: call.stdin.includes('FROM pg_roles r')
101
+ ? JSON.stringify(created ? [live] : [])
102
+ : '[]',
103
+ });
104
+ };
105
+ const result = await Effect.runPromise(
106
+ Effect.exit(
107
+ reconcileWithClient(
108
+ makePsqlExecutor(run, { database: 'postgres', username: 'postgres' }),
109
+ props,
110
+ env,
111
+ exists ? 'stale-seal' : '',
112
+ ),
113
+ ),
114
+ );
115
+ expect(result._tag).toBe(fail ? 'Failure' : 'Success');
116
+ const writes = calls.filter((call) => call.stdin.includes('PASSWORD'));
117
+ expect(writes).toHaveLength(1);
118
+ expect(writes[0]?.stdin).toContain("PASSWORD E'SCRAM-SHA-256$");
119
+ expect(JSON.stringify({ calls, result })).not.toContain(password);
120
+ expect(JSON.stringify(result)).not.toContain('SCRAM-SHA-256$');
121
+ }
122
+ }
123
+ });
@@ -0,0 +1,158 @@
1
+ /**
2
+ * The write-only password lifecycle (S25) against `fake-sql.ts`'s recording fake: the
3
+ * declaration is the NAME of an environment variable, the value exists only in memory and on
4
+ * the wire of the one dedicated `ALTER ROLE … PASSWORD` statement, and what state remembers is
5
+ * a scrypt seal. Create, adopt, rotate and the never-guessed refusals.
6
+ */
7
+ import { describe, expect, test } from 'bun:test';
8
+ import * as Effect from 'effect/Effect';
9
+ import type { PgExecutor } from './database-sql.ts';
10
+ import { seal, sealMatches } from '../secrets/write-only.ts';
11
+ import { makeFakeSql } from './fake-sql.ts';
12
+ import { PostgresRoleCreateVanished, PostgresRolePasswordEnvUnsetError } from './role-errors.ts';
13
+ import type { PostgresRoleAttributes, PostgresRoleProps } from './role-attrs.ts';
14
+ import { reconcileWithClient } from './role.ts';
15
+
16
+ const run = <A, E>(eff: Effect.Effect<A, E>): Promise<A> => Effect.runPromise(eff);
17
+ const fails = <A, E>(eff: Effect.Effect<A, E>): Promise<E> => Effect.runPromise(Effect.flip(eff));
18
+
19
+ const baseProps: PostgresRoleProps = {
20
+ name: 'seat-observability',
21
+ login: false,
22
+ connectionLimit: 10,
23
+ inherit: true,
24
+ };
25
+
26
+ const liveRole = (over: Partial<PostgresRoleAttributes> = {}): PostgresRoleAttributes => ({
27
+ name: 'seat-observability',
28
+ oid: 20000,
29
+ login: false,
30
+ connectionLimit: 10,
31
+ inherit: true,
32
+ validUntil: null,
33
+ memberOf: [],
34
+ passwordSeal: '',
35
+ ...over,
36
+ });
37
+
38
+ const startingWith = (fake: ReturnType<typeof makeFakeSql>, prefix: string) =>
39
+ fake.statements.filter((s) => s.text.startsWith(prefix));
40
+
41
+ const withPassword = {
42
+ ...baseProps,
43
+ password: { fromEnv: 'PG_SEAT_ROLE_PASSWORD' },
44
+ };
45
+
46
+ describe('password: create', () => {
47
+ test('travels in a dedicated ALTER, never in the CREATE, and only its seal lands in state', async () => {
48
+ const fake = makeFakeSql();
49
+ const attrs = await run(
50
+ reconcileWithClient(fake, withPassword, { PG_SEAT_ROLE_PASSWORD: 's3cret-value' }),
51
+ );
52
+ expect(startingWith(fake, 'CREATE ROLE')[0]?.text.includes('PASSWORD')).toBe(false);
53
+ // The statement carries the SCRAM-SHA-256 verifier, never the plain value: this is the text
54
+ // that reaches the span attribute `db.query.text` on the socket transport and the inlined
55
+ // stdin on the runner transport, so both are covered by one assertion over the shared text.
56
+ const sent = startingWith(fake, 'ALTER ROLE').map((s) => s.text);
57
+ expect(sent).toHaveLength(1);
58
+ expect(sent[0]).toMatch(/^ALTER ROLE "seat-observability" WITH PASSWORD E'SCRAM-SHA-256\$/);
59
+ expect(JSON.stringify(fake.statements)).not.toContain('s3cret-value');
60
+ expect(JSON.stringify(attrs)).not.toContain('s3cret-value');
61
+ // The seal stays a seal of the plain value — only the wire form changed.
62
+ expect(sealMatches(attrs.passwordSeal, { password: 's3cret-value' })).toBe(true);
63
+ });
64
+
65
+ test('a missing environment value refuses before any statement — never a guess', async () => {
66
+ const fake = makeFakeSql();
67
+ const error = await fails(reconcileWithClient(fake, withPassword, {}));
68
+ expect(error).toBeInstanceOf(PostgresRolePasswordEnvUnsetError);
69
+ expect((error as PostgresRolePasswordEnvUnsetError).variable).toBe('PG_SEAT_ROLE_PASSWORD');
70
+ // The reconcile reads first (one SELECT); the refusal lands before any write statement.
71
+ expect(startingWith(fake, 'CREATE').length).toBe(0);
72
+ expect(startingWith(fake, 'ALTER').length).toBe(0);
73
+ });
74
+ });
75
+
76
+ describe('password: live role', () => {
77
+ test('a rotated environment value rewrites the password under a fresh seal', async () => {
78
+ const oldSeal = seal({ password: 'old' }, 'fixed-salt');
79
+ const fake = makeFakeSql({ roleRows: [liveRole()] });
80
+ const attrs = await run(
81
+ reconcileWithClient(fake, withPassword, { PG_SEAT_ROLE_PASSWORD: 'new' }, oldSeal),
82
+ );
83
+ const sent = startingWith(fake, 'ALTER ROLE').map((s) => s.text);
84
+ expect(sent).toHaveLength(1);
85
+ expect(sent[0]).toMatch(/^ALTER ROLE "seat-observability" WITH PASSWORD E'SCRAM-SHA-256\$/);
86
+ expect(JSON.stringify(fake.statements)).not.toContain('new');
87
+ expect(sealMatches(attrs.passwordSeal, { password: 'new' })).toBe(true);
88
+ expect(sealMatches(attrs.passwordSeal, { password: 'old' })).toBe(false);
89
+ });
90
+
91
+ test('an unchanged environment value sends no password statement and keeps the seal bit-identical', async () => {
92
+ const oldSeal = seal({ password: 'old' }, 'fixed-salt');
93
+ const fake = makeFakeSql({ roleRows: [liveRole()] });
94
+ const attrs = await run(
95
+ reconcileWithClient(fake, withPassword, { PG_SEAT_ROLE_PASSWORD: 'old' }, oldSeal),
96
+ );
97
+ expect(fake.statements.some((s) => s.text.startsWith('ALTER'))).toBe(false);
98
+ expect(attrs.passwordSeal).toBe(oldSeal);
99
+ });
100
+
101
+ test('an adopted role (empty seal) gets the password written on the first reconcile that holds it', async () => {
102
+ const fake = makeFakeSql({ roleRows: [liveRole()] });
103
+ const attrs = await run(
104
+ reconcileWithClient(fake, withPassword, { PG_SEAT_ROLE_PASSWORD: 'first' }),
105
+ );
106
+ expect(startingWith(fake, 'ALTER ROLE').length).toBe(1);
107
+ expect(sealMatches(attrs.passwordSeal, { password: 'first' })).toBe(true);
108
+ });
109
+
110
+ test('an environment that went empty on a live role never churns the stored password', async () => {
111
+ const oldSeal = seal({ password: 'old' }, 'fixed-salt');
112
+ const fake = makeFakeSql({ roleRows: [liveRole()] });
113
+ const attrs = await run(
114
+ reconcileWithClient(fake, withPassword, { PG_SEAT_ROLE_PASSWORD: '' }, oldSeal),
115
+ );
116
+ expect(fake.statements.some((s) => s.text.startsWith('ALTER'))).toBe(false);
117
+ expect(attrs.passwordSeal).toBe(oldSeal);
118
+ });
119
+
120
+ test('a declared password whose seal is still empty refuses an unset variable on the update path', async () => {
121
+ // CREATE ROLE committed, then ALTER ROLE … PASSWORD failed or the process died before state
122
+ // was saved. The next run sees the role and an empty seal. Reporting success would leave a
123
+ // LOGIN role this stack has never sealed.
124
+ const fake = makeFakeSql({ roleRows: [liveRole({ login: true })] });
125
+ const error = await fails(
126
+ reconcileWithClient(fake, { ...withPassword, login: true }, { PG_SEAT_ROLE_PASSWORD: '' }),
127
+ );
128
+ expect(error).toBeInstanceOf(PostgresRolePasswordEnvUnsetError);
129
+ expect(fake.statements.some((s) => s.text.startsWith('ALTER'))).toBe(false);
130
+ });
131
+ });
132
+
133
+ describe('password: no new write statements on the no-op path', () => {
134
+ test('a create with no password declared never mentions PASSWORD at all', async () => {
135
+ const fake = makeFakeSql();
136
+ await run(reconcileWithClient(fake, baseProps));
137
+ expect(fake.statements.some((s) => s.text.includes('PASSWORD'))).toBe(false);
138
+ });
139
+
140
+ test('a vanished create on a password-carrying reconcile fails typed, with the password statement issued first', async () => {
141
+ // The blank executor answers every read with `[]` (absent), so the reconcile takes the
142
+ // create path: CREATE, then the dedicated password ALTER, then a re-read that finds nothing.
143
+ // The typed `PostgresRoleCreateVanished` surfaces that — the value stayed on the wire, and
144
+ // the failure is never a secret leak.
145
+ const unsafe = <A extends object>(_sql?: string) => Effect.succeed([] as ReadonlyArray<A>);
146
+ const blankPg: PgExecutor = {
147
+ unsafe,
148
+ transaction: (statements) =>
149
+ Effect.gen(function* () {
150
+ for (const sql of statements) yield* unsafe(sql).pipe(Effect.asVoid);
151
+ }),
152
+ };
153
+ const error = await fails(
154
+ reconcileWithClient(blankPg, withPassword, { PG_SEAT_ROLE_PASSWORD: 'v' }),
155
+ );
156
+ expect(error).toBeInstanceOf(PostgresRoleCreateVanished);
157
+ });
158
+ });
@@ -0,0 +1,107 @@
1
+ /**
2
+ * Adopt and alter of a role that already carries privileges this family never declares.
3
+ *
4
+ * The review of PR 336: `--adopt` of a SUPERUSER (or CREATEROLE / CREATEDB / REPLICATION /
5
+ * BYPASSRLS) role planned green and left the flag set, and a membership already granted
6
+ * `WITH ADMIN` matched by name so it was never revoked. `GRANT` also omitted `SET`, which
7
+ * upstream defaults to TRUE — a member can `SET ROLE` to the parent.
8
+ */
9
+ import { describe, expect, test } from 'bun:test';
10
+ import * as Effect from 'effect/Effect';
11
+ import { makeFakeSql } from './fake-sql.ts';
12
+ import { PostgresRolePrivilegedRefused } from './role-errors.ts';
13
+ import type { PostgresRoleAttributes, PostgresRoleProps } from './role-attrs.ts';
14
+ import { diffPostgresRole } from './role-diff.ts';
15
+ import { 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-widget',
22
+ login: false,
23
+ connectionLimit: -1,
24
+ inherit: true,
25
+ memberOf: ['hf_agent'],
26
+ };
27
+
28
+ const liveRole = (over: Partial<PostgresRoleAttributes> = {}): PostgresRoleAttributes => ({
29
+ name: 'seat-widget',
30
+ oid: 20000,
31
+ login: false,
32
+ connectionLimit: -1,
33
+ inherit: true,
34
+ validUntil: null,
35
+ memberOf: ['hf_agent'],
36
+ passwordSeal: '',
37
+ superuser: false,
38
+ createrole: false,
39
+ createdb: false,
40
+ replication: false,
41
+ bypassrls: false,
42
+ ...over,
43
+ });
44
+
45
+ describe('adopted privilege flags', () => {
46
+ test('a live SUPERUSER is refused before any write', async () => {
47
+ const fake = makeFakeSql({ roleRows: [liveRole({ superuser: true })] });
48
+ const error = await fails(reconcileWithClient(fake, baseProps));
49
+ expect(error).toBeInstanceOf(PostgresRolePrivilegedRefused);
50
+ expect(
51
+ fake.statements.some((s) => s.text.startsWith('ALTER') || s.text.startsWith('GRANT')),
52
+ ).toBe(false);
53
+ });
54
+
55
+ test('a membership granted WITH ADMIN by this session is repaired in place', async () => {
56
+ const fake = makeFakeSql({ roleRows: [liveRole()], roles: ['hf_agent'] });
57
+ fake.memberships.add('seat-widget\0hf_agent\0postgres');
58
+ fake.membershipOptions.set('seat-widget\0hf_agent\0postgres', { admin: true, set: true });
59
+ await run(reconcileWithClient(fake, baseProps));
60
+ const texts = fake.statements.map((s) => s.text);
61
+ // Option revocation keeps the membership, while RESTRICT protects dependent grants.
62
+ expect(texts).toContain(
63
+ 'REVOKE ADMIN OPTION FOR "hf_agent" FROM "seat-widget" GRANTED BY "postgres" RESTRICT',
64
+ );
65
+ expect(texts).toContain(
66
+ 'REVOKE SET OPTION FOR "hf_agent" FROM "seat-widget" GRANTED BY "postgres" RESTRICT',
67
+ );
68
+ expect(texts.some((text) => text.startsWith('GRANT'))).toBe(false);
69
+ expect(fake.memberships.has('seat-widget\0hf_agent\0postgres')).toBe(true);
70
+ expect(fake.membershipOptions.has('seat-widget\0hf_agent\0postgres')).toBe(false);
71
+ });
72
+
73
+ test("another grantor's options are revoked while its membership remains", async () => {
74
+ const fake = makeFakeSql({ roleRows: [liveRole()], roles: ['hf_agent'] });
75
+ fake.memberships.add('seat-widget\0hf_agent\0other');
76
+ fake.membershipOptions.set('seat-widget\0hf_agent\0other', { admin: true, set: true });
77
+ await run(reconcileWithClient(fake, baseProps));
78
+ const texts = fake.statements.map((s) => s.text);
79
+ expect(texts).toContain(
80
+ 'REVOKE ADMIN OPTION FOR "hf_agent" FROM "seat-widget" GRANTED BY "other" RESTRICT',
81
+ );
82
+ expect(texts).toContain(
83
+ 'REVOKE SET OPTION FOR "hf_agent" FROM "seat-widget" GRANTED BY "other" RESTRICT',
84
+ );
85
+ expect(fake.memberships.has('seat-widget\0hf_agent\0other')).toBe(true);
86
+ expect(fake.memberships.has('seat-widget\0hf_agent\0postgres')).toBe(false);
87
+ expect(fake.membershipOptions.has('seat-widget\0hf_agent\0other')).toBe(false);
88
+ });
89
+ });
90
+
91
+ test.each(['superuser', 'createrole', 'createdb', 'replication', 'bypassrls'] as const)(
92
+ 'live %s plans update and reconcile refuses before writing',
93
+ async (flag) => {
94
+ const live = liveRole({ [flag]: true });
95
+ expect(await run(diffPostgresRole(baseProps, liveRole(), {}, { found: live }))).toEqual({
96
+ action: 'update',
97
+ });
98
+ const fake = makeFakeSql({ roleRows: [live] });
99
+ const refused = await run(
100
+ reconcileWithClient(fake, baseProps, {}).pipe(
101
+ Effect.catchTag('PostgresRolePrivilegedRefused', (error) => Effect.succeed(error.flags)),
102
+ ),
103
+ );
104
+ expect(refused).toContain(flag.toUpperCase());
105
+ expect(fake.statements.every((s) => s.text.trimStart().startsWith('SELECT'))).toBe(true);
106
+ },
107
+ );