@speedkit/cli 4.23.1 → 4.23.3

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,18 @@
1
+ ## [4.23.3](https://gitlab.orestes.info/baqend/speed-kit-cli/compare/v4.23.2...v4.23.3) (2026-08-25)
2
+
3
+
4
+ ### Bug Fixes
5
+
6
+ * **deploy:** match refresh job names by prefix ([c593d9d](https://gitlab.orestes.info/baqend/speed-kit-cli/commit/c593d9d69db8e05ccb71b320cb4f9c7cbda5cdac))
7
+
8
+ ## [4.23.2](https://gitlab.orestes.info/baqend/speed-kit-cli/compare/v4.23.1...v4.23.2) (2026-08-21)
9
+
10
+
11
+ ### Bug Fixes
12
+
13
+ * **onboarding:** answer a Document Handler abort with 400, not 500 ([c4473ee](https://gitlab.orestes.info/baqend/speed-kit-cli/commit/c4473eee65011c260caddbd073098cb21e652ae8))
14
+ * **onboarding:** send the abort as errorcode;desc=303, not an invented cause ([0e3beb0](https://gitlab.orestes.info/baqend/speed-kit-cli/commit/0e3beb034ff5a47a4acffcb86191fc9a8f5683ea))
15
+
1
16
  ## [4.23.1](https://gitlab.orestes.info/baqend/speed-kit-cli/compare/v4.23.0...v4.23.1) (2026-08-21)
2
17
 
3
18
 
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.1 linux-x64 node-v22.23.2
24
+ @speedkit/cli/4.23.3 linux-x64 node-v22.23.2
25
25
  $ sk --help [COMMAND]
26
26
  USAGE
27
27
  $ sk COMMAND
@@ -1,4 +1,3 @@
1
- import { Set } from "baqend";
2
1
  import ApplicationError from "../../../error-handling/error/application-error.js";
3
2
  export class RefreshDeploymentJobCheck {
4
3
  entityManager;
@@ -12,8 +11,10 @@ export class RefreshDeploymentJobCheck {
12
11
  .find()
13
12
  .resultList();
14
13
  const requiredJobs = ["HTML", "Landing Page", "CSS, JS, Font"];
15
- const foundJobNames = new Set(jobs.map((job) => job?.data?.name || " "));
16
- const missingRequiredJobs = requiredJobs.filter((job) => !foundJobNames.has(job));
14
+ // Job names are free text and sometimes suffixed (e.g. "Landing Page (RegExp)"),
15
+ // so match on the prefix instead of the full name.
16
+ const foundJobNames = jobs.map((job) => (job?.data?.name || "").trim().toLowerCase());
17
+ const missingRequiredJobs = requiredJobs.filter((required) => !foundJobNames.some((name) => name.startsWith(required.toLowerCase())));
17
18
  // Deployment jobs: soft warning only
18
19
  const deploymentJobsMissing = jobs.filter((job) => job.type === "Deploymentjob" && job.created !== true);
19
20
  if (deploymentJobsMissing.length > 0) {
@@ -0,0 +1,33 @@
1
+ import { expect } from "chai";
2
+ import { describe, it } from "mocha";
3
+ import { RefreshDeploymentJobCheck } from "./refresh-deployment-job-check.js";
4
+ import ApplicationError from "../../../error-handling/error/application-error.js";
5
+ function entityManagerWithJobs(names) {
6
+ const jobs = names.map((name) => ({ data: { name }, type: "Refreshjob" }));
7
+ return {
8
+ "jobs.Definition": {
9
+ find: () => ({ resultList: async () => jobs }),
10
+ },
11
+ };
12
+ }
13
+ const cli = { writeWarning: () => undefined };
14
+ async function runCheck(names) {
15
+ try {
16
+ await new RefreshDeploymentJobCheck(entityManagerWithJobs(names), cli).check();
17
+ }
18
+ catch (error) {
19
+ return error;
20
+ }
21
+ return undefined;
22
+ }
23
+ describe("RefreshDeploymentJobCheck", () => {
24
+ it("accepts jobs whose names carry a suffix", async () => {
25
+ // As used by zeg and tectake.
26
+ expect(await runCheck(["HTML", "Landing Page (RegExp)", "CSS, JS, Font"])).to.equal(undefined);
27
+ });
28
+ it("reports jobs that are missing entirely", async () => {
29
+ const error = await runCheck(["HTML", "Images"]);
30
+ expect(error).to.be.instanceOf(ApplicationError);
31
+ expect(error.suggestions[0]).to.equal("Missing required jobs: Landing Page, CSS, JS, Font");
32
+ });
33
+ });
@@ -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(),
@@ -10,7 +10,9 @@ const database = require('./src/database-mock');
10
10
  const documentHandler = require('./src/testDocumentHandler');
11
11
  const fs = require('fs');
12
12
  const {resolve} = require("path");
13
- 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
+ };
14
16
 
15
17
  const url = '{{domain}}';
16
18
  const file = '{{name}}';
@@ -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.1"
1223
+ "version": "4.23.3"
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.1",
4
+ "version": "4.23.3",
5
5
  "author": {
6
6
  "name": "Baqend.com",
7
7
  "email": "info@baqend.com"