LCOV - code coverage report
Current view: top level - inference-streaming-proxy - index.mjs (source / functions) Coverage Total Hit
Test: gco-inference-streaming-proxy Lines: 100.0 % 1385 1385
Test Date: 2026-09-14 21:55:07 Functions: 100.0 % 73 73
Legend: Lines: hit not hit

            Line data    Source code
       1            5 : import { createHash, createHmac, randomBytes, X509Certificate } from "node:crypto";
       2            5 : import * as https from "node:https";
       3            5 : import { finished, pipeline } from "node:stream/promises";
       4            5 : import { checkServerIdentity } from "node:tls";
       5            5 : import { performance } from "node:perf_hooks";
       6            5 : 
       7            5 : import {
       8            5 :   GetSecretValueCommand,
       9            5 :   SecretsManagerClient,
      10            5 : } from "@aws-sdk/client-secrets-manager";
      11            5 : import { GetParameterCommand, SSMClient } from "@aws-sdk/client-ssm";
      12            5 : import {
      13            5 :   DescribeLoadBalancersCommand,
      14            5 :   DescribeTagsCommand,
      15            5 :   ElasticLoadBalancingV2Client,
      16            5 : } from "@aws-sdk/client-elastic-load-balancing-v2";
      17            5 : 
      18            5 : const HOP_BY_HOP_HEADERS = new Set([
      19            5 :   "connection",
      20            5 :   "keep-alive",
      21            5 :   "proxy-authenticate",
      22            5 :   "proxy-authorization",
      23            5 :   "te",
      24            5 :   "trailer",
      25            5 :   "transfer-encoding",
      26            5 :   "upgrade",
      27            5 : ]);
      28            5 : // Streaming response metadata has a single-value `headers` object. Inference
      29            5 : // backends are not allowed to set caller cookies, so drop Set-Cookie instead
      30            5 : // of corrupting repeated values by comma-folding them.
      31            5 : const BLOCKED_RESPONSE_HEADERS = new Set(["content-length", "set-cookie"]);
      32            5 : const ALLOWED_REQUEST_HEADERS = new Set([
      33            5 :   "accept",
      34            5 :   "accept-encoding",
      35            5 :   "cache-control",
      36            5 :   "content-encoding",
      37            5 :   "content-type",
      38            5 :   "idempotency-key",
      39            5 :   "if-match",
      40            5 :   "if-none-match",
      41            5 :   "prefer",
      42            5 :   "range",
      43            5 :   "user-agent",
      44            5 :   "x-request-id",
      45            5 : ]);
      46            5 : const INTERNAL_SIGNATURE_HEADERS = new Set([
      47            5 :   "x-gco-signature-version",
      48            5 :   "x-gco-signature",
      49            5 :   "x-gco-timestamp",
      50            5 :   "x-gco-nonce",
      51            5 :   "x-gco-content-sha256",
      52            5 : ]);
      53            5 : const ALLOWED_METHODS = new Set(["GET", "HEAD", "POST"]);
      54            5 : const RETRYABLE_METHODS = new Set(["GET", "HEAD"]);
      55            5 : const RETRYABLE_STATUS_CODES = new Set([429, 502, 503, 504]);
      56            5 : const REGION_RE = /^[a-z]{2,4}(?:-[a-z0-9]+)+-[0-9]+$/;
      57            5 : const DNS_NAME_RE = /^(?=.{1,253}$)(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/i;
      58            5 : const GLOBAL_IDLE_TIMEOUT_MS = 30_000;
      59            5 : const REGIONAL_IDLE_TIMEOUT_MS = 300_000;
      60            5 : const LAMBDA_MAX_FORWARD_MS = 899_000;
      61            5 : const RESPONSE_HEADROOM_MS = 1_000;
      62            5 : const DEFAULT_MAX_REQUEST_BODY_BYTES = 1_048_576;
      63            5 : const MAX_CONFIGURABLE_REQUEST_BODY_BYTES = 10 * 1024 * 1024;
      64            5 : const RESPONSE_METADATA_DELIMITER = Buffer.alloc(8);
      65            5 : const MAX_RESPONSE_METADATA_PREFIX_BYTES = 16 * 1024;
      66            5 : 
      67           84 : function boundedEnvFloat(name, defaultValue, minimum, maximum) {
      68           84 :   const raw = process.env[name];
      69           84 :   if (raw === undefined || raw.trim() === "") {
      70           46 :     return defaultValue;
      71           46 :   }
      72           22 :   const value = Number(raw);
      73           84 :   return Number.isFinite(value) && value >= minimum && value <= maximum
      74           84 :     ? value
      75           84 :     : defaultValue;
      76           84 : }
      77            5 : 
      78           21 : function boundedEnvInt(name, defaultValue, minimum, maximum) {
      79           21 :   const raw = process.env[name];
      80           21 :   if (raw === undefined || !/^[+-]?\d+$/.test(raw.trim())) {
      81            7 :     return defaultValue;
      82            7 :   }
      83            6 :   const value = Number(raw);
      84           21 :   return Number.isSafeInteger(value) && value >= minimum && value <= maximum
      85           21 :     ? value
      86           21 :     : defaultValue;
      87           21 : }
      88            5 : 
      89            5 : const MAX_REQUEST_BODY_BYTES = boundedEnvInt(
      90            5 :   "MAX_REQUEST_BODY_BYTES",
      91            5 :   DEFAULT_MAX_REQUEST_BODY_BYTES,
      92            5 :   1,
      93            5 :   MAX_CONFIGURABLE_REQUEST_BODY_BYTES,
      94            5 : );
      95            5 : 
      96           17 : function monotonicSeconds() {
      97           17 :   return performance.now() / 1_000;
      98           17 : }
      99            5 : 
     100          101 : function monotonicMilliseconds() {
     101          101 :   return performance.now();
     102          101 : }
     103            5 : 
     104            5 : class PublicError extends Error {
     105            5 :   constructor(statusCode, publicMessage, headers = {}, details = {}) {
     106           60 :     super(publicMessage);
     107           60 :     this.name = "PublicError";
     108           60 :     this.statusCode = statusCode;
     109           60 :     this.publicMessage = publicMessage;
     110           60 :     this.headers = headers;
     111           60 :     this.details = details;
     112           60 :   }
     113            5 : }
     114            5 : 
     115            5 : class UpstreamTimeoutError extends Error {
     116            5 :   constructor() {
     117            5 :     super("Upstream timeout");
     118            5 :     this.name = "UpstreamTimeoutError";
     119            5 :     this.code = "GCO_UPSTREAM_TIMEOUT";
     120            5 :   }
     121            5 : }
     122            5 : 
     123            5 : class DownstreamAbortError extends Error {
     124            5 :   constructor() {
     125            8 :     super("Downstream closed");
     126            8 :     this.name = "DownstreamAbortError";
     127            8 :     this.code = "GCO_DOWNSTREAM_ABORT";
     128            8 :   }
     129            5 : }
     130            5 : 
     131            5 : const SECRET_CACHE_TTL_SECONDS = boundedEnvFloat(
     132            5 :   "SECRET_CACHE_TTL_SECONDS",
     133            5 :   300,
     134            5 :   1,
     135            5 :   3_600,
     136            5 : );
     137            5 : const SECRET_CACHE_MAX_STALE_SECONDS = Math.max(
     138            5 :   SECRET_CACHE_TTL_SECONDS,
     139            5 :   boundedEnvFloat("SECRET_CACHE_MAX_STALE_SECONDS", 900, 1, 7_200),
     140            5 : );
     141            5 : const SECRET_CACHE_RETRY_SECONDS = boundedEnvFloat(
     142            5 :   "SECRET_CACHE_RETRY_SECONDS",
     143            5 :   5,
     144            5 :   0.1,
     145            5 :   60,
     146            5 : );
     147            5 : const MAX_RETRIES = boundedEnvInt("PROXY_MAX_RETRIES", 3, 1, 5);
     148            5 : const RETRY_BACKOFF_BASE_SECONDS = boundedEnvFloat(
     149            5 :   "PROXY_RETRY_BACKOFF_BASE",
     150            5 :   0.3,
     151            5 :   0,
     152            5 :   5,
     153            5 : );
     154            5 : 
     155            5 : const secretsClients = new Map();
     156            5 : const ssmClients = new Map();
     157            5 : const elbClients = new Map();
     158            5 : 
     159            5 : let cachedSecret = null;
     160            5 : let secretLastSuccessfulRefresh = 0;
     161            5 : let secretLastRefreshAttempt = 0;
     162            5 : let secretRefreshPromise = null;
     163            5 : 
     164            5 : let cachedTlsTransport = null;
     165            5 : let tlsLastSuccessfulRefresh = 0;
     166            5 : let tlsLastRefreshAttempt = 0;
     167            5 : let tlsRefreshPromise = null;
     168            5 : 
     169            5 : const regionalEndpointCache = new Map();
     170            5 : 
     171           22 : function secretRegion(secretArn) {
     172           22 :   const parts = String(secretArn || "").split(":");
     173           22 :   return parts.length >= 6 && parts[0] === "arn" && parts[2] === "secretsmanager"
     174           22 :     ? parts[3] || undefined
     175           22 :     : undefined;
     176           22 : }
     177            5 : 
     178           16 : function getSecretsClient(secretArn) {
     179           16 :   const region = secretRegion(secretArn);
     180           16 :   const key = region || "__default__";
     181           16 :   if (!secretsClients.has(key)) {
     182            2 :     secretsClients.set(
     183            2 :       key,
     184            2 :       new SecretsManagerClient(region ? { region } : {}),
     185            2 :     );
     186            2 :   }
     187           16 :   return secretsClients.get(key);
     188           16 : }
     189            5 : 
     190           22 : function getSsmClient(region) {
     191           22 :   if (!ssmClients.has(region)) {
     192            1 :     ssmClients.set(region, new SSMClient({ region }));
     193            1 :   }
     194           22 :   return ssmClients.get(region);
     195           22 : }
     196            5 : 
     197           22 : function getElbClient(region) {
     198           22 :   if (!elbClients.has(region)) {
     199            1 :     elbClients.set(region, new ElasticLoadBalancingV2Client({ region }));
     200            1 :   }
     201           22 :   return elbClients.get(region);
     202           22 : }
     203            5 : 
     204           14 : async function refreshSecret(now, ageAtAttempt) {
     205           14 :   secretLastRefreshAttempt = now;
     206           14 :   try {
     207           14 :     const secretArn = process.env.SECRET_ARN;
     208           14 :     if (!secretArn) {
     209            1 :       throw new Error("Signing secret is not configured");
     210            1 :     }
     211           13 :     const response = await getSecretsClient(secretArn).send(
     212           13 :       new GetSecretValueCommand({ SecretId: secretArn }),
     213           13 :     );
     214           14 :     if (typeof response.SecretString !== "string") {
     215            2 :       throw new Error("Signing secret has no string value");
     216            2 :     }
     217            7 :     const secretData = JSON.parse(response.SecretString);
     218           14 :     if (typeof secretData?.token !== "string" || secretData.token.length === 0) {
     219            4 :       throw new Error("Signing token is missing");
     220            4 :     }
     221            2 :     cachedSecret = secretData.token;
     222            2 :     secretLastSuccessfulRefresh = now;
     223            2 :     return cachedSecret;
     224           14 :   } catch {
     225           12 :     if (cachedSecret !== null && ageAtAttempt <= SECRET_CACHE_MAX_STALE_SECONDS) {
     226            1 :       console.warn("Secrets Manager refresh failed; using bounded stale signing key");
     227            1 :       return cachedSecret;
     228            1 :     }
     229           11 :     throw new Error("Authentication signing key is unavailable");
     230           11 :   }
     231           14 : }
     232            5 : 
     233           17 : async function getSecretToken(now = monotonicSeconds()) {
     234           17 :   const age = now - secretLastSuccessfulRefresh;
     235           17 :   if (cachedSecret !== null && age < SECRET_CACHE_TTL_SECONDS) {
     236            1 :     return cachedSecret;
     237            1 :   }
     238           16 :   if (
     239           17 :     cachedSecret !== null &&
     240           17 :     age <= SECRET_CACHE_MAX_STALE_SECONDS &&
     241            2 :     now - secretLastRefreshAttempt < SECRET_CACHE_RETRY_SECONDS
     242           17 :   ) {
     243            1 :     return cachedSecret;
     244            1 :   }
     245           17 :   if (secretRefreshPromise !== null) {
     246            1 :     return secretRefreshPromise;
     247            1 :   }
     248           14 : 
     249           14 :   const refresh = refreshSecret(now, age);
     250           14 :   secretRefreshPromise = refresh;
     251           14 :   try {
     252           14 :     return await refresh;
     253           14 :   } finally {
     254           14 :     if (secretRefreshPromise === refresh) {
     255           14 :       secretRefreshPromise = null;
     256           14 :     }
     257           14 :   }
     258           17 : }
     259            5 : 
     260           22 : function tlsSettings() {
     261           22 :   const serverName = String(process.env.BACKEND_TLS_SERVER_NAME || "")
     262           22 :     .trim()
     263           22 :     .replace(/\.+$/, "");
     264           22 :   const parameterName = String(process.env.BACKEND_TLS_ROOT_CA_PARAMETER || "").trim();
     265           22 :   const parameterRegion = String(process.env.BACKEND_TLS_ROOT_CA_REGION || "").trim();
     266           22 :   if (!DNS_NAME_RE.test(serverName)) {
     267            3 :     throw new Error("Backend TLS server identity is not configured");
     268            3 :   }
     269           22 :   if (!parameterName.startsWith("/") || !parameterRegion) {
     270            5 :     throw new Error("Backend TLS trust parameter is not configured");
     271            5 :   }
     272           14 : 
     273           14 :   const ttl = boundedEnvFloat("BACKEND_TLS_CA_CACHE_TTL_SECONDS", 300, 1, 3_600);
     274           14 :   const maxStale = Math.max(
     275           14 :     ttl,
     276           14 :     boundedEnvFloat("BACKEND_TLS_CA_MAX_STALE_SECONDS", 3_600, 1, 86_400),
     277           14 :   );
     278           14 :   const retry = boundedEnvFloat("BACKEND_TLS_CA_RETRY_SECONDS", 5, 0.1, 60);
     279           14 :   return { serverName, parameterName, parameterRegion, ttl, maxStale, retry };
     280           22 : }
     281            5 : 
     282           12 : function validateTrustBundle(trustBundle) {
     283           12 :   if (
     284           12 :     trustBundle.includes("PRIVATE KEY") ||
     285           11 :     !trustBundle.includes("-----BEGIN CERTIFICATE-----")
     286           12 :   ) {
     287            6 :     throw new Error("Backend TLS trust parameter contains invalid public material");
     288            6 :   }
     289            6 :   const certificates = trustBundle.match(
     290            6 :     /-----BEGIN CERTIFICATE-----[\s\S]*?-----END CERTIFICATE-----/g,
     291            6 :   );
     292           12 :   if (!certificates || certificates.length === 0) {
     293            1 :     throw new Error("Backend TLS trust parameter contains malformed certificates");
     294            1 :   }
     295            5 :   for (const certificate of certificates) {
     296            5 :     new X509Certificate(certificate);
     297            5 :   }
     298           12 : }
     299            5 : 
     300            7 : function newTlsTransport(serverName, trustBundle) {
     301            7 :   validateTrustBundle(trustBundle);
     302            7 :   const verifyServerIdentity = (_hostname, certificate) =>
     303            7 :     checkServerIdentity(serverName, certificate);
     304            7 :   const agent = new https.Agent({
     305            7 :     keepAlive: true,
     306            7 :     maxSockets: 10,
     307            7 :     maxFreeSockets: 4,
     308            7 :     rejectUnauthorized: true,
     309            7 :     ca: trustBundle,
     310            7 :     minVersion: "TLSv1.2",
     311            7 :     servername: serverName,
     312            7 :     checkServerIdentity: verifyServerIdentity,
     313            7 :   });
     314            7 :   return { agent, serverName, verifyServerIdentity };
     315            7 : }
     316            5 : 
     317           10 : async function refreshTlsTransport(settings, now, ageAtAttempt) {
     318           10 :   tlsLastRefreshAttempt = now;
     319           10 :   try {
     320           10 :     const response = await getSsmClient(settings.parameterRegion).send(
     321           10 :       new GetParameterCommand({ Name: settings.parameterName }),
     322           10 :     );
     323           10 :     const trustBundle = String(response.Parameter?.Value ?? "");
     324           10 :     const refreshed = newTlsTransport(settings.serverName, trustBundle);
     325           10 :     cachedTlsTransport = refreshed;
     326           10 :     tlsLastSuccessfulRefresh = now;
     327           10 :     return refreshed;
     328           10 :   } catch {
     329            8 :     if (cachedTlsTransport !== null && ageAtAttempt <= settings.maxStale) {
     330            1 :       console.warn("Backend TLS trust refresh failed; using bounded stale trust bundle");
     331            1 :       return cachedTlsTransport;
     332            1 :     }
     333            7 :     throw new Error("Backend TLS trust bundle is unavailable");
     334            7 :   }
     335           10 : }
     336            5 : 
     337           13 : async function getTlsTransport(now = monotonicSeconds()) {
     338           13 :   const settings = tlsSettings();
     339           13 :   const age = now - tlsLastSuccessfulRefresh;
     340           13 :   if (cachedTlsTransport !== null && age < settings.ttl) {
     341            1 :     return cachedTlsTransport;
     342            1 :   }
     343           12 :   if (
     344           13 :     cachedTlsTransport !== null &&
     345           13 :     age <= settings.maxStale &&
     346            2 :     now - tlsLastRefreshAttempt < settings.retry
     347           13 :   ) {
     348            1 :     return cachedTlsTransport;
     349            1 :   }
     350           13 :   if (tlsRefreshPromise !== null) {
     351            1 :     return tlsRefreshPromise;
     352            1 :   }
     353           10 : 
     354           10 :   const refresh = refreshTlsTransport(settings, now, age);
     355           10 :   tlsRefreshPromise = refresh;
     356           10 :   try {
     357           10 :     return await refresh;
     358           10 :   } finally {
     359           10 :     if (tlsRefreshPromise === refresh) {
     360           10 :       tlsRefreshPromise = null;
     361           10 :     }
     362           10 :   }
     363           13 : }
     364            5 : 
     365           12 : function regionalEndpointCacheTtl() {
     366           12 :   return boundedEnvFloat("REGIONAL_ENDPOINT_CACHE_TTL_SECONDS", 60, 0, 300);
     367           12 : }
     368            5 : 
     369           11 : function awsUrlSuffix() {
     370           11 :   const suffix = String(process.env.AWS_URL_SUFFIX || "").trim().toLowerCase();
     371           11 :   if (!DNS_NAME_RE.test(suffix)) {
     372            1 :     throw new Error("AWS URL suffix is not configured");
     373            1 :   }
     374           10 :   return suffix;
     375           11 : }
     376            5 : 
     377           15 : function validatedDnsName(value) {
     378           15 :   const endpoint = String(value ?? "").trim().replace(/\.+$/, "");
     379           15 :   const lower = endpoint.toLowerCase();
     380           15 :   if (
     381           15 :     !DNS_NAME_RE.test(endpoint) ||
     382            9 :     !lower.endsWith(`.elb.${awsUrlSuffix()}`)
     383           15 :   ) {
     384            8 :     throw new Error("Registered backend is invalid");
     385            8 :   }
     386            7 :   return endpoint;
     387           15 : }
     388            5 : 
     389           20 : async function validateRegionalEndpointOwnership(
     390           20 :   endpoint,
     391           20 :   region,
     392           20 :   expectedAccount,
     393           20 :   projectName,
     394           20 : ) {
     395           20 :   const client = getElbClient(region);
     396           20 :   let marker;
     397           20 :   let matched = null;
     398           20 :   for (let page = 0; page < 20; page += 1) {
     399           23 :     const response = await client.send(
     400           23 :       new DescribeLoadBalancersCommand(marker ? { Marker: marker } : {}),
     401           23 :     );
     402           23 :     for (const loadBalancer of response.LoadBalancers || []) {
     403           20 :       const dnsName = String(loadBalancer.DNSName || "").replace(/\.+$/, "");
     404           20 :       if (dnsName.toLowerCase() === endpoint.toLowerCase()) {
     405           16 :         matched = loadBalancer;
     406           16 :         break;
     407           16 :       }
     408           20 :     }
     409           23 :     if (matched !== null) {
     410           16 :       break;
     411           16 :     }
     412            7 :     marker = response.NextMarker;
     413           23 :     if (!marker) {
     414            4 :       break;
     415            4 :     }
     416           23 :   }
     417           20 : 
     418           20 :   if (matched === null) {
     419            4 :     throw new Error("Registered backend does not exist");
     420            4 :   }
     421           20 :   if (matched.Type !== "application" || matched.Scheme !== "internal") {
     422            2 :     throw new Error("Registered backend is not an internal ALB");
     423            2 :   }
     424           14 : 
     425           20 :   const arn = String(matched.LoadBalancerArn || "");
     426           20 :   const arnParts = arn.split(":", 6);
     427           20 :   if (
     428           20 :     arnParts.length !== 6 ||
     429           20 :     arnParts[2] !== "elasticloadbalancing" ||
     430           20 :     arnParts[3] !== region ||
     431           10 :     arnParts[4] !== expectedAccount
     432           20 :   ) {
     433            5 :     throw new Error("Registered backend ownership is invalid");
     434            5 :   }
     435            9 : 
     436            9 :   const tagResponse = await client.send(
     437            9 :     new DescribeTagsCommand({ ResourceArns: [arn] }),
     438            9 :   );
     439            9 :   const tags = {};
     440           20 :   for (const description of tagResponse.TagDescriptions || []) {
     441            8 :     for (const tag of description.Tags || []) {
     442           13 :       tags[String(tag.Key)] = String(tag.Value);
     443           13 :     }
     444            8 :   }
     445            9 : 
     446            9 :   const expectedCluster = `${projectName}-${region}`;
     447            9 :   const clusterMatch =
     448           20 :     tags["eks:eks-cluster-name"] === expectedCluster ||
     449           20 :     tags["elbv2.k8s.aws/cluster"] === expectedCluster;
     450           20 :   if (!clusterMatch) {
     451            3 :     throw new Error("Registered backend is not owned by the GCO cluster");
     452            3 :   }
     453            6 : 
     454            6 :   const platformMatch = tags["gco.aws/gateway"] === "gco-system/gco-gateway";
     455           20 :   if (!platformMatch) {
     456            3 :     throw new Error("Registered backend is not the GCO Gateway");
     457            3 :   }
     458           20 : }
     459            5 : 
     460           19 : async function resolveRegionalEndpoint(now = monotonicSeconds()) {
     461           19 :   const registryRegion = String(process.env.REGISTRY_REGION || "").trim();
     462           19 :   const targetRegion = String(process.env.TARGET_REGION || "").trim();
     463           19 :   const projectName = String(process.env.PROJECT_NAME || "").trim();
     464           19 :   const expectedAccount = String(process.env.AWS_ACCOUNT_ID || "").trim();
     465           19 :   if (
     466           19 :     !REGION_RE.test(registryRegion) ||
     467           19 :     !REGION_RE.test(targetRegion) ||
     468           19 :     !projectName ||
     469           13 :     !expectedAccount
     470           19 :   ) {
     471            8 :     throw new Error("Regional endpoint registry is not configured");
     472            8 :   }
     473           11 : 
     474           11 :   const cacheKey = JSON.stringify([
     475           11 :     registryRegion,
     476           11 :     targetRegion,
     477           11 :     projectName,
     478           11 :     expectedAccount,
     479           11 :   ]);
     480           11 :   const ttl = regionalEndpointCacheTtl();
     481           11 :   const cached = regionalEndpointCache.get(cacheKey);
     482           19 :   if (ttl > 0 && cached && now - cached.timestamp < ttl) {
     483            1 :     return cached.endpoint;
     484            1 :   }
     485           10 : 
     486           10 :   const parameterName = `/${projectName}/alb-hostname-${targetRegion}`;
     487           10 :   const response = await getSsmClient(registryRegion).send(
     488           10 :     new GetParameterCommand({ Name: parameterName }),
     489           10 :   );
     490           19 :   const endpoint = validatedDnsName(response.Parameter?.Value);
     491           19 :   await validateRegionalEndpointOwnership(
     492           19 :     endpoint,
     493           19 :     targetRegion,
     494           19 :     expectedAccount,
     495           19 :     projectName,
     496           19 :   );
     497            3 :   regionalEndpointCache.set(cacheKey, {
     498            3 :     timestamp: now,
     499            3 :     endpoint,
     500            3 :   });
     501            3 :   return endpoint;
     502           19 : }
     503            5 : 
     504           28 : function parseEndpoint(endpoint) {
     505           28 :   const value = String(endpoint || "").trim();
     506           28 :   const baseUrl = value.includes("://") ? value : `https://${value}`;
     507           28 :   let parsed;
     508           28 :   try {
     509           28 :     parsed = new URL(baseUrl);
     510           28 :   } catch {
     511            3 :     throw new Error("Invalid proxy endpoint");
     512            3 :   }
     513           10 :   if (
     514           28 :     parsed.protocol.toLowerCase() !== "https:" ||
     515           28 :     !parsed.hostname ||
     516           28 :     parsed.username ||
     517           28 :     parsed.password ||
     518           28 :     (parsed.port && parsed.port !== "443") ||
     519           28 :     parsed.search ||
     520           18 :     parsed.hash
     521           28 :   ) {
     522            8 :     throw new Error("Proxy endpoint must use HTTPS on port 443");
     523            8 :   }
     524           17 :   return {
     525           17 :     hostname: parsed.hostname,
     526           17 :     endpointPath: parsed.pathname.replace(/\/+$/, ""),
     527           17 :   };
     528           28 : }
     529            5 : 
     530           17 : function encodeRequestPath(path) {
     531           17 :   const repaired = path.replace(/%(?![0-9a-fA-F]{2})/g, "%25");
     532           17 :   return encodeURIComponent(repaired)
     533           17 :     .replace(/%2F/g, "/")
     534           17 :     .replace(/%3A/g, ":")
     535           17 :     .replace(/%40/g, "@")
     536           17 :     .replace(/%24/g, "$")
     537           17 :     .replace(/%26/g, "&")
     538           17 :     .replace(/%2B/g, "+")
     539           17 :     .replace(/%2C/g, ",")
     540           17 :     .replace(/%3B/g, ";")
     541           17 :     .replace(/%3D/g, "=")
     542           17 :     .replace(/%25/g, "%");
     543           17 : }
     544            5 : 
     545           60 : function pythonString(value) {
     546           60 :   if (value === null) {
     547            2 :     return "None";
     548            2 :   }
     549           60 :   if (value === true) {
     550            1 :     return "True";
     551            1 :   }
     552           60 :   if (value === false) {
     553            1 :     return "False";
     554            1 :   }
     555           22 :   return String(value);
     556           60 : }
     557            5 : 
     558           60 : function encodeQueryComponent(value) {
     559           60 :   return encodeURIComponent(pythonString(value))
     560           60 :     .replace(/[!'()*]/g, (character) =>
     561           60 :       `%${character.charCodeAt(0).toString(16).toUpperCase()}`,
     562           60 :     )
     563           60 :     .replace(/%20/g, "+");
     564           60 : }
     565            5 : 
     566          188 : function nonEmptyMapping(value) {
     567          188 :   return (
     568          188 :     value !== null &&
     569          188 :     typeof value === "object" &&
     570          188 :     !Array.isArray(value) &&
     571           87 :     Object.keys(value).length > 0
     572          188 :   );
     573          188 : }
     574            5 : 
     575           47 : function encodedQueryFromMapping(queryParameters) {
     576           47 :   const pairs = [];
     577           47 :   for (const [name, rawValue] of Object.entries(queryParameters)) {
     578           11 :     const values = Array.isArray(rawValue) ? rawValue : [rawValue];
     579           11 :     for (const value of values) {
     580           13 :       pairs.push(`${encodeQueryComponent(name)}=${encodeQueryComponent(value)}`);
     581           13 :     }
     582           11 :   }
     583           47 :   return pairs.join("&");
     584           47 : }
     585            5 : 
     586           51 : function requestQuery(event) {
     587           51 :   if (typeof event.rawQueryString === "string") {
     588            6 :     if (/[\r\n]/.test(event.rawQueryString)) {
     589            3 :       throw new PublicError(400, "Invalid query string");
     590            3 :     }
     591            2 :     return event.rawQueryString;
     592            2 :   }
     593           33 :   const parameters = nonEmptyMapping(event.multiValueQueryStringParameters)
     594           51 :     ? event.multiValueQueryStringParameters
     595           51 :     : nonEmptyMapping(event.queryStringParameters)
     596           32 :       ? event.queryStringParameters
     597           51 :       : {};
     598           51 :   return encodedQueryFromMapping(parameters);
     599           51 : }
     600            5 : 
     601           28 : function buildTarget(endpoint, requestPath, query) {
     602           28 :   const parsedEndpoint = parseEndpoint(endpoint);
     603           28 :   const encodedPath = encodeRequestPath(requestPath);
     604           28 :   const requestTarget = `${parsedEndpoint.endpointPath}${encodedPath}${
     605           28 :     query ? `?${query}` : ""
     606           28 :   }`;
     607           28 :   return {
     608           28 :     hostname: parsedEndpoint.hostname,
     609           28 :     requestTarget,
     610           28 :   };
     611           28 : }
     612            5 : 
     613           53 : function eventMethod(event) {
     614           53 :   const value = event?.httpMethod ?? event?.requestContext?.http?.method;
     615           53 :   if (typeof value !== "string") {
     616            6 :     throw new PublicError(400, "Invalid request method");
     617            6 :   }
     618           29 :   const method = value.toUpperCase();
     619           53 :   if (!ALLOWED_METHODS.has(method)) {
     620            1 :     throw new PublicError(405, "Method not allowed", {
     621            1 :       allow: "GET, HEAD, POST",
     622            1 :     });
     623            1 :   }
     624           28 :   return method;
     625           53 : }
     626            5 : 
     627           53 : function eventPath(event) {
     628           53 :   const value =
     629           53 :     typeof event.rawPath === "string"
     630           53 :       ? event.rawPath
     631           53 :       : event.path;
     632           53 :   if (
     633           53 :     typeof value !== "string" ||
     634           53 :     /[\r\n\0]/.test(value) ||
     635           53 :     !value.startsWith("/inference/") ||
     636           16 :     value.length <= "/inference/".length
     637           53 :   ) {
     638            8 :     throw new PublicError(404, "Not found");
     639            8 :   }
     640           15 :   return value;
     641           53 : }
     642            5 : 
     643           46 : function eventHeaders(event) {
     644           46 :   const merged = {};
     645           46 :   if (nonEmptyMapping(event?.headers)) {
     646           15 :     Object.assign(merged, event.headers);
     647           15 :   }
     648           46 :   if (nonEmptyMapping(event?.multiValueHeaders)) {
     649           19 :     // REST API v1 supplies repeated values here. Apply this map after the
     650           19 :     // scalar map so it wins even when API Gateway populated both forms.
     651           19 :     for (const [name, value] of Object.entries(event.multiValueHeaders)) {
     652            4 :       merged[name] = Array.isArray(value) ? value : [value];
     653            4 :     }
     654           19 :   }
     655           46 :   return merged;
     656           46 : }
     657            5 : 
     658           28 : function hasHeader(headers, expectedName) {
     659           28 :   const lowerExpected = expectedName.toLowerCase();
     660           28 :   return Object.keys(headers).some(
     661           28 :     (name) => String(name).toLowerCase() === lowerExpected,
     662           28 :   );
     663           28 : }
     664            5 : 
     665           15 : function sanitizeRequestHeaders(headers) {
     666           15 :   const sanitized = {};
     667           15 :   for (const [name, value] of Object.entries(headers)) {
     668           41 :     const normalized = String(name).trim().toLowerCase();
     669           41 :     if (
     670           41 :       value === null ||
     671           41 :       value === undefined ||
     672           41 :       HOP_BY_HOP_HEADERS.has(normalized) ||
     673           15 :       !ALLOWED_REQUEST_HEADERS.has(normalized)
     674           41 :     ) {
     675            8 :       continue;
     676            8 :     }
     677           41 :     const values = Array.isArray(value) ? value : [value];
     678           41 :     const sanitizedValues = values
     679           41 :       .filter((entry) => entry !== null && entry !== undefined)
     680           41 :       .map(String);
     681           41 :     if (sanitizedValues.length === 0) {
     682            1 :       continue;
     683            1 :     }
     684            9 :     sanitized[normalized] = Array.isArray(value)
     685           41 :       ? sanitizedValues
     686           41 :       : sanitizedValues[0];
     687           41 :   }
     688           15 :   return sanitized;
     689           15 : }
     690            5 : 
     691           14 : function buildSignedHeaders(signingKey, method, requestTarget, bodyBuffer) {
     692           14 :   const timestamp = String(Math.floor(Date.now() / 1_000));
     693           14 :   const nonce = randomBytes(16).toString("hex");
     694           14 :   const contentHash = createHash("sha256").update(bodyBuffer).digest("hex");
     695           14 :   const canonical = [
     696           14 :     "v1",
     697           14 :     timestamp,
     698           14 :     nonce,
     699           14 :     method.toUpperCase(),
     700           14 :     requestTarget,
     701           14 :     contentHash,
     702           14 :   ].join("\n");
     703           14 :   const signature = createHmac("sha256", Buffer.from(signingKey, "utf8"))
     704           14 :     .update(canonical, "utf8")
     705           14 :     .digest("hex");
     706           14 :   return {
     707           14 :     "x-gco-signature-version": "v1",
     708           14 :     "x-gco-signature": signature,
     709           14 :     "x-gco-timestamp": timestamp,
     710           14 :     "x-gco-nonce": nonce,
     711           14 :     "x-gco-content-sha256": contentHash,
     712           14 :   };
     713           14 : }
     714            5 : 
     715           14 : function outboundHeaders(headers) {
     716           14 :   const outbound = {};
     717           14 :   for (const [name, value] of Object.entries(headers)) {
     718           92 :     const normalized = String(name).toLowerCase();
     719           92 :     if (
     720           92 :       value !== null &&
     721           92 :       value !== undefined &&
     722           14 :       (ALLOWED_REQUEST_HEADERS.has(normalized) ||
     723           14 :         INTERNAL_SIGNATURE_HEADERS.has(normalized))
     724           92 :     ) {
     725           11 :       outbound[normalized] = Array.isArray(value)
     726           11 :         ? value.map(String)
     727           11 :         : String(value);
     728           11 :     }
     729           92 :   }
     730           14 :   return outbound;
     731           14 : }
     732            5 : 
     733            9 : function sanitizeResponseHeaders(headers) {
     734            9 :   const sanitized = {};
     735            9 :   for (const [name, value] of Object.entries(headers)) {
     736           19 :     const normalized = String(name).toLowerCase();
     737           19 :     if (
     738           19 :       HOP_BY_HOP_HEADERS.has(normalized) ||
     739           19 :       BLOCKED_RESPONSE_HEADERS.has(normalized) ||
     740           10 :       value === undefined
     741           19 :     ) {
     742           11 :       continue;
     743           11 :     }
     744            8 :     sanitized[normalized] = Array.isArray(value)
     745           19 :       ? value.map(String).join(", ")
     746           19 :       : String(value);
     747           19 :   }
     748            9 :   return sanitized;
     749            9 : }
     750            5 : 
     751           27 : function beginStreamingResponse(responseStream, metadata) {
     752           27 :   const encodedMetadata = Buffer.from(JSON.stringify(metadata), "utf8");
     753           27 :   const prefixLength =
     754           27 :     encodedMetadata.byteLength + RESPONSE_METADATA_DELIMITER.byteLength;
     755           27 :   if (prefixLength > MAX_RESPONSE_METADATA_PREFIX_BYTES) {
     756            2 :     throw new PublicError(502, "Upstream response metadata is too large");
     757            2 :   }
     758            7 :   responseStream.write(
     759            7 :     Buffer.concat([encodedMetadata, RESPONSE_METADATA_DELIMITER], prefixLength),
     760            7 :   );
     761            7 :   return responseStream;
     762           27 : }
     763            5 : 
     764           17 : function requestBudgetMilliseconds(context) {
     765           17 :   let available = LAMBDA_MAX_FORWARD_MS;
     766           17 :   try {
     767           17 :     if (typeof context?.getRemainingTimeInMillis === "function") {
     768            4 :       const remaining = Number(context.getRemainingTimeInMillis());
     769            4 :       if (Number.isFinite(remaining)) {
     770            3 :         available = Math.max(0, remaining - RESPONSE_HEADROOM_MS);
     771            3 :       }
     772            4 :     }
     773           17 :   } catch {
     774            1 :     available = LAMBDA_MAX_FORWARD_MS;
     775            1 :   }
     776           17 :   return Math.min(LAMBDA_MAX_FORWARD_MS, available);
     777           17 : }
     778            5 : 
     779           15 : function isTlsError(error) {
     780           15 :   const code = String(error?.code || "");
     781           15 :   return (
     782           15 :     code === "EPROTO" ||
     783           15 :     code.startsWith("ERR_TLS_") ||
     784           15 :     code.startsWith("ERR_SSL_") ||
     785           15 :     code.startsWith("CERT_") ||
     786           13 :     [
     787           13 :       "UNABLE_TO_GET_ISSUER_CERT",
     788           13 :       "UNABLE_TO_GET_ISSUER_CERT_LOCALLY",
     789           13 :       "UNABLE_TO_VERIFY_LEAF_SIGNATURE",
     790           13 :       "DEPTH_ZERO_SELF_SIGNED_CERT",
     791           13 :       "SELF_SIGNED_CERT_IN_CHAIN",
     792           13 :     ].includes(code)
     793           15 :   );
     794           15 : }
     795            5 : 
     796           17 : function transportFailureKind(error) {
     797           17 :   if (error instanceof DownstreamAbortError || error?.code === "GCO_DOWNSTREAM_ABORT") {
     798            2 :     return "downstream";
     799            2 :   }
     800           17 :   if (isTlsError(error)) {
     801            2 :     return "tls";
     802            2 :   }
     803           10 :   if (
     804            3 :     error instanceof UpstreamTimeoutError ||
     805           17 :     error?.code === "GCO_UPSTREAM_TIMEOUT" ||
     806           10 :     error?.code === "ETIMEDOUT"
     807           17 :   ) {
     808            4 :     return "timeout";
     809            4 :   }
     810            9 :   if (
     811            9 :     [
     812            9 :       "ECONNREFUSED",
     813            9 :       "ECONNRESET",
     814            9 :       "ECONNABORTED",
     815            9 :       "EHOSTUNREACH",
     816            9 :       "ENETUNREACH",
     817            9 :       "ENETDOWN",
     818            9 :       "ENOTFOUND",
     819            9 :       "EAI_AGAIN",
     820            9 :       "EPIPE",
     821           17 :     ].includes(String(error?.code || ""))
     822           17 :   ) {
     823            6 :     return "connection";
     824            6 :   }
     825            1 :   return "unexpected";
     826           17 : }
     827            5 : 
     828           10 : function publicErrorForTransportFailure(kind, attemptsMade = 0) {
     829           10 :   if (kind === "tls") {
     830            2 :     return new PublicError(502, "Backend TLS verification failed");
     831            2 :   }
     832           10 :   if (kind === "unexpected") {
     833            2 :     return new PublicError(500, "Internal server error");
     834            2 :   }
     835           10 :   if (kind === "connection") {
     836            2 :     return new PublicError(503, "Service unavailable", {}, {
     837            2 :       message: `Upstream failed after ${attemptsMade} attempt(s)`,
     838            2 :     });
     839            2 :   }
     840            1 :   if (kind === "timeout") {
     841            1 :     return new PublicError(504, "Gateway timeout", {}, {
     842            1 :       message: `Upstream failed after ${attemptsMade} attempt(s)`,
     843            1 :     });
     844            1 :   }
     845            1 :   return new PublicError(504, "Gateway timeout");
     846           10 : }
     847            5 : 
     848           14 : function openUpstream({
     849           14 :   target,
     850           14 :   method,
     851           14 :   headers,
     852           14 :   bodyBuffer,
     853           14 :   transport,
     854           14 :   idleTimeoutMs,
     855           14 :   remainingMs,
     856           14 :   signal,
     857           14 : }, requestFactory = https.request) {
     858           14 :   return new Promise((resolve, reject) => {
     859           14 :     let request;
     860           14 :     let response = null;
     861           14 :     let promiseSettled = false;
     862           14 :     let cleaned = false;
     863           14 :     let deadlineTimer;
     864           14 : 
     865           14 :     const cleanup = () => {
     866           14 :       if (cleaned) {
     867            1 :         return;
     868            1 :       }
     869           13 :       cleaned = true;
     870           13 :       clearTimeout(deadlineTimer);
     871           13 :       signal.removeEventListener("abort", onAbort);
     872           14 :       request?.setTimeout(0);
     873           14 :       response?.setTimeout(0);
     874           14 :     };
     875           14 : 
     876           14 :     const rejectBeforeResponse = (error) => {
     877           14 :       if (promiseSettled) {
     878            6 :         if (response && !response.destroyed) {
     879            1 :           response.destroy(error);
     880            1 :         }
     881            6 :         return;
     882            6 :       }
     883            8 :       promiseSettled = true;
     884            8 :       cleanup();
     885            8 :       reject(error);
     886           14 :     };
     887           14 : 
     888           14 :     const destroy = (error = new Error("Upstream request cancelled")) => {
     889            7 :       if (response && !response.destroyed) {
     890            3 :         response.destroy(error);
     891            3 :       }
     892            7 :       if (request && !request.destroyed) {
     893            7 :         request.destroy(error);
     894            7 :       }
     895           14 :     };
     896           14 : 
     897           14 :     const onAbort = () => {
     898            1 :       const error = new DownstreamAbortError();
     899            1 :       destroy(error);
     900            1 :       rejectBeforeResponse(error);
     901           14 :     };
     902           14 : 
     903           14 :     if (signal.aborted) {
     904            1 :       reject(new DownstreamAbortError());
     905            1 :       return;
     906            1 :     }
     907           13 :     signal.addEventListener("abort", onAbort, { once: true });
     908           13 : 
     909           13 :     try {
     910           13 :       request = requestFactory(
     911           13 :         {
     912           13 :           protocol: "https:",
     913           13 :           hostname: target.hostname,
     914           13 :           port: 443,
     915           13 :           method,
     916           13 :           path: target.requestTarget,
     917           13 :           headers,
     918           13 :           agent: transport.agent,
     919           13 :           servername: transport.serverName,
     920           13 :           rejectUnauthorized: true,
     921           13 :           minVersion: "TLSv1.2",
     922           13 :           checkServerIdentity: transport.verifyServerIdentity,
     923           13 :         },
     924           13 :         (incoming) => {
     925            6 :           if (promiseSettled) {
     926            1 :             incoming.destroy();
     927            1 :             return;
     928            1 :           }
     929            5 :           response = incoming;
     930            5 :           promiseSettled = true;
     931            5 :           response.setTimeout(idleTimeoutMs, () => {
     932            1 :             destroy(new UpstreamTimeoutError());
     933            5 :           });
     934            5 :           resolve({ request, response, cleanup, destroy });
     935           13 :         },
     936           13 :       );
     937           14 :     } catch (error) {
     938            1 :       rejectBeforeResponse(error);
     939            1 :       return;
     940            1 :     }
     941           12 : 
     942           12 :     request.on("error", (error) => {
     943           10 :       rejectBeforeResponse(error);
     944           12 :     });
     945           12 :     request.once("upgrade", (_incoming, socket) => {
     946            1 :       socket.destroy();
     947            1 :       rejectBeforeResponse(new Error("Unexpected protocol upgrade"));
     948           12 :     });
     949           12 :     request.setTimeout(idleTimeoutMs, () => {
     950            1 :       destroy(new UpstreamTimeoutError());
     951           12 :     });
     952           12 :     deadlineTimer = setTimeout(() => {
     953            2 :       destroy(new UpstreamTimeoutError());
     954           12 :     }, Math.max(1, Math.ceil(remainingMs)));
     955           12 : 
     956           12 :     try {
     957           14 :       request.end(bodyBuffer.length > 0 ? bodyBuffer : undefined);
     958           14 :     } catch (error) {
     959            1 :       destroy(error);
     960            1 :       rejectBeforeResponse(error);
     961            1 :     }
     962           14 :   });
     963           14 : }
     964            5 : 
     965            4 : function sleep(milliseconds, signal) {
     966            4 :   if (milliseconds <= 0) {
     967            1 :     return Promise.resolve();
     968            1 :   }
     969            3 :   return new Promise((resolve, reject) => {
     970            3 :     const timer = setTimeout(() => {
     971            1 :       signal.removeEventListener("abort", onAbort);
     972            1 :       resolve();
     973            3 :     }, milliseconds);
     974            3 :     const onAbort = () => {
     975            1 :       clearTimeout(timer);
     976            1 :       reject(new DownstreamAbortError());
     977            3 :     };
     978            3 :     if (signal.aborted) {
     979            1 :       clearTimeout(timer);
     980            1 :       reject(new DownstreamAbortError());
     981            1 :       return;
     982            1 :     }
     983            2 :     signal.addEventListener("abort", onAbort, { once: true });
     984            3 :   });
     985            4 : }
     986            5 : 
     987            7 : async function streamFinalResponse(resource, responseStream, state, signal) {
     988            7 :   let output;
     989            7 :   try {
     990            7 :     output = beginStreamingResponse(responseStream, {
     991            7 :       statusCode: resource.response.statusCode || 502,
     992            7 :       headers: sanitizeResponseHeaders(resource.response.headers),
     993            7 :     });
     994            7 :     state.started = true;
     995            7 :   } catch (error) {
     996            2 :     resource.cleanup();
     997            2 :     resource.destroy();
     998            2 :     if (error instanceof PublicError) {
     999            2 :       throw error;
    1000            2 :     }
    1001            2 :     throw new PublicError(500, "Internal server error");
    1002            2 :   }
    1003            5 : 
    1004            5 :   try {
    1005            5 :     await pipeline(resource.response, output, { signal });
    1006            7 :   } catch {
    1007            1 :     resource.destroy();
    1008            1 :     if (!signal.aborted) {
    1009            1 :       console.warn("Upstream response stream terminated before completion");
    1010            1 :     }
    1011            1 :   } finally {
    1012            1 :     resource.cleanup();
    1013            1 :   }
    1014            7 : }
    1015            5 : 
    1016            5 : const FORWARD_OPERATIONS = Object.freeze({
    1017            5 :   openUpstream,
    1018            5 :   sleep,
    1019            5 :   streamFinalResponse,
    1020            5 : });
    1021            5 : 
    1022           23 : async function forwardRequest({
    1023           23 :   target,
    1024           23 :   method,
    1025           23 :   headers,
    1026           23 :   bodyBuffer,
    1027           23 :   transport,
    1028           23 :   timeoutMs,
    1029           23 :   idleTimeoutMs,
    1030           23 :   responseStream,
    1031           23 :   responseState,
    1032           23 :   signal,
    1033           23 :   resignHeaders = null,
    1034           23 : }, operations = FORWARD_OPERATIONS) {
    1035           23 :   const maxAttempts = RETRYABLE_METHODS.has(method) ? MAX_RETRIES : 1;
    1036           23 :   const deadline = monotonicMilliseconds() + Math.max(timeoutMs, 0);
    1037           23 :   let lastFailureKind = null;
    1038           23 :   let attemptsMade = 0;
    1039           23 : 
    1040           23 :   for (let attempt = 0; attempt < maxAttempts; attempt += 1) {
    1041           49 :     const remaining = deadline - monotonicMilliseconds();
    1042           49 :     if (remaining <= 0) {
    1043            1 :       break;
    1044            1 :     }
    1045           48 :     attemptsMade = attempt + 1;
    1046           49 :     if (attempt > 0 && resignHeaders !== null) {
    1047            2 :       // The backend's HMAC envelope is single-use: its nonce is remembered
    1048            2 :       // for the signature window and a replay is refused with 403. A retry
    1049            2 :       // that re-sent the first attempt's envelope turned every retryable
    1050            2 :       // 5xx into a terminal 403, so each attempt signs afresh.
    1051            2 :       headers = { ...headers, ...resignHeaders() };
    1052            2 :     }
    1053           48 : 
    1054           48 :     let resource;
    1055           48 :     try {
    1056           48 :       resource = await operations.openUpstream({
    1057           48 :         target,
    1058           48 :         method,
    1059           48 :         headers,
    1060           48 :         bodyBuffer,
    1061           48 :         transport,
    1062           48 :         idleTimeoutMs,
    1063           48 :         remainingMs: remaining,
    1064           48 :         signal,
    1065           48 :       });
    1066           49 :     } catch (error) {
    1067           12 :       const kind = transportFailureKind(error);
    1068           12 :       if (kind === "downstream") {
    1069            1 :         throw error;
    1070            1 :       }
    1071           12 :       if (kind === "tls" || kind === "unexpected") {
    1072            2 :         throw publicErrorForTransportFailure(kind, attemptsMade);
    1073            2 :       }
    1074            9 :       lastFailureKind = kind;
    1075            9 :       console.warn(
    1076            9 :         `Upstream ${method} failed on attempt ${attempt + 1}/${maxAttempts}`,
    1077            9 :       );
    1078            9 : 
    1079           12 :       if (attempt >= maxAttempts - 1) {
    1080            2 :         break;
    1081            2 :       }
    1082            7 :       const backoffMs = RETRY_BACKOFF_BASE_SECONDS * 1_000 * 2 ** attempt;
    1083           12 :       if (deadline - monotonicMilliseconds() <= backoffMs) {
    1084            1 :         break;
    1085            1 :       }
    1086            6 :       await operations.sleep(backoffMs, signal);
    1087            6 :       continue;
    1088            6 :     }
    1089           36 : 
    1090           49 :     const statusCode = resource.response.statusCode || 502;
    1091           49 :     if (RETRYABLE_STATUS_CODES.has(statusCode) && attempt < maxAttempts - 1) {
    1092           21 :       const backoffMs = RETRY_BACKOFF_BASE_SECONDS * 1_000 * 2 ** attempt;
    1093           21 :       if (deadline - monotonicMilliseconds() > backoffMs) {
    1094           20 :         console.warn(
    1095           20 :           `Retryable upstream status ${statusCode} on attempt ${attempt + 1}/${maxAttempts} for ${method}`,
    1096           20 :         );
    1097           20 :         resource.cleanup();
    1098           20 :         resource.destroy();
    1099           20 :         await operations.sleep(backoffMs, signal);
    1100           20 :         continue;
    1101           20 :       }
    1102           21 :     }
    1103           16 : 
    1104           16 :     await operations.streamFinalResponse(
    1105           16 :       resource,
    1106           16 :       responseStream,
    1107           16 :       responseState,
    1108           16 :       signal,
    1109           16 :     );
    1110           16 :     return;
    1111           16 :   }
    1112            4 : 
    1113            4 :   throw publicErrorForTransportFailure(lastFailureKind, attemptsMade);
    1114           23 : }
    1115            5 : 
    1116           17 : async function sendJsonError(
    1117           17 :   responseStream,
    1118           17 :   statusCode,
    1119           17 :   message,
    1120           17 :   headers = {},
    1121           17 :   details = {},
    1122           17 : ) {
    1123           17 :   if (responseStream.destroyed) {
    1124            1 :     return;
    1125            1 :   }
    1126            3 :   const body = JSON.stringify({ error: message, ...details });
    1127            3 :   try {
    1128            3 :     const output = beginStreamingResponse(responseStream, {
    1129            3 :       statusCode,
    1130            3 :       headers: {
    1131            3 :         "content-type": "application/json",
    1132            3 :         ...headers,
    1133            3 :       },
    1134            3 :     });
    1135            3 :     output.end(body);
    1136            3 :     await finished(output, { cleanup: true });
    1137           17 :   } catch {
    1138            2 :     // The caller may have disconnected before the bounded error could be sent.
    1139            2 :   }
    1140           17 : }
    1141            5 : 
    1142           37 : function routingMode() {
    1143           37 :   const mode = String(process.env.ROUTING_MODE || "").trim();
    1144           37 :   if (mode !== "global" && mode !== "regional") {
    1145            5 :     throw new PublicError(503, "Backend routing is temporarily unavailable");
    1146            5 :   }
    1147           14 :   return mode;
    1148           37 : }
    1149            5 : 
    1150           48 : function preflightRequest(event) {
    1151           48 :   if (event?.isBase64Encoded) {
    1152            1 :     throw new PublicError(415, "Base64-encoded request bodies are not supported");
    1153            1 :   }
    1154           16 :   const method = eventMethod(event);
    1155           16 :   const path = eventPath(event);
    1156           16 :   const query = requestQuery(event);
    1157           16 :   const incomingHeaders = eventHeaders(event);
    1158           48 :   const body = event?.body ?? "";
    1159           48 :   if (typeof body !== "string") {
    1160            6 :     throw new PublicError(400, "Invalid request body");
    1161            6 :   }
    1162           12 :   const bodyBuffer = Buffer.from(body, "utf8");
    1163           48 :   if (bodyBuffer.byteLength > MAX_REQUEST_BODY_BYTES) {
    1164            5 :     throw new PublicError(
    1165            5 :       413,
    1166            5 :       `Request body exceeds maximum size of ${MAX_REQUEST_BODY_BYTES} bytes`,
    1167            5 :     );
    1168            5 :   }
    1169            7 :   const mode = routingMode();
    1170            7 : 
    1171           48 :   if (mode === "global" && hasHeader(incomingHeaders, "x-gco-target-region")) {
    1172            1 :     throw new PublicError(
    1173            1 :       400,
    1174            1 :       "X-GCO-Target-Region is not supported by the global endpoint; use the target region's regional API endpoint if authorized for direct access",
    1175            1 :     );
    1176            1 :   }
    1177            5 : 
    1178            5 :   return { method, path, query, incomingHeaders, bodyBuffer, mode };
    1179           48 : }
    1180            5 : 
    1181            5 : const PRODUCTION_DEPENDENCIES = Object.freeze({
    1182            5 :   getSecretToken,
    1183            5 :   resolveRegionalEndpoint,
    1184            5 :   getTlsTransport,
    1185            5 :   forwardRequest,
    1186            5 : });
    1187            5 : 
    1188           22 : async function streamingHandler(
    1189           22 :   event,
    1190           22 :   responseStream,
    1191           22 :   context,
    1192           22 :   dependencies = PRODUCTION_DEPENDENCIES,
    1193           22 : ) {
    1194           22 :   const responseState = { started: false };
    1195           22 :   const downstreamAbort = new AbortController();
    1196           22 :   const onDownstreamClose = () => {
    1197           19 :     if (!responseStream.writableFinished && !responseStream.writableEnded) {
    1198            1 :       downstreamAbort.abort(new DownstreamAbortError());
    1199            1 :     }
    1200           22 :   };
    1201           22 :   const onDownstreamError = () => {
    1202            1 :     downstreamAbort.abort(new DownstreamAbortError());
    1203           22 :   };
    1204           22 :   responseStream.once("close", onDownstreamClose);
    1205           22 :   responseStream.once("error", onDownstreamError);
    1206           22 : 
    1207           22 :   try {
    1208           22 :     const { method, path, query, incomingHeaders, bodyBuffer, mode } =
    1209           22 :       preflightRequest(event);
    1210           22 : 
    1211           22 :     let signingKey;
    1212           22 :     try {
    1213           22 :       signingKey = await dependencies.getSecretToken();
    1214           22 :     } catch {
    1215            1 :       throw new PublicError(
    1216            1 :         503,
    1217            1 :         "Backend authentication is temporarily unavailable",
    1218            1 :       );
    1219            1 :     }
    1220           17 : 
    1221           17 :     let endpoint;
    1222           17 :     try {
    1223           22 :       if (mode === "global") {
    1224           14 :         endpoint = process.env.GLOBAL_ACCELERATOR_ENDPOINT;
    1225           14 :         if (!endpoint) {
    1226            1 :           throw new Error("Global endpoint is not configured");
    1227            1 :         }
    1228           22 :       } else {
    1229            3 :         endpoint = await dependencies.resolveRegionalEndpoint();
    1230            2 :       }
    1231           22 :     } catch {
    1232            2 :       if (mode === "regional") {
    1233            1 :         console.warn("Regional backend resolution failed");
    1234            1 :         throw new PublicError(502, "Regional backend is temporarily unavailable");
    1235            1 :       }
    1236            1 :       throw new PublicError(
    1237            1 :         503,
    1238            1 :         "Global backend routing is temporarily unavailable",
    1239            1 :       );
    1240            1 :     }
    1241           15 : 
    1242           15 :     let target;
    1243           15 :     try {
    1244           15 :       target = buildTarget(endpoint, path, query);
    1245           22 :     } catch {
    1246            2 :       throw new PublicError(
    1247            2 :         mode === "global" ? 503 : 502,
    1248            2 :         mode === "global"
    1249            2 :           ? "Global backend routing is temporarily unavailable"
    1250            2 :           : "Regional backend is temporarily unavailable",
    1251            2 :       );
    1252            2 :     }
    1253           13 : 
    1254           13 :     let transport;
    1255           13 :     try {
    1256           13 :       transport = await dependencies.getTlsTransport();
    1257           22 :     } catch {
    1258            1 :       throw new PublicError(503, "Backend trust is temporarily unavailable");
    1259            1 :     }
    1260           12 : 
    1261           12 :     const requestHeaders = sanitizeRequestHeaders(incomingHeaders);
    1262           12 :     const signAttempt = () =>
    1263           12 :       buildSignedHeaders(signingKey, method, target.requestTarget, bodyBuffer);
    1264           12 :     Object.assign(requestHeaders, signAttempt());
    1265           12 :     const timeoutMs = requestBudgetMilliseconds(context);
    1266           22 :     if (timeoutMs <= 0) {
    1267            1 :       throw new PublicError(504, "Gateway timeout");
    1268            1 :     }
    1269           11 : 
    1270           11 :     await dependencies.forwardRequest({
    1271           11 :       target,
    1272           11 :       method,
    1273           11 :       headers: outboundHeaders(requestHeaders),
    1274           11 :       bodyBuffer,
    1275           11 :       transport,
    1276           11 :       timeoutMs,
    1277           11 :       idleTimeoutMs:
    1278           22 :         mode === "global" ? GLOBAL_IDLE_TIMEOUT_MS : REGIONAL_IDLE_TIMEOUT_MS,
    1279           22 :       responseStream,
    1280           22 :       responseState,
    1281           22 :       signal: downstreamAbort.signal,
    1282           22 :       resignHeaders: signAttempt,
    1283           22 :     });
    1284           22 :   } catch (error) {
    1285           13 :     if (
    1286           13 :       downstreamAbort.signal.aborted ||
    1287           13 :       error instanceof DownstreamAbortError ||
    1288           10 :       responseState.started
    1289           13 :     ) {
    1290            4 :       return;
    1291            4 :     }
    1292           13 :     if (error instanceof PublicError) {
    1293            8 :       await sendJsonError(
    1294            8 :         responseStream,
    1295            8 :         error.statusCode,
    1296            8 :         error.publicMessage,
    1297            8 :         error.headers,
    1298            8 :         error.details,
    1299            8 :       );
    1300            8 :       return;
    1301            8 :     }
    1302            1 :     await sendJsonError(responseStream, 500, "Internal server error");
    1303           22 :   } finally {
    1304           22 :     responseStream.removeListener("close", onDownstreamClose);
    1305           22 :     responseStream.removeListener("error", onDownstreamError);
    1306           22 :   }
    1307           22 : }
    1308            5 : 
    1309           70 : function resetRuntimeStateForTest() {
    1310           70 :   secretsClients.clear();
    1311           70 :   ssmClients.clear();
    1312           70 :   elbClients.clear();
    1313           70 :   regionalEndpointCache.clear();
    1314           70 :   cachedSecret = null;
    1315           70 :   secretLastSuccessfulRefresh = 0;
    1316           70 :   secretLastRefreshAttempt = 0;
    1317           70 :   secretRefreshPromise = null;
    1318           70 :   cachedTlsTransport = null;
    1319           70 :   tlsLastSuccessfulRefresh = 0;
    1320           70 :   tlsLastRefreshAttempt = 0;
    1321           70 :   tlsRefreshPromise = null;
    1322           70 : }
    1323            5 : 
    1324            5 : export const __test = Object.freeze({
    1325            5 :   MAX_REQUEST_BODY_BYTES,
    1326            5 :   PublicError,
    1327            5 :   UpstreamTimeoutError,
    1328            5 :   DownstreamAbortError,
    1329            5 :   boundedEnvFloat,
    1330            5 :   boundedEnvInt,
    1331            5 :   monotonicSeconds,
    1332            5 :   monotonicMilliseconds,
    1333            5 :   secretRegion,
    1334            5 :   getSecretsClient,
    1335            5 :   getSsmClient,
    1336            5 :   getElbClient,
    1337            5 :   refreshSecret,
    1338            5 :   getSecretToken,
    1339            5 :   tlsSettings,
    1340            5 :   validateTrustBundle,
    1341            5 :   newTlsTransport,
    1342            5 :   refreshTlsTransport,
    1343            5 :   getTlsTransport,
    1344            5 :   regionalEndpointCacheTtl,
    1345            5 :   awsUrlSuffix,
    1346            5 :   validatedDnsName,
    1347            5 :   validateRegionalEndpointOwnership,
    1348            5 :   resolveRegionalEndpoint,
    1349            5 :   parseEndpoint,
    1350            5 :   encodeRequestPath,
    1351            5 :   pythonString,
    1352            5 :   encodeQueryComponent,
    1353            5 :   nonEmptyMapping,
    1354            5 :   encodedQueryFromMapping,
    1355            5 :   eventMethod,
    1356            5 :   eventPath,
    1357            5 :   hasHeader,
    1358            5 :   isTlsError,
    1359            5 :   openUpstream,
    1360            5 :   sleep,
    1361            5 :   forwardRequest,
    1362            5 :   routingMode,
    1363            5 :   resetRuntimeStateForTest,
    1364            5 :   secretsClients,
    1365            5 :   ssmClients,
    1366            5 :   elbClients,
    1367            5 :   regionalEndpointCache,
    1368            5 :   preflightRequest,
    1369            5 :   requestQuery,
    1370            5 :   buildTarget,
    1371            5 :   eventHeaders,
    1372            5 :   sanitizeRequestHeaders,
    1373            5 :   outboundHeaders,
    1374            5 :   sanitizeResponseHeaders,
    1375            5 :   beginStreamingResponse,
    1376            5 :   buildSignedHeaders,
    1377            5 :   requestBudgetMilliseconds,
    1378            5 :   transportFailureKind,
    1379            5 :   publicErrorForTransportFailure,
    1380            5 :   streamFinalResponse,
    1381            5 :   sendJsonError,
    1382            5 :   streamingHandler,
    1383            5 : });
    1384            5 : 
    1385            5 : export const handler = awslambda.streamifyResponse(streamingHandler);
        

Generated by: LCOV version 2.0-1