Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -238,7 +238,7 @@ jobs:
- "packages/cli/src/{background-removal,registry,telemetry}/**"
- "packages/cli/src/capture/{captureCompositionFrame,captureTimeout,navigateForCapture,readBoundedResponse}.ts"
- "packages/cli/src/media-use/lib/config-lock.mjs"
- "packages/cli/src/auth/{paths,telemetryIdentity}.ts"
- "packages/cli/src/auth/{config,errors,paths,telemetryIdentity}.ts"
- "packages/cli/scripts/**"
- "packages/core/stubs/**"
- "packages/engine/src/**"
Expand Down
17 changes: 10 additions & 7 deletions docs/guides/authentication.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,9 @@ Signing in is the same OAuth step as creating an account — new users land on t
</Step>
</Steps>

The credential lives in `~/.heygen/credentials` (mode `0600`) — no per-repo `.env` to manage. Browser OAuth is a `hyperframes auth login` feature. The separate [`heygen` CLI](https://github.com/heygen-com/heygen-cli) (its own install — there's no `npx heygen`) is API-key-only, so `heygen auth login` just stores a key you paste. Both read the same `~/.heygen/credentials`, so signing in with one carries to the other.
HyperFrames uses its own public OAuth client and stores credentials in `~/.hyperframes/credentials` (mode `0600`). Sign in again after upgrading from the shared HeyGen login. The separate [`heygen` CLI](https://github.com/heygen-com/heygen-cli) keeps its own login in `~/.heygen`; HyperFrames does not move or delete it. An expired HyperFrames OAuth session requires refresh or sign-in and never selects a saved API key instead. An explicit API-key login or environment key selects API-key billing.

`auth status` verifies identity. For a dedicated HyperFrames OAuth login it reports the billing product, but the HyperFrames balance is unavailable here: `/v3/users/me` returns generic HeyGen account billing fields. Those raw fields remain in JSON and are not presented as the HyperFrames balance. Host-injected or custom-client OAuth is reported as an unverified billing product.

<Tip>
No account is needed to try HyperFrames locally. With no credential, voice can
Expand All @@ -54,12 +56,11 @@ sound retrieval. It resolves first-match-wins:
2. `HEYGEN_ACCESS_TOKEN` — an OAuth token a host app injects
3. `HEYGEN_API_KEY` — environment variable
4. `HYPERFRAMES_API_KEY` — alias, for parity with other tools
5. `~/.heygen/credentials` — written by `hyperframes auth login` (or `heygen auth login`)
5. `~/.hyperframes/credentials` — written by `hyperframes auth login`

A nearby project `.env` can supply the keys, but never `HEYGEN_API_BASE`. Point at a different config directory
with `HEYGEN_CONFIG_DIR`. The `hyperframes` CLI's own commands check the API keys before `HEYGEN_ACCESS_TOKEN`; see
the [CLI reference](/packages/cli). `media-use resolve` searches through the separate `heygen` CLI, which resolves its
own credential.
with `HYPERFRAMES_CONFIG_DIR`. The `hyperframes` CLI's own commands check the API keys before `HEYGEN_ACCESS_TOKEN`; see
the [CLI reference](/packages/cli). When `media-use` invokes the separate `heygen` executable, it supplies an isolated temporary credential containing the selected access token. That helper cannot refresh a HyperFrames grant or pick up another HeyGen login.

## Providers used by agent workflows

Expand All @@ -70,7 +71,7 @@ vision key is available.

| Capability | Provider order | Key(s) — first match wins | Local dependency |
|------------|----------------|---------------------------|------------------|
| **Voice (TTS)** | HeyGen → ElevenLabs → Kokoro | `HEYGEN_API_KEY` → `HYPERFRAMES_API_KEY` → `~/.heygen` · then `ELEVENLABS_API_KEY` | Kokoro: `pip install kokoro-onnx soundfile` |
| **Voice (TTS)** | HeyGen → ElevenLabs → Kokoro | `HEYGEN_API_KEY` → `HYPERFRAMES_API_KEY` → `~/.hyperframes` · then `ELEVENLABS_API_KEY` | Kokoro: `pip install kokoro-onnx soundfile` |
| **Music (BGM)** | HeyGen library → Lyria → MusicGen | HeyGen credential (above) · then `GEMINI_API_KEY` → `GOOGLE_API_KEY` | MusicGen: `pip install transformers torch soundfile numpy` |
| **Sound effects** | HeyGen library → bundled library | HeyGen credential (above) | bundled — no deps |
| **Capture descriptions** | OpenRouter → Gemini | `OPENROUTER_API_KEY` → `GEMINI_API_KEY` | None; optional for [website capture](/guides/product-launch-video) |
Expand Down Expand Up @@ -145,7 +146,9 @@ to a shared space with `--space`.
| `HEYGEN_ACCESS_TOKEN` | OAuth token a host app injects; never refreshed or saved. |
| `HEYGEN_API_BASE` | A host app's gateway for media workflows; never read from a project `.env`. |
| `HEYGEN_API_URL` | API base URL for `hyperframes` CLI commands (default `https://api.heygen.com`). |
| `HEYGEN_CONFIG_DIR` | Credentials directory (default `~/.heygen`). |
| `HYPERFRAMES_CONFIG_DIR` | HyperFrames credentials root (default `~/.hyperframes`). |
| `HYPERFRAMES_ENV` | `prod` (default) or `dev`; switches OAuth and API defaults together. Dev credentials live under the root’s `dev/` directory. |
| `HYPERFRAMES_OAUTH_CLIENT_ID` | Override the public client ID for a controlled OAuth environment. |
| `ELEVENLABS_API_KEY` | ElevenLabs TTS, used when no HeyGen credential is present. |
| `GEMINI_API_KEY` / `GOOGLE_API_KEY` | Explicit Gemini TTS selection and Lyria music generation; capture descriptions use `GEMINI_API_KEY`. |
| `GOOGLE_APPLICATION_CREDENTIALS` / `GCS_CREDS` | Gemini TTS service-account JSON file path / injected JSON. Used when neither API key is set. |
Expand Down
21 changes: 12 additions & 9 deletions docs/packages/cli.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -682,10 +682,11 @@ and are copied to `~/.media/` so other projects can reuse them. Sound effects
under CC BY get their credit written to `.media/audio/sfx/CREDITS.md`.

**What needs an account, and what can cost money.** The HeyGen sources run
through the separate `heygen` CLI, signed in with `heygen auth login --oauth`.
Searching the music, image, and icon catalogs is free. Text to speech and avatar
video draw on a free allowance when you are signed in with OAuth, then on your
HeyGen billing, so an agent should ask before generating them. When a
through the separate `heygen` executable using the login selected by
`hyperframes auth login`. It receives a temporary access-token credential and
cannot read the separate HeyGen login or refresh a HyperFrames grant.
The server decides feature eligibility and charges. An explicit API-key credential
uses API-key billing. When a
host app sets `HEYGEN_API_BASE` and `HEYGEN_API_KEY`, HeyGen calls go through that
host instead and are charged to its key. The `codex` image fallback uses your
ChatGPT subscription. Openverse, theSVG, GitHub, favicons, color grades, and the
Expand Down Expand Up @@ -2183,16 +2184,16 @@ it is anonymous until you sign in. Nothing calls it by hand.

## hyperframes auth

Sign in to HeyGen and manage credentials. They live in `~/.heygen/credentials`
(mode `0600`) and are **shared with the `heygen` CLI** — sign in with one and
the other picks up the session.
Sign into your HeyGen account through the dedicated HyperFrames OAuth client.
Credentials live in `~/.hyperframes/credentials` (mode `0600`), independently
of the `heygen` CLI. A previous shared login requires a fresh HyperFrames sign-in.

First match wins:

1. `HEYGEN_API_KEY`
2. `HYPERFRAMES_API_KEY` (a HyperFrames alias for the same thing)
3. `HEYGEN_ACCESS_TOKEN` (an OAuth token a host app injects; never refreshed or saved)
4. `~/.heygen/credentials`
4. `~/.hyperframes/credentials`

### `auth login`

Expand Down Expand Up @@ -2252,7 +2253,9 @@ hyperframes auth logout --yes # no prompt
| `HYPERFRAMES_API_KEY` | Alias for `HEYGEN_API_KEY`. |
| `HEYGEN_ACCESS_TOKEN` | OAuth token from a host app; never refreshed. |
| `HEYGEN_API_URL` | API base URL (default `https://api.heygen.com`). |
| `HEYGEN_CONFIG_DIR` | Credentials directory (default `~/.heygen`). |
| `HYPERFRAMES_CONFIG_DIR` | Credentials root (default `~/.hyperframes`). |
| `HYPERFRAMES_ENV` | `prod` (default) or `dev`; selects matching OAuth/API hosts and a separate dev credential directory. |
| `HYPERFRAMES_OAUTH_CLIENT_ID` | Override the selected public OAuth client. |

For the keys other capabilities use — ElevenLabs and Gemini for voice and music
fallback, OpenRouter and Gemini for capture — and how the skills prioritize
Expand Down
1 change: 1 addition & 0 deletions packages/cli/scripts/test-home.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ for (const name of [
"CODEX_HOME",
"CLAUDE_CONFIG_DIR",
"HEYGEN_CONFIG_DIR",
"HYPERFRAMES_CONFIG_DIR",
"HYPERFRAMES_CATALOG_ARTIFACT_DIR",
"HYPERFRAMES_MEDIA_HOME",
"HF_HOME",
Expand Down
11 changes: 9 additions & 2 deletions packages/cli/src/auth/_test-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,16 @@ const ENV_KEYS = [
"HEYGEN_API_KEY",
"HEYGEN_ACCESS_TOKEN",
"HYPERFRAMES_API_KEY",
"HYPERFRAMES_CONFIG_DIR",
"HEYGEN_CONFIG_DIR",
"HEYGEN_API_BASE",
"HEYGEN_API_URL",
"HYPERFRAMES_OAUTH_CLIENT_ID",
"HYPERFRAMES_ENV",
"HYPERFRAMES_OAUTH_TOKEN_URL",
"HYPERFRAMES_OAUTH_AUTHORIZE_URL",
"HYPERFRAMES_OAUTH_REVOKE_URL",
"HYPERFRAMES_OAUTH_DEVICE_URL",
] as const;

type EnvKey = (typeof ENV_KEYS)[number];
Expand All @@ -30,7 +37,7 @@ export interface EnvFixture {

/**
* Take a snapshot of the auth-related env, clear them, make a tmp
* `HEYGEN_CONFIG_DIR`, and return a `restore()` that undoes all of
* `HYPERFRAMES_CONFIG_DIR`, and return a `restore()` that undoes all of
* the above.
*/
export async function setupTempAuthEnv(prefix = "hf-auth-test-"): Promise<EnvFixture> {
Expand All @@ -40,7 +47,7 @@ export async function setupTempAuthEnv(prefix = "hf-auth-test-"): Promise<EnvFix
saved[k] = process.env[k];
delete process.env[k];
}
process.env["HEYGEN_CONFIG_DIR"] = dir;
process.env["HYPERFRAMES_CONFIG_DIR"] = dir;

const restore = async (): Promise<void> => {
for (const k of ENV_KEYS) {
Expand Down
6 changes: 4 additions & 2 deletions packages/cli/src/auth/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,8 @@ import { ErrApi, ErrUnauthenticated, isAuthError } from "./errors.js";
import type { ResolvedCredential } from "./resolver.js";
import { scrubCredentials } from "./scrub.js";
import type { OAuthTokens } from "./store.js";
import { oauthEnvironment } from "./config.js";

const DEFAULT_BASE_URL = "https://api.heygen.com";
export const HEYGEN_CLI_SOURCE_HEADER = "X-HeyGen-Source";
export const HEYGEN_CLI_SOURCE = "cli";
// Tool-attribution header identifying this CLI as the request origin. Sent on
Expand All @@ -32,7 +32,9 @@ export const HEYGEN_CLIENT_SOURCE = "hyperframes";

export function apiBaseUrl(): string {
const override = process.env["HEYGEN_API_URL"];
return override && override.length > 0 ? override.replace(/\/+$/, "") : DEFAULT_BASE_URL;
return override && override.length > 0
? override.replace(/\/+$/, "")
: `https://api.${oauthEnvironment() === "dev" ? "dev." : ""}heygen.com`;
}

export type BillingType = "wallet" | "subscription" | "usage_based" | string;
Expand Down
51 changes: 51 additions & 0 deletions packages/cli/src/auth/config.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import { afterEach, beforeEach, describe, expect, it } from "vitest";
import { setupTempAuthEnv } from "./_test-utils.js";
import {
assertOAuthIdentity,
authorizeEndpoint,
deviceAuthorizationEndpoint,
oauthIdentity,
resolveClientId,
revokeEndpoint,
tokenEndpoint,
} from "./config.js";
import { apiBaseUrl } from "./client.js";

describe("HyperFrames OAuth configuration", () => {
let fixture: Awaited<ReturnType<typeof setupTempAuthEnv>>;
beforeEach(async () => {
fixture = await setupTempAuthEnv();
});
afterEach(async () => {
await fixture.restore();
});

it("uses the dedicated public production client", () => {
expect(resolveClientId()).toBe("Zg3LF4FCLoeJoGs2YqMtZ17g");
expect(authorizeEndpoint()).toBe("https://app.heygen.com/oauth/authorize");
expect(tokenEndpoint()).toBe("https://api2.heygen.com/v1/oauth/token");
expect(apiBaseUrl()).toBe("https://api.heygen.com");
});

it("switches every default host together for development", () => {
process.env["HYPERFRAMES_ENV"] = "dev";
expect(resolveClientId()).toBe("9QoaiwCZov9O0cAkC4OZbR4p");
expect(authorizeEndpoint()).toBe("https://api2.dev.heygen.com/v1/oauth/authorize");
expect(tokenEndpoint()).toBe("https://api2.dev.heygen.com/v1/oauth/token");
expect(revokeEndpoint()).toBe("https://api2.dev.heygen.com/v1/oauth/revoke");
expect(deviceAuthorizationEndpoint()).toBe(
"https://api2.dev.heygen.com/v1/oauth/device_authorization",
);
expect(apiBaseUrl()).toBe("https://api.dev.heygen.com");
});

it("rejects a session after either its client or token server changes", () => {
const identity = oauthIdentity();
expect(() => assertOAuthIdentity(identity)).not.toThrow();
process.env["HYPERFRAMES_OAUTH_CLIENT_ID"] = "another-client";
expect(() => assertOAuthIdentity(identity)).toThrow("different OAuth client or environment");
delete process.env["HYPERFRAMES_OAUTH_CLIENT_ID"];
process.env["HYPERFRAMES_OAUTH_TOKEN_URL"] = "https://other.example/token";
expect(() => assertOAuthIdentity(identity)).toThrow("different OAuth client or environment");
});
});
67 changes: 67 additions & 0 deletions packages/cli/src/auth/config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import { AuthError, ErrOAuthNotConfigured } from "./errors.js";

const PROD_CLIENT_ID = "Zg3LF4FCLoeJoGs2YqMtZ17g";

export function oauthEnvironment(): "prod" | "dev" {
const value = process.env["HYPERFRAMES_ENV"] || "prod";
if (value === "prod" || value === "dev") return value;
throw new AuthError("OAUTH_NOT_CONFIGURED", "HYPERFRAMES_ENV must be prod or dev");
}

export function defaultClientId(): string {
return oauthEnvironment() === "prod" ? PROD_CLIENT_ID : "9QoaiwCZov9O0cAkC4OZbR4p";
}

export function resolveClientId(): string {
const id = process.env["HYPERFRAMES_OAUTH_CLIENT_ID"] || defaultClientId();
if (!id) throw ErrOAuthNotConfigured();
return id;
}

export function tokenEndpoint(): string {
return (
process.env["HYPERFRAMES_OAUTH_TOKEN_URL"] ||
`https://api2.${oauthEnvironment() === "dev" ? "dev." : ""}heygen.com/v1/oauth/token`
);
}

export function authorizeEndpoint(): string {
return (
process.env["HYPERFRAMES_OAUTH_AUTHORIZE_URL"] ||
(oauthEnvironment() === "dev"
? "https://api2.dev.heygen.com/v1/oauth/authorize"
: "https://app.heygen.com/oauth/authorize")
);
}

export function revokeEndpoint(): string {
return (
process.env["HYPERFRAMES_OAUTH_REVOKE_URL"] ||
`https://api2.${oauthEnvironment() === "dev" ? "dev." : ""}heygen.com/v1/oauth/revoke`
);
}

export function deviceAuthorizationEndpoint(): string {
return (
process.env["HYPERFRAMES_OAUTH_DEVICE_URL"] ||
`https://api2.${oauthEnvironment() === "dev" ? "dev." : ""}heygen.com/v1/oauth/device_authorization`
);
}

export function oauthIdentity(): { client_id: string; token_endpoint: string } {
return { client_id: resolveClientId(), token_endpoint: tokenEndpoint() };
}

export function assertOAuthIdentity(tokens: { client_id?: string; token_endpoint?: string }): void {
const identity = oauthIdentity();
if (
tokens.client_id !== identity.client_id ||
tokens.token_endpoint !== identity.token_endpoint
) {
throw new AuthError(
"LOGIN_EXPIRED",
"This login belongs to a different OAuth client or environment",
"Run `hyperframes auth login` to sign into the selected HyperFrames environment.",
);
}
}
2 changes: 1 addition & 1 deletion packages/cli/src/auth/errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ export const ErrInvalidStore = (detail: string) =>
new AuthError(
"INVALID_STORE",
`Credential file is unreadable: ${detail}`,
"Delete ~/.heygen/credentials and run `hyperframes auth login` to re-create it.",
"Delete ~/.hyperframes/credentials and run `hyperframes auth login` to re-create it.",
);

export const ErrUnauthenticated = (detail?: string) =>
Expand Down
Loading
Loading