Skip to content
Open
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
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ All services start with sensible defaults. No config file needed:
- **Apple** on `http://localhost:4004`
- **Microsoft** on `http://localhost:4005`
- **AWS** on `http://localhost:4006`
- **HeyGen** on `http://localhost:4007`

## CLI

Expand Down Expand Up @@ -678,3 +679,5 @@ Tokens are configured in the seed config and map to users. Pass them as `Authori
**Microsoft**: OIDC authorization code flow with PKCE support. Also supports client credentials grants. Microsoft Graph `/v1.0/me` available.

**AWS**: Bearer tokens or IAM access key credentials. Default key pair always seeded: `AKIAIOSFODNN7EXAMPLE` / `wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY`.

**HeyGen**: OAuth 2.0 authorization code flow with mandatory PKCE. Separate token (`/v1/oauth/token`) and refresh (`/v1/oauth/refresh_token`) endpoints. User profile at `/v1/user/me` returns HeyGen's `{ code, data, message }` wrapper.
7 changes: 7 additions & 0 deletions apps/web/app/heygen/layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { pageMetadata } from "@/lib/page-metadata";

export const metadata = pageMetadata("heygen");

export default function Layout({ children }: { children: React.ReactNode }) {
return children;
}
65 changes: 65 additions & 0 deletions apps/web/app/heygen/page.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
# HeyGen API

OAuth 2.0 authorization code flow with PKCE for local HeyGen auth testing.

## Routes

- `GET /oauth/authorize` - authorization endpoint (renders user-picker UI)
- `POST /oauth/authorize/callback` - form submission; issues code and redirects
- `POST /v1/oauth/token` - token exchange (authorization code grant with PKCE)
- `POST /v1/oauth/refresh_token` - refresh token grant
- `GET /v1/user/me` - get user profile

## Start

```bash
npx emulate --service heygen
# http://localhost:4007
```

## Environment Variables

Point your app at the emulator by overriding these variables:

```bash
HEYGEN_OAUTH_AUTHORIZE_URL=http://localhost:4007/oauth/authorize
HEYGEN_OAUTH_TOKEN_URL=http://localhost:4007/v1/oauth/token
HEYGEN_OAUTH_REFRESH_URL=http://localhost:4007/v1/oauth/refresh_token
HEYGEN_USER_ME_URL=http://localhost:4007/v1/user/me
HEYGEN_OAUTH_CLIENT_ID=dev_client_id
```

## User Response

The `GET /v1/user/me` response matches HeyGen's production shape:

```json
{
"code": 100,
"data": {
"user": {
"user_id": "heygen_a1b2c3d4",
"email": "testuser@heygen.com",
"username": "Test User",
"email_verified": true
}
},
"message": "Success"
}
```

## Seed Config

```yaml
heygen:
users:
- email: alice@example.com
name: Alice
picture: https://example.com/alice.jpg
oauth_clients:
- client_id: my_client_id
client_secret: my_client_secret
name: My App (dev)
redirect_uris:
- http://localhost:3000/api/auth/callback
```
1 change: 1 addition & 0 deletions apps/web/components/docs-mobile-nav.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ const nav = [
{ href: "/apple", label: "Apple Sign In" },
{ href: "/microsoft", label: "Microsoft Entra ID" },
{ href: "/aws", label: "AWS" },
{ href: "/heygen", label: "HeyGen API" },
{ href: "/authentication", label: "Authentication" },
{ href: "/architecture", label: "Architecture" },
];
Expand Down
1 change: 1 addition & 0 deletions apps/web/components/docs-nav.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ const nav = [
{ href: "/apple", label: "Apple Sign In" },
{ href: "/microsoft", label: "Microsoft Entra ID" },
{ href: "/aws", label: "AWS" },
{ href: "/heygen", label: "HeyGen API" },
{ href: "/authentication", label: "Authentication" },
{ href: "/architecture", label: "Architecture" },
];
Expand Down
1 change: 1 addition & 0 deletions apps/web/lib/docs-navigation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ export const allDocsPages: NavItem[] = [
{ name: "Apple Sign In", href: "/apple" },
{ name: "Microsoft Entra ID", href: "/microsoft" },
{ name: "AWS", href: "/aws" },
{ name: "HeyGen API", href: "/heygen" },
{ name: "Authentication", href: "/authentication" },
{ name: "Architecture", href: "/architecture" },
];
1 change: 1 addition & 0 deletions apps/web/lib/page-titles.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ export const PAGE_TITLES: Record<string, string> = {
apple: "Apple Sign In",
microsoft: "Microsoft Entra ID",
aws: "AWS",
heygen: "HeyGen API",
authentication: "Authentication",
architecture: "Architecture",
};
Expand Down
44 changes: 44 additions & 0 deletions packages/@emulators/heygen/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
{
"name": "@emulators/heygen",
"version": "0.1.0",
"license": "Apache-2.0",
"type": "module",
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
"exports": {
".": {
"import": "./dist/index.js",
"types": "./dist/index.d.ts"
}
},
"homepage": "https://emulate.dev",
"repository": {
"type": "git",
"url": "https://github.com/vercel-labs/emulate.git",
"directory": "packages/@emulators/heygen"
},
"bugs": {
"url": "https://github.com/vercel-labs/emulate/issues"
},
"publishConfig": {
"access": "public"
},
"files": [
"dist"
],
"scripts": {
"build": "tsup --clean",
"dev": "tsup --watch",
"test": "vitest run",
"clean": "rm -rf dist .turbo"
},
"dependencies": {
"@emulators/core": "workspace:*",
"hono": "^4"
},
"devDependencies": {
"tsup": "^8",
"typescript": "^5.7",
"vitest": "^4.1.0"
}
}
15 changes: 15 additions & 0 deletions packages/@emulators/heygen/src/entities.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import type { Entity } from "@emulators/core";

export interface HeyGenUser extends Entity {
user_id: string;
email: string;
name: string;
picture: string | null;
}

export interface HeyGenOAuthClient extends Entity {
client_id: string;
client_secret: string;
name: string;
redirect_uris: string[];
}
104 changes: 104 additions & 0 deletions packages/@emulators/heygen/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
import { randomBytes } from "crypto";
import type {
AppEnv,
RouteContext,
ServicePlugin,
Store,
TokenMap,
WebhookDispatcher,
} from "@emulators/core";
import type { Hono } from "hono";
import { oauthRoutes } from "./routes/oauth.js";
import { getHeyGenStore } from "./store.js";

export { getHeyGenStore, type HeyGenStore } from "./store.js";
export * from "./entities.js";

export interface HeyGenSeedUser {
email: string;
name?: string;
picture?: string;
}

export interface HeyGenSeedConfig {
port?: number;
users?: HeyGenSeedUser[];
oauth_clients?: Array<{
client_id: string;
client_secret: string;
name?: string;
redirect_uris: string[];
}>;
}

function generateUserId(): string {
return "heygen_" + randomBytes(8).toString("hex");
}

function seedDefaults(store: Store): void {
const hs = getHeyGenStore(store);
const defaultEmail = "testuser@heygen.com";

if (!hs.users.findOneBy("email", defaultEmail)) {
hs.users.insert({
user_id: generateUserId(),
email: defaultEmail,
name: "Test User",
picture: null,
});
}
}

export function seedFromConfig(store: Store, _baseUrl: string, config: HeyGenSeedConfig): void {
const hs = getHeyGenStore(store);

if (config.users) {
for (const user of config.users) {
const existing = hs.users.findOneBy("email", user.email);
if (!existing) {
hs.users.insert({
user_id: generateUserId(),
email: user.email,
name: user.name ?? user.email.split("@")[0],
picture: user.picture ?? null,
});
}
}
}

if (config.oauth_clients) {
for (const client of config.oauth_clients) {
const existing = hs.oauthClients.findOneBy("client_id", client.client_id);
if (existing) continue;
hs.oauthClients.insert({
client_id: client.client_id,
client_secret: client.client_secret,
name: client.name ?? "App (HeyGen)",
redirect_uris: client.redirect_uris,
});
}
}

if (!config.users || config.users.length === 0) {
seedDefaults(store);
}
}

export const heygenPlugin: ServicePlugin = {
name: "heygen",
register(
app: Hono<AppEnv>,
store: Store,
webhooks: WebhookDispatcher,
baseUrl: string,
tokenMap?: TokenMap,
): void {
const ctx: RouteContext = { app, store, webhooks, baseUrl, tokenMap };
oauthRoutes(ctx);
},
seed(store: Store): void {
seedDefaults(store);
},
};

export default heygenPlugin;
Loading