mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-03 04:38:33 +00:00
feat(media-use): use CLI free HeyGen usage (#2027)
* feat(media-use): use CLI free HeyGen usage * fix(media-use): address #2027 R1 nits — gate cli-source header to OAuth, export origin constant - X-HeyGen-Source is now sent only on OAuth (Bearer) requests, not API-key ones — the backend ignores it for API-key traffic (normal billing), so it was dead metadata there. buildAuthHeaders + heygenAuthHeaders + tests updated. - Export HEYGEN_CLI_ORIGIN_HEADER ("X-HeyGen-Client-Origin") for future cli:<origin> consumers. - Document the deliberate paid/X4 confirm-before-call decision on heygen.tts. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01H5k87mPZ4d6yiFwcWSb8Vv * refactor(cli): drop unused origin-header export, dedup auth-client tests Fallow flagged 5 findings on this PR: - major: HEYGEN_CLI_ORIGIN_HEADER was exported but never emitted or imported — speculative dead code ("future consumers"). Remove it; a real consumer can add the constant when one exists. - 4x minor duplication in client.test.ts: fold the repeated `.rejects.toSatisfy(auth-code)` assertion into expectAuthCode(), and the repeated try/catch scrubbed-message assertion into expectRejectionMessage(). No behavior change; auth/client tests still 17/17. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01H5k87mPZ4d6yiFwcWSb8Vv --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
1614dd3e5a
commit
3b93f516b4
@@ -1,5 +1,11 @@
|
||||
import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
||||
import { AuthClient, apiBaseUrl, buildAuthHeaders } from "./client.js";
|
||||
import {
|
||||
AuthClient,
|
||||
HEYGEN_CLI_SOURCE,
|
||||
HEYGEN_CLI_SOURCE_HEADER,
|
||||
apiBaseUrl,
|
||||
buildAuthHeaders,
|
||||
} from "./client.js";
|
||||
import { isAuthError } from "./errors.js";
|
||||
import type { ResolvedCredential } from "./resolver.js";
|
||||
|
||||
@@ -23,6 +29,27 @@ function makeClient(fetchImpl: typeof fetch): AuthClient {
|
||||
return new AuthClient({ baseUrl: "https://api.test.example", fetchImpl });
|
||||
}
|
||||
|
||||
// getCurrentUser is expected to reject with a specific auth-error code.
|
||||
async function expectAuthCode(promise: Promise<unknown>, code: string): Promise<void> {
|
||||
await expect(promise).rejects.toSatisfy(
|
||||
(err) => isAuthError(err) && (err as { code: string }).code === code,
|
||||
);
|
||||
}
|
||||
|
||||
// getCurrentUser is expected to reject; assertMessage inspects the scrubbed message.
|
||||
async function expectRejectionMessage(
|
||||
client: AuthClient,
|
||||
assertMessage: (msg: string) => void,
|
||||
): Promise<void> {
|
||||
try {
|
||||
await client.getCurrentUser(apiKeyCred());
|
||||
} catch (err) {
|
||||
assertMessage((err as Error).message);
|
||||
return;
|
||||
}
|
||||
throw new Error("expected rejection");
|
||||
}
|
||||
|
||||
describe("auth/client", () => {
|
||||
const original = process.env["HEYGEN_API_URL"];
|
||||
|
||||
@@ -51,11 +78,16 @@ describe("auth/client", () => {
|
||||
source: "file_json",
|
||||
refreshable: false,
|
||||
};
|
||||
expect(buildAuthHeaders(cred)).toEqual({ authorization: "Bearer at_123" });
|
||||
expect(buildAuthHeaders(cred)).toEqual({
|
||||
authorization: "Bearer at_123",
|
||||
[HEYGEN_CLI_SOURCE_HEADER]: HEYGEN_CLI_SOURCE,
|
||||
});
|
||||
});
|
||||
|
||||
it("buildAuthHeaders uses x-api-key for api_key", () => {
|
||||
expect(buildAuthHeaders(apiKeyCred())).toEqual({ "x-api-key": "hg_x" });
|
||||
it("buildAuthHeaders uses x-api-key for api_key, without the cli-source header", () => {
|
||||
expect(buildAuthHeaders(apiKeyCred())).toEqual({
|
||||
"x-api-key": "hg_x",
|
||||
});
|
||||
});
|
||||
|
||||
it("getCurrentUser parses a wrapped {data: {...}} payload", async () => {
|
||||
@@ -94,16 +126,12 @@ describe("auth/client", () => {
|
||||
|
||||
it("getCurrentUser throws ErrUnauthenticated on 401", async () => {
|
||||
const client = makeClient(textFetch("invalid token", 401));
|
||||
await expect(client.getCurrentUser(apiKeyCred())).rejects.toSatisfy((err) => {
|
||||
return isAuthError(err) && (err as { code: string }).code === "UNAUTHENTICATED";
|
||||
});
|
||||
await expectAuthCode(client.getCurrentUser(apiKeyCred()), "UNAUTHENTICATED");
|
||||
});
|
||||
|
||||
it("getCurrentUser throws ErrApi on 5xx", async () => {
|
||||
const client = makeClient(textFetch("upstream", 503));
|
||||
await expect(client.getCurrentUser(apiKeyCred())).rejects.toSatisfy((err) => {
|
||||
return isAuthError(err) && (err as { code: string }).code === "API_ERROR";
|
||||
});
|
||||
await expectAuthCode(client.getCurrentUser(apiKeyCred()), "API_ERROR");
|
||||
});
|
||||
|
||||
it("getCurrentUser throws ErrApi when 2xx body is not valid JSON", async () => {
|
||||
@@ -113,9 +141,7 @@ describe("auth/client", () => {
|
||||
headers: { "content-type": "text/html" },
|
||||
})) as unknown as typeof fetch;
|
||||
const client = makeClient(fetchImpl);
|
||||
await expect(client.getCurrentUser(apiKeyCred())).rejects.toSatisfy((err) => {
|
||||
return isAuthError(err) && (err as { code: string }).code === "API_ERROR";
|
||||
});
|
||||
await expectAuthCode(client.getCurrentUser(apiKeyCred()), "API_ERROR");
|
||||
});
|
||||
|
||||
it("getCurrentUser returns empty UserInfo when payload.data is an array", async () => {
|
||||
@@ -130,15 +156,10 @@ describe("auth/client", () => {
|
||||
401,
|
||||
);
|
||||
const client = makeClient(fetchImpl);
|
||||
try {
|
||||
await client.getCurrentUser(apiKeyCred());
|
||||
} catch (err) {
|
||||
const msg = (err as Error).message;
|
||||
await expectRejectionMessage(client, (msg) => {
|
||||
expect(msg).not.toContain("hg_supersecret_abc123");
|
||||
expect(msg).toContain("<redacted>");
|
||||
return;
|
||||
}
|
||||
throw new Error("expected rejection");
|
||||
});
|
||||
});
|
||||
|
||||
it("getCurrentUser redacts the full Authorization: Bearer value (not just the scheme)", async () => {
|
||||
@@ -147,16 +168,11 @@ describe("auth/client", () => {
|
||||
401,
|
||||
);
|
||||
const client = makeClient(fetchImpl);
|
||||
try {
|
||||
await client.getCurrentUser(apiKeyCred());
|
||||
} catch (err) {
|
||||
const msg = (err as Error).message;
|
||||
await expectRejectionMessage(client, (msg) => {
|
||||
expect(msg).not.toContain("at_opaque_secret_999");
|
||||
expect(msg).not.toContain("Bearer at_opaque_secret_999");
|
||||
expect(msg).toContain("<redacted>");
|
||||
return;
|
||||
}
|
||||
throw new Error("expected rejection");
|
||||
});
|
||||
});
|
||||
|
||||
it("getCurrentUser retries once on 401 when refresh hook is configured for OAuth", async () => {
|
||||
|
||||
@@ -21,6 +21,8 @@ import { scrubCredentials } from "./scrub.js";
|
||||
import type { OAuthTokens } from "./store.js";
|
||||
|
||||
const DEFAULT_BASE_URL = "https://api.heygen.com";
|
||||
export const HEYGEN_CLI_SOURCE_HEADER = "X-HeyGen-Source";
|
||||
export const HEYGEN_CLI_SOURCE = "cli";
|
||||
|
||||
export function apiBaseUrl(): string {
|
||||
const override = process.env["HEYGEN_API_URL"];
|
||||
@@ -177,8 +179,14 @@ export class AuthClient {
|
||||
|
||||
export function buildAuthHeaders(credential: ResolvedCredential): Record<string, string> {
|
||||
if (credential.type === "oauth") {
|
||||
return { authorization: `Bearer ${credential.access_token}` };
|
||||
return {
|
||||
authorization: `Bearer ${credential.access_token}`,
|
||||
[HEYGEN_CLI_SOURCE_HEADER]: HEYGEN_CLI_SOURCE,
|
||||
};
|
||||
}
|
||||
// API-key traffic keeps the normal billing path; the backend ignores the
|
||||
// cli-source header for it, so we don't send it (avoids a contradictory
|
||||
// "cli-source claim on an API-key request").
|
||||
return { "x-api-key": credential.key };
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user