app/cli/lib/credentials.ts
zzmq.dev@gmail.com/zz / app / cli / lib / credentials.ts
@ 568239fb
Download
Ladder Summary
- file This module manages the local storage and retrieval of the user’s Cloudflare‑API credentials for the **zz** CLI (reading, writing, clearing a JSON file in ~/.zz/credentials.json, exposing the default API base, and constructing an authenticated git clone URL). Exported symbols are: - **ZzCredentials** (interface) - **DEFAULT_API_BASE** (string) - **readCredentials()** - **requireCredentials()** - **clearCredentials()** - **writeCredentials(creds: ZzCredentials)** - **apiBase()** - **authedCloneUrl(base: string, token: string, slug: string)**.
// Credentials for the REMOTE (Cloudflare) backend's JSON API -- a
// bearer token from `zz login`, separate from config.ts's
// "which repos do I care about" local config (a different concern:
// this is who you are to the remote API, that's what you watch).
import { chmodSync, existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
import { homedir } from "node:os";
import { dirname, join } from "node:path";
import { resolveUserPath } from "./paths.js";
export interface ZzCredentials {
apiBase: string;
token: string;
username: string; // the real identity -- no separate internal id (see worker migration 0006)
}
// The deployed Worker's current URL -- the one thing in the CLI that
// really is a hardcoded default, since a fresh `zz login` run has no
// other way to know where to start. Must match whatever host the
// OAuth redirect URIs (CF_OAUTH_REDIRECT_URI/GOOGLE_OAUTH_REDIRECT_URI,
// worker/wrangler.jsonc) are pinned to -- a fresh `zz login` with no
// credentials.json yet falls back to this default to open the browser,
// and if that host doesn't match the pinned redirect_uri's host, the
// state cookie (set on this host) never reaches the callback (lands
// on the redirect_uri's host), failing with "OAuth state mismatch"
// every time. Confirmed live: this was still the pre-zzmq.dev value
// after the redirect URIs were flipped, breaking exactly this path
// for anyone with no prior login. Override with ZZ_API_BASE if it
// ever needs to differ from the current canonical domain.
export const DEFAULT_API_BASE = "https://zzmq.dev";
function credentialsPath(): string {
return process.env.ZZ_CREDENTIALS ? resolveUserPath(process.env.ZZ_CREDENTIALS) : join(homedir(), ".zz", "credentials.json");
}
export function readCredentials(): ZzCredentials | null {
const path = credentialsPath();
if (!existsSync(path)) return null;
return JSON.parse(readFileSync(path, "utf8")) as ZzCredentials;
}
export function requireCredentials(): ZzCredentials {
const creds = readCredentials();
if (!creds) throw new Error(`not logged in -- run \`zz login\` first (looked for credentials at ${credentialsPath()})`);
return creds;
}
/** `zz logout`: drops the local credentials file -- no server-side token revocation (the OAuth-minted token just stops being used locally; it's not a managed-agent token like identity.ts's --as path, which does get revoked). Returns the username that was logged out, or null if nothing was there to begin with. */
export function clearCredentials(): string | null {
const creds = readCredentials();
if (!creds) return null;
rmSync(credentialsPath(), { force: true });
return creds.username;
}
export function writeCredentials(creds: ZzCredentials): void {
const path = credentialsPath();
mkdirSync(dirname(path), { recursive: true });
writeFileSync(path, JSON.stringify(creds, null, 2) + "\n", "utf8");
try {
chmodSync(path, 0o600); // best-effort; a real secret, same spirit as ~/.ssh keys
} catch {
// chmod can fail on some filesystems (e.g. certain Windows setups) -- not fatal
}
}
export function apiBase(): string {
return process.env.ZZ_API_BASE ?? readCredentials()?.apiBase ?? DEFAULT_API_BASE;
}
/** A real `git clone`-able URL with the bearer token embedded as Basic Auth userinfo, against the zz-maintainer Worker's own git smart-HTTP proxy (not a raw Artifacts URL -- see site/content/git-compatibility). Shared by `zz repo clone` and the remote work loop's own ensureCloned, so there's one place this convention lives. */
export function authedCloneUrl(base: string, token: string, slug: string): string {
const host = base.replace(/^https?:\/\//, "");
return `https://${encodeURIComponent(token)}@${host}/${slug}`;
}