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  });