@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,215 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `Valkey.AclFile` against `fake-valkey.ts`'s recording fake — creating a user, converging a user
|
|
3
|
+
* tampered with outside the stack, rotating a password, removing a user, missing-password
|
|
4
|
+
* refusal, and read-back verification. Every case reverts cleanly by reverting `acl-ops.ts`/
|
|
5
|
+
* `acl-form.ts` locally.
|
|
6
|
+
*/
|
|
7
|
+
import { describe, expect, test } from 'bun:test';
|
|
8
|
+
import * as Effect from 'effect/Effect';
|
|
9
|
+
import { readWithExecutor } from './acl-ops.ts';
|
|
10
|
+
import { reconcileWithExecutor } from './acl-reconcile.ts';
|
|
11
|
+
import { matchesDeclared } from './acl-compare.ts';
|
|
12
|
+
import { buildSetUserArgs, parseAclLine, planAclUsers } from './acl-form.ts';
|
|
13
|
+
import { ValkeyAclPasswordMissing, ValkeyAclReadbackFailed } from './errors.ts';
|
|
14
|
+
import { makeFakeValkey } from './fake-valkey.ts';
|
|
15
|
+
import type { ValkeyAclFileProps, ValkeyAclUser } from './acl-attrs.ts';
|
|
16
|
+
|
|
17
|
+
const run = <A, E>(eff: Effect.Effect<A, E>): Promise<A> => Effect.runPromise(eff);
|
|
18
|
+
const fails = <A, E>(eff: Effect.Effect<A, E>): Promise<E> => Effect.runPromise(Effect.flip(eff));
|
|
19
|
+
|
|
20
|
+
const env = { CLAUDE_PW: 'FAKE-seat-password', GROK_PW: 'FAKE-grok-password' };
|
|
21
|
+
const claude: ValkeyAclUser = {
|
|
22
|
+
name: 'claude',
|
|
23
|
+
keyPrefix: 'claude:*',
|
|
24
|
+
profile: 'seat',
|
|
25
|
+
password: { fromEnv: 'CLAUDE_PW' },
|
|
26
|
+
};
|
|
27
|
+
const props = (over: Partial<ValkeyAclFileProps['users']> = {}): ValkeyAclFileProps => ({
|
|
28
|
+
instance: 'valkey-seats',
|
|
29
|
+
users: { claude, ...over },
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
describe('buildSetUserArgs', () => {
|
|
33
|
+
test('emits reset-first, on, password, key prefix, channel scope, and the seat deny list', async () => {
|
|
34
|
+
const args = await run(buildSetUserArgs(claude, 'FAKE-password'));
|
|
35
|
+
expect(args.slice(0, 4)).toEqual(['ACL', 'SETUSER', 'claude', 'reset']);
|
|
36
|
+
expect(args).toContain('on');
|
|
37
|
+
expect(args.some((arg) => /^#[a-f0-9]{64}$/.test(arg))).toBe(true);
|
|
38
|
+
expect(args.some((arg) => arg.includes('FAKE-password'))).toBe(false);
|
|
39
|
+
expect(args).toContain('~claude:*');
|
|
40
|
+
expect(args).toContain('&claude:*');
|
|
41
|
+
expect(args).toContain('-flushall');
|
|
42
|
+
expect(args).toContain('-acl');
|
|
43
|
+
expect(args).toContain('-scan');
|
|
44
|
+
// reset first: no stale rules survive a rebuild.
|
|
45
|
+
expect(args.indexOf('reset')).toBeLessThan(args.findIndex((arg) => arg.startsWith('#')));
|
|
46
|
+
});
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
describe('parseAclLine', () => {
|
|
50
|
+
test('parses the measured ACL LIST echo shape', () => {
|
|
51
|
+
// Seat echo after `reset`: `-@all` survives, and `resetchannels` is a marker, not a rule.
|
|
52
|
+
// A service user's `allchannels` echoes as `&*`.
|
|
53
|
+
const seat = parseAclLine(
|
|
54
|
+
'user claude on #hash ~claude:* resetchannels &claude:* -@all +@read +@write -scan',
|
|
55
|
+
);
|
|
56
|
+
expect(seat).toEqual({
|
|
57
|
+
name: 'claude',
|
|
58
|
+
on: true,
|
|
59
|
+
nopass: false,
|
|
60
|
+
hasPassword: true,
|
|
61
|
+
keyPatterns: ['~claude:*'],
|
|
62
|
+
channelPatterns: ['&claude:*'],
|
|
63
|
+
rules: ['-@all', '+@read', '+@write', '-scan'],
|
|
64
|
+
});
|
|
65
|
+
expect(seat.rules).toContain('-@all');
|
|
66
|
+
expect(seat.rules).not.toContain('resetchannels');
|
|
67
|
+
const service = parseAclLine('user litellm on #hash ~* &* +@all -@dangerous -scan');
|
|
68
|
+
expect(service.channelPatterns).toEqual(['&*']);
|
|
69
|
+
expect(service.rules).not.toContain('allchannels');
|
|
70
|
+
});
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
describe('matchesDeclared', () => {
|
|
74
|
+
test('rejects a user widened with an extra key pattern', () => {
|
|
75
|
+
const parsed = parseAclLine('user claude on #h ~claude:* ~* &claude:* +@read +@write -scan');
|
|
76
|
+
expect(matchesDeclared(parsed, claude)).toBe(false);
|
|
77
|
+
});
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
describe('planAclUsers', () => {
|
|
81
|
+
test('splits into create, update, remove — update catches external tampering, not only a prefix change', () => {
|
|
82
|
+
const declared = {
|
|
83
|
+
claude,
|
|
84
|
+
grok: {
|
|
85
|
+
name: 'grok',
|
|
86
|
+
keyPrefix: 'grok:*',
|
|
87
|
+
profile: 'seat',
|
|
88
|
+
password: { fromEnv: 'Y' },
|
|
89
|
+
} satisfies ValkeyAclUser,
|
|
90
|
+
};
|
|
91
|
+
const live = {
|
|
92
|
+
claude: parseAclLine('user claude on #h ~claude:OLD:* &claude:* +@read -scan'), // prefix drift
|
|
93
|
+
grok: parseAclLine('user grok on #h ~grok:* &grok:* +@all -scan'), // widened rules, same prefix
|
|
94
|
+
stale: parseAclLine('user stale on #h ~stale:* &stale:* +@read -scan'),
|
|
95
|
+
};
|
|
96
|
+
const plan = planAclUsers(declared, live, true);
|
|
97
|
+
expect(plan.create.map((u) => u.name)).toEqual([]);
|
|
98
|
+
expect(plan.update.map((u) => u.name).sort()).toEqual(['claude', 'grok']);
|
|
99
|
+
expect(plan.remove).toEqual(['stale']);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
test('a disabled user is an update, not a silent no-op', () => {
|
|
103
|
+
const live = {
|
|
104
|
+
claude: parseAclLine('user claude off #h ~claude:* &claude:* +@read -scan'),
|
|
105
|
+
};
|
|
106
|
+
expect(planAclUsers({ claude }, live).update.map((u) => u.name)).toEqual(['claude']);
|
|
107
|
+
});
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
describe('reconcile', () => {
|
|
111
|
+
test('creates a declared user and read-back confirms it with a fresh seal', async () => {
|
|
112
|
+
const fake = makeFakeValkey({ passwords: {} });
|
|
113
|
+
const attrs = await run(reconcileWithExecutor(fake, props(), undefined, env));
|
|
114
|
+
expect(attrs.users.claude?.keyPrefix).toBe('claude:*');
|
|
115
|
+
expect(attrs.users.claude?.on).toBe(true);
|
|
116
|
+
expect(attrs.users.claude?.hasPassword).toBe(true);
|
|
117
|
+
expect(attrs.users.claude?.passwordSeal).toStartWith('scrypt:');
|
|
118
|
+
expect(fake.commands.some((c) => c.args[0] === 'ACL' && c.args[1] === 'SETUSER')).toBe(true);
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
test('converges a user tampered with outside the stack instead of failing read-back forever', async () => {
|
|
122
|
+
// Same prefix and password, but widened rules (`+@all` over the seat profile) and an extra
|
|
123
|
+
// key pattern — exactly the tampering this family exists to undo.
|
|
124
|
+
const fake = makeFakeValkey({
|
|
125
|
+
acl: { claude: 'user claude on #h ~claude:* ~* &claude:* +@all -scan' },
|
|
126
|
+
});
|
|
127
|
+
const attrs = await run(reconcileWithExecutor(fake, props(), undefined, env));
|
|
128
|
+
expect(attrs.users.claude?.profile).toBe('seat');
|
|
129
|
+
expect(attrs.users.claude?.extraKeyPatterns).toEqual([]);
|
|
130
|
+
expect(attrs.users.claude?.channelPatterns).toEqual(['&claude:*']);
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
test('rotates a password when the stored seal disagrees with the resolved value', async () => {
|
|
134
|
+
const fake = makeFakeValkey({ passwords: {} });
|
|
135
|
+
// The first reconcile creates claude from FAKE-seat-password and mints the seal.
|
|
136
|
+
const first = await run(reconcileWithExecutor(fake, props(), undefined, env));
|
|
137
|
+
const seal = first.users.claude?.passwordSeal;
|
|
138
|
+
expect(seal).toStartWith('scrypt:');
|
|
139
|
+
const setUsers = (): number =>
|
|
140
|
+
fake.commands.filter((c) => c.args[0] === 'ACL' && c.args[1] === 'SETUSER').length;
|
|
141
|
+
const writes = setUsers();
|
|
142
|
+
// Same value again: the seal matches, so nothing is rewritten and the seal is kept.
|
|
143
|
+
const again = await run(reconcileWithExecutor(fake, props(), first, env));
|
|
144
|
+
expect(again.users.claude?.passwordSeal).toBe(seal);
|
|
145
|
+
expect(setUsers()).toBe(writes);
|
|
146
|
+
// A changed variable makes the stored seal stale: exactly one rewrite, a fresh seal.
|
|
147
|
+
const rotated = await run(
|
|
148
|
+
reconcileWithExecutor(fake, props(), first, { ...env, CLAUDE_PW: 'FAKE-rotated-password' }),
|
|
149
|
+
);
|
|
150
|
+
expect(rotated.users.claude?.passwordSeal).not.toBe(seal);
|
|
151
|
+
expect(rotated.users.claude?.passwordSeal).toStartWith('scrypt:');
|
|
152
|
+
expect(setUsers()).toBe(writes + 1);
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
test('never manages the connection username (the admin) — not read, planned or removed', async () => {
|
|
156
|
+
// The consuming stack connects as the instance's admin (docs/valkey.md): if the family
|
|
157
|
+
// planned or removed that user it would delete its own credential and lock itself out.
|
|
158
|
+
const fake = makeFakeValkey({
|
|
159
|
+
acl: { admin: 'user admin on >FAKE-seed ~* +@all' },
|
|
160
|
+
});
|
|
161
|
+
const attrs = await run(reconcileWithExecutor(fake, props(), undefined, env, 'admin'));
|
|
162
|
+
expect(attrs.users.admin).toBeUndefined();
|
|
163
|
+
expect(attrs.users.claude?.keyPrefix).toBe('claude:*');
|
|
164
|
+
// The admin line survives in the fake, and no DELUSER ever names it.
|
|
165
|
+
expect(fake.acl.get('admin')).toContain('user admin on');
|
|
166
|
+
const dels = fake.commands.filter((c) => c.args[0] === 'ACL' && c.args[1] === 'DELUSER');
|
|
167
|
+
expect(dels.every((c) => !c.args.slice(2).includes('admin'))).toBe(true);
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
test('refuses with a typed error when a password variable is missing', async () => {
|
|
171
|
+
const fake = makeFakeValkey({});
|
|
172
|
+
// An explicit empty environment: the refusal must not depend on what this process happens
|
|
173
|
+
// to hold, and nothing may have been written by the time it fails.
|
|
174
|
+
const error = await fails(reconcileWithExecutor(fake, props(), undefined, {}));
|
|
175
|
+
expect(error).toBeInstanceOf(ValkeyAclPasswordMissing);
|
|
176
|
+
expect((error as ValkeyAclPasswordMissing).variable).toBe('CLAUDE_PW');
|
|
177
|
+
expect(fake.commands.filter((c) => c.args[0] === 'ACL' && c.args[1] === 'SETUSER')).toEqual([]);
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
test('removes a user no longer declared', async () => {
|
|
181
|
+
const fake = makeFakeValkey({ acl: { stale: 'user stale on >FAKE-seed ~stale:* +@read' } });
|
|
182
|
+
const attrs = await run(
|
|
183
|
+
reconcileWithExecutor(
|
|
184
|
+
fake,
|
|
185
|
+
{ instance: 'valkey-seats', users: {}, exclusive: true },
|
|
186
|
+
undefined,
|
|
187
|
+
env,
|
|
188
|
+
),
|
|
189
|
+
);
|
|
190
|
+
expect(attrs.users.stale).toBeUndefined();
|
|
191
|
+
expect(fake.commands.some((c) => c.args[0] === 'ACL' && c.args[1] === 'DELUSER')).toBe(true);
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
test('read-back failure when a write does not stick is a typed failure, not a die', async () => {
|
|
195
|
+
// A concurrent mutator re-adds `ghost` after every successful write: every SETUSER reply
|
|
196
|
+
// says OK, so only the read-back — never the write reply — can see the undeclared user.
|
|
197
|
+
const fake = makeFakeValkey({
|
|
198
|
+
reAddAfterWrite: { name: 'ghost', line: 'user ghost on >FAKE-seed ~ghost:* +@read' },
|
|
199
|
+
});
|
|
200
|
+
const error = await fails(
|
|
201
|
+
reconcileWithExecutor(fake, { ...props(), exclusive: true }, undefined, env),
|
|
202
|
+
);
|
|
203
|
+
expect(error).toBeInstanceOf(ValkeyAclReadbackFailed);
|
|
204
|
+
expect((error as ValkeyAclReadbackFailed).user).toBe('ghost');
|
|
205
|
+
});
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
describe('read', () => {
|
|
209
|
+
test('reads live ACL users and stores no password value', async () => {
|
|
210
|
+
const fake = makeFakeValkey({ acl: { claude: 'user claude on >FAKE-seed ~claude:* +@read' } });
|
|
211
|
+
const attrs = await run(readWithExecutor(fake, 'valkey-seats'));
|
|
212
|
+
expect(attrs.users.claude?.keyPrefix).toBe('claude:*');
|
|
213
|
+
expect(attrs.users.claude?.passwordSeal).toBe('');
|
|
214
|
+
});
|
|
215
|
+
});
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `Valkey.AclFile` — the per-instance ACL users, each limited to a key prefix and a fixed command
|
|
3
|
+
* profile, with passwords by reference (never in state). The operations live in `acl-ops.ts`
|
|
4
|
+
* (extracted to keep both files under the cap); this file is the Resource and its handlers.
|
|
5
|
+
*
|
|
6
|
+
* ⛔ ONE ACL FILE PER INSTANCE, SEAT USERS ONLY. A shared file would let a seat user exist on the
|
|
7
|
+
* LiteLLM cache (:6380) and `litellm` on the seat store (:6381) — every seat could poison the
|
|
8
|
+
* response cache. The consuming stack declares one `Valkey.AclFile` per instance, holding only
|
|
9
|
+
* that instance's users.
|
|
10
|
+
* ★ NO OWNERSHIP MARK (H1). `read` answers `Unowned` for a match (the same rule as
|
|
11
|
+
* `Postgres.Database`), so an adopted instance's ACL needs `adopt(true)`.
|
|
12
|
+
*/
|
|
13
|
+
import { Resource } from 'alchemy';
|
|
14
|
+
import { Unowned } from 'alchemy/AdoptPolicy';
|
|
15
|
+
import { isResolved } from 'alchemy/Diff';
|
|
16
|
+
import * as Provider from 'alchemy/Provider';
|
|
17
|
+
import * as Effect from 'effect/Effect';
|
|
18
|
+
import type {
|
|
19
|
+
ValkeyAclFileAttributes,
|
|
20
|
+
ValkeyAclFileProps,
|
|
21
|
+
ValkeyAclUserAttributes,
|
|
22
|
+
} from './acl-attrs.ts';
|
|
23
|
+
import { buildDelUserArgs, passwordState, refusalBeforeWrite } from './acl-form.ts';
|
|
24
|
+
import { sameSet } from './acl-compare.ts';
|
|
25
|
+
import { aclUnreachable } from './acl-connection.ts';
|
|
26
|
+
import { managedUserNames } from './acl-managed.ts';
|
|
27
|
+
import { readAclFileConfig, readParsedUsers, readWithExecutor } from './acl-ops.ts';
|
|
28
|
+
import { reconcileWithExecutor } from './acl-reconcile.ts';
|
|
29
|
+
import { declaredChannels, effectiveKeyPrefix } from './acl-profiles.ts';
|
|
30
|
+
import { type ValkeyConnection, withValkey } from './connection.ts';
|
|
31
|
+
import { ValkeyAclFileRendered } from './errors.ts';
|
|
32
|
+
import { resolveAll } from '../secrets/write-only.ts';
|
|
33
|
+
import { ValkeyServerError } from './transport.ts';
|
|
34
|
+
|
|
35
|
+
export interface ValkeyAclFile extends Resource<
|
|
36
|
+
'Valkey.AclFile',
|
|
37
|
+
ValkeyAclFileProps,
|
|
38
|
+
ValkeyAclFileAttributes
|
|
39
|
+
> {}
|
|
40
|
+
|
|
41
|
+
export const ValkeyAclFile = Resource<ValkeyAclFile>('Valkey.AclFile', {
|
|
42
|
+
defaultRemovalPolicy: 'retain',
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
export const isValkeyAclFile = (value: unknown): value is ValkeyAclFile =>
|
|
46
|
+
(typeof value === 'object' || typeof value === 'function') &&
|
|
47
|
+
value !== null &&
|
|
48
|
+
(value as { Type?: unknown }).Type === 'Valkey.AclFile';
|
|
49
|
+
|
|
50
|
+
export const makeValkeyAclFileHandlers = (
|
|
51
|
+
connect: typeof withValkey = withValkey,
|
|
52
|
+
): Provider.ProviderService<
|
|
53
|
+
ValkeyAclFile,
|
|
54
|
+
ValkeyConnection,
|
|
55
|
+
never,
|
|
56
|
+
never,
|
|
57
|
+
ValkeyConnection,
|
|
58
|
+
ValkeyConnection
|
|
59
|
+
> =>
|
|
60
|
+
ValkeyAclFile.Provider.of({
|
|
61
|
+
list: () => Effect.succeed([]),
|
|
62
|
+
nuke: { skip: true },
|
|
63
|
+
|
|
64
|
+
read: ({ output, olds }) =>
|
|
65
|
+
Effect.gen(function* () {
|
|
66
|
+
const props = output ?? olds;
|
|
67
|
+
// Catch outside connect: acquisition can fail before the executor callback runs.
|
|
68
|
+
// AUTH server refusals keep ValkeyServerError; no failure is reported as absence.
|
|
69
|
+
const live = yield* connect(
|
|
70
|
+
(ex, config) => readWithExecutor(ex, props.instance, config.username),
|
|
71
|
+
props.instance,
|
|
72
|
+
).pipe(Effect.catchTag('ValkeySocketError', aclUnreachable(props.instance)));
|
|
73
|
+
// ★ THE STORED SEALS SURVIVE THE RE-READ (`mcp-server.ts`'s rule): a read must never
|
|
74
|
+
// mint, replace or drop a seal — it reports live users wearing the seals the store
|
|
75
|
+
// already holds, so an unchanged plan stays a no-op instead of churning the row.
|
|
76
|
+
if (output === undefined) return Unowned(live);
|
|
77
|
+
const users: Record<string, ValkeyAclUserAttributes> = {};
|
|
78
|
+
for (const [name, user] of Object.entries(live.users)) {
|
|
79
|
+
users[name] = { ...user, passwordSeal: output.users[name]?.passwordSeal ?? '' };
|
|
80
|
+
}
|
|
81
|
+
return { instance: live.instance, users, managedUsers: managedUserNames(output, olds) };
|
|
82
|
+
}),
|
|
83
|
+
|
|
84
|
+
diff: ({ news, output, olds }) =>
|
|
85
|
+
Effect.gen(function* () {
|
|
86
|
+
if (!isResolved(news)) return undefined;
|
|
87
|
+
const refused = refusalBeforeWrite(news.instance, news.users);
|
|
88
|
+
if (refused !== undefined) return yield* Effect.fail(refused);
|
|
89
|
+
if (output === undefined) return undefined;
|
|
90
|
+
const declared = Object.keys(news.users).sort();
|
|
91
|
+
// Ownership changes need an apply even if live rules already match or a removed user
|
|
92
|
+
// is already absent, so the next plan has the correct managed-user inventory.
|
|
93
|
+
if (!sameSet(declared, [...managedUserNames(output, olds)]))
|
|
94
|
+
return { action: 'update' } as const;
|
|
95
|
+
const liveNames = Object.keys(output.users).sort();
|
|
96
|
+
if (news.exclusive && declared.length !== liveNames.length)
|
|
97
|
+
return { action: 'update' } as const;
|
|
98
|
+
for (const [name, user] of Object.entries(news.users)) {
|
|
99
|
+
const live = output.users[name];
|
|
100
|
+
if (
|
|
101
|
+
live === undefined ||
|
|
102
|
+
live.keyPrefix !== effectiveKeyPrefix(user) ||
|
|
103
|
+
live.extraKeyPatterns.length > 0 ||
|
|
104
|
+
!sameSet(live.channelPatterns, [...declaredChannels(user)]) ||
|
|
105
|
+
live.profile !== user.profile ||
|
|
106
|
+
!live.on ||
|
|
107
|
+
live.nopass ||
|
|
108
|
+
!live.hasPassword
|
|
109
|
+
) {
|
|
110
|
+
return { action: 'update' } as const;
|
|
111
|
+
}
|
|
112
|
+
// A password only drifts when this process holds it and the seal disagrees — an unset
|
|
113
|
+
// variable is `unknown`, and a plan-only environment must not report every user rotated.
|
|
114
|
+
const { values } = resolveAll({ password: user.password });
|
|
115
|
+
if (passwordState(live.passwordSeal, values.password) === 'stale') {
|
|
116
|
+
return { action: 'update' } as const;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
return { action: 'noop' } as const;
|
|
120
|
+
}),
|
|
121
|
+
|
|
122
|
+
reconcile: ({ news, output, olds }) =>
|
|
123
|
+
// The catch covers connect and the whole operation. A server refusal keeps its own tag:
|
|
124
|
+
// the server was reached, it refused. A socket failure names the selected endpoint.
|
|
125
|
+
connect(
|
|
126
|
+
(ex, config) => reconcileWithExecutor(ex, news, output, process.env, config.username, olds),
|
|
127
|
+
news.instance,
|
|
128
|
+
).pipe(Effect.catchTag('ValkeySocketError', aclUnreachable(news.instance))),
|
|
129
|
+
|
|
130
|
+
delete: ({ olds, output }) =>
|
|
131
|
+
connect(
|
|
132
|
+
(ex, config) =>
|
|
133
|
+
Effect.gen(function* () {
|
|
134
|
+
// ⛔ GATE FIRST (F3): a rendered aclfile is the source of truth, so this family deletes
|
|
135
|
+
// nothing there either — the file's renderer re-adds what the kit would DELUSER.
|
|
136
|
+
const aclfile = yield* readAclFileConfig(ex);
|
|
137
|
+
if (aclfile !== '') {
|
|
138
|
+
return yield* Effect.fail(
|
|
139
|
+
new ValkeyAclFileRendered({ instance: olds.instance, path: aclfile }),
|
|
140
|
+
);
|
|
141
|
+
}
|
|
142
|
+
// Removing the declaration removes every user it managed (the engine runs this only
|
|
143
|
+
// when a human opts out of retain); the instance itself is untouched, and undeclared
|
|
144
|
+
// users are preserved (F1). `ACL DELUSER` ignores absent names (Valkey docs);
|
|
145
|
+
// reading first also avoids an empty DELUSER command on a repeated delete (S14).
|
|
146
|
+
// ★ OWNERSHIP IS THE STORED RECORD, THE SAME HELPER REVOCATION USES. `olds` is the last
|
|
147
|
+
// declaration the engine committed — it is written BEFORE reconcile runs, so after a
|
|
148
|
+
// failed update it names users this resource never managed — and `output.users` is
|
|
149
|
+
// every observed user. `managedUserNames` reads `output.managedUsers` (written only by
|
|
150
|
+
// a successful reconcile) and falls back to `olds` for state that predates it. `live`
|
|
151
|
+
// already hides `default` and the connection user, so neither can be a target.
|
|
152
|
+
const live = yield* readParsedUsers(ex, config.username);
|
|
153
|
+
const targets = managedUserNames(output, olds).filter(
|
|
154
|
+
(name) => live[name] !== undefined,
|
|
155
|
+
);
|
|
156
|
+
if (targets.length === 0) return;
|
|
157
|
+
const reply = yield* ex.send(buildDelUserArgs(targets));
|
|
158
|
+
if (reply.kind === 'error') {
|
|
159
|
+
return yield* Effect.fail(new ValkeyServerError({ detail: reply.message }));
|
|
160
|
+
}
|
|
161
|
+
return;
|
|
162
|
+
}),
|
|
163
|
+
olds.instance,
|
|
164
|
+
).pipe(Effect.catchTag('ValkeySocketError', aclUnreachable(olds.instance))),
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
export const valkeyAclFileHandlers = makeValkeyAclFileHandlers();
|
|
168
|
+
|
|
169
|
+
export const ValkeyAclFileProvider = () => Provider.succeed(ValkeyAclFile, valkeyAclFileHandlers);
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/** Named connections keep each resource on its own server when provider layers are combined. */
|
|
2
|
+
import * as Context from 'effect/Context';
|
|
3
|
+
import * as Data from 'effect/Data';
|
|
4
|
+
import * as Effect from 'effect/Effect';
|
|
5
|
+
import * as Layer from 'effect/Layer';
|
|
6
|
+
import * as Option from 'effect/Option';
|
|
7
|
+
import type { ValkeyConnectionConfig } from './connection.ts';
|
|
8
|
+
|
|
9
|
+
export class ValkeyConnectionMissing extends Data.TaggedError('ValkeyConnectionMissing')<{
|
|
10
|
+
readonly instance: string;
|
|
11
|
+
}> {
|
|
12
|
+
override get message(): string {
|
|
13
|
+
return `Valkey connection for "${this.instance}" is missing; configure that instance in valkeyProviders.`;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** The unnamed service is only for direct transport calls; resources always select a name. */
|
|
18
|
+
export class ValkeyConnection extends Context.Service<
|
|
19
|
+
ValkeyConnection,
|
|
20
|
+
Effect.Effect<ValkeyConnectionConfig>
|
|
21
|
+
>()('Valkey.Connection') {}
|
|
22
|
+
|
|
23
|
+
const connectionService = (
|
|
24
|
+
instance?: string,
|
|
25
|
+
): Context.Service<ValkeyConnection, Effect.Effect<ValkeyConnectionConfig>> =>
|
|
26
|
+
instance === undefined
|
|
27
|
+
? ValkeyConnection
|
|
28
|
+
: Context.Service<ValkeyConnection, Effect.Effect<ValkeyConnectionConfig>>(
|
|
29
|
+
`Valkey.Connection:${instance}`,
|
|
30
|
+
);
|
|
31
|
+
|
|
32
|
+
/** Use `Layer.provideMerge` on providers: their handlers resolve this service later. */
|
|
33
|
+
export const valkeyConnection = (config: ValkeyConnectionConfig): Layer.Layer<ValkeyConnection> =>
|
|
34
|
+
Layer.succeed(connectionService(config.instance), Effect.succeed(config));
|
|
35
|
+
|
|
36
|
+
export const resolveValkeyConnection = (
|
|
37
|
+
instance?: string,
|
|
38
|
+
): Effect.Effect<ValkeyConnectionConfig, ValkeyConnectionMissing, ValkeyConnection> =>
|
|
39
|
+
Effect.gen(function* () {
|
|
40
|
+
// No unnamed fallback: a typo must never send a seat ACL to another instance's cache.
|
|
41
|
+
const service = yield* Effect.serviceOption(connectionService(instance));
|
|
42
|
+
if (Option.isNone(service)) {
|
|
43
|
+
return yield* Effect.fail(new ValkeyConnectionMissing({ instance: instance ?? '<unnamed>' }));
|
|
44
|
+
}
|
|
45
|
+
return yield* service.value;
|
|
46
|
+
});
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Where a Valkey instance's socket comes from, and how each lifecycle operation gets an executor.
|
|
3
|
+
*
|
|
4
|
+
* ⛔ NO ESTATE VALUE LIVES HERE. The kit is PUBLIC; CT100's host, the two ports (6380/6381) and the
|
|
5
|
+
* ACL user a seat authenticates as are the CONSUMING stack's values, passed to
|
|
6
|
+
* `valkeyConnection(...)` in a house stack — never a default, never a fallback. The mini's own
|
|
7
|
+
* Valkey, or any later host, is a different `valkeyConnection` call, not a constant.
|
|
8
|
+
* ★ A LAZY `Context.Service` WHOSE VALUE IS AN `Effect` (S24) — the same shape `PostgresConnection`
|
|
9
|
+
* uses. `host` and `port` carry no secret; the ACL user's PASSWORD is not a prop here (S25): the
|
|
10
|
+
* `Valkey.AclFile` resource seals passwords by reference, and the connection itself authenticates
|
|
11
|
+
* with `AUTH` only when a stack supplies a password through `valkeyConnection`'s `password`
|
|
12
|
+
* reference — resolved at call time, never stored.
|
|
13
|
+
* ★ ONE SOCKET PER OPERATION, OPENED AND CLOSED IN A SCOPE. `withValkey` opens a `node:net` socket
|
|
14
|
+
* for the operation, runs it, and closes the socket when it finishes — one connection per
|
|
15
|
+
* `reconcile`, never held across a whole plan.
|
|
16
|
+
*/
|
|
17
|
+
import * as Effect from 'effect/Effect';
|
|
18
|
+
import {
|
|
19
|
+
type ValkeyConnection,
|
|
20
|
+
type ValkeyConnectionMissing,
|
|
21
|
+
resolveValkeyConnection,
|
|
22
|
+
} from './connection-service.ts';
|
|
23
|
+
export {
|
|
24
|
+
ValkeyConnection,
|
|
25
|
+
ValkeyConnectionMissing,
|
|
26
|
+
valkeyConnection,
|
|
27
|
+
} from './connection-service.ts';
|
|
28
|
+
import { type Socket, connect } from 'node:net';
|
|
29
|
+
import { type Environment, type FromEnv, resolveAll } from '../secrets/write-only.ts';
|
|
30
|
+
import { ValkeyAuthPasswordMissing } from './errors.ts';
|
|
31
|
+
import {
|
|
32
|
+
DEFAULT_SOCKET_TIMEOUT_MS,
|
|
33
|
+
type ValkeyExecutor,
|
|
34
|
+
ValkeyServerError,
|
|
35
|
+
ValkeySocketError,
|
|
36
|
+
type ValkeyTransportError,
|
|
37
|
+
makeSocketExecutor,
|
|
38
|
+
} from './transport.ts';
|
|
39
|
+
|
|
40
|
+
/** What a stack passes to reach one instance. `host`/`port` are the TCP endpoint; the ACL
|
|
41
|
+
* `username` and optional `password` (a `FromEnv` reference, never a value) are the credentials a
|
|
42
|
+
* stack authenticates with. Omitting both sends no `AUTH` — a bare `default` connection, which is
|
|
43
|
+
* correct only for a scratch instance a test owns.
|
|
44
|
+
*
|
|
45
|
+
* ⛔ A USERNAME REQUIRES A PASSWORD. The discriminated union makes `username` without `password`
|
|
46
|
+
* unrepresentable: on an instance whose `default` user is off (CT100's ACL files ship `user
|
|
47
|
+
* default off`), a username-only connection cannot authenticate at all, and silently skipping
|
|
48
|
+
* `AUTH` would surface much later as a confusing `NOAUTH` from the first command. The bare
|
|
49
|
+
* branch carries `username?: undefined` — a type marker, not a prop — so `config.username`
|
|
50
|
+
* reads on the whole union without narrowing first.
|
|
51
|
+
*
|
|
52
|
+
* `timeoutMs` bounds connect, each read, and the socket idle timer (default 10s). A blackholed
|
|
53
|
+
* host must stall a plan no longer than that deadline. */
|
|
54
|
+
export type ValkeyConnectionConfig = {
|
|
55
|
+
/** Resource routing key: required by valkeyProviders; omit only for direct transport calls. */
|
|
56
|
+
readonly instance?: string;
|
|
57
|
+
readonly host: string;
|
|
58
|
+
readonly port: number;
|
|
59
|
+
/** Per-connect/per-read deadline in ms. */
|
|
60
|
+
readonly timeoutMs?: number;
|
|
61
|
+
} & (
|
|
62
|
+
| { readonly username?: undefined }
|
|
63
|
+
| {
|
|
64
|
+
readonly username?: string;
|
|
65
|
+
readonly password: FromEnv;
|
|
66
|
+
}
|
|
67
|
+
);
|
|
68
|
+
|
|
69
|
+
const openSocket = (
|
|
70
|
+
host: string,
|
|
71
|
+
port: number,
|
|
72
|
+
timeoutMs: number,
|
|
73
|
+
): Effect.Effect<Socket, ValkeySocketError> =>
|
|
74
|
+
Effect.tryPromise({
|
|
75
|
+
try: (signal) =>
|
|
76
|
+
new Promise<Socket>((resolve, reject) => {
|
|
77
|
+
const socket = connect({ host, port });
|
|
78
|
+
let settled = false;
|
|
79
|
+
const onConnect = () => {
|
|
80
|
+
if (settled) return;
|
|
81
|
+
settled = true;
|
|
82
|
+
socket.off('error', onError);
|
|
83
|
+
socket.off('timeout', onTimeout);
|
|
84
|
+
signal.removeEventListener('abort', onAbort);
|
|
85
|
+
clearTimeout(timer);
|
|
86
|
+
socket.setTimeout(0);
|
|
87
|
+
resolve(socket);
|
|
88
|
+
};
|
|
89
|
+
const onError = (error: Error) => {
|
|
90
|
+
if (settled) return;
|
|
91
|
+
settled = true;
|
|
92
|
+
socket.off('connect', onConnect);
|
|
93
|
+
socket.off('timeout', onTimeout);
|
|
94
|
+
signal.removeEventListener('abort', onAbort);
|
|
95
|
+
clearTimeout(timer);
|
|
96
|
+
socket.destroy();
|
|
97
|
+
reject(new ValkeySocketError({ reason: error.message }));
|
|
98
|
+
};
|
|
99
|
+
const onTimeout = () => onError(new Error('connect timed out'));
|
|
100
|
+
const onAbort = () => onError(new Error('connect interrupted'));
|
|
101
|
+
// `node:net`'s own connect timeout is minutes-long and not configurable. Both the
|
|
102
|
+
// abort and `socket.setTimeout` bound the handshake; the read deadline is separate
|
|
103
|
+
// (a host that accepts and then sends nothing has already connected).
|
|
104
|
+
const timer = setTimeout(onTimeout, timeoutMs);
|
|
105
|
+
timer.unref();
|
|
106
|
+
socket.setTimeout(timeoutMs);
|
|
107
|
+
signal.addEventListener('abort', onAbort);
|
|
108
|
+
socket.once('connect', onConnect);
|
|
109
|
+
socket.once('error', onError);
|
|
110
|
+
socket.once('timeout', onTimeout);
|
|
111
|
+
}),
|
|
112
|
+
catch: (cause) =>
|
|
113
|
+
cause instanceof ValkeySocketError ? cause : new ValkeySocketError({ reason: String(cause) }),
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
/** `AUTH` when the config carries a password, resolved at call time; a missing or empty variable
|
|
117
|
+
* is the typed `ValkeyAuthPasswordMissing` (S21: a fact this family checked, not a sniffed server
|
|
118
|
+
* reply). A password without a username authenticates as `default`. */
|
|
119
|
+
export const auth = (
|
|
120
|
+
executor: ValkeyExecutor,
|
|
121
|
+
config: ValkeyConnectionConfig,
|
|
122
|
+
env: Environment,
|
|
123
|
+
): Effect.Effect<void, ValkeyTransportError | ValkeyAuthPasswordMissing> => {
|
|
124
|
+
if (!('password' in config)) return Effect.void;
|
|
125
|
+
const { username, password } = config;
|
|
126
|
+
return Effect.gen(function* () {
|
|
127
|
+
const { values, missing } = resolveAll({ password }, env);
|
|
128
|
+
const value = values.password;
|
|
129
|
+
if (missing.length > 0 || value === undefined) {
|
|
130
|
+
return yield* Effect.fail(
|
|
131
|
+
new ValkeyAuthPasswordMissing({ username, variable: password.fromEnv }),
|
|
132
|
+
);
|
|
133
|
+
}
|
|
134
|
+
const reply = yield* executor.send(
|
|
135
|
+
username === undefined ? ['AUTH', value] : ['AUTH', username, value],
|
|
136
|
+
);
|
|
137
|
+
if (reply.kind === 'error')
|
|
138
|
+
return yield* Effect.fail(new ValkeyServerError({ detail: 'Valkey AUTH was refused' }));
|
|
139
|
+
});
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Run one operation against a freshly opened, freshly closed socket. Every `read` and `reconcile`
|
|
144
|
+
* in this family calls this once for the whole operation, never once per command.
|
|
145
|
+
*/
|
|
146
|
+
export const withValkey = <A, E>(
|
|
147
|
+
build: (
|
|
148
|
+
executor: ValkeyExecutor,
|
|
149
|
+
config: ValkeyConnectionConfig,
|
|
150
|
+
) => Effect.Effect<A, E | ValkeyTransportError>,
|
|
151
|
+
instance?: string,
|
|
152
|
+
): Effect.Effect<
|
|
153
|
+
A,
|
|
154
|
+
E | ValkeyTransportError | ValkeyAuthPasswordMissing | ValkeyConnectionMissing,
|
|
155
|
+
ValkeyConnection
|
|
156
|
+
> =>
|
|
157
|
+
Effect.gen(function* () {
|
|
158
|
+
const config = yield* resolveValkeyConnection(instance);
|
|
159
|
+
const timeoutMs = config.timeoutMs ?? DEFAULT_SOCKET_TIMEOUT_MS;
|
|
160
|
+
const socket = yield* openSocket(config.host, config.port, timeoutMs);
|
|
161
|
+
// Listener first, then the idle deadline. A timeout destroys with an error, and Node
|
|
162
|
+
// exits the process if that `error` event has no listener.
|
|
163
|
+
const executor = makeSocketExecutor(socket, timeoutMs);
|
|
164
|
+
socket.setTimeout(timeoutMs);
|
|
165
|
+
const onIdle = () => {
|
|
166
|
+
socket.destroy(new Error('socket timed out'));
|
|
167
|
+
};
|
|
168
|
+
socket.on('timeout', onIdle);
|
|
169
|
+
return yield* Effect.ensuring(
|
|
170
|
+
Effect.gen(function* () {
|
|
171
|
+
yield* auth(executor, config, process.env);
|
|
172
|
+
return yield* build(executor, config);
|
|
173
|
+
}),
|
|
174
|
+
Effect.sync(() => {
|
|
175
|
+
socket.setTimeout(0);
|
|
176
|
+
socket.off('timeout', onIdle);
|
|
177
|
+
socket.destroy();
|
|
178
|
+
}),
|
|
179
|
+
);
|
|
180
|
+
});
|