Some checks failed
build-packages / resolve bundled mosh-client (push) Has been cancelled
build-packages / resolve bundled et-client (push) Has been cancelled
build-packages / build-macos (push) Has been cancelled
build-packages / build-windows (push) Has been cancelled
build-packages / build-linux-x64 (push) Has been cancelled
build-packages / build-linux-arm64 (push) Has been cancelled
build-packages / release (push) Has been cancelled
build-packages / update Nix release metadata (push) Has been cancelled
build-packages / bump homebrew tap (push) Has been cancelled
test / lint-and-test (push) Has been cancelled
AI automation / Route event (push) Has been cancelled
AI automation / Hand reopened issue to maintainers (push) Has been cancelled
AI automation / Clean source issue state (push) Has been cancelled
AI automation / Reconcile handoffs (push) Has been cancelled
AI automation / Classify issue (push) Has been cancelled
AI automation / Claude Code smoke (push) Has been cancelled
AI automation / Review issue follow-up (push) Has been cancelled
AI automation / Publish issue follow-up (push) Has been cancelled
AI automation / Implement with Claude Code (push) Has been cancelled
AI automation / Publish implement PR (push) Has been cancelled
AI automation / Continue queued issue comments (push) Has been cancelled
AI automation / Codex review loop (push) Has been cancelled
AI automation / Publish Codex fix (push) Has been cancelled
AI automation / Clear Codex dispatch marker (push) Has been cancelled
AI automation / Own PR re-request Codex (push) Has been cancelled
AI automation / External PR re-request Codex (push) Has been cancelled
AI automation / Poll Codex reaction / retry (push) Has been cancelled
build-et-binaries / build-linux-x64 (push) Has been cancelled
build-et-binaries / build-linux-arm64 (push) Has been cancelled
build-et-binaries / build-macos-universal (push) Has been cancelled
build-et-binaries / build-windows-x64 (push) Has been cancelled
build-et-binaries / release (push) Has been cancelled
241 lines
7.2 KiB
TypeScript
241 lines
7.2 KiB
TypeScript
import { isEncryptedCredentialPlaceholder, sanitizeCredentialValue } from "./credentials";
|
|
import type { GroupConfig, Host, Identity, ProxyConfig, ProxyProfile } from "./models";
|
|
|
|
const cloneProxyConfig = (config: ProxyConfig): ProxyConfig => ({
|
|
...config,
|
|
});
|
|
|
|
export const isValidProxyPort = (port: unknown): boolean => {
|
|
const value = Number(port);
|
|
return Number.isInteger(value) && value >= 1 && value <= 65535;
|
|
};
|
|
|
|
export const isProxyCommandConfig = (config: ProxyConfig | undefined): boolean => {
|
|
return config?.type === "command";
|
|
};
|
|
|
|
export const isEmptyProxyConfigDraft = (config: ProxyConfig | undefined): boolean => {
|
|
if (!config) return true;
|
|
if (isProxyCommandConfig(config)) return !config.command?.trim();
|
|
return !config.host.trim() && !config.username?.trim() && !config.password?.trim();
|
|
};
|
|
|
|
export const isCompleteProxyConfig = (config: ProxyConfig | undefined): boolean => {
|
|
if (isProxyCommandConfig(config)) return Boolean(config?.command?.trim());
|
|
return Boolean(config?.host.trim()) && isValidProxyPort(config?.port);
|
|
};
|
|
|
|
export const normalizeManualProxyConfig = (
|
|
config: ProxyConfig | undefined,
|
|
): ProxyConfig | undefined => {
|
|
if (!config || isEmptyProxyConfigDraft(config)) return undefined;
|
|
if (isProxyCommandConfig(config)) {
|
|
return {
|
|
type: "command",
|
|
host: "",
|
|
port: 0,
|
|
command: config.command?.trim(),
|
|
};
|
|
}
|
|
const normalized: ProxyConfig = {
|
|
type: config.type,
|
|
host: config.host.trim(),
|
|
port: Number(config.port),
|
|
};
|
|
if (config.identityId) {
|
|
normalized.identityId = config.identityId;
|
|
} else {
|
|
normalized.username = config.username?.trim() || undefined;
|
|
normalized.password = config.password || undefined;
|
|
}
|
|
return normalized;
|
|
};
|
|
|
|
export const updateProxyConfigField = (
|
|
config: ProxyConfig | undefined,
|
|
field: keyof ProxyConfig,
|
|
value: ProxyConfig[keyof ProxyConfig],
|
|
): ProxyConfig => {
|
|
const next: ProxyConfig = {
|
|
type: config?.type || "http",
|
|
host: config?.host || "",
|
|
port: config?.port || 8080,
|
|
...config,
|
|
[field]: value,
|
|
};
|
|
if (field === "identityId") {
|
|
delete next.username;
|
|
delete next.password;
|
|
if (!value) delete next.identityId;
|
|
} else if (field === "username" || field === "password") {
|
|
delete next.identityId;
|
|
} else if (field === "type") {
|
|
if (value === "command") {
|
|
delete next.identityId;
|
|
delete next.username;
|
|
delete next.password;
|
|
} else {
|
|
delete next.command;
|
|
}
|
|
}
|
|
return next;
|
|
};
|
|
|
|
const findIdentity = (
|
|
identityId: string | undefined,
|
|
identities: Identity[] = [],
|
|
): Identity | undefined => {
|
|
if (!identityId) return undefined;
|
|
return identities.find((identity) => identity.id === identityId);
|
|
};
|
|
|
|
export const findMissingProxyIdentityId = (
|
|
config: ProxyConfig | undefined,
|
|
identities: Identity[] = [],
|
|
): string | undefined => {
|
|
if (!config?.identityId) return undefined;
|
|
return findIdentity(config.identityId, identities) ? undefined : config.identityId;
|
|
};
|
|
|
|
export const findIncompleteProxyIdentityId = (
|
|
config: ProxyConfig | undefined,
|
|
identities: Identity[] = [],
|
|
): string | undefined => {
|
|
if (!config?.identityId) return undefined;
|
|
const identity = findIdentity(config.identityId, identities);
|
|
if (!identity) return undefined;
|
|
if (!identity.username?.trim()) return config.identityId;
|
|
if (isEncryptedCredentialPlaceholder(identity.password)) return undefined;
|
|
return sanitizeCredentialValue(identity.password)
|
|
? undefined
|
|
: config.identityId;
|
|
};
|
|
|
|
export const hasMissingProxyIdentity = (
|
|
config: ProxyConfig | undefined,
|
|
identities: Identity[] = [],
|
|
): boolean => {
|
|
return Boolean(findMissingProxyIdentityId(config, identities));
|
|
};
|
|
|
|
export const hasIncompleteProxyIdentity = (
|
|
config: ProxyConfig | undefined,
|
|
identities: Identity[] = [],
|
|
): boolean => {
|
|
return Boolean(findIncompleteProxyIdentityId(config, identities));
|
|
};
|
|
|
|
export const formatMissingProxyIdentityMessage = (
|
|
ownerLabel: string,
|
|
): string => {
|
|
return `Proxy identity for "${ownerLabel}" is missing. Open settings and select a valid proxy identity.`;
|
|
};
|
|
|
|
export const formatIncompleteProxyIdentityMessage = (
|
|
ownerLabel: string,
|
|
): string => {
|
|
return `Proxy identity for "${ownerLabel}" is incomplete. Select an identity with a username and password, or enter proxy credentials manually.`;
|
|
};
|
|
|
|
export const resolveProxyConfigAuth = (
|
|
config: ProxyConfig,
|
|
identities?: Identity[],
|
|
): ProxyConfig => {
|
|
const identity = findIdentity(config.identityId, identities);
|
|
const username = config.identityId
|
|
? identity?.username?.trim()
|
|
: config.username?.trim();
|
|
const password = config.identityId
|
|
? sanitizeCredentialValue(identity?.password)
|
|
: sanitizeCredentialValue(config.password);
|
|
|
|
const resolved: ProxyConfig = {
|
|
type: config.type,
|
|
host: config.host,
|
|
port: config.port,
|
|
username: username || undefined,
|
|
password,
|
|
};
|
|
if (config.command !== undefined) {
|
|
resolved.command = config.command;
|
|
}
|
|
return resolved;
|
|
};
|
|
|
|
export const hasUnreadableProxyCredential = (
|
|
config: ProxyConfig | undefined,
|
|
identities?: Identity[],
|
|
): boolean => {
|
|
if (!config) return false;
|
|
const identity = findIdentity(config.identityId, identities);
|
|
const username = config.identityId
|
|
? identity?.username?.trim()
|
|
: config.username?.trim();
|
|
const rawPassword = config.identityId ? identity?.password : config.password;
|
|
return Boolean(username) &&
|
|
isEncryptedCredentialPlaceholder(rawPassword) &&
|
|
!sanitizeCredentialValue(rawPassword);
|
|
};
|
|
|
|
export const hasUsableProxyConfig = (config: ProxyConfig | undefined): boolean => {
|
|
return isCompleteProxyConfig(config);
|
|
};
|
|
|
|
export const formatProxyConfigEndpoint = (config: ProxyConfig | undefined): string => {
|
|
if (!config) return "";
|
|
if (isProxyCommandConfig(config)) return "ProxyCommand";
|
|
return `${config.host}:${config.port}`;
|
|
};
|
|
|
|
export const formatProxyConfigType = (config: ProxyConfig | undefined): string => {
|
|
if (!config) return "";
|
|
if (isProxyCommandConfig(config)) return "ProxyCommand";
|
|
return config.type.toUpperCase();
|
|
};
|
|
|
|
export function findProxyProfile(
|
|
proxyProfileId: string | undefined,
|
|
proxyProfiles: ProxyProfile[],
|
|
): ProxyProfile | undefined {
|
|
if (!proxyProfileId) return undefined;
|
|
return proxyProfiles.find((profile) => profile.id === proxyProfileId);
|
|
}
|
|
|
|
export function materializeHostProxyProfile<T extends Host>(
|
|
host: T,
|
|
proxyProfiles: ProxyProfile[],
|
|
): T {
|
|
if (host.proxyConfig || !host.proxyProfileId) return host;
|
|
const profile = findProxyProfile(host.proxyProfileId, proxyProfiles);
|
|
if (!profile) return host;
|
|
return {
|
|
...host,
|
|
proxyConfig: cloneProxyConfig(profile.config),
|
|
};
|
|
}
|
|
|
|
const clearProxyProfileId = <T extends { proxyProfileId?: string }>(
|
|
item: T,
|
|
proxyProfileId: string,
|
|
): T => {
|
|
if (item.proxyProfileId !== proxyProfileId) return item;
|
|
const { proxyProfileId: _proxyProfileId, ...rest } = item;
|
|
return rest as T;
|
|
};
|
|
|
|
export function removeProxyProfileReferences(
|
|
proxyProfileId: string,
|
|
data: {
|
|
hosts: Host[];
|
|
groupConfigs: GroupConfig[];
|
|
},
|
|
): {
|
|
hosts: Host[];
|
|
groupConfigs: GroupConfig[];
|
|
} {
|
|
return {
|
|
hosts: data.hosts.map((host) => clearProxyProfileId(host, proxyProfileId)),
|
|
groupConfigs: data.groupConfigs.map((config) => clearProxyProfileId(config, proxyProfileId)),
|
|
};
|
|
}
|