@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,200 @@
1
+ /**
2
+ * Provenance: every `PostgresRoleProps` field traces to a real `CREATE ROLE` synopsis option AND
3
+ * a real `pg_authid`/`pg_auth_members` column, every synopsis option this family does NOT expose
4
+ * is named with a reason, and the `ALTER ROLE` synopsis backs every field the family alters (plus
5
+ * the "memberships move via GRANT and REVOKE" note the membership statements lean on). All four
6
+ * fixtures are parsed, not eyeballed — deleting a fixture line, or adding a prop with no mapping,
7
+ * fails this file (S38, the house's provenance rule).
8
+ */
9
+ import { describe, expect, test } from 'bun:test';
10
+ import { readFileSync } from 'node:fs';
11
+ import { join } from 'node:path';
12
+
13
+ const FIXTURES = join(import.meta.dir, 'fixtures');
14
+ const createRole = readFileSync(join(FIXTURES, 'pg-REL_18_6-create_role.sgml.txt'), 'utf8');
15
+ const alterRole = readFileSync(join(FIXTURES, 'pg-REL_18_6-alter_role.sgml.txt'), 'utf8');
16
+ const authid = readFileSync(join(FIXTURES, 'pg-REL_18_6-pg_authid.h.txt'), 'utf8');
17
+ const authMembers = readFileSync(join(FIXTURES, 'pg-REL_18_6-pg_auth_members.h.txt'), 'utf8');
18
+
19
+ const CREATE_ROLE_BLOB = 'cee23b1ea6b4dcf2d9d356d07a582e64808c2912';
20
+ const ALTER_ROLE_BLOB = '7b0a04bc4637c7c08246e597a9acf4466b467fa2';
21
+ const AUTHID_BLOB = 'b2f3e9d01eec972a3392e7037c9679ac644ddc7b';
22
+ const AUTH_MEMBERS_BLOB = '387316e44f07982264482f01f2298f4f85ffbafb';
23
+
24
+ /** Each alternative of a "where <option> can be:" list, normalised to its leading keyword
25
+ * sequence, keeping the optional `[ ENCRYPTED ]` prefix and multi-word forms such as
26
+ * `CONNECTION LIMIT` / `VALID UNTIL` intact. */
27
+ const optionTokens = (region: string): readonly string[] => {
28
+ const found: string[] = [];
29
+ for (const line of region.split('\n')) {
30
+ for (const raw of line.split('|')) {
31
+ const m = /^(\[ ENCRYPTED \] )?([A-Z]+(?: [A-Z]+)*)/.exec(raw.trim());
32
+ if (m !== null) found.push(`${m[1] ?? ''}${m[2] as string}`.trim());
33
+ }
34
+ }
35
+ return [...new Set(found)];
36
+ };
37
+
38
+ /** A `CATALOG` struct's column names, from the tab-indicated `type name;` lines. */
39
+ const structColumns = (fixture: string): readonly string[] => {
40
+ const re = /^\t[A-Za-z_][A-Za-z0-9_]*\s+(\w+)(?:\[\d+\])?\b/;
41
+ return fixture
42
+ .split('\n')
43
+ .map((line) => re.exec(line)?.[1])
44
+ .filter((name): name is string => name !== undefined);
45
+ };
46
+
47
+ /** The create synopsis's option list: from the first `can be:</phrase>` to `</synopsis>`. */
48
+ const createOptionsRegion = (): string => {
49
+ const start = createRole.indexOf('can be:</phrase>');
50
+ return createRole.slice(start, createRole.indexOf('</synopsis>', start));
51
+ };
52
+
53
+ /** The first ALTER variant's option list: from the first `can be:</phrase>` to `RENAME TO`. */
54
+ const alterOptionsRegion = (): string => {
55
+ const start = alterRole.indexOf('can be:</phrase>');
56
+ return alterRole.slice(start, alterRole.indexOf('RENAME TO', start));
57
+ };
58
+
59
+ /** Every asserted prop except `name`, which is the mandatory `CREATE ROLE <name>` token, checked
60
+ * separately below. A boolean prop claims both its affirmative and its negative option. */
61
+ const PROP_MAPPING: Readonly<
62
+ Record<string, { readonly option: string; readonly also?: string; readonly column: string }>
63
+ > = {
64
+ login: { option: 'LOGIN', also: 'NOLOGIN', column: 'rolcanlogin' },
65
+ inherit: { option: 'INHERIT', also: 'NOINHERIT', column: 'rolinherit' },
66
+ connectionLimit: { option: 'CONNECTION LIMIT', column: 'rolconnlimit' },
67
+ validUntil: { option: 'VALID UNTIL', column: 'rolvaliduntil' },
68
+ // The value is never a prop (S25): the prop names the environment variable, and the value
69
+ // travels only in the dedicated `ALTER ROLE … WITH PASSWORD` statement.
70
+ password: { option: '[ ENCRYPTED ] PASSWORD', column: 'rolpassword' },
71
+ };
72
+
73
+ /** Every synopsis option this family does NOT declare as a prop, and why. */
74
+ const EXCLUDED: Readonly<Record<string, string>> = {
75
+ SUPERUSER:
76
+ 'left at the server default (NOSUPERUSER) on CREATE; an adopt or alter that finds rolsuper true is PostgresRolePrivilegedRefused — the flag is not inherited, SET ROLE exercises it',
77
+ NOSUPERUSER:
78
+ 'the server default for CREATE ROLE; never declared, so the default is what a create lands on',
79
+ CREATEDB:
80
+ "left at the server default (NOCREATEDB); database creation is Postgres.Database's job, not a role prop",
81
+ NOCREATEDB: 'the server default for CREATE ROLE; never declared',
82
+ CREATEROLE:
83
+ 'left at the server default (NOCREATEROLE) on CREATE; an adopt or alter that finds rolcreaterole true is refused — a CREATEROLE role can manage other roles',
84
+ NOCREATEROLE: 'the server default for CREATE ROLE; never declared',
85
+ REPLICATION:
86
+ 'left at the server default (NOREPLICATION); replication is a cluster-operator concern, never a seat role',
87
+ NOREPLICATION: 'the server default for CREATE ROLE; never declared',
88
+ BYPASSRLS:
89
+ 'left at the server default (NOBYPASSRLS) on CREATE; an adopt or alter that finds rolbypassrls true is refused',
90
+ NOBYPASSRLS: 'the server default for CREATE ROLE; never declared',
91
+ 'PASSWORD NULL':
92
+ 'clears the password; this family never clears one — a password is either declared by reference or left alone',
93
+ 'IN ROLE':
94
+ 'the create-time membership shorthand; this family writes memberships with one explicit GRANT per parent instead, so the re-read stays authoritative',
95
+ ROLE: 'grants the listed roles membership in the NEW role — the inverse of memberOf; membership is declared in one direction only',
96
+ ADMIN:
97
+ 'grants membership with ADMIN OPTION; a live admin_option is revoked by grantor with RESTRICT, preserving membership and refusing dependent grants, and the seat GRANT says SET FALSE',
98
+ SYSID:
99
+ 'ignored by the server since PostgreSQL 8.1 (the docs keep it only for compatibility); this family never assigns oids',
100
+ };
101
+
102
+ describe('postgres role provenance', () => {
103
+ test('fixture headers name the measured blob shas', () => {
104
+ expect(createRole).toContain(CREATE_ROLE_BLOB);
105
+ expect(createRole).toContain('doc/src/sgml/ref/create_role.sgml');
106
+ expect(alterRole).toContain(ALTER_ROLE_BLOB);
107
+ expect(alterRole).toContain('doc/src/sgml/ref/alter_role.sgml');
108
+ expect(authid).toContain(AUTHID_BLOB);
109
+ expect(authid).toContain('src/include/catalog/pg_authid.h');
110
+ expect(authMembers).toContain(AUTH_MEMBERS_BLOB);
111
+ expect(authMembers).toContain('src/include/catalog/pg_auth_members.h');
112
+ });
113
+
114
+ test('the name token is documented and backed by rolname', () => {
115
+ expect(createRole).toContain('CREATE ROLE <replaceable class="parameter">name</replaceable>');
116
+ expect(structColumns(authid)).toContain('rolname');
117
+ });
118
+
119
+ test('every declared prop maps to a real synopsis option (affirmative and negative)', () => {
120
+ const options = optionTokens(createOptionsRegion());
121
+ for (const [prop, { option, also }] of Object.entries(PROP_MAPPING)) {
122
+ expect(options, `prop "${prop}" claims synopsis option "${option}"`).toContain(option);
123
+ if (also !== undefined) {
124
+ expect(options, `prop "${prop}" claims negative option "${also}"`).toContain(also);
125
+ }
126
+ }
127
+ });
128
+
129
+ test('every declared prop maps to a real pg_authid column', () => {
130
+ const columns = structColumns(authid);
131
+ for (const [prop, { column }] of Object.entries(PROP_MAPPING)) {
132
+ expect(columns, `prop "${prop}" claims column "${column}"`).toContain(column);
133
+ }
134
+ });
135
+
136
+ test('memberOf is backed by the pg_auth_members catalog columns the membership read joins on', () => {
137
+ const columns = structColumns(authMembers);
138
+ expect(columns).toContain('roleid');
139
+ expect(columns).toContain('member');
140
+ });
141
+
142
+ test('every synopsis option is either a declared prop or a documented exclusion', () => {
143
+ const declared = new Set(
144
+ Object.values(PROP_MAPPING).flatMap((m) => [
145
+ m.option,
146
+ ...(m.also !== undefined ? [m.also] : []),
147
+ ]),
148
+ );
149
+ for (const option of optionTokens(createOptionsRegion())) {
150
+ const accounted = declared.has(option) || option in EXCLUDED;
151
+ expect(accounted, `synopsis option "${option}" is neither a prop nor in EXCLUDED`).toBe(true);
152
+ }
153
+ });
154
+
155
+ test('no excluded option is silently also a declared prop', () => {
156
+ const declared = new Set(
157
+ Object.values(PROP_MAPPING).flatMap((m) => [
158
+ m.option,
159
+ ...(m.also !== undefined ? [m.also] : []),
160
+ ]),
161
+ );
162
+ for (const option of Object.keys(EXCLUDED)) {
163
+ expect(declared.has(option), `"${option}" is both excluded and declared`).toBe(false);
164
+ }
165
+ });
166
+
167
+ test('every excluded option actually exists in the synopsis (no stale entries)', () => {
168
+ const options = new Set(optionTokens(createOptionsRegion()));
169
+ for (const option of Object.keys(EXCLUDED)) {
170
+ expect(
171
+ options.has(option),
172
+ `EXCLUDED names "${option}", which the synopsis no longer has`,
173
+ ).toBe(true);
174
+ }
175
+ });
176
+
177
+ test('ALTER ROLE backs every field this family alters', () => {
178
+ const alterOptions = optionTokens(alterOptionsRegion());
179
+ for (const option of Object.values(PROP_MAPPING).map((m) => m.option)) {
180
+ expect(alterOptions, `ALTER ROLE cannot set "${option}"`).toContain(option);
181
+ }
182
+ });
183
+
184
+ test('ALTER ROLE offers no membership or oid options, which is why memberships move via GRANT/REVOKE', () => {
185
+ const alterOptions = new Set(optionTokens(alterOptionsRegion()));
186
+ for (const option of ['IN ROLE', 'ROLE', 'ADMIN', 'SYSID']) {
187
+ expect(alterOptions.has(option), `ALTER ROLE unexpectedly offers "${option}"`).toBe(false);
188
+ }
189
+ });
190
+
191
+ test('the fixture carries the memberships sentence the one-parent GRANT/REVOKE design leans on', () => {
192
+ const stripped = alterRole.replace(/<[^>]+>/g, ' ').replace(/\s+/g, ' ');
193
+ expect(stripped).toMatch(/no options[^;]*memberships/);
194
+ expect(stripped).toMatch(/use GRANT.{0,20}REVOKE/);
195
+ });
196
+
197
+ test('RENAME TO exists in the synopsis — the rename refusal is a choice, not an unknowing gap', () => {
198
+ expect(alterRole).toContain('RENAME TO');
199
+ });
200
+ });
@@ -0,0 +1,105 @@
1
+ /**
2
+ * `Postgres.Role`'s five lifecycle handlers, wired to `withPg` for anything that touches a
3
+ * client, and the one Layer a consuming stack provides. Split from `role.ts` so the file stays
4
+ * under the house cap: `role.ts` holds reconcile, `role-diff.ts` the plan diff; this file holds
5
+ * only the engine wiring.
6
+ */
7
+ import * as Provider from 'alchemy/Provider';
8
+ import { isResolved } from 'alchemy/Diff';
9
+ import * as Effect from 'effect/Effect';
10
+ import { ownedRead } from '../ownership/probe.ts';
11
+ import { noteUnfinished } from '../ownership/resume.ts';
12
+ import { refuseTakeover } from '../ownership/adopt.ts';
13
+ import { PostgresRole } from './role.ts';
14
+ import { readRole, reconcileWithClient, refuseAtPlan } from './role.ts';
15
+ import { diffPostgresRole } from './role-diff.ts';
16
+ import { PostgresRoleIdentityRefused } from './role-errors.ts';
17
+ import { buildDropRoleSql, declarationMatches, storedAttributes } from './role-sql.ts';
18
+ import { withPg } from './connection.ts';
19
+
20
+ export const postgresRoleHandlers = PostgresRole.Provider.of({
21
+ // ⚠️ EMPTY, NOT A LIVE SWEEP: `pg_roles` lists every role on the cluster, most of which nothing
22
+ // declared — the same reasoning `Postgres.Database`'s `list` documents.
23
+ list: () => Effect.succeed([]),
24
+ nuke: { skip: true },
25
+
26
+ read: ({ fqn, instanceId, olds, output }) =>
27
+ Effect.gen(function* () {
28
+ const name = output?.name ?? olds.name;
29
+ const live = yield* withPg((pg) => readRole(pg, name));
30
+ if (live === undefined) return undefined;
31
+ // A recycled name is not the role state recorded. Failing (rather than `Unowned`) matters
32
+ // because drift strips `Unowned` and would then reconcile the foreign role.
33
+ if (output !== undefined && live.oid !== output.oid) {
34
+ return yield* Effect.fail(
35
+ new PostgresRoleIdentityRefused({
36
+ role: name,
37
+ storedOid: output.oid,
38
+ liveOid: live.oid,
39
+ }),
40
+ );
41
+ }
42
+ const passwordSeal = output?.passwordSeal ?? '';
43
+ // Read and reconcile must expose the same shape to Alchemy's drift comparison.
44
+ const found = { ...storedAttributes(live), passwordSeal };
45
+ return yield* ownedRead(
46
+ { fqn, instanceId, output },
47
+ found,
48
+ Effect.succeed(declarationMatches(olds, live)),
49
+ );
50
+ }),
51
+
52
+ diff: ({ news, output, instanceId }) =>
53
+ Effect.gen(function* () {
54
+ // A `creating` row has no attributes. Mark it unfinished so `--adopt` can resume it
55
+ // (ownership/resume.ts); the engine treats `undefined` as "defer".
56
+ if (output === undefined) return yield* noteUnfinished(instanceId);
57
+ if (!isResolved(news)) return undefined;
58
+ const found = yield* withPg((pg) => readRole(pg, output.name));
59
+ return yield* diffPostgresRole(news, output, process.env, { found });
60
+ }),
61
+
62
+ reconcile: ({ fqn, instanceId, news, output }) =>
63
+ Effect.gen(function* () {
64
+ yield* refuseAtPlan(news);
65
+ return yield* withPg((pg) =>
66
+ Effect.gen(function* () {
67
+ // Alchemy skips the plan probe while a prop is still an Output, and a role can
68
+ // appear between plan and apply. A live role with no state is a takeover: refuse
69
+ // before any ALTER, PASSWORD or REVOKE, unless `--adopt` speaks for this apply.
70
+ const observed = yield* readRole(pg, news.name);
71
+ if (observed !== undefined && output === undefined) {
72
+ yield* refuseTakeover({ fqn, instanceId, output }, 'Postgres.Role');
73
+ }
74
+ return yield* reconcileWithClient(
75
+ pg,
76
+ news,
77
+ process.env,
78
+ output?.passwordSeal,
79
+ output?.oid,
80
+ );
81
+ }),
82
+ );
83
+ }),
84
+
85
+ delete: ({ output }) =>
86
+ withPg((pg) =>
87
+ Effect.gen(function* () {
88
+ if (output === undefined) return;
89
+ const live = yield* readRole(pg, output.name);
90
+ if (live === undefined) return;
91
+ if (live.oid !== output.oid) {
92
+ return yield* Effect.fail(
93
+ new PostgresRoleIdentityRefused({
94
+ role: output.name,
95
+ storedOid: output.oid,
96
+ liveOid: live.oid,
97
+ }),
98
+ );
99
+ }
100
+ yield* pg.unsafe(buildDropRoleSql(output.name)).pipe(Effect.asVoid);
101
+ }),
102
+ ),
103
+ });
104
+
105
+ export const PostgresRoleProvider = () => Provider.succeed(PostgresRole, postgresRoleHandlers);
@@ -0,0 +1,225 @@
1
+ /**
2
+ * Every plan-time refusal and offline comparison, checked without a client: name-byte precision
3
+ * (the truncation trap `Postgres.Database` refuses), the rename refusal, a full sweep proving
4
+ * `diff` answers `update` and never `replace`, per-field and membership drift semantics,
5
+ * quoting, and the write-only password shape (S25). Zone-free and unparseable `validUntil`
6
+ * live in `role-valid-until.test.ts`.
7
+ */
8
+ import { describe, expect, test } from 'bun:test';
9
+ import { readFileSync } from 'node:fs';
10
+ import * as Effect from 'effect/Effect';
11
+ import * as Redacted from 'effect/Redacted';
12
+ import { POSTGRES_NAME_MAX_BYTES } from './database-attrs.ts';
13
+ import { refuseAtPlan } from './role.ts';
14
+ import { diffPostgresRole } from './role-diff.ts';
15
+ import { resolvePassword } from './role-secrets.ts';
16
+ import { seal } from '../secrets/write-only.ts';
17
+ import { PostgresRoleNameRefused, PostgresRoleRenameRefused } from './role-errors.ts';
18
+ import type { PostgresRoleAttributes, PostgresRoleProps } from './role-attrs.ts';
19
+ import {
20
+ buildAlterRoleSql,
21
+ buildCreateRoleSql,
22
+ buildDropRoleSql,
23
+ buildSetPasswordSql,
24
+ scalarDrift,
25
+ } from './role-sql.ts';
26
+ import {
27
+ buildGrantMembershipSql,
28
+ buildRevokeGrantorMembershipSql,
29
+ membershipDrift,
30
+ } from './role-membership-sql.ts';
31
+
32
+ const run = <A, E>(eff: Effect.Effect<A, E>): Promise<A> => Effect.runPromise(eff);
33
+ const fails = <A, E>(eff: Effect.Effect<A, E>): Promise<E> => Effect.runPromise(Effect.flip(eff));
34
+
35
+ const base: PostgresRoleProps = {
36
+ name: 'seat-observability',
37
+ login: false,
38
+ connectionLimit: 10,
39
+ inherit: true,
40
+ };
41
+
42
+ const output: PostgresRoleAttributes = {
43
+ name: 'seat-observability',
44
+ oid: 20000,
45
+ login: false,
46
+ connectionLimit: 10,
47
+ inherit: true,
48
+ validUntil: null,
49
+ memberOf: [],
50
+ passwordSeal: '',
51
+ };
52
+
53
+ describe('name byte length', () => {
54
+ test('a 63-byte ASCII name passes the plan check', async () => {
55
+ await run(refuseAtPlan({ ...base, name: 'a'.repeat(63) }));
56
+ });
57
+
58
+ test('a 64-byte ASCII name is refused with the typed tag naming the measurement', async () => {
59
+ const error = await fails(refuseAtPlan({ ...base, name: 'a'.repeat(64) }));
60
+ expect(error).toBeInstanceOf(PostgresRoleNameRefused);
61
+ expect((error as PostgresRoleNameRefused).byteLength).toBe(64);
62
+ expect((error as PostgresRoleNameRefused).limit).toBe(POSTGRES_NAME_MAX_BYTES);
63
+ });
64
+
65
+ test('a 64-byte multibyte name is refused though .length is 32', async () => {
66
+ // U+00E9 ("é") is 2 UTF-8 bytes; 32 of them is 64 bytes — over the 63-byte limit.
67
+ const error = await fails(refuseAtPlan({ ...base, name: 'é'.repeat(32) }));
68
+ expect(error).toBeInstanceOf(PostgresRoleNameRefused);
69
+ });
70
+ });
71
+
72
+ describe('diff', () => {
73
+ test('a rename is refused at plan, before reconcile ever runs', async () => {
74
+ const error = await fails(diffPostgresRole({ ...base, name: 'seat-logs' }, output));
75
+ expect(error).toBeInstanceOf(PostgresRoleRenameRefused);
76
+ });
77
+
78
+ test('an unchanged declaration answers noop, and no output answers undefined (nothing to diff)', async () => {
79
+ expect(await run(diffPostgresRole(base, output))).toEqual({ action: 'noop' });
80
+ expect(await run(diffPostgresRole(base, undefined))).toBeUndefined();
81
+ });
82
+
83
+ test('a change in any single prop answers update, and NEVER replace \u2014 swept across every prop', async () => {
84
+ const variants: ReadonlyArray<
85
+ readonly [Partial<PostgresRoleProps>, Partial<PostgresRoleAttributes>]
86
+ > = [
87
+ [{ login: true }, {}],
88
+ [{ inherit: false }, {}],
89
+ [{ connectionLimit: 5 }, {}],
90
+ [{ validUntil: '2027-01-01T00:00:00Z' }, {}],
91
+ [{ memberOf: ['hf_agent'] }, {}],
92
+ [{ memberOf: [] }, { memberOf: ['hf_agent'] }],
93
+ [{ memberOf: ['hf_agent', 'seat-owners'] }, { memberOf: ['hf_agent'] }],
94
+ ];
95
+ for (const [variant, live] of variants) {
96
+ const result = await run(diffPostgresRole({ ...base, ...variant }, { ...output, ...live }));
97
+ expect(result?.action, `variant ${JSON.stringify(variant)}`).toBe('update');
98
+ expect(result).not.toEqual({ action: 'replace' });
99
+ }
100
+ });
101
+
102
+ test('a rotated password answers update even with no other drift; a matching or unreadable one does not', async () => {
103
+ const declared = { ...base, password: { fromEnv: 'PG_SEAT_ROLE_PASSWORD' } as const };
104
+ const stale = seal({ password: 'old' }, 'fixed-salt');
105
+ const matched = seal({ password: 'current' }, 'fixed-salt');
106
+ const held = { PG_SEAT_ROLE_PASSWORD: 'current' };
107
+ // A value the seal does not match — rotated, or an adopted role's empty seal — answers update.
108
+ expect(await run(diffPostgresRole(declared, { ...output, passwordSeal: stale }, held))).toEqual(
109
+ { action: 'update' },
110
+ );
111
+ expect(await run(diffPostgresRole(declared, output, held))).toEqual({ action: 'update' });
112
+ // Still matching: noop. The variable unset here: never stale — a plan that cannot read the
113
+ // secret never churns it.
114
+ expect(
115
+ await run(diffPostgresRole(declared, { ...output, passwordSeal: matched }, held)),
116
+ ).toEqual({ action: 'noop' });
117
+ expect(await run(diffPostgresRole(declared, { ...output, passwordSeal: stale }, {}))).toEqual({
118
+ action: 'noop',
119
+ });
120
+ });
121
+ });
122
+
123
+ describe('scalarDrift', () => {
124
+ test('lists one change per drifted field, in the family\u2019s compared order', () => {
125
+ expect(
126
+ scalarDrift(
127
+ {
128
+ ...base,
129
+ login: true,
130
+ inherit: false,
131
+ connectionLimit: 5,
132
+ validUntil: '2027-01-01T00:00:00Z',
133
+ },
134
+ { ...output, login: false, inherit: true, connectionLimit: 10 },
135
+ ),
136
+ ).toEqual([
137
+ { prop: 'login', value: true },
138
+ { prop: 'inherit', value: false },
139
+ { prop: 'connectionLimit', value: 5 },
140
+ { prop: 'validUntil', value: '2027-01-01T00:00:00Z' },
141
+ ]);
142
+ });
143
+
144
+ test('is empty when the live row matches, including the milliseconds case', () => {
145
+ expect(scalarDrift(base, output)).toEqual([]);
146
+ expect(
147
+ scalarDrift(
148
+ { ...base, validUntil: '2027-01-01T00:00:00Z' },
149
+ {
150
+ ...output,
151
+ validUntil: '2027-01-01T00:00:00.000Z',
152
+ },
153
+ ),
154
+ ).toEqual([]);
155
+ });
156
+ });
157
+
158
+ describe('membershipDrift', () => {
159
+ test('undefined asserts nothing; duplicates collapse; grants and revokes are sorted set differences', () => {
160
+ expect(membershipDrift(undefined, ['hf_agent'])).toEqual({ grants: [], revokes: [] });
161
+ expect(membershipDrift(['hf_agent', 'hf_agent', 'seat-b'], [])).toEqual({
162
+ grants: ['hf_agent', 'seat-b'],
163
+ revokes: [],
164
+ });
165
+ expect(membershipDrift(['hf_agent'], ['hf_agent', 'seat-b', 'seat-a'])).toEqual({
166
+ grants: [],
167
+ revokes: ['seat-a', 'seat-b'],
168
+ });
169
+ });
170
+ });
171
+
172
+ describe('quoting', () => {
173
+ test('a hostile role name is doubled in every statement that quotes it', () => {
174
+ expect(buildCreateRoleSql({ ...base, name: 'a"b' })).toBe(
175
+ 'CREATE ROLE "a""b" WITH NOLOGIN INHERIT CONNECTION LIMIT 10',
176
+ );
177
+ expect(buildAlterRoleSql('a"b', { prop: 'connectionLimit', value: 3 })).toBe(
178
+ 'ALTER ROLE "a""b" WITH CONNECTION LIMIT 3',
179
+ );
180
+ expect(buildGrantMembershipSql('m"1', 'p"2')).toBe('GRANT "p""2" TO "m""1" WITH SET FALSE');
181
+ expect(buildRevokeGrantorMembershipSql('m"1', 'p"2', 'g"3')).toBe(
182
+ 'REVOKE "p""2" FROM "m""1" GRANTED BY "g""3"',
183
+ );
184
+ expect(buildDropRoleSql('a"b')).toBe('DROP ROLE IF EXISTS "a""b"');
185
+ });
186
+
187
+ test('the value buildSetPasswordSql carries is single-quote escaped exactly once', () => {
188
+ // In production that value is a SCRAM verifier (`role-scram.ts`), which the base64 alphabet
189
+ // keeps free of `'`; the quoting property still holds for any string handed to the builder.
190
+ expect(buildSetPasswordSql('seat-observability', "O'Brien; DROP")).toBe(
191
+ "ALTER ROLE \"seat-observability\" WITH PASSWORD E'O''Brien; DROP'",
192
+ );
193
+ });
194
+ });
195
+
196
+ describe('write-only password shape (S25)', () => {
197
+ const props = { ...base, password: { fromEnv: 'PG_SEAT_ROLE_PASSWORD' } };
198
+
199
+ test('resolvePassword holds the value as Redacted, which never prints or serialises it', () => {
200
+ const { variable, value } = resolvePassword(props, { PG_SEAT_ROLE_PASSWORD: 's3cret-value' });
201
+ expect(variable).toBe('PG_SEAT_ROLE_PASSWORD');
202
+ expect(String(value)).toBe('<redacted>');
203
+ expect(JSON.stringify({ value })).not.toContain('s3cret-value');
204
+ expect(Redacted.isRedacted(value)).toBe(true);
205
+ if (Redacted.isRedacted(value)) {
206
+ expect(Redacted.value(value)).toBe('s3cret-value');
207
+ }
208
+ });
209
+
210
+ test('resolvePassword answers no value when the variable is unset or empty', () => {
211
+ expect(resolvePassword(props, {}).value).toBeUndefined();
212
+ expect(resolvePassword(props, { PG_SEAT_ROLE_PASSWORD: '' }).value).toBeUndefined();
213
+ expect(resolvePassword(base, { PG_SEAT_ROLE_PASSWORD: 's3cret-value' })).toEqual({
214
+ variable: undefined,
215
+ value: undefined,
216
+ });
217
+ });
218
+ });
219
+
220
+ describe('removal policy', () => {
221
+ test('defaultRemovalPolicy is retain (checked in source, declared alongside PostgresRole)', () => {
222
+ const source = readFileSync(new URL('./role.ts', import.meta.url), 'utf8');
223
+ expect(source).toContain("defaultRemovalPolicy: 'retain'");
224
+ });
225
+ });
@@ -0,0 +1,38 @@
1
+ /**
2
+ * The SCRAM-SHA-256 verifier `role-scram.ts` builds — the only password form this family ever
3
+ * sends. The golden vector below was computed OUTSIDE node (`hashlib`/`hmac` from the Python 3
4
+ * standard library, 2026-09-30) so the test is a cross-implementation check, not the function
5
+ * re-deriving its own math.
6
+ */
7
+ import { describe, expect, test } from 'bun:test';
8
+ import { scramSha256Verifier } from './role-scram.ts';
9
+
10
+ // A placeholder, never a credential; the salt is fixed so the vector is deterministic.
11
+ const GOLDEN_SALT = Buffer.from('a1b2c3d4e5f60718293a4b5c6d7e8f90', 'hex');
12
+ const PLACEHOLDER = 'placeholder-password';
13
+ const GOLDEN =
14
+ 'SCRAM-SHA-256$4096:obLD1OX2BxgpOktcbX6PkA==$' +
15
+ 'wP+XMQxcTjqewigDeRTAJB703b2ekDF1fCBumhTm4N8=:' +
16
+ 'Bcc+TZOxqllgMPNn8IX0u/TJpvlRhwqPJaNQ2VLm1F4=';
17
+
18
+ describe('scramSha256Verifier', () => {
19
+ test('matches a verifier computed by an independent implementation', () => {
20
+ expect(scramSha256Verifier(PLACEHOLDER, GOLDEN_SALT)).toBe(GOLDEN);
21
+ });
22
+
23
+ test('fresh salt per call, standard base64 parts at PostgreSQL defaults', () => {
24
+ const shape =
25
+ /^SCRAM-SHA-256\$4096:[A-Za-z0-9+/]{22}==\$[A-Za-z0-9+/]{43}=:[A-Za-z0-9+/]{43}=$/;
26
+ const a = scramSha256Verifier(PLACEHOLDER);
27
+ const b = scramSha256Verifier(PLACEHOLDER);
28
+ expect(a).not.toBe(b);
29
+ expect(a).toMatch(shape);
30
+ expect(b).toMatch(shape);
31
+ });
32
+
33
+ test('a different password gives a different verifier for the same salt', () => {
34
+ expect(scramSha256Verifier(PLACEHOLDER, GOLDEN_SALT)).not.toBe(
35
+ scramSha256Verifier('placeholder-password-two', GOLDEN_SALT),
36
+ );
37
+ });
38
+ });
@@ -0,0 +1,40 @@
1
+ /**
2
+ * SCRAM-SHA-256 password verifier, built client-side the way `psql \password` and `createuser -w`
3
+ * do (`createuser.c@REL_18_6` → `pg_format scram verifier`). Postgres stores the verifier as-is in
4
+ * `pg_authid.rolpassword` and never recovers the password from it.
5
+ *
6
+ * ⛔ THE PLAIN PASSWORD NEVER ENTERS A STATEMENT. Before this file, `ALTER ROLE … PASSWORD` carried
7
+ * the plain value as a literal, so it landed in the Effect SQL span attribute `db.query.text`,
8
+ * in `pg_stat_statements` (`track_utility=on` records utility statements), and in the server log
9
+ * of any failed `ALTER`. The verifier is the one form Postgres accepts that is not the secret:
10
+ * `PASSWORD 'SCRAM-SHA-256$4096:<salt>$<StoredKey>:<ServerKey>'` (`create_role.sgml`: a string
11
+ * already in SCRAM verifier format is stored as given).
12
+ * ★ WIRE FORMAT AND MATH (`scram_protocols` / RFC 5802 / `auth-scram.c`): salt 16 bytes,
13
+ * iterations 4096 (both Postgres defaults), standard base64. SaltedPassword = PBKDF2-HMAC-SHA-256
14
+ * (password, salt, 4096, 32 bytes); ClientKey = HMAC(SaltedPassword, "Client Key"); StoredKey =
15
+ * SHA-256(ClientKey); ServerKey = HMAC(SaltedPassword, "Server Key").
16
+ */
17
+ import { createHash, createHmac, pbkdf2Sync, randomBytes } from 'node:crypto';
18
+
19
+ const ITERATIONS = 4096;
20
+ const SALT_BYTES = 16;
21
+ const KEY_BYTES = 32;
22
+
23
+ const hmac = (key: Buffer, label: string): Buffer =>
24
+ createHmac('sha256', key).update(label, 'utf8').digest();
25
+
26
+ const b64 = (value: Buffer): string => value.toString('base64');
27
+
28
+ /**
29
+ * `SCRAM-SHA-256$4096:<salt>$<StoredKey>:<ServerKey>` for one password. The salt is fresh per
30
+ * call (same as `psql \password`); tests pass one to make the output deterministic.
31
+ * Callers first run `assertPasswordAscii`: raw non-ASCII may differ from PostgreSQL's
32
+ * SASLprep bytes (https://www.postgresql.org/docs/16/sasl-authentication.html).
33
+ */
34
+ export const scramSha256Verifier = (password: string, salt?: Buffer): string => {
35
+ const bytes = salt ?? randomBytes(SALT_BYTES);
36
+ const saltedPassword = pbkdf2Sync(password, bytes, ITERATIONS, KEY_BYTES, 'sha256');
37
+ const storedKey = createHash('sha256').update(hmac(saltedPassword, 'Client Key')).digest();
38
+ const serverKey = hmac(saltedPassword, 'Server Key');
39
+ return `SCRAM-SHA-256$${ITERATIONS}:${b64(bytes)}$${b64(storedKey)}:${b64(serverKey)}`;
40
+ };
@@ -0,0 +1,61 @@
1
+ /**
2
+ * The `Postgres.Role` password: resolution from the environment and the seal arithmetic that
3
+ * lets state answer "did this value already go out?" without storing it (S25).
4
+ *
5
+ * ⛔ THE PASSWORD IS A REFERENCE, NEVER A VALUE. It is declared as
6
+ * `{ fromEnv: 'PG_SEAT_WIDGET_PASSWORD' }`; the NAME lands in state, the value is resolved
7
+ * from the deploying process's environment at reconcile/diff time, held as `Redacted`, and
8
+ * quoted into the one `ALTER ROLE … PASSWORD` statement that needs it.
9
+ * ★ What state remembers is a `scrypt:<salt>:<digest>` seal ({@link sealPassword}), so a
10
+ * reconcile sends the password only when the environment value differs from the one last
11
+ * written — a plan never re-sends a secret that already matches, and never stores one.
12
+ */
13
+ import * as Redacted from 'effect/Redacted';
14
+ import * as Effect from 'effect/Effect';
15
+ import { PostgresRolePasswordNonAsciiRefused } from './role-errors.ts';
16
+ import { type Environment, resolveAll, seal, sealMatches } from '../secrets/write-only.ts';
17
+ import type { PostgresRoleProps } from './role-attrs.ts';
18
+
19
+ /** What the environment holds for the declared password. `value` is `undefined` when no
20
+ * password is declared, or when the variable is unset or empty (a renderer that failed writes
21
+ * `NAME=`; `resolveAll` treats empty as missing). ⛔ Reads the environment at CALL time. */
22
+ export interface ResolvedPassword {
23
+ readonly variable: string | undefined;
24
+ readonly value: Redacted.Redacted<string> | undefined;
25
+ }
26
+
27
+ /** ⛔ PostgreSQL applies SASLprep before SCRAM, with a raw-byte fallback for prohibited input:
28
+ * https://www.postgresql.org/docs/16/sasl-authentication.html#SASL-SCRAM-SHA-256
29
+ * Non-ASCII can normalize to different bytes (e.g. soft hyphen disappears). Refuse before
30
+ * any write, even with a matching old seal, until we have a vendor-compatible normalizer.
31
+ * ASCII is unchanged, including prohibited ASCII controls via PostgreSQL's fallback. */
32
+ export const assertPasswordAscii = (
33
+ role: string,
34
+ resolved: ResolvedPassword,
35
+ ): Effect.Effect<void, PostgresRolePasswordNonAsciiRefused> =>
36
+ resolved.value !== undefined && /[\u0080-\uffff]/.test(Redacted.value(resolved.value))
37
+ ? Effect.fail(new PostgresRolePasswordNonAsciiRefused({ role }))
38
+ : Effect.void;
39
+
40
+ export const resolvePassword = (
41
+ props: PostgresRoleProps,
42
+ env: Environment = process.env,
43
+ ): ResolvedPassword => {
44
+ if (props.password === undefined) return { variable: undefined, value: undefined };
45
+ const { values } = resolveAll({ password: props.password }, env);
46
+ const raw = values['password'];
47
+ return {
48
+ variable: props.password.fromEnv,
49
+ value: raw === undefined ? undefined : Redacted.make(raw),
50
+ };
51
+ };
52
+
53
+ /** `seal({ password })` of the resolved value — the one thing state remembers about it. */
54
+ export const sealPassword = (value: Redacted.Redacted<string>): string =>
55
+ seal({ password: Redacted.value(value) });
56
+
57
+ /** Whether the seal was made from the resolved value. Malformed/empty seals answer false, so an
58
+ * adopted role (seal `''`) or one written outside this stack gets the password written on the
59
+ * first reconcile that holds the variable — the declaration is authoritative. */
60
+ export const passwordMatchesSeal = (value: Redacted.Redacted<string>, sealed: string): boolean =>
61
+ sealMatches(sealed, { password: Redacted.value(value) });