mirror of
https://github.com/moltbot/moltbot.git
synced 2026-05-05 04:48:17 +00:00
fix(auth): strip line breaks from pasted keys
This commit is contained in:
@@ -1,4 +1,5 @@
|
|||||||
import type { AuthProfileCredential, AuthProfileStore } from "./types.js";
|
import type { AuthProfileCredential, AuthProfileStore } from "./types.js";
|
||||||
|
import { normalizeSecretInput } from "../../utils/normalize-secret-input.js";
|
||||||
import { normalizeProviderId } from "../model-selection.js";
|
import { normalizeProviderId } from "../model-selection.js";
|
||||||
import {
|
import {
|
||||||
ensureAuthProfileStore,
|
ensureAuthProfileStore,
|
||||||
@@ -49,8 +50,19 @@ export function upsertAuthProfile(params: {
|
|||||||
credential: AuthProfileCredential;
|
credential: AuthProfileCredential;
|
||||||
agentDir?: string;
|
agentDir?: string;
|
||||||
}): void {
|
}): void {
|
||||||
|
const credential =
|
||||||
|
params.credential.type === "api_key"
|
||||||
|
? {
|
||||||
|
...params.credential,
|
||||||
|
...(typeof params.credential.key === "string"
|
||||||
|
? { key: normalizeSecretInput(params.credential.key) }
|
||||||
|
: {}),
|
||||||
|
}
|
||||||
|
: params.credential.type === "token"
|
||||||
|
? { ...params.credential, token: normalizeSecretInput(params.credential.token) }
|
||||||
|
: params.credential;
|
||||||
const store = ensureAuthProfileStore(params.agentDir);
|
const store = ensureAuthProfileStore(params.agentDir);
|
||||||
store.profiles[params.profileId] = params.credential;
|
store.profiles[params.profileId] = credential;
|
||||||
saveAuthProfileStore(store, params.agentDir);
|
saveAuthProfileStore(store, params.agentDir);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import { normalizeSecretInput } from "../utils/normalize-secret-input.js";
|
||||||
|
|
||||||
type MinimaxBaseResp = {
|
type MinimaxBaseResp = {
|
||||||
status_code?: number;
|
status_code?: number;
|
||||||
status_msg?: string;
|
status_msg?: string;
|
||||||
@@ -44,7 +46,7 @@ export async function minimaxUnderstandImage(params: {
|
|||||||
apiHost?: string;
|
apiHost?: string;
|
||||||
modelBaseUrl?: string;
|
modelBaseUrl?: string;
|
||||||
}): Promise<string> {
|
}): Promise<string> {
|
||||||
const apiKey = params.apiKey.trim();
|
const apiKey = normalizeSecretInput(params.apiKey);
|
||||||
if (!apiKey) {
|
if (!apiKey) {
|
||||||
throw new Error("MiniMax VLM: apiKey required");
|
throw new Error("MiniMax VLM: apiKey required");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -511,4 +511,25 @@ describe("getApiKeyForModel", () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("strips embedded CR/LF from ANTHROPIC_API_KEY", async () => {
|
||||||
|
const previous = process.env.ANTHROPIC_API_KEY;
|
||||||
|
|
||||||
|
try {
|
||||||
|
process.env.ANTHROPIC_API_KEY = "sk-ant-test-\r\nkey";
|
||||||
|
|
||||||
|
vi.resetModules();
|
||||||
|
const { resolveEnvApiKey } = await import("./model-auth.js");
|
||||||
|
|
||||||
|
const resolved = resolveEnvApiKey("anthropic");
|
||||||
|
expect(resolved?.apiKey).toBe("sk-ant-test-key");
|
||||||
|
expect(resolved?.source).toContain("ANTHROPIC_API_KEY");
|
||||||
|
} finally {
|
||||||
|
if (previous === undefined) {
|
||||||
|
delete process.env.ANTHROPIC_API_KEY;
|
||||||
|
} else {
|
||||||
|
process.env.ANTHROPIC_API_KEY = previous;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -4,6 +4,10 @@ import type { OpenClawConfig } from "../config/config.js";
|
|||||||
import type { ModelProviderAuthMode, ModelProviderConfig } from "../config/types.js";
|
import type { ModelProviderAuthMode, ModelProviderConfig } from "../config/types.js";
|
||||||
import { formatCliCommand } from "../cli/command-format.js";
|
import { formatCliCommand } from "../cli/command-format.js";
|
||||||
import { getShellEnvAppliedKeys } from "../infra/shell-env.js";
|
import { getShellEnvAppliedKeys } from "../infra/shell-env.js";
|
||||||
|
import {
|
||||||
|
normalizeOptionalSecretInput,
|
||||||
|
normalizeSecretInput,
|
||||||
|
} from "../utils/normalize-secret-input.js";
|
||||||
import {
|
import {
|
||||||
type AuthProfileStore,
|
type AuthProfileStore,
|
||||||
ensureAuthProfileStore,
|
ensureAuthProfileStore,
|
||||||
@@ -48,8 +52,7 @@ export function getCustomProviderApiKey(
|
|||||||
provider: string,
|
provider: string,
|
||||||
): string | undefined {
|
): string | undefined {
|
||||||
const entry = resolveProviderConfig(cfg, provider);
|
const entry = resolveProviderConfig(cfg, provider);
|
||||||
const key = entry?.apiKey?.trim();
|
return normalizeOptionalSecretInput(entry?.apiKey);
|
||||||
return key || undefined;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function resolveProviderAuthOverride(
|
function resolveProviderAuthOverride(
|
||||||
@@ -236,7 +239,7 @@ export function resolveEnvApiKey(provider: string): EnvApiKeyResult | null {
|
|||||||
const normalized = normalizeProviderId(provider);
|
const normalized = normalizeProviderId(provider);
|
||||||
const applied = new Set(getShellEnvAppliedKeys());
|
const applied = new Set(getShellEnvAppliedKeys());
|
||||||
const pick = (envVar: string): EnvApiKeyResult | null => {
|
const pick = (envVar: string): EnvApiKeyResult | null => {
|
||||||
const value = process.env[envVar]?.trim();
|
const value = normalizeOptionalSecretInput(process.env[envVar]);
|
||||||
if (!value) {
|
if (!value) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -387,7 +390,7 @@ export async function getApiKeyForModel(params: {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function requireApiKey(auth: ResolvedProviderAuth, provider: string): string {
|
export function requireApiKey(auth: ResolvedProviderAuth, provider: string): string {
|
||||||
const key = auth.apiKey?.trim();
|
const key = normalizeSecretInput(auth.apiKey);
|
||||||
if (key) {
|
if (key) {
|
||||||
return key;
|
return key;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import type { AnyAgentTool } from "./common.js";
|
|||||||
import { fetchWithSsrFGuard } from "../../infra/net/fetch-guard.js";
|
import { fetchWithSsrFGuard } from "../../infra/net/fetch-guard.js";
|
||||||
import { SsrFBlockedError } from "../../infra/net/ssrf.js";
|
import { SsrFBlockedError } from "../../infra/net/ssrf.js";
|
||||||
import { wrapExternalContent, wrapWebContent } from "../../security/external-content.js";
|
import { wrapExternalContent, wrapWebContent } from "../../security/external-content.js";
|
||||||
|
import { normalizeSecretInput } from "../../utils/normalize-secret-input.js";
|
||||||
import { stringEnum } from "../schema/typebox.js";
|
import { stringEnum } from "../schema/typebox.js";
|
||||||
import { jsonResult, readNumberParam, readStringParam } from "./common.js";
|
import { jsonResult, readNumberParam, readStringParam } from "./common.js";
|
||||||
import {
|
import {
|
||||||
@@ -120,9 +121,9 @@ function resolveFirecrawlConfig(fetch?: WebFetchConfig): FirecrawlFetchConfig {
|
|||||||
function resolveFirecrawlApiKey(firecrawl?: FirecrawlFetchConfig): string | undefined {
|
function resolveFirecrawlApiKey(firecrawl?: FirecrawlFetchConfig): string | undefined {
|
||||||
const fromConfig =
|
const fromConfig =
|
||||||
firecrawl && "apiKey" in firecrawl && typeof firecrawl.apiKey === "string"
|
firecrawl && "apiKey" in firecrawl && typeof firecrawl.apiKey === "string"
|
||||||
? firecrawl.apiKey.trim()
|
? normalizeSecretInput(firecrawl.apiKey)
|
||||||
: "";
|
: "";
|
||||||
const fromEnv = (process.env.FIRECRAWL_API_KEY ?? "").trim();
|
const fromEnv = normalizeSecretInput(process.env.FIRECRAWL_API_KEY);
|
||||||
return fromConfig || fromEnv || undefined;
|
return fromConfig || fromEnv || undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -81,8 +81,18 @@ describe("web_search grok config resolution", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("returns undefined when no apiKey is available", () => {
|
it("returns undefined when no apiKey is available", () => {
|
||||||
expect(resolveGrokApiKey({})).toBeUndefined();
|
const previous = process.env.XAI_API_KEY;
|
||||||
expect(resolveGrokApiKey(undefined)).toBeUndefined();
|
try {
|
||||||
|
delete process.env.XAI_API_KEY;
|
||||||
|
expect(resolveGrokApiKey({})).toBeUndefined();
|
||||||
|
expect(resolveGrokApiKey(undefined)).toBeUndefined();
|
||||||
|
} finally {
|
||||||
|
if (previous === undefined) {
|
||||||
|
delete process.env.XAI_API_KEY;
|
||||||
|
} else {
|
||||||
|
process.env.XAI_API_KEY = previous;
|
||||||
|
}
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
it("uses default model when not specified", () => {
|
it("uses default model when not specified", () => {
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import type { OpenClawConfig } from "../../config/config.js";
|
|||||||
import type { AnyAgentTool } from "./common.js";
|
import type { AnyAgentTool } from "./common.js";
|
||||||
import { formatCliCommand } from "../../cli/command-format.js";
|
import { formatCliCommand } from "../../cli/command-format.js";
|
||||||
import { wrapWebContent } from "../../security/external-content.js";
|
import { wrapWebContent } from "../../security/external-content.js";
|
||||||
|
import { normalizeSecretInput } from "../../utils/normalize-secret-input.js";
|
||||||
import { jsonResult, readNumberParam, readStringParam } from "./common.js";
|
import { jsonResult, readNumberParam, readStringParam } from "./common.js";
|
||||||
import {
|
import {
|
||||||
CacheEntry,
|
CacheEntry,
|
||||||
@@ -142,8 +143,10 @@ function resolveSearchEnabled(params: { search?: WebSearchConfig; sandboxed?: bo
|
|||||||
|
|
||||||
function resolveSearchApiKey(search?: WebSearchConfig): string | undefined {
|
function resolveSearchApiKey(search?: WebSearchConfig): string | undefined {
|
||||||
const fromConfig =
|
const fromConfig =
|
||||||
search && "apiKey" in search && typeof search.apiKey === "string" ? search.apiKey.trim() : "";
|
search && "apiKey" in search && typeof search.apiKey === "string"
|
||||||
const fromEnv = (process.env.BRAVE_API_KEY ?? "").trim();
|
? normalizeSecretInput(search.apiKey)
|
||||||
|
: "";
|
||||||
|
const fromEnv = normalizeSecretInput(process.env.BRAVE_API_KEY);
|
||||||
return fromConfig || fromEnv || undefined;
|
return fromConfig || fromEnv || undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -222,7 +225,7 @@ function resolvePerplexityApiKey(perplexity?: PerplexityConfig): {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function normalizeApiKey(key: unknown): string {
|
function normalizeApiKey(key: unknown): string {
|
||||||
return typeof key === "string" ? key.trim() : "";
|
return normalizeSecretInput(key);
|
||||||
}
|
}
|
||||||
|
|
||||||
function inferPerplexityBaseUrlFromApiKey(apiKey?: string): PerplexityBaseUrlHint | undefined {
|
function inferPerplexityBaseUrlFromApiKey(apiKey?: string): PerplexityBaseUrlHint | undefined {
|
||||||
|
|||||||
93
src/commands/onboard-non-interactive.token.test.ts
Normal file
93
src/commands/onboard-non-interactive.token.test.ts
Normal file
@@ -0,0 +1,93 @@
|
|||||||
|
import fs from "node:fs/promises";
|
||||||
|
import os from "node:os";
|
||||||
|
import path from "node:path";
|
||||||
|
import { describe, expect, it, vi } from "vitest";
|
||||||
|
|
||||||
|
describe("onboard (non-interactive): token auth", () => {
|
||||||
|
it("writes token profile config and stores the token", async () => {
|
||||||
|
const prev = {
|
||||||
|
home: process.env.HOME,
|
||||||
|
stateDir: process.env.OPENCLAW_STATE_DIR,
|
||||||
|
configPath: process.env.OPENCLAW_CONFIG_PATH,
|
||||||
|
skipChannels: process.env.OPENCLAW_SKIP_CHANNELS,
|
||||||
|
skipGmail: process.env.OPENCLAW_SKIP_GMAIL_WATCHER,
|
||||||
|
skipCron: process.env.OPENCLAW_SKIP_CRON,
|
||||||
|
skipCanvas: process.env.OPENCLAW_SKIP_CANVAS_HOST,
|
||||||
|
token: process.env.OPENCLAW_GATEWAY_TOKEN,
|
||||||
|
password: process.env.OPENCLAW_GATEWAY_PASSWORD,
|
||||||
|
};
|
||||||
|
|
||||||
|
process.env.OPENCLAW_SKIP_CHANNELS = "1";
|
||||||
|
process.env.OPENCLAW_SKIP_GMAIL_WATCHER = "1";
|
||||||
|
process.env.OPENCLAW_SKIP_CRON = "1";
|
||||||
|
process.env.OPENCLAW_SKIP_CANVAS_HOST = "1";
|
||||||
|
delete process.env.OPENCLAW_GATEWAY_TOKEN;
|
||||||
|
delete process.env.OPENCLAW_GATEWAY_PASSWORD;
|
||||||
|
|
||||||
|
const tempHome = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-onboard-token-"));
|
||||||
|
process.env.HOME = tempHome;
|
||||||
|
process.env.OPENCLAW_STATE_DIR = tempHome;
|
||||||
|
process.env.OPENCLAW_CONFIG_PATH = path.join(tempHome, "openclaw.json");
|
||||||
|
vi.resetModules();
|
||||||
|
|
||||||
|
const cleanToken = `sk-ant-oat01-${"a".repeat(80)}`;
|
||||||
|
const token = `${cleanToken.slice(0, 30)}\r${cleanToken.slice(30)}`;
|
||||||
|
|
||||||
|
const runtime = {
|
||||||
|
log: () => {},
|
||||||
|
error: (msg: string) => {
|
||||||
|
throw new Error(msg);
|
||||||
|
},
|
||||||
|
exit: (code: number) => {
|
||||||
|
throw new Error(`exit:${code}`);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
const { runNonInteractiveOnboarding } = await import("./onboard-non-interactive.js");
|
||||||
|
await runNonInteractiveOnboarding(
|
||||||
|
{
|
||||||
|
nonInteractive: true,
|
||||||
|
authChoice: "token",
|
||||||
|
tokenProvider: "anthropic",
|
||||||
|
token,
|
||||||
|
tokenProfileId: "anthropic:default",
|
||||||
|
skipHealth: true,
|
||||||
|
skipChannels: true,
|
||||||
|
json: true,
|
||||||
|
},
|
||||||
|
runtime,
|
||||||
|
);
|
||||||
|
|
||||||
|
const { CONFIG_PATH } = await import("../config/config.js");
|
||||||
|
const cfg = JSON.parse(await fs.readFile(CONFIG_PATH, "utf8")) as {
|
||||||
|
auth?: {
|
||||||
|
profiles?: Record<string, { provider?: string; mode?: string }>;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
expect(cfg.auth?.profiles?.["anthropic:default"]?.provider).toBe("anthropic");
|
||||||
|
expect(cfg.auth?.profiles?.["anthropic:default"]?.mode).toBe("token");
|
||||||
|
|
||||||
|
const { ensureAuthProfileStore } = await import("../agents/auth-profiles.js");
|
||||||
|
const store = ensureAuthProfileStore();
|
||||||
|
const profile = store.profiles["anthropic:default"];
|
||||||
|
expect(profile?.type).toBe("token");
|
||||||
|
if (profile?.type === "token") {
|
||||||
|
expect(profile.provider).toBe("anthropic");
|
||||||
|
expect(profile.token).toBe(cleanToken);
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
await fs.rm(tempHome, { recursive: true, force: true });
|
||||||
|
process.env.HOME = prev.home;
|
||||||
|
process.env.OPENCLAW_STATE_DIR = prev.stateDir;
|
||||||
|
process.env.OPENCLAW_CONFIG_PATH = prev.configPath;
|
||||||
|
process.env.OPENCLAW_SKIP_CHANNELS = prev.skipChannels;
|
||||||
|
process.env.OPENCLAW_SKIP_GMAIL_WATCHER = prev.skipGmail;
|
||||||
|
process.env.OPENCLAW_SKIP_CRON = prev.skipCron;
|
||||||
|
process.env.OPENCLAW_SKIP_CANVAS_HOST = prev.skipCanvas;
|
||||||
|
process.env.OPENCLAW_GATEWAY_TOKEN = prev.token;
|
||||||
|
process.env.OPENCLAW_GATEWAY_PASSWORD = prev.password;
|
||||||
|
}
|
||||||
|
}, 60_000);
|
||||||
|
});
|
||||||
91
src/commands/onboard-non-interactive.xai.test.ts
Normal file
91
src/commands/onboard-non-interactive.xai.test.ts
Normal file
@@ -0,0 +1,91 @@
|
|||||||
|
import fs from "node:fs/promises";
|
||||||
|
import os from "node:os";
|
||||||
|
import path from "node:path";
|
||||||
|
import { describe, expect, it, vi } from "vitest";
|
||||||
|
|
||||||
|
describe("onboard (non-interactive): xAI", () => {
|
||||||
|
it("stores the API key and configures the default model", async () => {
|
||||||
|
const prev = {
|
||||||
|
home: process.env.HOME,
|
||||||
|
stateDir: process.env.OPENCLAW_STATE_DIR,
|
||||||
|
configPath: process.env.OPENCLAW_CONFIG_PATH,
|
||||||
|
skipChannels: process.env.OPENCLAW_SKIP_CHANNELS,
|
||||||
|
skipGmail: process.env.OPENCLAW_SKIP_GMAIL_WATCHER,
|
||||||
|
skipCron: process.env.OPENCLAW_SKIP_CRON,
|
||||||
|
skipCanvas: process.env.OPENCLAW_SKIP_CANVAS_HOST,
|
||||||
|
token: process.env.OPENCLAW_GATEWAY_TOKEN,
|
||||||
|
password: process.env.OPENCLAW_GATEWAY_PASSWORD,
|
||||||
|
};
|
||||||
|
|
||||||
|
process.env.OPENCLAW_SKIP_CHANNELS = "1";
|
||||||
|
process.env.OPENCLAW_SKIP_GMAIL_WATCHER = "1";
|
||||||
|
process.env.OPENCLAW_SKIP_CRON = "1";
|
||||||
|
process.env.OPENCLAW_SKIP_CANVAS_HOST = "1";
|
||||||
|
delete process.env.OPENCLAW_GATEWAY_TOKEN;
|
||||||
|
delete process.env.OPENCLAW_GATEWAY_PASSWORD;
|
||||||
|
|
||||||
|
const tempHome = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-onboard-xai-"));
|
||||||
|
process.env.HOME = tempHome;
|
||||||
|
process.env.OPENCLAW_STATE_DIR = tempHome;
|
||||||
|
process.env.OPENCLAW_CONFIG_PATH = path.join(tempHome, "openclaw.json");
|
||||||
|
vi.resetModules();
|
||||||
|
|
||||||
|
const runtime = {
|
||||||
|
log: () => {},
|
||||||
|
error: (msg: string) => {
|
||||||
|
throw new Error(msg);
|
||||||
|
},
|
||||||
|
exit: (code: number) => {
|
||||||
|
throw new Error(`exit:${code}`);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
const { runNonInteractiveOnboarding } = await import("./onboard-non-interactive.js");
|
||||||
|
await runNonInteractiveOnboarding(
|
||||||
|
{
|
||||||
|
nonInteractive: true,
|
||||||
|
authChoice: "xai-api-key",
|
||||||
|
xaiApiKey: "xai-test-\r\nkey",
|
||||||
|
skipHealth: true,
|
||||||
|
skipChannels: true,
|
||||||
|
skipSkills: true,
|
||||||
|
json: true,
|
||||||
|
},
|
||||||
|
runtime,
|
||||||
|
);
|
||||||
|
|
||||||
|
const { CONFIG_PATH } = await import("../config/config.js");
|
||||||
|
const cfg = JSON.parse(await fs.readFile(CONFIG_PATH, "utf8")) as {
|
||||||
|
auth?: {
|
||||||
|
profiles?: Record<string, { provider?: string; mode?: string }>;
|
||||||
|
};
|
||||||
|
agents?: { defaults?: { model?: { primary?: string } } };
|
||||||
|
};
|
||||||
|
|
||||||
|
expect(cfg.auth?.profiles?.["xai:default"]?.provider).toBe("xai");
|
||||||
|
expect(cfg.auth?.profiles?.["xai:default"]?.mode).toBe("api_key");
|
||||||
|
expect(cfg.agents?.defaults?.model?.primary).toBe("xai/grok-4");
|
||||||
|
|
||||||
|
const { ensureAuthProfileStore } = await import("../agents/auth-profiles.js");
|
||||||
|
const store = ensureAuthProfileStore();
|
||||||
|
const profile = store.profiles["xai:default"];
|
||||||
|
expect(profile?.type).toBe("api_key");
|
||||||
|
if (profile?.type === "api_key") {
|
||||||
|
expect(profile.provider).toBe("xai");
|
||||||
|
expect(profile.key).toBe("xai-test-key");
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
await fs.rm(tempHome, { recursive: true, force: true });
|
||||||
|
process.env.HOME = prev.home;
|
||||||
|
process.env.OPENCLAW_STATE_DIR = prev.stateDir;
|
||||||
|
process.env.OPENCLAW_CONFIG_PATH = prev.configPath;
|
||||||
|
process.env.OPENCLAW_SKIP_CHANNELS = prev.skipChannels;
|
||||||
|
process.env.OPENCLAW_SKIP_GMAIL_WATCHER = prev.skipGmail;
|
||||||
|
process.env.OPENCLAW_SKIP_CRON = prev.skipCron;
|
||||||
|
process.env.OPENCLAW_SKIP_CANVAS_HOST = prev.skipCanvas;
|
||||||
|
process.env.OPENCLAW_GATEWAY_TOKEN = prev.token;
|
||||||
|
process.env.OPENCLAW_GATEWAY_PASSWORD = prev.password;
|
||||||
|
}
|
||||||
|
}, 60_000);
|
||||||
|
});
|
||||||
@@ -6,6 +6,7 @@ import {
|
|||||||
resolveAuthProfileOrder,
|
resolveAuthProfileOrder,
|
||||||
} from "../../agents/auth-profiles.js";
|
} from "../../agents/auth-profiles.js";
|
||||||
import { resolveEnvApiKey } from "../../agents/model-auth.js";
|
import { resolveEnvApiKey } from "../../agents/model-auth.js";
|
||||||
|
import { normalizeOptionalSecretInput } from "../../utils/normalize-secret-input.js";
|
||||||
|
|
||||||
export type NonInteractiveApiKeySource = "flag" | "env" | "profile";
|
export type NonInteractiveApiKeySource = "flag" | "env" | "profile";
|
||||||
|
|
||||||
@@ -48,7 +49,7 @@ export async function resolveNonInteractiveApiKey(params: {
|
|||||||
agentDir?: string;
|
agentDir?: string;
|
||||||
allowProfile?: boolean;
|
allowProfile?: boolean;
|
||||||
}): Promise<{ key: string; source: NonInteractiveApiKeySource } | null> {
|
}): Promise<{ key: string; source: NonInteractiveApiKeySource } | null> {
|
||||||
const flagKey = params.flagValue?.trim();
|
const flagKey = normalizeOptionalSecretInput(params.flagValue);
|
||||||
if (flagKey) {
|
if (flagKey) {
|
||||||
return { key: flagKey, source: "flag" };
|
return { key: flagKey, source: "flag" };
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import { normalizeProviderId } from "../../../agents/model-selection.js";
|
|||||||
import { parseDurationMs } from "../../../cli/parse-duration.js";
|
import { parseDurationMs } from "../../../cli/parse-duration.js";
|
||||||
import { upsertSharedEnvVar } from "../../../infra/env-file.js";
|
import { upsertSharedEnvVar } from "../../../infra/env-file.js";
|
||||||
import { shortenHomePath } from "../../../utils.js";
|
import { shortenHomePath } from "../../../utils.js";
|
||||||
|
import { normalizeSecretInput } from "../../../utils/normalize-secret-input.js";
|
||||||
import { buildTokenProfileId, validateAnthropicSetupToken } from "../../auth-token.js";
|
import { buildTokenProfileId, validateAnthropicSetupToken } from "../../auth-token.js";
|
||||||
import { applyGoogleGeminiModelDefault } from "../../google-gemini-model-default.js";
|
import { applyGoogleGeminiModelDefault } from "../../google-gemini-model-default.js";
|
||||||
import {
|
import {
|
||||||
@@ -111,7 +112,7 @@ export async function applyNonInteractiveAuthChoice(params: {
|
|||||||
runtime.exit(1);
|
runtime.exit(1);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
const tokenRaw = opts.token?.trim();
|
const tokenRaw = normalizeSecretInput(opts.token);
|
||||||
if (!tokenRaw) {
|
if (!tokenRaw) {
|
||||||
runtime.error("Missing --token for --auth-choice token.");
|
runtime.error("Missing --token for --auth-choice token.");
|
||||||
runtime.exit(1);
|
runtime.exit(1);
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import type { WizardPrompter } from "../wizard/prompts.js";
|
|||||||
import { installSkill } from "../agents/skills-install.js";
|
import { installSkill } from "../agents/skills-install.js";
|
||||||
import { buildWorkspaceSkillStatus } from "../agents/skills-status.js";
|
import { buildWorkspaceSkillStatus } from "../agents/skills-status.js";
|
||||||
import { formatCliCommand } from "../cli/command-format.js";
|
import { formatCliCommand } from "../cli/command-format.js";
|
||||||
|
import { normalizeSecretInput } from "../utils/normalize-secret-input.js";
|
||||||
import { detectBinary, resolveNodeManagerOptions } from "./onboard-helpers.js";
|
import { detectBinary, resolveNodeManagerOptions } from "./onboard-helpers.js";
|
||||||
|
|
||||||
function summarizeInstallFailure(message: string): string | undefined {
|
function summarizeInstallFailure(message: string): string | undefined {
|
||||||
@@ -198,7 +199,7 @@ export async function setupSkills(
|
|||||||
validate: (value) => (value?.trim() ? undefined : "Required"),
|
validate: (value) => (value?.trim() ? undefined : "Required"),
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
next = upsertSkillEntry(next, skill.skillKey, { apiKey: apiKey.trim() });
|
next = upsertSkillEntry(next, skill.skillKey, { apiKey: normalizeSecretInput(apiKey) });
|
||||||
}
|
}
|
||||||
|
|
||||||
return next;
|
return next;
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import { loadWorkspaceSkillEntries, type SkillEntry } from "../../agents/skills.
|
|||||||
import { loadConfig, writeConfigFile } from "../../config/config.js";
|
import { loadConfig, writeConfigFile } from "../../config/config.js";
|
||||||
import { getRemoteSkillEligibility } from "../../infra/skills-remote.js";
|
import { getRemoteSkillEligibility } from "../../infra/skills-remote.js";
|
||||||
import { normalizeAgentId } from "../../routing/session-key.js";
|
import { normalizeAgentId } from "../../routing/session-key.js";
|
||||||
|
import { normalizeSecretInput } from "../../utils/normalize-secret-input.js";
|
||||||
import {
|
import {
|
||||||
ErrorCodes,
|
ErrorCodes,
|
||||||
errorShape,
|
errorShape,
|
||||||
@@ -181,7 +182,7 @@ export const skillsHandlers: GatewayRequestHandlers = {
|
|||||||
current.enabled = p.enabled;
|
current.enabled = p.enabled;
|
||||||
}
|
}
|
||||||
if (typeof p.apiKey === "string") {
|
if (typeof p.apiKey === "string") {
|
||||||
const trimmed = p.apiKey.trim();
|
const trimmed = normalizeSecretInput(p.apiKey);
|
||||||
if (trimmed) {
|
if (trimmed) {
|
||||||
current.apiKey = trimmed;
|
current.apiKey = trimmed;
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import {
|
|||||||
import { getCustomProviderApiKey, resolveEnvApiKey } from "../agents/model-auth.js";
|
import { getCustomProviderApiKey, resolveEnvApiKey } from "../agents/model-auth.js";
|
||||||
import { normalizeProviderId } from "../agents/model-selection.js";
|
import { normalizeProviderId } from "../agents/model-selection.js";
|
||||||
import { loadConfig } from "../config/config.js";
|
import { loadConfig } from "../config/config.js";
|
||||||
|
import { normalizeSecretInput } from "../utils/normalize-secret-input.js";
|
||||||
|
|
||||||
export type ProviderAuth = {
|
export type ProviderAuth = {
|
||||||
provider: UsageProviderId;
|
provider: UsageProviderId;
|
||||||
@@ -34,7 +35,8 @@ function parseGoogleToken(apiKey: string): { token: string } | null {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function resolveZaiApiKey(): string | undefined {
|
function resolveZaiApiKey(): string | undefined {
|
||||||
const envDirect = process.env.ZAI_API_KEY?.trim() || process.env.Z_AI_API_KEY?.trim();
|
const envDirect =
|
||||||
|
normalizeSecretInput(process.env.ZAI_API_KEY) || normalizeSecretInput(process.env.Z_AI_API_KEY);
|
||||||
if (envDirect) {
|
if (envDirect) {
|
||||||
return envDirect;
|
return envDirect;
|
||||||
}
|
}
|
||||||
@@ -57,8 +59,8 @@ function resolveZaiApiKey(): string | undefined {
|
|||||||
].find((id) => store.profiles[id]?.type === "api_key");
|
].find((id) => store.profiles[id]?.type === "api_key");
|
||||||
if (apiProfile) {
|
if (apiProfile) {
|
||||||
const cred = store.profiles[apiProfile];
|
const cred = store.profiles[apiProfile];
|
||||||
if (cred?.type === "api_key" && cred.key?.trim()) {
|
if (cred?.type === "api_key" && normalizeSecretInput(cred.key)) {
|
||||||
return cred.key.trim();
|
return normalizeSecretInput(cred.key);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -79,7 +81,8 @@ function resolveZaiApiKey(): string | undefined {
|
|||||||
|
|
||||||
function resolveMinimaxApiKey(): string | undefined {
|
function resolveMinimaxApiKey(): string | undefined {
|
||||||
const envDirect =
|
const envDirect =
|
||||||
process.env.MINIMAX_CODE_PLAN_KEY?.trim() || process.env.MINIMAX_API_KEY?.trim();
|
normalizeSecretInput(process.env.MINIMAX_CODE_PLAN_KEY) ||
|
||||||
|
normalizeSecretInput(process.env.MINIMAX_API_KEY);
|
||||||
if (envDirect) {
|
if (envDirect) {
|
||||||
return envDirect;
|
return envDirect;
|
||||||
}
|
}
|
||||||
@@ -104,17 +107,17 @@ function resolveMinimaxApiKey(): string | undefined {
|
|||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
const cred = store.profiles[apiProfile];
|
const cred = store.profiles[apiProfile];
|
||||||
if (cred?.type === "api_key" && cred.key?.trim()) {
|
if (cred?.type === "api_key" && normalizeSecretInput(cred.key)) {
|
||||||
return cred.key.trim();
|
return normalizeSecretInput(cred.key);
|
||||||
}
|
}
|
||||||
if (cred?.type === "token" && cred.token?.trim()) {
|
if (cred?.type === "token" && normalizeSecretInput(cred.token)) {
|
||||||
return cred.token.trim();
|
return normalizeSecretInput(cred.token);
|
||||||
}
|
}
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
function resolveXiaomiApiKey(): string | undefined {
|
function resolveXiaomiApiKey(): string | undefined {
|
||||||
const envDirect = process.env.XIAOMI_API_KEY?.trim();
|
const envDirect = normalizeSecretInput(process.env.XIAOMI_API_KEY);
|
||||||
if (envDirect) {
|
if (envDirect) {
|
||||||
return envDirect;
|
return envDirect;
|
||||||
}
|
}
|
||||||
@@ -139,11 +142,11 @@ function resolveXiaomiApiKey(): string | undefined {
|
|||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
const cred = store.profiles[apiProfile];
|
const cred = store.profiles[apiProfile];
|
||||||
if (cred?.type === "api_key" && cred.key?.trim()) {
|
if (cred?.type === "api_key" && normalizeSecretInput(cred.key)) {
|
||||||
return cred.key.trim();
|
return normalizeSecretInput(cred.key);
|
||||||
}
|
}
|
||||||
if (cred?.type === "token" && cred.token?.trim()) {
|
if (cred?.type === "token" && normalizeSecretInput(cred.token)) {
|
||||||
return cred.token.trim();
|
return normalizeSecretInput(cred.token);
|
||||||
}
|
}
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|||||||
20
src/utils/normalize-secret-input.ts
Normal file
20
src/utils/normalize-secret-input.ts
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
/**
|
||||||
|
* Secret normalization for copy/pasted credentials.
|
||||||
|
*
|
||||||
|
* Common footgun: line breaks (especially `\r`) embedded in API keys/tokens.
|
||||||
|
* We strip line breaks anywhere, then trim whitespace at the ends.
|
||||||
|
*
|
||||||
|
* Intentionally does NOT remove ordinary spaces inside the string to avoid
|
||||||
|
* silently altering "Bearer <token>" style values.
|
||||||
|
*/
|
||||||
|
export function normalizeSecretInput(value: unknown): string {
|
||||||
|
if (typeof value !== "string") {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
return value.replace(/[\r\n\u2028\u2029]+/g, "").trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function normalizeOptionalSecretInput(value: unknown): string | undefined {
|
||||||
|
const normalized = normalizeSecretInput(value);
|
||||||
|
return normalized ? normalized : undefined;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user