n8n-nodes-speedtest-net 0.1.0 → 0.2.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/README.md CHANGED
@@ -1,40 +1,19 @@
1
1
  # n8n-nodes-speedtest-net
2
- A TypeScript n8n community node for internet speed tests using [`speedtest-net`](https://github.com/ddsol/speedtest.net) and the Ookla Speedtest CLI. Adapted from the behavior of the included Node-RED example, which is left unchanged.
3
-
4
- ## Features
5
- - One speed test per input item, executed sequentially to avoid competing for bandwidth.
6
- - Download/upload throughput, ping, jitter, server details, ISP, and the raw Ookla result.
7
- - Convenient `downloadMbps` and `uploadMbps` fields (decimal megabits/second).
8
- - Optional Ookla server ID, source interface IP, and a real cancellation timeout (default 120 seconds).
9
- - n8n item linking, expressions, and **On Error → Continue (using regular output)** support.
10
- - Explicit license/GDPR consent switches, both disabled by default.
11
-
12
- ## Requirements and limitations
13
- - Self-hosted n8n with Node.js 22 or newer. This is not an n8n Cloud-compatible verified node: it runs a local executable and uses a third-party dependency.
14
- - The test measures the connection of the **n8n worker/container**, not your browser. VPNs, routing, and Docker networking affect the result.
15
- - `speedtest-net` downloads an Ookla executable on first use and needs outbound internet access plus a writable cache directory. Platform support is limited to that of `speedtest-net` and the Ookla binary. Alpine/musl containers may need a compatible runtime or a glibc-based worker image.
16
- - Review [Ookla's EULA](https://www.speedtest.net/about/eula), [Terms](https://www.speedtest.net/about/terms), and [Privacy Policy](https://www.speedtest.net/about/privacy). Ookla's CLI license has use restrictions; this package's MIT license does not override them.
17
- - Tests consume bandwidth and may use significant data. Start with a Manual Trigger and avoid short schedule intervals.
18
-
19
- ### Important: upstream native dependency
20
- `speedtest-net@2.2.0` imports `decompress-tarxz`, which depends on the old `lzma-native@4`. That native dependency can fail on modern Node.js even on platforms where the XZ extractor is not used.
21
-
22
- This repository uses a root npm override to select `lzma-native@8`. **npm overrides do not propagate from a dependency to its consumer.** When installing this package into a separate community-node project, add the following override to that project's root `package.json` before installation:
23
- ```json
24
- {
25
- "overrides": {
26
- "decompress-tarxz": {
27
- "lzma-native": "^8.0.6"
28
- }
29
- }
30
- }
31
- ```
2
+ TypeScript n8n community node with its own wrapper around the **official Ookla Speedtest CLI**. The npm `speedtest-net` dependency has been removed: this package launches the installed CLI directly, captures diagnostics, and produces structured errors.
32
3
 
33
- With npm 12, also allow the reviewed `lzma-native` install script in that project's `allowScripts` configuration. Do not blindly enable every dependency script. A one-click community-node install without this root configuration is **not verified** on modern Node.js.
4
+ ## Requirements
5
+ - Self-hosted n8n and Node.js 22 or newer. Not a verified n8n Cloud node.
6
+ - Install the official Ookla CLI on every worker that executes this node: https://www.speedtest.net/apps/cli
7
+ - The executable must be available as `speedtest` on PATH. Administrators can instead set `N8N_SPEEDTEST_BINARY` to its absolute path. Restart n8n after changing the environment.
8
+ - Do not install the unrelated Python `speedtest-cli`: it has different flags and output.
9
+ - No automatic executable downloads or npm native-dependency overrides are required anymore.
10
+ - For containers, bake the official CLI into a compatible image. A host installation is not automatically accessible inside the container. Check Ookla platform/runtime compatibility, particularly Alpine/musl.
34
11
 
35
- `speedtest-net` also brings legacy transitive dependencies with known npm audit findings. This implementation does not claim to resolve those upstream issues. Review them before deployment, especially for shared or production instances.
12
+ Tests measure the **n8n worker/container connection**, not your browser. They consume bandwidth and may use significant data. Tests are sequential within one node execution, but separate workflows/workers can still overlap. One input item causes one test.
36
13
 
37
- ## Build and test
14
+ Review [Ookla's EULA](https://www.speedtest.net/about/eula), [Terms](https://www.speedtest.net/about/terms), and [Privacy Policy](https://www.speedtest.net/about/privacy). Both consent switches are disabled by default and must be explicitly enabled. Ookla's licensing restrictions apply independently of this package's MIT license.
15
+
16
+ ## Build and install
38
17
  ```bash
39
18
  npm ci
40
19
  npm test
@@ -43,29 +22,59 @@ npm run build
43
22
  npm pack
44
23
  ```
45
24
 
46
- The generated package is `n8n-nodes-speedtest-net-0.1.0.tgz`. The Node-RED reference, tests, and development files are not shipped in it.
47
-
48
- ## Local installation into self-hosted n8n
49
- Build and pack the project first. Copy the tarball to the n8n host/container. In n8n's community-package directory (typically `~/.n8n/nodes`), create a root package.json if needed, apply the native-dependency override above, and install the tarball with npm. Run npm as the same user that owns n8n's files, then restart n8n. In queue mode, install the package on every worker as well as the main instance.
25
+ Install the generated `n8n-nodes-speedtest-net-0.2.1.tgz` into your self-hosted n8n community-package directory (typically `~/.n8n/nodes`) with npm, using the n8n file owner's account, then restart n8n. Install on all workers in queue mode. The official CLI must be installed separately in each execution environment.
50
26
 
51
- Alternatively, for development, expose the built `dist/nodes` directory through `N8N_CUSTOM_EXTENSIONS`, and make this project's `node_modules` available alongside it. Restart n8n after rebuilding. Installation, discovery, and a real speed test still need to be verified in your actual n8n environment.
27
+ For local development, point `N8N_CUSTOM_EXTENSIONS` at the built `dist/nodes` directory and make the project's dependencies available alongside it. Real n8n discovery and real Ookla requests still require verification in your environment.
52
28
 
53
29
  ## Usage
54
- 1. Add **Manual Trigger → Speedtest**.
55
- 2. Review Ookla's terms and explicitly enable both acceptance switches if you agree.
56
- 3. Optionally choose **Server ID**, **Source IP**, or **Timeout (Seconds)** under Options.
57
- 4. Execute the workflow.
58
- 5. Use `{{ $json.downloadMbps }}`, `{{ $json.uploadMbps }}`, or `{{ $json.ping.latency }}` downstream.
30
+ 1. Connect **Manual Trigger → Speedtest**.
31
+ 2. Review and enable both Ookla consent switches if you agree.
32
+ 3. Optionally choose **Server ID**, **Source IP**, or **Timeout (Seconds)** (default 120).
33
+ 4. Execute and use `{{ $json.downloadMbps }}`, `{{ $json.uploadMbps }}`, or `{{ $json.ping.latency }}`.
34
+
35
+ Success preserves the CLI JSON result and adds decimal `downloadMbps` and `uploadMbps`. Original bandwidth fields remain in bytes/second. All output items are linked to their input items.
36
+
37
+ ## Structured errors
38
+ Failures use native n8n error handling. There is no separate error-data switch:
39
+ - **Stop Workflow** (default): throws a node error. Structured wrapper diagnostics are preserved in the error's `context.speedtest`.
40
+ - **Continue (using error output)**: failed items go to n8n's **Error** output with the structured JSON below. Successful items stay on the **Success** output.
41
+ - **Continue (using regular output)**: explicitly keeps failed items on the regular output with the same structured JSON.
42
+
43
+ Configure this under **Settings → On Error**. Each continued failure carries n8n's top-level error marker and retains its input-item link. Existing saved `returnErrors` parameters are ignored.
44
+
45
+ Example for the reported Ookla rate limit:
46
+
47
+ ```json
48
+ {
49
+ "success": false,
50
+ "error": {
51
+ "code": "RATE_LIMITED",
52
+ "message": "Limit reached: Too many requests received.",
53
+ "rateLimited": true,
54
+ "retryAfterSeconds": null,
55
+ "retryAt": null,
56
+ "retrySource": null,
57
+ "stdout": "",
58
+ "stderr": "[2026-10-30 14:30:58.565] [error] Limit reached: Too many requests received.",
59
+ "exitCode": 1,
60
+ "signal": null
61
+ }
62
+ }
63
+ ```
64
+
65
+ ### What we can and cannot know
66
+ The wrapper preserves raw stdout/stderr, exit code, and termination signal when available. It can classify rate limits and recognize an explicit `retry after N seconds` or `retry in N seconds` message if the CLI emits one. This is parsed CLI text, not an HTTP header or a documented promise from Ookla.
67
+
68
+ **A direct CLI wrapper cannot expose hidden HTTP response headers.** If Ookla does not print a retry duration, it remains `null`. `retryAt` is currently always `null`; a log timestamp is never treated as a quota reset. No fabricated cooldown, automatic retries, or rate-limit bypass is implemented. A rate-limit/licensing error can require more than simply waiting.
59
69
 
60
- Output is the raw speedtest result plus the two Mbps fields. Timestamps are JSON strings. Original bandwidth values in `download.bandwidth` and `upload.bandwidth` remain in **bytes per second**. On continued failure, output is `{ "error": "..." }`, linked to its input item.
70
+ Error codes include `RATE_LIMITED`, `TIMEOUT`, `BINARY_NOT_FOUND`, `SPAWN_ERROR`, `CLI_ERROR`, `INVALID_RESULT`, `OUTPUT_LIMIT`, and `INVALID_CONFIGURATION`.
61
71
 
62
- The Node-RED example's `maxTime` setting is not a supported `speedtest-net@2.2.0` option. This node instead implements its own timeout using the library's cancellation API. Timeout covers first-use setup too; the wrapper may finish an already-started download before observing cancellation.
72
+ Timeout and output-limit failures kill the direct child and wait for process termination before returning. Captured stdout/stderr is limited to 1 MiB total. Diagnostics can contain network addresses or other sensitive information: restrict access to execution data and avoid publishing raw logs.
63
73
 
64
- ## Project layout
65
- - `nodes/Speedtest/Speedtest.node.ts`: n8n UI and execution integration.
66
- - `nodes/Speedtest/runSpeedtest.ts`: wrapper, timeout, and output conversion.
67
- - `tests/`: isolated unit tests; no Ookla connections or bandwidth tests.
68
- - `node-red-contrib-speedtest/`: original reference project, unchanged.
74
+ ## Tests and layout
75
+ - `nodes/Speedtest/Speedtest.node.ts`: n8n UI and per-item integration.
76
+ - `nodes/Speedtest/runSpeedtest.ts`: shell-free CLI execution, timeout, output bounds, and result conversion.
77
+ - `nodes/Speedtest/speedtestError.ts`: structured diagnostics and rate-limit classification.
78
+ - `tests/`: mocked process tests, n8n integration unit tests, and local fixture-process tests; no real Ookla requests.
69
79
 
70
- ## License
71
- MIT. See `LICENSE`. The original Node-RED example retains its own license and attribution.
80
+ Tests do not prove deployment compatibility or verify a real Ookla quota reset. The package is MIT licensed; see `LICENSE`.
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.Speedtest = void 0;
4
4
  const n8n_workflow_1 = require("n8n-workflow");
5
5
  const runSpeedtest_1 = require("./runSpeedtest");
6
+ const speedtestError_1 = require("./speedtestError");
6
7
  class Speedtest {
7
8
  description = {
8
9
  displayName: 'Speedtest',
@@ -96,11 +97,21 @@ class Speedtest {
96
97
  }
97
98
  catch (error) {
98
99
  const cause = error instanceof Error ? error : new Error(String(error));
100
+ const diagnostics = (0, speedtestError_1.structuredError)(cause);
101
+ const nodeError = new n8n_workflow_1.NodeOperationError(this.getNode(), cause, {
102
+ itemIndex,
103
+ });
104
+ nodeError.context.speedtest = diagnostics;
99
105
  if (this.continueOnFail()) {
100
- output.push({ json: { error: cause.message }, pairedItem: { item: itemIndex } });
106
+ // n8n needs the top-level error marker to route this item to its error output.
107
+ output.push({
108
+ json: { success: false, error: diagnostics },
109
+ error: nodeError,
110
+ pairedItem: { item: itemIndex },
111
+ });
101
112
  continue;
102
113
  }
103
- throw new n8n_workflow_1.NodeOperationError(this.getNode(), cause, { itemIndex });
114
+ throw nodeError;
104
115
  }
105
116
  }
106
117
  return [output];
@@ -1 +1 @@
1
- {"version":3,"file":"Speedtest.node.js","sourceRoot":"","sources":["../../../nodes/Speedtest/Speedtest.node.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"Speedtest.node.js","sourceRoot":"","sources":["../../../nodes/Speedtest/Speedtest.node.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ export declare function extractBinary(archive: Buffer): Buffer;
2
+ export declare function resolveBinary(): string | Promise<string>;
@@ -0,0 +1,116 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.extractBinary = extractBinary;
4
+ exports.resolveBinary = resolveBinary;
5
+ const node_crypto_1 = require("node:crypto");
6
+ const promises_1 = require("node:fs/promises");
7
+ const node_fs_1 = require("node:fs");
8
+ const node_path_1 = require("node:path");
9
+ const node_zlib_1 = require("node:zlib");
10
+ const speedtestError_1 = require("./speedtestError");
11
+ const VERSION = '1.2.0';
12
+ const RELEASES = {
13
+ 'linux-x64': { archive: 'linux-x86_64', sha256: '5690596c54ff9bed63fa3732f818a05dbc2db19ad36ed68f21ca5f64d5cfeeb7' },
14
+ 'linux-arm64': { archive: 'linux-aarch64', sha256: '3953d231da3783e2bf8904b6dd72767c5c6e533e163d3742fd0437affa431bd3' },
15
+ };
16
+ const MAX_ARCHIVE_BYTES = 16 * 1024 * 1024;
17
+ const pending = new Map();
18
+ // Extract only the known regular-file entry. Never trust archive paths or extract links.
19
+ function extractBinary(archive) {
20
+ const tar = (0, node_zlib_1.gunzipSync)(archive, { maxOutputLength: 32 * 1024 * 1024 });
21
+ for (let offset = 0; offset + 512 <= tar.length;) {
22
+ const header = tar.subarray(offset, offset + 512);
23
+ if (header.every(byte => byte === 0))
24
+ break;
25
+ const name = header.subarray(0, 100).toString().replace(/\0.*$/, '');
26
+ const sizeText = header.subarray(124, 136).toString().replace(/\0.*$/, '').trim();
27
+ if (!/^[0-7]+$/.test(sizeText))
28
+ throw new Error('Invalid tar entry size');
29
+ const size = parseInt(sizeText, 8);
30
+ const start = offset + 512;
31
+ if (start + size > tar.length)
32
+ throw new Error('Truncated tar entry');
33
+ if (name === 'speedtest' && (header[156] === 0 || header[156] === 48) && size > 0) {
34
+ return tar.subarray(start, start + size);
35
+ }
36
+ offset = start + Math.ceil(size / 512) * 512;
37
+ }
38
+ throw new Error('Archive contains no regular speedtest binary');
39
+ }
40
+ async function provision(key, directory) {
41
+ const release = RELEASES[key];
42
+ if (!release)
43
+ throw new speedtestError_1.SpeedtestError(`Automatic Ookla CLI provisioning is not supported on ${key}; set N8N_SPEEDTEST_BINARY`, 'UNSUPPORTED_PLATFORM');
44
+ const target = (0, node_path_1.join)(directory, `speedtest-${VERSION}-${key}`);
45
+ const archivePath = target + '.tgz';
46
+ try {
47
+ await (0, promises_1.mkdir)(directory, { recursive: true });
48
+ // Verify the cached archive on every resolution and regenerate a missing/modified executable.
49
+ let archive;
50
+ try {
51
+ archive = await (0, promises_1.readFile)(archivePath);
52
+ }
53
+ catch (error) {
54
+ if (error.code !== 'ENOENT')
55
+ throw error;
56
+ }
57
+ if (!archive) {
58
+ const response = await fetch(`https://install.speedtest.net/app/cli/ookla-speedtest-${VERSION}-${release.archive}.tgz`, {
59
+ signal: AbortSignal.timeout(60_000), redirect: 'error',
60
+ });
61
+ if (!response.ok || !response.body)
62
+ throw new Error(`Ookla download failed: HTTP ${response.status}`);
63
+ const chunks = [];
64
+ let size = 0;
65
+ for await (const chunk of response.body) {
66
+ size += chunk.length;
67
+ if (size > MAX_ARCHIVE_BYTES)
68
+ throw new Error('Ookla download exceeds 16 MiB');
69
+ chunks.push(Buffer.from(chunk));
70
+ }
71
+ archive = Buffer.concat(chunks);
72
+ }
73
+ if (archive.length > MAX_ARCHIVE_BYTES || (0, node_crypto_1.createHash)('sha256').update(archive).digest('hex') !== release.sha256) {
74
+ throw new Error('Ookla archive SHA-256 verification failed');
75
+ }
76
+ const binary = extractBinary(archive);
77
+ try {
78
+ const existing = await (0, promises_1.readFile)(target);
79
+ await (0, promises_1.access)(target, node_fs_1.constants.X_OK);
80
+ if (existing.equals(binary))
81
+ return target;
82
+ }
83
+ catch { /* Install atomically below. */ }
84
+ const staging = await (0, promises_1.mkdtemp)((0, node_path_1.join)(directory, 'install-'));
85
+ try {
86
+ await (0, promises_1.writeFile)((0, node_path_1.join)(staging, 'archive'), archive);
87
+ await (0, promises_1.writeFile)((0, node_path_1.join)(staging, 'binary'), binary, { mode: 0o755 });
88
+ await (0, promises_1.chmod)((0, node_path_1.join)(staging, 'binary'), 0o755);
89
+ await (0, promises_1.rename)((0, node_path_1.join)(staging, 'archive'), archivePath);
90
+ await (0, promises_1.rename)((0, node_path_1.join)(staging, 'binary'), target);
91
+ }
92
+ finally {
93
+ await (0, promises_1.rm)(staging, { recursive: true, force: true });
94
+ }
95
+ return target;
96
+ }
97
+ catch (error) {
98
+ throw new speedtestError_1.SpeedtestError(`Cannot provision Ookla CLI: ${error.message}`, 'BINARY_SETUP_ERROR');
99
+ }
100
+ }
101
+ function resolveBinary() {
102
+ // Overrides belong to the administrator, never to workflow input.
103
+ if (process.env.N8N_SPEEDTEST_BINARY)
104
+ return process.env.N8N_SPEEDTEST_BINARY;
105
+ const key = `${process.platform}-${process.arch}`;
106
+ // Visible cache alongside the installed node, as in the original package. No root or install scripts.
107
+ const directory = (0, node_path_1.resolve)(process.env.N8N_SPEEDTEST_CACHE_DIR || (0, node_path_1.join)(__dirname, 'binaries'));
108
+ const cacheKey = `${directory}:${key}`;
109
+ let result = pending.get(cacheKey);
110
+ if (!result) {
111
+ result = provision(key, directory).finally(() => { pending.delete(cacheKey); });
112
+ pending.set(cacheKey, result);
113
+ }
114
+ return result;
115
+ }
116
+ //# sourceMappingURL=resolveBinary.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"resolveBinary.js","sourceRoot":"","sources":["../../../nodes/Speedtest/resolveBinary.ts"],"names":[],"mappings":"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"}
@@ -1,36 +1,93 @@
1
1
  "use strict";
2
- var __importDefault = (this && this.__importDefault) || function (mod) {
3
- return (mod && mod.__esModule) ? mod : { "default": mod };
4
- };
5
2
  Object.defineProperty(exports, "__esModule", { value: true });
6
3
  exports.runSpeedtest = runSpeedtest;
7
- const speedtest_net_1 = __importDefault(require("speedtest-net"));
4
+ const node_child_process_1 = require("node:child_process");
5
+ const node_net_1 = require("node:net");
6
+ const speedtestError_1 = require("./speedtestError");
7
+ // Bound process output so a faulty executable cannot exhaust the worker's memory.
8
+ const MAX_OUTPUT_BYTES = 1024 * 1024;
8
9
  async function runSpeedtest(options, timeoutSeconds) {
9
- const cancel = speedtest_net_1.default.makeCancel();
10
- let timer;
11
- try {
12
- const result = await Promise.race([
13
- (0, speedtest_net_1.default)({ ...options, cancel }),
14
- new Promise((_, reject) => {
15
- timer = setTimeout(() => {
16
- reject(new Error('Speed test timed out after ' + timeoutSeconds + ' seconds'));
17
- cancel();
18
- }, timeoutSeconds * 1000);
19
- }),
20
- ]);
21
- if (!result || !Number.isFinite(result.download?.bandwidth) || !Number.isFinite(result.upload?.bandwidth)) {
22
- throw new Error('Speedtest returned no valid download/upload result');
23
- }
24
- // Ookla reports bandwidth in bytes/second; Mbps uses decimal megabits.
25
- return JSON.parse(JSON.stringify({
26
- ...result,
27
- downloadMbps: result.download.bandwidth * 8 / 1_000_000,
28
- uploadMbps: result.upload.bandwidth * 8 / 1_000_000,
29
- }));
10
+ if (!options.acceptLicense || !options.acceptGdpr) {
11
+ throw new speedtestError_1.SpeedtestError('Both Ookla consent options are required', 'INVALID_CONFIGURATION');
30
12
  }
31
- finally {
32
- if (timer !== undefined)
33
- clearTimeout(timer);
13
+ if (!Number.isInteger(timeoutSeconds) || timeoutSeconds < 1 || timeoutSeconds > 3600) {
14
+ throw new speedtestError_1.SpeedtestError('Timeout must be between 1 and 3600 seconds', 'INVALID_CONFIGURATION');
15
+ }
16
+ if (options.serverId && !/^\d+$/.test(options.serverId)) {
17
+ throw new speedtestError_1.SpeedtestError('Server ID must contain only digits', 'INVALID_CONFIGURATION');
18
+ }
19
+ if (options.sourceIp && !(0, node_net_1.isIP)(options.sourceIp)) {
20
+ throw new speedtestError_1.SpeedtestError('Source IP must be a valid IP address', 'INVALID_CONFIGURATION');
34
21
  }
22
+ const args = ['--format=json', '--progress=no', '--accept-license', '--accept-gdpr'];
23
+ if (options.serverId)
24
+ args.push('--server-id=' + options.serverId);
25
+ if (options.sourceIp)
26
+ args.push('--ip=' + options.sourceIp);
27
+ return new Promise((resolve, reject) => {
28
+ // Deliberately not configurable by workflow input: executable selection belongs to the administrator.
29
+ const child = (0, node_child_process_1.spawn)(process.env.N8N_SPEEDTEST_BINARY || 'speedtest', args, {
30
+ shell: false, windowsHide: true, stdio: ['ignore', 'pipe', 'pipe'],
31
+ });
32
+ let stdout = '';
33
+ let stderr = '';
34
+ let totalBytes = 0;
35
+ let failure;
36
+ const stop = (message, code) => {
37
+ if (failure)
38
+ return;
39
+ failure = { message, code };
40
+ // Ookla is the direct child; no shell or intermediary process is involved.
41
+ child.kill('SIGKILL');
42
+ };
43
+ const timer = setTimeout(() => stop(`Speed test timed out after ${timeoutSeconds} seconds`, 'TIMEOUT'), timeoutSeconds * 1000);
44
+ const collect = (stream, chunk) => {
45
+ const remaining = Math.max(0, MAX_OUTPUT_BYTES - totalBytes);
46
+ totalBytes += chunk.length;
47
+ const text = chunk.subarray(0, remaining).toString('utf8');
48
+ if (stream === 'stdout')
49
+ stdout += text;
50
+ else
51
+ stderr += text;
52
+ if (totalBytes > MAX_OUTPUT_BYTES)
53
+ stop('Speedtest output exceeded 1 MiB', 'OUTPUT_LIMIT');
54
+ };
55
+ child.stdout.on('data', (chunk) => collect('stdout', chunk));
56
+ child.stderr.on('data', (chunk) => collect('stderr', chunk));
57
+ child.on('error', (error) => {
58
+ clearTimeout(timer);
59
+ reject(new speedtestError_1.SpeedtestError(error.message, error.code === 'ENOENT' ? 'BINARY_NOT_FOUND' : 'SPAWN_ERROR', { stdout, stderr }));
60
+ });
61
+ child.on('close', (exitCode, signal) => {
62
+ clearTimeout(timer);
63
+ const diagnostics = { stdout, stderr, exitCode, signal };
64
+ if (failure || exitCode !== 0) {
65
+ reject(new speedtestError_1.SpeedtestError(failure?.message || stderr.trim() || `Speedtest exited with code ${exitCode}`, failure?.code || 'CLI_ERROR', diagnostics));
66
+ return;
67
+ }
68
+ try {
69
+ // Accept a single JSON result or newline-delimited CLI diagnostics followed by a result.
70
+ let result;
71
+ for (const line of stdout.split(/\r?\n/)) {
72
+ try {
73
+ const value = JSON.parse(line);
74
+ if (value && typeof value === 'object' && value.type === 'result')
75
+ result = value;
76
+ }
77
+ catch { /* Non-JSON diagnostics are retained in errors. */ }
78
+ }
79
+ const download = result?.download;
80
+ const upload = result?.upload;
81
+ if (typeof download?.bandwidth !== 'number' || !Number.isFinite(download.bandwidth) || download.bandwidth < 0 ||
82
+ typeof upload?.bandwidth !== 'number' || !Number.isFinite(upload.bandwidth) || upload.bandwidth < 0) {
83
+ throw new Error('Speedtest returned no valid download/upload result');
84
+ }
85
+ resolve({ ...result, downloadMbps: download.bandwidth * 8 / 1_000_000, uploadMbps: upload.bandwidth * 8 / 1_000_000 });
86
+ }
87
+ catch (error) {
88
+ reject(new speedtestError_1.SpeedtestError(error.message, 'INVALID_RESULT', diagnostics));
89
+ }
90
+ });
91
+ });
35
92
  }
36
93
  //# sourceMappingURL=runSpeedtest.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"runSpeedtest.js","sourceRoot":"","sources":["../../../nodes/Speedtest/runSpeedtest.ts"],"names":[],"mappings":";;;;;AAUA,oCAyBC;AAlCD,kEAAsC;AAS/B,KAAK,UAAU,YAAY,CAAC,OAAyB,EAAE,cAAsB;IACnF,MAAM,MAAM,GAAG,uBAAS,CAAC,UAAU,EAAE,CAAC;IACtC,IAAI,KAAgD,CAAC;IACrD,IAAI,CAAC;QACJ,MAAM,MAAM,GAAG,MAAM,OAAO,CAAC,IAAI,CAAC;YACjC,IAAA,uBAAS,EAAC,EAAE,GAAG,OAAO,EAAE,MAAM,EAAE,CAAC;YACjC,IAAI,OAAO,CAAQ,CAAC,CAAC,EAAE,MAAM,EAAE,EAAE;gBAChC,KAAK,GAAG,UAAU,CAAC,GAAG,EAAE;oBACvB,MAAM,CAAC,IAAI,KAAK,CAAC,6BAA6B,GAAG,cAAc,GAAG,UAAU,CAAC,CAAC,CAAC;oBAC/E,MAAM,EAAE,CAAC;gBACV,CAAC,EAAE,cAAc,GAAG,IAAI,CAAC,CAAC;YAC3B,CAAC,CAAC;SACF,CAAC,CAAC;QACH,IAAI,CAAC,MAAM,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,MAAM,CAAC,QAAQ,EAAE,SAAS,CAAC,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,SAAS,CAAC,EAAE,CAAC;YAC3G,MAAM,IAAI,KAAK,CAAC,oDAAoD,CAAC,CAAC;QACvE,CAAC;QACD,uEAAuE;QACvE,OAAO,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,SAAS,CAAC;YAChC,GAAG,MAAM;YACT,YAAY,EAAE,MAAM,CAAC,QAAQ,CAAC,SAAS,GAAG,CAAC,GAAG,SAAS;YACvD,UAAU,EAAE,MAAM,CAAC,MAAM,CAAC,SAAS,GAAG,CAAC,GAAG,SAAS;SACnD,CAAE,CAAgB,CAAC;IACrB,CAAC;YAAS,CAAC;QACV,IAAI,KAAK,KAAK,SAAS;YAAE,YAAY,CAAC,KAAK,CAAC,CAAC;IAC9C,CAAC;AACF,CAAC"}
1
+ {"version":3,"file":"runSpeedtest.js","sourceRoot":"","sources":["../../../nodes/Speedtest/runSpeedtest.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,14 @@
1
+ import type { IDataObject } from 'n8n-workflow';
2
+ export interface CliDiagnostics {
3
+ stdout?: string;
4
+ stderr?: string;
5
+ exitCode?: number | null;
6
+ signal?: string | null;
7
+ }
8
+ export declare class SpeedtestError extends Error {
9
+ readonly code: string;
10
+ readonly diagnostics: CliDiagnostics;
11
+ constructor(message: string, code: string, diagnostics?: CliDiagnostics);
12
+ toJSON(): IDataObject;
13
+ }
14
+ export declare function structuredError(error: Error): IDataObject;
@@ -0,0 +1,38 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.SpeedtestError = void 0;
4
+ exports.structuredError = structuredError;
5
+ class SpeedtestError extends Error {
6
+ code;
7
+ diagnostics;
8
+ constructor(message, code, diagnostics = {}) {
9
+ super(message);
10
+ this.code = code;
11
+ this.diagnostics = diagnostics;
12
+ this.name = 'SpeedtestError';
13
+ }
14
+ toJSON() {
15
+ const raw = [this.message, this.diagnostics.stderr, this.diagnostics.stdout].filter(Boolean).join('\n');
16
+ const rateLimited = /limit reached|too many requests|rate.?limit/i.test(raw);
17
+ // Only accept an explicit CLI retry duration, never infer a reset from a log timestamp.
18
+ const retry = raw.match(/retry\s+(?:after|in)\s+(\d+)\s*seconds?\b/i);
19
+ const seconds = rateLimited && retry ? Number(retry[1]) : null;
20
+ const retryAfterSeconds = seconds !== null && Number.isSafeInteger(seconds) ? seconds : null;
21
+ return {
22
+ code: rateLimited ? 'RATE_LIMITED' : this.code,
23
+ message: this.message,
24
+ rateLimited,
25
+ retryAfterSeconds,
26
+ retryAt: null,
27
+ retrySource: retryAfterSeconds === null ? null : 'cli_message',
28
+ ...this.diagnostics,
29
+ };
30
+ }
31
+ }
32
+ exports.SpeedtestError = SpeedtestError;
33
+ function structuredError(error) {
34
+ return error instanceof SpeedtestError
35
+ ? error.toJSON()
36
+ : new SpeedtestError(error.message, 'INVALID_CONFIGURATION').toJSON();
37
+ }
38
+ //# sourceMappingURL=speedtestError.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"speedtestError.js","sourceRoot":"","sources":["../../../nodes/Speedtest/speedtestError.ts"],"names":[],"mappings":";;;;AASA,oBAA4B,SAAQ,KAAK;IAGvB,IAAI;IACJ,WAAW;IAH5B,YACC,OAAe,EACC,IAAY,EACZ,WAAW,GAAmB,EAAE;QAEhD,KAAK,CAAC,OAAO,CAAC,CAAC;oBAHC,IAAI;2BACJ,WAAW;QAG3B,IAAI,CAAC,IAAI,GAAG,gBAAgB,CAAC;IAC9B,CAAC;IAED,MAAM;QACL,MAAM,GAAG,GAAG,CAAC,IAAI,CAAC,OAAO,EAAE,IAAI,CAAC,WAAW,CAAC,MAAM,EAAE,IAAI,CAAC,WAAW,CAAC,MAAM,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QACxG,MAAM,WAAW,GAAG,8CAA8C,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;QAC7E,wFAAwF;QACxF,MAAM,KAAK,GAAG,GAAG,CAAC,KAAK,CAAC,4CAA4C,CAAC,CAAC;QACtE,MAAM,OAAO,GAAG,WAAW,IAAI,KAAK,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC;QAC/D,MAAM,iBAAiB,GAAG,OAAO,KAAK,IAAI,IAAI,MAAM,CAAC,aAAa,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,IAAI,CAAC;QAC7F,OAAO;YACN,IAAI,EAAE,WAAW,CAAC,CAAC,CAAC,cAAc,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI;YAC9C,OAAO,EAAE,IAAI,CAAC,OAAO;YACrB,WAAW;YACX,iBAAiB;YACjB,OAAO,EAAE,IAAI;YACb,WAAW,EAAE,iBAAiB,KAAK,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,aAAa;YAC9D,GAAG,IAAI,CAAC,WAAW;SACnB,CAAC;IACH,CAAC;CACD;;AAED,yBAAgC,KAAY;IAC3C,OAAO,KAAK,YAAY,cAAc;QACrC,CAAC,CAAC,KAAK,CAAC,MAAM,EAAE;QAChB,CAAC,CAAC,IAAI,cAAc,CAAC,KAAK,CAAC,OAAO,EAAE,uBAAuB,CAAC,CAAC,MAAM,EAAE,CAAC;AACxE,CAAC"}
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "n8n-nodes-speedtest-net",
3
- "version": "0.1.0",
4
- "description": "Run Ookla internet speed tests in n8n using speedtest-net",
3
+ "version": "0.2.1",
4
+ "description": "Run Ookla internet speed tests in n8n with structured CLI diagnostics",
5
5
  "license": "MIT",
6
6
  "keywords": [
7
7
  "n8n-community-node-package",
@@ -27,26 +27,16 @@
27
27
  "dist/nodes/Speedtest/Speedtest.node.js"
28
28
  ]
29
29
  },
30
- "dependencies": {
31
- "speedtest-net": "^2.2.0"
32
- },
33
- "overrides": {
34
- "decompress-tarxz": {
35
- "lzma-native": "^8.0.6"
36
- }
37
- },
38
30
  "allowScripts": {
39
- "lzma-native": true,
40
31
  "esbuild": true
41
32
  },
42
33
  "peerDependencies": {
43
34
  "n8n-workflow": "*"
44
35
  },
45
36
  "devDependencies": {
46
- "n8n-workflow": "^2.16.0",
47
- "@types/node": "^22.0.0",
48
- "@types/speedtest-net": "^2.1.5",
49
- "typescript": "~5.9.3",
50
- "vitest": "^3.2.7"
37
+ "@types/node": "^26.6.6",
38
+ "n8n-workflow": "stable",
39
+ "typescript": "^7.0.2",
40
+ "vitest": "^5.0.5"
51
41
  }
52
42
  }