@speedkit/cli 4.19.0 → 4.20.1

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,22 @@
1
+ ## [4.20.1](https://gitlab.orestes.info/baqend/speed-kit-cli/compare/v4.20.0...v4.20.1) (2026-08-07)
2
+
3
+
4
+ ### Bug Fixes
5
+
6
+ * **customer-config:** marker detectDevice reads the passed doc, not document ([6f1b697](https://gitlab.orestes.info/baqend/speed-kit-cli/commit/6f1b697b52cc6786c7e975edfcbc85c52750749b))
7
+
8
+ # [4.20.0](https://gitlab.orestes.info/baqend/speed-kit-cli/compare/v4.19.0...v4.20.0) (2026-08-07)
9
+
10
+
11
+ ### Bug Fixes
12
+
13
+ * reuse origin nonce for forceInstall script in local mode ([dd1d4d8](https://gitlab.orestes.info/baqend/speed-kit-cli/commit/dd1d4d82e796d65708dd586ba25979c5d2dbc652))
14
+
15
+
16
+ ### Features
17
+
18
+ * carry over origin security headers in onboarding local mode ([cb7e7da](https://gitlab.orestes.info/baqend/speed-kit-cli/commit/cb7e7da7165b4071449270ee2baf71989dc16636))
19
+
1
20
  # [4.19.0](https://gitlab.orestes.info/baqend/speed-kit-cli/compare/v4.18.0...v4.19.0) (2026-08-06)
2
21
 
3
22
 
package/README.md CHANGED
@@ -21,7 +21,7 @@ $ npm install -g @speedkit/cli
21
21
  $ sk COMMAND
22
22
  running command...
23
23
  $ sk (--version)
24
- @speedkit/cli/4.19.0 linux-x64 node-v22.23.2
24
+ @speedkit/cli/4.20.1 linux-x64 node-v22.23.2
25
25
  $ sk --help [COMMAND]
26
26
  USAGE
27
27
  $ sk COMMAND
@@ -34,14 +34,23 @@
34
34
 
35
35
  {{#if isShopify}}
36
36
  import { ShopifyPlugin } from "speed-kit-config/ShopifyPlugin";
37
+ {{/if}}
38
+ {{#if addSSR}}
39
+ import { SSRPlugin } from "speed-kit-config/SSRPlugin";
40
+ {{/if}}
37
41
 
38
42
  (function() {
43
+ {{#if isShopify}}
39
44
  SpeedKit.configPlugins = SpeedKit.configPlugins || [];
40
45
  SpeedKit.configPlugins.push(new ShopifyPlugin());
41
-
42
- {{else}}
43
- (function() {
44
46
  {{/if}}
47
+ {{#if addSSR}}
48
+ {{#unless isShopify}}
49
+ SpeedKit.configPlugins = SpeedKit.configPlugins || [];
50
+ {{/unless}}
51
+ SpeedKit.configPlugins.push(new SSRPlugin());
52
+ {{/if}}
53
+
45
54
  var ENABLED_HOSTS_CONFIGS = [
46
55
  // Production:
47
56
  {
@@ -98,15 +107,20 @@ import { ShopifyPlugin } from "speed-kit-config/ShopifyPlugin";
98
107
  rumTracking: true,
99
108
  {{#if addDeviceDetection}}
100
109
  {{#unless addSSR}}
110
+ // Server-side responsive: origin returns different HTML per device (UA-keyed).
101
111
  userAgentDetection: true,
102
112
  detectDevice: function(doc) {
103
- // TODO: adjust condition to your needs
113
+ // TODO: set a marker present only in the mobile variation. Read it from `doc`.
104
114
  return doc.querySelector('<mobile-specific-element>') ? "mobile" : "desktop";
105
115
  },
106
116
  {{/unless}}
107
117
  {{#if addSSR}}
108
- customDevice: () => {
109
- return customDevice();
118
+ // SSR: pick the device by viewport width; SSRPlugin (above) handles resize/zoom/bfcache.
119
+ detectDevice: function(doc) {
120
+ // TODO: set the real breakpoint — discover it, don't guess (tools/sk-breakpoints.mjs).
121
+ var width = document.documentElement.clientWidth;
122
+ if (!width) return null;
123
+ return width <= 820 ? "mobile" : "desktop";
110
124
  },
111
125
  {{/if}}
112
126
  {{/if}}
@@ -458,29 +472,6 @@ import { ShopifyPlugin } from "speed-kit-config/ShopifyPlugin";
458
472
  // Utility Functions
459
473
  //================================================================================
460
474
 
461
- {{#if addSSR}}
462
- function customDevice() {
463
- var width = window.innerWidth || 1025;
464
- // TODO: adjust devices + breakpoints to your needs
465
- if (width <= 820) {
466
- return "mobile";
467
- }
468
- return "desktop";
469
- }
470
-
471
- // Re-evaluate device on orientation change and push to service worker
472
- window.addEventListener("orientationchange", function () {
473
- if (typeof SpeedKit !== "undefined" && SpeedKit.updateDevice) {
474
- SpeedKit.updateDevice(customDevice());
475
- }
476
- });
477
- window.addEventListener("resize", function () {
478
- if (typeof SpeedKit !== "undefined" && SpeedKit.updateDevice) {
479
- SpeedKit.updateDevice(customDevice());
480
- }
481
- });
482
- {{/if}}
483
-
484
475
  function getEnabledHosts(enabledHostsConfigs) {
485
476
  var allHosts = [];
486
477
  enabledHostsConfigs.forEach(function (enabledHostsConfig) {
@@ -7,12 +7,19 @@
7
7
  {{#if addDeviceDetection}}
8
8
  {{#unless addSSR}}
9
9
  detectDevice: (doc) => {
10
- // keep only if you need to check the DOM for screen width checks the SK config's detectDevice is sufficient
11
-
12
- // TODO: adjust condition to your needs
10
+ // Only if you need a DOM marker; for width checks the SK config's detectDevice suffices.
11
+ // TODO: set a marker present only in the mobile variation. Read it from `doc`.
13
12
  return doc.querySelector('<mobile-specific-element>') ? "mobile" : "desktop";
14
13
  },
15
14
  {{/unless}}
15
+ {{#if addSSR}}
16
+ // Mirror config_SpeedKit.js detectDevice — keep the breakpoint identical.
17
+ detectDevice: (doc) => {
18
+ let width = document.documentElement.clientWidth;
19
+ if (!width) return null;
20
+ return width <= 820 ? "mobile" : "desktop";
21
+ },
22
+ {{/if}}
16
23
  {{/if}}
17
24
  blocks: [
18
25
  // TODO: adjust/replace this exemplary list
@@ -19,7 +19,6 @@ export declare class CustomerDomainDocumentResponse implements FetchRequestPause
19
19
  private isHtmlContentHeader;
20
20
  private getHtmlContent;
21
21
  private isSpeedKitResponse;
22
- private rewriteInstallResource;
23
22
  private rewriteHtmlToLocalConfig;
24
23
  /**
25
24
  * Removes content policy headers from the given array of headers.
@@ -28,11 +27,4 @@ export declare class CustomerDomainDocumentResponse implements FetchRequestPause
28
27
  * @returns {Protocol.Fetch.HeaderEntry[]} - The modified array of header entries with content policy headers removed.
29
28
  */
30
29
  private removeContentSecurityHeaderFromResponse;
31
- /**
32
- * Removes the content security meta tag from the provided HTML content.
33
- *
34
- * @param {string} htmlContent - The HTML content from which to remove the meta tag.
35
- * @returns {string} - The HTML content without the content security meta tag.
36
- */
37
- private removeContentSecurityMetaTag;
38
30
  }
@@ -1,5 +1,6 @@
1
- import { META_CONTENT_TYPE_REGEX, SpeedKitInstallRegex, } from "../onboarding-model.js";
1
+ import { META_CONTENT_TYPE_REGEX, } from "../onboarding-model.js";
2
2
  import { OriginResponse } from "../browser/origin-response.js";
3
+ import { removeContentSecurityMetaTag, rewriteInstallResource, } from "../html-rewrites.js";
3
4
  import iconv from "iconv-lite";
4
5
  import Encoding from "encoding-japanese";
5
6
  import { UnsupportedEncodingError } from "../error/unsupported-encoding-error.js";
@@ -67,13 +68,13 @@ export class CustomerDomainDocumentResponse {
67
68
  ? this.removeContentSecurityHeaderFromResponse(event.responseHeaders)
68
69
  : event.responseHeaders;
69
70
  if (this.customerConfig.forceInstall) {
70
- text = this.rewriteInstallResource(text);
71
+ text = rewriteInstallResource(text, this.customerConfig);
71
72
  }
72
73
  if (this.isSpeedKitResponse(text)) {
73
74
  text = this.rewriteHtmlToLocalConfig(text);
74
75
  }
75
76
  const content = this.removeContentSecurityPolicy
76
- ? this.removeContentSecurityMetaTag(text)
77
+ ? removeContentSecurityMetaTag(text)
77
78
  : text;
78
79
  // replace origin contentTypeCharset with utf-8 as this is what it's converted to now
79
80
  let detectedEncoding = "utf-8";
@@ -140,23 +141,6 @@ export class CustomerDomainDocumentResponse {
140
141
  isSpeedKitResponse(text) {
141
142
  return text.includes("speed-kit-dynamic");
142
143
  }
143
- rewriteInstallResource(text) {
144
- text = text.replace(SpeedKitInstallRegex, "");
145
- const matchOpeningHead = /<\s*head\b[^>]*>/;
146
- const matchClosingHead = /<\/\s*head\s*>/;
147
- const nonceMatch = text.match(/<script\s(?:[^>]*?\s)?nonce\s*=\s*["']?([^\s"'>]*)/i);
148
- if (matchOpeningHead.test(text)) {
149
- return text.replace(matchOpeningHead, (head) => {
150
- return `${head}\n<script ${nonceMatch ? 'nonce="' + nonceMatch[1] + '"' : ""} src="${this.customerConfig.installPath}" ${this.customerConfig.installParams}></script>`;
151
- });
152
- }
153
- if (matchClosingHead.test(text)) {
154
- return text.replace(matchClosingHead, (head) => {
155
- return `<script ${nonceMatch ? 'nonce="' + nonceMatch[1] + '"' : ""} src="${this.customerConfig.installPath}" ${this.customerConfig.installParams}></script>${head}\n`;
156
- });
157
- }
158
- return text;
159
- }
160
144
  rewriteHtmlToLocalConfig(html) {
161
145
  const DF_CONFIG_PATTERN = /(<script[^>]* id="speed-kit-df-config"[^>]*>)([\S\s]*?)(<\/script>)/g;
162
146
  const DF_STYLES_PATTERN = /(<style[^>]* id="speed-kit-df-styles"[^>]*>)(?:(?!<\/style>)[\S\s])*(<\/style>)/g;
@@ -178,13 +162,4 @@ export class CustomerDomainDocumentResponse {
178
162
  return !header.name.toLowerCase().includes("content-security-policy");
179
163
  });
180
164
  }
181
- /**
182
- * Removes the content security meta tag from the provided HTML content.
183
- *
184
- * @param {string} htmlContent - The HTML content from which to remove the meta tag.
185
- * @returns {string} - The HTML content without the content security meta tag.
186
- */
187
- removeContentSecurityMetaTag(htmlContent) {
188
- return htmlContent.replace(/<meta[^>]*http-equiv=["']content-security-policy[^>]*>/is, "");
189
- }
190
165
  }
@@ -0,0 +1,19 @@
1
+ import { CustomerConfig } from "../integration-api/index.js";
2
+ /**
3
+ * Injects the Speed Kit install script into the given HTML, after removing any
4
+ * existing install snippet. The origin's script nonce is reused so a
5
+ * nonce-based CSP does not block the injected script. Injection prefers the
6
+ * opening `<head>`, falling back to before the closing `</head>`.
7
+ *
8
+ * @param {string} text - the HTML to rewrite
9
+ * @param {CustomerConfig} customerConfig - provides the install path/params
10
+ * @returns {string} - the HTML with the install script injected
11
+ */
12
+ export declare function rewriteInstallResource(text: string, customerConfig: CustomerConfig): string;
13
+ /**
14
+ * Removes the content security meta tag from the provided HTML content.
15
+ *
16
+ * @param {string} htmlContent - The HTML content from which to remove the meta tag.
17
+ * @returns {string} - The HTML content without the content security meta tag.
18
+ */
19
+ export declare function removeContentSecurityMetaTag(htmlContent: string): string;
@@ -0,0 +1,34 @@
1
+ import { SpeedKitInstallRegex } from "./onboarding-model.js";
2
+ /**
3
+ * Injects the Speed Kit install script into the given HTML, after removing any
4
+ * existing install snippet. The origin's script nonce is reused so a
5
+ * nonce-based CSP does not block the injected script. Injection prefers the
6
+ * opening `<head>`, falling back to before the closing `</head>`.
7
+ *
8
+ * @param {string} text - the HTML to rewrite
9
+ * @param {CustomerConfig} customerConfig - provides the install path/params
10
+ * @returns {string} - the HTML with the install script injected
11
+ */
12
+ export function rewriteInstallResource(text, customerConfig) {
13
+ text = text.replace(SpeedKitInstallRegex, "");
14
+ const matchOpeningHead = /<\s*head\b[^>]*>/;
15
+ const matchClosingHead = /<\/\s*head\s*>/;
16
+ const nonceMatch = text.match(/<script\s(?:[^>]*?\s)?nonce\s*=\s*["']?([^\s"'>]*)/i);
17
+ const installScript = `<script ${nonceMatch ? 'nonce="' + nonceMatch[1] + '"' : ""} src="${customerConfig.installPath}" ${customerConfig.installParams}></script>`;
18
+ if (matchOpeningHead.test(text)) {
19
+ return text.replace(matchOpeningHead, (head) => `${head}\n${installScript}`);
20
+ }
21
+ if (matchClosingHead.test(text)) {
22
+ return text.replace(matchClosingHead, (head) => `${installScript}${head}\n`);
23
+ }
24
+ return text;
25
+ }
26
+ /**
27
+ * Removes the content security meta tag from the provided HTML content.
28
+ *
29
+ * @param {string} htmlContent - The HTML content from which to remove the meta tag.
30
+ * @returns {string} - The HTML content without the content security meta tag.
31
+ */
32
+ export function removeContentSecurityMetaTag(htmlContent) {
33
+ return htmlContent.replace(/<meta[^>]*http-equiv=["']content-security-policy[^>]*>/is, "");
34
+ }
@@ -166,7 +166,7 @@ export class OnboardingServiceFactory {
166
166
  new DashboardRequest(new Dashboard(customerConfig, parameterQueryBuilder, athenaClient, requestDiffService, new DiffAgainstCurrentPage(DiffService, documentHandler, crawler, cli), cache)),
167
167
  ];
168
168
  if (this.context.local) {
169
- const orestesApp = new VirtualOrestesApp(customerConfig, crawler, documentHandler, cache, cli, messageHandler);
169
+ const orestesApp = new VirtualOrestesApp(customerConfig, crawler, documentHandler, cache, cli, messageHandler, this.context.ignoreContentSecurityPolicy);
170
170
  // let config-file changes re-warm the dh-cache in the background
171
171
  fileWatcher.setCacheRefresher(orestesApp);
172
172
  handlers.push(new SpeedKitAssetRequest(customerConfig, orestesApp), new SpeedKitRumPiRequest(customerConfig, orestesApp));
@@ -13,8 +13,9 @@ export declare class VirtualOrestesApp {
13
13
  private cache;
14
14
  private cli;
15
15
  private developmentToolsMessages;
16
+ private removeContentSecurityPolicy;
16
17
  private localCacheErrors;
17
- constructor(customerConfig: CustomerConfig, crawler: Crawler, documentHandler: DocumentHandlerServer, cache: Cache, cli: CliService, developmentToolsMessages: DevtoolsExtensionApi);
18
+ constructor(customerConfig: CustomerConfig, crawler: Crawler, documentHandler: DocumentHandlerServer, cache: Cache, cli: CliService, developmentToolsMessages: DevtoolsExtensionApi, removeContentSecurityPolicy?: boolean);
18
19
  fetchAssetResponse(event: Protocol.Fetch.RequestPausedEvent): Promise<Partial<Protocol.Fetch.FulfillRequestRequest> | AbortResponse>;
19
20
  /**
20
21
  * Re-warm all cached entries for the given origin url in the background.
@@ -60,5 +61,14 @@ export declare class VirtualOrestesApp {
60
61
  private prepareCustomHeaders;
61
62
  private isHtmlContentHeader;
62
63
  private isValidHtmlContent;
63
- private rewriteInstallResource;
64
+ /**
65
+ * Carries over the origin response's security headers onto the accelerated
66
+ * response, mirroring the Orestes server. The origin value wins over any
67
+ * default set in `prepareCustomHeaders`. Content-Security-Policy headers are
68
+ * skipped when `--ignoreContentSecurityPolicy` is set.
69
+ *
70
+ * @param {Response} response - the origin fetch response
71
+ * @param {{ name: string; value: string }[]} customHeaders - headers being built for the response (mutated in place)
72
+ */
73
+ private carryOverSecurityHeaders;
64
74
  }
@@ -1,12 +1,24 @@
1
1
  import { BaqendResponse } from "../browser/baqend-response.js";
2
2
  import { safe } from "../../../helpers/safe.js";
3
- import { META_CONTENT_TYPE_REGEX, SpeedKitInstallRegex, } from "../onboarding-model.js";
3
+ import { META_CONTENT_TYPE_REGEX, } from "../onboarding-model.js";
4
4
  import iconv from "iconv-lite";
5
5
  import { UnsupportedEncodingError } from "../error/unsupported-encoding-error.js";
6
+ import { removeContentSecurityMetaTag, rewriteInstallResource, } from "../html-rewrites.js";
6
7
  // for none encodable chars use a space, so the broken char is invisible.
7
8
  // We are using the same trick in our node.js server
8
9
  iconv.defaultCharSingleByte = " ";
9
10
  iconv.defaultCharUnicode = " ";
11
+ // Security headers the Orestes server carries over from the origin response
12
+ // onto the accelerated response. Kept in sync with the server's fixed lists.
13
+ const CONTENT_SECURITY_POLICY_HEADERS = [
14
+ "content-security-policy",
15
+ "content-security-policy-report-only",
16
+ ];
17
+ const SECURITY_HEADERS = [
18
+ "x-content-type-options",
19
+ "strict-transport-security",
20
+ "x-frame-options",
21
+ ];
10
22
  export class VirtualOrestesApp {
11
23
  customerConfig;
12
24
  crawler;
@@ -14,18 +26,20 @@ export class VirtualOrestesApp {
14
26
  cache;
15
27
  cli;
16
28
  developmentToolsMessages;
29
+ removeContentSecurityPolicy;
17
30
  // Local-mode transform errors keyed by `${url}|${variation}`. Emitted to
18
31
  // the devtools panel via DevtoolsExtensionApi.upsertMessage so the panel
19
32
  // can surface an "error for this URL" affordance in the Causes & sources
20
33
  // card. Cleared per-URL on the next successful transform.
21
34
  localCacheErrors = new Map();
22
- constructor(customerConfig, crawler, documentHandler, cache, cli, developmentToolsMessages) {
35
+ constructor(customerConfig, crawler, documentHandler, cache, cli, developmentToolsMessages, removeContentSecurityPolicy = false) {
23
36
  this.customerConfig = customerConfig;
24
37
  this.crawler = crawler;
25
38
  this.documentHandler = documentHandler;
26
39
  this.cache = cache;
27
40
  this.cli = cli;
28
41
  this.developmentToolsMessages = developmentToolsMessages;
42
+ this.removeContentSecurityPolicy = removeContentSecurityPolicy;
29
43
  }
30
44
  async fetchAssetResponse(event) {
31
45
  const cachedResponse = this.getCachedResponse(event.request.url);
@@ -131,7 +145,10 @@ export class VirtualOrestesApp {
131
145
  this.clearLocalCacheError(originUrl, variation);
132
146
  const skResponse = customResponse.data;
133
147
  if (!Buffer.isBuffer(skResponse.body) && this.customerConfig.forceInstall) {
134
- skResponse.body = this.rewriteInstallResource(skResponse.body);
148
+ skResponse.body = rewriteInstallResource(skResponse.body, this.customerConfig);
149
+ }
150
+ if (!Buffer.isBuffer(skResponse.body) && this.removeContentSecurityPolicy) {
151
+ skResponse.body = removeContentSecurityMetaTag(skResponse.body);
135
152
  }
136
153
  for (const key in skResponse?.headers) {
137
154
  customHeaders.push({
@@ -139,6 +156,11 @@ export class VirtualOrestesApp {
139
156
  value: skResponse?.headers[key],
140
157
  });
141
158
  }
159
+ // The document handler does not emit the origin's security headers, so —
160
+ // like the Orestes server — carry them over from the origin response onto
161
+ // the accelerated response. Content-Security-Policy is only carried over
162
+ // when `--ignoreContentSecurityPolicy` is not set.
163
+ this.carryOverSecurityHeaders(response, customHeaders);
142
164
  const originHeaders = [];
143
165
  response?.headers.forEach((value, key) => {
144
166
  originHeaders.push({ name: `x-origin-${key}`, value: value });
@@ -309,10 +331,31 @@ export class VirtualOrestesApp {
309
331
  isValidHtmlContent(text) {
310
332
  return text.includes("<html");
311
333
  }
312
- rewriteInstallResource(text) {
313
- text = text.replace(SpeedKitInstallRegex, "");
314
- return text.replace(/<\s*head\b[^>]*>/, (head) => {
315
- return `${head}\n<script src="${this.customerConfig.installPath}" ${this.customerConfig.installParams}></script>`;
316
- });
334
+ /**
335
+ * Carries over the origin response's security headers onto the accelerated
336
+ * response, mirroring the Orestes server. The origin value wins over any
337
+ * default set in `prepareCustomHeaders`. Content-Security-Policy headers are
338
+ * skipped when `--ignoreContentSecurityPolicy` is set.
339
+ *
340
+ * @param {Response} response - the origin fetch response
341
+ * @param {{ name: string; value: string }[]} customHeaders - headers being built for the response (mutated in place)
342
+ */
343
+ carryOverSecurityHeaders(response, customHeaders) {
344
+ const headersToCarryOver = this.removeContentSecurityPolicy
345
+ ? SECURITY_HEADERS
346
+ : [...CONTENT_SECURITY_POLICY_HEADERS, ...SECURITY_HEADERS];
347
+ for (const name of headersToCarryOver) {
348
+ const value = response?.headers?.get(name);
349
+ if (value == null) {
350
+ continue;
351
+ }
352
+ const existing = customHeaders.findIndex((header) => header.name.toLowerCase() === name);
353
+ if (existing === -1) {
354
+ customHeaders.push({ name, value });
355
+ }
356
+ else {
357
+ customHeaders[existing] = { name, value };
358
+ }
359
+ }
317
360
  }
318
361
  }
@@ -1001,5 +1001,5 @@
1001
1001
  ]
1002
1002
  }
1003
1003
  },
1004
- "version": "4.19.0"
1004
+ "version": "4.20.1"
1005
1005
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@speedkit/cli",
3
3
  "description": "Speed Kit CLI",
4
- "version": "4.19.0",
4
+ "version": "4.20.1",
5
5
  "author": {
6
6
  "name": "Baqend.com",
7
7
  "email": "info@baqend.com"