Skip to content

Commit 2b4e60a

Browse files
authored
Add --extra-headers flag and env var support for HTTP requests (#52)
Add support for custom HTTP headers via --extra-headers flag and SFCC_EXTRA_HEADERS environment variable. This enables CI environments to pass Cloudflare firewall headers via secrets. Changes: - Add headers field to ExtraParamsConfig interface - Update createExtraParamsMiddleware to apply extra headers - Add --extra-headers flag with SFCC_EXTRA_HEADERS env var - Add env vars to existing --extra-query (SFCC_EXTRA_QUERY) and --extra-body (SFCC_EXTRA_BODY) flags - Update E2E workflow to use SFCC_EXTRA_HEADERS secret - Add unit tests for header handling
1 parent 617132a commit 2b4e60a

File tree

5 files changed

+187
-2
lines changed

5 files changed

+187
-2
lines changed

.github/workflows/e2e-shell-tests.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,7 @@ jobs:
8080
SFCC_SANDBOX_API_HOST: ${{ vars.SFCC_SANDBOX_API_HOST }}
8181
SFCC_SHORTCODE: ${{ vars.SFCC_SHORTCODE }}
8282
TEST_REALM: ${{ vars.TEST_REALM }}
83+
SFCC_EXTRA_HEADERS: ${{ secrets.SFCC_EXTRA_HEADERS }}
8384
run: |
8485
echo "Running E2E shell tests with realm: ${TEST_REALM}"
8586
cd packages/b2c-cli

packages/b2c-tooling-sdk/src/cli/base-command.ts

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -75,11 +75,19 @@ export abstract class BaseCommand<T extends typeof Command> extends Command {
7575
}),
7676
'extra-query': Flags.string({
7777
description: 'Extra query parameters as JSON (e.g., \'{"debug":"true"}\')',
78+
env: 'SFCC_EXTRA_QUERY',
7879
helpGroup: 'GLOBAL',
7980
hidden: true,
8081
}),
8182
'extra-body': Flags.string({
8283
description: 'Extra body fields to merge as JSON (e.g., \'{"_internal":true}\')',
84+
env: 'SFCC_EXTRA_BODY',
85+
helpGroup: 'GLOBAL',
86+
hidden: true,
87+
}),
88+
'extra-headers': Flags.string({
89+
description: 'Extra HTTP headers as JSON (e.g., \'{"X-Custom-Header": "value"}\')',
90+
env: 'SFCC_EXTRA_HEADERS',
8391
helpGroup: 'GLOBAL',
8492
hidden: true,
8593
}),
@@ -307,16 +315,17 @@ export abstract class BaseCommand<T extends typeof Command> extends Command {
307315
}
308316

309317
/**
310-
* Parse extra params from --extra-query and --extra-body flags.
318+
* Parse extra params from --extra-query, --extra-body, and --extra-headers flags.
311319
* Returns undefined if no extra params are specified.
312320
*
313321
* @returns ExtraParamsConfig or undefined
314322
*/
315323
protected getExtraParams(): ExtraParamsConfig | undefined {
316324
const extraQuery = this.flags['extra-query'];
317325
const extraBody = this.flags['extra-body'];
326+
const extraHeaders = this.flags['extra-headers'];
318327

319-
if (!extraQuery && !extraBody) {
328+
if (!extraQuery && !extraBody && !extraHeaders) {
320329
return undefined;
321330
}
322331

@@ -338,6 +347,14 @@ export abstract class BaseCommand<T extends typeof Command> extends Command {
338347
}
339348
}
340349

350+
if (extraHeaders) {
351+
try {
352+
config.headers = JSON.parse(extraHeaders) as Record<string, string>;
353+
} catch {
354+
this.error(`Invalid JSON for --extra-headers: ${extraHeaders}`);
355+
}
356+
}
357+
341358
return config;
342359
}
343360
}

packages/b2c-tooling-sdk/src/clients/middleware.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@ export interface ExtraParamsConfig {
2323
query?: Record<string, string | number | boolean | undefined>;
2424
/** Extra body fields to merge into JSON request bodies */
2525
body?: Record<string, unknown>;
26+
/** Extra HTTP headers to add to all requests */
27+
headers?: Record<string, string>;
2628
}
2729

