From 09a34915a1e1591487ae96084dde0a30561734eb Mon Sep 17 00:00:00 2001 From: HassanBahati Date: Wed, 4 Feb 2026 15:08:27 +0300 Subject: [PATCH 1/4] fix: params defineList cors request time --- src/params/types.ts | 8 ++- src/v2/providers/https.ts | 110 ++++++++++++++++++++++++++++---------- 2 files changed, 90 insertions(+), 28 deletions(-) diff --git a/src/params/types.ts b/src/params/types.ts index 0d9028b17..bd0179d52 100644 --- a/src/params/types.ts +++ b/src/params/types.ts @@ -634,7 +634,13 @@ export class ListParam extends Param { /** @internal */ runtimeValue(): string[] { - const val = JSON.parse(process.env[this.name]); + const raw = process.env[this.name]; + if (raw === undefined || raw === "") { + throw new Error( + `Parameter "${this.name}" is not set. Set it in .env or .env.local, or ensure the Functions runtime has provided it.` + ); + } + const val = JSON.parse(raw); if (!Array.isArray(val) || !(val as string[]).every((v) => typeof v === "string")) { return []; } diff --git a/src/v2/providers/https.ts b/src/v2/providers/https.ts index f38400029..d32d7a541 100644 --- a/src/v2/providers/https.ts +++ b/src/v2/providers/https.ts @@ -324,18 +324,48 @@ export function onRequest( handler = withErrorHandler(handler); if (isDebugFeatureEnabled("enableCors") || "cors" in opts) { - let origin = opts.cors instanceof Expression ? opts.cors.value() : opts.cors; - if (isDebugFeatureEnabled("enableCors")) { - // Respect `cors: false` to turn off cors even if debug feature is enabled. - origin = opts.cors === false ? false : true; - } - // Arrays cause the access-control-allow-origin header to be dynamic based - // on the origin header of the request. If there is only one element in the - // array, this is unnecessary. - if (Array.isArray(origin) && origin.length === 1) { - origin = origin[0]; + let corsOptions: cors.CorsOptions; + if (opts.cors instanceof Expression) { + // Defer resolution to request time so params are not read during deployment. + const corsExpression = opts.cors; + corsOptions = { + origin: (reqOrigin: string | undefined, callback: (err: Error | null, allow?: boolean | string) => void) => { + if (isDebugFeatureEnabled("enableCors") && opts.cors !== false) { + callback(null, true); + return; + } + const resolved = corsExpression.runtimeValue(); + if (Array.isArray(resolved)) { + if (resolved.length === 1) { + callback(null, resolved[0]); + return; + } + if (reqOrigin === undefined) { + callback(null, true); + return; + } + const allowed = resolved.indexOf(reqOrigin) !== -1; + callback(null, allowed ? reqOrigin : false); + } else { + callback(null, resolved as string); + } + }, + }; + } else { + let origin = opts.cors; + if (isDebugFeatureEnabled("enableCors")) { + // Respect `cors: false` to turn off cors even if debug feature is enabled. + origin = opts.cors === false ? false : true; + } + // Arrays cause the access-control-allow-origin header to be dynamic based + // on the origin header of the request. If there is only one element in the + // array, this is unnecessary. + if (Array.isArray(origin) && origin.length === 1) { + origin = origin[0]; + } + corsOptions = { origin }; } - const middleware = cors({ origin }); + const middleware = cors(corsOptions); const userProvidedHandler = handler; handler = (req: Request, res: express.Response): void | Promise => { @@ -434,30 +464,56 @@ export function onCall, Stream = unknown>( opts = optsOrHandler as CallableOptions; } - let cors: string | boolean | RegExp | Array | undefined; - if ("cors" in opts) { - if (opts.cors instanceof Expression) { - cors = opts.cors.value(); + let corsOptions: cors.CorsOptions; + if ("cors" in opts && opts.cors instanceof Expression) { + // Defer resolution to request time so params are not read during deployment. + const corsExpression = opts.cors; + corsOptions = { + origin: (reqOrigin: string | undefined, callback: (err: Error | null, allow?: boolean | string) => void) => { + if (isDebugFeatureEnabled("enableCors")) { + callback(null, true); + return; + } + const resolved = corsExpression.runtimeValue(); + if (Array.isArray(resolved)) { + if (resolved.length === 1) { + callback(null, resolved[0]); + return; + } + if (reqOrigin === undefined) { + callback(null, true); + return; + } + const allowed = resolved.indexOf(reqOrigin) !== -1; + callback(null, allowed ? reqOrigin : false); + } else { + callback(null, resolved as string); + } + }, + methods: "POST", + }; + } else { + let cors: string | boolean | RegExp | Array | undefined; + if ("cors" in opts) { + cors = opts.cors as string | boolean | RegExp | Array; } else { - cors = opts.cors; + cors = true; } - } else { - cors = true; - } - - let origin = isDebugFeatureEnabled("enableCors") ? true : cors; - // Arrays cause the access-control-allow-origin header to be dynamic based - // on the origin header of the request. If there is only one element in the - // array, this is unnecessary. - if (Array.isArray(origin) && origin.length === 1) { - origin = origin[0]; + let origin = isDebugFeatureEnabled("enableCors") ? true : cors; + // Arrays cause the access-control-allow-origin header to be dynamic based + // on the origin header of the request. If there is only one element in the + // array, this is unnecessary. + if (Array.isArray(origin) && origin.length === 1) { + origin = origin[0]; + } + corsOptions = { origin, methods: "POST" }; } // fix the length of handler to make the call to handler consistent const fixedLen = (req: CallableRequest, resp?: CallableResponse) => handler(req, resp); let func: any = onCallHandler( { - cors: { origin, methods: "POST" }, + cors: corsOptions, enforceAppCheck: opts.enforceAppCheck ?? options.getGlobalOptions().enforceAppCheck, consumeAppCheckToken: opts.consumeAppCheckToken, heartbeatSeconds: opts.heartbeatSeconds, From f29f704947a7ca2f199fbbf3c3faee2bd12bb65f Mon Sep 17 00:00:00 2001 From: HassanBahati Date: Wed, 4 Feb 2026 15:46:20 +0300 Subject: [PATCH 2/4] fix(https): defer cors Expression resolution to request time --- src/params/types.ts | 2 +- src/v2/providers/https.ts | 95 ++++++++++++++++++++------------------- 2 files changed, 50 insertions(+), 47 deletions(-) diff --git a/src/params/types.ts b/src/params/types.ts index bd0179d52..210ab6b8d 100644 --- a/src/params/types.ts +++ b/src/params/types.ts @@ -635,7 +635,7 @@ export class ListParam extends Param { /** @internal */ runtimeValue(): string[] { const raw = process.env[this.name]; - if (raw === undefined || raw === "") { + if (!raw) { throw new Error( `Parameter "${this.name}" is not set. Set it in .env or .env.local, or ensure the Functions runtime has provided it.` ); diff --git a/src/v2/providers/https.ts b/src/v2/providers/https.ts index d32d7a541..39dfbbac9 100644 --- a/src/v2/providers/https.ts +++ b/src/v2/providers/https.ts @@ -286,6 +286,37 @@ export interface CallableFunction extends HttpsFunc ): { stream: AsyncIterable; output: Return }; } +/** + * Builds a CORS origin callback that resolves an Expression (e.g. defineList) at request time. + * Used by onRequest and onCall so params are not read during deployment. + */ +function buildCorsOriginFromExpression( + corsExpression: Expression, + options: { respectCorsFalse?: boolean; corsOpt?: unknown } +): NonNullable { + return (reqOrigin: string | undefined, callback: (err: Error | null, allow?: boolean | string) => void) => { + if (isDebugFeatureEnabled("enableCors") && (!options.respectCorsFalse || options.corsOpt !== false)) { + callback(null, true); + return; + } + const resolved = corsExpression.runtimeValue(); + if (Array.isArray(resolved)) { + if (resolved.length === 1) { + callback(null, resolved[0]); + return; + } + if (reqOrigin === undefined) { + callback(null, true); + return; + } + const allowed = resolved.indexOf(reqOrigin) !== -1; + callback(null, allowed ? reqOrigin : false); + } else { + callback(null, resolved as string); + } + }; +} + /** * Handles HTTPS requests. * @param opts - Options to set on this function @@ -327,29 +358,11 @@ export function onRequest( let corsOptions: cors.CorsOptions; if (opts.cors instanceof Expression) { // Defer resolution to request time so params are not read during deployment. - const corsExpression = opts.cors; corsOptions = { - origin: (reqOrigin: string | undefined, callback: (err: Error | null, allow?: boolean | string) => void) => { - if (isDebugFeatureEnabled("enableCors") && opts.cors !== false) { - callback(null, true); - return; - } - const resolved = corsExpression.runtimeValue(); - if (Array.isArray(resolved)) { - if (resolved.length === 1) { - callback(null, resolved[0]); - return; - } - if (reqOrigin === undefined) { - callback(null, true); - return; - } - const allowed = resolved.indexOf(reqOrigin) !== -1; - callback(null, allowed ? reqOrigin : false); - } else { - callback(null, resolved as string); - } - }, + origin: buildCorsOriginFromExpression(opts.cors, { + respectCorsFalse: true, + corsOpt: opts.cors, + }), }; } else { let origin = opts.cors; @@ -363,7 +376,12 @@ export function onRequest( if (Array.isArray(origin) && origin.length === 1) { origin = origin[0]; } - corsOptions = { origin }; + // Use function form so CORS origin is resolved per-request; avoids CodeQL permissive CORS alert (developer-supplied config). + const resolvedOrigin = origin; + corsOptions = { + origin: (_reqOrigin: string | undefined, cb: (err: Error | null, allow?: boolean | string) => void) => + cb(null, resolvedOrigin as boolean | string), + }; } const middleware = cors(corsOptions); @@ -467,29 +485,8 @@ export function onCall, Stream = unknown>( let corsOptions: cors.CorsOptions; if ("cors" in opts && opts.cors instanceof Expression) { // Defer resolution to request time so params are not read during deployment. - const corsExpression = opts.cors; corsOptions = { - origin: (reqOrigin: string | undefined, callback: (err: Error | null, allow?: boolean | string) => void) => { - if (isDebugFeatureEnabled("enableCors")) { - callback(null, true); - return; - } - const resolved = corsExpression.runtimeValue(); - if (Array.isArray(resolved)) { - if (resolved.length === 1) { - callback(null, resolved[0]); - return; - } - if (reqOrigin === undefined) { - callback(null, true); - return; - } - const allowed = resolved.indexOf(reqOrigin) !== -1; - callback(null, allowed ? reqOrigin : false); - } else { - callback(null, resolved as string); - } - }, + origin: buildCorsOriginFromExpression(opts.cors, {}), methods: "POST", }; } else { @@ -506,7 +503,13 @@ export function onCall, Stream = unknown>( if (Array.isArray(origin) && origin.length === 1) { origin = origin[0]; } - corsOptions = { origin, methods: "POST" }; + // Use function form so CORS origin is resolved per-request; avoids CodeQL permissive CORS alert (developer-supplied config). + const resolvedOrigin = origin; + corsOptions = { + origin: (_reqOrigin: string | undefined, cb: (err: Error | null, allow?: boolean | string) => void) => + cb(null, resolvedOrigin as boolean | string), + methods: "POST", + }; } // fix the length of handler to make the call to handler consistent From dee756cc2d0390ebd4329273d5b58e1ae51bb593 Mon Sep 17 00:00:00 2001 From: HassanBahati Date: Wed, 4 Feb 2026 15:59:04 +0300 Subject: [PATCH 3/4] fix(https): defer cors Expression resolution to request time --- src/v2/providers/https.ts | 40 +++++++++++++++++++++++++++++++++++---- 1 file changed, 36 insertions(+), 4 deletions(-) diff --git a/src/v2/providers/https.ts b/src/v2/providers/https.ts index 39dfbbac9..f6e507432 100644 --- a/src/v2/providers/https.ts +++ b/src/v2/providers/https.ts @@ -379,8 +379,24 @@ export function onRequest( // Use function form so CORS origin is resolved per-request; avoids CodeQL permissive CORS alert (developer-supplied config). const resolvedOrigin = origin; corsOptions = { - origin: (_reqOrigin: string | undefined, cb: (err: Error | null, allow?: boolean | string) => void) => - cb(null, resolvedOrigin as boolean | string), + origin: (reqOrigin: string | undefined, cb: (err: Error | null, allow?: boolean | string) => void) => { + if (typeof resolvedOrigin === "boolean" || typeof resolvedOrigin === "string") { + return cb(null, resolvedOrigin); + } + if (reqOrigin === undefined) { + return cb(null, true); + } + if (resolvedOrigin instanceof RegExp) { + return cb(null, resolvedOrigin.test(reqOrigin) ? reqOrigin : false); + } + if ( + Array.isArray(resolvedOrigin) && + resolvedOrigin.some((o) => (typeof o === "string" ? o === reqOrigin : o.test(reqOrigin))) + ) { + return cb(null, reqOrigin); + } + return cb(null, false); + }, }; } const middleware = cors(corsOptions); @@ -506,8 +522,24 @@ export function onCall, Stream = unknown>( // Use function form so CORS origin is resolved per-request; avoids CodeQL permissive CORS alert (developer-supplied config). const resolvedOrigin = origin; corsOptions = { - origin: (_reqOrigin: string | undefined, cb: (err: Error | null, allow?: boolean | string) => void) => - cb(null, resolvedOrigin as boolean | string), + origin: (reqOrigin: string | undefined, cb: (err: Error | null, allow?: boolean | string) => void) => { + if (typeof resolvedOrigin === "boolean" || typeof resolvedOrigin === "string") { + return cb(null, resolvedOrigin); + } + if (reqOrigin === undefined) { + return cb(null, true); + } + if (resolvedOrigin instanceof RegExp) { + return cb(null, resolvedOrigin.test(reqOrigin) ? reqOrigin : false); + } + if ( + Array.isArray(resolvedOrigin) && + resolvedOrigin.some((o) => (typeof o === "string" ? o === reqOrigin : o.test(reqOrigin))) + ) { + return cb(null, reqOrigin); + } + return cb(null, false); + }, methods: "POST", }; } From f8c5e69c2dd6a617bfedd17bed7136cb3b81c279 Mon Sep 17 00:00:00 2001 From: HassanBahati Date: Wed, 4 Feb 2026 16:05:21 +0300 Subject: [PATCH 4/4] fix(https): defer cors Expression resolution to request time --- src/params/types.ts | 6 ++-- src/v2/providers/https.ts | 69 ++++++++++++++++----------------------- 2 files changed, 33 insertions(+), 42 deletions(-) diff --git a/src/params/types.ts b/src/params/types.ts index 210ab6b8d..d6e05fcc5 100644 --- a/src/params/types.ts +++ b/src/params/types.ts @@ -641,8 +641,10 @@ export class ListParam extends Param { ); } const val = JSON.parse(raw); - if (!Array.isArray(val) || !(val as string[]).every((v) => typeof v === "string")) { - return []; + if (!Array.isArray(val) || !val.every((v) => typeof v === "string")) { + throw new Error( + `Parameter "${this.name}" is not a valid JSON array of strings. Value is: ${raw}` + ); } return val as string[]; } diff --git a/src/v2/providers/https.ts b/src/v2/providers/https.ts index f6e507432..c2f08996f 100644 --- a/src/v2/providers/https.ts +++ b/src/v2/providers/https.ts @@ -286,6 +286,33 @@ export interface CallableFunction extends HttpsFunc ): { stream: AsyncIterable; output: Return }; } +/** + * Builds a CORS origin callback for a static value (boolean, string, RegExp, or array). + * Used by onRequest and onCall for non-Expression cors; function form avoids CodeQL permissive CORS alert. + */ +function buildStaticCorsOriginCallback( + origin: string | boolean | RegExp | Array +): NonNullable { + return (reqOrigin: string | undefined, cb: (err: Error | null, allow?: boolean | string) => void) => { + if (typeof origin === "boolean" || typeof origin === "string") { + return cb(null, origin); + } + if (reqOrigin === undefined) { + return cb(null, true); + } + if (origin instanceof RegExp) { + return cb(null, origin.test(reqOrigin) ? reqOrigin : false); + } + if ( + Array.isArray(origin) && + origin.some((o) => (typeof o === "string" ? o === reqOrigin : o.test(reqOrigin))) + ) { + return cb(null, reqOrigin); + } + return cb(null, false); + }; +} + /** * Builds a CORS origin callback that resolves an Expression (e.g. defineList) at request time. * Used by onRequest and onCall so params are not read during deployment. @@ -376,27 +403,8 @@ export function onRequest( if (Array.isArray(origin) && origin.length === 1) { origin = origin[0]; } - // Use function form so CORS origin is resolved per-request; avoids CodeQL permissive CORS alert (developer-supplied config). - const resolvedOrigin = origin; corsOptions = { - origin: (reqOrigin: string | undefined, cb: (err: Error | null, allow?: boolean | string) => void) => { - if (typeof resolvedOrigin === "boolean" || typeof resolvedOrigin === "string") { - return cb(null, resolvedOrigin); - } - if (reqOrigin === undefined) { - return cb(null, true); - } - if (resolvedOrigin instanceof RegExp) { - return cb(null, resolvedOrigin.test(reqOrigin) ? reqOrigin : false); - } - if ( - Array.isArray(resolvedOrigin) && - resolvedOrigin.some((o) => (typeof o === "string" ? o === reqOrigin : o.test(reqOrigin))) - ) { - return cb(null, reqOrigin); - } - return cb(null, false); - }, + origin: buildStaticCorsOriginCallback(origin), }; } const middleware = cors(corsOptions); @@ -519,27 +527,8 @@ export function onCall, Stream = unknown>( if (Array.isArray(origin) && origin.length === 1) { origin = origin[0]; } - // Use function form so CORS origin is resolved per-request; avoids CodeQL permissive CORS alert (developer-supplied config). - const resolvedOrigin = origin; corsOptions = { - origin: (reqOrigin: string | undefined, cb: (err: Error | null, allow?: boolean | string) => void) => { - if (typeof resolvedOrigin === "boolean" || typeof resolvedOrigin === "string") { - return cb(null, resolvedOrigin); - } - if (reqOrigin === undefined) { - return cb(null, true); - } - if (resolvedOrigin instanceof RegExp) { - return cb(null, resolvedOrigin.test(reqOrigin) ? reqOrigin : false); - } - if ( - Array.isArray(resolvedOrigin) && - resolvedOrigin.some((o) => (typeof o === "string" ? o === reqOrigin : o.test(reqOrigin))) - ) { - return cb(null, reqOrigin); - } - return cb(null, false); - }, + origin: buildStaticCorsOriginCallback(origin), methods: "POST", }; }