n8n-nodes-speedtest-net 0.0.0-stage → 0.2.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.
- package/LICENSE +21 -0
- package/README.md +74 -2
- package/dist/nodes/Speedtest/Speedtest.node.d.ts +5 -0
- package/dist/nodes/Speedtest/Speedtest.node.js +118 -0
- package/dist/nodes/Speedtest/Speedtest.node.js.map +1 -0
- package/dist/nodes/Speedtest/runSpeedtest.d.ts +8 -0
- package/dist/nodes/Speedtest/runSpeedtest.js +93 -0
- package/dist/nodes/Speedtest/runSpeedtest.js.map +1 -0
- package/dist/nodes/Speedtest/speedtest.svg +1 -0
- package/dist/nodes/Speedtest/speedtestError.d.ts +14 -0
- package/dist/nodes/Speedtest/speedtestError.js +38 -0
- package/dist/nodes/Speedtest/speedtestError.js.map +1 -0
- package/package.json +40 -4
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Daniel Dolezal
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,75 @@
|
|
|
1
|
-
#
|
|
1
|
+
# n8n-nodes-speedtest-net
|
|
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.
|
|
2
3
|
|
|
3
|
-
|
|
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.
|
|
11
|
+
|
|
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.
|
|
13
|
+
|
|
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
|
|
17
|
+
```bash
|
|
18
|
+
npm ci
|
|
19
|
+
npm test
|
|
20
|
+
npm run typecheck
|
|
21
|
+
npm run build
|
|
22
|
+
npm pack
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
Install the generated `n8n-nodes-speedtest-net-0.2.0.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.
|
|
26
|
+
|
|
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.
|
|
28
|
+
|
|
29
|
+
## Usage
|
|
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 (default)
|
|
38
|
+
**Return Errors as Data is enabled by default.** Failures return JSON on the regular output instead of failing the node. Route errors with an IF node using `{{ $json.success === false }}`. Disable the switch to use normal n8n failure handling; n8n's continue-on-fail setting also produces structured JSON.
|
|
39
|
+
|
|
40
|
+
Example for the reported Ookla rate limit:
|
|
41
|
+
|
|
42
|
+
```json
|
|
43
|
+
{
|
|
44
|
+
"success": false,
|
|
45
|
+
"error": {
|
|
46
|
+
"code": "RATE_LIMITED",
|
|
47
|
+
"message": "Limit reached: Too many requests received.",
|
|
48
|
+
"rateLimited": true,
|
|
49
|
+
"retryAfterSeconds": null,
|
|
50
|
+
"retryAt": null,
|
|
51
|
+
"retrySource": null,
|
|
52
|
+
"stdout": "",
|
|
53
|
+
"stderr": "[2026-10-30 14:30:58.565] [error] Limit reached: Too many requests received.",
|
|
54
|
+
"exitCode": 1,
|
|
55
|
+
"signal": null
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
### What we can and cannot know
|
|
61
|
+
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.
|
|
62
|
+
|
|
63
|
+
**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.
|
|
64
|
+
|
|
65
|
+
Error codes include `RATE_LIMITED`, `TIMEOUT`, `BINARY_NOT_FOUND`, `SPAWN_ERROR`, `CLI_ERROR`, `INVALID_RESULT`, `OUTPUT_LIMIT`, and `INVALID_CONFIGURATION`.
|
|
66
|
+
|
|
67
|
+
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.
|
|
68
|
+
|
|
69
|
+
## Tests and layout
|
|
70
|
+
- `nodes/Speedtest/Speedtest.node.ts`: n8n UI and per-item integration.
|
|
71
|
+
- `nodes/Speedtest/runSpeedtest.ts`: shell-free CLI execution, timeout, output bounds, and result conversion.
|
|
72
|
+
- `nodes/Speedtest/speedtestError.ts`: structured diagnostics and rate-limit classification.
|
|
73
|
+
- `tests/`: mocked process tests, n8n integration unit tests, and local fixture-process tests; no real Ookla requests.
|
|
74
|
+
|
|
75
|
+
Tests do not prove deployment compatibility or verify a real Ookla quota reset. The package is MIT licensed; see `LICENSE`.
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { type IExecuteFunctions, type INodeExecutionData, type INodeType, type INodeTypeDescription } from 'n8n-workflow';
|
|
2
|
+
export declare class Speedtest implements INodeType {
|
|
3
|
+
description: INodeTypeDescription;
|
|
4
|
+
execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]>;
|
|
5
|
+
}
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.Speedtest = void 0;
|
|
4
|
+
const n8n_workflow_1 = require("n8n-workflow");
|
|
5
|
+
const runSpeedtest_1 = require("./runSpeedtest");
|
|
6
|
+
const speedtestError_1 = require("./speedtestError");
|
|
7
|
+
class Speedtest {
|
|
8
|
+
description = {
|
|
9
|
+
displayName: 'Speedtest',
|
|
10
|
+
name: 'speedtest',
|
|
11
|
+
icon: 'file:speedtest.svg',
|
|
12
|
+
group: ['transform'],
|
|
13
|
+
version: 1,
|
|
14
|
+
description: 'Measure internet download, upload, and latency using Ookla Speedtest',
|
|
15
|
+
defaults: { name: 'Speedtest' },
|
|
16
|
+
inputs: ['main'],
|
|
17
|
+
outputs: ['main'],
|
|
18
|
+
properties: [
|
|
19
|
+
{
|
|
20
|
+
displayName: 'Tests run on the n8n worker, not your browser. They can consume substantial bandwidth. Ookla terms apply.',
|
|
21
|
+
name: 'notice',
|
|
22
|
+
type: 'notice',
|
|
23
|
+
default: '',
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
displayName: 'Accept Ookla License',
|
|
27
|
+
name: 'acceptLicense',
|
|
28
|
+
type: 'boolean',
|
|
29
|
+
default: false,
|
|
30
|
+
description: 'Whether you accept the Ookla EULA, Terms of Use, and Privacy Policy (speedtest.net/about/eula, /terms, /privacy)',
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
displayName: 'Accept Ookla GDPR Terms',
|
|
34
|
+
name: 'acceptGdpr',
|
|
35
|
+
type: 'boolean',
|
|
36
|
+
default: false,
|
|
37
|
+
description: 'Whether you accept the Ookla GDPR terms and consent to the processing described by Ookla',
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
displayName: 'Return Errors as Data',
|
|
41
|
+
name: 'returnErrors',
|
|
42
|
+
type: 'boolean',
|
|
43
|
+
default: true,
|
|
44
|
+
description: 'Whether to return structured errors on the regular output instead of failing the node. Diagnostics can contain network information.',
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
displayName: 'Options',
|
|
48
|
+
name: 'options',
|
|
49
|
+
type: 'collection',
|
|
50
|
+
placeholder: 'Add Option',
|
|
51
|
+
default: {},
|
|
52
|
+
options: [
|
|
53
|
+
{
|
|
54
|
+
displayName: 'Server ID',
|
|
55
|
+
name: 'serverId',
|
|
56
|
+
type: 'string',
|
|
57
|
+
default: '',
|
|
58
|
+
description: 'Ookla server ID. Leave empty to select a server automatically.',
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
displayName: 'Source IP',
|
|
62
|
+
name: 'sourceIp',
|
|
63
|
+
type: 'string',
|
|
64
|
+
default: '',
|
|
65
|
+
description: 'Local IP address of the interface to test. Leave empty for the default route.',
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
displayName: 'Timeout (Seconds)',
|
|
69
|
+
name: 'timeout',
|
|
70
|
+
type: 'number',
|
|
71
|
+
default: 120,
|
|
72
|
+
typeOptions: { minValue: 1, maxValue: 3600, numberPrecision: 0 },
|
|
73
|
+
description: 'Maximum time to wait for a result before cancelling the test',
|
|
74
|
+
},
|
|
75
|
+
],
|
|
76
|
+
},
|
|
77
|
+
],
|
|
78
|
+
};
|
|
79
|
+
async execute() {
|
|
80
|
+
const items = this.getInputData();
|
|
81
|
+
const output = [];
|
|
82
|
+
// Run sequentially: parallel speed tests would distort each other's results.
|
|
83
|
+
for (let itemIndex = 0; itemIndex < items.length; itemIndex++) {
|
|
84
|
+
try {
|
|
85
|
+
const acceptLicense = this.getNodeParameter('acceptLicense', itemIndex);
|
|
86
|
+
const acceptGdpr = this.getNodeParameter('acceptGdpr', itemIndex);
|
|
87
|
+
if (!acceptLicense || !acceptGdpr) {
|
|
88
|
+
throw new Error('Review and enable both Ookla consent options before running a speed test');
|
|
89
|
+
}
|
|
90
|
+
const options = this.getNodeParameter('options', itemIndex, {});
|
|
91
|
+
const timeout = options.timeout ?? 120;
|
|
92
|
+
if (!Number.isInteger(timeout) || timeout < 1 || timeout > 3600) {
|
|
93
|
+
throw new Error('Timeout must be an integer between 1 and 3600 seconds');
|
|
94
|
+
}
|
|
95
|
+
const serverId = options.serverId?.trim();
|
|
96
|
+
const sourceIp = options.sourceIp?.trim();
|
|
97
|
+
const json = await (0, runSpeedtest_1.runSpeedtest)({
|
|
98
|
+
acceptLicense,
|
|
99
|
+
acceptGdpr,
|
|
100
|
+
...(serverId ? { serverId } : {}),
|
|
101
|
+
...(sourceIp ? { sourceIp } : {}),
|
|
102
|
+
}, timeout);
|
|
103
|
+
output.push({ json, pairedItem: { item: itemIndex } });
|
|
104
|
+
}
|
|
105
|
+
catch (error) {
|
|
106
|
+
const cause = error instanceof Error ? error : new Error(String(error));
|
|
107
|
+
if (this.getNodeParameter('returnErrors', itemIndex, true) || this.continueOnFail()) {
|
|
108
|
+
output.push({ json: { success: false, error: (0, speedtestError_1.structuredError)(cause) }, pairedItem: { item: itemIndex } });
|
|
109
|
+
continue;
|
|
110
|
+
}
|
|
111
|
+
throw new n8n_workflow_1.NodeOperationError(this.getNode(), cause, { itemIndex });
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
return [output];
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
exports.Speedtest = Speedtest;
|
|
118
|
+
//# sourceMappingURL=Speedtest.node.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"Speedtest.node.js","sourceRoot":"","sources":["../../../nodes/Speedtest/Speedtest.node.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { IDataObject } from 'n8n-workflow';
|
|
2
|
+
export interface SpeedtestOptions {
|
|
3
|
+
acceptLicense: boolean;
|
|
4
|
+
acceptGdpr: boolean;
|
|
5
|
+
serverId?: string;
|
|
6
|
+
sourceIp?: string;
|
|
7
|
+
}
|
|
8
|
+
export declare function runSpeedtest(options: SpeedtestOptions, timeoutSeconds: number): Promise<IDataObject>;
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.runSpeedtest = runSpeedtest;
|
|
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;
|
|
9
|
+
async function runSpeedtest(options, timeoutSeconds) {
|
|
10
|
+
if (!options.acceptLicense || !options.acceptGdpr) {
|
|
11
|
+
throw new speedtestError_1.SpeedtestError('Both Ookla consent options are required', 'INVALID_CONFIGURATION');
|
|
12
|
+
}
|
|
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');
|
|
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
|
+
});
|
|
92
|
+
}
|
|
93
|
+
//# sourceMappingURL=runSpeedtest.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"runSpeedtest.js","sourceRoot":"","sources":["../../../nodes/Speedtest/runSpeedtest.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64"><rect width="64" height="64" rx="12" fill="#182b49"/><path d="M12 46a23 23 0 1 1 40 0" fill="none" stroke="#40c9a2" stroke-width="6"/><path d="m32 36 14-15" stroke="white" stroke-width="5" stroke-linecap="round"/><circle cx="32" cy="36" r="5" fill="white"/></svg>
|
|
@@ -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,6 +1,42 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "n8n-nodes-speedtest-net",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "Run Ookla internet speed tests in n8n with structured CLI diagnostics",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"keywords": [
|
|
7
|
+
"n8n-community-node-package",
|
|
8
|
+
"n8n",
|
|
9
|
+
"speedtest",
|
|
10
|
+
"speedtest-net"
|
|
11
|
+
],
|
|
12
|
+
"files": [
|
|
13
|
+
"dist"
|
|
14
|
+
],
|
|
15
|
+
"scripts": {
|
|
16
|
+
"build": "tsc && node scripts/copy-assets.mjs",
|
|
17
|
+
"test": "vitest run",
|
|
18
|
+
"typecheck": "tsc --noEmit",
|
|
19
|
+
"prepack": "npm run build"
|
|
20
|
+
},
|
|
21
|
+
"engines": {
|
|
22
|
+
"node": ">=22"
|
|
23
|
+
},
|
|
24
|
+
"n8n": {
|
|
25
|
+
"n8nNodesApiVersion": 1,
|
|
26
|
+
"nodes": [
|
|
27
|
+
"dist/nodes/Speedtest/Speedtest.node.js"
|
|
28
|
+
]
|
|
29
|
+
},
|
|
30
|
+
"allowScripts": {
|
|
31
|
+
"esbuild": true
|
|
32
|
+
},
|
|
33
|
+
"peerDependencies": {
|
|
34
|
+
"n8n-workflow": "*"
|
|
35
|
+
},
|
|
36
|
+
"devDependencies": {
|
|
37
|
+
"@types/node": "^26.6.6",
|
|
38
|
+
"n8n-workflow": "stable",
|
|
39
|
+
"typescript": "^7.0.2",
|
|
40
|
+
"vitest": "^5.0.5"
|
|
41
|
+
}
|
|
42
|
+
}
|