2830
/**
@@ -206,6 +208,21 @@ export function createExtraParamsMiddleware(config: ExtraParamsConfig): Middlewa
206208
async onRequest({request}) {
207209
let modifiedRequest = request;
208210

211+
// Add extra headers first (before other modifications)
212+
if (config.headers && Object.keys(config.headers).length > 0) {
213+
const newHeaders = new Headers(modifiedRequest.headers);
214+
for (const [key, value] of Object.entries(config.headers)) {
215+
newHeaders.set(key, value);
216+
}
217+
logger.trace({extraHeaders: config.headers}, '[ExtraParams] Adding extra headers to request');
218+
modifiedRequest = new Request(modifiedRequest.url, {
219+
method: modifiedRequest.method,
220+
headers: newHeaders,
221+
body: modifiedRequest.body,
222+
duplex: modifiedRequest.body ? 'half' : undefined,
223+
} as RequestInit);
224+
}
225+
209226
// Add extra query parameters
210227
if (config.query && Object.keys(config.query).length > 0) {
211228
const url = new URL(request.url);

packages/b2c-tooling-sdk/test/cli/base-command.test.ts

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -449,6 +449,73 @@ describe('cli/base-command', () => {
449449
cmd.error = originalError;
450450
cmd.parse = originalParse;
451451
});
452+
453+
it('parses extra-headers flag', async () => {
454+
const cmd = command as MockableBaseCommand;
455+
const originalParse = cmd.parse.bind(command);
456+
cmd.parse = (async () => ({
457+
args: {},
458+
flags: {'extra-headers': '{"X-Custom-Header":"value"}'},
459+
metadata: {},
460+
})) as typeof cmd.parse;
461+
462+
await cmd.init();
463+
const params = command.testGetExtraParams();
464+
expect(params?.headers).to.deep.equal({'X-Custom-Header': 'value'});
465+
466+
cmd.parse = originalParse;
467+
});
468+
469+
it('parses extra-query, extra-body, and extra-headers together', async () => {
470+
const cmd = command as MockableBaseCommand;
471+
const originalParse = cmd.parse.bind(command);
472+
cmd.parse = (async () => ({
473+
args: {},
474+
flags: {
475+
'extra-query': '{"debug":"true"}',
476+
'extra-body': '{"_internal":true}',
477+
'extra-headers': '{"X-Custom":"value"}',
478+
},
479+
metadata: {},
480+
})) as typeof cmd.parse;
481+
482+
await cmd.init();
483+
const params = command.testGetExtraParams();
484+
expect(params?.query).to.deep.equal({debug: 'true'});
485+
expect(params?.body).to.deep.equal({_internal: true});
486+
expect(params?.headers).to.deep.equal({'X-Custom': 'value'});
487+
488+
cmd.parse = originalParse;
489+
});
490+
491+
it('throws error for invalid JSON in extra-headers', async () => {
492+
const cmd = command as MockableBaseCommand;
493+
const originalParse = cmd.parse.bind(command);
494+
cmd.parse = (async () => ({
495+
args: {},
496+
flags: {'extra-headers': 'invalid-json'},
497+
metadata: {},
498+
})) as typeof cmd.parse;
499+
500+
await cmd.init();
501+
let errorCalled = false;
502+
const originalError = cmd.error.bind(command);
503+
cmd.error = () => {
504+
errorCalled = true;
505+
throw new Error('Expected error');
506+
};
507+
508+
try {
509+
command.testGetExtraParams();
510+
} catch {
511+
// Expected
512+
}
513+
514+
expect(errorCalled).to.be.true;
515+
516+
cmd.error = originalError;
517+
cmd.parse = originalParse;
518+
});
452519
});
453520

454521
describe('baseCommandTest', () => {

packages/b2c-tooling-sdk/test/clients/middleware.test.ts

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -256,6 +256,89 @@ describe('clients/middleware', () => {
256256
const body = JSON.parse(await modifiedRequest.text()) as Record<string, unknown>;
257257
expect(body.forced).to.equal(true);
258258
});
259+
260+
it('adds extra headers to request', async () => {
261+
const middleware = createExtraParamsMiddleware({headers: {'X-Custom': 'value'}});
262+
type OnRequestParams = Parameters<NonNullable<typeof middleware.onRequest>>[0];
263+
264+
const request = new Request('https://example.com/items', {method: 'GET'});
265+
const modifiedRequest = await middleware.onRequest!({request} as unknown as OnRequestParams);
266+
267+
if (!modifiedRequest) {
268+
throw new Error('Expected middleware to return a Request');
269+
}
270+
271+
expect(modifiedRequest.headers.get('X-Custom')).to.equal('value');
272+
});
273+
274+
it('overwrites existing headers with extra headers', async () => {
275+
const middleware = createExtraParamsMiddleware({headers: {'X-Custom': 'new-value'}});
276+
type OnRequestParams = Parameters<NonNullable<typeof middleware.onRequest>>[0];
277+
278+
const request = new Request('https://example.com/items', {
279+
method: 'GET',
280+
headers: {'X-Custom': 'old-value'},
281+
});
282+
const modifiedRequest = await middleware.onRequest!({request} as unknown as OnRequestParams);
283+
284+
if (!modifiedRequest) {
285+
throw new Error('Expected middleware to return a Request');
286+
}
287+
288+
expect(modifiedRequest.headers.get('X-Custom')).to.equal('new-value');
289+
});
290+
291+
it('preserves other headers when adding extra headers', async () => {
292+
const middleware = createExtraParamsMiddleware({headers: {'X-Custom': 'value'}});
293+
type OnRequestParams = Parameters<NonNullable<typeof middleware.onRequest>>[0];
294+
295+
const request = new Request('https://example.com/items', {
296+
method: 'GET',
297+
headers: {'Content-Type': 'application/json'},
298+
});
299+
const modifiedRequest = await middleware.onRequest!({request} as unknown as OnRequestParams);
300+
301+
if (!modifiedRequest) {
302+
throw new Error('Expected middleware to return a Request');
303+
}
304+
305+
expect(modifiedRequest.headers.get('X-Custom')).to.equal('value');
306+
expect(modifiedRequest.headers.get('Content-Type')).to.equal('application/json');
307+
});
308+
309+
it('does nothing when headers config is empty', async () => {
310+
const middleware = createExtraParamsMiddleware({headers: {}});
311+
type OnRequestParams = Parameters<NonNullable<typeof middleware.onRequest>>[0];
312+
313+
const request = new Request('https://example.com/items', {method: 'GET'});
314+
const modifiedRequest = await middleware.onRequest!({request} as unknown as OnRequestParams);
315+
316+
if (!modifiedRequest) {
317+
throw new Error('Expected middleware to return a Request');
318+
}
319+
320+
expect(modifiedRequest.url).to.equal(request.url);
321+
});
322+
323+
it('adds multiple extra headers', async () => {
324+
const middleware = createExtraParamsMiddleware({
325+
headers: {
326+
'CF-Access-Client-Id': 'client-id',
327+
'CF-Access-Client-Secret': 'client-secret',
328+
},
329+
});
330+
type OnRequestParams = Parameters<NonNullable<typeof middleware.onRequest>>[0];
331+
332+
const request = new Request('https://example.com/items', {method: 'GET'});
333+
const modifiedRequest = await middleware.onRequest!({request} as unknown as OnRequestParams);
334+
335+
if (!modifiedRequest) {
336+
throw new Error('Expected middleware to return a Request');
337+
}
338+
339+
expect(modifiedRequest.headers.get('CF-Access-Client-Id')).to.equal('client-id');
340+
expect(modifiedRequest.headers.get('CF-Access-Client-Secret')).to.equal('client-secret');
341+
});
259342
});
260343

261344
describe('createLoggingMiddleware', () => {

0 commit comments

Comments
 (0)