From 14880fe94cd4046a8c44c8a05e4a018132ae1bd7 Mon Sep 17 00:00:00 2001 From: Hermann_Kitio Date: Sat, 25 Apr 2026 05:49:45 +0300 Subject: [PATCH] fix(deepgram): revert to /v1/auth/grant for temporary JWT tokens MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - /v1/projects/{id}/keys creates permanent API keys, not WebSocket-compatible JWT tokens - /v1/auth/grant requires Member-scoped API key (now configured) - Remove DEEPGRAM_PROJECT_ID dependency - 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, 33 insertions(+), 56 deletions(-) diff --git a/.env.example b/.env.example index d38a90c..a59443e 100644 --- a/.env.example +++ b/.env.example @@ -6,7 +6,6 @@ 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 e8b02b2..e63c7c8 100644 --- a/src/lib/deepgram.ts +++ b/src/lib/deepgram.ts @@ -1,16 +1,18 @@ /** - * Client Deepgram — Sprint 4b (corrigé Sprint 4b.1). + * Client Deepgram — Sprint 4b. * - * 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. + * Génère un token éphémère que le frontend utilise pour ouvrir une connexion + * WebSocket directe à Deepgram (transcription live). Le token est passé en + * query string `?token=...` lors de l'init de la WS — c'est le seul mécanisme + * de tokens éphémères WebSocket-compatible côté Deepgram. Les clés API créées + * via `/v1/projects/{id}/keys` sont permanentes et ne fonctionnent pas en + * query string sur la WS. * - * Endpoint : POST https://api.deepgram.com/v1/projects/{project_id}/keys - * Doc : https://developers.deepgram.com/reference/create-key + * Endpoint : POST https://api.deepgram.com/v1/auth/grant + * Doc : https://developers.deepgram.com/docs/create-temporary-api-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. + * Pré-requis : la clé `DEEPGRAM_API_KEY` doit avoir le scope « Member » du + * projet. Sans ce scope, l'endpoint renvoie 403. */ const DEEPGRAM_API_KEY = process.env.DEEPGRAM_API_KEY ?? ""; @@ -22,35 +24,18 @@ 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 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), + const response = await fetch(`${DEEPGRAM_BASE_URL}/v1/auth/grant`, { + method: "POST", + headers: { + "Content-Type": "application/json", + Authorization: `Token ${DEEPGRAM_API_KEY}`, }, - ); + body: JSON.stringify({ ttl_seconds: ttlSeconds }), + signal: AbortSignal.timeout(DEEPGRAM_TIMEOUT_MS), + }); if (!response.ok) { throw new Error( @@ -58,13 +43,13 @@ export async function createTemporaryToken( ); } - const data = (await response.json()) as { key?: string }; + const data = (await response.json()) as { access_token?: string }; - if (typeof data.key !== "string" || data.key.length === 0) { - throw new Error("Deepgram API: key manquant dans la réponse"); + if (typeof data.access_token !== "string" || data.access_token.length === 0) { + throw new Error("Deepgram API: access_token manquant dans la réponse"); } - // 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 }; + // L'API retourne le TTL effectif dans le payload (champ `expires_in`), + // mais on retourne la valeur demandée pour cohérence avec le frontend. + return { token: data.access_token, expires_in: ttlSeconds }; } diff --git a/src/routes/__tests__/transcriptionsToken.test.ts b/src/routes/__tests__/transcriptionsToken.test.ts index 3e5be9a..e575f2a 100644 --- a/src/routes/__tests__/transcriptionsToken.test.ts +++ b/src/routes/__tests__/transcriptionsToken.test.ts @@ -33,7 +33,6 @@ 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 () => { @@ -48,8 +47,8 @@ describe("POST /transcriptions/token — Sprint 4b", () => { vi.fn().mockResolvedValue({ ok: true, json: async () => ({ - key: "dg-temp-abc123", - api_key_id: "id-1", + access_token: "dg-temp-abc123", + expires_in: 600, }), }), ); @@ -66,10 +65,10 @@ describe("POST /transcriptions/token — Sprint 4b", () => { expect(body.expires_in).toBe(600); }); - it("appelle POST /v1/projects/{id}/keys avec Authorization Token et le bon body", async () => { + it("appelle POST /v1/auth/grant avec Authorization Token et ttl_seconds=600", async () => { const fetchMock = vi.fn().mockResolvedValue({ ok: true, - json: async () => ({ key: "tok" }), + json: async () => ({ access_token: "tok" }), }); vi.stubGlobal("fetch", fetchMock); @@ -81,16 +80,10 @@ describe("POST /transcriptions/token — Sprint 4b", () => { expect(fetchMock).toHaveBeenCalledTimes(1); const [url, init] = fetchMock.mock.calls[0]; - expect(String(url)).toBe( - "https://api.deepgram.com/v1/projects/proj-test-123/keys", - ); + expect(String(url)).toBe("https://api.deepgram.com/v1/auth/grant"); const headers = (init?.headers ?? {}) as Record; expect(headers["Authorization"]).toMatch(/^Token /); - expect(JSON.parse(init.body as string)).toEqual({ - comment: "expria-temp", - scopes: ["usage:write"], - time_to_live_in_seconds: 600, - }); + expect(JSON.parse(init.body as string)).toEqual({ ttl_seconds: 600 }); }); it("500 INTERNAL_ERROR si Deepgram non-OK", async () => { @@ -129,12 +122,12 @@ describe("POST /transcriptions/token — Sprint 4b", () => { expect(res.status).toBe(500); }); - it("500 INTERNAL_ERROR si key absent dans la réponse Deepgram", async () => { + it("500 INTERNAL_ERROR si access_token absent dans la réponse Deepgram", async () => { vi.stubGlobal( "fetch", vi.fn().mockResolvedValue({ ok: true, - json: async () => ({ api_key_id: "id-1" }), + json: async () => ({ expires_in: 600 }), }), );