@homeflare/alchemy 0.43.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 (604) 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 +40 -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 +1975 -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 +109 -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 +126 -0
  233. package/dist/postgres/role-errors.d.ts.map +1 -0
  234. package/dist/postgres/role-membership-sql.d.ts +75 -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 +6 -0
  239. package/dist/postgres/role-scram.d.ts.map +1 -0
  240. package/dist/postgres/role-secrets.d.ts +30 -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 +114 -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/litellm/access-group-form.ts +105 -0
  344. package/src/litellm/access-group-operations.ts +92 -0
  345. package/src/litellm/access-group-types.ts +49 -0
  346. package/src/litellm/access-group.test.ts +197 -0
  347. package/src/litellm/access-group.ts +151 -0
  348. package/src/litellm/credential-errors.ts +183 -0
  349. package/src/litellm/credential-form.test.ts +151 -0
  350. package/src/litellm/credential-form.ts +211 -0
  351. package/src/litellm/credential-guards.test.ts +241 -0
  352. package/src/litellm/credential-operations.ts +208 -0
  353. package/src/litellm/credential-recovery.test.ts +93 -0
  354. package/src/litellm/credential-rewrite.test.ts +164 -0
  355. package/src/litellm/credential-rewrite.ts +50 -0
  356. package/src/litellm/credential-types.ts +117 -0
  357. package/src/litellm/credential-values.ts +94 -0
  358. package/src/litellm/credential.test.ts +218 -0
  359. package/src/litellm/credential.ts +227 -0
  360. package/src/litellm/fake-access-group-litellm.ts +127 -0
  361. package/src/litellm/fake-credential-litellm.ts +216 -0
  362. package/src/litellm/fake-keys.ts +190 -0
  363. package/src/litellm/fake-litellm.ts +19 -9
  364. package/src/litellm/fake-mcp-litellm.ts +214 -0
  365. package/src/litellm/fake-model-litellm-post-update.ts +103 -0
  366. package/src/litellm/fake-model-litellm.ts +233 -0
  367. package/src/litellm/fake-policy-attachment-litellm.ts +97 -0
  368. package/src/litellm/fake-policy-litellm.ts +186 -0
  369. package/src/litellm/fake-registry-base.ts +69 -0
  370. package/src/litellm/fake-stack.ts +17 -1
  371. package/src/litellm/fake-team-litellm.ts +229 -0
  372. package/src/litellm/fake-tool-litellm.ts +88 -0
  373. package/src/litellm/fake-toolset-litellm.ts +110 -0
  374. package/src/litellm/index.ts +140 -0
  375. package/src/litellm/key-delete.test.ts +212 -0
  376. package/src/litellm/key-errors.ts +241 -0
  377. package/src/litellm/key-form.ts +200 -0
  378. package/src/litellm/key-harness.ts +121 -0
  379. package/src/litellm/key-metadata-errors.ts +49 -0
  380. package/src/litellm/key-metadata.test.ts +156 -0
  381. package/src/litellm/key-metadata.ts +74 -0
  382. package/src/litellm/key-operations.ts +184 -0
  383. package/src/litellm/key-rotation.test.ts +110 -0
  384. package/src/litellm/key-scope.test.ts +135 -0
  385. package/src/litellm/key-secret.test.ts +248 -0
  386. package/src/litellm/key-secret.ts +137 -0
  387. package/src/litellm/key-transport.test.ts +166 -0
  388. package/src/litellm/key.test.ts +231 -0
  389. package/src/litellm/key.ts +213 -0
  390. package/src/litellm/mcp-server-adopt.test.ts +86 -0
  391. package/src/litellm/mcp-server-alias.test.ts +184 -0
  392. package/src/litellm/mcp-server-credential.ts +85 -0
  393. package/src/litellm/mcp-server-errors.ts +141 -0
  394. package/src/litellm/mcp-server-exports.test.ts +15 -0
  395. package/src/litellm/mcp-server-form.test.ts +241 -0
  396. package/src/litellm/mcp-server-form.ts +233 -0
  397. package/src/litellm/mcp-server-handlers.test.ts +150 -0
  398. package/src/litellm/mcp-server-locate.ts +66 -0
  399. package/src/litellm/mcp-server-operations.ts +111 -0
  400. package/src/litellm/mcp-server-registry.test.ts +146 -0
  401. package/src/litellm/mcp-server-retype.test.ts +114 -0
  402. package/src/litellm/mcp-server-shadow.test.ts +91 -0
  403. package/src/litellm/mcp-server-shadow.ts +45 -0
  404. package/src/litellm/mcp-server-tools.test.ts +85 -0
  405. package/src/litellm/mcp-server-types.ts +143 -0
  406. package/src/litellm/mcp-server-url.ts +66 -0
  407. package/src/litellm/mcp-server.test.ts +213 -0
  408. package/src/litellm/mcp-server.ts +225 -0
  409. package/src/litellm/model-claim.ts +69 -0
  410. package/src/litellm/model-credential.ts +45 -0
  411. package/src/litellm/model-errors.ts +168 -0
  412. package/src/litellm/model-foreign-row.test.ts +173 -0
  413. package/src/litellm/model-form.test.ts +228 -0
  414. package/src/litellm/model-form.ts +223 -0
  415. package/src/litellm/model-locate.test.ts +128 -0
  416. package/src/litellm/model-locate.ts +67 -0
  417. package/src/litellm/model-operations.test.ts +93 -0
  418. package/src/litellm/model-operations.ts +146 -0
  419. package/src/litellm/model-params-preserve.test.ts +119 -0
  420. package/src/litellm/model-recovery.test.ts +57 -0
  421. package/src/litellm/model-types.ts +112 -0
  422. package/src/litellm/model.test.ts +222 -0
  423. package/src/litellm/model.ts +237 -0
  424. package/src/litellm/policy-attachment-form.ts +103 -0
  425. package/src/litellm/policy-attachment-operations.ts +74 -0
  426. package/src/litellm/policy-attachment-types.ts +48 -0
  427. package/src/litellm/policy-attachment.test.ts +198 -0
  428. package/src/litellm/policy-attachment.ts +178 -0
  429. package/src/litellm/policy-form.ts +122 -0
  430. package/src/litellm/policy-operations.ts +112 -0
  431. package/src/litellm/policy-types.ts +58 -0
  432. package/src/litellm/policy.test.ts +214 -0
  433. package/src/litellm/policy.ts +146 -0
  434. package/src/litellm/providers.ts +24 -3
  435. package/src/litellm/registry-errors.ts +88 -0
  436. package/src/litellm/registry-exports.test.ts +66 -0
  437. package/src/litellm/registry-support.ts +109 -0
  438. package/src/litellm/team-form.ts +140 -0
  439. package/src/litellm/team-members.test.ts +150 -0
  440. package/src/litellm/team-members.ts +58 -0
  441. package/src/litellm/team-operations.ts +90 -0
  442. package/src/litellm/team-permission.ts +159 -0
  443. package/src/litellm/team-read.test.ts +89 -0
  444. package/src/litellm/team-types.ts +106 -0
  445. package/src/litellm/team.test.ts +206 -0
  446. package/src/litellm/team.ts +159 -0
  447. package/src/litellm/tool-policy-form.ts +94 -0
  448. package/src/litellm/tool-policy-operations.ts +58 -0
  449. package/src/litellm/tool-policy-types.ts +48 -0
  450. package/src/litellm/tool-policy.test.ts +172 -0
  451. package/src/litellm/tool-policy.ts +125 -0
  452. package/src/litellm/toolset-form.ts +104 -0
  453. package/src/litellm/toolset-operations.ts +90 -0
  454. package/src/litellm/toolset-types.ts +47 -0
  455. package/src/litellm/toolset.test.ts +198 -0
  456. package/src/litellm/toolset.ts +151 -0
  457. package/src/ownership/ownership.test.ts +33 -1
  458. package/src/ownership/rows.ts +53 -0
  459. package/src/postgres/connection.ts +42 -7
  460. package/src/postgres/database-bigint-serialization.test.ts +1 -0
  461. package/src/postgres/database-refusals.test.ts +2 -2
  462. package/src/postgres/database-sql.ts +36 -9
  463. package/src/postgres/fake-grants-parse.ts +189 -0
  464. package/src/postgres/fake-grants-read.ts +250 -0
  465. package/src/postgres/fake-grants-sql.ts +207 -0
  466. package/src/postgres/fake-grants-write.ts +90 -0
  467. package/src/postgres/fake-role-sql.ts +220 -0
  468. package/src/postgres/fake-schema-semantics.test.ts +36 -0
  469. package/src/postgres/fake-schema-sql.ts +146 -0
  470. package/src/postgres/fake-sql-parse.ts +86 -0
  471. package/src/postgres/fake-sql-quote.ts +36 -0
  472. package/src/postgres/fake-sql.ts +161 -46
  473. package/src/postgres/fixtures/pg-REL_18_6-acl.h.txt +147 -0
  474. package/src/postgres/fixtures/pg-REL_18_6-aclexplode-func.txt +31 -0
  475. package/src/postgres/fixtures/pg-REL_18_6-alter-default-privileges-synopsis.txt +82 -0
  476. package/src/postgres/fixtures/pg-REL_18_6-alter_role.sgml.txt +44 -0
  477. package/src/postgres/fixtures/pg-REL_18_6-create_role.sgml.txt +26 -0
  478. package/src/postgres/fixtures/pg-REL_18_6-grant-synopsis.txt +95 -0
  479. package/src/postgres/fixtures/pg-REL_18_6-pg_attribute.h.txt +24 -0
  480. package/src/postgres/fixtures/pg-REL_18_6-pg_auth_members.h.txt +62 -0
  481. package/src/postgres/fixtures/pg-REL_18_6-pg_authid.h.txt +70 -0
  482. package/src/postgres/fixtures/pg-REL_18_6-pg_class.h.txt +33 -0
  483. package/src/postgres/fixtures/pg-REL_18_6-pg_default_acl.h.txt +81 -0
  484. package/src/postgres/fixtures/pg-REL_18_6-pg_namespace.h.txt +28 -0
  485. package/src/postgres/fixtures/pg-REL_18_6-revoke-synopsis.txt +124 -0
  486. package/src/postgres/grants-acl.test.ts +61 -0
  487. package/src/postgres/grants-attrs.ts +185 -0
  488. package/src/postgres/grants-database.test.ts +158 -0
  489. package/src/postgres/grants-declare.ts +172 -0
  490. package/src/postgres/grants-delete-proof.test.ts +154 -0
  491. package/src/postgres/grants-diff.ts +101 -0
  492. package/src/postgres/grants-errors.ts +215 -0
  493. package/src/postgres/grants-existence-guards.test.ts +113 -0
  494. package/src/postgres/grants-existence.ts +101 -0
  495. package/src/postgres/grants-lifecycle-guards.test.ts +208 -0
  496. package/src/postgres/grants-lifecycle-repair.test.ts +208 -0
  497. package/src/postgres/grants-lifecycle.test.ts +181 -0
  498. package/src/postgres/grants-ops.ts +229 -0
  499. package/src/postgres/grants-owner-revoke.test.ts +121 -0
  500. package/src/postgres/grants-ownership.ts +49 -0
  501. package/src/postgres/grants-plan-collateral.test.ts +202 -0
  502. package/src/postgres/grants-plan-diff.test.ts +204 -0
  503. package/src/postgres/grants-plan-restore.ts +43 -0
  504. package/src/postgres/grants-plan-revocations.test.ts +69 -0
  505. package/src/postgres/grants-plan.test.ts +148 -0
  506. package/src/postgres/grants-plan.ts +245 -0
  507. package/src/postgres/grants-provenance.test.ts +134 -0
  508. package/src/postgres/grants-psql-lifecycle.test.ts +105 -0
  509. package/src/postgres/grants-read-absent.test.ts +91 -0
  510. package/src/postgres/grants-read-defaults.ts +51 -0
  511. package/src/postgres/grants-read.ts +250 -0
  512. package/src/postgres/grants-refusals.test.ts +136 -0
  513. package/src/postgres/grants-refuse.ts +119 -0
  514. package/src/postgres/grants-relkind-wire.test.ts +111 -0
  515. package/src/postgres/grants-repair-plan.test.ts +201 -0
  516. package/src/postgres/grants-sql.test.ts +128 -0
  517. package/src/postgres/grants-sql.ts +144 -0
  518. package/src/postgres/grants-words.ts +34 -0
  519. package/src/postgres/grants.ts +110 -0
  520. package/src/postgres/index.ts +128 -5
  521. package/src/postgres/installed-classifier.ts +36 -0
  522. package/src/postgres/providers.ts +33 -9
  523. package/src/postgres/psql-executor-escaping.test.ts +67 -0
  524. package/src/postgres/psql-executor.test.ts +63 -2
  525. package/src/postgres/psql-executor.ts +74 -34
  526. package/src/postgres/role-adopt.test.ts +98 -0
  527. package/src/postgres/role-attrs.ts +134 -0
  528. package/src/postgres/role-diff.ts +89 -0
  529. package/src/postgres/role-errors.ts +198 -0
  530. package/src/postgres/role-guard.test.ts +60 -0
  531. package/src/postgres/role-identity.test.ts +185 -0
  532. package/src/postgres/role-lifecycle.test.ts +228 -0
  533. package/src/postgres/role-membership-sql.ts +198 -0
  534. package/src/postgres/role-password.test.ts +158 -0
  535. package/src/postgres/role-privilege.test.ts +80 -0
  536. package/src/postgres/role-provenance.test.ts +200 -0
  537. package/src/postgres/role-provider.ts +104 -0
  538. package/src/postgres/role-refusals.test.ts +250 -0
  539. package/src/postgres/role-scram.test.ts +38 -0
  540. package/src/postgres/role-scram.ts +38 -0
  541. package/src/postgres/role-secrets.ts +46 -0
  542. package/src/postgres/role-sql.ts +211 -0
  543. package/src/postgres/role-valid-until.test.ts +68 -0
  544. package/src/postgres/role.ts +218 -0
  545. package/src/postgres/schema-assert.ts +139 -0
  546. package/src/postgres/schema-attrs.ts +71 -0
  547. package/src/postgres/schema-delete-proofs.test.ts +192 -0
  548. package/src/postgres/schema-delete.ts +96 -0
  549. package/src/postgres/schema-diff.ts +44 -0
  550. package/src/postgres/schema-drop-2bp01.test.ts +146 -0
  551. package/src/postgres/schema-errors.ts +209 -0
  552. package/src/postgres/schema-handlers.test.ts +193 -0
  553. package/src/postgres/schema-identity.ts +39 -0
  554. package/src/postgres/schema-lifecycle.test.ts +246 -0
  555. package/src/postgres/schema-race.test.ts +70 -0
  556. package/src/postgres/schema-reconcile.ts +100 -0
  557. package/src/postgres/schema-refusals.test.ts +144 -0
  558. package/src/postgres/schema-socket.test.ts +161 -0
  559. package/src/postgres/schema-sql.ts +130 -0
  560. package/src/postgres/schema-takeover.test.ts +149 -0
  561. package/src/postgres/schema-test-kit.ts +85 -0
  562. package/src/postgres/schema.ts +130 -0
  563. package/src/release/binary-alias.test.ts +3 -3
  564. package/src/release/binary-claims.test.ts +3 -3
  565. package/src/release/binary-directory-umask.test.ts +65 -0
  566. package/src/release/fake-engine.ts +15 -0
  567. package/src/valkey/acl-attrs.ts +98 -0
  568. package/src/valkey/acl-compare.ts +57 -0
  569. package/src/valkey/acl-connection.ts +13 -0
  570. package/src/valkey/acl-delete.test.ts +127 -0
  571. package/src/valkey/acl-form.ts +239 -0
  572. package/src/valkey/acl-managed.ts +13 -0
  573. package/src/valkey/acl-ops.ts +122 -0
  574. package/src/valkey/acl-ownership.test.ts +118 -0
  575. package/src/valkey/acl-prefix.test.ts +38 -0
  576. package/src/valkey/acl-profiles.ts +81 -0
  577. package/src/valkey/acl-reconcile.ts +180 -0
  578. package/src/valkey/acl-reserved.test.ts +69 -0
  579. package/src/valkey/acl-revocation.test.ts +94 -0
  580. package/src/valkey/acl-seat-prefix.test.ts +111 -0
  581. package/src/valkey/acl-validation.test.ts +54 -0
  582. package/src/valkey/acl.test.ts +215 -0
  583. package/src/valkey/acl.ts +169 -0
  584. package/src/valkey/connection-service.ts +46 -0
  585. package/src/valkey/connection.ts +180 -0
  586. package/src/valkey/errors.ts +239 -0
  587. package/src/valkey/fake-acl.ts +128 -0
  588. package/src/valkey/fake-valkey.ts +224 -0
  589. package/src/valkey/handler-test-fixture.ts +43 -0
  590. package/src/valkey/handlers.test.ts +205 -0
  591. package/src/valkey/index.ts +54 -0
  592. package/src/valkey/instance-attrs.ts +48 -0
  593. package/src/valkey/instance-form.ts +104 -0
  594. package/src/valkey/instance.test.ts +108 -0
  595. package/src/valkey/instance.ts +160 -0
  596. package/src/valkey/provider-socket-fixture.ts +71 -0
  597. package/src/valkey/providers.test.ts +129 -0
  598. package/src/valkey/providers.ts +21 -0
  599. package/src/valkey/resp.ts +212 -0
  600. package/src/valkey/transport-bounds.test.ts +150 -0
  601. package/src/valkey/transport-error.ts +37 -0
  602. package/src/valkey/transport.test.ts +158 -0
  603. package/src/valkey/transport.ts +109 -0
  604. package/src/valkey/upstream-bounds.test.ts +14 -0
