( token: string, secret: string, )
| 80 | }; |
| 81 | |
| 82 | export const verifyTranscriptToken = async ( |
| 83 | token: string, |
| 84 | secret: string, |
| 85 | ): Promise<TranscriptTokenPayload | null> => { |
| 86 | const parts = token.split("."); |
| 87 | if (parts.length !== 3) return null; |
| 88 | const [header, payload, signature] = parts; |
| 89 | if (!header || !payload || !signature) return null; |
| 90 | |
| 91 | const encoder = new TextEncoder(); |
| 92 | const key = await crypto.subtle.importKey( |
| 93 | "raw", |
| 94 | encoder.encode(secret), |
| 95 | { name: "HMAC", hash: "SHA-256" }, |
| 96 | false, |
| 97 | ["verify"], |
| 98 | ); |
| 99 | const valid = await crypto.subtle.verify( |
| 100 | "HMAC", |
| 101 | key, |
| 102 | base64UrlToBytes(signature), |
| 103 | encoder.encode(`${header}.${payload}`), |
| 104 | ); |
| 105 | if (!valid) return null; |
| 106 | |
| 107 | const parsed = safeJsonParse( |
| 108 | new TextDecoder().decode(base64UrlToBytes(payload)), |
| 109 | ); |
| 110 | if (!parsed || typeof parsed !== "object") return null; |
| 111 | const data = parsed as TranscriptTokenPayload; |
| 112 | if (data.aud !== "conclave-transcript-worker") return null; |
| 113 | if (!data.exp || data.exp * 1000 < Date.now()) return null; |
| 114 | if (!data.userId || !data.roomId) return null; |
| 115 | return { |
| 116 | aud: data.aud, |
| 117 | exp: data.exp, |
| 118 | sub: data.sub, |
| 119 | userId: data.userId, |
| 120 | displayName: data.displayName, |
| 121 | roomId: data.roomId, |
| 122 | clientId: data.clientId, |
| 123 | channelId: data.channelId, |
| 124 | isAdmin: data.isAdmin, |
| 125 | isHost: data.isHost, |
| 126 | capabilities: data.capabilities, |
| 127 | }; |
| 128 | }; |
| 129 | |
| 130 | export const verifyTranscriptRoomToken = async ( |
| 131 | token: string, |
no test coverage detected