mirror of
https://github.com/OpenRouterTeam/spawn.git
synced 2026-04-29 20:39:29 +00:00
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:
parent
b066b3a1ac
commit
bb4deaf24c
4 changed files with 42 additions and 23 deletions
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "@openrouter/spawn",
|
"name": "@openrouter/spawn",
|
||||||
"version": "0.11.21",
|
"version": "0.11.22",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"bin": {
|
"bin": {
|
||||||
"spawn": "cli.js"
|
"spawn": "cli.js"
|
||||||
|
|
|
||||||
|
|
@ -156,36 +156,28 @@ function getConfigPath(): string {
|
||||||
return join(process.env.HOME || homedir(), ".config", "spawn", "digitalocean.json");
|
return join(process.env.HOME || homedir(), ".config", "spawn", "digitalocean.json");
|
||||||
}
|
}
|
||||||
|
|
||||||
interface DoConfig {
|
function loadConfig(): Record<string, unknown> | null {
|
||||||
api_key?: string;
|
|
||||||
token?: string;
|
|
||||||
refresh_token?: string;
|
|
||||||
expires_at?: number;
|
|
||||||
auth_method?: "oauth" | "manual";
|
|
||||||
}
|
|
||||||
|
|
||||||
function loadConfig(): DoConfig | null {
|
|
||||||
try {
|
try {
|
||||||
return JSON.parse(readFileSync(getConfigPath(), "utf-8"));
|
return parseJsonObj(readFileSync(getConfigPath(), "utf-8"));
|
||||||
} catch {
|
} catch {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function saveConfig(config: DoConfig): Promise<void> {
|
async function saveConfig(values: Record<string, unknown>): Promise<void> {
|
||||||
const configPath = getConfigPath();
|
const configPath = getConfigPath();
|
||||||
const dir = configPath.replace(/\/[^/]+$/, "");
|
const dir = configPath.replace(/\/[^/]+$/, "");
|
||||||
mkdirSync(dir, {
|
mkdirSync(dir, {
|
||||||
recursive: true,
|
recursive: true,
|
||||||
mode: 0o700,
|
mode: 0o700,
|
||||||
});
|
});
|
||||||
await Bun.write(configPath, JSON.stringify(config, null, 2) + "\n", {
|
await Bun.write(configPath, JSON.stringify(values, null, 2) + "\n", {
|
||||||
mode: 0o600,
|
mode: 0o600,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async function saveTokenToConfig(token: string, refreshToken?: string, expiresIn?: number): Promise<void> {
|
async function saveTokenToConfig(token: string, refreshToken?: string, expiresIn?: number): Promise<void> {
|
||||||
const config: DoConfig = {
|
const config: Record<string, unknown> = {
|
||||||
api_key: token,
|
api_key: token,
|
||||||
token,
|
token,
|
||||||
};
|
};
|
||||||
|
|
@ -204,7 +196,9 @@ function loadTokenFromConfig(): string | null {
|
||||||
if (!data) {
|
if (!data) {
|
||||||
return null;
|
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) {
|
if (!token) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
@ -216,22 +210,30 @@ function loadTokenFromConfig(): string | null {
|
||||||
|
|
||||||
function loadRefreshToken(): string | null {
|
function loadRefreshToken(): string | null {
|
||||||
const data = loadConfig();
|
const data = loadConfig();
|
||||||
if (!data?.refresh_token) {
|
if (!data) {
|
||||||
return null;
|
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 null;
|
||||||
}
|
}
|
||||||
return data.refresh_token;
|
if (!/^[a-zA-Z0-9._/@:+=, -]+$/.test(refreshToken)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return refreshToken;
|
||||||
}
|
}
|
||||||
|
|
||||||
function isTokenExpired(): boolean {
|
function isTokenExpired(): boolean {
|
||||||
const data = loadConfig();
|
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;
|
return false;
|
||||||
}
|
}
|
||||||
// Consider expired 5 minutes before actual expiry
|
// 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 ────────────────────────────────────────────────────────
|
// ─── Token Validation ────────────────────────────────────────────────────────
|
||||||
|
|
|
||||||
|
|
@ -187,13 +187,27 @@ function getGcloudCmd(): string | null {
|
||||||
return 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. */
|
/** Run a gcloud command and return stdout. */
|
||||||
function gcloudSync(args: string[]): {
|
function gcloudSync(args: string[]): {
|
||||||
stdout: string;
|
stdout: string;
|
||||||
stderr: string;
|
stderr: string;
|
||||||
exitCode: number;
|
exitCode: number;
|
||||||
} {
|
} {
|
||||||
const cmd = getGcloudCmd()!;
|
const cmd = requireGcloudCmd();
|
||||||
const proc = Bun.spawnSync(
|
const proc = Bun.spawnSync(
|
||||||
[
|
[
|
||||||
cmd,
|
cmd,
|
||||||
|
|
@ -221,7 +235,7 @@ async function gcloud(args: string[]): Promise<{
|
||||||
stderr: string;
|
stderr: string;
|
||||||
exitCode: number;
|
exitCode: number;
|
||||||
}> {
|
}> {
|
||||||
const cmd = getGcloudCmd()!;
|
const cmd = requireGcloudCmd();
|
||||||
const proc = Bun.spawn(
|
const proc = Bun.spawn(
|
||||||
[
|
[
|
||||||
cmd,
|
cmd,
|
||||||
|
|
@ -250,7 +264,7 @@ async function gcloud(args: string[]): Promise<{
|
||||||
|
|
||||||
/** Run a gcloud command interactively (inheriting stdio). */
|
/** Run a gcloud command interactively (inheriting stdio). */
|
||||||
async function gcloudInteractive(args: string[]): Promise<number> {
|
async function gcloudInteractive(args: string[]): Promise<number> {
|
||||||
const cmd = getGcloudCmd()!;
|
const cmd = requireGcloudCmd();
|
||||||
const proc = Bun.spawn(
|
const proc = Bun.spawn(
|
||||||
[
|
[
|
||||||
cmd,
|
cmd,
|
||||||
|
|
|
||||||
|
|
@ -198,6 +198,7 @@ function tryLoadFromDiskCache(): Manifest | null {
|
||||||
function updateCache(manifest: Manifest): Manifest {
|
function updateCache(manifest: Manifest): Manifest {
|
||||||
writeCache(manifest);
|
writeCache(manifest);
|
||||||
_cached = manifest;
|
_cached = manifest;
|
||||||
|
_staleCache = false;
|
||||||
return manifest;
|
return manifest;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -233,6 +234,7 @@ export async function loadManifest(forceRefresh = false): Promise<Manifest> {
|
||||||
const local = tryLoadLocalManifest();
|
const local = tryLoadLocalManifest();
|
||||||
if (local) {
|
if (local) {
|
||||||
_cached = local;
|
_cached = local;
|
||||||
|
_staleCache = false;
|
||||||
return local;
|
return local;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -241,6 +243,7 @@ export async function loadManifest(forceRefresh = false): Promise<Manifest> {
|
||||||
const cached = tryLoadFromDiskCache();
|
const cached = tryLoadFromDiskCache();
|
||||||
if (cached) {
|
if (cached) {
|
||||||
_cached = cached;
|
_cached = cached;
|
||||||
|
_staleCache = false;
|
||||||
return cached;
|
return cached;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue