feat: support optional per-installation Basic Auth on the MCP proxy
All checks were successful
CI / test (push) Successful in 31s

Lets a client still using Basic Auth credentials (e.g. against the
previous nginx + htpasswd setup) keep working without switching to a
bearer token immediately. Bearer auth remains the default and the
only option when no Basic Auth credentials are configured.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-10-08 05:05:11 +02:00
parent 021fa2220e
commit fb59d422a2
6 changed files with 124 additions and 7 deletions

View File

@@ -30,3 +30,16 @@ CREATE TABLE IF NOT EXISTS installations (
updated_at TEXT NOT NULL
);
`);
// Added after the initial release — ALTER TABLE has no "ADD COLUMN IF NOT EXISTS" in SQLite,
// so just ignore the "duplicate column" error on a DB that already has it.
for (const ddl of [
"ALTER TABLE installations ADD COLUMN basic_auth_username TEXT",
"ALTER TABLE installations ADD COLUMN basic_auth_password_encrypted TEXT",
]) {
try {
db.exec(ddl);
} catch (err) {
if (!(err as Error).message.includes("duplicate column")) throw err;
}
}

View File

@@ -29,6 +29,8 @@ export interface Installation {
hostEnvVar: string;
env: Record<string, string>;
bearerToken: string | null;
basicAuthUsername: string | null;
basicAuthPassword: string | null;
createdAt: string;
updatedAt: string;
}
@@ -50,6 +52,8 @@ interface Row {
host_env_var: string;
bearer_token_encrypted: string | null;
env_json_encrypted: string | null;
basic_auth_username: string | null;
basic_auth_password_encrypted: string | null;
created_at: string;
updated_at: string;
}
@@ -72,6 +76,8 @@ function rowToInstallation(row: Row): Installation {
hostEnvVar: row.host_env_var,
env: row.env_json_encrypted ? JSON.parse(decryptSecret(row.env_json_encrypted)) : {},
bearerToken: row.bearer_token_encrypted ? decryptSecret(row.bearer_token_encrypted) : null,
basicAuthUsername: row.basic_auth_username,
basicAuthPassword: row.basic_auth_password_encrypted ? decryptSecret(row.basic_auth_password_encrypted) : null,
createdAt: row.created_at,
updatedAt: row.updated_at,
};
@@ -132,6 +138,8 @@ export interface InstallationUpdate {
hostEnvVar?: string;
env?: Record<string, string>;
bearerToken?: string | null;
basicAuthUsername?: string | null;
basicAuthPassword?: string | null;
gitRef?: string | null;
}
@@ -150,6 +158,10 @@ export function updateInstallation(id: string, patch: InstallationUpdate): Insta
hostEnvVar: patch.hostEnvVar ?? current.hostEnvVar,
env: patch.env ?? current.env,
bearerToken: patch.bearerToken !== undefined ? patch.bearerToken : current.bearerToken,
basicAuthUsername:
patch.basicAuthUsername !== undefined ? patch.basicAuthUsername : current.basicAuthUsername,
basicAuthPassword:
patch.basicAuthPassword !== undefined ? patch.basicAuthPassword : current.basicAuthPassword,
gitRef: patch.gitRef !== undefined ? patch.gitRef : current.gitRef,
};
@@ -165,6 +177,8 @@ export function updateInstallation(id: string, patch: InstallationUpdate): Insta
host_env_var = @hostEnvVar,
env_json_encrypted = @envJsonEncrypted,
bearer_token_encrypted = @bearerTokenEncrypted,
basic_auth_username = @basicAuthUsername,
basic_auth_password_encrypted = @basicAuthPasswordEncrypted,
git_ref = @gitRef,
updated_at = @updatedAt
WHERE id = @id`,
@@ -180,6 +194,8 @@ export function updateInstallation(id: string, patch: InstallationUpdate): Insta
hostEnvVar: next.hostEnvVar,
envJsonEncrypted: encryptSecret(JSON.stringify(next.env)),
bearerTokenEncrypted: next.bearerToken ? encryptSecret(next.bearerToken) : null,
basicAuthUsername: next.basicAuthUsername,
basicAuthPasswordEncrypted: next.basicAuthPassword ? encryptSecret(next.basicAuthPassword) : null,
gitRef: next.gitRef,
updatedAt: new Date().toISOString(),
});