Files
hyperframes/packages/cli/src/auth/errors.ts
T
James RussoandClaude Opus 5.5 d03bdeae9d fix(cli): prefer OAuth session over HEYGEN_API_KEY (#7051)
* fix(cli): prefer OAuth session over HEYGEN_API_KEY

A signed-in `hyperframes auth login` session now wins over an API key in
the environment, so a paid plan on the login is used even when the shell
also exports HEYGEN_API_KEY.

New order: HEYGEN_ACCESS_TOKEN (host app), the stored login (refreshed
when expired), HEYGEN_API_KEY, HYPERFRAMES_API_KEY, the saved API key.
The env key still comes first when HEYGEN_API_URL or HEYGEN_API_BASE
names a host app's gateway, and takes over for the rest of the process
when the login cannot be refreshed. The media-use audio helper follows
the same order.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* fix(cli): fall back to the env key only when HeyGen refuses the refresh

A 503, a network error or an overlapping refresh no longer switches the
process to HEYGEN_API_KEY. The fallback now needs a refused grant or an
expired login with no refresh token. A login another process stored since
is re-read first, and refreshes that overlap in one process share one
request. The media-use helper no longer treats HeyGen's own API base as a
gateway.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* fix(cli): keep the login when the refresh reply can't be read

Only a 400/401 from the token endpoint marks the grant as refused and
switches to the env key. A 200 whose body can't be parsed now throws and
leaves the login in place, so the next call refreshes again.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-11-03 01:52:30 +00:00

100 lines
2.8 KiB
TypeScript

/**
* Typed errors for the auth layer. Callers branch on `code` so commands
* can map specific failures to friendly UX without parsing messages.
*/
export type AuthErrorCode =
| "NOT_CONFIGURED"
| "INVALID_STORE"
| "API_ERROR"
| "UNAUTHENTICATED"
| "OAUTH_NOT_CONFIGURED"
| "DEVICE_AUTH_FAILED"
| "REFRESH_FAILED"
| "LOGIN_EXPIRED"
| "LOGIN_CHANGED";
export class AuthError extends Error {
readonly code: AuthErrorCode;
readonly hint?: string;
/** HeyGen refused a token refresh (400/401), so the grant is gone; a reply the CLI couldn't read is not this. */
readonly refused: boolean;
constructor(
code: AuthErrorCode,
message: string,
hint?: string,
opts: { refused?: boolean } = {},
) {
super(message);
this.name = "AuthError";
this.code = code;
this.hint = hint;
this.refused = opts.refused ?? false;
}
}
export const ErrNotConfigured = () =>
new AuthError(
"NOT_CONFIGURED",
"No HeyGen credentials found",
"Run `hyperframes auth login` to sign in.",
);
export const ErrInvalidStore = (detail: string) =>
new AuthError(
"INVALID_STORE",
`Credential file is unreadable: ${detail}`,
"Delete ~/.hyperframes/credentials and run `hyperframes auth login` to re-create it.",
);
export const ErrUnauthenticated = (detail?: string) =>
new AuthError(
"UNAUTHENTICATED",
detail ? `HeyGen rejected the credential: ${detail}` : "HeyGen rejected the credential",
"Run `hyperframes auth login` to re-authenticate.",
);
export const ErrApi = (status: number, detail: string) =>
new AuthError("API_ERROR", `HeyGen API error (${status}): ${detail}`);
export const ErrOAuthNotConfigured = () =>
new AuthError(
"OAUTH_NOT_CONFIGURED",
"OAuth client is not configured",
"Set HYPERFRAMES_OAUTH_CLIENT_ID, or run `hyperframes auth login --api-key`.",
);
export const ErrRefreshFailed = (detail?: string, opts: { refused?: boolean } = {}) =>
new AuthError(
"REFRESH_FAILED",
detail ? `Failed to refresh OAuth tokens: ${detail}` : "Failed to refresh OAuth tokens",
"Run `hyperframes auth login` to re-authenticate.",
opts,
);
export const ErrLoginExpired = () =>
new AuthError(
"LOGIN_EXPIRED",
"Your HeyGen login expired",
"Run `hyperframes auth login` to sign in again.",
);
export const ErrLoginChanged = () =>
new AuthError(
"LOGIN_CHANGED",
"Your HeyGen login changed while this command ran",
"Run the command again.",
);
export const ErrDeviceAuthFailed = (detail: string) =>
new AuthError(
"DEVICE_AUTH_FAILED",
`Device authorization failed: ${detail}`,
"Run `hyperframes auth login --device` to start a new code.",
);
export function isAuthError(err: unknown): err is AuthError {
return err instanceof AuthError;
}