From a62b4816a2dae0afdecc1cc032f8115955657da8 Mon Sep 17 00:00:00 2001 From: Hermann_Kitio Date: Sat, 25 Apr 2026 05:36:19 +0300 Subject: [PATCH] fix(deepgram): use correct /v1/projects/{project_id}/keys endpoint MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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) --- .env.example | 1 + src/lib/deepgram.ts | 65 ++++++++++++------- .../__tests__/transcriptionsToken.test.ts | 23 ++++--- 3 files changed, 57 insertions(+), 32 deletions(-) diff --git a/.env.example b/.env.example index a59443e..d38a90c 100644 --- a/.env.example +++ b/.env.example @@ -6,6 +6,7 @@ SUPABASE_SERVICE_ROLE_KEY=xxx DEEPSEEK_API_KEY=xxx GEMINI_API_KEY=xxx DEEPGRAM_API_KEY=xxx +DEEPGRAM_PROJECT_ID=xxx # Stripe STRIPE_SECRET_KEY=xxx diff --git a/src/lib/deepgram.ts b/src/lib/deepgram.ts index be6fb34..e8b02b2 100644 --- a/src/lib/deepgram.ts +++ b/src/lib/deepgram.ts @@ -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 - * directe à Deepgram (transcription live). Le token a une durée de vie courte - * (par défaut 600 s) et n'expose pas la clé maître `DEEPGRAM_API_KEY`. + * Génère une clé API éphémère que le frontend utilise pour ouvrir une connexion + * directe à Deepgram (transcription live). La clé temporaire est créée comme + * 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 - * Doc : https://developers.deepgram.com/docs/create-temporary-api-key + * Endpoint : POST https://api.deepgram.com/v1/projects/{project_id}/keys + * 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 ?? ""; @@ -18,18 +22,35 @@ export interface DeepgramToken { 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( ttlSeconds: number, ): Promise { - const response = await fetch(`${DEEPGRAM_BASE_URL}/v1/auth/grant`, { - method: "POST", - headers: { - "Content-Type": "application/json", - Authorization: `Token ${DEEPGRAM_API_KEY}`, + const projectId = getProjectId(); + + const response = await fetch( + `${DEEPGRAM_BASE_URL}/v1/projects/${projectId}/keys`, + { + 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) { throw new Error( @@ -37,17 +58,13 @@ export async function createTemporaryToken( ); } - const data = (await response.json()) as { - access_token?: string; - expires_in?: number; - }; + const data = (await response.json()) as { key?: string }; - if (typeof data.access_token !== "string" || data.access_token.length === 0) { - throw new Error("Deepgram API: access_token manquant dans la réponse"); + if (typeof data.key !== "string" || data.key.length === 0) { + throw new Error("Deepgram API: key manquant dans la réponse"); } - const expiresIn = - typeof data.expires_in === "number" ? data.expires_in : ttlSeconds; - - return { token: data.access_token, expires_in: expiresIn }; + // L'API ne renvoie pas la TTL — on retourne celle qui a été demandée, + // qui matche ce que l'API a appliqué (champ `time_to_live_in_seconds`). + return { token: data.key, expires_in: ttlSeconds }; } diff --git a/src/routes/__tests__/transcriptionsToken.test.ts b/src/routes/__tests__/transcriptionsToken.test.ts index 5b511c7..3e5be9a 100644 --- a/src/routes/__tests__/transcriptionsToken.test.ts +++ b/src/routes/__tests__/transcriptionsToken.test.ts @@ -33,6 +33,7 @@ const JSON_HEADERS = { describe("POST /transcriptions/token — Sprint 4b", () => { beforeEach(() => { vi.restoreAllMocks(); + process.env.DEEPGRAM_PROJECT_ID = "proj-test-123"; }); it("401 sans Authorization", async () => { @@ -47,8 +48,8 @@ describe("POST /transcriptions/token — Sprint 4b", () => { vi.fn().mockResolvedValue({ ok: true, json: async () => ({ - access_token: "dg-temp-abc123", - expires_in: 600, + key: "dg-temp-abc123", + api_key_id: "id-1", }), }), ); @@ -65,10 +66,10 @@ describe("POST /transcriptions/token — Sprint 4b", () => { expect(body.expires_in).toBe(600); }); - it("appelle Deepgram avec ttl_seconds=600 et Authorization Token ", async () => { + it("appelle POST /v1/projects/{id}/keys avec Authorization Token et le bon body", async () => { const fetchMock = vi.fn().mockResolvedValue({ ok: true, - json: async () => ({ access_token: "tok", expires_in: 600 }), + json: async () => ({ key: "tok" }), }); vi.stubGlobal("fetch", fetchMock); @@ -80,10 +81,16 @@ describe("POST /transcriptions/token — Sprint 4b", () => { expect(fetchMock).toHaveBeenCalledTimes(1); 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; 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 () => { @@ -122,12 +129,12 @@ describe("POST /transcriptions/token — Sprint 4b", () => { 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( "fetch", vi.fn().mockResolvedValue({ ok: true, - json: async () => ({ expires_in: 600 }), + json: async () => ({ api_key_id: "id-1" }), }), );