@dylandepass/helix-product-pipeline 2.1.0 → 2.2.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.
package/CHANGELOG.md CHANGED
@@ -1,3 +1,10 @@
1
+ # [2.2.0](https://github.com/adobe-rnd/helix-product-pipeline/compare/v2.1.0...v2.2.0) (2026-02-04)
2
+
3
+
4
+ ### Features
5
+
6
+ * Remove media pipe and move to helix-product-pipeline-worker ([645d2f5](https://github.com/adobe-rnd/helix-product-pipeline/commit/645d2f5850f0b4f5abfe25e6330c68e31508d812))
7
+
1
8
  # [2.1.0](https://github.com/adobe-rnd/helix-product-pipeline/compare/v2.0.2...v2.1.0) (2026-02-03)
2
9
 
3
10
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dylandepass/helix-product-pipeline",
3
- "version": "2.1.0",
3
+ "version": "2.2.0",
4
4
  "description": "Helix Product Pipeline",
5
5
  "main": "src/index.js",
6
6
  "types": "src/index.d.ts",
package/src/index.js CHANGED
@@ -15,4 +15,4 @@ export { productHTMLPipe } from './product-html-pipe.js';
15
15
  export { productIndexPipe } from './product-index-pipe.js';
16
16
  export { productMerchantFeedPipe } from './product-merchant-feed-pipe.js';
17
17
  export { productSitemapPipe } from './product-sitemap-pipe.js';
18
- export { productMediaPipe } from './product-media-pipe.js';
18
+ export { applyByoCdnHeaders } from './utils/cache-headers.js';
@@ -16,13 +16,10 @@ import {
16
16
  computeProductPathKey,
17
17
  computeSiteKey,
18
18
  compute404Key,
19
- computeMediaKeys,
20
19
  computeAuthoredContentKey,
21
20
  computeProductKeys,
22
21
  } from '@dylandepass/helix-product-shared';
23
-
24
- export const isMediaRequest = (url) => /\/media_[0-9a-f]{40,}[/a-zA-Z0-9_-]*\.[0-9a-z]+$/.test(url.pathname);
25
- const BYO_CDN_TYPES = ['akamai', 'cloudflare', 'fastly', 'cloudfront'];
22
+ import { applyByoCdnHeaders } from '../utils/cache-headers.js';
26
23
 
27
24
  /**
28
25
  * Sets the cache headers for a product
@@ -32,92 +29,29 @@ const BYO_CDN_TYPES = ['akamai', 'cloudflare', 'fastly', 'cloudfront'];
32
29
  * @returns {void}
33
30
  */
34
31
  export function setCachingHeaders(req, resp, cacheKeys) {
35
- const url = new URL(req.url);
36
-
37
- // determine BYO CDN type
38
- let byo_cdn_type = req.headers.get('x-byo-cdn-type');
39
- const via = req.headers.get('via') || '';
40
- const cdn_loop = req.headers.get('cdn-loop') || '';
41
- // FIXME: cloudflare seems to strip the CDN-Loop header, i.e. the worker can't see it...
42
- if (!BYO_CDN_TYPES.includes(byo_cdn_type)) {
43
- // sniff downstream cdn type
44
- if (/[Aa]kamai/.test(via)) {
45
- byo_cdn_type = 'akamai';
46
- } else if (via.includes('varnish') || cdn_loop.startsWith('Fastly')) {
47
- byo_cdn_type = 'fastly';
48
- } else if (cdn_loop.includes('cloudflare') || req.headers.has('cf-worker')) {
49
- byo_cdn_type = 'cloudflare';
50
- } else if (via.includes('CloudFront')) {
51
- byo_cdn_type = 'cloudfront';
52
- } else {
53
- // invalid/unsupported CDN type
54
- byo_cdn_type = undefined;
55
- }
56
- }
57
-
58
32
  // cache instructions for BYO CDN and the browser
59
33
  const pushInvalidation = req.headers.get('x-push-invalidation') === 'enabled';
34
+
60
35
  // browser ttl & browser cache-control
61
36
  let browserTTL;
62
37
  if ([302, 400, 401].includes(resp.status)) {
63
38
  browserTTL = 0;
64
39
  resp.headers.set('cache-control', 'private; no-cache');
65
40
  } else {
66
- if (isMediaRequest(url)) {
67
- browserTTL = resp.status === 200 ? 2592000 : 3600;
68
- } else {
69
- browserTTL = 7200;
70
- }
41
+ browserTTL = 7200;
71
42
  resp.headers.set('cache-control', `max-age=${browserTTL}, must-revalidate`);
72
43
  }
73
44
 
74
45
  // CDN ttl
75
46
  let cdnTTL;
76
47
  if ([400, 401].includes(resp.status)) {
77
- // don't cache 400 & 401 repsonses in order to avoid potential downstream caching issues
48
+ // don't cache 400 & 401 responses in order to avoid potential downstream caching issues
78
49
  cdnTTL = 0;
79
- } else if (isMediaRequest(url)) {
80
- cdnTTL = resp.status === 200 ? 2592000 : 3600;
81
50
  } else {
82
51
  cdnTTL = pushInvalidation ? 172800 : 300;
83
52
  }
84
- switch (byo_cdn_type) {
85
- case 'cloudflare':
86
- resp.headers.set('cdn-cache-control', `max-age=${cdnTTL}, must-revalidate`);
87
- break;
88
- case 'fastly':
89
- resp.headers.set('surrogate-control', `max-age=${cdnTTL}, stale-while-revalidate=0`);
90
- break;
91
- case 'akamai':
92
- resp.headers.set('edge-control', `!no-store,max-age=${cdnTTL}s,downstream-ttl=${browserTTL}s`);
93
- break;
94
- case 'cloudfront':
95
- resp.headers.set('cache-control', `max-age=${browserTTL}, s-maxage=${cdnTTL}, must-revalidate`);
96
- break;
97
- default:
98
- break;
99
- }
100
53
 
101
- // BYO CDN cache tags
102
- // apply cache tags: transform Fastly Surrogate-Key to Cloudflare Cache-Tag
103
- if (cacheKeys.length) {
104
- switch (byo_cdn_type) {
105
- case 'fastly':
106
- resp.headers.set('surrogate-key', cacheKeys.join(' '));
107
- break;
108
- case 'akamai':
109
- resp.headers.set('edge-cache-tag', cacheKeys.join(' '));
110
- break;
111
- case 'cloudflare':
112
- resp.headers.set('cache-tag', `${cacheKeys.join(',')}`);
113
- break;
114
- case 'cloudfront':
115
- // cloudfront doesn't support cache tags/keys ...
116
- break;
117
- default:
118
- break;
119
- }
120
- }
54
+ applyByoCdnHeaders(resp, req, cacheKeys, browserTTL, cdnTTL);
121
55
  }
122
56
 
123
57
  /**
@@ -172,18 +106,3 @@ export async function setProductCacheHeaders(state, req, resp) {
172
106
  const productKeys = await computeProductKeys(org, site, info.path, contentBusId);
173
107
  setCachingHeaders(req, resp, productKeys);
174
108
  }
175
-
176
- /**
177
- * Sets the cache headers for a product
178
- * @param {PipelineState} state
179
- * @param {PipelineRequest} req
180
- * @param {PipelineResponse} resp
181
- * @returns {Promise<void>}
182
- */
183
- export async function setMediaCacheHeaders(state, req, resp) {
184
- const { org, site } = state;
185
- const url = new URL(req.url);
186
- const keys = await computeMediaKeys(org, site, url.pathname);
187
-
188
- setCachingHeaders(req, resp, keys);
189
- }
@@ -0,0 +1,76 @@
1
+ /*
2
+ * Copyright 2025 Adobe. All rights reserved.
3
+ * This file is licensed to you under the Apache License, Version 2.0 (the "License");
4
+ * you may not use this file except in compliance with the License. You may obtain a copy
5
+ * of the License at http://www.apache.org/licenses/LICENSE-2.0
6
+ *
7
+ * Unless required by applicable law or agreed to in writing, software distributed under
8
+ * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
9
+ * OF ANY KIND, either express or implied. See the License for the specific language
10
+ * governing permissions and limitations under the License.
11
+ */
12
+
13
+ const BYO_CDN_TYPES = ['akamai', 'cloudflare', 'fastly', 'cloudfront'];
14
+
15
+ /**
16
+ * Detects the BYO CDN type from the request headers.
17
+ *
18
+ * @param {Request|object} req - The request object (must have headers)
19
+ * @returns {string|undefined} - The detected CDN type or undefined
20
+ */
21
+ const detectByoCdnType = (req) => {
22
+ const { headers } = req;
23
+ let byoCdnType = headers.get('x-byo-cdn-type');
24
+ const via = headers.get('via') || '';
25
+ const cdnLoop = headers.get('cdn-loop') || '';
26
+
27
+ if (!BYO_CDN_TYPES.includes(byoCdnType)) {
28
+ if (/[Aa]kamai/.test(via)) {
29
+ byoCdnType = 'akamai';
30
+ } else if (via.includes('varnish') || cdnLoop.startsWith('Fastly')) {
31
+ byoCdnType = 'fastly';
32
+ } else if (cdnLoop.includes('cloudflare') || headers.has('cf-worker')) {
33
+ byoCdnType = 'cloudflare';
34
+ } else if (via.includes('CloudFront')) {
35
+ byoCdnType = 'cloudfront';
36
+ } else {
37
+ byoCdnType = undefined;
38
+ }
39
+ }
40
+
41
+ return BYO_CDN_TYPES.includes(byoCdnType) ? byoCdnType : undefined;
42
+ };
43
+
44
+ /**
45
+ * Applies BYO CDN specific cache headers to the response.
46
+ *
47
+ * @param {Response|object} resp - The response object
48
+ * @param {Request|object} req - The request object
49
+ * @param {string[]} cacheKeys - List of cache keys/tags
50
+ * @param {number} browserTTL - Browser TTL in seconds
51
+ * @param {number} cdnTTL - CDN TTL in seconds
52
+ */
53
+ export const applyByoCdnHeaders = (resp, req, cacheKeys, browserTTL, cdnTTL) => {
54
+ const byoCdnType = detectByoCdnType(req);
55
+ const keys = cacheKeys && cacheKeys.length ? cacheKeys : [];
56
+
57
+ switch (byoCdnType) {
58
+ case 'cloudflare':
59
+ resp.headers.set('cdn-cache-control', `max-age=${cdnTTL}, must-revalidate`);
60
+ if (keys.length) resp.headers.set('cache-tag', keys.join(','));
61
+ break;
62
+ case 'fastly':
63
+ resp.headers.set('surrogate-control', `max-age=${cdnTTL}, stale-while-revalidate=0`);
64
+ if (keys.length) resp.headers.set('surrogate-key', keys.join(' '));
65
+ break;
66
+ case 'akamai':
67
+ resp.headers.set('edge-control', `!no-store,max-age=${cdnTTL}s,downstream-ttl=${browserTTL}s`);
68
+ if (keys.length) resp.headers.set('edge-cache-tag', keys.join(' '));
69
+ break;
70
+ case 'cloudfront':
71
+ resp.headers.set('cache-control', `max-age=${browserTTL}, s-maxage=${cdnTTL}, must-revalidate`);
72
+ break;
73
+ default:
74
+ break;
75
+ }
76
+ };
@@ -1,64 +0,0 @@
1
- /*
2
- * Copyright 2025 Adobe. All rights reserved.
3
- * This file is licensed to you under the Apache License, Version 2.0 (the "License");
4
- * you may not use this file except in compliance with the License. You may obtain a copy
5
- * of the License at http://www.apache.org/licenses/LICENSE-2.0
6
- *
7
- * Unless required by applicable law or agreed to in writing, software distributed under
8
- * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
9
- * OF ANY KIND, either express or implied. See the License for the specific language
10
- * governing permissions and limitations under the License.
11
- */
12
-
13
- import { PipelineResponse, PipelineStatusError } from '@adobe/helix-html-pipeline';
14
- import { validatePathInfo } from './utils/path.js';
15
- import initConfig from './steps/init-config.js';
16
- import fetchMedia from './steps/fetch-media.js';
17
- import { setLastModified } from './utils/last-modified.js';
18
- import { setMediaCacheHeaders } from './steps/set-cache-headers.js';
19
-
20
- export async function productMediaPipe(state, req) {
21
- state.type = 'media';
22
-
23
- if (!validatePathInfo(state.info)) {
24
- return new PipelineResponse('', {
25
- status: 404,
26
- headers: {
27
- 'x-error': 'invalid path',
28
- },
29
- });
30
- }
31
-
32
- const res = new PipelineResponse('');
33
-
34
- try {
35
- await initConfig(state, req, res);
36
-
37
- state.timer?.update('content-fetch');
38
- await fetchMedia(state, res);
39
- if (res.status === 404) {
40
- return new PipelineResponse('', {
41
- status: 404,
42
- headers: {
43
- 'x-error': 'not found',
44
- },
45
- });
46
- } else if (res.error) {
47
- if (res.status < 400) {
48
- return res;
49
- }
50
- throw new PipelineStatusError(res.status, res.error);
51
- }
52
-
53
- setLastModified(state, res);
54
-
55
- await setMediaCacheHeaders(state, req, res);
56
-
57
- res.headers = state.content.headers;
58
- res.body = state.content.data;
59
- } catch (e) {
60
- res.error = e.message;
61
- }
62
-
63
- return res;
64
- }
@@ -1,41 +0,0 @@
1
- /*
2
- * Copyright 2022 Adobe. All rights reserved.
3
- * This file is licensed to you under the Apache License, Version 2.0 (the "License");
4
- * you may not use this file except in compliance with the License. You may obtain a copy
5
- * of the License at http://www.apache.org/licenses/LICENSE-2.0
6
- *
7
- * Unless required by applicable law or agreed to in writing, software distributed under
8
- * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
9
- * OF ANY KIND, either express or implied. See the License for the specific language
10
- * governing permissions and limitations under the License.
11
- */
12
- import { extractLastModified, recordLastModified } from '../utils/last-modified.js';
13
-
14
- /**
15
- * Loads the content from either the content-bus or code-bus and stores it in `state.content`
16
- * @param {PipelineState} state
17
- * @param {PipelineResponse} res
18
- * @returns {Promise<void>}
19
- */
20
- export default async function fetchMedia(state, res) {
21
- const {
22
- info, org, site,
23
- } = state;
24
- const bucketId = 'helix-product-bus';
25
-
26
- const key = `${org}/${site}/media/${info.originalFilename}`;
27
- const ret = await state.s3Loader.getObject(bucketId, key);
28
-
29
- if (ret.status === 200) {
30
- res.status = 200;
31
- delete res.error;
32
- state.content.data = ret.body;
33
- state.content.headers = ret.headers;
34
-
35
- recordLastModified(state, res, 'content', extractLastModified(ret.headers));
36
- } else {
37
- // keep 404, but propagate others as 502
38
- res.status = ret.status === 404 ? 404 : 502;
39
- res.error = `failed to load ${info.resourcePath} from product-bus: ${ret.status}`;
40
- }
41
- }