@speedkit/cli 4.23.0 → 4.23.2

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 (24) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/README.md +1 -1
  3. package/dist/hooks/init/dns-result-order.d.ts +12 -0
  4. package/dist/hooks/init/dns-result-order.js +14 -0
  5. package/dist/hooks/init/dns-result-order.spec.d.ts +1 -0
  6. package/dist/hooks/init/dns-result-order.spec.js +15 -0
  7. package/dist/services/document-handler-runtime/context/document-handler-runtime-context.d.ts +1 -0
  8. package/dist/services/document-handler-runtime/context/document-handler-runtime-context.js +4 -0
  9. package/dist/services/document-handler-runtime/document-handler-abort.d.ts +58 -0
  10. package/dist/services/document-handler-runtime/document-handler-abort.js +66 -0
  11. package/dist/services/document-handler-runtime/document-handler-abort.spec.d.ts +1 -0
  12. package/dist/services/document-handler-runtime/document-handler-abort.spec.js +84 -0
  13. package/dist/services/document-handler-runtime/document-handler-server.js +2 -1
  14. package/dist/services/document-handler-runtime/server/request.d.ts +1 -0
  15. package/dist/services/document-handler-runtime/server/request.js +7 -1
  16. package/dist/services/document-handler-runtime/templates/execute-document-handler.js +2 -0
  17. package/dist/services/document-handler-runtime/templates/orestes-mock.js +2 -0
  18. package/dist/services/document-handler-runtime/templates/test.js +6 -1
  19. package/dist/services/onboarding/error/document-handler-transform-error.d.ts +6 -0
  20. package/dist/services/onboarding/error/document-handler-transform-error.js +8 -0
  21. package/dist/services/onboarding/virtual-orestes-app/index.d.ts +8 -0
  22. package/dist/services/onboarding/virtual-orestes-app/index.js +21 -1
  23. package/oclif.manifest.json +1 -1
  24. package/package.json +6 -1