@@ -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
+ });
@@ -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,80 @@
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 { 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-widget',
21
+ login: false,
22
+ connectionLimit: -1,
23
+ inherit: true,
24
+ memberOf: ['hf_agent'],
25
+ };
26
+
27
+ const liveRole = (over: Partial<PostgresRoleAttributes> = {}): PostgresRoleAttributes => ({
28
+ name: 'seat-widget',
29
+ oid: 20000,
30
+ login: false,
31
+ connectionLimit: -1,
32
+ inherit: true,
33
+ validUntil: null,
34
+ memberOf: ['hf_agent'],
35
+ passwordSeal: '',
36
+ superuser: false,
37
+ createrole: false,
38
+ createdb: false,
39
+ replication: false,
40
+ bypassrls: false,
41
+ ...over,
42
+ });
43
+
44
+ describe('adopted privilege flags', () => {
45
+ test('a live SUPERUSER is refused before any write', async () => {
46
+ const fake = makeFakeSql({ roleRows: [liveRole({ superuser: true })] });
47
+ const error = await fails(reconcileWithClient(fake, baseProps));
48
+ expect(error).toBeInstanceOf(PostgresRolePrivilegedRefused);
49
+ expect(
50
+ fake.statements.some((s) => s.text.startsWith('ALTER') || s.text.startsWith('GRANT')),
51
+ ).toBe(false);
52
+ });
53
+
54
+ test('a membership granted WITH ADMIN by this session is repaired in place', async () => {
55
+ const fake = makeFakeSql({ roleRows: [liveRole()], roles: ['hf_agent'] });
56
+ fake.memberships.add('seat-widget\0hf_agent\0postgres');
57
+ fake.membershipOptions.set('seat-widget\0hf_agent\0postgres', { admin: true, set: true });
58
+ await run(reconcileWithClient(fake, baseProps));
59
+ const texts = fake.statements.map((s) => s.text);
60
+ // `AddRoleMems` updates the session grantor's row. One full-options GRANT clears ADMIN
61
+ // and SET without a REVOKE that would drop the membership the declaration still wants.
62
+ expect(texts).toContain('GRANT "hf_agent" TO "seat-widget" WITH ADMIN FALSE, SET FALSE');
63
+ expect(texts.some((text) => text.startsWith('REVOKE'))).toBe(false);
64
+ expect(fake.memberships.has('seat-widget\0hf_agent\0postgres')).toBe(true);
65
+ expect(fake.membershipOptions.has('seat-widget\0hf_agent\0postgres')).toBe(false);
66
+ });
67
+
68
+ test("another grantor's ADMIN row is revoked by that grantor and the seat grant remains", async () => {
69
+ const fake = makeFakeSql({ roleRows: [liveRole()], roles: ['hf_agent'] });
70
+ fake.memberships.add('seat-widget\0hf_agent\0other');
71
+ fake.membershipOptions.set('seat-widget\0hf_agent\0other', { admin: true, set: true });
72
+ await run(reconcileWithClient(fake, baseProps));
73
+ const texts = fake.statements.map((s) => s.text);
74
+ expect(texts).toContain('GRANT "hf_agent" TO "seat-widget" WITH ADMIN FALSE, SET FALSE');
75
+ expect(texts).toContain('REVOKE "hf_agent" FROM "seat-widget" GRANTED BY "other"');
76
+ expect(fake.memberships.has('seat-widget\0hf_agent\0other')).toBe(false);
77
+ expect(fake.memberships.has('seat-widget\0hf_agent\0postgres')).toBe(true);
78
+ expect(fake.membershipOptions.has('seat-widget\0hf_agent\0postgres')).toBe(false);
79
+ });
80
+ });