@speedkit/cli 4.23.2 → 4.24.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 (31) hide show
  1. package/.env.sample +7 -0
  2. package/CHANGELOG.md +21 -0
  3. package/README.md +1 -1
  4. package/dist/helpers/origin-proxy.d.ts +89 -0
  5. package/dist/helpers/origin-proxy.js +207 -0
  6. package/dist/helpers/origin-proxy.spec.d.ts +1 -0
  7. package/dist/helpers/origin-proxy.spec.js +167 -0
  8. package/dist/helpers/scrape.js +2 -2
  9. package/dist/helpers/site-analyzer.js +2 -2
  10. package/dist/services/bundler/bundle-service-factory.d.ts +2 -1
  11. package/dist/services/bundler/bundle-service-factory.js +4 -2
  12. package/dist/services/bundler/bundle-service.d.ts +30 -2
  13. package/dist/services/bundler/bundle-service.js +81 -6
  14. package/dist/services/bundler/bundle-service.spec.js +124 -0
  15. package/dist/services/deploy/checks/post-deploy/refresh-deployment-job-check.js +4 -3
  16. package/dist/services/deploy/checks/post-deploy/refresh-deployment-job-check.spec.d.ts +1 -0
  17. package/dist/services/deploy/checks/post-deploy/refresh-deployment-job-check.spec.js +33 -0
  18. package/dist/services/document-handler-runtime/document-handler-server.d.ts +23 -3
  19. package/dist/services/document-handler-runtime/document-handler-server.js +66 -43
  20. package/dist/services/document-handler-runtime/factory/document-handler-runtime-service-factory.js +7 -2
  21. package/dist/services/onboarding/dashboard/request-diff-service.js +5 -11
  22. package/dist/services/onboarding/onboarding-service-factory.d.ts +0 -2
  23. package/dist/services/onboarding/onboarding-service-factory.js +5 -43
  24. package/dist/services/onboarding/virtual-orestes-app/crawler.d.ts +1 -3
  25. package/dist/services/onboarding/virtual-orestes-app/crawler.js +6 -10
  26. package/dist/services/onboarding/virtual-orestes-app/crawler.spec.d.ts +1 -0
  27. package/dist/services/onboarding/virtual-orestes-app/crawler.spec.js +49 -0
  28. package/dist/services/origin-request/origin-request-service-factory.js +5 -1
  29. package/dist/services/origin-request/origin-request-service.js +2 -1
  30. package/oclif.manifest.json +1 -1
  31. package/package.json +2 -1
package/.env.sample CHANGED
@@ -20,6 +20,13 @@
20
20
  # Chrome Test Browser will use an existing sub-profile named 'Default'
21
21
  # within CHROME_USER_PROFILE_PATH or creates one automatically on start up.
22
22
 
23
+ # Enforce a SOCKS proxy for every request the CLI sends to a customer origin.
24
+ # The value is a Chrome `--proxy-server` URL, so Chrome and the CLI share one setting.
25
+ # It overrides a `--proxy-server` entry in the customer config's chromeFlags. The CLI then names
26
+ # both proxies, because the customer config pins the exit region its origin expects.
27
+ # Requests to Baqend stay on a direct connection.
28
+ # SK_PROXY_SERVER='socks5://ap-northeast-1.proxy.baqend.com:1080'
29
+
23
30
  # Slack Deploy Token
24
31
  # SLACK_TOKEN='<SLACK_TOKEN_FROM_1PASSWORD>'
25
32
 
