@salesforce/mrt-utilities 0.0.1 → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (69) hide show
  1. package/README.md +49 -45
  2. package/dist/cjs/index.d.ts +4 -0
  3. package/dist/cjs/index.js +10 -0
  4. package/dist/cjs/index.js.map +1 -0
  5. package/dist/cjs/metrics/index.d.ts +1 -0
  6. package/dist/cjs/metrics/index.js +7 -0
  7. package/dist/cjs/metrics/index.js.map +1 -0
  8. package/dist/cjs/metrics/metrics-sender.d.ts +136 -0
  9. package/dist/cjs/metrics/metrics-sender.js +240 -0
  10. package/dist/cjs/metrics/metrics-sender.js.map +1 -0
  11. package/dist/cjs/middleware/data-store.d.ts +56 -0
  12. package/dist/cjs/middleware/data-store.js +124 -0
  13. package/dist/cjs/middleware/data-store.js.map +1 -0
  14. package/dist/cjs/middleware/index.d.ts +3 -0
  15. package/dist/cjs/middleware/index.js +8 -0
  16. package/dist/cjs/middleware/index.js.map +1 -0
  17. package/dist/cjs/middleware/middleware.d.ts +126 -0
  18. package/dist/cjs/middleware/middleware.js +411 -0
  19. package/dist/cjs/middleware/middleware.js.map +1 -0
  20. package/dist/cjs/package.json +1 -0
  21. package/dist/cjs/streaming/create-lambda-adapter.d.ts +68 -0
  22. package/dist/cjs/streaming/create-lambda-adapter.js +797 -0
  23. package/dist/cjs/streaming/create-lambda-adapter.js.map +1 -0
  24. package/dist/cjs/streaming/index.d.ts +1 -0
  25. package/dist/cjs/streaming/index.js +7 -0
  26. package/dist/cjs/streaming/index.js.map +1 -0
  27. package/dist/cjs/utils/configure-proxying.d.ts +160 -0
  28. package/dist/cjs/utils/configure-proxying.js +204 -0
  29. package/dist/cjs/utils/configure-proxying.js.map +1 -0
  30. package/dist/cjs/utils/ssr-proxying.d.ts +300 -0
  31. package/dist/cjs/utils/ssr-proxying.js +713 -0
  32. package/dist/cjs/utils/ssr-proxying.js.map +1 -0
  33. package/dist/cjs/utils/utils.d.ts +27 -0
  34. package/dist/cjs/utils/utils.js +44 -0
  35. package/dist/cjs/utils/utils.js.map +1 -0
  36. package/dist/esm/index.d.ts +4 -0
  37. package/dist/esm/index.js +10 -0
  38. package/dist/esm/index.js.map +1 -0
  39. package/dist/esm/metrics/index.d.ts +1 -0
  40. package/dist/esm/metrics/index.js +7 -0
  41. package/dist/esm/metrics/index.js.map +1 -0
  42. package/dist/esm/metrics/metrics-sender.d.ts +136 -0
  43. package/dist/esm/metrics/metrics-sender.js +240 -0
  44. package/dist/esm/metrics/metrics-sender.js.map +1 -0
  45. package/dist/esm/middleware/data-store.d.ts +56 -0
  46. package/dist/esm/middleware/data-store.js +124 -0
  47. package/dist/esm/middleware/data-store.js.map +1 -0
  48. package/dist/esm/middleware/index.d.ts +3 -0
  49. package/dist/esm/middleware/index.js +9 -0
  50. package/dist/esm/middleware/index.js.map +1 -0
  51. package/dist/esm/middleware/middleware.d.ts +126 -0
  52. package/dist/esm/middleware/middleware.js +411 -0
  53. package/dist/esm/middleware/middleware.js.map +1 -0
  54. package/dist/esm/streaming/create-lambda-adapter.d.ts +68 -0
  55. package/dist/esm/streaming/create-lambda-adapter.js +797 -0
  56. package/dist/esm/streaming/create-lambda-adapter.js.map +1 -0
  57. package/dist/esm/streaming/index.d.ts +1 -0
  58. package/dist/esm/streaming/index.js +7 -0
  59. package/dist/esm/streaming/index.js.map +1 -0
  60. package/dist/esm/utils/configure-proxying.d.ts +160 -0
  61. package/dist/esm/utils/configure-proxying.js +204 -0
  62. package/dist/esm/utils/configure-proxying.js.map +1 -0
  63. package/dist/esm/utils/ssr-proxying.d.ts +300 -0
  64. package/dist/esm/utils/ssr-proxying.js +713 -0
  65. package/dist/esm/utils/ssr-proxying.js.map +1 -0
  66. package/dist/esm/utils/utils.d.ts +27 -0
  67. package/dist/esm/utils/utils.js +44 -0
  68. package/dist/esm/utils/utils.js.map +1 -0
  69. package/package.json +129 -7
