github.com/supabase/cli@v1.168.1/internal/functions/serve/templates/main.ts (about) 1 import { 2 STATUS_CODE, 3 STATUS_TEXT, 4 } from "https://deno.land/std/http/status.ts"; 5 6 import * as jose from "https://deno.land/x/jose@v4.13.1/index.ts"; 7 8 const SB_SPECIFIC_ERROR_CODE = { 9 BootError: STATUS_CODE.ServiceUnavailable, /** Service Unavailable (RFC 7231, 6.6.4) */ 10 WorkerRequestCancelled: STATUS_CODE.BadGateway, /** Bad Gateway (RFC 7231, 6.6.3) */ 11 WorkerLimit: 546, /** Extended */ 12 }; 13 14 const SB_SPECIFIC_ERROR_TEXT = { 15 [SB_SPECIFIC_ERROR_CODE.BootError]: "BOOT_ERROR", 16 [SB_SPECIFIC_ERROR_CODE.WorkerRequestCancelled]: "WORKER_REQUEST_CANCELLED", 17 [SB_SPECIFIC_ERROR_CODE.WorkerLimit]: "WORKER_LIMIT", 18 }; 19 20 const SB_SPECIFIC_ERROR_REASON = { 21 [SB_SPECIFIC_ERROR_CODE.BootError]: "Worker failed to boot (please check logs)", 22 [SB_SPECIFIC_ERROR_CODE.WorkerRequestCancelled]: "Request cancelled by the proxy due to an error or resource limit of worker (please check logs)", 23 [SB_SPECIFIC_ERROR_CODE.WorkerLimit]: "Worker failed to respond due to an error or resource limit (please check logs)", 24 } 25 26 // OS stuff - we don't want to expose these to the functions. 27 const EXCLUDED_ENVS = ["HOME", "HOSTNAME", "PATH", "PWD"]; 28 29 const JWT_SECRET = Deno.env.get("SUPABASE_INTERNAL_JWT_SECRET")!; 30 const HOST_PORT = Deno.env.get("SUPABASE_INTERNAL_HOST_PORT")!; 31 const FUNCTIONS_PATH = Deno.env.get("SUPABASE_INTERNAL_FUNCTIONS_PATH")!; 32 const DEBUG = Deno.env.get("SUPABASE_INTERNAL_DEBUG") === "true"; 33 const FUNCTIONS_CONFIG_STRING = Deno.env.get( 34 "SUPABASE_INTERNAL_FUNCTIONS_CONFIG", 35 )!; 36 37 const DENO_SB_ERROR_MAP = new Map([ 38 [Deno.errors.InvalidWorkerCreation, SB_SPECIFIC_ERROR_CODE.BootError], 39 [Deno.errors.InvalidWorkerResponse, SB_SPECIFIC_ERROR_CODE.WorkerLimit], 40 [Deno.errors.WorkerRequestCancelled, SB_SPECIFIC_ERROR_CODE.WorkerRequestCancelled], 41 ]); 42 43 interface FunctionConfig { 44 importMapPath: string; 45 verifyJWT: boolean; 46 } 47 48 function getResponse(payload: any, status: number, customHeaders = {}) { 49 const headers = { ...customHeaders }; 50 let body: string | null = null; 51 52 if (payload) { 53 if (typeof payload === "object") { 54 headers["Content-Type"] = "application/json"; 55 body = JSON.stringify(payload); 56 } else if (typeof payload === "string") { 57 headers["Content-Type"] = "text/plain"; 58 body = payload; 59 } else { 60 body = null; 61 } 62 } 63 64 return new Response(body, { status, headers }); 65 } 66 67 const functionsConfig: Record<string, FunctionConfig> = (() => { 68 try { 69 const functionsConfig = JSON.parse(FUNCTIONS_CONFIG_STRING); 70 71 if (DEBUG) { 72 console.log( 73 "Functions config:", 74 JSON.stringify(functionsConfig, null, 2), 75 ); 76 } 77 78 return functionsConfig; 79 } catch (cause) { 80 throw new Error("Failed to parse functions config", { cause }); 81 } 82 })(); 83 84 function getAuthToken(req: Request) { 85 const authHeader = req.headers.get("authorization"); 86 if (!authHeader) { 87 throw new Error("Missing authorization header"); 88 } 89 const [bearer, token] = authHeader.split(" "); 90 if (bearer !== "Bearer") { 91 throw new Error(`Auth header is not 'Bearer {token}'`); 92 } 93 return token; 94 } 95 96 async function verifyJWT(jwt: string): Promise<boolean> { 97 const encoder = new TextEncoder(); 98 const secretKey = encoder.encode(JWT_SECRET); 99 try { 100 await jose.jwtVerify(jwt, secretKey); 101 } catch (e) { 102 console.error(e); 103 return false; 104 } 105 return true; 106 } 107 108 Deno.serve({ 109 handler: async (req: Request) => { 110 const url = new URL(req.url); 111 const { pathname } = url; 112 113 // handle health checks 114 if (pathname === "/_internal/health") { 115 return getResponse({ message: "ok" }, STATUS_CODE.OK); 116 } 117 118 const pathParts = pathname.split("/"); 119 const functionName = pathParts[1]; 120 121 if (!functionName || !(functionName in functionsConfig)) { 122 return getResponse("Function not found", STATUS_CODE.NotFound); 123 } 124 125 if (req.method !== "OPTIONS" && functionsConfig[functionName].verifyJWT) { 126 try { 127 const token = getAuthToken(req); 128 const isValidJWT = await verifyJWT(token); 129 130 if (!isValidJWT) { 131 return getResponse({ msg: "Invalid JWT" }, STATUS_CODE.Unauthorized); 132 } 133 } catch (e) { 134 console.error(e); 135 return getResponse({ msg: e.toString() }, STATUS_CODE.Unauthorized); 136 } 137 } 138 139 const servicePath = `${FUNCTIONS_PATH}/${functionName}`; 140 console.error(`serving the request with ${servicePath}`); 141 142 const memoryLimitMb = 150; 143 const workerTimeoutMs = 400 * 1000; 144 const noModuleCache = false; 145 const envVarsObj = Deno.env.toObject(); 146 const envVars = Object.entries(envVarsObj) 147 .filter(([name, _]) => 148 !EXCLUDED_ENVS.includes(name) && !name.startsWith("SUPABASE_INTERNAL_") 149 ); 150 151 const forceCreate = true; 152 const customModuleRoot = ""; // empty string to allow any local path 153 const cpuTimeSoftLimitMs = 1000; 154 const cpuTimeHardLimitMs = 2000; 155 156 // NOTE(Nyannyacha): Decorator type has been set to tc39 by Lakshan's request, 157 // but in my opinion, we should probably expose this to customers at some 158 // point, as their migration process will not be easy. 159 const decoratorType = "tc39"; 160 161 try { 162 const worker = await EdgeRuntime.userWorkers.create({ 163 servicePath, 164 memoryLimitMb, 165 workerTimeoutMs, 166 noModuleCache, 167 importMapPath: functionsConfig[functionName].importMapPath, 168 envVars, 169 forceCreate, 170 customModuleRoot, 171 cpuTimeSoftLimitMs, 172 cpuTimeHardLimitMs, 173 decoratorType 174 }); 175 176 const controller = new AbortController(); 177 const { signal } = controller; 178 179 // Note: Requests are aborted after 200s (same config as in production) 180 // TODO: make this configuarable 181 setTimeout(() => controller.abort(), 200 * 1000); 182 183 return await worker.fetch(req, { signal }); 184 } catch (e) { 185 console.error(e); 186 187 for (const [denoError, sbCode] of DENO_SB_ERROR_MAP.entries()) { 188 if (denoError !== void 0 && e instanceof denoError) { 189 return getResponse( 190 { 191 code: SB_SPECIFIC_ERROR_TEXT[sbCode], 192 message: SB_SPECIFIC_ERROR_REASON[sbCode], 193 }, 194 sbCode 195 ); 196 } 197 } 198 199 return getResponse( 200 { 201 code: STATUS_TEXT[STATUS_CODE.InternalServerError], 202 message: "Request failed due to an internal server error", 203 trace: JSON.stringify(e.stack) 204 }, 205 STATUS_CODE.InternalServerError, 206 ); 207 } 208 }, 209 210 onListen: () => { 211 console.log( 212 `Serving functions on http://127.0.0.1:${HOST_PORT}/functions/v1/<function-name>\nUsing ${Deno.version.deno}`, 213 ); 214 }, 215 216 onError: e => { 217 return getResponse( 218 { 219 code: STATUS_TEXT[STATUS_CODE.InternalServerError], 220 message: "Request failed due to an internal server error", 221 trace: JSON.stringify(e.stack) 222 }, 223 STATUS_CODE.InternalServerError 224 ) 225 } 226 });