mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-09 03:16:38 +00:00
fix(cli): address code-review findings on auth PR
This commit is contained in:
@@ -0,0 +1,123 @@
|
||||
/**
|
||||
* Chain resolver for HeyGen credentials.
|
||||
*
|
||||
* Priority — first non-empty wins:
|
||||
* 1. `HEYGEN_API_KEY` env (matches heygen-cli)
|
||||
* 2. `HYPERFRAMES_API_KEY` env (alias for parity with other tools)
|
||||
* 3. `~/.heygen/credentials` (JSON) — unexpired OAuth, else api_key
|
||||
*
|
||||
* Absent sources fall through. A broken file (parse error, bad shape)
|
||||
* surfaces immediately as `ErrInvalidStore` — silently falling back
|
||||
* would mask user config bugs.
|
||||
*
|
||||
* Expiry policy: an OAuth access_token whose `expires_at` is in the
|
||||
* past (60s skew) is considered expired. If a `refresh_token` is also
|
||||
* present, callers can still use it via `refreshable: true`. Otherwise
|
||||
* the api_key (if any) wins.
|
||||
*/
|
||||
|
||||
import { isHeaderSafe, readStore } from "./store.js";
|
||||
import { ErrInvalidStore, ErrNotConfigured, isAuthError } from "./errors.js";
|
||||
|
||||
type CredentialSource = "env" | "env_alias" | "file_json" | "file_legacy";
|
||||
|
||||
interface ApiKeyCredential {
|
||||
type: "api_key";
|
||||
key: string;
|
||||
source: CredentialSource;
|
||||
}
|
||||
|
||||
interface OAuthCredential {
|
||||
type: "oauth";
|
||||
access_token: string;
|
||||
refresh_token?: string;
|
||||
expires_at?: Date;
|
||||
scope?: string;
|
||||
source: CredentialSource;
|
||||
/** True when the access_token is expired but a refresh_token exists. */
|
||||
refreshable: boolean;
|
||||
}
|
||||
|
||||
export type ResolvedCredential = ApiKeyCredential | OAuthCredential;
|
||||
|
||||
const EXPIRY_SKEW_MS = 60 * 1000;
|
||||
|
||||
export interface ResolveOptions {
|
||||
now?: () => Date;
|
||||
}
|
||||
|
||||
export async function resolveCredential(opts: ResolveOptions = {}): Promise<ResolvedCredential> {
|
||||
const now = (opts.now ?? (() => new Date()))();
|
||||
|
||||
const heygenEnv = process.env["HEYGEN_API_KEY"];
|
||||
if (heygenEnv && heygenEnv.length > 0) {
|
||||
if (!isHeaderSafe(heygenEnv)) {
|
||||
throw ErrInvalidStore("HEYGEN_API_KEY contains control characters");
|
||||
}
|
||||
return { type: "api_key", key: heygenEnv, source: "env" };
|
||||
}
|
||||
|
||||
const hfEnv = process.env["HYPERFRAMES_API_KEY"];
|
||||
if (hfEnv && hfEnv.length > 0) {
|
||||
if (!isHeaderSafe(hfEnv)) {
|
||||
throw ErrInvalidStore("HYPERFRAMES_API_KEY contains control characters");
|
||||
}
|
||||
return { type: "api_key", key: hfEnv, source: "env_alias" };
|
||||
}
|
||||
|
||||
const { credentials, source } = await readStore();
|
||||
if (source === "absent") throw ErrNotConfigured();
|
||||
|
||||
const fileSource: CredentialSource = source === "file_legacy" ? "file_legacy" : "file_json";
|
||||
|
||||
if (credentials.oauth) {
|
||||
const oauth = pickOAuth(credentials.oauth, now, fileSource);
|
||||
if (oauth) return oauth;
|
||||
}
|
||||
if (credentials.api_key) {
|
||||
return { type: "api_key", key: credentials.api_key, source: fileSource };
|
||||
}
|
||||
throw ErrNotConfigured();
|
||||
}
|
||||
|
||||
/** Like `resolveCredential` but returns `null` instead of throwing `NOT_CONFIGURED`. */
|
||||
export async function tryResolveCredential(
|
||||
opts: ResolveOptions = {},
|
||||
): Promise<ResolvedCredential | null> {
|
||||
try {
|
||||
return await resolveCredential(opts);
|
||||
} catch (err) {
|
||||
if (isAuthError(err) && err.code === "NOT_CONFIGURED") {
|
||||
return null;
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
function pickOAuth(
|
||||
tokens: NonNullable<Awaited<ReturnType<typeof readStore>>["credentials"]["oauth"]>,
|
||||
now: Date,
|
||||
source: CredentialSource,
|
||||
): OAuthCredential | null {
|
||||
const expiresAt = parseDate(tokens.expires_at);
|
||||
const expired = expiresAt !== undefined && expiresAt.getTime() - EXPIRY_SKEW_MS < now.getTime();
|
||||
|
||||
if (expired && !tokens.refresh_token) return null;
|
||||
|
||||
const out: OAuthCredential = {
|
||||
type: "oauth",
|
||||
access_token: tokens.access_token,
|
||||
source,
|
||||
refreshable: expired && tokens.refresh_token !== undefined,
|
||||
};
|
||||
if (tokens.refresh_token) out.refresh_token = tokens.refresh_token;
|
||||
if (expiresAt) out.expires_at = expiresAt;
|
||||
if (tokens.scope) out.scope = tokens.scope;
|
||||
return out;
|
||||
}
|
||||
|
||||
function parseDate(s: string | undefined): Date | undefined {
|
||||
if (!s) return undefined;
|
||||
const d = new Date(s);
|
||||
return Number.isNaN(d.getTime()) ? undefined : d;
|
||||
}
|
||||
Reference in New Issue
Block a user