package/CHANGELOG.md CHANGED
@@ -1,3 +1,19 @@
1
+ ## [4.23.2](https://gitlab.orestes.info/baqend/speed-kit-cli/compare/v4.23.1...v4.23.2) (2026-08-21)
2
+
3
+
4
+ ### Bug Fixes
5
+
6
+ * **onboarding:** answer a Document Handler abort with 400, not 500 ([c4473ee](https://gitlab.orestes.info/baqend/speed-kit-cli/commit/c4473eee65011c260caddbd073098cb21e652ae8))
7
+ * **onboarding:** send the abort as errorcode;desc=303, not an invented cause ([0e3beb0](https://gitlab.orestes.info/baqend/speed-kit-cli/commit/0e3beb034ff5a47a4acffcb86191fc9a8f5683ea))
8
+
9
+ ## [4.23.1](https://gitlab.orestes.info/baqend/speed-kit-cli/compare/v4.23.0...v4.23.1) (2026-08-21)
10
+
11
+
12
+ ### Bug Fixes
13
+
14
+ * **cli:** resolve IPv4 addresses before IPv6 ([057fa59](https://gitlab.orestes.info/baqend/speed-kit-cli/commit/057fa59bb06f3d49af5b8631681e7850a062d585))
15
+ * **onboarding:** allow one hour Shopify staleness for local runs ([4e31124](https://gitlab.orestes.info/baqend/speed-kit-cli/commit/4e31124aa3c217f698ef3bb48a51c83fb5395ca4))
16
+
1
17
  # [4.23.0](https://gitlab.orestes.info/baqend/speed-kit-cli/compare/v4.22.1...v4.23.0) (2026-08-21)
2
18
 
3
19
 
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.23.0 linux-x64 node-v22.23.2
24
+ @speedkit/cli/4.23.2 linux-x64 node-v22.23.2
25
25
  $ sk --help [COMMAND]
26
26
  USAGE
27
27
  $ sk COMMAND
@@ -0,0 +1,12 @@
1
+ import { Hook } from "@oclif/core";
2
+ /**
3
+ * Puts IPv4 addresses first for every lookup the CLI makes. This is the runtime equivalent of
4
+ * `NODE_OPTIONS=--dns-result-order=ipv4first`, so no entrypoint has to export that variable.
5
+ *
6
+ * Node resolves `localhost` to `::1` first. WSL in mirrored networking mode shares `localhost`
7
+ * between Windows and Linux, but Chrome's remote debugging port and the local dev server both
8
+ * listen on IPv4 only. A lookup that answers `::1` therefore never reaches the listener on the
9
+ * other side of the boundary, and the connection fails instead of crossing over.
10
+ */
11
+ declare const hook: Hook<"init">;
12
+ export default hook;
@@ -0,0 +1,14 @@
1
+ import { setDefaultResultOrder } from "node:dns";
2
+ /**
3
+ * Puts IPv4 addresses first for every lookup the CLI makes. This is the runtime equivalent of
4
+ * `NODE_OPTIONS=--dns-result-order=ipv4first`, so no entrypoint has to export that variable.
5
+ *
6
+ * Node resolves `localhost` to `::1` first. WSL in mirrored networking mode shares `localhost`
7
+ * between Windows and Linux, but Chrome's remote debugging port and the local dev server both
8
+ * listen on IPv4 only. A lookup that answers `::1` therefore never reaches the listener on the
9
+ * other side of the boundary, and the connection fails instead of crossing over.
10
+ */
11
+ const hook = async () => {
12
+ setDefaultResultOrder("ipv4first");
13
+ };
14
+ export default hook;
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,15 @@
1
+ import { expect } from "chai";
2
+ import { after, describe, it } from "mocha";
3
+ import { getDefaultResultOrder, setDefaultResultOrder } from "node:dns";
4
+ import hook from "./dns-result-order.js";
5
+ describe("dns-result-order hook", () => {
6
+ const previousOrder = getDefaultResultOrder();
7
+ after(() => {
8
+ setDefaultResultOrder(previousOrder);
9
+ });
10
+ it("puts IPv4 addresses first", async () => {
11
+ setDefaultResultOrder("verbatim");
12
+ await hook.call(null, {});
13
+ expect(getDefaultResultOrder()).to.equal("ipv4first");
14
+ });
15
+ });
@@ -6,6 +6,7 @@ export declare const LOCAL_DYNAMIC_FETCHER_CONFIG = "config_dynamicBlocks";
6
6
  export declare const LOCAL_STYLES = "config_dynamicStyles";
7
7
  export declare const LATEST_DOCUMENT_HANDLER_URL = "https://www.baqend.com/speed-kit-handler/latest/DocumentHandler.js";
8
8
  export declare const LATEST_DYNAMIC_FETCHER_URL = "https://www.baqend.com/speed-kit/latest/dynamic-fetcher.js";
9
+ export declare const LOCAL_TRIGGERED_BY = "STREAM:QA";
9
10
  export declare const TEST_DOCUMENT_HANDLER_FILE_NAME = "testDocumentHandler";
10
11
  export declare const TEST_DATABASE_MOCK_FILE_NAME = "database-mock";
11
12
  export declare const TEST_ORESTES_MOCK_FILE_NAME = "orestes-mock";
@@ -6,6 +6,10 @@ export const LOCAL_DYNAMIC_FETCHER_CONFIG = "config_dynamicBlocks";
6
6
  export const LOCAL_STYLES = "config_dynamicStyles";
7
7
  export const LATEST_DOCUMENT_HANDLER_URL = "https://www.baqend.com/speed-kit-handler/latest/DocumentHandler.js";
8
8
  export const LATEST_DYNAMIC_FETCHER_URL = "https://www.baqend.com/speed-kit/latest/dynamic-fetcher.js";
9
+ // Local runs have no persistent cache, so an origin HTML is served straight to the developer and may
10
+ // legitimately be older than the default five minutes. STREAM:QA relaxes the document handler's
11
+ // Shopify staleness check to one hour, see ONE_HOUR_STALENESS_TRIGGERS in document-handler.
12
+ export const LOCAL_TRIGGERED_BY = "STREAM:QA";
9
13
  export const TEST_DOCUMENT_HANDLER_FILE_NAME = "testDocumentHandler";
10
14
  export const TEST_DATABASE_MOCK_FILE_NAME = "database-mock";
11
15
  export const TEST_ORESTES_MOCK_FILE_NAME = "orestes-mock";
@@ -0,0 +1,58 @@
1
+ /**
2
+ * Baqend's runtime exposes `Abort` as a global to Document Handler code. Throwing it
3
+ * signals a deliberate stop — a `blacklistHTML` guard, or a pre-render that came back
4
+ * non-200 — rather than a fault. Baqend extracts the message and answers a Bad Request,
5
+ * and the service worker falls back to origin.
6
+ *
7
+ * The shape follows `typings/Abort/index.d.ts` in the document-handler repository:
8
+ * `name` is always `"Abort"`, plus a `status` and an optional `data` payload.
9
+ */
10
+ export declare class DocumentHandlerAbort<T = unknown> extends Error {
11
+ readonly name = "Abort";
12
+ status: number;
13
+ data?: T;
14
+ /**
15
+ * Creates a Document Handler abort.
16
+ *
17
+ * @param message - Reason the Document Handler stopped.
18
+ * @param data - Optional abort payload.
19
+ */
20
+ constructor(message?: string, data?: T);
21
+ }
22
+ /**
23
+ * Status code Baqend answers a Document Handler abort with. It must stay below 500:
24
+ * the service worker treats 5xx from the Asset API as an infrastructure failure and
25
+ * disconnects Speed Kit, while a client error is classified from `server-timing`.
26
+ */
27
+ export declare const ABORT_STATUS_CODE = 400;
28
+ /**
29
+ * Speed Kit error code for a Document Handler abort. Baqend reports the numeric code,
30
+ * not a cause name, and the backend resolves 303 to `ServerDocumentHandlerAbort`.
31
+ *
32
+ * Sibling code 314 is `ServerDocumentHandlerError`, which is what a Document Handler
33
+ * that throws an ordinary error produces. Keeping the two apart is the point of this
34
+ * module.
35
+ */
36
+ export declare const ABORT_ERROR_CODE = "303";
37
+ /**
38
+ * `server-timing` value that makes the service worker classify a response as an abort.
39
+ *
40
+ * Only the `errorcode` metric is sent, which is what Baqend sends. The service worker
41
+ * reads `errorCause` and `errorcode` separately and builds the cause as
42
+ * `` `${errorCause?.desc || ClientError}${errorCode ? `-${errorCode.desc}` : ''}` ``
43
+ * (`AssetAPIHandler.ts:200-205`). With no `errorCause` metric it therefore reports
44
+ * `ClientError-303`, and the cause name is resolved from the code further downstream.
45
+ * Sending an `errorCause` metric instead would invent a cause the backend never sends.
46
+ */
47
+ export declare const ABORT_SERVER_TIMING = "errorcode;desc=303";
48
+ /**
49
+ * Whether an error is a Document Handler abort rather than a failure.
50
+ *
51
+ * Checks the `name` marker instead of `instanceof`: the error is constructed inside a
52
+ * `vm` context and is re-wrapped by `DocumentHandlerTransformError` on the way out, so
53
+ * neither the realm nor the prototype chain survives reliably. `isAbort` covers the
54
+ * wrapped case.
55
+ *
56
+ * @param error - Error to classify.
57
+ */
58
+ export declare function isDocumentHandlerAbort(error?: Error): boolean;
@@ -0,0 +1,66 @@
1
+ /**
2
+ * Baqend's runtime exposes `Abort` as a global to Document Handler code. Throwing it
3
+ * signals a deliberate stop — a `blacklistHTML` guard, or a pre-render that came back
4
+ * non-200 — rather than a fault. Baqend extracts the message and answers a Bad Request,
5
+ * and the service worker falls back to origin.
6
+ *
7
+ * The shape follows `typings/Abort/index.d.ts` in the document-handler repository:
8
+ * `name` is always `"Abort"`, plus a `status` and an optional `data` payload.
9
+ */
10
+ export class DocumentHandlerAbort extends Error {
11
+ name = "Abort";
12
+ status = ABORT_STATUS_CODE;
13
+ data;
14
+ /**
15
+ * Creates a Document Handler abort.
16
+ *
17
+ * @param message - Reason the Document Handler stopped.
18
+ * @param data - Optional abort payload.
19
+ */
20
+ constructor(message, data) {
21
+ super(message);
22
+ this.data = data;
23
+ }
24
+ }
25
+ /**
26
+ * Status code Baqend answers a Document Handler abort with. It must stay below 500:
27
+ * the service worker treats 5xx from the Asset API as an infrastructure failure and
28
+ * disconnects Speed Kit, while a client error is classified from `server-timing`.
29
+ */
30
+ export const ABORT_STATUS_CODE = 400;
31
+ /**
32
+ * Speed Kit error code for a Document Handler abort. Baqend reports the numeric code,
33
+ * not a cause name, and the backend resolves 303 to `ServerDocumentHandlerAbort`.
34
+ *
35
+ * Sibling code 314 is `ServerDocumentHandlerError`, which is what a Document Handler
36
+ * that throws an ordinary error produces. Keeping the two apart is the point of this
37
+ * module.
38
+ */
39
+ export const ABORT_ERROR_CODE = "303";
40
+ /**
41
+ * `server-timing` value that makes the service worker classify a response as an abort.
42
+ *
43
+ * Only the `errorcode` metric is sent, which is what Baqend sends. The service worker
44
+ * reads `errorCause` and `errorcode` separately and builds the cause as
45
+ * `` `${errorCause?.desc || ClientError}${errorCode ? `-${errorCode.desc}` : ''}` ``
46
+ * (`AssetAPIHandler.ts:200-205`). With no `errorCause` metric it therefore reports
47
+ * `ClientError-303`, and the cause name is resolved from the code further downstream.
48
+ * Sending an `errorCause` metric instead would invent a cause the backend never sends.
49
+ */
50
+ export const ABORT_SERVER_TIMING = `errorcode;desc=${ABORT_ERROR_CODE}`;
51
+ /**
52
+ * Whether an error is a Document Handler abort rather than a failure.
53
+ *
54
+ * Checks the `name` marker instead of `instanceof`: the error is constructed inside a
55
+ * `vm` context and is re-wrapped by `DocumentHandlerTransformError` on the way out, so
56
+ * neither the realm nor the prototype chain survives reliably. `isAbort` covers the
57
+ * wrapped case.
58
+ *
59
+ * @param error - Error to classify.
60
+ */
61
+ export function isDocumentHandlerAbort(error) {
62
+ if (!error) {
63
+ return false;
64
+ }
65
+ return (error.name === "Abort" || error.isAbort === true);
66
+ }
@@ -0,0 +1,84 @@
1
+ import { expect } from "chai";
2
+ import { describe, it } from "mocha";
3
+ import { ABORT_ERROR_CODE, ABORT_SERVER_TIMING, ABORT_STATUS_CODE, DocumentHandlerAbort, isDocumentHandlerAbort, } from "./document-handler-abort.js";
4
+ import { DocumentHandlerTransformError } from "../onboarding/error/document-handler-transform-error.js";
5
+ /**
6
+ * Mirrors the service worker's `parseServerTimingHeader`, so the header this module
7
+ * emits is asserted against the parser that has to read it.
8
+ */
9
+ function parseServerTiming(header) {
10
+ const metrics = {};
11
+ header.split(",").forEach((metric) => {
12
+ const key = metric.includes(";") ? metric.trim().split(";")[0] : metric;
13
+ const descMatch = metric.match(/desc=([^;]*)/);
14
+ metrics[key] = { desc: descMatch ? descMatch[1] : "" };
15
+ });
16
+ return metrics;
17
+ }
18
+ /**
19
+ * Mirrors how the service worker builds the response cause in
20
+ * `AssetAPIHandler.ts:200-205`, to prove which cause the emitted header produces.
21
+ */
22
+ function composeResponseCause(header) {
23
+ const metrics = parseServerTiming(header);
24
+ const errorCode = metrics.errorcode;
25
+ const errorCodeString = errorCode ? `-${errorCode.desc}` : "";
26
+ return `${metrics.errorCause?.desc || "ClientError"}${errorCodeString}`;
27
+ }
28
+ describe("DocumentHandlerAbort", () => {
29
+ it("matches the runtime contract the Document Handler expects", () => {
30
+ const abort = new DocumentHandlerAbort("blacklisted", { url: "/a" });
31
+ expect(abort.name).to.equal("Abort");
32
+ expect(abort.message).to.equal("blacklisted");
33
+ expect(abort.status).to.equal(ABORT_STATUS_CODE);
34
+ expect(abort.data).to.deep.equal({ url: "/a" });
35
+ expect(abort).to.be.instanceOf(Error);
36
+ });
37
+ it("keeps the abort status below the range that disconnects Speed Kit", () => {
38
+ expect(ABORT_STATUS_CODE).to.be.lessThan(500);
39
+ });
40
+ });
41
+ describe("isDocumentHandlerAbort", () => {
42
+ it("recognises an abort by its name marker", () => {
43
+ expect(isDocumentHandlerAbort(new DocumentHandlerAbort("stop"))).to.equal(true);
44
+ });
45
+ it("recognises an abort that only carries the wrapped flag", () => {
46
+ const wrapped = Object.assign(new Error("stop"), { isAbort: true });
47
+ expect(isDocumentHandlerAbort(wrapped)).to.equal(true);
48
+ });
49
+ it("does not treat an ordinary failure as an abort", () => {
50
+ expect(isDocumentHandlerAbort(new Error("boom"))).to.equal(false);
51
+ expect(isDocumentHandlerAbort(undefined)).to.equal(false);
52
+ });
53
+ });
54
+ describe("DocumentHandlerTransformError", () => {
55
+ it("carries the abort marker across the wrapper", () => {
56
+ const wrapped = new DocumentHandlerTransformError(new DocumentHandlerAbort("No category grid"));
57
+ expect(wrapped.isAbort).to.equal(true);
58
+ expect(wrapped.message).to.equal("No category grid");
59
+ expect(isDocumentHandlerAbort(wrapped)).to.equal(true);
60
+ });
61
+ it("leaves an ordinary failure unmarked", () => {
62
+ const wrapped = new DocumentHandlerTransformError(new Error("boom"));
63
+ expect(wrapped.isAbort).to.equal(false);
64
+ expect(isDocumentHandlerAbort(wrapped)).to.equal(false);
65
+ });
66
+ });
67
+ describe("ABORT_SERVER_TIMING", () => {
68
+ it("sends the error code under the metric name Baqend uses", () => {
69
+ const metrics = parseServerTiming(ABORT_SERVER_TIMING);
70
+ expect(metrics.errorcode.desc).to.equal(ABORT_ERROR_CODE);
71
+ });
72
+ it("sends no errorCause metric, which Baqend does not send either", () => {
73
+ expect(parseServerTiming(ABORT_SERVER_TIMING).errorCause).to.equal(undefined);
74
+ });
75
+ it("makes the service worker report ClientError-303", () => {
76
+ expect(composeResponseCause(ABORT_SERVER_TIMING)).to.equal("ClientError-303");
77
+ });
78
+ it("is matched by the prewarm error-code selector when a metric follows", () => {
79
+ // TIMING_HEADER_ERROR_CODE_SELECTOR expects a trailing comma, as production
80
+ // sends further metrics after the code.
81
+ const [, code] = `${ABORT_SERVER_TIMING},dur=0`.match(/errorcode;desc=([^,]*),/) || [];
82
+ expect(code).to.equal(ABORT_ERROR_CODE);
83
+ });
84
+ });
@@ -8,6 +8,7 @@ import { INTEGRATION_FILES } from "../../models/files.js";
8
8
  import fetch from "node-fetch";
9
9
  import { Request } from "./server/request.js";
10
10
  import { DocumentHandlerResponse } from "./server/document-handler-response.js";
11
+ import { DocumentHandlerAbort } from "./document-handler-abort.js";
11
12
  import { safe } from "../../helpers/safe.js";
12
13
  import { VmEmptyResponseError } from "../onboarding/error/vm-empty-response-error.js";
13
14
  import { DocumentHandlerTransformError } from "../onboarding/error/document-handler-transform-error.js";
@@ -46,7 +47,7 @@ export class DocumentHandlerServer {
46
47
  skInternalResponse,
47
48
  skInternalHtml: "",
48
49
  skInternalError: {},
49
- Abort: Error,
50
+ Abort: DocumentHandlerAbort,
50
51
  APP: this.customerConfig.app,
51
52
  DYNAMIC_FETCHER: await this.getDynamicFetcher(),
52
53
  DYNAMIC_FETCHER_CONFIG: await this.getDynamicFetcherConfig(),
@@ -2,6 +2,7 @@ interface query {
2
2
  url: string;
3
3
  variation: string;
4
4
  headers: string;
5
+ triggeredBy: string;
5
6
  }
6
7
  export declare class Request {
7
8
  query: query;
@@ -1,9 +1,15 @@
1
+ import { LOCAL_TRIGGERED_BY } from "../context/document-handler-runtime-context.js";
1
2
  export class Request {
2
3
  query;
3
4
  body;
4
5
  headers;
5
6
  constructor(url, body, variation, originalHeaders, requestHeaders = {}) {
6
- this.query = { variation, headers: JSON.stringify(originalHeaders), url };
7
+ this.query = {
8
+ variation,
9
+ headers: JSON.stringify(originalHeaders),
10
+ url,
11
+ triggeredBy: LOCAL_TRIGGERED_BY,
12
+ };
7
13
  this.body = body;
8
14
  this.headers = {};
9
15
  for (const key of Object.keys(requestHeaders)) {
@@ -1,4 +1,5 @@
1
1
  import AbstractTemplate from "./abstract-template.js";
2
+ import { LOCAL_TRIGGERED_BY } from "../context/document-handler-runtime-context.js";
2
3
  export default class ExecuteDocumentHandler extends AbstractTemplate {
3
4
  buildContext(domain, file) {
4
5
  return { domain, file };
@@ -35,6 +36,7 @@ async function run(iterator) {
35
36
  variation: item.variation || 'DEFAULT',
36
37
  headers: item?.headers ? item.headers : JSON.stringify(DEFAULT_HEADERS),
37
38
  url: item.url,
39
+ triggeredBy: '${LOCAL_TRIGGERED_BY}',
38
40
  }
39
41
  let req = {
40
42
  query, body: file
@@ -1,4 +1,5 @@
1
1
  import AbstractTemplate from "./abstract-template.js";
2
+ import { LOCAL_TRIGGERED_BY } from "../context/document-handler-runtime-context.js";
2
3
  export default class OrestesMock extends AbstractTemplate {
3
4
  buildContext() {
4
5
  return {};
@@ -79,6 +80,7 @@ const server = http.createServer(async function (request, response) {
79
80
  variation: UrlObject.searchParams.has('bqvariation')?UrlObject.searchParams.get('bqvariation'):'DEFAULT',
80
81
  headers: '',
81
82
  url: url,
83
+ triggeredBy: '${LOCAL_TRIGGERED_BY}',
82
84
  }
83
85
  let req = {query, body}
84
86
  const output = await documentHandler.transform(database,req)
@@ -1,4 +1,5 @@
1
1
  import AbstractTemplate from "./abstract-template.js";
2
+ import { LOCAL_TRIGGERED_BY } from "../context/document-handler-runtime-context.js";
2
3
  export default class Test extends AbstractTemplate {
3
4
  buildContext(domain, name, timeout = 2000) {
4
5
  return { domain, name, timeout };
@@ -9,7 +10,9 @@ const database = require('./src/database-mock');
9
10
  const documentHandler = require('./src/testDocumentHandler');
10
11
  const fs = require('fs');
11
12
  const {resolve} = require("path");
12
- global.Abort = Error;
13
+ global.Abort = class Abort extends Error {
14
+ constructor(message, data) { super(message); this.name = 'Abort'; this.status = 400; this.data = data; }
15
+ };
13
16
 
14
17
  const url = '{{domain}}';
15
18
  const file = '{{name}}';
@@ -25,6 +28,7 @@ describe(\`documentHandler run on \${file}\`, function () {
25
28
  variation: 'DEFAULT',
26
29
  headers: JSON.stringify(DEFAULT_HEADERS),
27
30
  url: url,
31
+ triggeredBy: '${LOCAL_TRIGGERED_BY}',
28
32
  }
29
33
 
30
34
  let req = {query, body}
@@ -42,6 +46,7 @@ describe(\`create files to diff for \${file}\`,function (){
42
46
  variation: 'DEFAULT',
43
47
  headers: JSON.stringify(DEFAULT_HEADERS),
44
48
  url: url,
49
+ triggeredBy: '${LOCAL_TRIGGERED_BY}',
45
50
  }
46
51
 
47
52
  let req = {query, body}
@@ -1,3 +1,9 @@
1
1
  export declare class DocumentHandlerTransformError extends Error {
2
+ /**
3
+ * True when the Document Handler stopped deliberately by throwing `Abort`, so the
4
+ * caller can answer with a client error instead of a 500. The wrapper drops the
5
+ * original prototype, so the flag has to be carried explicitly.
6
+ */
7
+ readonly isAbort: boolean;
2
8
  constructor(error: Error);
3
9
  }
@@ -1,7 +1,15 @@
1
+ import { isDocumentHandlerAbort } from "../../document-handler-runtime/document-handler-abort.js";
1
2
  export class DocumentHandlerTransformError extends Error {
3
+ /**
4
+ * True when the Document Handler stopped deliberately by throwing `Abort`, so the
5
+ * caller can answer with a client error instead of a 500. The wrapper drops the
6
+ * original prototype, so the flag has to be carried explicitly.
7
+ */
8
+ isAbort;
2
9
  constructor(error) {
3
10
  super(error.message);
4
11
  this.stack = error.stack;
5
12
  this.cause = error.cause;
13
+ this.isAbort = isDocumentHandlerAbort(error);
6
14
  }
7
15
  }
@@ -52,6 +52,14 @@ export declare class VirtualOrestesApp {
52
52
  private recordLocalCacheError;
53
53
  private clearLocalCacheError;
54
54
  private emitLocalCacheErrors;
55
+ /**
56
+ * Answers a Document Handler abort the way Baqend does: a Bad Request carrying the
57
+ * Speed Kit error code in `server-timing`. The service worker reports
58
+ * `ClientError-303` and falls back to origin, and the backend resolves 303 to
59
+ * `ServerDocumentHandlerAbort`. Answering 500 instead makes the service worker treat
60
+ * the page as an infrastructure failure and disconnect Speed Kit.
61
+ */
62
+ private returnAbortResponse;
55
63
  private returnErrorResponse;
56
64
  private convertResponseHeadersToOrestesFormat;
57
65
  private returnRedirect;
@@ -1,4 +1,5 @@
1
1
  import { BaqendResponse } from "../browser/baqend-response.js";
2
+ import { ABORT_SERVER_TIMING, ABORT_STATUS_CODE, isDocumentHandlerAbort, } from "../../document-handler-runtime/document-handler-abort.js";
2
3
  import { safe } from "../../../helpers/safe.js";
3
4
  import { META_CONTENT_TYPE_REGEX, } from "../onboarding-model.js";
4
5
  import iconv from "iconv-lite";
@@ -142,7 +143,10 @@ export class VirtualOrestesApp {
142
143
  }
143
144
  const customResponse = await safe(this.documentHandler.transform(`text/html;charset=${textEncoding}`, responseContent, variation, originUrl, this.convertResponseHeadersToOrestesFormat(response)));
144
145
  if (customResponse.success === false) {
145
- this.recordLocalCacheError(originUrl, requestUrl, variation, customResponse);
146
+ // An abort is a decision, not a fault, so it must not surface as a cache error.
147
+ if (!isDocumentHandlerAbort(customResponse.errorObj)) {
148
+ this.recordLocalCacheError(originUrl, requestUrl, variation, customResponse);
149
+ }
146
150
  return this.returnErrorResponse(customResponse, customHeaders);
147
151
  }
148
152
  this.clearLocalCacheError(originUrl, variation);
@@ -231,7 +235,23 @@ export class VirtualOrestesApp {
231
235
  data: [...this.localCacheErrors.values()],
232
236
  });
233
237
  }
238
+ /**
239
+ * Answers a Document Handler abort the way Baqend does: a Bad Request carrying the
240
+ * Speed Kit error code in `server-timing`. The service worker reports
241
+ * `ClientError-303` and falls back to origin, and the backend resolves 303 to
242
+ * `ServerDocumentHandlerAbort`. Answering 500 instead makes the service worker treat
243
+ * the page as an infrastructure failure and disconnect Speed Kit.
244
+ */
245
+ returnAbortResponse(customResponse, customHeaders) {
246
+ const message = customResponse.error;
247
+ this.cli.writeWarning(`[VirtualOrestes]: Document Handler aborted: ${message}`);
248
+ customHeaders.push({ name: "x-error", value: message }, { name: "server-timing", value: ABORT_SERVER_TIMING });
249
+ return new BaqendResponse(message, null, customHeaders, ABORT_STATUS_CODE);
250
+ }
234
251
  returnErrorResponse(customResponse, customHeaders) {
252
+ if (isDocumentHandlerAbort(customResponse.errorObj)) {
253
+ return this.returnAbortResponse(customResponse, customHeaders);
254
+ }
235
255
  const message = `Could not transform html: - ${customResponse.error}`;
236
256
  this.cli.writeError(`[VirtualOrestes]: ${message}`);
237
257
  this.cli.comment(customResponse.errorObj.stack);
@@ -1220,5 +1220,5 @@
1220
1220
  ]
1221
1221
  }
1222
1222
  },
1223
- "version": "4.23.0"
1223
+ "version": "4.23.2"
1224
1224
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@speedkit/cli",
3
3
  "description": "Speed Kit CLI",
4
- "version": "4.23.0",
4
+ "version": "4.23.2",
5
5
  "author": {
6
6
  "name": "Baqend.com",
7
7
  "email": "info@baqend.com"
@@ -64,6 +64,11 @@
64
64
  ],
65
65
  "oclif": {
66
66
  "commands": "./dist/commands",
67
+ "hooks": {
68
+ "init": [
69
+ "./dist/hooks/init/dns-result-order.js"
70
+ ]
71
+ },
67
72
  "bin": "sk",
68
73
  "dirname": "speed-kit-cli",
69
74
  "plugins": [