Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,10 @@ export default function ClaudeToolCard({
// Restore selected key from file: match token stored in file against known keys
const tokenFromFile = env.ANTHROPIC_AUTH_TOKEN;
if (tokenFromFile) {
const matchedKey = apiKeys?.find((k) => k.key === tokenFromFile);
// (#523) Keys from /api/keys are masked (first 8 + "****" + last 4).
// Mask the token from file to compare against the masked list.
const maskedToken = tokenFromFile.slice(0, 8) + "****" + tokenFromFile.slice(-4);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

This masking logic is hardcoded and assumes the token is at least 12 characters long. It is also fragile as it depends on the exact masking format (first 8 + last 4) used by the /api/keys endpoint. If the backend masking logic changes or if a shorter token is encountered, this comparison will fail to match correctly.

const matchedKey = apiKeys?.find((k) => k.key === maskedToken);
if (matchedKey) setSelectedApiKey(matchedKey.id);
}
}
Expand Down
12 changes: 7 additions & 5 deletions src/app/api/cli-tools/claude-settings/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -95,17 +95,19 @@ export async function POST(request: Request) {
return NextResponse.json({ error: writeGuard }, { status: 403 });
}

// (#523/#526) Extract keyId BEFORE validation — Zod strips unknown fields!
// The /api/keys list endpoint returns masked key strings — sending those to
// disk would save an unusable half-hidden token. Resolving by ID guarantees
// we always write the full key value to the config file.
const keyId = typeof rawBody?.keyId === "string" ? rawBody.keyId.trim() : null;

const validation = validateBody(cliSettingsEnvSchema, rawBody);
if (isValidationFailure(validation)) {
return NextResponse.json({ error: validation.error }, { status: 400 });
}
const { env } = validation.data;

// (#523/#526) If a keyId was provided, resolve the real API key from DB.
// The /api/keys list endpoint returns masked key strings — sending those to
// disk would save an unusable half-hidden token. Resolving by ID guarantees
// we always write the full key value to the config file.
const keyId = typeof rawBody?.keyId === "string" ? rawBody.keyId.trim() : null;
// Resolve the real API key from DB by ID
if (keyId) {
try {
const keyRecord = await getApiKeyById(keyId);
Expand Down
6 changes: 4 additions & 2 deletions src/app/api/cli-tools/cline-settings/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -122,14 +122,16 @@ export async function POST(request: Request) {
return NextResponse.json({ error: writeGuard }, { status: 403 });
}

// (#526) Extract keyId BEFORE validation — Zod strips unknown fields!
const keyId = typeof rawBody?.keyId === "string" ? rawBody.keyId.trim() : null;

const validation = validateBody(cliModelConfigSchema, rawBody);
if (isValidationFailure(validation)) {
return NextResponse.json({ error: validation.error }, { status: 400 });
}
let { baseUrl, apiKey, model } = validation.data;

// (#526) Resolve real key from DB if keyId was provided
const keyId = typeof rawBody?.keyId === "string" ? rawBody.keyId.trim() : null;
// Resolve real key from DB by ID
if (keyId) {
try {
const keyRecord = await getApiKeyById(keyId);
Expand Down
10 changes: 6 additions & 4 deletions src/app/api/cli-tools/codex-settings/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,11 @@ export async function POST(request: Request) {
return NextResponse.json({ error: writeGuard }, { status: 403 });
}

// (#549) Extract keyId BEFORE validation — Zod strips unknown fields!
// The dashboard sends masked key strings — resolving by ID guarantees
// we always write the full key value to the config file.
const keyId = typeof rawBody?.keyId === "string" ? rawBody.keyId.trim() : null;

const validation = validateBody(cliModelConfigSchema, rawBody);
if (isValidationFailure(validation)) {
return NextResponse.json({ error: validation.error }, { status: 400 });
Expand All @@ -176,10 +181,7 @@ export async function POST(request: Request) {
);
}

// (#549) Resolve real key from DB if keyId was provided.
// The dashboard sends masked key strings — resolving by ID guarantees
// we always write the full key value to the config file.
const keyId = typeof rawBody?.keyId === "string" ? rawBody.keyId.trim() : null;
// Resolve real key from DB by ID

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

The if (!apiKey) check (lines 177-182) occurs before the keyId resolution logic. If a client provides a keyId but omits the apiKey in the request body (expecting the backend to resolve it from the database), the request will fail with a 400 error at line 177. To fix this, the validation check for apiKey should be moved after the if (keyId) block.

if (keyId) {
try {
const keyRecord = await getApiKeyById(keyId);
Expand Down
6 changes: 4 additions & 2 deletions src/app/api/cli-tools/droid-settings/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -98,15 +98,17 @@ export async function POST(request: Request) {
return NextResponse.json({ error: writeGuard }, { status: 403 });
}

// (#549) Extract keyId BEFORE validation — Zod strips unknown fields!
const keyId = typeof rawBody?.keyId === "string" ? rawBody.keyId.trim() : null;

const validation = validateBody(cliModelConfigSchema, rawBody);
if (isValidationFailure(validation)) {
return NextResponse.json({ error: validation.error }, { status: 400 });
}
const { baseUrl, model } = validation.data;
let { apiKey } = validation.data;

// (#549) Resolve real key from DB if keyId was provided.
const keyId = typeof rawBody?.keyId === "string" ? rawBody.keyId.trim() : null;
// Resolve real key from DB by ID
if (keyId) {
try {
const keyRecord = await getApiKeyById(keyId);
Expand Down
6 changes: 4 additions & 2 deletions src/app/api/cli-tools/kilo-settings/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -130,15 +130,17 @@ export async function POST(request) {
return NextResponse.json({ error: writeGuard }, { status: 403 });
}

// (#549) Extract keyId BEFORE validation — Zod strips unknown fields!
const keyId = typeof rawBody?.keyId === "string" ? rawBody.keyId.trim() : null;

const validation = validateBody(cliModelConfigSchema, rawBody);
if (isValidationFailure(validation)) {
return NextResponse.json({ error: validation.error }, { status: 400 });
}
const { baseUrl, model } = validation.data;
let { apiKey } = validation.data;

// (#549) Resolve real key from DB if keyId was provided.
const keyId = typeof rawBody?.keyId === "string" ? rawBody.keyId.trim() : null;
// Resolve real key from DB by ID
if (keyId) {
try {
const keyRecord = await getApiKeyById(keyId);
Expand Down
6 changes: 4 additions & 2 deletions src/app/api/cli-tools/openclaw-settings/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -98,14 +98,16 @@ export async function POST(request: Request) {
return NextResponse.json({ error: writeGuard }, { status: 403 });
}

// (#526) Extract keyId BEFORE validation — Zod strips unknown fields!
const keyId = typeof rawBody?.keyId === "string" ? rawBody.keyId.trim() : null;

const validation = validateBody(cliModelConfigSchema, rawBody);
if (isValidationFailure(validation)) {
return NextResponse.json({ error: validation.error }, { status: 400 });
}
let { baseUrl, apiKey, model } = validation.data;

// (#526) Resolve real key from DB if keyId was provided
const keyId = typeof rawBody?.keyId === "string" ? rawBody.keyId.trim() : null;
// Resolve real key from DB by ID
if (keyId) {
try {
const keyRecord = await getApiKeyById(keyId);
Expand Down
5 changes: 4 additions & 1 deletion src/app/api/providers/[id]/models/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -623,7 +623,10 @@ export async function GET(
baseUrl = baseUrl.slice(0, -9);
}

const url = `${baseUrl}/models`;
// Use modelsPath from provider node if available, otherwise default to /models
const psd = asRecord(connection.providerSpecificData);
const modelsPath = toNonEmptyString(psd.modelsPath) || "/models";

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

critical

The function toNonEmptyString is not defined in this file, nor is it imported from any of the modules listed in the imports section. This will result in a ReferenceError at runtime when this code path is executed. You should either define this helper function or use an inline check to ensure psd.modelsPath is a valid string.

Suggested change
const modelsPath = toNonEmptyString(psd.modelsPath) || "/models";
const modelsPath = typeof psd.modelsPath === "string" && psd.modelsPath.trim() ? psd.modelsPath.trim() : "/models";

const url = `${baseUrl}${modelsPath}`;
const token = accessToken || apiKey;
const response = await runWithProxyContext(proxy, () =>
fetch(url, {
Expand Down