Skip to content

Commit 2b5d086

Browse files
authored
Merge pull request #3411 from SalesforceCommerceCloud/feature/slas-proxy-extensibility
Adds support for SLAS proxy req/res callbacks
2 parents baa0359 + 1bf252c commit 2b5d086

File tree

3 files changed

+257
-4
lines changed

3 files changed

+257
-4
lines changed

packages/pwa-kit-runtime/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
## v3.14.0-dev (Sep 26, 2025)
22
- Replace aws-serverless-express with @h4ad/serverless-adapter [#3325](https://github.com/SalesforceCommerceCloud/pwa-kit/pull/3325)
3+
- Add extensibility hooks for SLAS private client proxy with `onSLASPrivateProxyReq` and `onSLASPrivateProxyRes` callbacks [#3411](https://github.com/SalesforceCommerceCloud/pwa-kit/pull/3411)
34

45
## v3.13.0 (Sep 25, 2025)
56

packages/pwa-kit-runtime/src/ssr/server/build-remote-server.js

Lines changed: 69 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -157,7 +157,19 @@ export const RemoteServerFactory = {
157157
// To allow additional SLAS endpoints, users can override this value in
158158
// their project's ssr.js.
159159
applySLASPrivateClientToEndpoints:
160-
/\/oauth2\/(token|passwordless\/(login|token)|password\/(reset|action))/
160+
/\/oauth2\/(token|passwordless\/(login|token)|password\/(reset|action))/,
161+
162+
// Custom callback to modify the SLAS private client proxy request. This callback is invoked
163+
// after the built-in proxy request handling. Users can provide additional
164+
// request modifications (e.g., custom headers).
165+
// Signature: (proxyRequest, incomingRequest, res) => void
166+
onSLASPrivateProxyReq: undefined,
167+
168+
// Custom callback to modify the SLAS private client proxy response. This callback is invoked
169+
// after the built-in proxy response handling. Users can modify or replace
170+
// the response buffer.
171+
// Signature: (responseBuffer, proxyRes, req, res) => Buffer
172+
onSLASPrivateProxyRes: undefined
161173
}
162174

163175
options = Object.assign({}, defaults, options)
@@ -930,8 +942,23 @@ export const RemoteServerFactory = {
930942
// purpose so we don't want to overwrite the header for those calls.
931943
proxyRequest.setHeader('Authorization', `Basic ${encodedSlasCredentials}`)
932944
}
945+
946+
// Allow users to apply additional custom modifications to the proxy request
947+
if (typeof options.onSLASPrivateProxyReq === 'function') {
948+
try {
949+
options.onSLASPrivateProxyReq(proxyRequest, incomingRequest, res)
950+
} catch (error) {
951+
logger.error('Error in custom onSLASPrivateProxyReq callback', {
952+
namespace: '_setupSlasPrivateClientProxy',
953+
additionalProperties: {
954+
error: error
955+
}
956+
})
957+
}
958+
}
933959
},
934960
onProxyRes: responseInterceptor((responseBuffer, proxyRes, req, res) => {
961+
let workingBuffer = responseBuffer
935962
try {
936963
// If the passwordless login endpoint returns a 404, which corresponds to a user
937964
// email not being found, we mask it with a 200 OK response so that it is not
@@ -946,15 +973,43 @@ export const RemoteServerFactory = {
946973

947974
// When a /passwordless/login endpoint response returns 200, it has no body
948975
// so we return an empty body here to match an actual 200 response.
949-
return Buffer.from('', 'utf8')
976+
workingBuffer = Buffer.from('', 'utf8')
950977
}
951-
return responseBuffer
978+
979+
// Allow users to apply additional custom modifications to the proxy response
980+
if (typeof options.onSLASPrivateProxyRes === 'function') {
981+
try {
982+
const customBuffer = options.onSLASPrivateProxyRes(
983+
workingBuffer,
984+
proxyRes,
985+
req,
986+
res
987+
)
988+
// Only use the custom buffer if it was returned
989+
if (customBuffer !== undefined) {
990+
workingBuffer = customBuffer
991+
}
992+
} catch (error) {
993+
logger.error(
994+
'Error in custom onSLASPrivateProxyRes callback',
995+
/* istanbul ignore next */
996+
{
997+
namespace: '_setupSlasPrivateClientProxy',
998+
additionalProperties: {
999+
error: error
1000+
}
1001+
}
1002+
)
1003+
}
1004+
}
1005+
1006+
return workingBuffer
9521007
} catch (error) {
9531008
console.error(
9541009
'There is an error processing the response from SLAS. Returning original response.',
9551010
error
9561011
)
957-
return responseBuffer
1012+
return workingBuffer
9581013
}
9591014
})
9601015
})
@@ -1319,6 +1374,16 @@ export const RemoteServerFactory = {
13191374
* @param {Boolean} [options.allowCookies] - This boolean value indicates
13201375
* whether or not we strip cookies from requests and block setting of cookies. Defaults
13211376
* to 'false'.
1377+
* @param {Boolean} [options.useSLASPrivateClient=false] - Enable the SLAS private client
1378+
* proxy handler. Requires PWA_KIT_SLAS_CLIENT_SECRET environment variable.
1379+
* @param {RegExp} [options.applySLASPrivateClientToEndpoints] - A regex pattern to match
1380+
* SLAS endpoints where the Authorization header should be injected.
1381+
* @param {function} [options.onSLASPrivateProxyReq] - Custom callback to modify SLAS private client
1382+
* proxy requests. Called after built-in request handling. Signature: (proxyRequest, incomingRequest, res) => void.
1383+
* Use this to add custom headers or modify the proxy request.
1384+
* @param {function} [options.onSLASPrivateProxyRes] - Custom callback to modify SLAS private client
1385+
* proxy responses. Called after built-in response handling. Signature: (responseBuffer, proxyRes, req, res) => Buffer.
1386+
* Should return the modified buffer or undefined to use the existing buffer.
13221387
*/
13231388
createHandler(options, customizeApp) {
13241389
process.on('unhandledRejection', catchAndLog)

packages/pwa-kit-runtime/src/ssr/server/build-remote-server.test.js

Lines changed: 187 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -156,3 +156,190 @@ describe('isBinary function', () => {
156156
expect(isBinary(headers)).toBe(false)
157157
})
158158
})
159+
160+
describe('SLAS private proxy', () => {
161+
let request
162+
let mockExpress
163+
164+
beforeEach(() => {
165+
// Mock express application
166+
mockExpress = require('express')
167+
request = require('supertest')
168+
})
169+
170+
afterEach(() => {
171+
// Clean up environment variables
172+
delete process.env.PWA_KIT_SLAS_CLIENT_SECRET
173+
})
174+
175+
test('returns 404 when useSLASPrivateClient is false', async () => {
176+
const app = mockExpress()
177+
const options = {
178+
useSLASPrivateClient: false,
179+
mobify: {
180+
app: {
181+
commerceAPI: {
182+
parameters: {
183+
shortCode: 'test',
184+
clientId: 'test-client-id'
185+
}
186+
}
187+
}
188+
}
189+
}
190+
191+
RemoteServerFactory._setupSlasPrivateClientProxy(app, options)
192+
193+
// Attempt to access the SLAS private proxy path
194+
const response = await request(app).get('/mobify/slas/private/shopper/auth/v1/oauth2/token')
195+
196+
expect(response.status).toBe(404)
197+
})
198+
199+
test('returns 501 when useSLASPrivateClient is true but no secret is set', async () => {
200+
const app = mockExpress()
201+
const options = RemoteServerFactory._configure({
202+
useSLASPrivateClient: true,
203+
mobify: {
204+
app: {
205+
commerceAPI: {
206+
parameters: {
207+
shortCode: 'test',
208+
organizationId: 'f_ecom_test',
209+
clientId: 'test-client-id'
210+
}
211+
}
212+
}
213+
}
214+
})
215+
216+
RemoteServerFactory._setupSlasPrivateClientProxy(app, options)
217+
218+
const response = await request(app).get('/mobify/slas/private/shopper/auth/v1/oauth2/token')
219+
220+
expect(response.status).toBe(501)
221+
})
222+
223+
test('returns 403 for non-SLAS auth paths', async () => {
224+
const app = mockExpress()
225+
const options = RemoteServerFactory._configure({
226+
useSLASPrivateClient: true,
227+
mobify: {
228+
app: {
229+
commerceAPI: {
230+
parameters: {
231+
shortCode: 'test',
232+
organizationId: 'f_ecom_test',
233+
clientId: 'test-client-id'
234+
}
235+
}
236+
}
237+
}
238+
})
239+
240+
process.env.PWA_KIT_SLAS_CLIENT_SECRET = 'test-secret'
241+
242+
RemoteServerFactory._setupSlasPrivateClientProxy(app, options)
243+
244+
const response = await request(app).get('/mobify/slas/private/shopper/products/v1')
245+
246+
expect(response.status).toBe(403)
247+
})
248+
249+
test('returns 403 for trusted-system paths', async () => {
250+
const app = mockExpress()
251+
const options = RemoteServerFactory._configure({
252+
useSLASPrivateClient: true,
253+
mobify: {
254+
app: {
255+
commerceAPI: {
256+
parameters: {
257+
shortCode: 'test',
258+
organizationId: 'f_ecom_test',
259+
clientId: 'test-client-id'
260+
}
261+
}
262+
}
263+
}
264+
})
265+
266+
process.env.PWA_KIT_SLAS_CLIENT_SECRET = 'test-secret'
267+
268+
RemoteServerFactory._setupSlasPrivateClientProxy(app, options)
269+
270+
const response = await request(app).post(
271+
'/mobify/slas/private/shopper/auth/v1/oauth2/trusted-system/token'
272+
)
273+
274+
expect(response.status).toBe(403)
275+
})
276+
277+
test('invokes onSLASPrivateProxyReq callback and onSLASPrivateProxyRes callback', async () => {
278+
// Create a mock SLAS endpoint for the http-proxy to consume
279+
const mockSlasServer = mockExpress()
280+
mockSlasServer.post('/shopper/auth/v1/oauth2/token', (req, res) => {
281+
// Reflect the custom header back in the response to verify it was set
282+
res.status(200).json({
283+
access_token: 'mock-token',
284+
reflected_header: req.headers['x-custom-request-header']
285+
})
286+
})
287+
288+
const mockSlasServerInstance = mockSlasServer.listen(0)
289+
const mockSlasPort = mockSlasServerInstance.address().port
290+
291+
try {
292+
const onSLASPrivateProxyReqMock = jest.fn((proxyRequest) => {
293+
proxyRequest.setHeader('X-Custom-Request-Header', 'CustomRequestValue')
294+
})
295+
296+
const onSLASPrivateProxyResMock = jest.fn((responseBuffer, proxyRes, req, res) => {
297+
// Add a custom response header
298+
res.setHeader('X-Custom-Response-Header', 'CustomResponseValue')
299+
return responseBuffer
300+
})
301+
302+
const app = mockExpress()
303+
const options = RemoteServerFactory._configure({
304+
useSLASPrivateClient: true,
305+
slasTarget: `http://localhost:${mockSlasPort}`,
306+
onSLASPrivateProxyReq: onSLASPrivateProxyReqMock,
307+
onSLASPrivateProxyRes: onSLASPrivateProxyResMock,
308+
mobify: {
309+
app: {
310+
commerceAPI: {
311+
parameters: {
312+
shortCode: 'test',
313+
organizationId: 'f_ecom_test',
314+
clientId: 'test-client-id'
315+
}
316+
}
317+
}
318+
}
319+
})
320+
321+
process.env.PWA_KIT_SLAS_CLIENT_SECRET = 'test-secret'
322+
323+
RemoteServerFactory._setupSlasPrivateClientProxy(app, options)
324+
325+
const response = await request(app).post(
326+
'/mobify/slas/private/shopper/auth/v1/oauth2/token'
327+
)
328+
329+
// Verify the request was successful
330+
expect(response.status).toBe(200)
331+
332+
// Verify the callbacks were invoked
333+
expect(onSLASPrivateProxyReqMock).toHaveBeenCalled()
334+
expect(onSLASPrivateProxyResMock).toHaveBeenCalled()
335+
336+
// Verify the custom request header was added (reflected back in response)
337+
expect(response.body.reflected_header).toBe('CustomRequestValue')
338+
339+
// Verify the custom response header was added
340+
expect(response.headers['x-custom-response-header']).toBe('CustomResponseValue')
341+
} finally {
342+
mockSlasServerInstance.close()
343+
}
344+
})
345+
})

0 commit comments

Comments
 (0)