supabase-cli/internal/functions/serve/templates/main.ts

247 lines
7.3 KiB
TypeScript

import { STATUS_CODE, STATUS_TEXT } from "https://deno.land/std/http/status.ts";
import * as posix from "https://deno.land/std/path/posix/mod.ts";
import * as jose from "https://deno.land/x/jose@v4.13.1/index.ts";
const SB_SPECIFIC_ERROR_CODE = {
BootError:
STATUS_CODE.ServiceUnavailable, /** Service Unavailable (RFC 7231, 6.6.4) */
InvalidWorkerResponse:
STATUS_CODE.InternalServerError, /** Internal Server Error (RFC 7231, 6.6.1) */
WorkerLimit: 546, /** Extended */
};
const SB_SPECIFIC_ERROR_TEXT = {
[SB_SPECIFIC_ERROR_CODE.BootError]: "BOOT_ERROR",
[SB_SPECIFIC_ERROR_CODE.InvalidWorkerResponse]: "WORKER_ERROR",
[SB_SPECIFIC_ERROR_CODE.WorkerLimit]: "WORKER_LIMIT",
};
const SB_SPECIFIC_ERROR_REASON = {
[SB_SPECIFIC_ERROR_CODE.BootError]:
"Worker failed to boot (please check logs)",
[SB_SPECIFIC_ERROR_CODE.InvalidWorkerResponse]:
"Function exited due to an error (please check logs)",
[SB_SPECIFIC_ERROR_CODE.WorkerLimit]:
"Worker failed to respond due to a resource limit (please check logs)",
};
// OS stuff - we don't want to expose these to the functions.
const EXCLUDED_ENVS = ["HOME", "HOSTNAME", "PATH", "PWD"];
const JWT_SECRET = Deno.env.get("SUPABASE_INTERNAL_JWT_SECRET")!;
const HOST_PORT = Deno.env.get("SUPABASE_INTERNAL_HOST_PORT")!;
const DEBUG = Deno.env.get("SUPABASE_INTERNAL_DEBUG") === "true";
const FUNCTIONS_CONFIG_STRING = Deno.env.get(
"SUPABASE_INTERNAL_FUNCTIONS_CONFIG",
)!;
const WALLCLOCK_LIMIT_SEC = parseInt(
Deno.env.get("SUPABASE_INTERNAL_WALLCLOCK_LIMIT_SEC"),
);
const DENO_SB_ERROR_MAP = new Map([
[Deno.errors.InvalidWorkerCreation, SB_SPECIFIC_ERROR_CODE.BootError],
[Deno.errors.InvalidWorkerResponse, SB_SPECIFIC_ERROR_CODE.InvalidWorkerResponse],
[
Deno.errors.WorkerRequestCancelled,
SB_SPECIFIC_ERROR_CODE.WorkerLimit,
],
]);
interface FunctionConfig {
entrypointPath: string;
importMapPath: string;
verifyJWT: boolean;
}
function getResponse(payload: any, status: number, customHeaders = {}) {
const headers = { ...customHeaders };
let body: string | null = null;
if (payload) {
if (typeof payload === "object") {
headers["Content-Type"] = "application/json";
body = JSON.stringify(payload);
} else if (typeof payload === "string") {
headers["Content-Type"] = "text/plain";
body = payload;
} else {
body = null;
}
}
return new Response(body, { status, headers });
}
const functionsConfig: Record<string, FunctionConfig> = (() => {
try {
const functionsConfig = JSON.parse(FUNCTIONS_CONFIG_STRING);
if (DEBUG) {
console.log(
"Functions config:",
JSON.stringify(functionsConfig, null, 2),
);
}
return functionsConfig;
} catch (cause) {
throw new Error("Failed to parse functions config", { cause });
}
})();
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 verifyJWT(jwt: string): Promise<boolean> {
const encoder = new TextEncoder();
const secretKey = encoder.encode(JWT_SECRET);
try {
await jose.jwtVerify(jwt, secretKey);
} catch (e) {
console.error(e);
return false;
}
return true;
}
Deno.serve({
handler: async (req: Request) => {
const url = new URL(req.url);
const { pathname } = url;
// handle health checks
if (pathname === "/_internal/health") {
return getResponse({ message: "ok" }, STATUS_CODE.OK);
}
// handle metrics
if (pathname === '/_internal/metric') {
const metric = await EdgeRuntime.getRuntimeMetrics();
return Response.json(metric);
}
const pathParts = pathname.split("/");
const functionName = pathParts[1];
if (!functionName || !(functionName in functionsConfig)) {
return getResponse("Function not found", STATUS_CODE.NotFound);
}
if (req.method !== "OPTIONS" && functionsConfig[functionName].verifyJWT) {
try {
const token = getAuthToken(req);
const isValidJWT = await verifyJWT(token);
if (!isValidJWT) {
return getResponse({ msg: "Invalid JWT" }, STATUS_CODE.Unauthorized);
}
} catch (e) {
console.error(e);
return getResponse({ msg: e.toString() }, STATUS_CODE.Unauthorized);
}
}
const servicePath = posix.dirname(functionsConfig[functionName].entrypointPath);
console.error(`serving the request with ${servicePath}`);
// Ref: https://supabase.com/docs/guides/functions/limits
const memoryLimitMb = 256;
const workerTimeoutMs = isFinite(WALLCLOCK_LIMIT_SEC) ? WALLCLOCK_LIMIT_SEC * 1000 : 400 * 1000;
const noModuleCache = false;
const envVarsObj = Deno.env.toObject();
const envVars = Object.entries(envVarsObj)
.filter(([name, _]) =>
!EXCLUDED_ENVS.includes(name) && !name.startsWith("SUPABASE_INTERNAL_")
);
const forceCreate = false;
const customModuleRoot = ""; // empty string to allow any local path
const cpuTimeSoftLimitMs = 1000;
const cpuTimeHardLimitMs = 2000;
// NOTE(Nyannyacha): Decorator type has been set to tc39 by Lakshan's request,
// but in my opinion, we should probably expose this to customers at some
// point, as their migration process will not be easy.
const decoratorType = "tc39";
const absEntrypoint = posix.join(Deno.cwd(), functionsConfig[functionName].entrypointPath);
const maybeEntrypoint = posix.toFileUrl(absEntrypoint).href;
const staticPatterns = functionsConfig[functionName].staticFiles;
try {
const worker = await EdgeRuntime.userWorkers.create({
servicePath,
memoryLimitMb,
workerTimeoutMs,
noModuleCache,
importMapPath: functionsConfig[functionName].importMapPath,
envVars,
forceCreate,
customModuleRoot,
cpuTimeSoftLimitMs,
cpuTimeHardLimitMs,
decoratorType,
maybeEntrypoint,
context: {
useReadSyncFileAPI: true,
},
staticPatterns,
});
return await worker.fetch(req);
} catch (e) {
console.error(e);
for (const [denoError, sbCode] of DENO_SB_ERROR_MAP.entries()) {
if (denoError !== void 0 && e instanceof denoError) {
return getResponse(
{
code: SB_SPECIFIC_ERROR_TEXT[sbCode],
message: SB_SPECIFIC_ERROR_REASON[sbCode],
},
sbCode
);
}
}
return getResponse(
{
code: STATUS_TEXT[STATUS_CODE.InternalServerError],
message: "Request failed due to an internal server error",
trace: JSON.stringify(e.stack)
},
STATUS_CODE.InternalServerError,
);
}
},
onListen: () => {
console.log(
`Serving functions on http://127.0.0.1:${HOST_PORT}/functions/v1/<function-name>\nUsing ${Deno.version.deno}`,
);
},
onError: e => {
return getResponse(
{
code: STATUS_TEXT[STATUS_CODE.InternalServerError],
message: "Request failed due to an internal server error",
trace: JSON.stringify(e.stack)
},
STATUS_CODE.InternalServerError
)
}
});