fix(deepgram): use correct /v1/projects/{project_id}/keys endpoint

- Replace non-existent /v1/auth/grant with /v1/projects/{project_id}/keys
- Add DEEPGRAM_PROJECT_ID env variable
- Update request body and response parsing
- Update tests

Typecheck: OK · Tests: 241/241 

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Hermann_Kitio 2026-04-25 05:36:19 +03:00
parent 7cac057062
commit a62b4816a2
3 changed files with 57 additions and 32 deletions

View file

@ -6,6 +6,7 @@ SUPABASE_SERVICE_ROLE_KEY=xxx
DEEPSEEK_API_KEY=xxx DEEPSEEK_API_KEY=xxx
GEMINI_API_KEY=xxx GEMINI_API_KEY=xxx
DEEPGRAM_API_KEY=xxx DEEPGRAM_API_KEY=xxx
DEEPGRAM_PROJECT_ID=xxx
# Stripe # Stripe
STRIPE_SECRET_KEY=xxx STRIPE_SECRET_KEY=xxx

View file

@ -1,12 +1,16 @@
/** /**
* Client Deepgram Sprint 4b. * Client Deepgram Sprint 4b (corrigé Sprint 4b.1).
* *
* Génère un token éphémère que le frontend utilise pour ouvrir une connexion * Génère une clé API éphémère que le frontend utilise pour ouvrir une connexion
* directe à Deepgram (transcription live). Le token a une durée de vie courte * directe à Deepgram (transcription live). La clé temporaire est créée comme
* (par défaut 600 s) et n'expose pas la clé maître `DEEPGRAM_API_KEY`. * sub-key d'un projet existant la clé maître `DEEPGRAM_API_KEY` reste côté
* serveur et n'est jamais exposée au navigateur.
* *
* Endpoint : POST https://api.deepgram.com/v1/auth/grant * Endpoint : POST https://api.deepgram.com/v1/projects/{project_id}/keys
* Doc : https://developers.deepgram.com/docs/create-temporary-api-key * Doc : https://developers.deepgram.com/reference/create-key
*
* Le scope `usage:write` permet d'ouvrir une session live (POST /listen ou
* WebSocket) sans donner accès aux endpoints de management du projet.
*/ */
const DEEPGRAM_API_KEY = process.env.DEEPGRAM_API_KEY ?? ""; const DEEPGRAM_API_KEY = process.env.DEEPGRAM_API_KEY ?? "";
@ -18,18 +22,35 @@ export interface DeepgramToken {
expires_in: number; expires_in: number;
} }
function getProjectId(): string {
const id = process.env.DEEPGRAM_PROJECT_ID ?? "";
if (!id) {
throw new Error("DEEPGRAM_PROJECT_ID is not set");
}
return id;
}
export async function createTemporaryToken( export async function createTemporaryToken(
ttlSeconds: number, ttlSeconds: number,
): Promise<DeepgramToken> { ): Promise<DeepgramToken> {
const response = await fetch(`${DEEPGRAM_BASE_URL}/v1/auth/grant`, { const projectId = getProjectId();
method: "POST",
headers: { const response = await fetch(
"Content-Type": "application/json", `${DEEPGRAM_BASE_URL}/v1/projects/${projectId}/keys`,
Authorization: `Token ${DEEPGRAM_API_KEY}`, {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Token ${DEEPGRAM_API_KEY}`,
},
body: JSON.stringify({
comment: "expria-temp",
scopes: ["usage:write"],
time_to_live_in_seconds: ttlSeconds,
}),
signal: AbortSignal.timeout(DEEPGRAM_TIMEOUT_MS),
}, },
body: JSON.stringify({ ttl_seconds: ttlSeconds }), );
signal: AbortSignal.timeout(DEEPGRAM_TIMEOUT_MS),
});
if (!response.ok) { if (!response.ok) {
throw new Error( throw new Error(
@ -37,17 +58,13 @@ export async function createTemporaryToken(
); );
} }
const data = (await response.json()) as { const data = (await response.json()) as { key?: string };
access_token?: string;
expires_in?: number;
};
if (typeof data.access_token !== "string" || data.access_token.length === 0) { if (typeof data.key !== "string" || data.key.length === 0) {
throw new Error("Deepgram API: access_token manquant dans la réponse"); throw new Error("Deepgram API: key manquant dans la réponse");
} }
const expiresIn = // L'API ne renvoie pas la TTL — on retourne celle qui a été demandée,
typeof data.expires_in === "number" ? data.expires_in : ttlSeconds; // qui matche ce que l'API a appliqué (champ `time_to_live_in_seconds`).
return { token: data.key, expires_in: ttlSeconds };
return { token: data.access_token, expires_in: expiresIn };
} }

View file

@ -33,6 +33,7 @@ const JSON_HEADERS = {
describe("POST /transcriptions/token — Sprint 4b", () => { describe("POST /transcriptions/token — Sprint 4b", () => {
beforeEach(() => { beforeEach(() => {
vi.restoreAllMocks(); vi.restoreAllMocks();
process.env.DEEPGRAM_PROJECT_ID = "proj-test-123";
}); });
it("401 sans Authorization", async () => { it("401 sans Authorization", async () => {
@ -47,8 +48,8 @@ describe("POST /transcriptions/token — Sprint 4b", () => {
vi.fn().mockResolvedValue({ vi.fn().mockResolvedValue({
ok: true, ok: true,
json: async () => ({ json: async () => ({
access_token: "dg-temp-abc123", key: "dg-temp-abc123",
expires_in: 600, api_key_id: "id-1",
}), }),
}), }),
); );
@ -65,10 +66,10 @@ describe("POST /transcriptions/token — Sprint 4b", () => {
expect(body.expires_in).toBe(600); expect(body.expires_in).toBe(600);
}); });
it("appelle Deepgram avec ttl_seconds=600 et Authorization Token <key>", async () => { it("appelle POST /v1/projects/{id}/keys avec Authorization Token <key> et le bon body", async () => {
const fetchMock = vi.fn().mockResolvedValue({ const fetchMock = vi.fn().mockResolvedValue({
ok: true, ok: true,
json: async () => ({ access_token: "tok", expires_in: 600 }), json: async () => ({ key: "tok" }),
}); });
vi.stubGlobal("fetch", fetchMock); vi.stubGlobal("fetch", fetchMock);
@ -80,10 +81,16 @@ describe("POST /transcriptions/token — Sprint 4b", () => {
expect(fetchMock).toHaveBeenCalledTimes(1); expect(fetchMock).toHaveBeenCalledTimes(1);
const [url, init] = fetchMock.mock.calls[0]; const [url, init] = fetchMock.mock.calls[0];
expect(String(url)).toContain("/v1/auth/grant"); expect(String(url)).toBe(
"https://api.deepgram.com/v1/projects/proj-test-123/keys",
);
const headers = (init?.headers ?? {}) as Record<string, string>; const headers = (init?.headers ?? {}) as Record<string, string>;
expect(headers["Authorization"]).toMatch(/^Token /); expect(headers["Authorization"]).toMatch(/^Token /);
expect(JSON.parse(init.body as string)).toEqual({ ttl_seconds: 600 }); expect(JSON.parse(init.body as string)).toEqual({
comment: "expria-temp",
scopes: ["usage:write"],
time_to_live_in_seconds: 600,
});
}); });
it("500 INTERNAL_ERROR si Deepgram non-OK", async () => { it("500 INTERNAL_ERROR si Deepgram non-OK", async () => {
@ -122,12 +129,12 @@ describe("POST /transcriptions/token — Sprint 4b", () => {
expect(res.status).toBe(500); expect(res.status).toBe(500);
}); });
it("500 INTERNAL_ERROR si access_token absent dans la réponse Deepgram", async () => { it("500 INTERNAL_ERROR si key absent dans la réponse Deepgram", async () => {
vi.stubGlobal( vi.stubGlobal(
"fetch", "fetch",
vi.fn().mockResolvedValue({ vi.fn().mockResolvedValue({
ok: true, ok: true,
json: async () => ({ expires_in: 600 }), json: async () => ({ api_key_id: "id-1" }),
}), }),
); );