From ad4176ada5caab579abf66372de1b453bcedc3b0 Mon Sep 17 00:00:00 2001 From: Avinash Kumar Date: Thu, 26 Feb 2026 15:44:35 -0500 Subject: [PATCH 1/6] TUrnstile in SDK --- packages/commerce-sdk-react/CHANGELOG.md | 4 + packages/commerce-sdk-react/package.json | 2 +- .../commerce-sdk-react/src/auth/index.test.ts | 46 +++++- packages/commerce-sdk-react/src/auth/index.ts | 132 ++++++++++++++---- 4 files changed, 153 insertions(+), 31 deletions(-) diff --git a/packages/commerce-sdk-react/CHANGELOG.md b/packages/commerce-sdk-react/CHANGELOG.md index b0523d6f60..6a86ba674d 100644 --- a/packages/commerce-sdk-react/CHANGELOG.md +++ b/packages/commerce-sdk-react/CHANGELOG.md @@ -1,3 +1,7 @@ +## v5.0.0-turnstile-preview.0 (Jan 30, 2026) + +- [Feature] Add custom fetch for `authorizePasswordless` when `turnstileResponse` is present. The commerce-sdk-isomorphic helper does not forward the Turnstile token; this change performs the request via custom fetch so the token reaches the BFF/MRT for server-side verification via Cloudflare Siteverify. + ## v5.1.0-dev - Add Node 24 support. Drop Node 16 support. [#3652](https://github.com/SalesforceCommerceCloud/pwa-kit/pull/3652) - Add Shopper Consents API support [#3674](https://github.com/SalesforceCommerceCloud/pwa-kit/pull/3674) diff --git a/packages/commerce-sdk-react/package.json b/packages/commerce-sdk-react/package.json index c82d251d10..db8956c4e2 100644 --- a/packages/commerce-sdk-react/package.json +++ b/packages/commerce-sdk-react/package.json @@ -1,6 +1,6 @@ { "name": "@salesforce/commerce-sdk-react", - "version": "5.1.0-dev", + "version": "5.0.0-turnstile-preview.0", "description": "A library that provides react hooks for fetching data from Commerce Cloud", "homepage": "https://github.com/SalesforceCommerceCloud/pwa-kit/tree/develop/packages/ecom-react-hooks#readme", "bugs": { diff --git a/packages/commerce-sdk-react/src/auth/index.test.ts b/packages/commerce-sdk-react/src/auth/index.test.ts index d8331ea5a2..b2c3846764 100644 --- a/packages/commerce-sdk-react/src/auth/index.test.ts +++ b/packages/commerce-sdk-react/src/auth/index.test.ts @@ -989,7 +989,7 @@ describe('Auth', () => { const mockErrorResponse = { status: 400, - json: jest.fn().mockResolvedValue({message: 'Invalid request'}) + text: jest.fn().mockResolvedValue(JSON.stringify({message: 'Invalid request'})) } ;(helpers.authorizePasswordless as jest.Mock).mockResolvedValueOnce(mockErrorResponse) @@ -998,6 +998,50 @@ describe('Auth', () => { ) }) + test('authorizePasswordless with turnstileResponse uses custom fetch instead of helper', async () => { + const mockFetch = jest.fn().mockResolvedValue({status: 200, text: () => Promise.resolve('')}) + global.fetch = mockFetch + + const auth = new Auth(configSLASPrivate) + // @ts-expect-error private method + auth.set('usid', 'test-usid') + + await auth.authorizePasswordless({ + userid: 'user@example.com', + turnstileResponse: 'turnstile-token-123' + }) + + expect(helpers.authorizePasswordless).not.toHaveBeenCalled() + expect(mockFetch).toHaveBeenCalledTimes(1) + const [url, options] = mockFetch.mock.calls[0] + expect(url).toBe( + 'proxy/shopper/auth/v1/organizations/organizationId/oauth2/passwordless/login' + ) + expect(options.method).toBe('POST') + expect(options.headers['Content-Type']).toBe('application/x-www-form-urlencoded') + const body = new URLSearchParams(options.body) + expect(body.get('user_id')).toBe('user@example.com') + expect(body.get('turnstileResponse')).toBe('turnstile-token-123') + expect(body.get('usid')).toBe('test-usid') + }) + + test('authorizePasswordless with turnstileResponse throws on non-200 fetch response', async () => { + const mockFetch = jest.fn().mockResolvedValue({ + status: 404, + text: () => Promise.resolve('') + }) + global.fetch = mockFetch + + const auth = new Auth(configSLASPrivate) + + await expect( + auth.authorizePasswordless({ + userid: 'guest@example.com', + turnstileResponse: 'token' + }) + ).rejects.toThrow('404') + }) + test('getPasswordLessAccessToken calls isomorphic getPasswordLessAccessToken', async () => { const auth = new Auth(config) await auth.getPasswordLessAccessToken({pwdlessLoginToken: '12345678'}) diff --git a/packages/commerce-sdk-react/src/auth/index.ts b/packages/commerce-sdk-react/src/auth/index.ts index a9712a9373..97c20c4e1f 100644 --- a/packages/commerce-sdk-react/src/auth/index.ts +++ b/packages/commerce-sdk-react/src/auth/index.ts @@ -103,6 +103,8 @@ type AuthorizePasswordlessParams = { last_name?: string email?: string phone_number?: string + /** Cloudflare Turnstile response token for bot protection. Backend/MRT should verify via Siteverify and strip before forwarding to SLAS. */ + turnstileResponse?: string } type GetPasswordLessAccessTokenParams = { @@ -284,14 +286,18 @@ class Auth { | undefined private hybridAuthEnabled: boolean + /** Base URL for SLAS client (same as proxy passed to ShopperLogin). Used for custom fetch when turnstileResponse is present. */ + private slasClientBaseUrl: string constructor(config: AuthConfig) { // Special proxy endpoint for injecting SLAS private client secret. // We prioritize config.privateClientProxyEndpoint since that allows us to use the new envBasePath feature - this.client = new ShopperLogin({ - proxy: config.enablePWAKitPrivateClient + this.slasClientBaseUrl = + config.enablePWAKitPrivateClient && config.privateClientProxyEndpoint ? config.privateClientProxyEndpoint - : config.proxy, + : config.proxy + this.client = new ShopperLogin({ + proxy: this.slasClientBaseUrl, headers: config.headers || {}, parameters: { clientId: config.clientId, @@ -1276,6 +1282,8 @@ class Auth { /** * A wrapper method for commerce-sdk-isomorphic helper: authorizePasswordless. + * When turnstileResponse is provided, we perform the request ourselves so the token + * is included in the body (commerce-sdk-isomorphic helper does not forward turnstileResponse). */ async authorizePasswordless(parameters: AuthorizePasswordlessParams) { const usid = this.get('usid') @@ -1284,34 +1292,100 @@ class Auth { const mode = parameters.mode || 'callback' const callbackURI = parameters.callbackURI || this.passwordlessLoginCallbackURI - const res = await helpers.authorizePasswordless({ - slasClient: this.client, - credentials: { - clientSecret: this.clientSecret - }, - parameters: { - ...(callbackURI && {callbackURI}), - ...(usid && {usid}), - ...(parameters.locale && {locale: parameters.locale}), - userid: parameters.userid, - mode, - ...(parameters.register_customer !== undefined && { - registerCustomer: - typeof parameters.register_customer === 'boolean' - ? parameters.register_customer - : parameters.register_customer === 'true' - ? true - : false - }), - ...(parameters.last_name && {lastName: parameters.last_name}), - ...(parameters.email && {email: parameters.email}), - ...(parameters.first_name && {firstName: parameters.first_name}), - ...(parameters.phone_number && {phoneNumber: parameters.phone_number}) + const {turnstileResponse, ...restParams} = parameters + + let res: Response + + if (turnstileResponse) { + // commerce-sdk-isomorphic helper does not include turnstileResponse in the POST body. + // Perform the request ourselves so the Turnstile token reaches the server. + const clientConfig = (this.client as { + clientConfig?: { + parameters?: { organizationId?: string; siteId?: string } + fetchOptions?: RequestInit + } + }).clientConfig + const organizationId = clientConfig?.parameters?.organizationId + const channelId = clientConfig?.parameters?.siteId + if (!organizationId || !channelId) { + throw new Error('Missing organizationId or siteId for passwordless login') } - }) + // Use SLAS proxy base URL (same-origin) so the request is allowed by CSP and reaches the app server + const url = `${this.slasClientBaseUrl}/shopper/auth/v1/organizations/${organizationId}/oauth2/passwordless/login` + const bodyEntries: Array<[string, string]> = [ + ['user_id', restParams.userid], + ['mode', mode], + ['channel_id', channelId] + ] + if (restParams.locale) bodyEntries.push(['locale', restParams.locale]) + if (usid) bodyEntries.push(['usid', usid]) + if (callbackURI) bodyEntries.push(['callback_uri', callbackURI]) + if (restParams.register_customer !== undefined) { + const val = + typeof restParams.register_customer === 'boolean' + ? restParams.register_customer + : restParams.register_customer === 'true' + bodyEntries.push(['register_customer', val ? 'true' : 'false']) + } + if (restParams.last_name) bodyEntries.push(['last_name', restParams.last_name]) + if (restParams.email) bodyEntries.push(['email', restParams.email]) + if (restParams.first_name) bodyEntries.push(['first_name', restParams.first_name]) + if (restParams.phone_number) bodyEntries.push(['phone_number', restParams.phone_number]) + bodyEntries.push(['turnstileResponse', turnstileResponse]) + + const body = new URLSearchParams(bodyEntries).toString() + const fetchOptions = clientConfig?.fetchOptions ?? {} + res = await fetch(url, { + ...fetchOptions, + method: 'POST', + headers: { + ...(fetchOptions.headers as Record), + 'Content-Type': 'application/x-www-form-urlencoded' + }, + body + }) + } else { + res = await helpers.authorizePasswordless({ + slasClient: this.client, + credentials: { + clientSecret: this.clientSecret + }, + parameters: { + ...(callbackURI && {callbackURI}), + ...(usid && {usid}), + ...(restParams.locale && {locale: restParams.locale}), + userid: restParams.userid, + mode, + ...(restParams.register_customer !== undefined && { + registerCustomer: + typeof restParams.register_customer === 'boolean' + ? restParams.register_customer + : restParams.register_customer === 'true' + ? true + : false + }), + ...(restParams.last_name && {lastName: restParams.last_name}), + ...(restParams.email && {email: restParams.email}), + ...(restParams.first_name && {firstName: restParams.first_name}), + ...(restParams.phone_number && {phoneNumber: restParams.phone_number}) + } + }) + } + if (res && res.status !== 200) { - const errorData = await res.json() - throw new Error(`${res.status} ${String(errorData.message)}`) + let errorMessage = '' + try { + const text = await res.text() + if (text.trim()) { + const errorData = JSON.parse(text) as { message?: string } + errorMessage = String(errorData?.message ?? '').trim() + } + } catch { + // Empty or invalid JSON body (e.g. 404 for guest user) + } + throw new Error( + [res.status, errorMessage].filter(Boolean).join(' ') || String(res.status) + ) } return res } From 25c3342bb9aea9398084231dd3e103180bb1aa5b Mon Sep 17 00:00:00 2001 From: Avinash Kumar Date: Thu, 26 Feb 2026 15:48:26 -0500 Subject: [PATCH 2/6] TUrnstile in SDK --- packages/commerce-sdk-react/CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/commerce-sdk-react/CHANGELOG.md b/packages/commerce-sdk-react/CHANGELOG.md index 6a86ba674d..ccd8a58722 100644 --- a/packages/commerce-sdk-react/CHANGELOG.md +++ b/packages/commerce-sdk-react/CHANGELOG.md @@ -8,6 +8,8 @@ ## v5.0.0 (Feb 12, 2026) - Upgrade to commerce-sdk-isomorphic v5.0.0 and introduce Payment Instrument SCAPI integration [#3552](https://github.com/SalesforceCommerceCloud/pwa-kit/pull/3552) + +## v4.4.0-dev (Dec 17, 2025) - [Bugfix] Ensure code_verifier can be optional in resetPassword call [#3567](https://github.com/SalesforceCommerceCloud/pwa-kit/pull/3567) - [Improvement] Strengthening typescript types on custom endpoint options and fetchOptions types [#3589](https://github.com/SalesforceCommerceCloud/pwa-kit/pull/3589) - [Feature] Update `authorizePasswordless`, `getPasswordResetToken`, and `resetPassword` to support use of `email` mode [#3525](https://github.com/SalesforceCommerceCloud/pwa-kit/pull/3525) From 31d8ee62e9765899d7a58aee8454a03153208dae Mon Sep 17 00:00:00 2001 From: Avinash Kumar Date: Thu, 26 Feb 2026 15:57:31 -0500 Subject: [PATCH 3/6] updated the versions --- packages/commerce-sdk-react/CHANGELOG.md | 5 +---- packages/commerce-sdk-react/package.json | 2 +- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/packages/commerce-sdk-react/CHANGELOG.md b/packages/commerce-sdk-react/CHANGELOG.md index ccd8a58722..a32d1686f9 100644 --- a/packages/commerce-sdk-react/CHANGELOG.md +++ b/packages/commerce-sdk-react/CHANGELOG.md @@ -1,8 +1,5 @@ -## v5.0.0-turnstile-preview.0 (Jan 30, 2026) - -- [Feature] Add custom fetch for `authorizePasswordless` when `turnstileResponse` is present. The commerce-sdk-isomorphic helper does not forward the Turnstile token; this change performs the request via custom fetch so the token reaches the BFF/MRT for server-side verification via Cloudflare Siteverify. - ## v5.1.0-dev +- Add custom fetch for `authorizePasswordless` when `turnstileResponse` is present (Cloudflare Turnstile support). The commerce-sdk-isomorphic helper does not forward the Turnstile token; this change performs the request via custom fetch so the token reaches the BFF/MRT for server-side verification via Cloudflare Siteverify. - Add Node 24 support. Drop Node 16 support. [#3652](https://github.com/SalesforceCommerceCloud/pwa-kit/pull/3652) - Add Shopper Consents API support [#3674](https://github.com/SalesforceCommerceCloud/pwa-kit/pull/3674) diff --git a/packages/commerce-sdk-react/package.json b/packages/commerce-sdk-react/package.json index db8956c4e2..c82d251d10 100644 --- a/packages/commerce-sdk-react/package.json +++ b/packages/commerce-sdk-react/package.json @@ -1,6 +1,6 @@ { "name": "@salesforce/commerce-sdk-react", - "version": "5.0.0-turnstile-preview.0", + "version": "5.1.0-dev", "description": "A library that provides react hooks for fetching data from Commerce Cloud", "homepage": "https://github.com/SalesforceCommerceCloud/pwa-kit/tree/develop/packages/ecom-react-hooks#readme", "bugs": { From f17d3da1f8cff885d56d7fd3e558af775732ba56 Mon Sep 17 00:00:00 2001 From: Avinash Kumar Date: Thu, 26 Feb 2026 16:04:40 -0500 Subject: [PATCH 4/6] lint fixes --- packages/commerce-sdk-react/src/auth/index.test.ts | 4 +++- packages/commerce-sdk-react/src/auth/index.ts | 14 ++++++++------ packages/template-retail-react-app/package.json | 2 +- 3 files changed, 12 insertions(+), 8 deletions(-) diff --git a/packages/commerce-sdk-react/src/auth/index.test.ts b/packages/commerce-sdk-react/src/auth/index.test.ts index b2c3846764..083e062f6f 100644 --- a/packages/commerce-sdk-react/src/auth/index.test.ts +++ b/packages/commerce-sdk-react/src/auth/index.test.ts @@ -999,7 +999,9 @@ describe('Auth', () => { }) test('authorizePasswordless with turnstileResponse uses custom fetch instead of helper', async () => { - const mockFetch = jest.fn().mockResolvedValue({status: 200, text: () => Promise.resolve('')}) + const mockFetch = jest + .fn() + .mockResolvedValue({status: 200, text: () => Promise.resolve('')}) global.fetch = mockFetch const auth = new Auth(configSLASPrivate) diff --git a/packages/commerce-sdk-react/src/auth/index.ts b/packages/commerce-sdk-react/src/auth/index.ts index 97c20c4e1f..f38962d2ae 100644 --- a/packages/commerce-sdk-react/src/auth/index.ts +++ b/packages/commerce-sdk-react/src/auth/index.ts @@ -1299,12 +1299,14 @@ class Auth { if (turnstileResponse) { // commerce-sdk-isomorphic helper does not include turnstileResponse in the POST body. // Perform the request ourselves so the Turnstile token reaches the server. - const clientConfig = (this.client as { - clientConfig?: { - parameters?: { organizationId?: string; siteId?: string } - fetchOptions?: RequestInit + const clientConfig = ( + this.client as { + clientConfig?: { + parameters?: {organizationId?: string; siteId?: string} + fetchOptions?: RequestInit + } } - }).clientConfig + ).clientConfig const organizationId = clientConfig?.parameters?.organizationId const channelId = clientConfig?.parameters?.siteId if (!organizationId || !channelId) { @@ -1377,7 +1379,7 @@ class Auth { try { const text = await res.text() if (text.trim()) { - const errorData = JSON.parse(text) as { message?: string } + const errorData = JSON.parse(text) as {message?: string} errorMessage = String(errorData?.message ?? '').trim() } } catch { diff --git a/packages/template-retail-react-app/package.json b/packages/template-retail-react-app/package.json index 9f133f8039..6f8538e1ad 100644 --- a/packages/template-retail-react-app/package.json +++ b/packages/template-retail-react-app/package.json @@ -105,7 +105,7 @@ }, { "path": "build/vendor.js", - "maxSize": "366 kB" + "maxSize": "367 kB" } ] } From 738d0819281e04292caeccbbc5f19d59c2f699ad Mon Sep 17 00:00:00 2001 From: Avinash Kumar Date: Thu, 26 Feb 2026 16:07:29 -0500 Subject: [PATCH 5/6] lint fixes --- packages/template-retail-react-app/CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/template-retail-react-app/CHANGELOG.md b/packages/template-retail-react-app/CHANGELOG.md index f567f4438e..70e15d03ae 100644 --- a/packages/template-retail-react-app/CHANGELOG.md +++ b/packages/template-retail-react-app/CHANGELOG.md @@ -1,4 +1,5 @@ ## v9.1.0-dev +- Increase vendor.js bundlesize limit to 367 kB - Add Node 24 support. Drop Node 16 support [#3652](https://github.com/SalesforceCommerceCloud/pwa-kit/pull/3652) - [Feature] Subscribe to marketing communications. Email capture component updated in footer section to use Shopper Consents API. [#3674](https://github.com/SalesforceCommerceCloud/pwa-kit/pull/3674) - [Bugfix] Fix for custom billing address as returning shoppers in 1CC [#3693](https://github.com/SalesforceCommerceCloud/pwa-kit/pull/3693) From f021d65bd99ea62621d73b0e751e1509691d6305 Mon Sep 17 00:00:00 2001 From: Avinash Kumar Date: Thu, 26 Feb 2026 16:12:00 -0500 Subject: [PATCH 6/6] lint fixes --- packages/commerce-sdk-react/CHANGELOG.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/packages/commerce-sdk-react/CHANGELOG.md b/packages/commerce-sdk-react/CHANGELOG.md index a32d1686f9..0a4e6bdd2c 100644 --- a/packages/commerce-sdk-react/CHANGELOG.md +++ b/packages/commerce-sdk-react/CHANGELOG.md @@ -5,8 +5,6 @@ ## v5.0.0 (Feb 12, 2026) - Upgrade to commerce-sdk-isomorphic v5.0.0 and introduce Payment Instrument SCAPI integration [#3552](https://github.com/SalesforceCommerceCloud/pwa-kit/pull/3552) - -## v4.4.0-dev (Dec 17, 2025) - [Bugfix] Ensure code_verifier can be optional in resetPassword call [#3567](https://github.com/SalesforceCommerceCloud/pwa-kit/pull/3567) - [Improvement] Strengthening typescript types on custom endpoint options and fetchOptions types [#3589](https://github.com/SalesforceCommerceCloud/pwa-kit/pull/3589) - [Feature] Update `authorizePasswordless`, `getPasswordResetToken`, and `resetPassword` to support use of `email` mode [#3525](https://github.com/SalesforceCommerceCloud/pwa-kit/pull/3525)