diff --git a/packages/cli/package.json b/packages/cli/package.json index e2af99fd..2255850e 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -1,6 +1,6 @@ { "name": "@openrouter/spawn", - "version": "0.12.9", + "version": "0.12.10", "type": "module", "bin": { "spawn": "cli.js" diff --git a/packages/cli/src/daytona/daytona.ts b/packages/cli/src/daytona/daytona.ts index 7f706aa3..2b73663f 100644 --- a/packages/cli/src/daytona/daytona.ts +++ b/packages/cli/src/daytona/daytona.ts @@ -12,6 +12,7 @@ import { prompt, jsonEscape, getSpawnCloudConfigPath, + loadApiToken, validateServerName, toKebabCase, defaultSpawnName, @@ -108,19 +109,7 @@ async function saveTokenToConfig(token: string): Promise { } function loadTokenFromConfig(): string | null { - try { - const data = JSON.parse(readFileSync(getSpawnCloudConfigPath("daytona"), "utf-8")); - const token = data.api_key || data.token || ""; - if (!token) { - return null; - } - if (!/^[a-zA-Z0-9._/@:+=, -]+$/.test(token)) { - return null; - } - return token; - } catch { - return null; - } + return loadApiToken("daytona"); } async function testDaytonaToken(): Promise { diff --git a/packages/cli/src/hetzner/hetzner.ts b/packages/cli/src/hetzner/hetzner.ts index 3205bb34..d60a3056 100644 --- a/packages/cli/src/hetzner/hetzner.ts +++ b/packages/cli/src/hetzner/hetzner.ts @@ -12,6 +12,7 @@ import { prompt, jsonEscape, getSpawnCloudConfigPath, + loadApiToken, validateServerName, validateRegionName, toKebabCase, @@ -105,20 +106,7 @@ async function saveTokenToConfig(token: string): Promise { } function loadTokenFromConfig(): string | null { - try { - const data = JSON.parse(readFileSync(getSpawnCloudConfigPath("hetzner"), "utf-8")); - const token = data.api_key || data.token || ""; - if (!token) { - return null; - } - // Security: validate token chars - if (!/^[a-zA-Z0-9._/@:+=, -]+$/.test(token)) { - return null; - } - return token; - } catch { - return null; - } + return loadApiToken("hetzner"); } // ─── Token Validation ──────────────────────────────────────────────────────── diff --git a/packages/cli/src/shared/ui.ts b/packages/cli/src/shared/ui.ts index 029ff7ad..4ec6e09b 100644 --- a/packages/cli/src/shared/ui.ts +++ b/packages/cli/src/shared/ui.ts @@ -2,6 +2,7 @@ // @clack/prompts is bundled into cli.js at build time. import * as p from "@clack/prompts"; +import { readFileSync } from "node:fs"; import { homedir } from "node:os"; import { join } from "node:path"; import { isString } from "./type-guards"; @@ -225,6 +226,27 @@ export function getSpawnCloudConfigPath(cloud: string): string { return join(process.env.HOME || homedir(), ".config", "spawn", `${cloud}.json`); } +/** + * Load an API token from the per-cloud config file. + * Reads `api_key` or `token` field and validates allowed characters. + * Returns null if the file is missing, unreadable, or the token is invalid. + */ +export function loadApiToken(cloud: string): string | null { + try { + const data = JSON.parse(readFileSync(getSpawnCloudConfigPath(cloud), "utf-8")); + const token = (isString(data.api_key) ? data.api_key : "") || (isString(data.token) ? data.token : ""); + if (!token) { + return null; + } + if (!/^[a-zA-Z0-9._/@:+=, -]+$/.test(token)) { + return null; + } + return token; + } catch { + return null; + } +} + /** JSON-escape a string (returns the quoted JSON string). */ export function jsonEscape(s: string): string { return JSON.stringify(s); diff --git a/packages/cli/src/update-check.ts b/packages/cli/src/update-check.ts index 0554fbe3..b1e9fa74 100644 --- a/packages/cli/src/update-check.ts +++ b/packages/cli/src/update-check.ts @@ -21,15 +21,15 @@ export const executor = { // ── Constants ────────────────────────────────────────────────────────────────── +const FETCH_TIMEOUT = 10000; // 10 seconds +const UPDATE_BACKOFF_MS = 60 * 60 * 1000; // 1 hour + // ── Schemas ────────────────────────────────────────────────────────────────── const PkgVersionSchema = v.object({ version: v.string(), }); -const FETCH_TIMEOUT = 10000; // 10 seconds -const UPDATE_BACKOFF_MS = 60 * 60 * 1000; // 1 hour - // Validate RAW_BASE matches expected GitHub raw content URL pattern (defense-in-depth, CWE-78) const GITHUB_RAW_URL_PATTERN = /^https:\/\/raw\.githubusercontent\.com\/[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/;