package/CHANGELOG.md CHANGED
@@ -1,3 +1,24 @@
1
+ # [4.24.0](https://gitlab.orestes.info/baqend/speed-kit-cli/compare/v4.23.3...v4.24.0) (2026-08-28)
2
+
3
+
4
+ ### Bug Fixes
5
+
6
+ * **onboarding:** apply the chromeFlags socks proxy to origin requests ([db0ce41](https://gitlab.orestes.info/baqend/speed-kit-cli/commit/db0ce412b3d0a9056d9eb1778da11d6d84649695))
7
+ * **onboarding:** fetch external modules from app ([59646e7](https://gitlab.orestes.info/baqend/speed-kit-cli/commit/59646e744fdeaf1f4b9e0091cc1c0c625478f2ab))
8
+ * **onboarding:** guard the crawler against a config without authentications ([10c75b8](https://gitlab.orestes.info/baqend/speed-kit-cli/commit/10c75b86fd2e7191057d04f2d1b257f7b2c4b577))
9
+
10
+
11
+ ### Features
12
+
13
+ * **cli:** enforce a socks proxy for every origin request via SK_PROXY_SERVER ([66fab11](https://gitlab.orestes.info/baqend/speed-kit-cli/commit/66fab119e3a5cb2f040c6d91c5f1f10b19e4e2a8))
14
+
15
+ ## [4.23.3](https://gitlab.orestes.info/baqend/speed-kit-cli/compare/v4.23.2...v4.23.3) (2026-08-25)
16
+
17
+
18
+ ### Bug Fixes
19
+
20
+ * **deploy:** match refresh job names by prefix ([c593d9d](https://gitlab.orestes.info/baqend/speed-kit-cli/commit/c593d9d69db8e05ccb71b320cb4f9c7cbda5cdac))
21
+
1
22
  ## [4.23.2](https://gitlab.orestes.info/baqend/speed-kit-cli/compare/v4.23.1...v4.23.2) (2026-08-21)
2
23
 
3
24
 
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.2 linux-x64 node-v22.23.2
24
+ @speedkit/cli/4.24.0 linux-x64 node-v22.23.2
25
25
  $ sk --help [COMMAND]
26
26
  USAGE
27
27
  $ sk COMMAND
@@ -0,0 +1,89 @@
1
+ /**
2
+ * Environment variable that enforces a SOCKS proxy for every origin-directed request the CLI
3
+ * makes. It carries a Chrome `--proxy-server` value, so one string serves Chrome and the CLI, e.g.
4
+ * `SK_PROXY_SERVER=socks5://ap-northeast-1.proxy.baqend.com:1080`.
5
+ */
6
+ export declare const PROXY_ENVIRONMENT_VARIABLE = "SK_PROXY_SERVER";
7
+ /** SOCKS proxy in the shape `socksDispatcher` expects. */
8
+ export interface OriginProxy {
9
+ type: 4 | 5;
10
+ host: string;
11
+ port: number;
12
+ }
13
+ /** Only `writeWarning` is used, so this helper does not depend on the CLI service. */
14
+ export interface ProxyLogger {
15
+ writeWarning(message: string): void;
16
+ }
17
+ /** Per-request transport choice. */
18
+ export interface OriginFetchOptions {
19
+ /**
20
+ * Accepts an untrusted certificate and allows legacy renegotiation. Set it only where the call
21
+ * site already tolerated a broken origin handshake, because it hides a real certificate fault.
22
+ */
23
+ tolerateLegacyTls?: boolean;
24
+ }
25
+ /**
26
+ * Parses a Chrome `--proxy-server` value into a SOCKS proxy.
27
+ *
28
+ * A value without a port takes 1080, the port Chrome assumes for a SOCKS proxy, so one string keeps
29
+ * working for both.
30
+ *
31
+ * @param value - Proxy URL, e.g. `socks5://proxy.example.com:1080`.
32
+ * @returns The parsed proxy, or undefined when the value is empty or not a SOCKS URL.
33
+ */
34
+ export declare function parseSocksProxy(value?: string): OriginProxy | undefined;
35
+ /** Renders a proxy as the URL that Chrome and the log lines use. */
36
+ export declare function formatProxy(proxy: OriginProxy): string;
37
+ /** Renders a proxy as the Chrome flag that routes the browser through it. */
38
+ export declare function chromeProxyFlag(proxy: OriginProxy): string;
39
+ /**
40
+ * Finds the SOCKS proxy that a customer config pins through its Chrome flags.
41
+ *
42
+ * @param chromeFlags - `chromeFlags` of the customer config.
43
+ * @returns The `--proxy-server` value of the first SOCKS entry, or undefined.
44
+ */
45
+ export declare function findProxyInChromeFlags(chromeFlags?: string[]): string;
46
+ /**
47
+ * Selects the SOCKS proxy for all origin-directed requests and installs it for `originFetch`.
48
+ *
49
+ * `SK_PROXY_SERVER` wins over the customer config, so an operator can route a session through
50
+ * another region without editing a customer directory. The customer's `--proxy-server` chrome flag
51
+ * is the fallback.
52
+ *
53
+ * A customer pins the proxy for a reason: its origin expects the exit region the config names. The
54
+ * logger therefore reports both values whenever the environment displaces one, so a forgotten
55
+ * export cannot silently move a session out of the customer's region.
56
+ *
57
+ * @param chromeFlags - `chromeFlags` of the customer config, when a command has one.
58
+ * @param logger - Receives one line naming the proxy in use.
59
+ * @returns The proxy in use, or undefined for a direct connection.
60
+ * @throws When `SK_PROXY_SERVER` holds something that is not a SOCKS URL. Failing is safer than a
61
+ * silent direct connection, because the operator asked for a proxy.
62
+ */
63
+ export declare function applyOriginProxy(chromeFlags?: string[], logger?: ProxyLogger): OriginProxy | undefined;
64
+ /**
65
+ * Installs the proxy that `originFetch` uses. Prefer `applyOriginProxy`. This entry point exists
66
+ * for tests and for callers that resolved the proxy themselves.
67
+ */
68
+ export declare function setOriginProxy(proxy?: OriginProxy, fromEnvironment?: boolean): void;
69
+ /** Drops the installed proxy, so the next call resolves it again. */
70
+ export declare function resetOriginProxy(): void;
71
+ /**
72
+ * Appends the enforced proxy to the Chrome flags of a launch.
73
+ *
74
+ * Chrome takes the last `--proxy-server`, so the environment beats a customer config here just as
75
+ * it does for the CLI. A proxy that came from the flags already is left untouched.
76
+ *
77
+ * @param chromeFlags - `chromeFlags` of the customer config.
78
+ */
79
+ export declare function withOriginProxyFlag(chromeFlags?: string[]): string[];
80
+ /**
81
+ * Fetches a customer origin through the enforced SOCKS proxy.
82
+ *
83
+ * Every request that leaves the CLI for a customer origin has to use this instead of `fetch`, which
84
+ * carries no proxy support. Requests to Baqend keep their own client and connect directly.
85
+ *
86
+ * Node's built-in `fetch` rejects a dispatcher built by the `undici` package, so a request that
87
+ * needs one runs through undici's own `fetch`. A plain direct request keeps the global `fetch`.
88
+ */
89
+ export declare function originFetch(url: string, init?: RequestInit, options?: OriginFetchOptions): Promise<Response>;
@@ -0,0 +1,207 @@
1
+ import crypto from "node:crypto";
2
+ import { socksDispatcher } from "fetch-socks";
3
+ import { fetch as undiciFetch } from "undici";
4
+ /**
5
+ * Environment variable that enforces a SOCKS proxy for every origin-directed request the CLI
6
+ * makes. It carries a Chrome `--proxy-server` value, so one string serves Chrome and the CLI, e.g.
7
+ * `SK_PROXY_SERVER=socks5://ap-northeast-1.proxy.baqend.com:1080`.
8
+ */
9
+ export const PROXY_ENVIRONMENT_VARIABLE = "SK_PROXY_SERVER";
10
+ const PROXY_SCHEMES = {
11
+ "socks4:": 4,
12
+ "socks4a:": 4,
13
+ "socks5:": 5,
14
+ "socks5h:": 5,
15
+ };
16
+ const CHROME_PROXY_FLAG = "--proxy-server=";
17
+ /** Port Chrome assumes for a SOCKS proxy that names none. */
18
+ const DEFAULT_SOCKS_PORT = 1080;
19
+ let state;
20
+ /**
21
+ * Parses a Chrome `--proxy-server` value into a SOCKS proxy.
22
+ *
23
+ * A value without a port takes 1080, the port Chrome assumes for a SOCKS proxy, so one string keeps
24
+ * working for both.
25
+ *
26
+ * @param value - Proxy URL, e.g. `socks5://proxy.example.com:1080`.
27
+ * @returns The parsed proxy, or undefined when the value is empty or not a SOCKS URL.
28
+ */
29
+ export function parseSocksProxy(value) {
30
+ const trimmed = (value || "").trim();
31
+ if (!trimmed) {
32
+ return;
33
+ }
34
+ let url;
35
+ try {
36
+ url = new URL(trimmed);
37
+ }
38
+ catch {
39
+ return;
40
+ }
41
+ const type = PROXY_SCHEMES[url.protocol];
42
+ if (!type || !url.hostname) {
43
+ return;
44
+ }
45
+ return {
46
+ type,
47
+ host: url.hostname,
48
+ port: url.port ? Number(url.port) : DEFAULT_SOCKS_PORT,
49
+ };
50
+ }
51
+ /** Renders a proxy as the URL that Chrome and the log lines use. */
52
+ export function formatProxy(proxy) {
53
+ return `socks${proxy.type}://${proxy.host}:${proxy.port}`;
54
+ }
55
+ /** Renders a proxy as the Chrome flag that routes the browser through it. */
56
+ export function chromeProxyFlag(proxy) {
57
+ return `${CHROME_PROXY_FLAG}${formatProxy(proxy)}`;
58
+ }
59
+ /**
60
+ * Finds the SOCKS proxy that a customer config pins through its Chrome flags.
61
+ *
62
+ * @param chromeFlags - `chromeFlags` of the customer config.
63
+ * @returns The `--proxy-server` value of the first SOCKS entry, or undefined.
64
+ */
65
+ export function findProxyInChromeFlags(chromeFlags = []) {
66
+ for (const flag of chromeFlags) {
67
+ if (!flag.startsWith(CHROME_PROXY_FLAG)) {
68
+ continue;
69
+ }
70
+ const value = flag.slice(CHROME_PROXY_FLAG.length);
71
+ if (parseSocksProxy(value)) {
72
+ return value;
73
+ }
74
+ }
75
+ }
76
+ /**
77
+ * Selects the SOCKS proxy for all origin-directed requests and installs it for `originFetch`.
78
+ *
79
+ * `SK_PROXY_SERVER` wins over the customer config, so an operator can route a session through
80
+ * another region without editing a customer directory. The customer's `--proxy-server` chrome flag
81
+ * is the fallback.
82
+ *
83
+ * A customer pins the proxy for a reason: its origin expects the exit region the config names. The
84
+ * logger therefore reports both values whenever the environment displaces one, so a forgotten
85
+ * export cannot silently move a session out of the customer's region.
86
+ *
87
+ * @param chromeFlags - `chromeFlags` of the customer config, when a command has one.
88
+ * @param logger - Receives one line naming the proxy in use.
89
+ * @returns The proxy in use, or undefined for a direct connection.
90
+ * @throws When `SK_PROXY_SERVER` holds something that is not a SOCKS URL. Failing is safer than a
91
+ * silent direct connection, because the operator asked for a proxy.
92
+ */
93
+ export function applyOriginProxy(chromeFlags = [], logger) {
94
+ const fromEnvironment = (process.env[PROXY_ENVIRONMENT_VARIABLE] || "").trim();
95
+ if (fromEnvironment) {
96
+ const proxy = parseSocksProxy(fromEnvironment);
97
+ if (!proxy) {
98
+ throw new Error(`${PROXY_ENVIRONMENT_VARIABLE}="${fromEnvironment}" is not a SOCKS proxy URL. ` +
99
+ `Use socks4://host:port or socks5://host:port.`);
100
+ }
101
+ setOriginProxy(proxy, true);
102
+ logger?.writeWarning(enforcedProxyMessage(proxy, chromeFlags));
103
+ return proxy;
104
+ }
105
+ const proxy = parseSocksProxy(findProxyInChromeFlags(chromeFlags));
106
+ setOriginProxy(proxy, false);
107
+ if (proxy) {
108
+ logger?.writeWarning(`use proxy from chromeFlags for every origin request: ${formatProxy(proxy)}`);
109
+ }
110
+ return proxy;
111
+ }
112
+ /** True when both proxies name the same SOCKS endpoint. */
113
+ function isSameProxy(one, other) {
114
+ return (one.type === other.type &&
115
+ one.host === other.host &&
116
+ one.port === other.port);
117
+ }
118
+ /**
119
+ * Builds the line that reports an enforced proxy. A displaced customer proxy is named, because the
120
+ * operator has to see which region the session left behind.
121
+ */
122
+ function enforcedProxyMessage(proxy, chromeFlags) {
123
+ const pinned = parseSocksProxy(findProxyInChromeFlags(chromeFlags));
124
+ if (pinned && !isSameProxy(pinned, proxy)) {
125
+ return (`${PROXY_ENVIRONMENT_VARIABLE} replaces the proxy of the customer config. ` +
126
+ `Chrome and every origin request leave through ${formatProxy(proxy)}, ` +
127
+ `not through ${formatProxy(pinned)}. Unset the variable to use the customer's region.`);
128
+ }
129
+ return `${PROXY_ENVIRONMENT_VARIABLE} routes Chrome and every origin request through ${formatProxy(proxy)}`;
130
+ }
131
+ /**
132
+ * Installs the proxy that `originFetch` uses. Prefer `applyOriginProxy`. This entry point exists
133
+ * for tests and for callers that resolved the proxy themselves.
134
+ */
135
+ export function setOriginProxy(proxy, fromEnvironment = false) {
136
+ state = { proxy, fromEnvironment };
137
+ }
138
+ /** Drops the installed proxy, so the next call resolves it again. */
139
+ export function resetOriginProxy() {
140
+ state = undefined;
141
+ }
142
+ /**
143
+ * Appends the enforced proxy to the Chrome flags of a launch.
144
+ *
145
+ * Chrome takes the last `--proxy-server`, so the environment beats a customer config here just as
146
+ * it does for the CLI. A proxy that came from the flags already is left untouched.
147
+ *
148
+ * @param chromeFlags - `chromeFlags` of the customer config.
149
+ */
150
+ export function withOriginProxyFlag(chromeFlags = []) {
151
+ const current = currentState();
152
+ if (!current.proxy || !current.fromEnvironment) {
153
+ return chromeFlags;
154
+ }
155
+ return [...chromeFlags, chromeProxyFlag(current.proxy)];
156
+ }
157
+ /**
158
+ * Fetches a customer origin through the enforced SOCKS proxy.
159
+ *
160
+ * Every request that leaves the CLI for a customer origin has to use this instead of `fetch`, which
161
+ * carries no proxy support. Requests to Baqend keep their own client and connect directly.
162
+ *
163
+ * Node's built-in `fetch` rejects a dispatcher built by the `undici` package, so a request that
164
+ * needs one runs through undici's own `fetch`. A plain direct request keeps the global `fetch`.
165
+ */
166
+ export function originFetch(url, init, options = {}) {
167
+ const dispatcher = originDispatcher(options.tolerateLegacyTls === true);
168
+ if (!dispatcher) {
169
+ return fetch(url, init);
170
+ }
171
+ const request = { ...init, dispatcher };
172
+ return undiciFetch(url, request);
173
+ }
174
+ function currentState() {
175
+ if (!state) {
176
+ applyOriginProxy();
177
+ }
178
+ return state;
179
+ }
180
+ /**
181
+ * Builds the dispatcher for one transport and caches it.
182
+ *
183
+ * `socksDispatcher` connects directly when it gets no proxy, so the same builder serves a tolerant
184
+ * direct request. A strict direct request needs no dispatcher at all.
185
+ *
186
+ * @param tolerateLegacyTls - Accept an untrusted certificate and allow legacy renegotiation.
187
+ */
188
+ function originDispatcher(tolerateLegacyTls) {
189
+ const current = currentState();
190
+ if (!current.proxy && !tolerateLegacyTls) {
191
+ return;
192
+ }
193
+ const proxies = current.proxy ? [current.proxy] : [];
194
+ const connect = tolerateLegacyTls
195
+ ? {
196
+ keepAlive: true,
197
+ rejectUnauthorized: false,
198
+ secureOptions: crypto.constants.SSL_OP_LEGACY_SERVER_CONNECT,
199
+ }
200
+ : { keepAlive: true };
201
+ if (tolerateLegacyTls) {
202
+ current.tolerantDispatcher ??= socksDispatcher(proxies, { connect });
203
+ return current.tolerantDispatcher;
204
+ }
205
+ current.strictDispatcher ??= socksDispatcher(proxies, { connect });
206
+ return current.strictDispatcher;
207
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,167 @@
1
+ import { expect } from "chai";
2
+ import { describe, beforeEach, afterEach, it } from "mocha";
3
+ import http from "node:http";
4
+ import { applyOriginProxy, chromeProxyFlag, findProxyInChromeFlags, originFetch, parseSocksProxy, PROXY_ENVIRONMENT_VARIABLE, resetOriginProxy, setOriginProxy, withOriginProxyFlag, } from "./origin-proxy.js";
5
+ /** Sets or clears the enforcing environment variable. */
6
+ function setEnvironmentProxy(value) {
7
+ if (value === undefined) {
8
+ delete process.env.SK_PROXY_SERVER;
9
+ return;
10
+ }
11
+ process.env.SK_PROXY_SERVER = value;
12
+ }
13
+ /** Collects the warnings a caller would print. */
14
+ function recordingLogger() {
15
+ const warnings = [];
16
+ return {
17
+ warnings,
18
+ writeWarning: (message) => warnings.push(message),
19
+ };
20
+ }
21
+ describe("origin proxy", () => {
22
+ const originalEnvironment = process.env.SK_PROXY_SERVER;
23
+ beforeEach(() => {
24
+ setEnvironmentProxy(undefined);
25
+ resetOriginProxy();
26
+ });
27
+ afterEach(() => {
28
+ setEnvironmentProxy(originalEnvironment);
29
+ resetOriginProxy();
30
+ });
31
+ describe("parseSocksProxy", () => {
32
+ it("keeps the host without the scheme separator", () => {
33
+ expect(parseSocksProxy("socks5://ap-northeast-1.proxy.baqend.com:1080")).to.deep.equal({
34
+ type: 5,
35
+ host: "ap-northeast-1.proxy.baqend.com",
36
+ port: 1080,
37
+ });
38
+ });
39
+ it("maps every socks scheme to its protocol version", () => {
40
+ expect(parseSocksProxy("socks4://proxy.example.com:1080").type).to.equal(4);
41
+ expect(parseSocksProxy("socks4a://proxy.example.com:1080").type).to.equal(4);
42
+ expect(parseSocksProxy("socks5h://proxy.example.com:1080").type).to.equal(5);
43
+ });
44
+ it("takes 1080 for a value without a port, like chrome does", () => {
45
+ expect(parseSocksProxy("socks5://eu-central-1.proxy.baqend.com")).to.deep.equal({
46
+ type: 5,
47
+ host: "eu-central-1.proxy.baqend.com",
48
+ port: 1080,
49
+ });
50
+ });
51
+ it("rejects a value that is not a socks url", () => {
52
+ expect(parseSocksProxy("")).to.equal(undefined);
53
+ expect(parseSocksProxy("http://proxy.example.com:8080")).to.equal(undefined);
54
+ expect(parseSocksProxy("https://eu-central-1.proxy.baqend.com/")).to.equal(undefined);
55
+ expect(parseSocksProxy("proxy.example.com:1080")).to.equal(undefined);
56
+ });
57
+ });
58
+ describe("findProxyInChromeFlags", () => {
59
+ it("finds the socks entry among unrelated flags", () => {
60
+ const flags = [
61
+ "--window-size=390,844",
62
+ "--proxy-server=socks5://proxy.example.com:1080",
63
+ ];
64
+ expect(findProxyInChromeFlags(flags)).to.equal("socks5://proxy.example.com:1080");
65
+ });
66
+ it("ignores a non-socks proxy flag", () => {
67
+ expect(findProxyInChromeFlags([
68
+ "--proxy-server=http://proxy.example.com:8080",
69
+ ])).to.equal(undefined);
70
+ });
71
+ it("survives a config without chrome flags", () => {
72
+ expect(findProxyInChromeFlags()).to.equal(undefined);
73
+ expect(findProxyInChromeFlags([])).to.equal(undefined);
74
+ });
75
+ });
76
+ describe("applyOriginProxy", () => {
77
+ it("takes the proxy from the customer chrome flags", () => {
78
+ const logger = recordingLogger();
79
+ const proxy = applyOriginProxy(["--proxy-server=socks5://proxy.example.com:1080"], logger);
80
+ expect(proxy).to.deep.equal({
81
+ type: 5,
82
+ host: "proxy.example.com",
83
+ port: 1080,
84
+ });
85
+ expect(logger.warnings).to.have.length(1);
86
+ });
87
+ it("lets the environment override the customer chrome flags", () => {
88
+ setEnvironmentProxy("socks5://enforced.example.com:1080");
89
+ const proxy = applyOriginProxy([
90
+ "--proxy-server=socks5://customer.example.com:1080",
91
+ ]);
92
+ expect(proxy.host).to.equal("enforced.example.com");
93
+ });
94
+ it("names both proxies when it displaces the customer's region", () => {
95
+ setEnvironmentProxy("socks5://enforced.example.com:1080");
96
+ const logger = recordingLogger();
97
+ applyOriginProxy(["--proxy-server=socks5://ap-northeast-1.proxy.baqend.com:1080"], logger);
98
+ expect(logger.warnings).to.have.length(1);
99
+ expect(logger.warnings[0]).to.contain("enforced.example.com");
100
+ expect(logger.warnings[0]).to.contain("ap-northeast-1.proxy.baqend.com:1080");
101
+ });
102
+ it("reports no displacement when both sources name one endpoint", () => {
103
+ setEnvironmentProxy("socks5h://proxy.example.com:1080");
104
+ const logger = recordingLogger();
105
+ applyOriginProxy(["--proxy-server=socks5://proxy.example.com:1080"], logger);
106
+ expect(logger.warnings[0]).to.not.contain("replaces");
107
+ });
108
+ it("fails loudly on an unusable environment value", () => {
109
+ setEnvironmentProxy("http://proxy.example.com:8080");
110
+ expect(() => applyOriginProxy()).to.throw(PROXY_ENVIRONMENT_VARIABLE);
111
+ });
112
+ it("reports no proxy when neither source sets one", () => {
113
+ const logger = recordingLogger();
114
+ expect(applyOriginProxy([], logger)).to.equal(undefined);
115
+ expect(logger.warnings).to.have.length(0);
116
+ });
117
+ });
118
+ describe("withOriginProxyFlag", () => {
119
+ it("appends the enforced proxy last, so chrome takes it", () => {
120
+ setEnvironmentProxy("socks5://enforced.example.com:1080");
121
+ applyOriginProxy(["--proxy-server=socks5://customer.example.com:1080"]);
122
+ expect(withOriginProxyFlag([
123
+ "--proxy-server=socks5://customer.example.com:1080",
124
+ ])).to.deep.equal([
125
+ "--proxy-server=socks5://customer.example.com:1080",
126
+ "--proxy-server=socks5://enforced.example.com:1080",
127
+ ]);
128
+ });
129
+ it("leaves a proxy that already came from the flags untouched", () => {
130
+ const flags = ["--proxy-server=socks5://customer.example.com:1080"];
131
+ applyOriginProxy(flags);
132
+ expect(withOriginProxyFlag(flags)).to.deep.equal(flags);
133
+ });
134
+ });
135
+ describe("chromeProxyFlag", () => {
136
+ it("round-trips a parsed proxy", () => {
137
+ const value = "socks5://proxy.example.com:1080";
138
+ expect(chromeProxyFlag(parseSocksProxy(value))).to.equal(`--proxy-server=${value}`);
139
+ });
140
+ });
141
+ describe("originFetch", () => {
142
+ let server;
143
+ let url;
144
+ beforeEach(async () => {
145
+ server = http.createServer((_request, response) => response.end("ok"));
146
+ await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve));
147
+ url = `http://127.0.0.1:${server.address().port}/`;
148
+ });
149
+ afterEach(() => server.close());
150
+ it("connects directly when no proxy is set", async () => {
151
+ const response = await originFetch(url);
152
+ expect(await response.text()).to.equal("ok");
153
+ });
154
+ it("connects directly when only legacy tls is tolerated", async () => {
155
+ const response = await originFetch(url, undefined, {
156
+ tolerateLegacyTls: true,
157
+ });
158
+ expect(await response.text()).to.equal("ok");
159
+ });
160
+ it("routes through the installed proxy", async () => {
161
+ // Nothing listens on port 1, so a proxied request must fail to connect.
162
+ setOriginProxy({ type: 5, host: "127.0.0.1", port: 1 });
163
+ const failure = await originFetch(url).then(() => null, (error) => error);
164
+ expect(failure).to.be.instanceOf(Error);
165
+ });
166
+ });
167
+ });
@@ -1,6 +1,6 @@
1
- import fetch from "node-fetch";
1
+ import { originFetch } from "./origin-proxy.js";
2
2
  export default async function scrape(url) {
3
- const response = await fetch(url);
3
+ const response = await originFetch(url);
4
4
  if (response.status !== 200) {
5
5
  return null;
6
6
  }
@@ -1,5 +1,5 @@
1
- import fetch from "node-fetch";
2
1
  import { safe } from "./safe.js";
2
+ import { originFetch } from "./origin-proxy.js";
3
3
  const EMPTY_FLAGS = {
4
4
  useGATracking: false,
5
5
  withGoogleOptimize: false,
@@ -78,7 +78,7 @@ function findUrl(html, host, pattern) {
78
78
  return `https://${host}${path}`;
79
79
  }
80
80
  async function fetchPage(url, signal) {
81
- const result = await safe(fetch(url, { signal: signal }));
81
+ const result = await safe(originFetch(url, { signal }));
82
82
  if (!result.success)
83
83
  return null;
84
84
  const response = result.data;
@@ -1,6 +1,7 @@
1
1
  import { BundleService } from "./bundle-service.js";
2
2
  export declare class BundleServiceFactory {
3
3
  private customerFolder?;
4
- constructor(customerFolder?: string);
4
+ private additionalModulePaths;
5
+ constructor(customerFolder?: string, additionalModulePaths?: string[]);
5
6
  buildService(): BundleService;
6
7
  }
@@ -1,10 +1,12 @@
1
1
  import { BundleService } from "./bundle-service.js";
2
2
  export class BundleServiceFactory {
3
3
  customerFolder;
4
- constructor(customerFolder) {
4
+ additionalModulePaths;
5
+ constructor(customerFolder, additionalModulePaths = []) {
5
6
  this.customerFolder = customerFolder;
7
+ this.additionalModulePaths = additionalModulePaths;
6
8
  }
7
9
  buildService() {
8
- return new BundleService(this.customerFolder);
10
+ return new BundleService(this.customerFolder, this.additionalModulePaths);
9
11
  }
10
12
  }
@@ -1,10 +1,22 @@
1
1
  import { PluginBuild, LogLevel } from "esbuild";
2
2
  export type BundleEntryPoints = Record<string, string>;
3
3
  export type BundleBasePaths = Record<string, string>;
4
+ /**
5
+ * Returns the source of a relative import that could not be resolved on disk, or `null` when the
6
+ * fallback has nothing to offer. Returning `null` lets esbuild report its original resolve error.
7
+ */
8
+ export type BundleResolveFallback = (specifier: string) => Promise<string | null>;
4
9
  export declare class BundleService {
5
10
  private moduleDiscoveryDir;
6
- constructor(moduleDiscoveryDir: string);
7
- bundle({ entryPoints, basePaths, define, target, minify, logLevel, alias, platform, }: {
11
+ private additionalModulePaths;
12
+ /**
13
+ * @param moduleDiscoveryDir the directory bundled code resolves its own imports from
14
+ * @param additionalModulePaths extra `node_modules` directories to search, like `NODE_PATH`.
15
+ * `sk` installs the packages a customer config declares under `dependencies` into its cache
16
+ * rather than into the customer folder, so esbuild would otherwise never find them.
17
+ */
18
+ constructor(moduleDiscoveryDir: string, additionalModulePaths?: string[]);
19
+ bundle({ entryPoints, basePaths, define, target, minify, logLevel, alias, platform, resolveFallback, }: {
8
20
  entryPoints: BundleEntryPoints;
9
21
  basePaths?: BundleBasePaths;
10
22
  target?: string | string[];
@@ -13,6 +25,7 @@ export declare class BundleService {
13
25
  alias?: Record<string, string>;
14
26
  logLevel?: LogLevel;
15
27
  platform?: "browser" | "node" | "neutral";
28
+ resolveFallback?: BundleResolveFallback;
16
29
  }): Promise<string>;
17
30
  /**
18
31
  * Turns each alias target into an absolute path, resolved from the same directory the bundled
@@ -26,9 +39,24 @@ export declare class BundleService {
26
39
  * A target that is not installed stays a bare name, so esbuild reports it as before.
27
40
  */
28
41
  private resolveAliases;
42
+ /**
43
+ * Looks the target up the way the bundled code would: the customer folder first, then the
44
+ * additional module paths. Returns `null` when it is installed nowhere, so the bare name reaches
45
+ * esbuild and is reported as before.
46
+ */
47
+ private resolveAliasTarget;
29
48
  private entryPointsPlugin;
30
49
  baqendPlugin(basePaths: BundleBasePaths): {
31
50
  name: string;
32
51
  setup(build: PluginBuild): void;
33
52
  };
53
+ /**
54
+ * Resolves relative imports that exist nowhere in the customer folder through `resolveFallback`.
55
+ *
56
+ * A document handler shares its source with the Baqend app it is deployed to, so it may
57
+ * `require("./someModule")` a module that only lives in the app. Disk always wins: the fallback
58
+ * is only consulted after esbuild's own resolution has failed, and only for relative specifiers —
59
+ * a bare name is an npm package, and a missing one stays esbuild's error to report.
60
+ */
61
+ private resolveFallbackPlugin;
34
62
  }