@homeflare/alchemy 0.42.0 → 0.44.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +10 -1
- package/dist/caddy/index.js +35 -1
- package/dist/caddy/index.js.map +3 -3
- package/dist/launchd/index.js +35 -1
- package/dist/launchd/index.js.map +3 -3
- package/dist/linux/index.js +35 -1
- package/dist/linux/index.js.map +3 -3
- package/dist/litellm/access-group-form.d.ts +26 -0
- package/dist/litellm/access-group-form.d.ts.map +1 -0
- package/dist/litellm/access-group-operations.d.ts +37 -0
- package/dist/litellm/access-group-operations.d.ts.map +1 -0
- package/dist/litellm/access-group-types.d.ts +49 -0
- package/dist/litellm/access-group-types.d.ts.map +1 -0
- package/dist/litellm/access-group.d.ts +55 -0
- package/dist/litellm/access-group.d.ts.map +1 -0
- package/dist/litellm/budget-errors.d.ts +36 -0
- package/dist/litellm/budget-errors.d.ts.map +1 -0
- package/dist/litellm/budget-form.d.ts +50 -0
- package/dist/litellm/budget-form.d.ts.map +1 -0
- package/dist/litellm/budget-operations.d.ts +20 -0
- package/dist/litellm/budget-operations.d.ts.map +1 -0
- package/dist/litellm/budget.d.ts +61 -0
- package/dist/litellm/budget.d.ts.map +1 -0
- package/dist/litellm/credential-errors.d.ts +131 -0
- package/dist/litellm/credential-errors.d.ts.map +1 -0
- package/dist/litellm/credential-form.d.ts +79 -0
- package/dist/litellm/credential-form.d.ts.map +1 -0
- package/dist/litellm/credential-operations.d.ts +84 -0
- package/dist/litellm/credential-operations.d.ts.map +1 -0
- package/dist/litellm/credential-rewrite.d.ts +6 -0
- package/dist/litellm/credential-rewrite.d.ts.map +1 -0
- package/dist/litellm/credential-types.d.ts +87 -0
- package/dist/litellm/credential-types.d.ts.map +1 -0
- package/dist/litellm/credential-values.d.ts +53 -0
- package/dist/litellm/credential-values.d.ts.map +1 -0
- package/dist/litellm/credential.d.ts +83 -0
- package/dist/litellm/credential.d.ts.map +1 -0
- package/dist/litellm/fake-access-group-litellm.d.ts +34 -0
- package/dist/litellm/fake-access-group-litellm.d.ts.map +1 -0
- package/dist/litellm/fake-credential-litellm.d.ts +54 -0
- package/dist/litellm/fake-credential-litellm.d.ts.map +1 -0
- package/dist/litellm/fake-keys.d.ts +53 -0
- package/dist/litellm/fake-keys.d.ts.map +1 -0
- package/dist/litellm/fake-litellm.d.ts +9 -1
- package/dist/litellm/fake-litellm.d.ts.map +1 -1
- package/dist/litellm/fake-mcp-litellm.d.ts +67 -0
- package/dist/litellm/fake-mcp-litellm.d.ts.map +1 -0
- package/dist/litellm/fake-model-litellm-post-update.d.ts +49 -0
- package/dist/litellm/fake-model-litellm-post-update.d.ts.map +1 -0
- package/dist/litellm/fake-model-litellm.d.ts +48 -0
- package/dist/litellm/fake-model-litellm.d.ts.map +1 -0
- package/dist/litellm/fake-policy-attachment-litellm.d.ts +33 -0
- package/dist/litellm/fake-policy-attachment-litellm.d.ts.map +1 -0
- package/dist/litellm/fake-policy-litellm.d.ts +32 -0
- package/dist/litellm/fake-policy-litellm.d.ts.map +1 -0
- package/dist/litellm/fake-registry-base.d.ts +42 -0
- package/dist/litellm/fake-registry-base.d.ts.map +1 -0
- package/dist/litellm/fake-stack.d.ts +8 -1
- package/dist/litellm/fake-stack.d.ts.map +1 -1
- package/dist/litellm/fake-team-litellm.d.ts +47 -0
- package/dist/litellm/fake-team-litellm.d.ts.map +1 -0
- package/dist/litellm/fake-tool-litellm.d.ts +28 -0
- package/dist/litellm/fake-tool-litellm.d.ts.map +1 -0
- package/dist/litellm/fake-toolset-litellm.d.ts +29 -0
- package/dist/litellm/fake-toolset-litellm.d.ts.map +1 -0
- package/dist/litellm/index.d.ts +20 -0
- package/dist/litellm/index.d.ts.map +1 -1
- package/dist/litellm/index.js +3201 -2
- package/dist/litellm/index.js.map +70 -5
- package/dist/litellm/key-errors.d.ts +169 -0
- package/dist/litellm/key-errors.d.ts.map +1 -0
- package/dist/litellm/key-form.d.ts +119 -0
- package/dist/litellm/key-form.d.ts.map +1 -0
- package/dist/litellm/key-harness.d.ts +41 -0
- package/dist/litellm/key-harness.d.ts.map +1 -0
- package/dist/litellm/key-metadata-errors.d.ts +30 -0
- package/dist/litellm/key-metadata-errors.d.ts.map +1 -0
- package/dist/litellm/key-metadata.d.ts +22 -0
- package/dist/litellm/key-metadata.d.ts.map +1 -0
- package/dist/litellm/key-operations.d.ts +63 -0
- package/dist/litellm/key-operations.d.ts.map +1 -0
- package/dist/litellm/key-secret.d.ts +33 -0
- package/dist/litellm/key-secret.d.ts.map +1 -0
- package/dist/litellm/key.d.ts +78 -0
- package/dist/litellm/key.d.ts.map +1 -0
- package/dist/litellm/mcp-server-credential.d.ts +50 -0
- package/dist/litellm/mcp-server-credential.d.ts.map +1 -0
- package/dist/litellm/mcp-server-errors.d.ts +101 -0
- package/dist/litellm/mcp-server-errors.d.ts.map +1 -0
- package/dist/litellm/mcp-server-form.d.ts +56 -0
- package/dist/litellm/mcp-server-form.d.ts.map +1 -0
- package/dist/litellm/mcp-server-locate.d.ts +26 -0
- package/dist/litellm/mcp-server-locate.d.ts.map +1 -0
- package/dist/litellm/mcp-server-operations.d.ts +63 -0
- package/dist/litellm/mcp-server-operations.d.ts.map +1 -0
- package/dist/litellm/mcp-server-shadow.d.ts +24 -0
- package/dist/litellm/mcp-server-shadow.d.ts.map +1 -0
- package/dist/litellm/mcp-server-types.d.ts +118 -0
- package/dist/litellm/mcp-server-types.d.ts.map +1 -0
- package/dist/litellm/mcp-server-url.d.ts +11 -0
- package/dist/litellm/mcp-server-url.d.ts.map +1 -0
- package/dist/litellm/mcp-server.d.ts +95 -0
- package/dist/litellm/mcp-server.d.ts.map +1 -0
- package/dist/litellm/model-claim.d.ts +24 -0
- package/dist/litellm/model-claim.d.ts.map +1 -0
- package/dist/litellm/model-credential.d.ts +12 -0
- package/dist/litellm/model-credential.d.ts.map +1 -0
- package/dist/litellm/model-errors.d.ts +119 -0
- package/dist/litellm/model-errors.d.ts.map +1 -0
- package/dist/litellm/model-form.d.ts +78 -0
- package/dist/litellm/model-form.d.ts.map +1 -0
- package/dist/litellm/model-locate.d.ts +13 -0
- package/dist/litellm/model-locate.d.ts.map +1 -0
- package/dist/litellm/model-operations.d.ts +59 -0
- package/dist/litellm/model-operations.d.ts.map +1 -0
- package/dist/litellm/model-types.d.ts +91 -0
- package/dist/litellm/model-types.d.ts.map +1 -0
- package/dist/litellm/model.d.ts +88 -0
- package/dist/litellm/model.d.ts.map +1 -0
- package/dist/litellm/operations.d.ts +22 -0
- package/dist/litellm/operations.d.ts.map +1 -1
- package/dist/litellm/policy-attachment-form.d.ts +19 -0
- package/dist/litellm/policy-attachment-form.d.ts.map +1 -0
- package/dist/litellm/policy-attachment-operations.d.ts +31 -0
- package/dist/litellm/policy-attachment-operations.d.ts.map +1 -0
- package/dist/litellm/policy-attachment-types.d.ts +48 -0
- package/dist/litellm/policy-attachment-types.d.ts.map +1 -0
- package/dist/litellm/policy-attachment.d.ts +61 -0
- package/dist/litellm/policy-attachment.d.ts.map +1 -0
- package/dist/litellm/policy-form.d.ts +22 -0
- package/dist/litellm/policy-form.d.ts.map +1 -0
- package/dist/litellm/policy-operations.d.ts +42 -0
- package/dist/litellm/policy-operations.d.ts.map +1 -0
- package/dist/litellm/policy-types.d.ts +57 -0
- package/dist/litellm/policy-types.d.ts.map +1 -0
- package/dist/litellm/policy.d.ts +65 -0
- package/dist/litellm/policy.d.ts.map +1 -0
- package/dist/litellm/providers.d.ts +1 -1
- package/dist/litellm/providers.d.ts.map +1 -1
- package/dist/litellm/registry-errors.d.ts +62 -0
- package/dist/litellm/registry-errors.d.ts.map +1 -0
- package/dist/litellm/registry-support.d.ts +42 -0
- package/dist/litellm/registry-support.d.ts.map +1 -0
- package/dist/litellm/team-form.d.ts +26 -0
- package/dist/litellm/team-form.d.ts.map +1 -0
- package/dist/litellm/team-members.d.ts +10 -0
- package/dist/litellm/team-members.d.ts.map +1 -0
- package/dist/litellm/team-operations.d.ts +38 -0
- package/dist/litellm/team-operations.d.ts.map +1 -0
- package/dist/litellm/team-permission.d.ts +35 -0
- package/dist/litellm/team-permission.d.ts.map +1 -0
- package/dist/litellm/team-types.d.ts +101 -0
- package/dist/litellm/team-types.d.ts.map +1 -0
- package/dist/litellm/team.d.ts +67 -0
- package/dist/litellm/team.d.ts.map +1 -0
- package/dist/litellm/tool-policy-form.d.ts +21 -0
- package/dist/litellm/tool-policy-form.d.ts.map +1 -0
- package/dist/litellm/tool-policy-operations.d.ts +30 -0
- package/dist/litellm/tool-policy-operations.d.ts.map +1 -0
- package/dist/litellm/tool-policy-types.d.ts +46 -0
- package/dist/litellm/tool-policy-types.d.ts.map +1 -0
- package/dist/litellm/tool-policy.d.ts +62 -0
- package/dist/litellm/tool-policy.d.ts.map +1 -0
- package/dist/litellm/toolset-form.d.ts +20 -0
- package/dist/litellm/toolset-form.d.ts.map +1 -0
- package/dist/litellm/toolset-operations.d.ts +39 -0
- package/dist/litellm/toolset-operations.d.ts.map +1 -0
- package/dist/litellm/toolset-types.d.ts +46 -0
- package/dist/litellm/toolset-types.d.ts.map +1 -0
- package/dist/litellm/toolset.d.ts +55 -0
- package/dist/litellm/toolset.d.ts.map +1 -0
- package/dist/openbao/auth-role-form.d.ts +9 -0
- package/dist/openbao/auth-role-form.d.ts.map +1 -1
- package/dist/openbao/auth-role-wire.d.ts.map +1 -1
- package/dist/openbao/fake-engines-roles.d.ts.map +1 -1
- package/dist/openbao/index.js +43 -6
- package/dist/openbao/index.js.map +5 -5
- package/dist/ownership/rows.d.ts +11 -0
- package/dist/ownership/rows.d.ts.map +1 -1
- package/dist/postgres/connection.d.ts +32 -2
- package/dist/postgres/connection.d.ts.map +1 -1
- package/dist/postgres/database-sql.d.ts +28 -9
- package/dist/postgres/database-sql.d.ts.map +1 -1
- package/dist/postgres/database.d.ts +1 -1
- package/dist/postgres/database.d.ts.map +1 -1
- package/dist/postgres/fake-grants-parse.d.ts +37 -0
- package/dist/postgres/fake-grants-parse.d.ts.map +1 -0
- package/dist/postgres/fake-grants-read.d.ts +61 -0
- package/dist/postgres/fake-grants-read.d.ts.map +1 -0
- package/dist/postgres/fake-grants-sql.d.ts +46 -0
- package/dist/postgres/fake-grants-sql.d.ts.map +1 -0
- package/dist/postgres/fake-grants-write.d.ts +29 -0
- package/dist/postgres/fake-grants-write.d.ts.map +1 -0
- package/dist/postgres/fake-role-sql.d.ts +40 -0
- package/dist/postgres/fake-role-sql.d.ts.map +1 -0
- package/dist/postgres/fake-schema-sql.d.ts +42 -0
- package/dist/postgres/fake-schema-sql.d.ts.map +1 -0
- package/dist/postgres/fake-sql-parse.d.ts +23 -0
- package/dist/postgres/fake-sql-parse.d.ts.map +1 -0
- package/dist/postgres/fake-sql-quote.d.ts +12 -0
- package/dist/postgres/fake-sql-quote.d.ts.map +1 -0
- package/dist/postgres/fake-sql.d.ts +42 -0
- package/dist/postgres/fake-sql.d.ts.map +1 -1
- package/dist/postgres/grants-attrs.d.ts +115 -0
- package/dist/postgres/grants-attrs.d.ts.map +1 -0
- package/dist/postgres/grants-declare.d.ts +88 -0
- package/dist/postgres/grants-declare.d.ts.map +1 -0
- package/dist/postgres/grants-diff.d.ts +25 -0
- package/dist/postgres/grants-diff.d.ts.map +1 -0
- package/dist/postgres/grants-errors.d.ts +137 -0
- package/dist/postgres/grants-errors.d.ts.map +1 -0
- package/dist/postgres/grants-existence.d.ts +14 -0
- package/dist/postgres/grants-existence.d.ts.map +1 -0
- package/dist/postgres/grants-ops.d.ts +65 -0
- package/dist/postgres/grants-ops.d.ts.map +1 -0
- package/dist/postgres/grants-ownership.d.ts +18 -0
- package/dist/postgres/grants-ownership.d.ts.map +1 -0
- package/dist/postgres/grants-plan-restore.d.ts +22 -0
- package/dist/postgres/grants-plan-restore.d.ts.map +1 -0
- package/dist/postgres/grants-plan.d.ts +65 -0
- package/dist/postgres/grants-plan.d.ts.map +1 -0
- package/dist/postgres/grants-read-defaults.d.ts +17 -0
- package/dist/postgres/grants-read-defaults.d.ts.map +1 -0
- package/dist/postgres/grants-read.d.ts +77 -0
- package/dist/postgres/grants-read.d.ts.map +1 -0
- package/dist/postgres/grants-refuse.d.ts +25 -0
- package/dist/postgres/grants-refuse.d.ts.map +1 -0
- package/dist/postgres/grants-sql.d.ts +42 -0
- package/dist/postgres/grants-sql.d.ts.map +1 -0
- package/dist/postgres/grants-words.d.ts +29 -0
- package/dist/postgres/grants-words.d.ts.map +1 -0
- package/dist/postgres/grants.d.ts +37 -0
- package/dist/postgres/grants.d.ts.map +1 -0
- package/dist/postgres/index.d.ts +35 -8
- package/dist/postgres/index.d.ts.map +1 -1
- package/dist/postgres/index.js +2063 -39
- package/dist/postgres/index.js.map +47 -7
- package/dist/postgres/installed-classifier.d.ts +13 -0
- package/dist/postgres/installed-classifier.d.ts.map +1 -0
- package/dist/postgres/providers.d.ts +17 -7
- package/dist/postgres/providers.d.ts.map +1 -1
- package/dist/postgres/psql-executor.d.ts +25 -0
- package/dist/postgres/psql-executor.d.ts.map +1 -0
- package/dist/postgres/role-attrs.d.ts +109 -0
- package/dist/postgres/role-attrs.d.ts.map +1 -0
- package/dist/postgres/role-diff.d.ts +22 -0
- package/dist/postgres/role-diff.d.ts.map +1 -0
- package/dist/postgres/role-errors.d.ts +126 -0
- package/dist/postgres/role-errors.d.ts.map +1 -0
- package/dist/postgres/role-membership-sql.d.ts +75 -0
- package/dist/postgres/role-membership-sql.d.ts.map +1 -0
- package/dist/postgres/role-provider.d.ts +11 -0
- package/dist/postgres/role-provider.d.ts.map +1 -0
- package/dist/postgres/role-scram.d.ts +6 -0
- package/dist/postgres/role-scram.d.ts.map +1 -0
- package/dist/postgres/role-secrets.d.ts +30 -0
- package/dist/postgres/role-secrets.d.ts.map +1 -0
- package/dist/postgres/role-sql.d.ts +68 -0
- package/dist/postgres/role-sql.d.ts.map +1 -0
- package/dist/postgres/role.d.ts +57 -0
- package/dist/postgres/role.d.ts.map +1 -0
- package/dist/postgres/schema-assert.d.ts +50 -0
- package/dist/postgres/schema-assert.d.ts.map +1 -0
- package/dist/postgres/schema-attrs.d.ts +51 -0
- package/dist/postgres/schema-attrs.d.ts.map +1 -0
- package/dist/postgres/schema-delete.d.ts +28 -0
- package/dist/postgres/schema-delete.d.ts.map +1 -0
- package/dist/postgres/schema-diff.d.ts +14 -0
- package/dist/postgres/schema-diff.d.ts.map +1 -0
- package/dist/postgres/schema-errors.d.ts +139 -0
- package/dist/postgres/schema-errors.d.ts.map +1 -0
- package/dist/postgres/schema-identity.d.ts +8 -0
- package/dist/postgres/schema-identity.d.ts.map +1 -0
- package/dist/postgres/schema-reconcile.d.ts +12 -0
- package/dist/postgres/schema-reconcile.d.ts.map +1 -0
- package/dist/postgres/schema-sql.d.ts +45 -0
- package/dist/postgres/schema-sql.d.ts.map +1 -0
- package/dist/postgres/schema-test-kit.d.ts +67 -0
- package/dist/postgres/schema-test-kit.d.ts.map +1 -0
- package/dist/postgres/schema.d.ts +54 -0
- package/dist/postgres/schema.d.ts.map +1 -0
- package/dist/proxmox/ceph-flag-types.d.ts +57 -0
- package/dist/proxmox/ceph-flag-types.d.ts.map +1 -0
- package/dist/proxmox/ceph-flag-wire.d.ts +77 -0
- package/dist/proxmox/ceph-flag-wire.d.ts.map +1 -0
- package/dist/proxmox/ceph-flag.d.ts +15 -50
- package/dist/proxmox/ceph-flag.d.ts.map +1 -1
- package/dist/proxmox/ceph-osd-write.d.ts +10 -13
- package/dist/proxmox/ceph-osd-write.d.ts.map +1 -1
- package/dist/proxmox/index.js +806 -740
- package/dist/proxmox/index.js.map +9 -8
- package/dist/release/fake-engine.d.ts +10 -0
- package/dist/release/fake-engine.d.ts.map +1 -1
- package/dist/release/index.js +35 -1
- package/dist/release/index.js.map +3 -3
- package/dist/valkey/acl-attrs.d.ts +94 -0
- package/dist/valkey/acl-attrs.d.ts.map +1 -0
- package/dist/valkey/acl-compare.d.ts +28 -0
- package/dist/valkey/acl-compare.d.ts.map +1 -0
- package/dist/valkey/acl-connection.d.ts +6 -0
- package/dist/valkey/acl-connection.d.ts.map +1 -0
- package/dist/valkey/acl-form.d.ts +62 -0
- package/dist/valkey/acl-form.d.ts.map +1 -0
- package/dist/valkey/acl-managed.d.ts +4 -0
- package/dist/valkey/acl-managed.d.ts.map +1 -0
- package/dist/valkey/acl-ops.d.ts +37 -0
- package/dist/valkey/acl-ops.d.ts.map +1 -0
- package/dist/valkey/acl-profiles.d.ts +53 -0
- package/dist/valkey/acl-profiles.d.ts.map +1 -0
- package/dist/valkey/acl-reconcile.d.ts +26 -0
- package/dist/valkey/acl-reconcile.d.ts.map +1 -0
- package/dist/valkey/acl.d.ts +24 -0
- package/dist/valkey/acl.d.ts.map +1 -0
- package/dist/valkey/connection-service.d.ts +22 -0
- package/dist/valkey/connection-service.d.ts.map +1 -0
- package/dist/valkey/connection.d.ts +59 -0
- package/dist/valkey/connection.d.ts.map +1 -0
- package/dist/valkey/errors.d.ts +165 -0
- package/dist/valkey/errors.d.ts.map +1 -0
- package/dist/valkey/fake-acl.d.ts +29 -0
- package/dist/valkey/fake-acl.d.ts.map +1 -0
- package/dist/valkey/fake-valkey.d.ts +35 -0
- package/dist/valkey/fake-valkey.d.ts.map +1 -0
- package/dist/valkey/handler-test-fixture.d.ts +20 -0
- package/dist/valkey/handler-test-fixture.d.ts.map +1 -0
- package/dist/valkey/index.d.ts +18 -0
- package/dist/valkey/index.d.ts.map +1 -0
- package/dist/valkey/index.js +964 -0
- package/dist/valkey/index.js.map +27 -0
- package/dist/valkey/instance-attrs.d.ts +46 -0
- package/dist/valkey/instance-attrs.d.ts.map +1 -0
- package/dist/valkey/instance-form.d.ts +22 -0
- package/dist/valkey/instance-form.d.ts.map +1 -0
- package/dist/valkey/instance.d.ts +48 -0
- package/dist/valkey/instance.d.ts.map +1 -0
- package/dist/valkey/provider-socket-fixture.d.ts +8 -0
- package/dist/valkey/provider-socket-fixture.d.ts.map +1 -0
- package/dist/valkey/providers.d.ts +16 -0
- package/dist/valkey/providers.d.ts.map +1 -0
- package/dist/valkey/resp.d.ts +38 -0
- package/dist/valkey/resp.d.ts.map +1 -0
- package/dist/valkey/transport-error.d.ts +36 -0
- package/dist/valkey/transport-error.d.ts.map +1 -0
- package/dist/valkey/transport.d.ts +50 -0
- package/dist/valkey/transport.d.ts.map +1 -0
- package/docs/litellm-access-group.md +105 -0
- package/docs/litellm-budget.md +55 -0
- package/docs/litellm-credential.md +60 -0
- package/docs/litellm-key-metadata.md +56 -0
- package/docs/litellm-key.md +194 -0
- package/docs/litellm-mcp.md +187 -0
- package/docs/litellm-model.md +111 -0
- package/docs/litellm-policy-attachment.md +86 -0
- package/docs/litellm-policy.md +103 -0
- package/docs/litellm-team.md +117 -0
- package/docs/litellm-tool-policy.md +90 -0
- package/docs/litellm-toolset.md +92 -0
- package/docs/litellm.md +16 -8
- package/docs/peers.md +15 -6
- package/docs/postgres-grants-example.md +55 -0
- package/docs/postgres-grants.md +157 -0
- package/docs/postgres-role.md +114 -0
- package/docs/postgres-schema.md +139 -0
- package/docs/postgres.md +39 -0
- package/docs/proxmox-sdk-migration.md +45 -0
- package/docs/valkey-transport.md +29 -0
- package/docs/valkey.md +187 -0
- package/package.json +7 -3
- package/src/litellm/access-group-form.ts +105 -0
- package/src/litellm/access-group-operations.ts +92 -0
- package/src/litellm/access-group-types.ts +49 -0
- package/src/litellm/access-group.test.ts +197 -0
- package/src/litellm/access-group.ts +151 -0
- package/src/litellm/budget-errors.ts +38 -0
- package/src/litellm/budget-form.ts +94 -0
- package/src/litellm/budget-operations.ts +51 -0
- package/src/litellm/budget.test.ts +105 -0
- package/src/litellm/budget.ts +127 -0
- package/src/litellm/credential-errors.ts +183 -0
- package/src/litellm/credential-form.test.ts +151 -0
- package/src/litellm/credential-form.ts +211 -0
- package/src/litellm/credential-guards.test.ts +241 -0
- package/src/litellm/credential-operations.ts +208 -0
- package/src/litellm/credential-recovery.test.ts +93 -0
- package/src/litellm/credential-rewrite.test.ts +164 -0
- package/src/litellm/credential-rewrite.ts +50 -0
- package/src/litellm/credential-types.ts +117 -0
- package/src/litellm/credential-values.ts +94 -0
- package/src/litellm/credential.test.ts +218 -0
- package/src/litellm/credential.ts +227 -0
- package/src/litellm/fake-access-group-litellm.ts +127 -0
- package/src/litellm/fake-credential-litellm.ts +216 -0
- package/src/litellm/fake-keys.ts +190 -0
- package/src/litellm/fake-litellm.ts +66 -7
- package/src/litellm/fake-mcp-litellm.ts +214 -0
- package/src/litellm/fake-model-litellm-post-update.ts +103 -0
- package/src/litellm/fake-model-litellm.ts +233 -0
- package/src/litellm/fake-policy-attachment-litellm.ts +97 -0
- package/src/litellm/fake-policy-litellm.ts +186 -0
- package/src/litellm/fake-registry-base.ts +69 -0
- package/src/litellm/fake-stack.ts +17 -1
- package/src/litellm/fake-team-litellm.ts +229 -0
- package/src/litellm/fake-tool-litellm.ts +88 -0
- package/src/litellm/fake-toolset-litellm.ts +110 -0
- package/src/litellm/index.ts +153 -0
- package/src/litellm/key-delete.test.ts +212 -0
- package/src/litellm/key-errors.ts +241 -0
- package/src/litellm/key-form.ts +200 -0
- package/src/litellm/key-harness.ts +121 -0
- package/src/litellm/key-metadata-errors.ts +49 -0
- package/src/litellm/key-metadata.test.ts +156 -0
- package/src/litellm/key-metadata.ts +74 -0
- package/src/litellm/key-operations.ts +184 -0
- package/src/litellm/key-rotation.test.ts +110 -0
- package/src/litellm/key-scope.test.ts +135 -0
- package/src/litellm/key-secret.test.ts +248 -0
- package/src/litellm/key-secret.ts +137 -0
- package/src/litellm/key-transport.test.ts +166 -0
- package/src/litellm/key.test.ts +231 -0
- package/src/litellm/key.ts +213 -0
- package/src/litellm/mcp-server-adopt.test.ts +86 -0
- package/src/litellm/mcp-server-alias.test.ts +184 -0
- package/src/litellm/mcp-server-credential.ts +85 -0
- package/src/litellm/mcp-server-errors.ts +141 -0
- package/src/litellm/mcp-server-exports.test.ts +15 -0
- package/src/litellm/mcp-server-form.test.ts +241 -0
- package/src/litellm/mcp-server-form.ts +233 -0
- package/src/litellm/mcp-server-handlers.test.ts +150 -0
- package/src/litellm/mcp-server-locate.ts +66 -0
- package/src/litellm/mcp-server-operations.ts +111 -0
- package/src/litellm/mcp-server-registry.test.ts +146 -0
- package/src/litellm/mcp-server-retype.test.ts +114 -0
- package/src/litellm/mcp-server-shadow.test.ts +91 -0
- package/src/litellm/mcp-server-shadow.ts +45 -0
- package/src/litellm/mcp-server-tools.test.ts +85 -0
- package/src/litellm/mcp-server-types.ts +143 -0
- package/src/litellm/mcp-server-url.ts +66 -0
- package/src/litellm/mcp-server.test.ts +213 -0
- package/src/litellm/mcp-server.ts +225 -0
- package/src/litellm/model-claim.ts +69 -0
- package/src/litellm/model-credential.ts +45 -0
- package/src/litellm/model-errors.ts +168 -0
- package/src/litellm/model-foreign-row.test.ts +173 -0
- package/src/litellm/model-form.test.ts +228 -0
- package/src/litellm/model-form.ts +223 -0
- package/src/litellm/model-locate.test.ts +128 -0
- package/src/litellm/model-locate.ts +67 -0
- package/src/litellm/model-operations.test.ts +93 -0
- package/src/litellm/model-operations.ts +146 -0
- package/src/litellm/model-params-preserve.test.ts +119 -0
- package/src/litellm/model-recovery.test.ts +57 -0
- package/src/litellm/model-types.ts +112 -0
- package/src/litellm/model.test.ts +222 -0
- package/src/litellm/model.ts +237 -0
- package/src/litellm/operations.ts +1 -1
- package/src/litellm/policy-attachment-form.ts +103 -0
- package/src/litellm/policy-attachment-operations.ts +74 -0
- package/src/litellm/policy-attachment-types.ts +48 -0
- package/src/litellm/policy-attachment.test.ts +198 -0
- package/src/litellm/policy-attachment.ts +178 -0
- package/src/litellm/policy-form.ts +122 -0
- package/src/litellm/policy-operations.ts +112 -0
- package/src/litellm/policy-types.ts +58 -0
- package/src/litellm/policy.test.ts +214 -0
- package/src/litellm/policy.ts +146 -0
- package/src/litellm/providers.ts +25 -1
- package/src/litellm/registry-errors.ts +88 -0
- package/src/litellm/registry-exports.test.ts +66 -0
- package/src/litellm/registry-support.ts +109 -0
- package/src/litellm/team-form.ts +140 -0
- package/src/litellm/team-members.test.ts +150 -0
- package/src/litellm/team-members.ts +58 -0
- package/src/litellm/team-operations.ts +90 -0
- package/src/litellm/team-permission.ts +159 -0
- package/src/litellm/team-read.test.ts +89 -0
- package/src/litellm/team-types.ts +106 -0
- package/src/litellm/team.test.ts +206 -0
- package/src/litellm/team.ts +159 -0
- package/src/litellm/tool-policy-form.ts +94 -0
- package/src/litellm/tool-policy-operations.ts +58 -0
- package/src/litellm/tool-policy-types.ts +48 -0
- package/src/litellm/tool-policy.test.ts +172 -0
- package/src/litellm/tool-policy.ts +125 -0
- package/src/litellm/toolset-form.ts +104 -0
- package/src/litellm/toolset-operations.ts +90 -0
- package/src/litellm/toolset-types.ts +47 -0
- package/src/litellm/toolset.test.ts +198 -0
- package/src/litellm/toolset.ts +151 -0
- package/src/openbao/auth-role-form.test.ts +37 -0
- package/src/openbao/auth-role-form.ts +12 -1
- package/src/openbao/auth-role-wire.test.ts +1 -0
- package/src/openbao/auth-role-wire.ts +13 -1
- package/src/openbao/distilled-retry.test.ts +1 -0
- package/src/openbao/fake-engines-roles.ts +7 -1
- package/src/ownership/ownership.test.ts +33 -1
- package/src/ownership/rows.ts +53 -0
- package/src/postgres/connection.ts +72 -3
- package/src/postgres/database-bigint-serialization.test.ts +1 -0
- package/src/postgres/database-refusals.test.ts +2 -2
- package/src/postgres/database-sql.ts +40 -12
- package/src/postgres/database.ts +3 -2
- package/src/postgres/fake-grants-parse.ts +189 -0
- package/src/postgres/fake-grants-read.ts +250 -0
- package/src/postgres/fake-grants-sql.ts +207 -0
- package/src/postgres/fake-grants-write.ts +90 -0
- package/src/postgres/fake-role-sql.ts +220 -0
- package/src/postgres/fake-schema-semantics.test.ts +36 -0
- package/src/postgres/fake-schema-sql.ts +146 -0
- package/src/postgres/fake-sql-parse.ts +86 -0
- package/src/postgres/fake-sql-quote.ts +36 -0
- package/src/postgres/fake-sql.ts +161 -46
- package/src/postgres/fixtures/pg-REL_18_6-acl.h.txt +147 -0
- package/src/postgres/fixtures/pg-REL_18_6-aclexplode-func.txt +31 -0
- package/src/postgres/fixtures/pg-REL_18_6-alter-default-privileges-synopsis.txt +82 -0
- package/src/postgres/fixtures/pg-REL_18_6-alter_role.sgml.txt +44 -0
- package/src/postgres/fixtures/pg-REL_18_6-create_role.sgml.txt +26 -0
- package/src/postgres/fixtures/pg-REL_18_6-grant-synopsis.txt +95 -0
- package/src/postgres/fixtures/pg-REL_18_6-pg_attribute.h.txt +24 -0
- package/src/postgres/fixtures/pg-REL_18_6-pg_auth_members.h.txt +62 -0
- package/src/postgres/fixtures/pg-REL_18_6-pg_authid.h.txt +70 -0
- package/src/postgres/fixtures/pg-REL_18_6-pg_class.h.txt +33 -0
- package/src/postgres/fixtures/pg-REL_18_6-pg_default_acl.h.txt +81 -0
- package/src/postgres/fixtures/pg-REL_18_6-pg_namespace.h.txt +28 -0
- package/src/postgres/fixtures/pg-REL_18_6-revoke-synopsis.txt +124 -0
- package/src/postgres/grants-acl.test.ts +61 -0
- package/src/postgres/grants-attrs.ts +185 -0
- package/src/postgres/grants-database.test.ts +158 -0
- package/src/postgres/grants-declare.ts +172 -0
- package/src/postgres/grants-delete-proof.test.ts +154 -0
- package/src/postgres/grants-diff.ts +101 -0
- package/src/postgres/grants-errors.ts +215 -0
- package/src/postgres/grants-existence-guards.test.ts +113 -0
- package/src/postgres/grants-existence.ts +101 -0
- package/src/postgres/grants-lifecycle-guards.test.ts +208 -0
- package/src/postgres/grants-lifecycle-repair.test.ts +208 -0
- package/src/postgres/grants-lifecycle.test.ts +181 -0
- package/src/postgres/grants-ops.ts +229 -0
- package/src/postgres/grants-owner-revoke.test.ts +121 -0
- package/src/postgres/grants-ownership.ts +49 -0
- package/src/postgres/grants-plan-collateral.test.ts +202 -0
- package/src/postgres/grants-plan-diff.test.ts +204 -0
- package/src/postgres/grants-plan-restore.ts +43 -0
- package/src/postgres/grants-plan-revocations.test.ts +69 -0
- package/src/postgres/grants-plan.test.ts +148 -0
- package/src/postgres/grants-plan.ts +245 -0
- package/src/postgres/grants-provenance.test.ts +134 -0
- package/src/postgres/grants-psql-lifecycle.test.ts +105 -0
- package/src/postgres/grants-read-absent.test.ts +91 -0
- package/src/postgres/grants-read-defaults.ts +51 -0
- package/src/postgres/grants-read.ts +250 -0
- package/src/postgres/grants-refusals.test.ts +136 -0
- package/src/postgres/grants-refuse.ts +119 -0
- package/src/postgres/grants-relkind-wire.test.ts +111 -0
- package/src/postgres/grants-repair-plan.test.ts +201 -0
- package/src/postgres/grants-sql.test.ts +128 -0
- package/src/postgres/grants-sql.ts +144 -0
- package/src/postgres/grants-words.ts +34 -0
- package/src/postgres/grants.ts +110 -0
- package/src/postgres/index.ts +137 -7
- package/src/postgres/installed-classifier.ts +36 -0
- package/src/postgres/providers.ts +44 -7
- package/src/postgres/psql-executor-escaping.test.ts +67 -0
- package/src/postgres/psql-executor.test.ts +167 -0
- package/src/postgres/psql-executor.ts +170 -0
- package/src/postgres/role-adopt.test.ts +98 -0
- package/src/postgres/role-attrs.ts +134 -0
- package/src/postgres/role-diff.ts +89 -0
- package/src/postgres/role-errors.ts +198 -0
- package/src/postgres/role-guard.test.ts +60 -0
- package/src/postgres/role-identity.test.ts +185 -0
- package/src/postgres/role-lifecycle.test.ts +228 -0
- package/src/postgres/role-membership-sql.ts +198 -0
- package/src/postgres/role-password.test.ts +158 -0
- package/src/postgres/role-privilege.test.ts +80 -0
- package/src/postgres/role-provenance.test.ts +200 -0
- package/src/postgres/role-provider.ts +104 -0
- package/src/postgres/role-refusals.test.ts +250 -0
- package/src/postgres/role-scram.test.ts +38 -0
- package/src/postgres/role-scram.ts +38 -0
- package/src/postgres/role-secrets.ts +46 -0
- package/src/postgres/role-sql.ts +211 -0
- package/src/postgres/role-valid-until.test.ts +68 -0
- package/src/postgres/role.ts +218 -0
- package/src/postgres/schema-assert.ts +139 -0
- package/src/postgres/schema-attrs.ts +71 -0
- package/src/postgres/schema-delete-proofs.test.ts +192 -0
- package/src/postgres/schema-delete.ts +96 -0
- package/src/postgres/schema-diff.ts +44 -0
- package/src/postgres/schema-drop-2bp01.test.ts +146 -0
- package/src/postgres/schema-errors.ts +209 -0
- package/src/postgres/schema-handlers.test.ts +193 -0
- package/src/postgres/schema-identity.ts +39 -0
- package/src/postgres/schema-lifecycle.test.ts +246 -0
- package/src/postgres/schema-race.test.ts +70 -0
- package/src/postgres/schema-reconcile.ts +100 -0
- package/src/postgres/schema-refusals.test.ts +144 -0
- package/src/postgres/schema-socket.test.ts +161 -0
- package/src/postgres/schema-sql.ts +130 -0
- package/src/postgres/schema-takeover.test.ts +149 -0
- package/src/postgres/schema-test-kit.ts +85 -0
- package/src/postgres/schema.ts +130 -0
- package/src/proxmox/ceph-flag-types.ts +70 -0
- package/src/proxmox/ceph-flag-wire.test.ts +72 -0
- package/src/proxmox/ceph-flag-wire.ts +123 -0
- package/src/proxmox/ceph-flag.ts +77 -130
- package/src/proxmox/ceph-osd-write.ts +10 -13
- package/src/release/binary-alias.test.ts +3 -3
- package/src/release/binary-claims.test.ts +3 -3
- package/src/release/binary-directory-umask.test.ts +65 -0
- package/src/release/fake-engine.ts +15 -0
- package/src/valkey/acl-attrs.ts +98 -0
- package/src/valkey/acl-compare.ts +57 -0
- package/src/valkey/acl-connection.ts +13 -0
- package/src/valkey/acl-delete.test.ts +127 -0
- package/src/valkey/acl-form.ts +239 -0
- package/src/valkey/acl-managed.ts +13 -0
- package/src/valkey/acl-ops.ts +122 -0
- package/src/valkey/acl-ownership.test.ts +118 -0
- package/src/valkey/acl-prefix.test.ts +38 -0
- package/src/valkey/acl-profiles.ts +81 -0
- package/src/valkey/acl-reconcile.ts +180 -0
- package/src/valkey/acl-reserved.test.ts +69 -0
- package/src/valkey/acl-revocation.test.ts +94 -0
- package/src/valkey/acl-seat-prefix.test.ts +111 -0
- package/src/valkey/acl-validation.test.ts +54 -0
- package/src/valkey/acl.test.ts +215 -0
- package/src/valkey/acl.ts +169 -0
- package/src/valkey/connection-service.ts +46 -0
- package/src/valkey/connection.ts +180 -0
- package/src/valkey/errors.ts +239 -0
- package/src/valkey/fake-acl.ts +128 -0
- package/src/valkey/fake-valkey.ts +224 -0
- package/src/valkey/handler-test-fixture.ts +43 -0
- package/src/valkey/handlers.test.ts +205 -0
- package/src/valkey/index.ts +54 -0
- package/src/valkey/instance-attrs.ts +48 -0
- package/src/valkey/instance-form.ts +104 -0
- package/src/valkey/instance.test.ts +108 -0
- package/src/valkey/instance.ts +160 -0
- package/src/valkey/provider-socket-fixture.ts +71 -0
- package/src/valkey/providers.test.ts +129 -0
- package/src/valkey/providers.ts +21 -0
- package/src/valkey/resp.ts +212 -0
- package/src/valkey/transport-bounds.test.ts +150 -0
- package/src/valkey/transport-error.ts +37 -0
- package/src/valkey/transport.test.ts +158 -0
- package/src/valkey/transport.ts +109 -0
- package/src/valkey/upstream-bounds.test.ts +14 -0
|
@@ -0,0 +1,250 @@
|
|
|
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('ADMIN or SET on the live read answers update; stored membership rows do not', async () => {
|
|
103
|
+
const declared = { ...base, memberOf: ['hf_agent'] as const };
|
|
104
|
+
const stored = { ...output, memberOf: ['hf_agent'] as const };
|
|
105
|
+
const row = (admin: boolean, set: boolean) => ({
|
|
106
|
+
found: {
|
|
107
|
+
...stored,
|
|
108
|
+
memberships: [{ parent: 'hf_agent', grantor: 'postgres', admin, set }],
|
|
109
|
+
},
|
|
110
|
+
});
|
|
111
|
+
expect(await run(diffPostgresRole(declared, stored, {}, row(true, true)))).toEqual({
|
|
112
|
+
action: 'update',
|
|
113
|
+
});
|
|
114
|
+
expect(await run(diffPostgresRole(declared, stored, {}, row(false, true)))).toEqual({
|
|
115
|
+
action: 'update',
|
|
116
|
+
});
|
|
117
|
+
expect(await run(diffPostgresRole(declared, stored, {}, row(false, false)))).toEqual({
|
|
118
|
+
action: 'noop',
|
|
119
|
+
});
|
|
120
|
+
const stuffed = {
|
|
121
|
+
...stored,
|
|
122
|
+
memberships: [{ parent: 'hf_agent', grantor: 'postgres', admin: true, set: true }],
|
|
123
|
+
};
|
|
124
|
+
expect(await run(diffPostgresRole(declared, stuffed))).toEqual({ action: 'noop' });
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
test('a rotated password answers update even with no other drift; a matching or unreadable one does not', async () => {
|
|
128
|
+
const declared = { ...base, password: { fromEnv: 'PG_SEAT_ROLE_PASSWORD' } as const };
|
|
129
|
+
const stale = seal({ password: 'old' }, 'fixed-salt');
|
|
130
|
+
const matched = seal({ password: 'current' }, 'fixed-salt');
|
|
131
|
+
const held = { PG_SEAT_ROLE_PASSWORD: 'current' };
|
|
132
|
+
// A value the seal does not match — rotated, or an adopted role's empty seal — answers update.
|
|
133
|
+
expect(await run(diffPostgresRole(declared, { ...output, passwordSeal: stale }, held))).toEqual(
|
|
134
|
+
{ action: 'update' },
|
|
135
|
+
);
|
|
136
|
+
expect(await run(diffPostgresRole(declared, output, held))).toEqual({ action: 'update' });
|
|
137
|
+
// Still matching: noop. The variable unset here: never stale — a plan that cannot read the
|
|
138
|
+
// secret never churns it.
|
|
139
|
+
expect(
|
|
140
|
+
await run(diffPostgresRole(declared, { ...output, passwordSeal: matched }, held)),
|
|
141
|
+
).toEqual({ action: 'noop' });
|
|
142
|
+
expect(await run(diffPostgresRole(declared, { ...output, passwordSeal: stale }, {}))).toEqual({
|
|
143
|
+
action: 'noop',
|
|
144
|
+
});
|
|
145
|
+
});
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
describe('scalarDrift', () => {
|
|
149
|
+
test('lists one change per drifted field, in the family\u2019s compared order', () => {
|
|
150
|
+
expect(
|
|
151
|
+
scalarDrift(
|
|
152
|
+
{
|
|
153
|
+
...base,
|
|
154
|
+
login: true,
|
|
155
|
+
inherit: false,
|
|
156
|
+
connectionLimit: 5,
|
|
157
|
+
validUntil: '2027-01-01T00:00:00Z',
|
|
158
|
+
},
|
|
159
|
+
{ ...output, login: false, inherit: true, connectionLimit: 10 },
|
|
160
|
+
),
|
|
161
|
+
).toEqual([
|
|
162
|
+
{ prop: 'login', value: true },
|
|
163
|
+
{ prop: 'inherit', value: false },
|
|
164
|
+
{ prop: 'connectionLimit', value: 5 },
|
|
165
|
+
{ prop: 'validUntil', value: '2027-01-01T00:00:00Z' },
|
|
166
|
+
]);
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
test('is empty when the live row matches, including the milliseconds case', () => {
|
|
170
|
+
expect(scalarDrift(base, output)).toEqual([]);
|
|
171
|
+
expect(
|
|
172
|
+
scalarDrift(
|
|
173
|
+
{ ...base, validUntil: '2027-01-01T00:00:00Z' },
|
|
174
|
+
{
|
|
175
|
+
...output,
|
|
176
|
+
validUntil: '2027-01-01T00:00:00.000Z',
|
|
177
|
+
},
|
|
178
|
+
),
|
|
179
|
+
).toEqual([]);
|
|
180
|
+
});
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
describe('membershipDrift', () => {
|
|
184
|
+
test('undefined asserts nothing; duplicates collapse; grants and revokes are sorted set differences', () => {
|
|
185
|
+
expect(membershipDrift(undefined, ['hf_agent'])).toEqual({ grants: [], revokes: [] });
|
|
186
|
+
expect(membershipDrift(['hf_agent', 'hf_agent', 'seat-b'], [])).toEqual({
|
|
187
|
+
grants: ['hf_agent', 'seat-b'],
|
|
188
|
+
revokes: [],
|
|
189
|
+
});
|
|
190
|
+
expect(membershipDrift(['hf_agent'], ['hf_agent', 'seat-b', 'seat-a'])).toEqual({
|
|
191
|
+
grants: [],
|
|
192
|
+
revokes: ['seat-a', 'seat-b'],
|
|
193
|
+
});
|
|
194
|
+
});
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
describe('quoting', () => {
|
|
198
|
+
test('a hostile role name is doubled in every statement that quotes it', () => {
|
|
199
|
+
expect(buildCreateRoleSql({ ...base, name: 'a"b' })).toBe(
|
|
200
|
+
'CREATE ROLE "a""b" WITH NOLOGIN INHERIT CONNECTION LIMIT 10',
|
|
201
|
+
);
|
|
202
|
+
expect(buildAlterRoleSql('a"b', { prop: 'connectionLimit', value: 3 })).toBe(
|
|
203
|
+
'ALTER ROLE "a""b" WITH CONNECTION LIMIT 3',
|
|
204
|
+
);
|
|
205
|
+
expect(buildGrantMembershipSql('m"1', 'p"2')).toBe('GRANT "p""2" TO "m""1" WITH SET FALSE');
|
|
206
|
+
expect(buildRevokeGrantorMembershipSql('m"1', 'p"2', 'g"3')).toBe(
|
|
207
|
+
'REVOKE "p""2" FROM "m""1" GRANTED BY "g""3"',
|
|
208
|
+
);
|
|
209
|
+
expect(buildDropRoleSql('a"b')).toBe('DROP ROLE IF EXISTS "a""b"');
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
test('the value buildSetPasswordSql carries is single-quote escaped exactly once', () => {
|
|
213
|
+
// In production that value is a SCRAM verifier (`role-scram.ts`), which the base64 alphabet
|
|
214
|
+
// keeps free of `'`; the quoting property still holds for any string handed to the builder.
|
|
215
|
+
expect(buildSetPasswordSql('seat-observability', "O'Brien; DROP")).toBe(
|
|
216
|
+
"ALTER ROLE \"seat-observability\" WITH PASSWORD E'O''Brien; DROP'",
|
|
217
|
+
);
|
|
218
|
+
});
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
describe('write-only password shape (S25)', () => {
|
|
222
|
+
const props = { ...base, password: { fromEnv: 'PG_SEAT_ROLE_PASSWORD' } };
|
|
223
|
+
|
|
224
|
+
test('resolvePassword holds the value as Redacted, which never prints or serialises it', () => {
|
|
225
|
+
const { variable, value } = resolvePassword(props, { PG_SEAT_ROLE_PASSWORD: 's3cret-value' });
|
|
226
|
+
expect(variable).toBe('PG_SEAT_ROLE_PASSWORD');
|
|
227
|
+
expect(String(value)).toBe('<redacted>');
|
|
228
|
+
expect(JSON.stringify({ value })).not.toContain('s3cret-value');
|
|
229
|
+
expect(Redacted.isRedacted(value)).toBe(true);
|
|
230
|
+
if (Redacted.isRedacted(value)) {
|
|
231
|
+
expect(Redacted.value(value)).toBe('s3cret-value');
|
|
232
|
+
}
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
test('resolvePassword answers no value when the variable is unset or empty', () => {
|
|
236
|
+
expect(resolvePassword(props, {}).value).toBeUndefined();
|
|
237
|
+
expect(resolvePassword(props, { PG_SEAT_ROLE_PASSWORD: '' }).value).toBeUndefined();
|
|
238
|
+
expect(resolvePassword(base, { PG_SEAT_ROLE_PASSWORD: 's3cret-value' })).toEqual({
|
|
239
|
+
variable: undefined,
|
|
240
|
+
value: undefined,
|
|
241
|
+
});
|
|
242
|
+
});
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
describe('removal policy', () => {
|
|
246
|
+
test('defaultRemovalPolicy is retain (checked in source, declared alongside PostgresRole)', () => {
|
|
247
|
+
const source = readFileSync(new URL('./role.ts', import.meta.url), 'utf8');
|
|
248
|
+
expect(source).toContain("defaultRemovalPolicy: 'retain'");
|
|
249
|
+
});
|
|
250
|
+
});
|
|
@@ -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,38 @@
|
|
|
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
|
+
*/
|
|
32
|
+
export const scramSha256Verifier = (password: string, salt?: Buffer): string => {
|
|
33
|
+
const bytes = salt ?? randomBytes(SALT_BYTES);
|
|
34
|
+
const saltedPassword = pbkdf2Sync(password, bytes, ITERATIONS, KEY_BYTES, 'sha256');
|
|
35
|
+
const storedKey = createHash('sha256').update(hmac(saltedPassword, 'Client Key')).digest();
|
|
36
|
+
const serverKey = hmac(saltedPassword, 'Server Key');
|
|
37
|
+
return `SCRAM-SHA-256$${ITERATIONS}:${b64(bytes)}$${b64(storedKey)}:${b64(serverKey)}`;
|
|
38
|
+
};
|
|
@@ -0,0 +1,46 @@
|
|
|
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 { type Environment, resolveAll, seal, sealMatches } from '../secrets/write-only.ts';
|
|
15
|
+
import type { PostgresRoleProps } from './role-attrs.ts';
|
|
16
|
+
|
|
17
|
+
/** What the environment holds for the declared password. `value` is `undefined` when no
|
|
18
|
+
* password is declared, or when the variable is unset or empty (a renderer that failed writes
|
|
19
|
+
* `NAME=`; `resolveAll` treats empty as missing). ⛔ Reads the environment at CALL time. */
|
|
20
|
+
export interface ResolvedPassword {
|
|
21
|
+
readonly variable: string | undefined;
|
|
22
|
+
readonly value: Redacted.Redacted<string> | undefined;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export const resolvePassword = (
|
|
26
|
+
props: PostgresRoleProps,
|
|
27
|
+
env: Environment = process.env,
|
|
28
|
+
): ResolvedPassword => {
|
|
29
|
+
if (props.password === undefined) return { variable: undefined, value: undefined };
|
|
30
|
+
const { values } = resolveAll({ password: props.password }, env);
|
|
31
|
+
const raw = values['password'];
|
|
32
|
+
return {
|
|
33
|
+
variable: props.password.fromEnv,
|
|
34
|
+
value: raw === undefined ? undefined : Redacted.make(raw),
|
|
35
|
+
};
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
/** `seal({ password })` of the resolved value — the one thing state remembers about it. */
|
|
39
|
+
export const sealPassword = (value: Redacted.Redacted<string>): string =>
|
|
40
|
+
seal({ password: Redacted.value(value) });
|
|
41
|
+
|
|
42
|
+
/** Whether the seal was made from the resolved value. Malformed/empty seals answer false, so an
|
|
43
|
+
* adopted role (seal `''`) or one written outside this stack gets the password written on the
|
|
44
|
+
* first reconcile that holds the variable — the declaration is authoritative. */
|
|
45
|
+
export const passwordMatchesSeal = (value: Redacted.Redacted<string>, sealed: string): boolean =>
|
|
46
|
+
sealMatches(sealed, { password: Redacted.value(value) });
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The statements `Postgres.Role` ever issues, and the one read that backs them.
|
|
3
|
+
*
|
|
4
|
+
* ⛔ `CREATE ROLE` / `ALTER ROLE` TAKE NO BIND PARAMETERS FOR ANY OF THEIR VALUES. Measured at
|
|
5
|
+
* `gram.y@REL_18_6`: every option value in `CreateOptRoleStmt` is a `RoleSpec`, a signed
|
|
6
|
+
* `Iconst` or an `Sconst`, and none of those productions reaches `PARAM` (`$n`). Only the
|
|
7
|
+
* catalog reads below bind a role name (`$1`).
|
|
8
|
+
* ⛔ THE PASSWORD NEVER CROSSES THE WIRE AS A PLAIN LITERAL. Postgres has no bind form for it in
|
|
9
|
+
* `CREATE`/`ALTER ROLE`, so the statement carries the SCRAM-SHA-256 verifier `role-scram.ts`
|
|
10
|
+
* computes client-side (what `psql \password` sends; stored as-is, never reversible): the
|
|
11
|
+
* statement text that reaches the span attribute `db.query.text`, `pg_stat_statements` and a
|
|
12
|
+
* failed `ALTER`'s server log holds no secret. It is quoted with `quoteStringLiteral` like every
|
|
13
|
+
* other string value and NEVER written to state — only its seal is (`role-attrs.ts`,
|
|
14
|
+
* `passwordSeal`).
|
|
15
|
+
* ★ MEMBERSHIP IS TWO ONE-PARENT STATEMENTS (`GRANT` / `REVOKE`), never a multi-role list, so a
|
|
16
|
+
* partially-failed membership write is observable in the state that remains. Those statements
|
|
17
|
+
* and the `pg_auth_members` read live in `role-membership-sql.ts` — this file stays the role
|
|
18
|
+
* row (`pg_roles`), its scalar statements and the compare between them.
|
|
19
|
+
*/
|
|
20
|
+
import type { SqlError } from 'effect/unstable/sql/SqlError';
|
|
21
|
+
import * as Effect from 'effect/Effect';
|
|
22
|
+
import type { PgExecutor } from './database-sql.ts';
|
|
23
|
+
import { quoteIdent, quoteStringLiteral } from './database-sql.ts';
|
|
24
|
+
import { buildGrantMembershipSql, selectRoleMemberships } from './role-membership-sql.ts';
|
|
25
|
+
import {
|
|
26
|
+
type PostgresRoleAttributes,
|
|
27
|
+
type PostgresRoleProps,
|
|
28
|
+
sameValidUntil,
|
|
29
|
+
} from './role-attrs.ts';
|
|
30
|
+
|
|
31
|
+
/** The one read: the row from the `pg_roles` view, plus this role's memberships from
|
|
32
|
+
* `pg_auth_members`. `rolvaliduntil` is a `timestamptz`; it is serialised on the server side
|
|
33
|
+
* (`to_char(rolvaliduntil AT TIME ZONE 'UTC', …)`) so the value that reaches state is a plain
|
|
34
|
+
* ISO-8601 string, never a driver-specific Date that `encodeState` would persist differently
|
|
35
|
+
* across transports (the socket client decodes it as a JS Date; the runner transport would hand
|
|
36
|
+
* back a string). */
|
|
37
|
+
const SELECT_ROLE_SQL = `SELECT
|
|
38
|
+
r.oid AS oid,
|
|
39
|
+
r.rolname AS name,
|
|
40
|
+
r.rolcanlogin AS login,
|
|
41
|
+
r.rolconnlimit AS "connectionLimit",
|
|
42
|
+
r.rolinherit AS inherit,
|
|
43
|
+
r.rolsuper AS superuser,
|
|
44
|
+
r.rolcreaterole AS createrole,
|
|
45
|
+
r.rolcreatedb AS createdb,
|
|
46
|
+
r.rolreplication AS replication,
|
|
47
|
+
r.rolbypassrls AS bypassrls,
|
|
48
|
+
to_char(r.rolvaliduntil AT TIME ZONE 'UTC', 'YYYY-MM-DD"T"HH24:MI:SS.MS"Z"') AS "validUntil"
|
|
49
|
+
FROM pg_roles r
|
|
50
|
+
WHERE r.rolname = $1`;
|
|
51
|
+
|
|
52
|
+
export const selectRole = (
|
|
53
|
+
pg: PgExecutor,
|
|
54
|
+
name: string,
|
|
55
|
+
): Effect.Effect<Omit<PostgresRoleAttributes, 'memberOf' | 'passwordSeal'> | undefined, SqlError> =>
|
|
56
|
+
Effect.map(
|
|
57
|
+
pg.unsafe<Omit<PostgresRoleAttributes, 'memberOf' | 'passwordSeal'>>(SELECT_ROLE_SQL, [name]),
|
|
58
|
+
(rows) => rows[0],
|
|
59
|
+
);
|
|
60
|
+
|
|
61
|
+
/** One combined read used by reconcile, so a no-op reconcile issues exactly one round trip.
|
|
62
|
+
* `passwordSeal` is state-only — `pg_roles` answers no password (S25) — so the caller merges it. */
|
|
63
|
+
export const readRoleWithClient = (
|
|
64
|
+
pg: PgExecutor,
|
|
65
|
+
name: string,
|
|
66
|
+
): Effect.Effect<Omit<PostgresRoleAttributes, 'passwordSeal'> | undefined, SqlError> =>
|
|
67
|
+
Effect.gen(function* () {
|
|
68
|
+
const row = yield* selectRole(pg, name);
|
|
69
|
+
if (row === undefined) return undefined;
|
|
70
|
+
const memberships = yield* selectRoleMemberships(pg, name);
|
|
71
|
+
// Two grantors of one parent are two catalog rows. `memberOf` is the set of names.
|
|
72
|
+
return {
|
|
73
|
+
...row,
|
|
74
|
+
memberOf: [...new Set(memberships.map((membership) => membership.parent))].sort(),
|
|
75
|
+
memberships,
|
|
76
|
+
};
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
/** The `WITH` options a create carries, in one place so `diff` and the fake agree on the text.
|
|
80
|
+
* A create carries no PASSWORD clause — the password travels through
|
|
81
|
+
* {@link buildSetPasswordSql} as its SCRAM verifier, so the one quoting path is exercised exactly
|
|
82
|
+
* once and a `CREATE ROLE` never carries a credential in its log line. */
|
|
83
|
+
const withOptions = (props: PostgresRoleProps): string => {
|
|
84
|
+
const parts: string[] = [];
|
|
85
|
+
parts.push(props.login ? 'LOGIN' : 'NOLOGIN');
|
|
86
|
+
parts.push(props.inherit ? 'INHERIT' : 'NOINHERIT');
|
|
87
|
+
parts.push(`CONNECTION LIMIT ${String(Math.trunc(props.connectionLimit))}`);
|
|
88
|
+
if (props.validUntil !== undefined) {
|
|
89
|
+
parts.push(`VALID UNTIL ${quoteStringLiteral(props.validUntil)}`);
|
|
90
|
+
}
|
|
91
|
+
return parts.join(' ');
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
export const buildCreateRoleSql = (props: PostgresRoleProps): string =>
|
|
95
|
+
`CREATE ROLE ${quoteIdent(props.name)} WITH ${withOptions(props)}`;
|
|
96
|
+
|
|
97
|
+
/** One `ALTER ROLE` per drifted scalar. Membership and password are separate statements. */
|
|
98
|
+
export const buildAlterRoleSql = (
|
|
99
|
+
name: string,
|
|
100
|
+
change:
|
|
101
|
+
| { prop: 'login'; value: boolean }
|
|
102
|
+
| { prop: 'inherit'; value: boolean }
|
|
103
|
+
| {
|
|
104
|
+
prop: 'connectionLimit';
|
|
105
|
+
value: number;
|
|
106
|
+
}
|
|
107
|
+
| { prop: 'validUntil'; value: string },
|
|
108
|
+
): string => {
|
|
109
|
+
const who = quoteIdent(name);
|
|
110
|
+
switch (change.prop) {
|
|
111
|
+
case 'login':
|
|
112
|
+
return `ALTER ROLE ${who} WITH ${change.value ? 'LOGIN' : 'NOLOGIN'}`;
|
|
113
|
+
case 'inherit':
|
|
114
|
+
return `ALTER ROLE ${who} WITH ${change.value ? 'INHERIT' : 'NOINHERIT'}`;
|
|
115
|
+
case 'connectionLimit':
|
|
116
|
+
return `ALTER ROLE ${who} WITH CONNECTION LIMIT ${String(Math.trunc(change.value))}`;
|
|
117
|
+
case 'validUntil':
|
|
118
|
+
return `ALTER ROLE ${who} WITH VALID UNTIL ${quoteStringLiteral(change.value)}`;
|
|
119
|
+
}
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
/** One dedicated `ALTER ROLE … PASSWORD`. It carries the SCRAM-SHA-256 verifier
|
|
123
|
+
* `role-scram.ts` computes, never the plain password: Postgres takes no bind parameter for a role
|
|
124
|
+
* password, but a string already in SCRAM verifier format is stored as-is (`create_role.sgml`),
|
|
125
|
+
* so the statement text that reaches the span, `pg_stat_statements` and a failed `ALTER`'s server
|
|
126
|
+
* log holds no secret. */
|
|
127
|
+
export const buildSetPasswordSql = (name: string, verifier: string): string =>
|
|
128
|
+
`ALTER ROLE ${quoteIdent(name)} WITH PASSWORD ${quoteStringLiteral(verifier)}`;
|
|
129
|
+
|
|
130
|
+
export const buildDropRoleSql = (name: string): string => `DROP ROLE IF EXISTS ${quoteIdent(name)}`;
|
|
131
|
+
|
|
132
|
+
/** `CREATE`, the optional password `ALTER`, and one seat `GRANT` per declared parent, in the
|
|
133
|
+
* order a single transaction runs them. Grants are sorted so the script is stable. */
|
|
134
|
+
export const buildCreateStatements = (
|
|
135
|
+
props: PostgresRoleProps,
|
|
136
|
+
verifier: string | undefined,
|
|
137
|
+
): readonly string[] => [
|
|
138
|
+
buildCreateRoleSql(props),
|
|
139
|
+
...(verifier === undefined ? [] : [buildSetPasswordSql(props.name, verifier)]),
|
|
140
|
+
...[...new Set(props.memberOf ?? [])]
|
|
141
|
+
.sort()
|
|
142
|
+
.map((parent) => buildGrantMembershipSql(props.name, parent)),
|
|
143
|
+
];
|
|
144
|
+
|
|
145
|
+
/** Compare every declared scalar prop against the live row; answer the list of statements
|
|
146
|
+
* reconcile needs to run, empty when nothing drifted. `name` is compared by the caller. */
|
|
147
|
+
export const scalarDrift = (
|
|
148
|
+
props: PostgresRoleProps,
|
|
149
|
+
live: Omit<PostgresRoleAttributes, 'passwordSeal'>,
|
|
150
|
+
): ReadonlyArray<Parameters<typeof buildAlterRoleSql>[1]> => {
|
|
151
|
+
const changes: Parameters<typeof buildAlterRoleSql>[1][] = [];
|
|
152
|
+
if (props.login !== live.login) changes.push({ prop: 'login', value: props.login });
|
|
153
|
+
if (props.inherit !== live.inherit) changes.push({ prop: 'inherit', value: props.inherit });
|
|
154
|
+
if (props.connectionLimit !== live.connectionLimit) {
|
|
155
|
+
changes.push({ prop: 'connectionLimit', value: props.connectionLimit });
|
|
156
|
+
}
|
|
157
|
+
// Instant comparison, never string equality: the server serialises `rolvaliduntil` with
|
|
158
|
+
// milliseconds, so `'2027-01-01T00:00:00Z'` (declared) and `'2027-01-01T00:00:00.000Z'`
|
|
159
|
+
// (read back) are the same value and must not churn an `ALTER` on every plan.
|
|
160
|
+
if (props.validUntil !== undefined && !sameValidUntil(props.validUntil, live.validUntil)) {
|
|
161
|
+
changes.push({ prop: 'validUntil', value: props.validUntil });
|
|
162
|
+
}
|
|
163
|
+
return changes;
|
|
164
|
+
};
|
|
165
|
+
|
|
166
|
+
/** Whether a live role is the one an interrupted create would have left: every declared scalar,
|
|
167
|
+
* the declared parent set, and no privilege flag or unsafe membership option. The password is
|
|
168
|
+
* not in `pg_roles`, so it is not part of the proof. */
|
|
169
|
+
export const declarationMatches = (
|
|
170
|
+
props: PostgresRoleProps,
|
|
171
|
+
live: Omit<PostgresRoleAttributes, 'passwordSeal'>,
|
|
172
|
+
): boolean => {
|
|
173
|
+
const wanted =
|
|
174
|
+
props.memberOf === undefined ? undefined : [...new Set(props.memberOf)].sort().join('\0');
|
|
175
|
+
const found = [...(live.memberOf ?? [])].sort().join('\0');
|
|
176
|
+
return (
|
|
177
|
+
props.name === live.name &&
|
|
178
|
+
props.login === live.login &&
|
|
179
|
+
props.inherit === live.inherit &&
|
|
180
|
+
props.connectionLimit === live.connectionLimit &&
|
|
181
|
+
(props.validUntil === undefined || sameValidUntil(props.validUntil, live.validUntil)) &&
|
|
182
|
+
(wanted === undefined || wanted === found) &&
|
|
183
|
+
privilegedFlags(live).length === 0 &&
|
|
184
|
+
(live.memberships ?? []).every((row) => !row.admin && !row.set)
|
|
185
|
+
);
|
|
186
|
+
};
|
|
187
|
+
|
|
188
|
+
/** Drop the observe-only membership options before attributes reach state. Alchemy's plan diff
|
|
189
|
+
* receives the stored attributes (`Plan.ts` passes `oldState.attr`), so an `admin`/`set` row
|
|
190
|
+
* kept here would still not be what `diff` sees — the provider reads `pg_auth_members` live.
|
|
191
|
+
* What state stores is the parent-name set. */
|
|
192
|
+
export const storedAttributes = (
|
|
193
|
+
live: Omit<PostgresRoleAttributes, 'passwordSeal'>,
|
|
194
|
+
): Omit<PostgresRoleAttributes, 'passwordSeal'> => {
|
|
195
|
+
const { memberships: _observeOnly, ...stored } = live;
|
|
196
|
+
return stored;
|
|
197
|
+
};
|
|
198
|
+
|
|
199
|
+
/** Catalog flags this family never declares. A create lands on the server default (all false);
|
|
200
|
+
* an absent field on a seeded test row is that default. */
|
|
201
|
+
export const privilegedFlags = (
|
|
202
|
+
live: Omit<PostgresRoleAttributes, 'passwordSeal'>,
|
|
203
|
+
): readonly string[] => {
|
|
204
|
+
const flags: string[] = [];
|
|
205
|
+
if (live.superuser === true) flags.push('SUPERUSER');
|
|
206
|
+
if (live.createrole === true) flags.push('CREATEROLE');
|
|
207
|
+
if (live.createdb === true) flags.push('CREATEDB');
|
|
208
|
+
if (live.replication === true) flags.push('REPLICATION');
|
|
209
|
+
if (live.bypassrls === true) flags.push('BYPASSRLS');
|
|
210
|
+
return flags;
|
|
211
|
+
};
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Plan-time `validUntil` refusals and the instant compare, extracted from
|
|
3
|
+
* `role-refusals.test.ts` so that file stays under the house 250-line cap.
|
|
4
|
+
*
|
|
5
|
+
* A zone suffix is not a timestamp. `2027-13-01T00:00:00Z` and `not-a-dateZ` match the offset
|
|
6
|
+
* regex and `Date.parse` to NaN; a plan that accepts them sends a `VALID UNTIL` PostgreSQL
|
|
7
|
+
* rejects, or one that never compares equal on read-back, so every later reconcile re-issues
|
|
8
|
+
* the same `ALTER`.
|
|
9
|
+
*/
|
|
10
|
+
import { describe, expect, test } from 'bun:test';
|
|
11
|
+
import * as Effect from 'effect/Effect';
|
|
12
|
+
import { sameValidUntil, validUntilRefusal } from './role-attrs.ts';
|
|
13
|
+
import type { PostgresRoleAttributes, PostgresRoleProps } from './role-attrs.ts';
|
|
14
|
+
import { PostgresRoleValidUntilRefused } from './role-errors.ts';
|
|
15
|
+
import { diffPostgresRole } from './role-diff.ts';
|
|
16
|
+
import { refuseAtPlan } from './role.ts';
|
|
17
|
+
|
|
18
|
+
const fails = <A, E>(eff: Effect.Effect<A, E>): Promise<E> => Effect.runPromise(Effect.flip(eff));
|
|
19
|
+
|
|
20
|
+
const base: PostgresRoleProps = {
|
|
21
|
+
name: 'seat-observability',
|
|
22
|
+
login: false,
|
|
23
|
+
connectionLimit: 10,
|
|
24
|
+
inherit: true,
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
const output: PostgresRoleAttributes = {
|
|
28
|
+
name: 'seat-observability',
|
|
29
|
+
oid: 20000,
|
|
30
|
+
login: false,
|
|
31
|
+
connectionLimit: 10,
|
|
32
|
+
inherit: true,
|
|
33
|
+
validUntil: null,
|
|
34
|
+
memberOf: [],
|
|
35
|
+
passwordSeal: '',
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
describe('validUntil', () => {
|
|
39
|
+
test('a zone-carrying value passes; a zone-free or unparseable one is refused with its reason', () => {
|
|
40
|
+
expect(validUntilRefusal('2027-01-01T00:00:00Z')).toBeUndefined();
|
|
41
|
+
expect(validUntilRefusal('2027-01-01T02:00:00+02:00')).toBeUndefined();
|
|
42
|
+
expect(validUntilRefusal('2027-01-01T00:00:00')).toEqual({ reason: 'zone-free' });
|
|
43
|
+
expect(validUntilRefusal('2027-01-01')).toEqual({ reason: 'zone-free' });
|
|
44
|
+
expect(validUntilRefusal('not a timestamp')).toEqual({ reason: 'unparseable' });
|
|
45
|
+
// A zone suffix is not enough: Date.parse of these is NaN, and a plan that accepts them
|
|
46
|
+
// sends a VALID UNTIL PostgreSQL rejects or that never compares equal on read-back.
|
|
47
|
+
expect(validUntilRefusal('2027-13-01T00:00:00Z')).toEqual({ reason: 'unparseable' });
|
|
48
|
+
expect(validUntilRefusal('not-a-dateZ')).toEqual({ reason: 'unparseable' });
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
test('refuseAtPlan raises the typed tag for a zone-free value', async () => {
|
|
52
|
+
const error = await fails(refuseAtPlan({ ...base, validUntil: '2027-01-01' }));
|
|
53
|
+
expect(error).toBeInstanceOf(PostgresRoleValidUntilRefused);
|
|
54
|
+
expect((error as PostgresRoleValidUntilRefused).reason).toBe('zone-free');
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
test('diff refuses the same values, so reconcile and diff can never disagree', async () => {
|
|
58
|
+
const error = await fails(diffPostgresRole({ ...base, validUntil: '2027-01-01' }, output));
|
|
59
|
+
expect(error).toBeInstanceOf(PostgresRoleValidUntilRefused);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
test('sameValidUntil compares instants, so the server’s milliseconds never churn a plan', () => {
|
|
63
|
+
expect(sameValidUntil('2027-01-01T00:00:00Z', '2027-01-01T00:00:00.000Z')).toBe(true);
|
|
64
|
+
expect(sameValidUntil('2027-01-01T02:00:00+02:00', '2027-01-01T00:00:00.000Z')).toBe(true);
|
|
65
|
+
expect(sameValidUntil('2027-01-01T00:00:00Z', null)).toBe(false);
|
|
66
|
+
expect(sameValidUntil('2027-01-01T00:00:00Z', '2028-01-01T00:00:00.000Z')).toBe(false);
|
|
67
|
+
});
|
|
68
|
+
});
|