feat(cli): vendor initial hyperframes cloud client codegen (#1109)

* feat(cli): vendor initial hyperframes cloud client codegen

Generated by experiment-framework/scripts/generate_hyperframes_cli_client.py
(see heygen-com/experiment-framework#37896). Sets up the baseline for the
sync workflow to diff against on future spec changes.

The follow-up PR adds the orchestration layer (zip + upload + poll +
download) and the user-facing 'hyperframes cloud render/list/get/delete'
commands on top of this generated client.

The fallow ignore pattern is necessary because the generated request()
method is intentionally a single switch that handles all 5 endpoints
in one place; refactoring it here would just be re-introduced on the
next codegen run.

* chore(cli): regenerate cloud client with mimeType parameter on multipart uploads

Adds optional mimeType arg to uploadAsset (and any future multipart
endpoints). Without it, FormData sends application/octet-stream which
is correct for the documented media surface (png/jpeg/mp4/etc.) but
ambiguous for the private-beta zip uploads the cloud render flow uses.
Callers that pass `mimeType: "application/zip"` tag the multipart
part with the right Content-Type so downstream proxies, WAFs, and any
future server-side change that keys off the part MIME (instead of the
current magic-byte detection) all see the intended type.

Addresses review feedback on heygen-com/experiment-framework#37896.
Generated by scripts/generate_hyperframes_cli_client.py with the
matching update to the multipart emit path.
This commit is contained in:
James Russo
2026-05-28 13:36:58 -04:00
committed by GitHub
parent d625dc8509
commit e9f45b7c33
3 changed files with 568 additions and 0 deletions
+305
View File
@@ -0,0 +1,305 @@
/**
* AUTO-GENERATED from experiment-framework/openapi/external-api.json.
* DO NOT EDIT MANUALLY. Re-run
* `python scripts/generate_hyperframes_cli_client.py` in
* experiment-framework to regenerate.
*/
import type {
CreateHyperframesRenderRequest,
CreateHyperframesRenderResponse,
DeleteHyperframesRenderResponse,
HyperframesRenderDetail,
UploadAssetV3Response,
} from "./types.js";
export type AuthHeaders = Record<string, string>;
/**
* Caller-provided context. Keep the shape narrow so the cli/src/auth/
* module stays the single owner of credential resolution.
*/
export interface HyperframesCloudClientOptions {
/** Base URL like "https://api.heygen.com" (no trailing slash). */
baseUrl: string;
/**
* Return the auth headers to attach to every request. Called once per
* request so callers can refresh OAuth tokens transparently.
*/
getAuthHeaders: () => Promise<AuthHeaders> | AuthHeaders;
/** Override fetch (used by tests). */
fetchImpl?: typeof fetch;
}
/**
* Standard error envelope used by /v3 endpoints. See StandardAPIError in
* types.ts for the field shape.
*/
export class HyperframesApiError extends Error {
readonly status: number;
readonly code?: string;
readonly param?: string | null;
readonly docUrl?: string | null;
readonly raw: unknown;
constructor(opts: {
status: number;
message: string;
code?: string;
param?: string | null;
docUrl?: string | null;
raw?: unknown;
}) {
super(opts.message);
this.name = "HyperframesApiError";
this.status = opts.status;
this.code = opts.code;
this.param = opts.param;
this.docUrl = opts.docUrl;
this.raw = opts.raw;
}
}
interface RequestOptions {
method: string;
path: string;
query?: Record<string, string | number | undefined>;
body?: unknown;
multipart?: FormData;
idempotencyKey?: string;
signal?: AbortSignal;
/**
* When true (default), the ``data`` wrapper of the standard /v3
* response envelope is unwrapped before returning. List endpoints set
* this to false so callers can read ``has_more`` / ``next_token``.
*/
unwrapData?: boolean;
}
/**
* Typed client for the HyperFrames cloud-render API. Auto-generated; do
* not hand-edit. Submit new endpoints by adding them to
* scripts/generate_hyperframes_cli_client.py in experiment-framework.
*/
export class HyperframesCloudClient {
private readonly baseUrl: string;
private readonly getAuthHeaders: () => Promise<AuthHeaders> | AuthHeaders;
private readonly fetchImpl: typeof fetch;
constructor(opts: HyperframesCloudClientOptions) {
this.baseUrl = opts.baseUrl.replace(/\/+$/, "");
this.getAuthHeaders = opts.getAuthHeaders;
this.fetchImpl = opts.fetchImpl ?? fetch;
}
private async request<T>(opts: RequestOptions): Promise<T> {
const url = this.buildUrl(opts.path, opts.query);
const auth = await this.getAuthHeaders();
const headers: Record<string, string> = { ...auth };
let body: BodyInit | undefined;
if (opts.multipart) {
body = opts.multipart;
// fetch sets the multipart boundary automatically; do NOT set
// Content-Type here or the upload will be rejected.
} else if (opts.body !== undefined) {
headers["content-type"] = "application/json";
body = JSON.stringify(opts.body);
}
if (opts.idempotencyKey) {
headers["Idempotency-Key"] = opts.idempotencyKey;
}
const res = await this.fetchImpl(url, {
method: opts.method,
headers,
body,
signal: opts.signal,
});
if (!res.ok) {
throw await this.toApiError(res);
}
// 204 No Content
if (res.status === 204) {
return undefined as T;
}
const text = await res.text();
if (!text) {
return undefined as T;
}
let parsed: unknown;
try {
parsed = JSON.parse(text);
} catch (err) {
throw new HyperframesApiError({
status: res.status,
message: `Invalid JSON response: ${(err as Error).message}`,
raw: text.slice(0, 500),
});
}
// The /v3 envelope is {data: T, ...}. Unwrap when present and the
// call site asked for it (the default) so consumers read the inner
// payload directly. List endpoints opt out so they can read
// ``has_more`` / ``next_token``.
const unwrap = opts.unwrapData !== false;
if (
unwrap &&
parsed &&
typeof parsed === "object" &&
"data" in (parsed as Record<string, unknown>)
) {
const envelope = parsed as { data: T };
return envelope.data;
}
return parsed as T;
}
private buildUrl(path: string, query?: Record<string, string | number | undefined>): string {
const url = new URL(this.baseUrl + path);
if (query) {
for (const [k, v] of Object.entries(query)) {
if (v === undefined) continue;
url.searchParams.set(k, String(v));
}
}
return url.toString();
}
private async toApiError(res: Response): Promise<HyperframesApiError> {
let parsed: unknown;
try {
parsed = await res.json();
} catch {
parsed = undefined;
}
const err =
parsed && typeof parsed === "object" && "error" in (parsed as Record<string, unknown>)
? ((parsed as Record<string, unknown>).error as Record<string, unknown> | undefined)
: undefined;
return new HyperframesApiError({
status: res.status,
message:
(err && typeof err.message === "string" && err.message) ||
`HTTP ${res.status} ${res.statusText}`,
code: err && typeof err.code === "string" ? err.code : undefined,
param:
err && (typeof err.param === "string" || err.param === null)
? (err.param as string | null)
: undefined,
docUrl:
err && (typeof err.doc_url === "string" || err.doc_url === null)
? (err.doc_url as string | null)
: undefined,
raw: parsed,
});
}
/**
* Upload Asset
*
* Uploads a file (image, video, audio, or PDF) and returns an asset_id for use in other endpoints. Max 32 MB. Supported types: png, jpeg, mp4, webm, mp3, wav, pdf.
*/
async uploadAsset(args: {
file: Blob | Buffer | Uint8Array;
filename: string;
mimeType?: string;
idempotencyKey?: string;
signal?: AbortSignal;
}): Promise<UploadAssetV3Response> {
const fd = new FormData();
const blobOpts = args.mimeType ? { type: args.mimeType } : undefined;
const blob =
args.file instanceof Blob
? args.file
: new Blob([args.file as unknown as BlobPart], blobOpts);
fd.append("file", blob, args.filename);
return await this.request<UploadAssetV3Response>({
method: "POST",
path: "/v3/assets",
multipart: fd,
idempotencyKey: args.idempotencyKey,
signal: args.signal,
});
}
/**
* Create HyperFrames Render
*
* Renders a HyperFrames composition (an HTML+JS+assets project bundled as a .zip) into a video. Submit the project via `url`, `asset_id` (pre-uploaded via POST /v3/assets), or inline `base64`. Returns a `render_id` to poll via GET /v3/hyperframes/renders/{render_id}.
*/
async createRender(args: {
body: CreateHyperframesRenderRequest;
idempotencyKey?: string;
signal?: AbortSignal;
}): Promise<CreateHyperframesRenderResponse> {
return await this.request<CreateHyperframesRenderResponse>({
method: "POST",
path: "/v3/hyperframes/renders",
body: args.body,
idempotencyKey: args.idempotencyKey,
signal: args.signal,
});
}
/**
* List HyperFrames Renders
*
* Returns a cursor-paginated list of HyperFrames renders in the account, newest first.
*/
async listRenders(args: { limit?: number; token?: string; signal?: AbortSignal }): Promise<{
data?: Array<HyperframesRenderDetail>;
has_more?: boolean;
next_token?: string | null;
}> {
const query: Record<string, string | number | undefined> = {
limit: args.limit,
token: args.token,
};
return await this.request<{
data?: Array<HyperframesRenderDetail>;
has_more?: boolean;
next_token?: string | null;
}>({
method: "GET",
path: "/v3/hyperframes/renders",
query,
unwrapData: false,
signal: args.signal,
});
}
/**
* Get HyperFrames Render
*
* Returns full details for a single HyperFrames render, including status and signed video_url when complete.
*/
async getRender(args: {
render_id: string;
signal?: AbortSignal;
}): Promise<HyperframesRenderDetail> {
return await this.request<HyperframesRenderDetail>({
method: "GET",
path: `/v3/hyperframes/renders/${encodeURIComponent(args.render_id)}`,
signal: args.signal,
});
}
/**
* Delete HyperFrames Render
*
* Soft-deletes a HyperFrames render. Subsequent GETs return 404.
*/
async deleteRender(args: {
render_id: string;
signal?: AbortSignal;
}): Promise<DeleteHyperframesRenderResponse> {
return await this.request<DeleteHyperframesRenderResponse>({
method: "DELETE",
path: `/v3/hyperframes/renders/${encodeURIComponent(args.render_id)}`,
signal: args.signal,
});
}
}
+258
View File
@@ -0,0 +1,258 @@
/**
* AUTO-GENERATED from experiment-framework/openapi/external-api.json.
* DO NOT EDIT MANUALLY. Re-run
* `python scripts/generate_hyperframes_cli_client.py` in
* experiment-framework to regenerate.
*/
// Component schemas reachable from the cloud-render endpoint set.
// Add a new path/method to TARGET_ENDPOINTS in
// scripts/generate_hyperframes_cli_client.py to extend this list.
/**
* Asset input via base64-encoded content.
*/
export interface AssetBase64 {
/**
* Input type discriminator
*/
type: "base64";
/**
* MIME type of the encoded content (e.g. "image/png")
*/
media_type: string;
/**
* Base64-encoded file content
*/
data: string;
}
/**
* Asset input via HeyGen asset ID from the asset upload endpoint.
*/
export interface AssetId {
/**
* Input type discriminator
*/
type: "asset_id";
/**
* HeyGen asset ID from the asset upload endpoint
*/
asset_id: string;
}
/**
* Asset input via publicly accessible HTTPS URL.
*/
export interface AssetUrl {
/**
* Input type discriminator
*/
type: "url";
/**
* Publicly accessible HTTPS URL for the asset
*/
url: string;
}
/**
* Request body for POST /v3/hyperframes/renders.
*/
export interface CreateHyperframesRenderRequest {
/**
* HyperFrames composition .zip — provide as {type: 'url', url: '...'}, {type:
* 'asset_id', asset_id: '...'} (pre-uploaded via POST /v3/assets), or {type:
* 'base64', media_type: 'application/zip', data: '...'}. Zip must contain
* index.html at the root (or the path you set in `composition`).
*/
project: AssetUrl | AssetId | AssetBase64;
/**
* Output frames per second. Defaults to 30 if not provided.
*/
fps?: number | null;
/**
* Render quality preset; higher quality is slower.
*/
quality?: "draft" | "standard" | "high";
/**
* Output container/codec.
*/
format?: "mp4" | "webm" | "mov";
/**
* Optional resolution preset. If omitted, the composition's own declared
* dimensions are used.
*/
resolution?:
| "landscape"
| "portrait"
| "landscape-4k"
| "portrait-4k"
| "square"
| "square-4k"
| null;
/**
* Entry HTML file relative to the project root (e.g. compositions/intro.html).
* Defaults to index.html when omitted.
*/
composition?: string | null;
/**
* Optional overrides for the composition's data-composition-variables. Use
* this to parameterise a single composition across multiple renders.
*/
variables?: Record<string, unknown> | null;
/**
* Free-text label for the render; echoed back in detail responses.
*/
title?: string | null;
/**
* Opaque client tracking ID, echoed back in webhook payloads.
*/
callback_id?: string | null;
/**
* Per-request HTTPS webhook URL the render fires when it terminates.
*/
callback_url?: string | null;
}
/**
* Response for POST /v3/hyperframes/renders.
*/
export interface CreateHyperframesRenderResponse {
/**
* HyperFrames render identifier — poll GET /v3/hyperframes/renders/{render_id}
* for status.
*/
render_id: string;
}
/**
* Response for DELETE /v3/hyperframes/renders/{render_id}.
*/
export interface DeleteHyperframesRenderResponse {
/**
* ID of the deleted render.
*/
render_id: string;
}
/**
* Detailed HyperFrames render resource.
*/
export interface HyperframesRenderDetail {
/**
* Unique render identifier.
*/
render_id: string;
/**
* Current lifecycle state.
*/
status: HyperframesRenderStatus;
/**
* Caller-supplied free-text label.
*/
title?: string | null;
/**
* Caller-supplied client tracking ID.
*/
callback_id?: string | null;
/**
* Presigned download URL for the rendered video. Present only when status is
* 'completed'.
*/
video_url?: string | null;
/**
* Presigned download URL for the auto-generated thumbnail.
*/
thumbnail_url?: string | null;
/**
* Video duration in seconds; null until completed.
*/
duration?: number | null;
/**
* Frames per second the render was created at.
*/
fps?: number | null;
/**
* Render quality preset.
*/
quality?: "draft" | "standard" | "high" | null;
/**
* Output container/codec.
*/
format: "mp4" | "webm" | "mov";
/**
* Resolution preset, if one was set.
*/
resolution?:
| "landscape"
| "portrait"
| "landscape-4k"
| "portrait-4k"
| "square"
| "square-4k"
| null;
/**
* Composition entry file path.
*/
composition?: string | null;
/**
* Unix timestamp when the render was created.
*/
created_at?: number | null;
/**
* Unix timestamp when the render terminated. Null until status is 'completed'
* or 'failed'.
*/
completed_at?: number | null;
/**
* Error description. Present only when status is 'failed'.
*/
failure_message?: string | null;
}
/**
* Lifecycle status of a HyperFrames render.
*/
export type HyperframesRenderStatus = "queued" | "rendering" | "completed" | "failed";
export interface StandardAPIError {
/**
* Machine-readable error code
*/
code: string;
/**
* Human-readable error message
*/
message: string;
/**
* Which request field caused the error
*/
param?: string | null;
/**
* Link to error documentation
*/
doc_url?: string | null;
}
/**
* Response from uploading an asset via POST /v3/assets.
*/
export interface UploadAssetV3Response {
/**
* Unique asset identifier for use in other endpoints like POST
* /v3/video-agents
*/
asset_id: string;
/**
* Public URL of the uploaded asset
*/
url: string;
/**
* Detected MIME type of the file
*/
mime_type: string;
/**
* File size in bytes
*/
size_bytes: number;
}