fix: reset stale cache flag, guard gcloud null, validate DO config (#2073)

- manifest.ts: Reset _staleCache on successful fetch/cache load so
  isStaleCache() doesn't falsely report stale data after reconnecting
- gcp.ts: Replace getGcloudCmd()! with requireGcloudCmd() that throws
  a descriptive error instead of crashing with null dereference
- digitalocean.ts: Replace unvalidated JSON.parse return with
  parseJsonObj() + isString()/isNumber() guards for type safety

Agent: code-health

Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
A 2026-03-01 14:08:38 -08:00 committed by GitHub
parent b066b3a1ac
commit bb4deaf24c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 42 additions and 23 deletions

View file

@ -1,6 +1,6 @@
{
"name": "@openrouter/spawn",
"version": "0.11.21",
"version": "0.11.22",
"type": "module",
"bin": {
"spawn": "cli.js"

View file

@ -156,36 +156,28 @@ function getConfigPath(): string {
return join(process.env.HOME || homedir(), ".config", "spawn", "digitalocean.json");
}
interface DoConfig {
api_key?: string;
token?: string;
refresh_token?: string;
expires_at?: number;
auth_method?: "oauth" | "manual";
}
function loadConfig(): DoConfig | null {
function loadConfig(): Record<string, unknown> | null {
try {
return JSON.parse(readFileSync(getConfigPath(), "utf-8"));
return parseJsonObj(readFileSync(getConfigPath(), "utf-8"));
} catch {
return null;
}
}
async function saveConfig(config: DoConfig): Promise<void> {
async function saveConfig(values: Record<string, unknown>): Promise<void> {
const configPath = getConfigPath();
const dir = configPath.replace(/\/[^/]+$/, "");
mkdirSync(dir, {
recursive: true,
mode: 0o700,
});
await Bun.write(configPath, JSON.stringify(config, null, 2) + "\n", {
await Bun.write(configPath, JSON.stringify(values, null, 2) + "\n", {
mode: 0o600,
});
}
async function saveTokenToConfig(token: string, refreshToken?: string, expiresIn?: number): Promise<void> {
const config: DoConfig = {
const config: Record<string, unknown> = {
api_key: token,
token,
};
@ -204,7 +196,9 @@ function loadTokenFromConfig(): string | null {
if (!data) {
return null;
}
const token = data.api_key || data.token || "";
const apiKey = isString(data.api_key) ? data.api_key : "";
const tok = isString(data.token) ? data.token : "";
const token = apiKey || tok;
if (!token) {
return null;
}
@ -216,22 +210,30 @@ function loadTokenFromConfig(): string | null {
function loadRefreshToken(): string | null {
const data = loadConfig();
if (!data?.refresh_token) {
if (!data) {
return null;
}
if (!/^[a-zA-Z0-9._/@:+=, -]+$/.test(data.refresh_token)) {
const refreshToken = isString(data.refresh_token) ? data.refresh_token : "";
if (!refreshToken) {
return null;
}
return data.refresh_token;
if (!/^[a-zA-Z0-9._/@:+=, -]+$/.test(refreshToken)) {
return null;
}
return refreshToken;
}
function isTokenExpired(): boolean {
const data = loadConfig();
if (!data?.expires_at) {
if (!data) {
return false;
}
const expiresAt = isNumber(data.expires_at) ? data.expires_at : 0;
if (!expiresAt) {
return false;
}
// Consider expired 5 minutes before actual expiry
return Math.floor(Date.now() / 1000) >= data.expires_at - 300;
return Math.floor(Date.now() / 1000) >= expiresAt - 300;
}
// ─── Token Validation ────────────────────────────────────────────────────────

View file

@ -187,13 +187,27 @@ function getGcloudCmd(): string | null {
return null;
}
/** Get gcloud path or throw a descriptive error. */
function requireGcloudCmd(): string {
const cmd = getGcloudCmd();
if (!cmd) {
throw new Error(
"gcloud CLI not found. Install it first:\n" +
" macOS: brew install --cask google-cloud-sdk\n" +
" Linux: curl https://sdk.cloud.google.com | bash\n" +
" Or run: spawn <agent> gcp (auto-installs gcloud)",
);
}
return cmd;
}
/** Run a gcloud command and return stdout. */
function gcloudSync(args: string[]): {
stdout: string;
stderr: string;
exitCode: number;
} {
const cmd = getGcloudCmd()!;
const cmd = requireGcloudCmd();
const proc = Bun.spawnSync(
[
cmd,
@ -221,7 +235,7 @@ async function gcloud(args: string[]): Promise<{
stderr: string;
exitCode: number;
}> {
const cmd = getGcloudCmd()!;
const cmd = requireGcloudCmd();
const proc = Bun.spawn(
[
cmd,
@ -250,7 +264,7 @@ async function gcloud(args: string[]): Promise<{
/** Run a gcloud command interactively (inheriting stdio). */
async function gcloudInteractive(args: string[]): Promise<number> {
const cmd = getGcloudCmd()!;
const cmd = requireGcloudCmd();
const proc = Bun.spawn(
[
cmd,

View file

@ -198,6 +198,7 @@ function tryLoadFromDiskCache(): Manifest | null {
function updateCache(manifest: Manifest): Manifest {
writeCache(manifest);
_cached = manifest;
_staleCache = false;
return manifest;
}
@ -233,6 +234,7 @@ export async function loadManifest(forceRefresh = false): Promise<Manifest> {
const local = tryLoadLocalManifest();
if (local) {
_cached = local;
_staleCache = false;
return local;
}
@ -241,6 +243,7 @@ export async function loadManifest(forceRefresh = false): Promise<Manifest> {
const cached = tryLoadFromDiskCache();
if (cached) {
_cached = cached;
_staleCache = false;
return cached;
}
}