From 61206015337fdbb58d44578bc611e1c309422aef Mon Sep 17 00:00:00 2001 From: Izaak Gough Date: Fri, 15 May 2026 11:41:07 +0100 Subject: [PATCH 1/9] fix: route v2 callables through wrapV2 when platform is gcfv2 --- spec/main.spec.ts | 19 +++++++++++++++++++ src/main.ts | 16 ++++++++++++++-- src/v2.ts | 10 +++++++--- 3 files changed, 40 insertions(+), 5 deletions(-) diff --git a/spec/main.spec.ts b/spec/main.spec.ts index 703495c8..c13bd6bf 100644 --- a/spec/main.spec.ts +++ b/spec/main.spec.ts @@ -239,6 +239,25 @@ describe('main', () => { }); }); + describe('v2 callable functions', () => { + it('should route to v2 path when outer and run have length 0', () => { + const cloudFunction = (..._args: unknown[]) => {}; + set(cloudFunction, 'run', (req) => { + return { auth: req.auth }; + }); + set(cloudFunction, '__endpoint', { + platform: 'gcfv2', + callableTrigger: {}, + }); + const wrapped = wrap(cloudFunction as functions.CloudFunction); + const result = wrapped({ + data: { ping: true }, + auth: { uid: 'test-user' }, + }); + expect(result.auth).to.deep.equal({ uid: 'test-user' }); + }); + }); + describe('callable functions', () => { let wrappedCF; diff --git a/src/main.ts b/src/main.ts index 13a23bba..02536bbf 100644 --- a/src/main.ts +++ b/src/main.ts @@ -38,7 +38,12 @@ import { import { wrapV1, WrappedFunction, WrappedScheduledFunction } from './v1'; -import { wrapV2, WrappedV2Function, WrappedV2CallableFunction } from './v2'; +import { + wrapV2, + WrappedV2Function, + WrappedV2CallableFunction, + isCallableV2Function, +} from './v2'; type HttpsFunctionOrCloudFunctionV1 = U extends HttpsFunction & Runnable @@ -80,8 +85,12 @@ export function wrap>( | WrappedFunction | WrappedV2Function | WrappedV2CallableFunction { + if (isCallableV2Function(cloudFunction)) { + const v2Callable = cloudFunction as CallableFunction; + return wrapV2(v2Callable); + } if (isV2CloudFunction(cloudFunction)) { - return wrapV2(cloudFunction as CloudFunctionV2); + return wrapV2(cloudFunction as CloudFunctionV2); } return wrapV1( cloudFunction as HttpsFunctionOrCloudFunctionV1 @@ -100,5 +109,8 @@ export function wrap>( function isV2CloudFunction>( cloudFunction: any ): cloudFunction is CloudFunctionV2 { + if (cloudFunction?.__endpoint?.platform === 'gcfv2') { + return true; + } return cloudFunction.length === 1 && cloudFunction?.run?.length === 1; } diff --git a/src/v2.ts b/src/v2.ts index e3fef189..7e03b7cb 100644 --- a/src/v2.ts +++ b/src/v2.ts @@ -38,10 +38,14 @@ export type WrappedV2CallableFunction = ( data: CallableRequest ) => T | Promise; -function isCallableV2Function>( - cf: CloudFunction | CallableFunction +export function isCallableV2Function( + cf: any ): cf is CallableFunction { - return !!cf?.__endpoint?.callableTrigger; + return ( + !!cf?.__endpoint?.callableTrigger && + // ensures that v1 callables are not treated as v2 + cf.__endpoint.platform === 'gcfv2' + ); } function assertIsCloudFunction>( From 9ddc18259d9513960bab627b5bdd958ef0cce765 Mon Sep 17 00:00:00 2001 From: Izaak Gough Date: Wed, 27 May 2026 13:04:41 +0100 Subject: [PATCH 2/9] docs: clarify v1/v2 function detection logic --- src/main.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/main.ts b/src/main.ts index 02536bbf..340962f2 100644 --- a/src/main.ts +++ b/src/main.ts @@ -110,6 +110,10 @@ function isV2CloudFunction>( cloudFunction: any ): cloudFunction is CloudFunctionV2 { if (cloudFunction?.__endpoint?.platform === 'gcfv2') { + // Prefer __endpoint.platform when available: this catches + // firebase-functions@7-style rest-param wrappers, where both function + // lengths can be 0. The arity check below remains as a fallback for older + // v2 functions that may not have __endpoint.platform set. return true; } return cloudFunction.length === 1 && cloudFunction?.run?.length === 1; From c51664f9aa3d29af771184afda595e65c0e8da08 Mon Sep 17 00:00:00 2001 From: Izaak Gough Date: Wed, 27 May 2026 13:06:22 +0100 Subject: [PATCH 3/9] tests: update test and negative test for v1 callable --- spec/main.spec.ts | 30 ++++++++++++++++++++++++++++-- 1 file changed, 28 insertions(+), 2 deletions(-) diff --git a/spec/main.spec.ts b/spec/main.spec.ts index c13bd6bf..d51fed56 100644 --- a/spec/main.spec.ts +++ b/spec/main.spec.ts @@ -242,8 +242,9 @@ describe('main', () => { describe('v2 callable functions', () => { it('should route to v2 path when outer and run have length 0', () => { const cloudFunction = (..._args: unknown[]) => {}; - set(cloudFunction, 'run', (req) => { - return { auth: req.auth }; + set(cloudFunction, 'run', (...args: unknown[]) => { + const [req] = args; + return { auth: (req as any).auth }; }); set(cloudFunction, '__endpoint', { platform: 'gcfv2', @@ -258,6 +259,31 @@ describe('main', () => { }); }); + describe('v1 callable functions with endpoint metadata', () => { + it('should route v1 callables with callableTrigger metadata to wrapV1', () => { + const cloudFunction = (input) => input; + set(cloudFunction, 'run', (data, context) => { + return { data, context }; + }); + set(cloudFunction, '__endpoint', { + platform: 'gcfv1', + callableTrigger: {}, + }); + const wrapped: any = wrap(cloudFunction as any); + const result = wrapped('data', { + auth: { uid: 'abc' }, + app: { appId: 'efg' }, + instanceIdToken: '123', + rawRequest: { body: 'hello' }, + }); + expect(result.data).to.equal('data'); + expect(result.context.auth).to.deep.equal({ uid: 'abc' }); + expect(result.context.app).to.deep.equal({ appId: 'efg' }); + expect(result.context.instanceIdToken).to.equal('123'); + expect(result.context.rawRequest).to.deep.equal({ body: 'hello' }); + }); + }); + describe('callable functions', () => { let wrappedCF; From 9a8eda23703493fabc03554a81e2c6f77499808d Mon Sep 17 00:00:00 2001 From: Izaak Gough Date: Wed, 14 Oct 2026 16:45:52 +0100 Subject: [PATCH 4/9] fix: only route v2 callables to wrapV2 by trigger Matching on platform sent v2 onRequest, onSchedule and blocking functions to wrapV2. Only callables need the trigger check, so the arity check handles the rest as before. --- spec/main.spec.ts | 13 +++++++++++++ src/main.ts | 9 ++------- 2 files changed, 15 insertions(+), 7 deletions(-) diff --git a/spec/main.spec.ts b/spec/main.spec.ts index d51fed56..54ead9ae 100644 --- a/spec/main.spec.ts +++ b/spec/main.spec.ts @@ -259,6 +259,19 @@ describe('main', () => { }); }); + describe('v2 onRequest functions', () => { + it('should throw the onCall-only error', () => { + const cloudFunction = (_req, _res) => {}; + set(cloudFunction, '__endpoint', { + platform: 'gcfv2', + httpsTrigger: {}, + }); + expect(() => wrap(cloudFunction as any)).to.throw( + 'Wrap function is only available for `onCall` HTTP functions, not `onRequest`.' + ); + }); + }); + describe('v1 callable functions with endpoint metadata', () => { it('should route v1 callables with callableTrigger metadata to wrapV1', () => { const cloudFunction = (input) => input; diff --git a/src/main.ts b/src/main.ts index 340962f2..590e2b5b 100644 --- a/src/main.ts +++ b/src/main.ts @@ -85,6 +85,8 @@ export function wrap>( | WrappedFunction | WrappedV2Function | WrappedV2CallableFunction { + // v2 callables are checked by trigger rather than arity, since + // firebase-functions@7 wraps them in rest-param functions of length 0. if (isCallableV2Function(cloudFunction)) { const v2Callable = cloudFunction as CallableFunction; return wrapV2(v2Callable); @@ -109,12 +111,5 @@ export function wrap>( function isV2CloudFunction>( cloudFunction: any ): cloudFunction is CloudFunctionV2 { - if (cloudFunction?.__endpoint?.platform === 'gcfv2') { - // Prefer __endpoint.platform when available: this catches - // firebase-functions@7-style rest-param wrappers, where both function - // lengths can be 0. The arity check below remains as a fallback for older - // v2 functions that may not have __endpoint.platform set. - return true; - } return cloudFunction.length === 1 && cloudFunction?.run?.length === 1; } From d571882acc181748ac709c81a8fb812ef5670560 Mon Sep 17 00:00:00 2001 From: Izaak Gough Date: Thu, 15 Oct 2026 13:06:11 +0100 Subject: [PATCH 5/9] Update src/main.ts Co-authored-by: Jacob Cable <32874567+cabljac@users.noreply.github.com> --- src/main.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/main.ts b/src/main.ts index 590e2b5b..9fc651ea 100644 --- a/src/main.ts +++ b/src/main.ts @@ -88,8 +88,7 @@ export function wrap>( // v2 callables are checked by trigger rather than arity, since // firebase-functions@7 wraps them in rest-param functions of length 0. if (isCallableV2Function(cloudFunction)) { - const v2Callable = cloudFunction as CallableFunction; - return wrapV2(v2Callable); + return wrapV2(cloudFunction); } if (isV2CloudFunction(cloudFunction)) { return wrapV2(cloudFunction as CloudFunctionV2); From e10b531b4b2270e74f53bfb00e4ba6838b625d12 Mon Sep 17 00:00:00 2001 From: Izaak Gough Date: Thu, 15 Oct 2026 13:06:41 +0100 Subject: [PATCH 6/9] Update src/v2.ts Co-authored-by: Jacob Cable <32874567+cabljac@users.noreply.github.com> --- src/v2.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/v2.ts b/src/v2.ts index 7e03b7cb..edc73c9e 100644 --- a/src/v2.ts +++ b/src/v2.ts @@ -43,7 +43,6 @@ export function isCallableV2Function( ): cf is CallableFunction { return ( !!cf?.__endpoint?.callableTrigger && - // ensures that v1 callables are not treated as v2 cf.__endpoint.platform === 'gcfv2' ); } From e9a3806b6a2b659b390e0331b52ba8758b2fe3db Mon Sep 17 00:00:00 2001 From: Izaak Gough Date: Thu, 15 Oct 2026 13:08:20 +0100 Subject: [PATCH 7/9] Update src/v2.ts Co-authored-by: Jacob Cable <32874567+cabljac@users.noreply.github.com> --- src/v2.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/v2.ts b/src/v2.ts index edc73c9e..77da8bb5 100644 --- a/src/v2.ts +++ b/src/v2.ts @@ -38,9 +38,9 @@ export type WrappedV2CallableFunction = ( data: CallableRequest ) => T | Promise; -export function isCallableV2Function( - cf: any -): cf is CallableFunction { +export function isCallableV2Function(cf: { + __endpoint?: { platform?: string; callableTrigger?: unknown }; +}): cf is CallableFunction { return ( !!cf?.__endpoint?.callableTrigger && cf.__endpoint.platform === 'gcfv2' From bd5e9c631d564482887e6030cec7770199421182 Mon Sep 17 00:00:00 2001 From: Izaak Gough Date: Thu, 15 Oct 2026 13:12:08 +0100 Subject: [PATCH 8/9] Update src/main.ts Co-authored-by: Jacob Cable <32874567+cabljac@users.noreply.github.com> --- src/main.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main.ts b/src/main.ts index 9fc651ea..681479be 100644 --- a/src/main.ts +++ b/src/main.ts @@ -91,7 +91,7 @@ export function wrap>( return wrapV2(cloudFunction); } if (isV2CloudFunction(cloudFunction)) { - return wrapV2(cloudFunction as CloudFunctionV2); + return wrapV2(cloudFunction as CloudFunctionV2); } return wrapV1( cloudFunction as HttpsFunctionOrCloudFunctionV1 From a479c7680da17e1ff8a5031c435381129e44876d Mon Sep 17 00:00:00 2001 From: Izaak Gough Date: Thu, 15 Oct 2026 13:30:15 +0100 Subject: [PATCH 9/9] chore: format src/v2.ts --- src/v2.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/v2.ts b/src/v2.ts index 77da8bb5..41ad0772 100644 --- a/src/v2.ts +++ b/src/v2.ts @@ -42,8 +42,7 @@ export function isCallableV2Function(cf: { __endpoint?: { platform?: string; callableTrigger?: unknown }; }): cf is CallableFunction { return ( - !!cf?.__endpoint?.callableTrigger && - cf.__endpoint.platform === 'gcfv2' + !!cf?.__endpoint?.callableTrigger && cf.__endpoint.platform === 'gcfv2' ); }