import * as jose from 'jsr:@panva/jose@6' console.log('main function started') const JWT_SECRET = Deno.env.get('JWT_SECRET') const SUPABASE_JWKS = parseJwks(Deno.env.get('SUPABASE_JWKS')) const VERIFY_JWT = Deno.env.get('VERIFY_JWT') === 'true' // NOTE:(kallebysantos) We don't check for valid keys but just the bare array parsing, // let this for 'jose' lib verification export function parseJwks(raw: string | undefined): jose.JSONWebKeySet | null { if (!raw) return null try { const parsed = JSON.parse(raw) if (parsed?.keys && Array.isArray(parsed.keys)) { return parsed as jose.JSONWebKeySet } return null } catch { return null } } /** * Extract JWT token from Authorization header * * Parses the Authorization header to extract the Bearer token. * Expects format: "Bearer " * * @param req - The HTTP request object * @returns The JWT token string * @throws Error if Authorization header is missing or malformed */ function getAuthToken(req: Request) { const authHeader = req.headers.get('authorization') if (!authHeader) { throw new Error('Missing authorization header') } const [bearer, token] = authHeader.split(' ') if (bearer !== 'Bearer') { throw new Error(`Auth header is not 'Bearer {token}'`) } return token } async function isValidLegacyJWT(jwt: string): Promise { if (!JWT_SECRET) { console.error('JWT_SECRET not available for HS256 token verification') return false } const encoder = new TextEncoder(); const secretKey = encoder.encode(JWT_SECRET); try { await jose.jwtVerify(jwt, secretKey); } catch (e) { console.error('Symmetric Legacy JWT verification error', e); return false; } return true; } async function isValidJWT(jwt: string): Promise { if (!SUPABASE_JWKS) { console.error('JWKS not available for ES256/RS256 token verification') return false } try { const localJwks = jose.createLocalJWKSet(SUPABASE_JWKS); await jose.jwtVerify(jwt, localJwks); } catch (e) { console.error('Asymmetric JWT verification error', e); return false } return true; } /** * Verify JWT token, handling both legacy (HS256) and newer (ES256/RS256) algorithms * * This function automatically detects the algorithm used in the token and applies * the appropriate verification method: * - HS256: Uses JWT_SECRET (symmetric key) * - ES256/RS256: Uses JWKS endpoint (asymmetric public keys) * * This fix ensures compatibility with both legacy tokens and newer asymmetric tokens, * resolving the "Key for the ES256 algorithm must be of type CryptoKey" error. * * @param jwt - The JWT token string to verify * @returns Promise resolving to true if verification succeeds, false otherwise */ async function isValidHybridJWT(jwt: string): Promise { const { alg: jwtAlgorithm } = jose.decodeProtectedHeader(jwt) if (jwtAlgorithm === 'HS256') { console.log(`Legacy token type detected, attempting ${jwtAlgorithm} verification.`) return await isValidLegacyJWT(jwt) } if (jwtAlgorithm === 'ES256' || jwtAlgorithm === 'RS256') { return await isValidJWT(jwt) } return false; } Deno.serve(async (req: Request) => { if (req.method !== 'OPTIONS' && VERIFY_JWT) { try { const token = getAuthToken(req) const isValidJWT = await isValidHybridJWT(token); if (!isValidJWT) { return new Response(JSON.stringify({ msg: 'Invalid JWT' }), { status: 401, headers: { 'Content-Type': 'application/json' }, }) } } catch (e) { console.error(e) return new Response(JSON.stringify({ msg: e.toString() }), { status: 401, headers: { 'Content-Type': 'application/json' }, }) } } const url = new URL(req.url) const { pathname } = url const path_parts = pathname.split('/') const service_name = path_parts[1] if (!service_name || service_name === '') { const error = { msg: 'missing function name in request' } return new Response(JSON.stringify(error), { status: 400, headers: { 'Content-Type': 'application/json' }, }) } const servicePath = `/home/deno/functions/${service_name}` console.error(`serving the request with ${servicePath}`) const memoryLimitMb = 150 const workerTimeoutMs = 1 * 60 * 1000 const noModuleCache = false // Using a common Import Map for all functions // to use a scope 'deno.json' it must be dinamically resolved base on the 'service_name' const importMapPath = `/home/deno/functions/deno.jsonc` // SUPABASE_FUNCTION_SLUG is listed after the container env snapshot so // nothing in it can shadow the value, and it is per-request because only this // worker knows which function the request resolved to. const envVarsObj = { ...Deno.env.toObject(), SUPABASE_FUNCTION_SLUG: service_name } const envVars = Object.keys(envVarsObj).map((k) => [k, envVarsObj[k]]) try { const worker = await EdgeRuntime.userWorkers.create({ servicePath, memoryLimitMb, workerTimeoutMs, noModuleCache, importMapPath, envVars, }) return await worker.fetch(req) } catch (e) { const error = { msg: e.toString() } return new Response(JSON.stringify(error), { status: 500, headers: { 'Content-Type': 'application/json' }, }) } })