chore: merge origin/dev into codex auth profile branch

This commit is contained in:
Tam Nhu Tran committed 2026-05-17 15:33:06 -04:00
commit ea421c4a20
14 files changed
+344 -45

No files matched your search

@@ -152,6 +152,92 @@ describe('ai-provider service stable ids', () => {
).toBe('sk-openrouter');
});
it('redacts custom headers and URL userinfo in provider list views', async () => {
const { listAiProviders } = await loadAiProviderService();
writeCliproxyConfig(tempHome, {
'claude-api-key': [
{
id: 'claude-secret-route',
'api-key': 'sk-ant-provider-secret-1234',
'base-url': 'https://baseuser:basepass@anthropic.example/v1',
'proxy-url': 'http://proxyuser:proxypass@example.internal:8080',
headers: {
Authorization: 'Bearer HEADER-SECRET-123456',
'X-API-Key': 'x-api-key-secret-abcdef',
},
},
],
'openai-compatibility': [
{
id: 'openrouter-secret-route',
name: 'openrouter',
'base-url': 'https://routeruser:routerpass@openrouter.example/api/v1',
headers: {
Authorization: 'Bearer OPENAI-COMPAT-HEADER-SECRET',
},
'api-key-entries': [{ 'api-key': 'sk-openrouter-secret-9999' }],
},
],
});
const listed = await listAiProviders();
const claudeEntry = listed.families.find((entry) => entry.id === 'claude-api-key')?.entries[0];
const openAiEntry = listed.families.find((entry) => entry.id === 'openai-compatibility')
?.entries[0];
expect(claudeEntry?.baseUrl).toBe('https://***:***@anthropic.example/v1');
expect(claudeEntry?.proxyUrl).toBe('http://***:***@example.internal:8080/');
expect(claudeEntry?.headers).toEqual([
{ key: 'Authorization', value: '...3456' },
{ key: 'X-API-Key', value: '...cdef' },
]);
expect(openAiEntry?.baseUrl).toBe('https://***:***@openrouter.example/api/v1');
expect(openAiEntry?.headers).toEqual([{ key: 'Authorization', value: '...CRET' }]);
});
it('preserves stored header and URL secrets when saving unchanged redacted values', async () => {
const { listAiProviders, updateAiProviderEntry } = await loadAiProviderService();
writeCliproxyConfig(tempHome, {
'claude-api-key': [
{
id: 'claude-secret-route',
'api-key': 'sk-ant-provider-secret-1234',
'base-url': 'https://baseuser:basepass@anthropic.example/v1',
'proxy-url': 'http://proxyuser:proxypass@example.internal:8080',
headers: {
Authorization: 'Bearer HEADER-SECRET-123456',
'X-Project': 'public-routing-context',
},
},
],
});
const listed = await listAiProviders();
const entry = listed.families.find((family) => family.id === 'claude-api-key')?.entries[0];
expect(entry).toBeDefined();
await updateAiProviderEntry('claude-api-key', 'claude-secret-route', {
apiKey: 'sk-ant-provider-secret-1234',
baseUrl: entry?.baseUrl,
proxyUrl: entry?.proxyUrl,
headers: entry?.headers,
preserveSecrets: true,
});
const persisted = readCliproxyConfig(tempHome)['claude-api-key'] as Array<
Record<string, unknown>
>;
expect(persisted[0]?.['base-url']).toBe('https://baseuser:basepass@anthropic.example/v1');
expect(persisted[0]?.['proxy-url']).toBe('http://proxyuser:proxypass@example.internal:8080');
expect(persisted[0]?.headers).toEqual({
Authorization: 'Bearer HEADER-SECRET-123456',
'X-Project': 'public-routing-context',
});
});
it('normalizes plain openai-compatible model rules without aliases', async () => {
const { listAiProviders } = await loadAiProviderService();
+47 -12
View File
@@ -17,14 +17,39 @@ function maskSecret(value: string | undefined): string | undefined {
return value.length > 8 ? `...${value.slice(-4)}` : '***';
}
function sanitizeUrlForView(value: string | undefined): string | undefined {
const trimmed = value?.trim();
if (!trimmed) return undefined;
try {
const parsed = new URL(trimmed);
if (parsed.username) parsed.username = '***';
if (parsed.password) parsed.password = '***';
return parsed.toString();
} catch {
return trimmed;
}
}
function restoreMaskedViewValue(
value: string | undefined,
existing: string | undefined,
sanitizeForView: (value: string | undefined) => string | undefined = maskSecret
): string | undefined {
const next = value?.trim() || undefined;
if (!next || !existing) return next;
return next === sanitizeForView(existing) ? existing : next;
}
function normalizeHeaders(
headers: Array<{ key: string; value: string }> | undefined
headers: Array<{ key: string; value: string }> | undefined,
existing?: Record<string, string>
): Record<string, string> | undefined {
if (!headers) return undefined;
const normalized = headers.reduce<Record<string, string>>((acc, header) => {
const key = header.key.trim();
if (!key) return acc;
acc[key] = header.value;
acc[key] = restoreMaskedViewValue(header.value, existing?.[key]) || '';
return acc;
}, {});
return Object.keys(normalized).length > 0 ? normalized : undefined;
@@ -33,7 +58,10 @@ function normalizeHeaders(
function toHeaderPairs(
headers: Record<string, string> | undefined
): Array<{ key: string; value: string }> {
return Object.entries(headers || {}).map(([key, value]) => ({ key, value }));
return Object.entries(headers || {}).map(([key, value]) => ({
key,
value: maskSecret(value) || '***',
}));
}
function readModelRulePart(model: unknown, key: keyof AiProviderModelAlias) {
@@ -62,9 +90,9 @@ function buildApiKeyEntryView(
return {
id: entry.id || `${family}:${index}`,
index,
label: entry.prefix?.trim() || entry['base-url']?.trim() || `Entry ${index + 1}`,
baseUrl: entry['base-url']?.trim() || undefined,
proxyUrl: entry['proxy-url']?.trim() || undefined,
label: entry.prefix?.trim() || sanitizeUrlForView(entry['base-url']) || `Entry ${index + 1}`,
baseUrl: sanitizeUrlForView(entry['base-url']),
proxyUrl: sanitizeUrlForView(entry['proxy-url']),
prefix: entry.prefix?.trim() || undefined,
headers: toHeaderPairs(entry.headers),
excludedModels: [...(entry['excluded-models'] || [])],
@@ -80,7 +108,7 @@ function buildOpenAiCompatEntryView(entry: OpenAICompatEntry, index: number): Ai
index,
name: entry.name,
label: entry.name,
baseUrl: entry['base-url']?.trim() || undefined,
baseUrl: sanitizeUrlForView(entry['base-url']),
headers: toHeaderPairs(entry.headers),
excludedModels: [],
models: normalizeModelAliases(entry.models),
@@ -142,10 +170,14 @@ function toApiKeyEntry(
return {
id: existing?.id,
'api-key': nextSecret,
'base-url': input.baseUrl?.trim() || undefined,
'proxy-url': input.proxyUrl?.trim() || undefined,
'base-url': restoreMaskedViewValue(input.baseUrl, existing?.['base-url'], sanitizeUrlForView),
'proxy-url': restoreMaskedViewValue(
input.proxyUrl,
existing?.['proxy-url'],
sanitizeUrlForView
),
prefix: input.prefix?.trim() || undefined,
headers: normalizeHeaders(input.headers),
headers: normalizeHeaders(input.headers, existing?.headers),
'excluded-models': (input.excludedModels || [])
.map((value) => value.trim())
.filter((value) => value.length > 0),
@@ -167,8 +199,11 @@ function toOpenAiCompatEntry(
return {
id: existing?.id,
name: input.name?.trim() || existing?.name || 'connector',
'base-url': input.baseUrl?.trim() || existing?.['base-url'] || '',
headers: normalizeHeaders(input.headers),
'base-url':
restoreMaskedViewValue(input.baseUrl, existing?.['base-url'], sanitizeUrlForView) ||
existing?.['base-url'] ||
'',
headers: normalizeHeaders(input.headers, existing?.headers),
'api-key-entries': nextApiKeys.map((apiKey) => ({ 'api-key': apiKey })),
models: normalizeModelAliases(input.models),
};
+20 -19
View File
@@ -50,22 +50,16 @@ function asObject(value: unknown): Record<string, unknown> | null {
: null;
}
function normalizeLocalProviderUrl(value: unknown): string | null {
if (typeof value !== 'string') return null;
function isValidCodexCliproxyBaseUrl(value: unknown): value is string {
if (typeof value !== 'string') return false;
const trimmed = value.trim();
if (!trimmed) return false;
try {
const url = new URL(trimmed);
if (
(url.hostname === 'localhost' || url.hostname === '127.0.0.1') &&
url.pathname === '/api/provider/codex'
) {
url.hostname = '127.0.0.1';
return url.toString().replace(/\/$/, '');
}
return url.protocol === 'http:' || url.protocol === 'https:';
} catch {
return null;
return false;
}
return null;
}
function resolveProviderEnvKey(provider: Record<string, unknown> | null): string {
@@ -76,14 +70,10 @@ function resolveProviderEnvKey(provider: Record<string, unknown> | null): string
return CODEX_CLIPROXY_PROVIDER_ENV_KEY;
}
function isProviderReady(
provider: Record<string, unknown>,
expectedBaseUrl: string,
envKey: string
): boolean {
function isProviderReady(provider: Record<string, unknown>, envKey: string): boolean {
return (
provider.name === CODEX_CLIPROXY_PROVIDER_NAME &&
normalizeLocalProviderUrl(provider.base_url) === expectedBaseUrl &&
isValidCodexCliproxyBaseUrl(provider.base_url) &&
provider.env_key === envKey &&
provider.wire_api === 'responses' &&
provider.requires_openai_auth === false &&
@@ -102,6 +92,17 @@ function buildProviderConfig(baseUrl: string, envKey: string): Record<string, un
};
}
function resolveProviderBaseUrl(
provider: Record<string, unknown>,
fallbackBaseUrl: string
): string {
const baseUrl = provider.base_url;
if (isValidCodexCliproxyBaseUrl(baseUrl)) {
return baseUrl.trim();
}
return fallbackBaseUrl;
}
function appendProviderBlock(rawText: string, baseUrl: string): string {
const prefix = rawText.trimEnd();
const providerBlock = stringifyTomlObject({
@@ -179,12 +180,12 @@ export async function ensureCodexCliproxyProviderConfig(
}
const envKey = resolveProviderEnvKey(currentProvider);
const providerReady = isProviderReady(currentProvider, expectedBaseUrl, envKey);
const providerReady = isProviderReady(currentProvider, envKey);
if (!providerReady) {
providers[CODEX_CLIPROXY_PROVIDER_ID] = {
...currentProvider,
...buildProviderConfig(expectedBaseUrl, envKey),
...buildProviderConfig(resolveProviderBaseUrl(currentProvider, expectedBaseUrl), envKey),
};
}
+2 -1
View File
@@ -9,6 +9,7 @@ import session from 'express-session';
import rateLimit from 'express-rate-limit';
import crypto from 'crypto';
import * as net from 'net';
import fs from 'fs';
import path from 'path';
import {
@@ -161,7 +162,7 @@ function isLoopbackHostname(value: string | undefined): boolean {
return (
normalized === 'localhost' ||
normalized.endsWith('.localhost') ||
isLoopbackRemoteAddress(normalized)
(net.isIP(normalized) !== 0 && isLoopbackRemoteAddress(normalized))
);
}
@@ -26,9 +26,12 @@ import {
type ClaudeExtensionActionTarget,
verifyClaudeExtensionBinding,
} from '../services/claude-extension-settings-service';
import { requireLocalAccessWhenAuthDisabled } from '../middleware/auth-middleware';
const router = Router();
const VALID_HOSTS = new Set(CLAUDE_EXTENSION_HOSTS.map((host) => host.id));
const SETUP_LOCAL_ACCESS_ERROR =
'Claude extension setup requires localhost access when dashboard auth is disabled.';
const VALID_TARGETS = new Set<ClaudeExtensionActionTarget>(['shared', 'ide', 'all']);
function getHostFromRequest(req: Request): ClaudeExtensionHost {
@@ -79,6 +82,10 @@ router.get('/profiles', (_req: Request, res: Response): void => {
});
router.get('/setup', async (req: Request, res: Response): Promise<void> => {
if (!requireLocalAccessWhenAuthDisabled(req, res, SETUP_LOCAL_ACCESS_ERROR)) {
return;
}
const rawProfile = typeof req.query.profile === 'string' ? req.query.profile.trim() : '';
if (!rawProfile) {
res.status(400).json({ error: 'Missing required query parameter: profile' });