From ecaec79aff7103e65d37082e177679d27fb00148 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Przemys=C5=82aw=20=C5=BBydek?= Date: Thu, 2 Apr 2026 12:08:33 +0200 Subject: [PATCH 1/6] chore: exit pre-release mode --- .changeset/pre.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/pre.json b/.changeset/pre.json index 50fe62fd..c405ad3c 100644 --- a/.changeset/pre.json +++ b/.changeset/pre.json @@ -1,5 +1,5 @@ { - "mode": "pre", + "mode": "exit", "tag": "rc", "initialVersions": { "@fingerprint/aws-cloudfront-proxy": "2.1.1", From 8a2b6dd373e526a12a21ebe9639cf143fbde97d4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Przemys=C5=82aw=20=C5=BBydek?= Date: Thu, 2 Apr 2026 12:09:05 +0200 Subject: [PATCH 2/6] chore: remove invalid changeset for '@fingerprint/aws-cloudfront-proxy' and update pre.json --- .changeset/pre.json | 7 +------ .changeset/slimy-tables-shave.md | 5 ----- 2 files changed, 1 insertion(+), 11 deletions(-) delete mode 100644 .changeset/slimy-tables-shave.md diff --git a/.changeset/pre.json b/.changeset/pre.json index c405ad3c..10ed9eab 100644 --- a/.changeset/pre.json +++ b/.changeset/pre.json @@ -6,10 +6,5 @@ "e2e-tests": "0.0.0", "website": "0.0.0" }, - "changesets": [ - "cold-needles-warn", - "cuddly-tires-tan", - "slimy-tables-shave", - "yummy-bananas-worry" - ] + "changesets": ["cold-needles-warn", "cuddly-tires-tan", "yummy-bananas-worry"] } diff --git a/.changeset/slimy-tables-shave.md b/.changeset/slimy-tables-shave.md deleted file mode 100644 index dc30f211..00000000 --- a/.changeset/slimy-tables-shave.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@fingerprint/aws-cloudfront-proxy': patch ---- - -Remove invalid dependency from publish workflow From e731243d788964a91bbad2b5ecde4df893340bf6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Przemys=C5=82aw=20=C5=BBydek?= Date: Thu, 2 Apr 2026 12:11:14 +0200 Subject: [PATCH 3/6] chore: update changeset to include API V4 docs links --- .changeset/cold-needles-warn.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/.changeset/cold-needles-warn.md b/.changeset/cold-needles-warn.md index c75d3134..c6c5ad4e 100644 --- a/.changeset/cold-needles-warn.md +++ b/.changeset/cold-needles-warn.md @@ -2,4 +2,8 @@ '@fingerprint/aws-cloudfront-proxy': minor --- -Add support for API V4 +Add support for API V4. + +Docs: +- [CloudFront JavaScript Agent V4 Migration Guide](https://docs.fingerprint.com/docs/cloudfront-integration-migration-to-js-agent-v4) +- [CloudFront Terraform guide](https://docs.fingerprint.com/docs/aws-cloudfront-integration-via-terraform) From 8f009ecfb7aac5ad22e921ec61f61cbab56e916a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Przemys=C5=82aw=20=C5=BBydek?= Date: Fri, 3 Apr 2026 09:56:58 +0200 Subject: [PATCH 4/6] chore: add non-negative integer validation for BehaviorPathNestLevel and refactor related tests --- .../v4/handleAgentDownloading.test.ts | 20 ++++++++ proxy/test/handlers/v4/handleIngress.test.ts | 48 +++++++++++++++++++ proxy/utils/cache.ts | 4 +- proxy/utils/customer-variables/types.ts | 15 ++++-- proxy/utils/validation.ts | 3 ++ 5 files changed, 86 insertions(+), 4 deletions(-) create mode 100644 proxy/utils/validation.ts diff --git a/proxy/test/handlers/v4/handleAgentDownloading.test.ts b/proxy/test/handlers/v4/handleAgentDownloading.test.ts index 3a017879..8b319a57 100644 --- a/proxy/test/handlers/v4/handleAgentDownloading.test.ts +++ b/proxy/test/handlers/v4/handleAgentDownloading.test.ts @@ -3,6 +3,7 @@ import { EventEmitter } from 'events' import { mockEvent, mockRequest } from '../../aws' import { handler } from '../../../app' import { generateErrorResponse } from '../../../utils/generateErrorResponse' +import { CustomerVariableName } from '../../../utils/customer-variables/types' const requestUri = '/behavior/web/v4/ujKG34hUYKLJKJ1F' describe('Download agent endpoint V4', () => { @@ -71,6 +72,25 @@ describe('Download agent endpoint V4', () => { expect(url.toString()).toEqual(`https://${origin}/web/v4/ujKG34hUYKLJKJ1F`) }) + test('Successful call with nested behavior path', async () => { + const event = mockEvent(mockRequest({ uri: `/nested${requestUri}`, querystring: '', method: 'GET' })) + + event.Records[0].cf.request.origin!.s3!.customHeaders[CustomerVariableName.BehaviorPathNestLevel] = [ + { + key: CustomerVariableName.BehaviorPathNestLevel, + value: '2', + }, + ] + + await handler(event) + + expect(requestSpy).toHaveBeenCalledTimes(1) + + const [url] = requestSpy.mock.calls[0] + + expect(url.toString()).toEqual(`https://${origin}/web/v4/ujKG34hUYKLJKJ1F`) + }) + test('Call with a custom query', async () => { const request = mockRequest({ uri: requestUri, diff --git a/proxy/test/handlers/v4/handleIngress.test.ts b/proxy/test/handlers/v4/handleIngress.test.ts index e3f4d3f9..b5b12b9b 100644 --- a/proxy/test/handlers/v4/handleIngress.test.ts +++ b/proxy/test/handlers/v4/handleIngress.test.ts @@ -6,6 +6,7 @@ import https, { Agent } from 'https' import { EventEmitter } from 'events' import { ClientRequest, IncomingMessage } from 'http' import { Socket } from 'net' +import { CustomerVariableName } from '../../../utils/customer-variables/types' describe('Result Endpoint V4', () => { const requestUri = '/behavior' @@ -148,6 +149,53 @@ describe('Result Endpoint V4', () => { ) }) + test('Call with suffix and nested behavior path', async () => { + const event = mockEvent( + mockRequest({ + uri: '/behavior/nested/with/suffix', + querystring: '', + }) + ) + event.Records[0].cf.request.origin!.s3!.customHeaders[CustomerVariableName.BehaviorPathNestLevel] = [ + { + key: CustomerVariableName.BehaviorPathNestLevel, + value: '2', + }, + ] + await handler(event) + + expect(https.request).toHaveBeenCalledWith( + `https://${origin}/with/suffix${queryString}`, + expect.anything(), + expect.anything() + ) + }) + + test.each([NaN, -1, 'test', ' 1', Infinity, -Infinity])( + 'Call with suffix and invalid nested path level variable: %s', + async (value) => { + const event = mockEvent( + mockRequest({ + uri: '/behavior/with/suffix', + querystring: '', + }) + ) + event.Records[0].cf.request.origin!.s3!.customHeaders[CustomerVariableName.BehaviorPathNestLevel] = [ + { + key: CustomerVariableName.BehaviorPathNestLevel, + value: value as any, + }, + ] + await handler(event) + + expect(https.request).toHaveBeenCalledWith( + `https://${origin}/with/suffix${queryString}`, + expect.anything(), + expect.anything() + ) + } + ) + test('Traffic monitoring', async () => { const event = mockEvent(mockRequest({ uri: requestUri, querystring: '' })) await handler(event) diff --git a/proxy/utils/cache.ts b/proxy/utils/cache.ts index 629949ac..72d3b386 100644 --- a/proxy/utils/cache.ts +++ b/proxy/utils/cache.ts @@ -1,3 +1,5 @@ +import { isNonNegativeInteger } from './validation' + interface CacheItem { value: T expiresAt: number @@ -51,6 +53,6 @@ export class TTLCache { } static isValidTTL(value?: number): value is number { - return typeof value === 'number' && !Number.isNaN(value) && Number.isFinite(value) && value >= 0 + return isNonNegativeInteger(value) } } diff --git a/proxy/utils/customer-variables/types.ts b/proxy/utils/customer-variables/types.ts index cbf2105b..32bd9e5a 100644 --- a/proxy/utils/customer-variables/types.ts +++ b/proxy/utils/customer-variables/types.ts @@ -1,3 +1,5 @@ +import { isNonNegativeInteger } from '../validation' + export enum CustomerVariableName { GetResultPath = 'fpjs_get_result_path', BehaviorPathNestLevel = 'fpjs_integration_path_depth', @@ -12,16 +14,23 @@ export const internalVariables: Set = new Set = (value: T) => boolean + const stringParser = (value: string) => value -const intParser = (fallbackValue: number) => (value: string) => { +const intParser = (fallbackValue: number, validation: ParserValidator) => (value: string) => { const parsed = parseInt(value) - return isNaN(parsed) ? fallbackValue : parsed + + if (validation(parsed)) { + return parsed + } + + return fallbackValue } export const customerVariableParsers = { [CustomerVariableName.GetResultPath]: stringParser, - [CustomerVariableName.BehaviorPathNestLevel]: intParser(1), + [CustomerVariableName.BehaviorPathNestLevel]: intParser(1, isNonNegativeInteger), [CustomerVariableName.PreSharedSecret]: stringParser, [CustomerVariableName.AgentDownloadPath]: stringParser, [CustomerVariableName.FpCdnUrl]: stringParser, diff --git a/proxy/utils/validation.ts b/proxy/utils/validation.ts new file mode 100644 index 00000000..ecdf771d --- /dev/null +++ b/proxy/utils/validation.ts @@ -0,0 +1,3 @@ +export function isNonNegativeInteger(value?: unknown): value is number { + return Boolean(typeof value === 'number' && Number.isInteger(value) && value >= 0) +} From 7b93386e4f7cf8ea16920fac4aaa9774376de85e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Przemys=C5=82aw=20=C5=BBydek?= Date: Fri, 3 Apr 2026 19:12:08 +0200 Subject: [PATCH 5/6] feat: add FPJS_INTEGRATION_PATH_DEPTH header to CloudFormation template --- .changeset/fifty-bars-pull.md | 5 +++++ cloudformation/template.yml | 3 +++ 2 files changed, 8 insertions(+) create mode 100644 .changeset/fifty-bars-pull.md diff --git a/.changeset/fifty-bars-pull.md b/.changeset/fifty-bars-pull.md new file mode 100644 index 00000000..454bf8a2 --- /dev/null +++ b/.changeset/fifty-bars-pull.md @@ -0,0 +1,5 @@ +--- +'@fingerprint/aws-cloudfront-proxy': minor +--- + +Add FPJS_INTEGRATION_PATH_DEPTH header to CloudFormation template diff --git a/cloudformation/template.yml b/cloudformation/template.yml index 9f72d4df..b62e7c7f 100644 --- a/cloudformation/template.yml +++ b/cloudformation/template.yml @@ -200,6 +200,9 @@ Resources: OriginCustomHeaders: - HeaderName: FPJS_SECRET_NAME HeaderValue: !Ref FingerprintIntegrationSettingsSecret + + - HeaderName: FPJS_INTEGRATION_PATH_DEPTH + HeaderValue: '0' CustomOriginConfig: HTTPPort: 80 HTTPSPort: 443 From 072223e8450bb26c3fae35bd8a00960d15caaef2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Przemys=C5=82aw=20=C5=BBydek?= Date: Tue, 7 Apr 2026 10:33:42 +0200 Subject: [PATCH 6/6] test: add unit tests for root path integration handling in V4 handler --- proxy/test/handlers/v4/handleIngress.test.ts | 40 ++++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/proxy/test/handlers/v4/handleIngress.test.ts b/proxy/test/handlers/v4/handleIngress.test.ts index b5b12b9b..0d55403c 100644 --- a/proxy/test/handlers/v4/handleIngress.test.ts +++ b/proxy/test/handlers/v4/handleIngress.test.ts @@ -171,6 +171,46 @@ describe('Result Endpoint V4', () => { ) }) + test('Call with integration served under root path', async () => { + const event = mockEvent( + mockRequest({ + uri: '/', + querystring: '', + }) + ) + event.Records[0].cf.request.origin!.s3!.customHeaders[CustomerVariableName.BehaviorPathNestLevel] = [ + { + key: CustomerVariableName.BehaviorPathNestLevel, + value: '0', + }, + ] + await handler(event) + + expect(https.request).toHaveBeenCalledWith(`https://${origin}/${queryString}`, expect.anything(), expect.anything()) + }) + + test('Call suffix and with integration served under root path', async () => { + const event = mockEvent( + mockRequest({ + uri: '/with/suffix', + querystring: '', + }) + ) + event.Records[0].cf.request.origin!.s3!.customHeaders[CustomerVariableName.BehaviorPathNestLevel] = [ + { + key: CustomerVariableName.BehaviorPathNestLevel, + value: '0', + }, + ] + await handler(event) + + expect(https.request).toHaveBeenCalledWith( + `https://${origin}/with/suffix${queryString}`, + expect.anything(), + expect.anything() + ) + }) + test.each([NaN, -1, 'test', ' 1', Infinity, -Infinity])( 'Call with suffix and invalid nested path level variable: %s', async (value) => {