fix(deepgram): revert to /v1/auth/grant for temporary JWT tokens
- /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) <noreply@anthropic.com>
This commit is contained in:
parent
a62b4816a2
commit
14880fe94c
3 changed files with 33 additions and 56 deletions
|
|
@ -6,7 +6,6 @@ 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
|
||||||
|
|
|
||||||
|
|
@ -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
|
* Génère un token éphémère que le frontend utilise pour ouvrir une connexion
|
||||||
* directe à Deepgram (transcription live). La clé temporaire est créée comme
|
* WebSocket directe à Deepgram (transcription live). Le token est passé en
|
||||||
* sub-key d'un projet existant — la clé maître `DEEPGRAM_API_KEY` reste côté
|
* query string `?token=...` lors de l'init de la WS — c'est le seul mécanisme
|
||||||
* serveur et n'est jamais exposée au navigateur.
|
* 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
|
* Endpoint : POST https://api.deepgram.com/v1/auth/grant
|
||||||
* Doc : https://developers.deepgram.com/reference/create-key
|
* Doc : https://developers.deepgram.com/docs/create-temporary-api-key
|
||||||
*
|
*
|
||||||
* Le scope `usage:write` permet d'ouvrir une session live (POST /listen ou
|
* Pré-requis : la clé `DEEPGRAM_API_KEY` doit avoir le scope « Member » du
|
||||||
* WebSocket) sans donner accès aux endpoints de management du projet.
|
* projet. Sans ce scope, l'endpoint renvoie 403.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const DEEPGRAM_API_KEY = process.env.DEEPGRAM_API_KEY ?? "";
|
const DEEPGRAM_API_KEY = process.env.DEEPGRAM_API_KEY ?? "";
|
||||||
|
|
@ -22,35 +24,18 @@ 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 projectId = getProjectId();
|
const response = await fetch(`${DEEPGRAM_BASE_URL}/v1/auth/grant`, {
|
||||||
|
method: "POST",
|
||||||
const response = await fetch(
|
headers: {
|
||||||
`${DEEPGRAM_BASE_URL}/v1/projects/${projectId}/keys`,
|
"Content-Type": "application/json",
|
||||||
{
|
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(
|
||||||
|
|
@ -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) {
|
if (typeof data.access_token !== "string" || data.access_token.length === 0) {
|
||||||
throw new Error("Deepgram API: key manquant dans la réponse");
|
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,
|
// L'API retourne le TTL effectif dans le payload (champ `expires_in`),
|
||||||
// qui matche ce que l'API a appliqué (champ `time_to_live_in_seconds`).
|
// mais on retourne la valeur demandée pour cohérence avec le frontend.
|
||||||
return { token: data.key, expires_in: ttlSeconds };
|
return { token: data.access_token, expires_in: ttlSeconds };
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -33,7 +33,6 @@ 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 () => {
|
||||||
|
|
@ -48,8 +47,8 @@ describe("POST /transcriptions/token — Sprint 4b", () => {
|
||||||
vi.fn().mockResolvedValue({
|
vi.fn().mockResolvedValue({
|
||||||
ok: true,
|
ok: true,
|
||||||
json: async () => ({
|
json: async () => ({
|
||||||
key: "dg-temp-abc123",
|
access_token: "dg-temp-abc123",
|
||||||
api_key_id: "id-1",
|
expires_in: 600,
|
||||||
}),
|
}),
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
@ -66,10 +65,10 @@ describe("POST /transcriptions/token — Sprint 4b", () => {
|
||||||
expect(body.expires_in).toBe(600);
|
expect(body.expires_in).toBe(600);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("appelle POST /v1/projects/{id}/keys avec Authorization Token <key> et le bon body", async () => {
|
it("appelle POST /v1/auth/grant avec Authorization Token <key> et ttl_seconds=600", async () => {
|
||||||
const fetchMock = vi.fn().mockResolvedValue({
|
const fetchMock = vi.fn().mockResolvedValue({
|
||||||
ok: true,
|
ok: true,
|
||||||
json: async () => ({ key: "tok" }),
|
json: async () => ({ access_token: "tok" }),
|
||||||
});
|
});
|
||||||
vi.stubGlobal("fetch", fetchMock);
|
vi.stubGlobal("fetch", fetchMock);
|
||||||
|
|
||||||
|
|
@ -81,16 +80,10 @@ 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)).toBe(
|
expect(String(url)).toBe("https://api.deepgram.com/v1/auth/grant");
|
||||||
"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({
|
expect(JSON.parse(init.body as string)).toEqual({ ttl_seconds: 600 });
|
||||||
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 () => {
|
||||||
|
|
@ -129,12 +122,12 @@ describe("POST /transcriptions/token — Sprint 4b", () => {
|
||||||
expect(res.status).toBe(500);
|
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(
|
vi.stubGlobal(
|
||||||
"fetch",
|
"fetch",
|
||||||
vi.fn().mockResolvedValue({
|
vi.fn().mockResolvedValue({
|
||||||
ok: true,
|
ok: true,
|
||||||
json: async () => ({ api_key_id: "id-1" }),
|
json: async () => ({ expires_in: 600 }),
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue