@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.
- 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/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 +4 -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 +18 -0
- package/dist/litellm/index.d.ts.map +1 -1
- package/dist/litellm/index.js +3078 -2
- package/dist/litellm/index.js.map +65 -4
- 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/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/index.js +35 -1
- package/dist/openbao/index.js.map +3 -3
- package/dist/ownership/rows.d.ts +11 -0
- package/dist/ownership/rows.d.ts.map +1 -1
- package/dist/postgres/connection.d.ts +14 -2
- package/dist/postgres/connection.d.ts.map +1 -1
- package/dist/postgres/database-sql.d.ts +27 -8
- package/dist/postgres/database-sql.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 +41 -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 +31 -6
- package/dist/postgres/index.d.ts.map +1 -1
- package/dist/postgres/index.js +1998 -37
- package/dist/postgres/index.js.map +46 -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 +16 -8
- package/dist/postgres/providers.d.ts.map +1 -1
- package/dist/postgres/psql-executor.d.ts +4 -0
- package/dist/postgres/psql-executor.d.ts.map +1 -1
- package/dist/postgres/role-attrs.d.ts +112 -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 +132 -0
- package/dist/postgres/role-errors.d.ts.map +1 -0
- package/dist/postgres/role-membership-sql.d.ts +78 -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 +8 -0
- package/dist/postgres/role-scram.d.ts.map +1 -0
- package/dist/postgres/role-secrets.d.ts +38 -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/index.js +35 -1
- package/dist/proxmox/index.js.map +3 -3
- 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-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 -9
- 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 +126 -0
- package/docs/postgres-schema.md +139 -0
- package/docs/postgres.md +20 -3
- package/docs/valkey-transport.md +29 -0
- package/docs/valkey.md +187 -0
- package/package.json +6 -2
- package/src/ceph/ceph-argv.test.ts +1 -1
- package/src/launchd/sudo-allowlist.test.ts +2 -2
- package/src/linux/sudo-allowlist.test.ts +3 -3
- package/src/linux/sudo-runner.test.ts +2 -2
- 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/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 +19 -9
- 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 +140 -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/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 +24 -3
- 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/ownership/ownership.test.ts +33 -1
- package/src/ownership/rows.ts +53 -0
- package/src/postgres/connection.ts +42 -7
- 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 +36 -9
- 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 +240 -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 +166 -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 +129 -5
- package/src/postgres/installed-classifier.ts +36 -0
- package/src/postgres/providers.ts +33 -9
- package/src/postgres/psql-executor-escaping.test.ts +67 -0
- package/src/postgres/psql-executor.test.ts +63 -2
- package/src/postgres/psql-executor.ts +74 -34
- package/src/postgres/role-adopt.test.ts +98 -0
- package/src/postgres/role-attrs.ts +137 -0
- package/src/postgres/role-diff.ts +93 -0
- package/src/postgres/role-errors.ts +206 -0
- package/src/postgres/role-guard.test.ts +60 -0
- package/src/postgres/role-identity.test.ts +197 -0
- package/src/postgres/role-lifecycle.test.ts +228 -0
- package/src/postgres/role-membership-options.test.ts +73 -0
- package/src/postgres/role-membership-repair.test.ts +153 -0
- package/src/postgres/role-membership-sql.ts +223 -0
- package/src/postgres/role-password-ascii.test.ts +53 -0
- package/src/postgres/role-password-transports.test.ts +123 -0
- package/src/postgres/role-password.test.ts +158 -0
- package/src/postgres/role-privilege.test.ts +107 -0
- package/src/postgres/role-provenance.test.ts +200 -0
- package/src/postgres/role-provider.ts +105 -0
- package/src/postgres/role-refusals.test.ts +225 -0
- package/src/postgres/role-scram.test.ts +38 -0
- package/src/postgres/role-scram.ts +40 -0
- package/src/postgres/role-secrets.ts +61 -0
- package/src/postgres/role-sql.ts +213 -0
- package/src/postgres/role-valid-until.test.ts +68 -0
- package/src/postgres/role.ts +226 -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 +162 -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/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,198 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `LiteLLM.Toolset` through Alchemy's real Plan and Apply over the fake proxy
|
|
3
|
+
* (`fake-toolset-litellm.ts`): create, no-op, adopt by name, update, rename and delete.
|
|
4
|
+
*
|
|
5
|
+
* ★ EVERY VALUE IS `FAKE-*`.
|
|
6
|
+
* ⚠️ WHAT THE FAKE MODELS IS READ FROM THE 1.103.0 SOURCE, NOT MEASURED ON A LIVE PROXY (its header).
|
|
7
|
+
*/
|
|
8
|
+
import { expect, test } from 'bun:test';
|
|
9
|
+
import { credentials } from '@distilled.cloud/litellm/Credentials';
|
|
10
|
+
import * as Effect from 'effect/Effect';
|
|
11
|
+
import * as Layer from 'effect/Layer';
|
|
12
|
+
import * as FetchHttpClient from 'effect/unstable/http/FetchHttpClient';
|
|
13
|
+
import { FAKE_BASE } from './fake-litellm.ts';
|
|
14
|
+
import { FAKE_KEY } from './fake-registry-base.ts';
|
|
15
|
+
import {
|
|
16
|
+
type FakeToolsetLitellm,
|
|
17
|
+
startFakeToolsetLitellm,
|
|
18
|
+
toolsetRow,
|
|
19
|
+
} from './fake-toolset-litellm.ts';
|
|
20
|
+
import { fakeStack, writesOf } from './fake-stack.ts';
|
|
21
|
+
import { LiteLLMToolset } from './toolset.ts';
|
|
22
|
+
import { deleteToolset } from './toolset-operations.ts';
|
|
23
|
+
import type { ToolsetProps } from './toolset-types.ts';
|
|
24
|
+
|
|
25
|
+
const stack = (fake: FakeToolsetLitellm) =>
|
|
26
|
+
fakeStack({ apiKey: FAKE_KEY, baseUrl: FAKE_BASE }, fake.fetch);
|
|
27
|
+
const declare = (props: ToolsetProps, name = 'Web') =>
|
|
28
|
+
Effect.gen(function* () {
|
|
29
|
+
yield* LiteLLMToolset(name, props);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
const web: ToolsetProps = {
|
|
33
|
+
description: 'FAKE web tools',
|
|
34
|
+
tools: [
|
|
35
|
+
{ serverId: 'FAKE-server-a', toolName: 'search' },
|
|
36
|
+
{ serverId: 'FAKE-server-b', toolName: 'fetch' },
|
|
37
|
+
],
|
|
38
|
+
toolsetName: 'FAKE-web',
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
test('creates a toolset with the pairs, remembers the id the proxy issued, then writes nothing', async () => {
|
|
42
|
+
const fake = startFakeToolsetLitellm();
|
|
43
|
+
const same = stack(fake);
|
|
44
|
+
expect(await same.deploy(declare(web))).toEqual({ Web: 'create' });
|
|
45
|
+
expect(fake.toolsets()[0]).toMatchObject({
|
|
46
|
+
description: 'FAKE web tools',
|
|
47
|
+
tools: web.tools.map((tool) => ({ server_id: tool.serverId, tool_name: tool.toolName })),
|
|
48
|
+
toolset_name: 'FAKE-web',
|
|
49
|
+
});
|
|
50
|
+
const before = fake.requests().length;
|
|
51
|
+
expect(await same.deploy(declare(web))).toEqual({ Web: 'noop' });
|
|
52
|
+
expect(writesOf(fake.requests().slice(before))).toEqual([]);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
test('the list is used ONLY to adopt: with state, every read is by id and the list is never asked', async () => {
|
|
56
|
+
const fake = startFakeToolsetLitellm();
|
|
57
|
+
const same = stack(fake);
|
|
58
|
+
await same.deploy(declare(web));
|
|
59
|
+
const before = fake.requests().length;
|
|
60
|
+
await same.deploy(declare({ ...web, description: 'FAKE changed' }));
|
|
61
|
+
const paths = fake
|
|
62
|
+
.requests()
|
|
63
|
+
.slice(before)
|
|
64
|
+
.map((each) => `${each.method} ${each.path}`);
|
|
65
|
+
expect(paths).not.toContain('GET /v1/mcp/toolset');
|
|
66
|
+
expect(paths.some((path) => path.startsWith('GET /v1/mcp/toolset/FAKE-toolset-'))).toBe(true);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
test('a changed selection is an update that sends the id and ONLY the changed field', async () => {
|
|
70
|
+
const fake = startFakeToolsetLitellm();
|
|
71
|
+
const same = stack(fake);
|
|
72
|
+
await same.deploy(declare(web));
|
|
73
|
+
const planned = await same.deploy(
|
|
74
|
+
declare({ ...web, tools: [{ serverId: 'FAKE-server-a', toolName: 'search' }] }),
|
|
75
|
+
);
|
|
76
|
+
expect(planned).toEqual({ Web: 'update' });
|
|
77
|
+
expect(fake.bodies().at(-1)).toEqual({
|
|
78
|
+
tools: [{ server_id: 'FAKE-server-a', tool_name: 'search' }],
|
|
79
|
+
toolset_id: 'FAKE-toolset-0001',
|
|
80
|
+
});
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
test('an empty selection is sent as an explicit empty list, because a null would be a no-op', async () => {
|
|
84
|
+
const fake = startFakeToolsetLitellm();
|
|
85
|
+
const same = stack(fake);
|
|
86
|
+
await same.deploy(declare(web));
|
|
87
|
+
await same.deploy(declare({ ...web, tools: [] }));
|
|
88
|
+
expect(fake.bodies().at(-1)).toMatchObject({ tools: [] });
|
|
89
|
+
expect(fake.toolsets()[0]?.['tools']).toEqual([]);
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
test('a rename is an update of the SAME row', async () => {
|
|
93
|
+
const fake = startFakeToolsetLitellm();
|
|
94
|
+
const same = stack(fake);
|
|
95
|
+
await same.deploy(declare(web));
|
|
96
|
+
expect(await same.deploy(declare({ ...web, toolsetName: 'FAKE-renamed' }))).toEqual({
|
|
97
|
+
Web: 'update',
|
|
98
|
+
});
|
|
99
|
+
expect(fake.toolsets()).toHaveLength(1);
|
|
100
|
+
expect(fake.toolsets()[0]).toMatchObject({
|
|
101
|
+
toolset_id: 'FAKE-toolset-0001',
|
|
102
|
+
toolset_name: 'FAKE-renamed',
|
|
103
|
+
});
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
test('a live row is Unowned: refused without --adopt, adopted by name with it, and a matching row writes nothing', async () => {
|
|
107
|
+
const live = toolsetRow({
|
|
108
|
+
description: 'FAKE web tools',
|
|
109
|
+
tools: [
|
|
110
|
+
{ server_id: 'FAKE-server-b', tool_name: 'fetch' },
|
|
111
|
+
{ server_id: 'FAKE-server-a', tool_name: 'search' },
|
|
112
|
+
],
|
|
113
|
+
toolset_id: 'FAKE-live-1',
|
|
114
|
+
toolset_name: 'FAKE-web',
|
|
115
|
+
});
|
|
116
|
+
const fake = startFakeToolsetLitellm({ seed: [live] });
|
|
117
|
+
const engine = stack(fake);
|
|
118
|
+
await expect(engine.deploy(declare(web))).rejects.toThrow();
|
|
119
|
+
expect(writesOf(fake.requests())).toEqual([]);
|
|
120
|
+
expect(Object.values(await engine.deploy(declare(web), { adopt: true }))).toEqual(['adopted']);
|
|
121
|
+
expect(writesOf(fake.requests())).toEqual([]);
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
test('adopting a toolset with MORE tools than declared corrects it, and keeps undeclared text', async () => {
|
|
125
|
+
const wide = toolsetRow({
|
|
126
|
+
description: 'written by a person',
|
|
127
|
+
tools: [
|
|
128
|
+
{ server_id: 'FAKE-server-a', tool_name: 'search' },
|
|
129
|
+
{ server_id: 'FAKE-server-b', tool_name: 'fetch' },
|
|
130
|
+
{ server_id: 'FAKE-server-b', tool_name: 'FAKE-extra' },
|
|
131
|
+
],
|
|
132
|
+
toolset_id: 'FAKE-live-2',
|
|
133
|
+
toolset_name: 'FAKE-web',
|
|
134
|
+
});
|
|
135
|
+
const fake = startFakeToolsetLitellm({ seed: [wide] });
|
|
136
|
+
const { description: _kept, ...withoutDescription } = web;
|
|
137
|
+
await stack(fake).deploy(declare(withoutDescription), { adopt: true });
|
|
138
|
+
expect(fake.toolsets()[0]?.['tools']).toHaveLength(2);
|
|
139
|
+
expect(fake.toolsets()[0]?.['description']).toBe('written by a person');
|
|
140
|
+
expect(fake.bodies()[0]).not.toHaveProperty('description');
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
test('a list that swallowed a database error cannot hide an existing name: the create fails on the 409', async () => {
|
|
144
|
+
// ⛔ measured in the source: list_mcp_toolsets answers [] on any exception
|
|
145
|
+
const live = toolsetRow({ toolset_id: 'FAKE-live-3', toolset_name: 'FAKE-web' });
|
|
146
|
+
const fake = startFakeToolsetLitellm({ listSwallowsErrors: true, seed: [live] });
|
|
147
|
+
await expect(stack(fake).deploy(declare(web))).rejects.toThrow();
|
|
148
|
+
expect(fake.toolsets()).toHaveLength(1);
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
test('a refused declaration fails the plan before a single request', async () => {
|
|
152
|
+
for (const props of [
|
|
153
|
+
{ ...web, toolsetName: ' ' },
|
|
154
|
+
{ ...web, toolsetName: 'a/b' },
|
|
155
|
+
{ ...web, description: ' ' },
|
|
156
|
+
{ ...web, tools: [{ serverId: '', toolName: 'search' }] },
|
|
157
|
+
{ ...web, tools: [web.tools[0], web.tools[0]] as never },
|
|
158
|
+
]) {
|
|
159
|
+
const fake = startFakeToolsetLitellm();
|
|
160
|
+
await expect(stack(fake).deploy(declare(props))).rejects.toThrow();
|
|
161
|
+
expect(fake.requests()).toEqual([]);
|
|
162
|
+
}
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
test('removing the declaration RETAINS the toolset', async () => {
|
|
166
|
+
const fake = startFakeToolsetLitellm();
|
|
167
|
+
const same = stack(fake);
|
|
168
|
+
await same.deploy(declare(web));
|
|
169
|
+
await same.deploy(Effect.void);
|
|
170
|
+
expect(fake.toolsets()).toHaveLength(1);
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
const run = <A, E>(fetchFn: typeof globalThis.fetch, effect: Effect.Effect<A, E, unknown>) =>
|
|
174
|
+
Effect.runPromise(
|
|
175
|
+
effect.pipe(
|
|
176
|
+
Effect.provide(FetchHttpClient.layer),
|
|
177
|
+
Effect.provide(Layer.succeed(FetchHttpClient.Fetch, fetchFn)),
|
|
178
|
+
Effect.provide(credentials({ apiKey: FAKE_KEY, baseUrl: FAKE_BASE })),
|
|
179
|
+
) as Effect.Effect<A, E, never>,
|
|
180
|
+
);
|
|
181
|
+
|
|
182
|
+
test('delete answers 202 with no body, and a missing id is swallowed only because a by-id read says so', async () => {
|
|
183
|
+
const fake = startFakeToolsetLitellm({
|
|
184
|
+
seed: [toolsetRow({ toolset_id: 'FAKE-live-4', toolset_name: 'FAKE-web' })],
|
|
185
|
+
});
|
|
186
|
+
await run(fake.fetch, deleteToolset('FAKE-live-4'));
|
|
187
|
+
expect(fake.toolsets()).toEqual([]);
|
|
188
|
+
await run(fake.fetch, deleteToolset('FAKE-live-4'));
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
test('a delete refused for lack of rights re-raises the ORIGINAL error and the row stays', async () => {
|
|
192
|
+
const fake = startFakeToolsetLitellm({
|
|
193
|
+
forbidWrites: true,
|
|
194
|
+
seed: [toolsetRow({ toolset_id: 'FAKE-live-5', toolset_name: 'FAKE-web' })],
|
|
195
|
+
});
|
|
196
|
+
await expect(run(fake.fetch, deleteToolset('FAKE-live-5'))).rejects.toThrow();
|
|
197
|
+
expect(fake.toolsets()).toHaveLength(1);
|
|
198
|
+
});
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `LiteLLM.Toolset` — one row of LiteLLM's MCP toolset table (`/v1/mcp/toolset`): a named selection of
|
|
3
|
+
* `{ server_id, tool_name }` pairs a key or team can be granted instead of whole servers. Props and
|
|
4
|
+
* what is left out: toolset-types.ts.
|
|
5
|
+
*
|
|
6
|
+
* ★ ADOPT BY NAME. `toolset_name` is unique, so `toolsetName` finds the one live row (from the LIST,
|
|
7
|
+
* which is the only by-name lookup the API has, and which cannot prove absence: toolset-operations.ts).
|
|
8
|
+
* A live row with no state is `Unowned`, so it needs `--adopt`.
|
|
9
|
+
* ★ THE ID IS THE PROXY'S and is what state remembers; every later read is BY ID, from the table.
|
|
10
|
+
* ★ `defaultRemovalPolicy: 'retain'` — deleting a toolset does not detach it: teams and keys that hold
|
|
11
|
+
* its id keep a dangling grant, and every caller reaching MCP through it loses the tools.
|
|
12
|
+
* ⚠️ NO LICENCE GATE ON THESE ROUTES: `mcp_management_endpoints.py`'s toolset block has no
|
|
13
|
+
* `premium_user` check (1.103.0), and docs.litellm.ai/docs/mcp_toolsets makes no Enterprise statement.
|
|
14
|
+
* Writes need PROXY_ADMIN (403 otherwise).
|
|
15
|
+
* ⛔ NO LITELLM CREDENTIAL IS A PROP; `litellmProviders`' layer supplies `Credentials`.
|
|
16
|
+
*/
|
|
17
|
+
import { Resource } from 'alchemy';
|
|
18
|
+
import { Unowned } from 'alchemy/AdoptPolicy';
|
|
19
|
+
import { isResolved } from 'alchemy/Diff';
|
|
20
|
+
import type { Input } from 'alchemy/Input';
|
|
21
|
+
import * as Provider from 'alchemy/Provider';
|
|
22
|
+
import * as Effect from 'effect/Effect';
|
|
23
|
+
import * as Predicate from 'effect/Predicate';
|
|
24
|
+
import {
|
|
25
|
+
LitellmRegistryAbsentAfterWriteError,
|
|
26
|
+
LitellmRegistryAmbiguousError,
|
|
27
|
+
LitellmRegistryNotConvergedError,
|
|
28
|
+
LitellmRegistryUnreadableError,
|
|
29
|
+
} from './registry-errors.ts';
|
|
30
|
+
import { dieIfInvalid, failIfInvalid } from './registry-support.ts';
|
|
31
|
+
import { createBody, differing, firstProblem, updateBody } from './toolset-form.ts';
|
|
32
|
+
import {
|
|
33
|
+
createToolset,
|
|
34
|
+
deleteToolset,
|
|
35
|
+
listToolsets,
|
|
36
|
+
readToolset,
|
|
37
|
+
updateToolset,
|
|
38
|
+
} from './toolset-operations.ts';
|
|
39
|
+
import type { ToolsetAttributes, ToolsetProps } from './toolset-types.ts';
|
|
40
|
+
|
|
41
|
+
export type { ToolsetAttributes, ToolsetProps };
|
|
42
|
+
export type { RegistryError as ToolsetError } from './registry-errors.ts';
|
|
43
|
+
|
|
44
|
+
const RESOURCE = 'LiteLLM.Toolset';
|
|
45
|
+
|
|
46
|
+
export interface LiteLLMToolset extends Resource<
|
|
47
|
+
'LiteLLM.Toolset',
|
|
48
|
+
ToolsetProps,
|
|
49
|
+
ToolsetAttributes
|
|
50
|
+
> {}
|
|
51
|
+
|
|
52
|
+
export const LiteLLMToolset = Resource<LiteLLMToolset>('LiteLLM.Toolset', {
|
|
53
|
+
defaultRemovalPolicy: 'retain',
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
export const isLiteLLMToolset = (value: unknown): value is LiteLLMToolset =>
|
|
57
|
+
Predicate.hasProperty(value, 'Type') && value.Type === 'LiteLLM.Toolset';
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* The live row this declaration means: BY ID from the table when state has one (a vanished row is
|
|
61
|
+
* `undefined`, never taken over by name), else by name from the list. Two rows sharing a name would
|
|
62
|
+
* mean the unique index is gone; refused, never guessed.
|
|
63
|
+
*/
|
|
64
|
+
const locate = (props: ToolsetProps, output: ToolsetAttributes | undefined) =>
|
|
65
|
+
Effect.gen(function* () {
|
|
66
|
+
if (output !== undefined) return yield* readToolset(output.toolsetId);
|
|
67
|
+
const named = (yield* listToolsets()).filter((row) => row.toolsetName === props.toolsetName);
|
|
68
|
+
if (named.length > 1) {
|
|
69
|
+
return yield* Effect.fail(
|
|
70
|
+
new LitellmRegistryAmbiguousError({
|
|
71
|
+
ids: named.map((row) => row.toolsetId),
|
|
72
|
+
name: props.toolsetName,
|
|
73
|
+
resource: RESOURCE,
|
|
74
|
+
}),
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
return named[0];
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
/** ⛔ TEST-ONLY EXPORT. Not on index.ts's barrel. */
|
|
81
|
+
export const toolsetHandlers = {
|
|
82
|
+
/** ⛔ With no `output` this is the adoption probe, so the refusals run here too (registry-support.ts). */
|
|
83
|
+
read: ({ olds, output }: { olds: ToolsetProps; output: ToolsetAttributes | undefined }) =>
|
|
84
|
+
Effect.gen(function* () {
|
|
85
|
+
if (output === undefined) {
|
|
86
|
+
yield* dieIfInvalid(RESOURCE, String(olds.toolsetName), firstProblem(olds));
|
|
87
|
+
}
|
|
88
|
+
const live = yield* locate(olds, output);
|
|
89
|
+
if (live === undefined) return undefined;
|
|
90
|
+
return output === undefined ? Unowned(live) : live;
|
|
91
|
+
}),
|
|
92
|
+
|
|
93
|
+
diff: ({ news, output }: { news: Input<ToolsetProps>; output: ToolsetAttributes | undefined }) =>
|
|
94
|
+
Effect.gen(function* () {
|
|
95
|
+
if (!isResolved(news)) return undefined;
|
|
96
|
+
yield* failIfInvalid(RESOURCE, String(news.toolsetName), firstProblem(news));
|
|
97
|
+
if (output === undefined) return undefined;
|
|
98
|
+
return differing(output, news).length === 0
|
|
99
|
+
? ({ action: 'noop' } as const)
|
|
100
|
+
: ({ action: 'update' } as const);
|
|
101
|
+
}),
|
|
102
|
+
|
|
103
|
+
reconcile: ({ news, output }: { news: ToolsetProps; output: ToolsetAttributes | undefined }) =>
|
|
104
|
+
Effect.gen(function* () {
|
|
105
|
+
yield* failIfInvalid(RESOURCE, String(news.toolsetName), firstProblem(news));
|
|
106
|
+
const before = yield* locate(news, output);
|
|
107
|
+
let toolsetId: string;
|
|
108
|
+
if (before === undefined) {
|
|
109
|
+
const created = yield* createToolset(createBody(news));
|
|
110
|
+
if (created === undefined) {
|
|
111
|
+
return yield* Effect.fail(
|
|
112
|
+
new LitellmRegistryUnreadableError({
|
|
113
|
+
reason: 'the create answered something that is not a toolset',
|
|
114
|
+
resource: RESOURCE,
|
|
115
|
+
}),
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
toolsetId = created.toolsetId;
|
|
119
|
+
} else {
|
|
120
|
+
toolsetId = before.toolsetId;
|
|
121
|
+
if (differing(before, news).length > 0) yield* updateToolset(updateBody(news, before));
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// ★ BY ID FROM THE TABLE, never the list: the list is the caller-narrowed, error-swallowing view.
|
|
125
|
+
const after = yield* readToolset(toolsetId);
|
|
126
|
+
if (after === undefined) {
|
|
127
|
+
return yield* Effect.fail(
|
|
128
|
+
new LitellmRegistryAbsentAfterWriteError({ name: news.toolsetName, resource: RESOURCE }),
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
const left = differing(after, news);
|
|
132
|
+
if (left.length > 0) {
|
|
133
|
+
return yield* Effect.fail(
|
|
134
|
+
new LitellmRegistryNotConvergedError({
|
|
135
|
+
fields: left,
|
|
136
|
+
name: news.toolsetName,
|
|
137
|
+
resource: RESOURCE,
|
|
138
|
+
}),
|
|
139
|
+
);
|
|
140
|
+
}
|
|
141
|
+
return after;
|
|
142
|
+
}),
|
|
143
|
+
|
|
144
|
+
delete: ({ output }: { output: ToolsetAttributes }) => deleteToolset(output.toolsetId),
|
|
145
|
+
|
|
146
|
+
/** ⛔ EMPTY: adoption is an explicit act (`--adopt`), and no ownership mark exists to filter by. */
|
|
147
|
+
list: () => Effect.succeed([]),
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
export const LiteLLMToolsetProvider = () =>
|
|
151
|
+
Provider.effect(LiteLLMToolset, Effect.succeed(LiteLLMToolset.Provider.of(toolsetHandlers)));
|
|
@@ -12,7 +12,7 @@ import * as Effect from 'effect/Effect';
|
|
|
12
12
|
import { refuseTakeover } from './adopt.ts';
|
|
13
13
|
import { ownedRead } from './probe.ts';
|
|
14
14
|
import { noteResume, noteUnfinished, resumes } from './resume.ts';
|
|
15
|
-
import { forgetRefusedCreate, isCreate, recordedInstance } from './rows.ts';
|
|
15
|
+
import { forgetRefusedCreate, isCreate, olderGenerationIds, recordedInstance } from './rows.ts';
|
|
16
16
|
import { row, withStore } from './store-fixture.ts';
|
|
17
17
|
|
|
18
18
|
describe('recordedInstance', () => {
|
|
@@ -50,6 +50,38 @@ describe('isCreate', () => {
|
|
|
50
50
|
});
|
|
51
51
|
});
|
|
52
52
|
|
|
53
|
+
describe('olderGenerationIds', () => {
|
|
54
|
+
test('collects attr.id from every generation older than the instance, through the old chain', async () => {
|
|
55
|
+
const rows = {
|
|
56
|
+
A: {
|
|
57
|
+
...row('A', 'new', { ...row('A', 'older'), attr: { id: 'older-id' } }),
|
|
58
|
+
attr: { id: 'new-id' },
|
|
59
|
+
},
|
|
60
|
+
};
|
|
61
|
+
expect(await withStore(rows, olderGenerationIds('A', 'new'))).toEqual(['older-id']);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
test('walks a former FQN the instance moved away from (renamedFrom)', async () => {
|
|
65
|
+
// Z is declared renamedFrom('X'). beta.79 moves the row (preserving instanceId) and deletes
|
|
66
|
+
// the former FQN; while a pre-move row still sits at X, the FormerFqns walk finds it and
|
|
67
|
+
// collects the older generation it holds.
|
|
68
|
+
const rows = {
|
|
69
|
+
Z: row('Z', 'new'),
|
|
70
|
+
X: {
|
|
71
|
+
...row('X', 'new', { ...row('X', 'older'), attr: { id: 'older-id' } }),
|
|
72
|
+
attr: { id: 'new-id' },
|
|
73
|
+
},
|
|
74
|
+
};
|
|
75
|
+
expect(await withStore(rows, olderGenerationIds('Z', 'new'))).toEqual(['older-id']);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
test('the current generation itself is skipped, and an empty store answers nothing', async () => {
|
|
79
|
+
const rows = { A: { ...row('A', 'new'), attr: { id: 'new-id' } } };
|
|
80
|
+
expect(await withStore(rows, olderGenerationIds('A', 'new'))).toEqual([]);
|
|
81
|
+
expect(await Effect.runPromise(olderGenerationIds('A', 'new'))).toEqual([]);
|
|
82
|
+
});
|
|
83
|
+
});
|
|
84
|
+
|
|
53
85
|
describe('forgetRefusedCreate', () => {
|
|
54
86
|
test('drops only the creating row of this instance, with no attributes', async () => {
|
|
55
87
|
const rows: Record<string, ResourceState> = {
|
package/src/ownership/rows.ts
CHANGED
|
@@ -99,6 +99,59 @@ export const isCreate = (fqn: string, instanceId: string): Effect.Effect<boolean
|
|
|
99
99
|
(generation) => generation?.status === 'creating',
|
|
100
100
|
);
|
|
101
101
|
|
|
102
|
+
const idOf = (attr: unknown): string | undefined => {
|
|
103
|
+
if (typeof attr !== 'object' || attr === null) return undefined;
|
|
104
|
+
const id = (attr as { readonly id?: unknown }).id;
|
|
105
|
+
return typeof id === 'string' && id !== '' ? id : undefined;
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* `attr.id` on every generation OLDER than `instanceId` in this FQN's state chain (and a former
|
|
110
|
+
* FQN `renamedFrom` names). Walks `old` the way {@link generationOf} does, and skips the current
|
|
111
|
+
* generation itself.
|
|
112
|
+
* ⛔ THOSE IDS ARE NOT THIS GENERATION'S. A `replacing` row's reconcile runs with no attributes, so
|
|
113
|
+
* a live row found by name can be the previous generation's own deployment. Recording that id on
|
|
114
|
+
* the new generation makes both the same id, and `destroy()` then deletes the old generation by
|
|
115
|
+
* it (Apply.ts, the retain check near 2168 and the provider delete that follows when the policy
|
|
116
|
+
* is not `retain`). A same-instance `old` snapshot (an `updating` row) is not one of these.
|
|
117
|
+
*/
|
|
118
|
+
export const olderGenerationIds = (
|
|
119
|
+
fqn: string,
|
|
120
|
+
instanceId: string,
|
|
121
|
+
): Effect.Effect<readonly string[]> =>
|
|
122
|
+
Effect.gen(function* () {
|
|
123
|
+
const found = yield* storeOf;
|
|
124
|
+
if (found === undefined) return [];
|
|
125
|
+
const ids: string[] = [];
|
|
126
|
+
for (const at of [fqn, ...(found.resources[fqn]?.FormerFqns ?? [])]) {
|
|
127
|
+
collectOlder(yield* rowAt(found.store, found.where, at), instanceId, ids);
|
|
128
|
+
}
|
|
129
|
+
return ids;
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Once `instanceId` has been seen, collect `attr.id` from generations with a DIFFERENT instance
|
|
134
|
+
* id. An `updating` row's `old` is the same instance's pre-update snapshot (Apply.ts), not a
|
|
135
|
+
* replacement generation — that snapshot's id is the row this apply is already writing.
|
|
136
|
+
*/
|
|
137
|
+
const collectOlder = (row: unknown, instanceId: string, ids: string[]): void => {
|
|
138
|
+
let at = row;
|
|
139
|
+
let seen = false;
|
|
140
|
+
while (typeof at === 'object' && at !== null && !isActionState(at as never)) {
|
|
141
|
+
const generation = at as Generation;
|
|
142
|
+
if (!seen) {
|
|
143
|
+
seen = generation.instanceId === instanceId;
|
|
144
|
+
at = generation.old;
|
|
145
|
+
continue;
|
|
146
|
+
}
|
|
147
|
+
if (generation.instanceId !== instanceId) {
|
|
148
|
+
const id = idOf(generation.attr);
|
|
149
|
+
if (id !== undefined) ids.push(id);
|
|
150
|
+
}
|
|
151
|
+
at = generation.old;
|
|
152
|
+
}
|
|
153
|
+
};
|
|
154
|
+
|
|
102
155
|
/**
|
|
103
156
|
* ⛔ A REFUSED CREATE MUST NOT LEAVE A ROW THAT CLAIMS THE OBJECT IT REFUSED. Apply commits a
|
|
104
157
|
* `creating` row for this instance before it calls reconcile, and a failed reconcile leaves it
|
|
@@ -44,9 +44,12 @@ export interface PostgresRunnerConfig {
|
|
|
44
44
|
readonly template?: string;
|
|
45
45
|
}
|
|
46
46
|
|
|
47
|
-
/** What `withPg`'s callback learns about the transport beyond the client.
|
|
47
|
+
/** What `withPg`'s callback learns about the transport beyond the client. `database` is the
|
|
48
|
+
* database the statements run IN: the family connection's, or the override `Postgres.Schema`
|
|
49
|
+
* and `Postgres.Grants` pass so they open their declared database and then prove it. */
|
|
48
50
|
export interface PgContext {
|
|
49
51
|
readonly template?: string;
|
|
52
|
+
readonly database: string;
|
|
50
53
|
}
|
|
51
54
|
|
|
52
55
|
/** The lazy connection service. Its value is an `Effect` of the config, per S24 — see the file
|
|
@@ -67,6 +70,11 @@ export const postgresRunnerConnection = (
|
|
|
67
70
|
config: PostgresRunnerConfig,
|
|
68
71
|
): Layer.Layer<PostgresConnection> => Layer.succeed(PostgresConnection, Effect.succeed(config));
|
|
69
72
|
|
|
73
|
+
/** Effect-scoped pool factory, like BaoGate: tests override it without process-wide module mocks. */
|
|
74
|
+
export const PostgresPool = Context.Reference<typeof PgClient.layer>('homeflare/postgres/Pool', {
|
|
75
|
+
defaultValue: () => PgClient.layer,
|
|
76
|
+
});
|
|
77
|
+
|
|
70
78
|
const isRunner = (c: PostgresConnectionConfig | PostgresRunnerConfig): c is PostgresRunnerConfig =>
|
|
71
79
|
'run' in c;
|
|
72
80
|
|
|
@@ -77,20 +85,47 @@ const isRunner = (c: PostgresConnectionConfig | PostgresRunnerConfig): c is Post
|
|
|
77
85
|
* operation — never once per statement — so a reconcile that runs a `SELECT`, then a `CREATE`,
|
|
78
86
|
* then a re-read shares one pool and closes it once, while a plan-only `read` still opens and
|
|
79
87
|
* closes its own.
|
|
88
|
+
*
|
|
89
|
+
* `database` overrides which database of the SAME cluster the pool opens — the declared
|
|
90
|
+
* database of `Postgres.Schema` and `Postgres.Grants`. The family connection is pointed at a
|
|
91
|
+
* maintenance database (a brand-new database cannot be connected to on a cold plan), so those
|
|
92
|
+
* resources pass their declared database here: the runner transport swaps the `psql -d` target,
|
|
93
|
+
* the socket path swaps the pool's `database`. The swap is proven, not trusted: schema rows
|
|
94
|
+
* carry `current_database()` (`schema-sql.ts`) and grants calls the same function before writing.
|
|
80
95
|
*/
|
|
81
96
|
export const withPg = <A, E>(
|
|
82
|
-
build: (pg:
|
|
97
|
+
build: (pg: PgExecutor, context: PgContext) => Effect.Effect<A, E>,
|
|
98
|
+
database?: string,
|
|
83
99
|
): Effect.Effect<A, E | SqlError, PostgresConnection> =>
|
|
84
100
|
Effect.gen(function* () {
|
|
85
101
|
const resolveConfig = yield* PostgresConnection;
|
|
86
102
|
const config = yield* resolveConfig;
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
103
|
+
const target: PostgresConnectionConfig | PostgresRunnerConfig =
|
|
104
|
+
database === undefined ? config : { ...config, database };
|
|
105
|
+
if (isRunner(target)) {
|
|
106
|
+
return yield* build(makePsqlExecutor(target.run, target), {
|
|
107
|
+
template: target.template ?? 'template0',
|
|
108
|
+
database: target.database,
|
|
90
109
|
});
|
|
91
110
|
}
|
|
111
|
+
const pool = yield* PostgresPool;
|
|
92
112
|
return yield* Effect.provide(
|
|
93
|
-
Effect.flatMap(PgClient.PgClient, (pg) =>
|
|
94
|
-
|
|
113
|
+
Effect.flatMap(PgClient.PgClient, (pg) =>
|
|
114
|
+
build(
|
|
115
|
+
{
|
|
116
|
+
unsafe: (sql, params) => pg.unsafe(sql, params),
|
|
117
|
+
// One reserved connection (`SqlClient.withTransaction`). A pool checkout per
|
|
118
|
+
// statement would autocommit CREATE before GRANT.
|
|
119
|
+
transaction: (statements) =>
|
|
120
|
+
pg.withTransaction(
|
|
121
|
+
Effect.gen(function* () {
|
|
122
|
+
for (const sql of statements) yield* pg.unsafe(sql).pipe(Effect.asVoid);
|
|
123
|
+
}),
|
|
124
|
+
),
|
|
125
|
+
},
|
|
126
|
+
{ database: target.database },
|
|
127
|
+
),
|
|
128
|
+
),
|
|
129
|
+
pool(target),
|
|
95
130
|
);
|
|
96
131
|
});
|
|
@@ -50,12 +50,12 @@ describe('quoting', () => {
|
|
|
50
50
|
});
|
|
51
51
|
|
|
52
52
|
test('quoteStringLiteral doubles an embedded single quote', () => {
|
|
53
|
-
expect(quoteStringLiteral("en_US.UTF-8'; DROP")).toBe("'en_US.UTF-8''; DROP'");
|
|
53
|
+
expect(quoteStringLiteral("en_US.UTF-8'; DROP")).toBe("E'en_US.UTF-8''; DROP'");
|
|
54
54
|
});
|
|
55
55
|
|
|
56
56
|
test('buildCreateDatabaseSql never string-concatenates an unescaped value', () => {
|
|
57
57
|
const sql = buildCreateDatabaseSql({ name: 'db"1', owner: 'ro"le', encoding: "UT'F8" });
|
|
58
|
-
expect(sql).toBe('CREATE DATABASE "db""1" WITH OWNER "ro""le" ENCODING \'UT\'\'F8\'');
|
|
58
|
+
expect(sql).toBe('CREATE DATABASE "db""1" WITH OWNER "ro""le" ENCODING E\'UT\'\'F8\'');
|
|
59
59
|
});
|
|
60
60
|
});
|
|
61
61
|
|
|
@@ -19,23 +19,33 @@
|
|
|
19
19
|
* `createdb_opt_item`'s string branch is `opt_boolean_or_string → NonReservedWord_or_Sconst →
|
|
20
20
|
* NonReservedWord | Sconst`. A locale like `en_US.UTF-8` would hit the same dot-splitting
|
|
21
21
|
* `quoteIdent` avoids if it were identifier-quoted; a plain `Sconst` (`quoteStringLiteral`) has
|
|
22
|
-
* no such rule.
|
|
23
|
-
*
|
|
24
|
-
*
|
|
22
|
+
* no such rule. Use E'…' with doubled backslashes and quotes: PostgreSQL 18 lexical
|
|
23
|
+
* section 4.1.2.2 makes escape strings independent of standard_conforming_strings.
|
|
24
|
+
* Measured 2026-10-01: the former quote-only escaper allowed comment SQL injection when
|
|
25
|
+
* that setting was off; an input backslash escaped the first quote of a doubled pair.
|
|
25
26
|
*/
|
|
26
27
|
import type { SqlError } from 'effect/unstable/sql/SqlError';
|
|
27
28
|
import * as Effect from 'effect/Effect';
|
|
28
29
|
import type { PostgresDatabaseAttributes, PostgresDatabaseProps } from './database-attrs.ts';
|
|
29
30
|
|
|
30
31
|
/** What every function in this file needs from a client — real (`PgClient.PgClient`) or fake
|
|
31
|
-
* (`fake-sql.ts`). Deliberately smaller than `SqlClient`: nothing here
|
|
32
|
-
*
|
|
33
|
-
*
|
|
32
|
+
* (`fake-sql.ts`). Deliberately smaller than `SqlClient`: nothing here needs the tagged template
|
|
33
|
+
* or streaming. `transaction` is the exception `Postgres.Role`'s create and `Postgres.Grants`'
|
|
34
|
+
* repair share: role DDL commits per statement unless the statements share a transaction, and
|
|
35
|
+
* a grants repair's `REVOKE` must not autocommit apart from the `GRANT`s that restore it. */
|
|
34
36
|
export interface PgExecutor {
|
|
35
37
|
readonly unsafe: <A extends object>(
|
|
36
38
|
sql: string,
|
|
37
39
|
params?: ReadonlyArray<unknown>,
|
|
38
40
|
) => Effect.Effect<ReadonlyArray<A>, SqlError>;
|
|
41
|
+
/**
|
|
42
|
+
* Run `statements` in one transaction. `CREATE ROLE`, `ALTER ROLE` and `GRANT`/`REVOKE` of
|
|
43
|
+
* membership are transactional (`utility.c@REL_18_6`: those cases do not call
|
|
44
|
+
* `PreventInTransactionBlock`; `CREATE DATABASE` does). The socket client reserves one
|
|
45
|
+
* connection (`SqlClient.withTransaction`); the runner sends one `psql` script, because each
|
|
46
|
+
* `unsafe` call is its own session and would autocommit.
|
|
47
|
+
*/
|
|
48
|
+
readonly transaction: (statements: readonly string[]) => Effect.Effect<void, SqlError>;
|
|
39
49
|
}
|
|
40
50
|
|
|
41
51
|
/** Single-token identifier quoting: wrap in `"`, double any embedded `"`. No dot-splitting —
|
|
@@ -43,9 +53,10 @@ export interface PgExecutor {
|
|
|
43
53
|
* `NonReservedWord` too, for `OWNER` and `TABLESPACE`. */
|
|
44
54
|
export const quoteIdent = (value: string): string => `"${value.replace(/"/g, '""')}"`;
|
|
45
55
|
|
|
46
|
-
/**
|
|
47
|
-
*
|
|
48
|
-
export const quoteStringLiteral = (value: string): string =>
|
|
56
|
+
/** Escape-string quoting is safe with standard_conforming_strings either on or off.
|
|
57
|
+
* Double backslashes BEFORE quotes, so caller text cannot escape a closing delimiter. */
|
|
58
|
+
export const quoteStringLiteral = (value: string): string =>
|
|
59
|
+
`E'${value.replace(/\\/g, '\\\\').replace(/'/g, "''")}'`;
|
|
49
60
|
|
|
50
61
|
const withOptions = (props: PostgresDatabaseProps, template?: string): string => {
|
|
51
62
|
const parts: string[] = [`OWNER ${quoteIdent(props.owner)}`];
|
|
@@ -103,6 +114,22 @@ export const roleExists = (pg: PgExecutor, role: string): Effect.Effect<boolean,
|
|
|
103
114
|
(rows) => rows.length > 0,
|
|
104
115
|
);
|
|
105
116
|
|
|
117
|
+
const DATABASE_EXISTS_SQL = 'SELECT 1 AS present FROM pg_database WHERE datname = $1';
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Does one database of the cluster exist? `Postgres.Schema` runs this over the FAMILY
|
|
121
|
+
* connection (no `withPg` database override) before `read` and `delete`, so a declared
|
|
122
|
+
* database that does not exist yet answers "schema absent" instead of the transports' untyped
|
|
123
|
+
* connect failures (`ConnectionError` over psql, `UnknownError` 3D000 over the socket). The
|
|
124
|
+
* probe targets the maintenance database the family connection already points at, which a
|
|
125
|
+
* cold plan can always reach.
|
|
126
|
+
*/
|
|
127
|
+
export const databaseExists = (pg: PgExecutor, name: string): Effect.Effect<boolean, SqlError> =>
|
|
128
|
+
Effect.map(
|
|
129
|
+
pg.unsafe<{ readonly present: number }>(DATABASE_EXISTS_SQL, [name]),
|
|
130
|
+
(rows) => rows.length > 0,
|
|
131
|
+
);
|
|
132
|
+
|
|
106
133
|
/** `datlocprovider` is Postgres's internal 1-byte `"char"` type, which this client decodes as
|
|
107
134
|
* raw bytes rather than text (measured 2026-09-23 against the live socket — `docs/postgres.md`);
|
|
108
135
|
* the `CASE` maps it to the same three words `pg_collation.h@REL_18_6#collprovider_name` returns
|