@@ -0,0 +1,713 @@
1
+ /*
2
+ * Copyright (c) 2025, Salesforce, Inc.
3
+ * SPDX-License-Identifier: Apache-2
4
+ * For full license text, see the license.txt file in the repo root or http://www.apache.org/licenses/LICENSE-2.0
5
+ */
6
+ /**
7
+ * @fileoverview SSR (Server-Side Rendering) Proxying utilities for MRT middleware.
8
+ *
9
+ * This module provides utilities for handling HTTP headers, cookies, and proxying
10
+ * in both Express.js applications and AWS Lambda@Edge functions. It's designed
11
+ * to work in multiple contexts while maintaining consistency.
12
+ *
13
+ * Special requirements:
14
+ * - Don't add any functionality in here that is not required by the proxying code
15
+ * - Avoid importing any other modules not explicitly used by this code
16
+ * - Must work in both Express.js and Lambda@Edge environments
17
+ *
18
+ * @author Salesforce Commerce Cloud
19
+ * @version 0.0.1
20
+ */
21
+ /*
22
+ There are some special requirements for this module, which is used in the
23
+ SDK and also in Lambda@Edge functions run by CloudFront. Specifically:
24
+ - Don't add any functionality in here that is not required by the
25
+ proxying code.
26
+ - Avoid importing any other modules not explicitly used by this code
27
+ */
28
+ import { parse as parseSetCookie } from 'set-cookie-parser';
29
+ import { trainCase } from 'change-case';
30
+ import { URL } from 'url';
31
+ const AC_ALLOW_ORIGIN = 'access-control-allow-origin';
32
+ const HOST = 'host';
33
+ const LOCATION = 'location';
34
+ const ORIGIN = 'origin';
35
+ const SET_COOKIE = 'set-cookie';
36
+ const USER_AGENT = 'user-agent';
37
+ const HEADER_FORMATS = ['http', 'aws'];
38
+ export const X_PROXY_REQUEST_URL = 'x-proxy-request-url';
39
+ export const X_MOBIFY_REQUEST_CLASS = 'x-mobify-request-class';
40
+ export const MAX_URL_LENGTH_BYTES = 8192;
41
+ /**
42
+ * This class provides a representation of HTTP request or response
43
+ * headers, that operates in the same way in multiple contexts
44
+ * (i.e. within the Express app as well as the request-processor).
45
+ *
46
+ * Within a Headers instance, headers are referenced using lower-case
47
+ * names. Use getHeader to access the value for a header. If there
48
+ * are multiple values, this will return the first value. This class
49
+ * internally supports round-trip preservation of multi-value headers,
50
+ * but does not yet provide a way to access them.
51
+ */
52
+ export class Headers {
53
+ httpFormat;
54
+ headers;
55
+ _modified;
56
+ /*
57
+ A Lambda@Edge event contains headers in this form:
58
+ "headers": {
59
+ "host": [
60
+ {
61
+ "key": "Host",
62
+ "value": "d111111abcdef8.cloudfront.net"
63
+ }
64
+ ],
65
+ "user-agent": [
66
+ {
67
+ "key": "User-Agent",
68
+ "value": "curl/7.18.1"
69
+ }
70
+ ]
71
+ }
72
+
73
+ The http.IncomingMessage format is a simple object:
74
+ {
75
+ 'user-agent': 'curl/7.22.0',
76
+ host: '127.0.0.1:8000'
77
+ }
78
+
79
+ However, for IncomingMessage:
80
+ Duplicates of age, authorization, content-length, content-type, etag,
81
+ expires, from, host, if-modified-since, if-unmodified-since,
82
+ last-modified, location, max-forwards, proxy-authorization, referer,
83
+ retry-after, or user-agent are discarded.
84
+ The value for set-cookie is always an array. Duplicates are added to the array.
85
+ For all other headers, the values are joined together with ','
86
+
87
+ */
88
+ /**
89
+ * Construct a Headers object from either an AWS Lambda Event headers
90
+ * object, or an http.IncomingMessage headers object.
91
+ *
92
+ * Project code should never need to call this constructor.
93
+ *
94
+ * @private
95
+ * @param headers the input headers
96
+ * @param format either 'http' or 'aws'
97
+ */
98
+ constructor(headers, format) {
99
+ if (!HEADER_FORMATS.includes(format)) {
100
+ throw new Error(`Headers format must be one of ${HEADER_FORMATS.join(', ')}`);
101
+ }
102
+ this.httpFormat = format === 'http';
103
+ // Within this class, headers are represented by an object that maps
104
+ // header names (lower-case) to arrays of values.
105
+ this.headers = {};
106
+ for (const [key, values] of Object.entries(headers)) {
107
+ // For http format, the value will be a comma-separated
108
+ // list of values (https://www.w3.org/Protocols/rfc2616/rfc2616-sec4.html#sec4.2)
109
+ // For AWS format, the value is an array of key/value objects.
110
+ if (this.httpFormat) {
111
+ // The Set-Cookie header is always passed to us as an array,
112
+ // so we do not split.
113
+ if (key === SET_COOKIE) {
114
+ this.headers[key] = values.slice();
115
+ }
116
+ else {
117
+ this.headers[key] = values.split(/,\s*/).map((value) => value.trim());
118
+ }
119
+ }
120
+ else {
121
+ this.headers[key] = values.map((value) => value.value.trim());
122
+ }
123
+ }
124
+ this._modified = false;
125
+ }
126
+ /**
127
+ * Return true if and only if any set or delete methods were called on
128
+ * this instance after construction. This does not actually test if
129
+ * the headers values have been changed, just whether any mutating
130
+ * methods have been called.
131
+ * @returns {Boolean}
132
+ */
133
+ get modified() {
134
+ return this._modified;
135
+ }
136
+ /**
137
+ * Return an array of the header keys (all lower-case)
138
+ */
139
+ keys() {
140
+ return Object.keys(this.headers);
141
+ }
142
+ /**
143
+ * Get the value of the set-cookie header(s), returning an array
144
+ * of strings. Always returns an array, even if it's empty.
145
+ */
146
+ getSetCookie() {
147
+ return this.headers[SET_COOKIE] || [];
148
+ }
149
+ /**
150
+ * Set the value of the set-cookie header(s)
151
+ * @param values Array of set-cookie header values
152
+ */
153
+ setSetCookie(values) {
154
+ this._modified = true;
155
+ if (!(values && values.length)) {
156
+ delete this.headers[SET_COOKIE];
157
+ return;
158
+ }
159
+ // Clone the array
160
+ this.headers[SET_COOKIE] = values.slice();
161
+ }
162
+ /**
163
+ * Return the FIRST value of the header with the given key.
164
+ * This is for single-value headers only: Location, Access-Control-*, etc
165
+ * If the header is not present, returns undefined.
166
+ * @param key header name
167
+ */
168
+ getHeader(key) {
169
+ const keyLC = key.toLowerCase();
170
+ const values = this.headers[keyLC];
171
+ if (!values) {
172
+ return undefined;
173
+ }
174
+ return values[0];
175
+ }
176
+ /**
177
+ * Set the value of the header with the given key. This is for single-
178
+ * value headers only (see getHeader). Setting the value removes ALL other
179
+ * values for the given key.
180
+ * @param key header name
181
+ * @param value header value
182
+ */
183
+ setHeader(key, value) {
184
+ this._modified = true;
185
+ const keyLC = key.toLowerCase();
186
+ this.headers[keyLC] = [value];
187
+ }
188
+ /**
189
+ * Remove any header with the given key
190
+ * @param key header name to remove
191
+ */
192
+ deleteHeader(key) {
193
+ this._modified = true;
194
+ const keyLC = key.toLowerCase();
195
+ delete this.headers[keyLC];
196
+ }
197
+ /**
198
+ * Return the headers in AWS (Lambda event) format.
199
+ *
200
+ * Project code should never need to use this method.
201
+ */
202
+ toAWSFormat() {
203
+ const result = {};
204
+ for (const [key, values] of Object.entries(this.headers)) {
205
+ // Some customer servers return headers with unusual keys; for
206
+ // example, 'cached_response'. Underscores are technically legal
207
+ // in header keys, but are unexpected. The problem is that the
208
+ // header-case package maps underscore to '-' to get "legal"
209
+ // names, which breaks Lambda validation. So if the key
210
+ // contains an underscore, we use it as-is.
211
+ const finalKey = key.includes('_') ? key : trainCase(key);
212
+ result[key] = values.map((value) => ({
213
+ key: finalKey,
214
+ value,
215
+ }));
216
+ }
217
+ return result;
218
+ }
219
+ /**
220
+ * Return the headers in Express (http.IncomingMessage) format.
221
+ *
222
+ * RFC2616 allows some flexibility in how multiple values are
223
+ * combined into a single header value. We separate with ', '
224
+ * rather than just ',' to maintain previous behaviour.
225
+ *
226
+ * Project code should never need to use this method.
227
+ */
228
+ toHTTPFormat() {
229
+ const result = {};
230
+ for (const [key, values] of Object.entries(this.headers)) {
231
+ // The Set-Cookie header is always returned as an array
232
+ if (key === SET_COOKIE) {
233
+ result[key] = values.slice();
234
+ }
235
+ else {
236
+ result[key] = values.join(', ');
237
+ }
238
+ }
239
+ return result;
240
+ }
241
+ /**
242
+ * Return the headers in the same format (aws or http) that was
243
+ * used to construct them.
244
+ *
245
+ * Project code should never need to use this method.
246
+ */
247
+ toObject() {
248
+ return this.httpFormat ? this.toHTTPFormat() : this.toAWSFormat();
249
+ }
250
+ }
251
+ const dayNames = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'];
252
+ const monthAbbr = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
253
+ // Return the given number as a 2-digit string with a leading zero
254
+ const leadingZero = (n) => {
255
+ const s = Number(n).toString();
256
+ return s.length > 1 ? s : `0${s}`;
257
+ };
258
+ /**
259
+ * Return the given date as an RFC1123-format string, suitable for
260
+ * use in a Set-Cookie or Date header. The result is always in UTC.
261
+ * @function
262
+ * @param date Date object
263
+ * @returns RFC1123 formatted date string
264
+ */
265
+ export const rfc1123 = (date) => {
266
+ const time = [
267
+ leadingZero(date.getUTCHours()),
268
+ leadingZero(date.getUTCMinutes()),
269
+ leadingZero(date.getUTCSeconds()),
270
+ ].join(':');
271
+ return [
272
+ `${dayNames[date.getUTCDay()]},`,
273
+ leadingZero(date.getUTCDate()),
274
+ monthAbbr[date.getUTCMonth()],
275
+ date.getUTCFullYear(),
276
+ time,
277
+ 'GMT',
278
+ ].join(' ');
279
+ };
280
+ /**
281
+ * Given a cookie object parsed by set-cookie-parser,
282
+ * return a set-cookie header value for it.
283
+ * @private
284
+ */
285
+ export const cookieAsString = (cookie) => {
286
+ const elements = [`${cookie.name}=${cookie.value}`];
287
+ if (cookie.path) {
288
+ elements.push(`Path=${cookie.path}`);
289
+ }
290
+ if (cookie.expires) {
291
+ // This is a Date object and must be represented as
292
+ // an HTTP-date timestamp (RFC1123 format). For example,
293
+ // Wed, 24-Oct-2018 00:13:20 GMT
294
+ elements.push(`Expires=${rfc1123(cookie.expires)}`);
295
+ }
296
+ if (cookie.domain) {
297
+ elements.push(`Domain=${cookie.domain}`);
298
+ }
299
+ if (cookie.maxAge) {
300
+ elements.push(`Max-Age=${cookie.maxAge}`);
301
+ }
302
+ if (cookie.secure) {
303
+ elements.push('Secure');
304
+ }
305
+ if (cookie.httpOnly) {
306
+ elements.push('HttpOnly');
307
+ }
308
+ if (cookie.sameSite) {
309
+ elements.push(`SameSite=${cookie.sameSite}`);
310
+ }
311
+ return elements.join('; ');
312
+ };
313
+ const IPV6 = 'IPV6';
314
+ const IPV4 = 'IPV4';
315
+ const LOCALHOST = 'localhost';
316
+ /**
317
+ * An array of {type, regexp, isIPOrLocalhost} objects, where the type is used
318
+ * to determine how to get a port, regexp will spot an IPv4, IPv6 or hostname,
319
+ * and the isIPOrLocalhost flag is true for IP addresses or locahost, false for
320
+ * hostnames. The main reason for detecting IP address/localhost is to determine
321
+ * whether it can have subdomains.
322
+ * @private
323
+ */
324
+ const HOST_AND_PORT_TESTS = [
325
+ // IPV6 address plus port
326
+ {
327
+ type: IPV6,
328
+ regexp: /^\[([a-fA-F0-9:]+)\]:(\d+)$/,
329
+ isIPOrLocalhost: true,
330
+ hasPort: true,
331
+ },
332
+ // IPV6 address without port
333
+ {
334
+ type: IPV6,
335
+ regexp: /^([a-fA-F0-9:]+)$/,
336
+ isIPOrLocalhost: true,
337
+ hasPort: false,
338
+ },
339
+ // IPV4 address plus port
340
+ {
341
+ type: IPV4,
342
+ regexp: /^(\d+\.\d+\.\d+\.\d+):(\d+)$/,
343
+ isIPOrLocalhost: true,
344
+ hasPort: true,
345
+ },
346
+ // IPV4 address plus without port
347
+ {
348
+ type: IPV4,
349
+ regexp: /^(\d+\.\d+\.\d+\.\d+)$/,
350
+ isIPOrLocalhost: true,
351
+ hasPort: false,
352
+ },
353
+ // localhost plus port
354
+ {
355
+ type: LOCALHOST,
356
+ regexp: /^(localhost):(\d+)$/,
357
+ isIPOrLocalhost: true,
358
+ hasPort: true,
359
+ },
360
+ // localhost without port
361
+ {
362
+ type: LOCALHOST,
363
+ regexp: /^(localhost)$/,
364
+ isIPOrLocalhost: true,
365
+ hasPort: false,
366
+ },
367
+ // hostname plus port
368
+ {
369
+ type: null,
370
+ regexp: /(.+):\d+/,
371
+ isIPOrLocalhost: false, // False means the hostname may have a subdomain
372
+ hasPort: true,
373
+ },
374
+ ];
375
+ /**
376
+ * Given a hostname that may be a hostname or ip address optionally
377
+ * followed by a port, return an object with 'host' being the ip address,
378
+ * the hostname, a port (if there is one), and 'isIPOrLocalhost' true for an ip
379
+ * address or localhost, false for a hostname
380
+ * @private
381
+ * @param host Can be localhost, an IP or domain name
382
+ * @return ParsedHost object
383
+ */
384
+ export const parseHost = (host) => {
385
+ for (const test of HOST_AND_PORT_TESTS) {
386
+ const match = test.regexp.exec(host);
387
+ if (!match) {
388
+ continue;
389
+ }
390
+ const result = {
391
+ host,
392
+ hostname: match[1],
393
+ isIPOrLocalhost: test.isIPOrLocalhost,
394
+ };
395
+ // Split apart and get the hostname and port from the host
396
+ if (test.hasPort) {
397
+ let parts;
398
+ switch (test.type) {
399
+ case IPV6:
400
+ // Filter out empty nodes fromt he array. We only care about
401
+ // the ones that have values
402
+ parts = result.host.split(test.regexp).filter((part) => part.length > 0);
403
+ break;
404
+ case IPV4:
405
+ case LOCALHOST:
406
+ default:
407
+ parts = result.host.split(':');
408
+ break;
409
+ }
410
+ [result.hostname, result.port] = parts;
411
+ }
412
+ return result;
413
+ }
414
+ return {
415
+ host,
416
+ hostname: host,
417
+ isIPOrLocalhost: false,
418
+ };
419
+ };
420
+ // Cookie domain rewrite logic for rewriteSetCookies below
421
+ export const rewriteDomain = (domain, appHostname, targetHost) => {
422
+ // Strip any leading dots off the domain and split into elements
423
+ const domainElements = domain.split('.').filter((x) => x);
424
+ const domainString = domainElements.join('.');
425
+ // Does the appHostname include a port number? We need a version
426
+ // of it without the port (hostname) because set-cookie domains cannot
427
+ // include ports. We can't just test for ':' because the host might be an
428
+ // ipv6 address. An ipv6 address containing a port contains the
429
+ // actual IP surrounded by [] (e.g. [2001:db8::1]:8080)
430
+ // RFC3986
431
+ const parsedHost = parseHost(appHostname);
432
+ // If the target host equals or ends with the domainString
433
+ // value, then we change the domain to be the appHostname
434
+ // (though for localhost, we strip off the port number)
435
+ if (targetHost === domainString) {
436
+ // Straight replacement
437
+ return parsedHost.hostname;
438
+ }
439
+ if (!targetHost.endsWith(domainString)) {
440
+ // Third-party cookie... leave unchanged
441
+ return domain;
442
+ }
443
+ // Cookie is set for a subdomain.
444
+ if (parsedHost.isIPOrLocalhost) {
445
+ // No subdomains for IP addresses or localhost, so return just that domain
446
+ return parsedHost.hostname;
447
+ }
448
+ // This is tricky... there's no standard way to get the domain for
449
+ // a hostname. We use a shortcut - we build up a subdomain based on the
450
+ // appHost that has the same number of elements as the cookie domain.
451
+ const targetHostElements = targetHost.split('.');
452
+ // Work out how many elements have been removed to form the subdomain
453
+ const strippedOff = targetHostElements.length - domainElements.length;
454
+ // Strip the same number of elements off the appHost
455
+ const appHostnameElements = parsedHost.hostname.split('.');
456
+ return appHostnameElements.slice(strippedOff).join('.');
457
+ };
458
+ /**
459
+ * Given a headers object, rewrite any set-cookie headers in it
460
+ * so that they apply to the app hostname rather than the target
461
+ * hostname.
462
+ *
463
+ * @private
464
+ * @param params Configuration object for rewriting set-cookies
465
+ * @returns string[] of rewritten set-cookie header values
466
+ */
467
+ export const rewriteSetCookies = ({ appHostname, setCookies, targetHost, logging = false, }) => {
468
+ if (!(setCookies && setCookies.length)) {
469
+ return [];
470
+ }
471
+ // Parse the set-cookie headers into a set of objects
472
+ const oldCookies = parseSetCookie(setCookies, { decodeValues: false });
473
+ // Map the oldCookies array into an array of updated objects
474
+ const newCookies = oldCookies.map((cookie) => {
475
+ if (cookie.domain) {
476
+ const newDomain = rewriteDomain(cookie.domain, appHostname, targetHost);
477
+ /* istanbul ignore next */
478
+ if (logging) {
479
+ console.log(`Rewriting proxy response set-cookie header domain from "${cookie.domain}" to "${newDomain}"`);
480
+ }
481
+ cookie.domain = newDomain;
482
+ }
483
+ return cookie;
484
+ });
485
+ // Convert the cookies back to string values
486
+ return newCookies.map(cookieAsString);
487
+ };
488
+ /**
489
+ * Rewrite headers for a proxied response.
490
+ *
491
+ * 1. If the original domain appears in the
492
+ * Access-Control-Allow-Origin header, it's replaced with the
493
+ * appOrigin.
494
+ * 2. If the response is a 30x redirection and contains a Location
495
+ * header on the target host, that header is rewritten to use the
496
+ * app host and proxy path.
497
+ *
498
+ * For a caching proxy, we also remove any Set-Cookie headers - caching
499
+ * proxies don't pass Cookie headers for requests and don't allow Set-Cookie
500
+ * in responses, so that they may be cached independently of any cookie
501
+ * values.
502
+ *
503
+ * @private
504
+ * @param params Configuration object for rewriting proxy response headers
505
+ * @returns the modified response headers
506
+ */
507
+ export const rewriteProxyResponseHeaders = ({ appHostname, caching, headers, headerFormat = 'http', proxyPath, requestUrl, statusCode = 200, targetProtocol, targetHost, appProtocol = 'https', logging = false, }) => {
508
+ const workingHeaders = new Headers(headers ? { ...headers } : {}, headerFormat);
509
+ const appOrigin = `${appProtocol}://${appHostname}`;
510
+ const targetOrigin = `${targetProtocol}://${targetHost}`;
511
+ // Set the X-Proxy-Request-Url header, if we have the request URL
512
+ if (requestUrl) {
513
+ // If the requestUrl is just a path, prepend the targetOrigin.
514
+ // Including the full URL as a header value risks exceeding limits
515
+ // on header value sizes. CloudFront limits URLs to 8192 bytes.
516
+ // Even though API Gateway has a header value limit of
517
+ // 10240 bytes, we choose to limit the length of the header
518
+ // value to 8192 bytes.
519
+ const fullRequestUrl = (requestUrl.startsWith('/') ? `${targetOrigin}${requestUrl}` : requestUrl).slice(0, MAX_URL_LENGTH_BYTES);
520
+ if (logging) {
521
+ console.log(`Setting proxy response ${X_PROXY_REQUEST_URL} header to "${fullRequestUrl}"`);
522
+ }
523
+ workingHeaders.setHeader(X_PROXY_REQUEST_URL, fullRequestUrl);
524
+ }
525
+ // Get a version of the proxyPath that does not end in a slash
526
+ /* istanbul ignore next */
527
+ const proxyPathBase = proxyPath.endsWith('/') ? proxyPath.slice(0, -1) : proxyPath;
528
+ const allowOrigin = workingHeaders.getHeader(AC_ALLOW_ORIGIN);
529
+ if (logging && allowOrigin) {
530
+ console.log(`Header ${AC_ALLOW_ORIGIN} has value "${allowOrigin}"`);
531
+ }
532
+ if (allowOrigin === targetOrigin) {
533
+ /* istanbul ignore else */
534
+ if (logging) {
535
+ console.log(`Rewriting proxy response ${AC_ALLOW_ORIGIN} header to "${appOrigin}"`);
536
+ }
537
+ workingHeaders.setHeader(AC_ALLOW_ORIGIN, appOrigin);
538
+ }
539
+ if (caching) {
540
+ // For a caching proxy, remove any Set-Cookie headers
541
+ workingHeaders.deleteHeader(SET_COOKIE);
542
+ }
543
+ else {
544
+ // For a standard proxy, rewrite domains in any set-cookie headers.
545
+ const updatedCookies = rewriteSetCookies({
546
+ appHostname,
547
+ setCookies: workingHeaders.getSetCookie(),
548
+ targetHost,
549
+ logging,
550
+ });
551
+ workingHeaders.setSetCookie(updatedCookies);
552
+ }
553
+ // Handle any redirect
554
+ if (statusCode >= 301 && statusCode <= 308) {
555
+ if (logging) {
556
+ console.log(`Status code is ${statusCode}, checking Location header`);
557
+ }
558
+ const location = workingHeaders.getHeader(LOCATION);
559
+ if (logging) {
560
+ console.log(`Location header has value "${location}"`);
561
+ }
562
+ /* istanbul ignore else */
563
+ if (location) {
564
+ // The Location header is defined as a URL, meaning that it
565
+ // can be both protocol- and host-relative, so we expand it
566
+ // relative to the targetOrigin.
567
+ const locUrl = new URL(location, targetOrigin);
568
+ // If the location header URL is on the targetOrigin, we rewrite it.
569
+ if (locUrl.protocol === `${targetProtocol}:` && locUrl.host === targetHost) {
570
+ // Rewrite the Location value to map to the proxy path
571
+ // on the app host.
572
+ locUrl.protocol = appProtocol;
573
+ locUrl.host = appHostname;
574
+ // Since the proxyPath ends with a slash and locUrl.pathname
575
+ // will start with a slash, we need to
576
+ locUrl.pathname = proxyPathBase + locUrl.pathname;
577
+ const newLocation = locUrl.toString();
578
+ workingHeaders.setHeader(LOCATION, newLocation);
579
+ /* istanbul ignore else */
580
+ if (logging) {
581
+ console.log(`Rewriting proxy response Location header from "${location}" to "${newLocation}"`);
582
+ }
583
+ }
584
+ }
585
+ }
586
+ return workingHeaders.toObject();
587
+ };
588
+ /**
589
+ * List of x- headers that are removed from proxied requests.
590
+ * @private
591
+ */
592
+ export const X_HEADERS_TO_REMOVE_PROXY = ['x-mobify-access-key', 'x-sfdc-access-control'];
593
+ /**
594
+ * List of x- headers that are removed from origin requests.
595
+ * @private
596
+ */
597
+ export const X_HEADERS_TO_REMOVE_ORIGIN = [
598
+ 'x-api-key',
599
+ 'x-apigateway-event',
600
+ 'x-apigateway-context',
601
+ 'x-mobify-access-key',
602
+ 'x-sfdc-access-control',
603
+ ];
604
+ /**
605
+ * X-header key and values to add to proxied requests
606
+ * @private
607
+ */
608
+ export const X_HEADERS_TO_ADD = {
609
+ 'x-mobify': 'true',
610
+ };
611
+ /**
612
+ * List of headers that are allowed for a caching proxy request.
613
+ * This must match the allowlist that CloudFront uses for a
614
+ * CacheBehavior that does not pass cookies and is not configured
615
+ * to cache based on headers.
616
+ *
617
+ * This is a map from lower-case header name to 'true' - we use an object
618
+ * to make lookups fast, since this mapping might be used for many requests.
619
+ *
620
+ * Also see what is configured in the SSR Manager (ssr-infrastructure repo),
621
+ * in the CloudFront configuration. This list is a superset of that list,
622
+ * since the proxying code must also allow headers that it adds, such as
623
+ * Host, Origin, etc.
624
+ *
625
+ * See https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/header-caching.html
626
+ * See https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/Cookies.html
627
+ *
628
+ * @private
629
+ */
630
+ export const ALLOWED_CACHING_PROXY_REQUEST_HEADERS = {
631
+ // This is the set of headers allowed for CloudFront
632
+ accept: true,
633
+ 'accept-charset': true,
634
+ 'accept-encoding': true,
635
+ 'accept-language': true,
636
+ authorization: true,
637
+ range: true,
638
+ // These headers must be preserved in the request so that
639
+ // header processing works correctly.
640
+ host: true,
641
+ origin: true,
642
+ // Where CloudFront does the proxying, these headers are
643
+ // generated by CloudFront itself. Where the Express app
644
+ // does it, we forward them.
645
+ 'if-match': true,
646
+ 'if-modified-since': true,
647
+ 'if-none-match': true,
648
+ 'if-range': true,
649
+ 'if-unmodified-since': true,
650
+ };
651
+ /**
652
+ * Rewrite headers for a request that is being proxied.
653
+ *
654
+ * 1. If the request contains a Host header, rewrite it so that the
655
+ * value is the target host.
656
+ * 2. If the request contains an Origin header, rewrite it so that the
657
+ * value is the target host.
658
+ * 3. ALL other header values are left unchanged. If they are multi-value
659
+ * headers whose values are stored as arrays, the values are left as arrays.
660
+ *
661
+ * @private
662
+ * @param params Configuration object for rewriting proxy request headers
663
+ * @returns the modified request headers
664
+ */
665
+ export const rewriteProxyRequestHeaders = ({ caching = false, headers, headerFormat = 'http', targetProtocol, targetHost, logging = false, }) => {
666
+ if (!headers) {
667
+ return {};
668
+ }
669
+ const workingHeaders = new Headers({ ...headers }, headerFormat);
670
+ // Strip out some specific X-headers
671
+ X_HEADERS_TO_REMOVE_PROXY.forEach((key) => workingHeaders.deleteHeader(key));
672
+ // For a caching proxy, apply special header processing
673
+ if (caching) {
674
+ // Remove any headers that are not on the allowlist
675
+ workingHeaders.keys().forEach((key) => {
676
+ if (!ALLOWED_CACHING_PROXY_REQUEST_HEADERS[key]) {
677
+ workingHeaders.deleteHeader(key);
678
+ }
679
+ });
680
+ // Override user-agent - mimic the behaviour of CloudFront
681
+ workingHeaders.setHeader(USER_AGENT, 'Amazon CloudFront');
682
+ }
683
+ // Fix up any Host header. We ignore any current value and
684
+ // always replace it with the target host.
685
+ // Host: <host>:<port>
686
+ const hostHeader = workingHeaders.getHeader(HOST);
687
+ if (hostHeader !== targetHost) {
688
+ /* istanbul ignore else */
689
+ if (logging) {
690
+ console.log(`Rewriting proxy request Host header from "${hostHeader}" to "${targetHost}"`);
691
+ }
692
+ workingHeaders.setHeader(HOST, targetHost);
693
+ }
694
+ // Fix up any Origin header. We ignore any current value and
695
+ // always replace it with the targetOrigin
696
+ // Origin: <scheme> "://" <hostname> [ ":" <port> ]
697
+ const originHeader = workingHeaders.getHeader(ORIGIN);
698
+ const targetOrigin = `${targetProtocol}://${targetHost}`;
699
+ if (originHeader && originHeader !== targetOrigin) {
700
+ workingHeaders.setHeader(ORIGIN, targetOrigin);
701
+ }
702
+ // Replace some headers with hardwired values
703
+ if (workingHeaders.getHeader(USER_AGENT)) {
704
+ // Mimic the behaviour of CloudFront
705
+ workingHeaders.setHeader(USER_AGENT, 'Amazon CloudFront');
706
+ }
707
+ // Add some specific X-headers
708
+ Object.entries(X_HEADERS_TO_ADD).forEach(([key, value]) => {
709
+ workingHeaders.setHeader(key, value);
710
+ });
711
+ return workingHeaders.toObject();
712
+ };
713
+ //# sourceMappingURL=ssr-proxying.js.map