Files
Michael Kriese 320f770c36 fix(cache): hash URL credentials in cache keys
Co-Authored-By: Claude Fable 5.1 <michael.kriese+claude-code@mend.io>
Co-Authored-By: Claude Opus 5 <michael.kriese+claude-code@mend.io>
Co-Authored-By: Claude Opus 5.5 <michael.kriese+claude-code@mend.io>
2026-10-20 09:05:00 +01:00

256 lines
8.7 KiB
TypeScript

import { isEmptyString } from '@sindresorhus/is';
import { PLATFORM_HOST_TYPES } from '../../constants/platforms.ts';
import { logger } from '../../logger/index.ts';
import type { HostRule } from '../../types/index.ts';
import { detectPlatform } from '../common.ts';
import type { ResolvedChildEnv } from '../exec/utils.ts';
import { find, findDetachedCredentials, getAll } from '../host-rules.ts';
import { regEx } from '../regex.ts';
import { createURLFromHostOrURL, isHttpUrl } from '../url.ts';
import { addGitConfigEnvironmentVariables } from './config.ts';
import type { AuthenticationRule } from './types.ts';
import { getTokenCredentials, parseGitUrl } from './url.ts';
const platformHostTypes = new Set<string>(PLATFORM_HOST_TYPES);
const githubApiUrls = new Set([
'github.com',
'api.github.com',
'https://api.github.com',
'https://api.github.com/',
]);
/**
* Append Git authentication configuration to a resolved child environment.
* @returns a new environment without modifying the input
*/
export function getGitAuthenticatedEnvironmentVariables(
environmentVariables: Readonly<ResolvedChildEnv>,
originalGitUrl: string,
{ token, username, password, hostType, matchHost }: HostRule,
): ResolvedChildEnv {
if (!token && !(username && password)) {
logger.warn(
{ host: matchHost },
`Could not create environment variable for host as neither token or username and password was set`,
);
return { ...environmentVariables };
}
let authenticationRules: AuthenticationRule[];
if (token) {
authenticationRules = getAuthenticationRulesWithToken(
originalGitUrl,
hostType,
token,
);
} else {
const encodedUsername = encodeURIComponent(username!);
const encodedPassword = encodeURIComponent(password!);
authenticationRules = getAuthenticationRules(
originalGitUrl,
hostType,
`${encodedUsername}:${encodedPassword}`,
);
}
return addGitConfigEnvironmentVariables(
environmentVariables,
authenticationRules.map((rule) => ({
key: `url.${rule.url}.insteadOf`,
value: rule.insteadOf,
})),
);
}
function getAuthenticationRulesWithToken(
url: string,
hostType: string | undefined | null,
authToken: string,
): AuthenticationRule[] {
const type = hostType ?? detectPlatform(url);
const platform =
hostType && platformHostTypes.has(hostType)
? hostType
: detectPlatform(url);
return getAuthenticationRules(
url,
type,
getTokenCredentials(platform, authToken),
);
}
/**
* Generates the authentication rules for later git usage for the given host
* @link https://coolaj86.com/articles/vanilla-devops-git-credentials-cheatsheet/
* @param gitUrl Git repository URL
* @param hostType Git host type
* @param token Authentication token or `username:password` string
*/
export function getAuthenticationRules(
gitUrl: string,
hostType: string | undefined | null,
token: string,
): AuthenticationRule[] {
const authenticationRules = [];
const hasUser = token.split(':').length > 1;
const insteadUrl = parseGitUrl(gitUrl);
let sshPort = insteadUrl.port;
if (hostType === 'bitbucket-server') {
// For Bitbucket Server/Data Center, `source` must be `bitbucket-server`
// to generate HTTP(s) URLs correctly.
// https://github.com/IonicaBizau/git-url-parse/blob/28828546c148d58bbcff61409915a4e1e8f7eb11/lib/index.js#L304
insteadUrl.source = 'bitbucket-server';
// sshPort is string, wrong type!
// https://github.com/DefinitelyTyped/DefinitelyTyped/pull/72361
// https://github.com/IonicaBizau/parse-path/blob/87f71f273da90f85f6845937a70b7c032eeae4e3/lib/index.js#L45
// v8 ignore next -- TODO: add test #40625
if (!sshPort || isEmptyString(sshPort)) {
// By default, bitbucket-server SSH port is 7999.
// For non-default port, the generated auth config will likely be incorrect.
sshPort = '7999';
}
}
const url = { ...insteadUrl };
// git-url-parse would prefix `x-token-auth:` again on bitbucket.org
if (hasUser && url.source === 'bitbucket.org') {
url.source = 'bitbucket';
}
const protocol = regEx(/^https?$/).test(url.protocol)
? url.protocol
: 'https';
// ssh protocol with user if empty
url.token = hasUser ? token : `ssh:${token}`;
authenticationRules.push({
url: url.toString(protocol),
// only edge case, need to stringify ourself because the exact syntax is not supported by the library
// https://github.com/IonicaBizau/git-url-parse/blob/246c9119fb42c2ea1c280028fe77c53eb34c190c/lib/index.js#L246
insteadOf: `ssh://git@${insteadUrl.resource}${
sshPort ? `:${sshPort}` : ''
}/${insteadUrl.full_name}${insteadUrl.git_suffix ? '.git' : ''}`,
});
// alternative ssh protocol with user if empty
url.token = hasUser ? token : `git:${token}`;
authenticationRules.push({
url: url.toString(protocol),
insteadOf: { ...insteadUrl, port: sshPort }.toString('ssh'),
});
// https protocol with no user as default fallback
url.token = token;
authenticationRules.push({
url: url.toString(protocol),
insteadOf: insteadUrl.toString(protocol),
});
return authenticationRules;
}
/**
* Appends Git authentication for the repository `url` from the credentials registered from URL credentials for it with `hostType`, a username without a password as a percent-encoded token; configured host rules come through `getAll()` instead.
* @returns a new environment without modifying the input
*/
function getGitUrlEnvironmentVariables(
environmentVariables: Readonly<ResolvedChildEnv>,
url: string,
hostType: string,
): ResolvedChildEnv {
const credentials = findDetachedCredentials({ hostType, url });
if (!credentials?.username) {
return { ...environmentVariables };
}
const auth: HostRule = credentials.password
? credentials
: { token: encodeURIComponent(credentials.username) };
return getGitAuthenticatedEnvironmentVariables(environmentVariables, url, {
...auth,
hostType,
});
}
/**
* Appends Git authentication from the host rules with a `matchHost` for the platform host types and `hostTypes`, and, when `url` is given, for that single repository from the credentials registered from URL credentials for it with each of `hostTypes`.
* @returns a new environment without modifying the input
*/
export function getGitEnvironmentVariables(
environmentVariables: Readonly<ResolvedChildEnv>,
hostTypes: readonly string[] = [],
url?: string,
): ResolvedChildEnv {
let gitEnvironmentVariables: ResolvedChildEnv = { ...environmentVariables };
// hard-coded logic to use authentication for github.com based on the githubToken for api.github.com
const gitHubHostRule = find({
hostType: 'github',
url: 'https://api.github.com/',
});
if (gitHubHostRule?.token) {
gitEnvironmentVariables = getGitAuthenticatedEnvironmentVariables(
gitEnvironmentVariables,
'https://github.com/',
gitHubHostRule,
);
}
// Manager-scoped rules are opt-in so unrelated credentials are not exposed to Git.
const gitAllowedHostTypes = new Set<string>([
...PLATFORM_HOST_TYPES,
...hostTypes,
]);
// filter rules without `matchHost` and `token` or username and password and github api github rules
const hostRules = getAll()
.filter((r) => r.matchHost && (r.token ?? (r.username && r.password)))
.filter((r) => !gitHubHostRule || !githubApiUrls.has(r.matchHost!));
// for each hostRule without hostType we add additional authentication variables to the environmentVariables
// for each hostRule with hostType we add additional authentication variables to the environmentVariables
for (const hostRule of hostRules) {
if (!hostRule.hostType || gitAllowedHostTypes.has(hostRule.hostType)) {
gitEnvironmentVariables = addAuthFromHostRule(
hostRule,
gitEnvironmentVariables,
);
}
}
// `getAll()` leaves out the rules registered from URL credentials, which only apply to the repository they came from
if (url) {
for (const hostType of hostTypes) {
gitEnvironmentVariables = getGitUrlEnvironmentVariables(
gitEnvironmentVariables,
url,
hostType,
);
}
}
return gitEnvironmentVariables;
}
function addAuthFromHostRule(
hostRule: HostRule,
environmentVariables: Readonly<ResolvedChildEnv>,
): ResolvedChildEnv {
const httpUrl = createURLFromHostOrURL(hostRule.matchHost!)?.toString();
if (isHttpUrl(httpUrl)) {
logger.trace(`Adding Git authentication for ${httpUrl} using token auth.`);
return getGitAuthenticatedEnvironmentVariables(
environmentVariables,
httpUrl!,
hostRule,
);
}
logger.debug(
`Could not parse registryUrl ${hostRule.matchHost!} or not using http(s). Ignoring`,
);
return { ...environmentVariables };
}