space-data-module-sdk 0.1.0 → 0.2.5
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 +190 -0
- package/README.md +236 -73
- package/bin/space-data-module.js +24 -0
- package/package.json +16 -4
- package/schemas/ModuleBundle.fbs +108 -0
- package/schemas/PluginInvokeRequest.fbs +18 -0
- package/schemas/PluginInvokeResponse.fbs +30 -0
- package/schemas/PluginManifest.fbs +33 -1
- package/schemas/TypedArenaBuffer.fbs +23 -2
- package/src/bundle/codec.js +268 -0
- package/src/bundle/constants.js +8 -0
- package/src/bundle/index.js +3 -0
- package/src/bundle/wasm.js +447 -0
- package/src/compiler/compileModule.js +353 -37
- package/src/compiler/emceptionNode.js +217 -0
- package/src/compiler/flatcSupport.js +66 -0
- package/src/compiler/invokeGlue.js +884 -0
- package/src/compliance/pluginCompliance.js +575 -1
- package/src/generated/orbpro/invoke/plugin-invoke-request.d.ts +51 -0
- package/src/generated/orbpro/invoke/plugin-invoke-request.d.ts.map +1 -0
- package/src/generated/orbpro/invoke/plugin-invoke-request.js +131 -0
- package/src/generated/orbpro/invoke/plugin-invoke-request.js.map +1 -0
- package/src/generated/orbpro/invoke/plugin-invoke-request.ts +173 -0
- package/src/generated/orbpro/invoke/plugin-invoke-response.d.ts +76 -0
- package/src/generated/orbpro/invoke/plugin-invoke-response.d.ts.map +1 -0
- package/src/generated/orbpro/invoke/plugin-invoke-response.js +184 -0
- package/src/generated/orbpro/invoke/plugin-invoke-response.js.map +1 -0
- package/src/generated/orbpro/invoke/plugin-invoke-response.ts +243 -0
- package/src/generated/orbpro/invoke.d.ts +3 -0
- package/src/generated/orbpro/invoke.d.ts.map +1 -0
- package/src/generated/orbpro/invoke.js +5 -0
- package/src/generated/orbpro/invoke.js.map +1 -0
- package/src/generated/orbpro/invoke.ts +6 -0
- package/src/generated/orbpro/manifest/accepted-type-set.d.ts +4 -4
- package/src/generated/orbpro/manifest/accepted-type-set.d.ts.map +1 -1
- package/src/generated/orbpro/manifest/accepted-type-set.js +18 -11
- package/src/generated/orbpro/manifest/accepted-type-set.js.map +1 -1
- package/src/generated/orbpro/manifest/build-artifact.d.ts +1 -1
- package/src/generated/orbpro/manifest/build-artifact.d.ts.map +1 -1
- package/src/generated/orbpro/manifest/build-artifact.js +28 -15
- package/src/generated/orbpro/manifest/build-artifact.js.map +1 -1
- package/src/generated/orbpro/manifest/capability-kind.d.ts +26 -1
- package/src/generated/orbpro/manifest/capability-kind.d.ts.map +1 -1
- package/src/generated/orbpro/manifest/capability-kind.js +25 -0
- package/src/generated/orbpro/manifest/capability-kind.js.map +1 -1
- package/src/generated/orbpro/manifest/capability-kind.ts +25 -0
- package/src/generated/orbpro/manifest/drain-policy.d.ts.map +1 -1
- package/src/generated/orbpro/manifest/drain-policy.js.map +1 -1
- package/src/generated/orbpro/manifest/host-capability.d.ts +2 -2
- package/src/generated/orbpro/manifest/host-capability.d.ts.map +1 -1
- package/src/generated/orbpro/manifest/host-capability.js +19 -11
- package/src/generated/orbpro/manifest/host-capability.js.map +1 -1
- package/src/generated/orbpro/manifest/invoke-surface.d.ts +8 -0
- package/src/generated/orbpro/manifest/invoke-surface.d.ts.map +1 -0
- package/src/generated/orbpro/manifest/invoke-surface.js +11 -0
- package/src/generated/orbpro/manifest/invoke-surface.js.map +1 -0
- package/src/generated/orbpro/manifest/invoke-surface.ts +11 -0
- package/src/generated/orbpro/manifest/method-manifest.d.ts +6 -6
- package/src/generated/orbpro/manifest/method-manifest.d.ts.map +1 -1
- package/src/generated/orbpro/manifest/method-manifest.js +33 -16
- package/src/generated/orbpro/manifest/method-manifest.js.map +1 -1
- package/src/generated/orbpro/manifest/plugin-family.d.ts.map +1 -1
- package/src/generated/orbpro/manifest/plugin-family.js.map +1 -1
- package/src/generated/orbpro/manifest/plugin-manifest.d.ts +10 -2
- package/src/generated/orbpro/manifest/plugin-manifest.d.ts.map +1 -1
- package/src/generated/orbpro/manifest/plugin-manifest.js +48 -9
- package/src/generated/orbpro/manifest/plugin-manifest.js.map +1 -1
- package/src/generated/orbpro/manifest/plugin-manifest.ts +322 -491
- package/src/generated/orbpro/manifest/port-manifest.d.ts +4 -4
- package/src/generated/orbpro/manifest/port-manifest.d.ts.map +1 -1
- package/src/generated/orbpro/manifest/port-manifest.js +26 -13
- package/src/generated/orbpro/manifest/port-manifest.js.map +1 -1
- package/src/generated/orbpro/manifest/protocol-spec.d.ts +1 -1
- package/src/generated/orbpro/manifest/protocol-spec.d.ts.map +1 -1
- package/src/generated/orbpro/manifest/protocol-spec.js +28 -15
- package/src/generated/orbpro/manifest/protocol-spec.js.map +1 -1
- package/src/generated/orbpro/manifest/timer-spec.d.ts +1 -1
- package/src/generated/orbpro/manifest/timer-spec.d.ts.map +1 -1
- package/src/generated/orbpro/manifest/timer-spec.js +27 -16
- package/src/generated/orbpro/manifest/timer-spec.js.map +1 -1
- package/src/generated/orbpro/manifest.d.ts +13 -0
- package/src/generated/orbpro/manifest.d.ts.map +1 -0
- package/src/generated/orbpro/manifest.js +1 -0
- package/src/generated/orbpro/manifest.js.map +1 -0
- package/src/generated/orbpro/manifest.ts +16 -0
- package/src/generated/orbpro/module/canonicalization-rule.d.ts +48 -0
- package/src/generated/orbpro/module/canonicalization-rule.js +95 -0
- package/src/generated/orbpro/module/canonicalization-rule.ts +142 -0
- package/src/generated/orbpro/module/module-bundle-entry-role.d.ts +11 -0
- package/src/generated/orbpro/module/module-bundle-entry-role.js +14 -0
- package/src/generated/orbpro/module/module-bundle-entry-role.ts +15 -0
- package/src/generated/orbpro/module/module-bundle-entry.d.ts +97 -0
- package/src/generated/orbpro/module/module-bundle-entry.js +219 -0
- package/src/generated/orbpro/module/module-bundle-entry.ts +287 -0
- package/src/generated/orbpro/module/module-bundle.d.ts +86 -0
- package/src/generated/orbpro/module/module-bundle.js +213 -0
- package/src/generated/orbpro/module/module-bundle.ts +277 -0
- package/src/generated/orbpro/module/module-payload-encoding.d.ts +9 -0
- package/src/generated/orbpro/module/module-payload-encoding.js +12 -0
- package/src/generated/orbpro/module/module-payload-encoding.ts +13 -0
- package/src/generated/orbpro/module.d.ts +5 -0
- package/src/generated/orbpro/module.js +7 -0
- package/src/generated/orbpro/module.ts +9 -0
- package/src/generated/orbpro/stream/buffer-mutability.d.ts.map +1 -1
- package/src/generated/orbpro/stream/buffer-mutability.js.map +1 -1
- package/src/generated/orbpro/stream/buffer-ownership.d.ts.map +1 -1
- package/src/generated/orbpro/stream/buffer-ownership.js.map +1 -1
- package/src/generated/orbpro/stream/flat-buffer-type-ref.d.ts +22 -5
- package/src/generated/orbpro/stream/flat-buffer-type-ref.d.ts.map +1 -1
- package/src/generated/orbpro/stream/flat-buffer-type-ref.js +107 -17
- package/src/generated/orbpro/stream/flat-buffer-type-ref.js.map +1 -1
- package/src/generated/orbpro/stream/flat-buffer-type-ref.ts +126 -2
- package/src/generated/orbpro/stream/payload-wire-format.d.ts +8 -0
- package/src/generated/orbpro/stream/payload-wire-format.d.ts.map +1 -0
- package/src/generated/orbpro/stream/payload-wire-format.js +11 -0
- package/src/generated/orbpro/stream/payload-wire-format.js.map +1 -0
- package/src/generated/orbpro/stream/payload-wire-format.ts +11 -0
- package/src/generated/orbpro/stream/typed-arena-buffer.d.ts +4 -4
- package/src/generated/orbpro/stream/typed-arena-buffer.d.ts.map +1 -1
- package/src/generated/orbpro/stream/typed-arena-buffer.js +42 -24
- package/src/generated/orbpro/stream/typed-arena-buffer.js.map +1 -1
- package/src/host/abi.js +282 -0
- package/src/host/cron.js +247 -0
- package/src/host/index.js +3 -0
- package/src/host/nodeHost.js +2165 -0
- package/src/index.d.ts +958 -0
- package/src/index.js +12 -2
- package/src/invoke/codec.js +278 -0
- package/src/invoke/index.js +9 -0
- package/src/manifest/codec.js +10 -2
- package/src/manifest/index.js +5 -2
- package/src/manifest/normalize.js +90 -1
- package/src/runtime/constants.js +29 -0
- package/src/transport/pki.js +0 -5
- package/src/utils/encoding.js +9 -1
- package/src/utils/wasmCrypto.js +49 -1
|
@@ -0,0 +1,2165 @@
|
|
|
1
|
+
import dgram from "node:dgram";
|
|
2
|
+
import net from "node:net";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { spawn } from "node:child_process";
|
|
5
|
+
import { mkdir, readFile, readdir, rename, rm, stat, writeFile } from "node:fs/promises";
|
|
6
|
+
import { performance } from "node:perf_hooks";
|
|
7
|
+
import nodeTls from "node:tls";
|
|
8
|
+
|
|
9
|
+
import {
|
|
10
|
+
decryptBytesFromEnvelope,
|
|
11
|
+
encryptBytesForRecipient,
|
|
12
|
+
generateX25519Keypair,
|
|
13
|
+
} from "../transport/pki.js";
|
|
14
|
+
import { RuntimeTarget } from "../runtime/constants.js";
|
|
15
|
+
import { sha256Bytes } from "../utils/crypto.js";
|
|
16
|
+
import { toUint8Array } from "../utils/encoding.js";
|
|
17
|
+
import {
|
|
18
|
+
aesGcmDecrypt,
|
|
19
|
+
aesGcmEncrypt,
|
|
20
|
+
ed25519PublicKey,
|
|
21
|
+
ed25519Sign,
|
|
22
|
+
ed25519Verify,
|
|
23
|
+
hkdfBytes,
|
|
24
|
+
randomBytes,
|
|
25
|
+
secp256k1PublicKey,
|
|
26
|
+
secp256k1SignDigest,
|
|
27
|
+
secp256k1VerifyDigest,
|
|
28
|
+
sha512Bytes,
|
|
29
|
+
x25519PublicKey,
|
|
30
|
+
x25519SharedSecret,
|
|
31
|
+
} from "../utils/wasmCrypto.js";
|
|
32
|
+
import {
|
|
33
|
+
matchesCronExpression,
|
|
34
|
+
nextCronOccurrence,
|
|
35
|
+
parseCronExpression,
|
|
36
|
+
} from "./cron.js";
|
|
37
|
+
|
|
38
|
+
const textEncoder = new TextEncoder();
|
|
39
|
+
const textDecoder = new TextDecoder();
|
|
40
|
+
|
|
41
|
+
export const NodeHostSupportedCapabilities = Object.freeze([
|
|
42
|
+
"clock",
|
|
43
|
+
"random",
|
|
44
|
+
"timers",
|
|
45
|
+
"schedule_cron",
|
|
46
|
+
"http",
|
|
47
|
+
"websocket",
|
|
48
|
+
"mqtt",
|
|
49
|
+
"filesystem",
|
|
50
|
+
"tcp",
|
|
51
|
+
"udp",
|
|
52
|
+
"tls",
|
|
53
|
+
"context_read",
|
|
54
|
+
"context_write",
|
|
55
|
+
"crypto_hash",
|
|
56
|
+
"crypto_sign",
|
|
57
|
+
"crypto_verify",
|
|
58
|
+
"crypto_encrypt",
|
|
59
|
+
"crypto_decrypt",
|
|
60
|
+
"crypto_key_agreement",
|
|
61
|
+
"crypto_kdf",
|
|
62
|
+
"process_exec",
|
|
63
|
+
]);
|
|
64
|
+
|
|
65
|
+
export const NodeHostSupportedOperations = Object.freeze([
|
|
66
|
+
"clock.now",
|
|
67
|
+
"clock.monotonicNow",
|
|
68
|
+
"clock.nowIso",
|
|
69
|
+
"random.bytes",
|
|
70
|
+
"timers.delay",
|
|
71
|
+
"schedule.parse",
|
|
72
|
+
"schedule.matches",
|
|
73
|
+
"schedule.next",
|
|
74
|
+
"http.request",
|
|
75
|
+
"websocket.exchange",
|
|
76
|
+
"mqtt.publish",
|
|
77
|
+
"mqtt.subscribeOnce",
|
|
78
|
+
"filesystem.resolvePath",
|
|
79
|
+
"filesystem.readFile",
|
|
80
|
+
"filesystem.writeFile",
|
|
81
|
+
"filesystem.appendFile",
|
|
82
|
+
"filesystem.deleteFile",
|
|
83
|
+
"filesystem.mkdir",
|
|
84
|
+
"filesystem.readdir",
|
|
85
|
+
"filesystem.stat",
|
|
86
|
+
"filesystem.rename",
|
|
87
|
+
"tcp.request",
|
|
88
|
+
"udp.request",
|
|
89
|
+
"tls.request",
|
|
90
|
+
"exec.execFile",
|
|
91
|
+
"context.get",
|
|
92
|
+
"context.set",
|
|
93
|
+
"context.delete",
|
|
94
|
+
"context.listKeys",
|
|
95
|
+
"context.listScopes",
|
|
96
|
+
"crypto.sha256",
|
|
97
|
+
"crypto.sha512",
|
|
98
|
+
"crypto.hkdf",
|
|
99
|
+
"crypto.aesGcmEncrypt",
|
|
100
|
+
"crypto.aesGcmDecrypt",
|
|
101
|
+
"crypto.x25519.generateKeypair",
|
|
102
|
+
"crypto.x25519.publicKey",
|
|
103
|
+
"crypto.x25519.sharedSecret",
|
|
104
|
+
"crypto.sealedBox.encryptForRecipient",
|
|
105
|
+
"crypto.sealedBox.decryptFromEnvelope",
|
|
106
|
+
"crypto.secp256k1.publicKeyFromPrivate",
|
|
107
|
+
"crypto.secp256k1.signDigest",
|
|
108
|
+
"crypto.secp256k1.verifyDigest",
|
|
109
|
+
"crypto.ed25519.publicKeyFromSeed",
|
|
110
|
+
"crypto.ed25519.sign",
|
|
111
|
+
"crypto.ed25519.verify",
|
|
112
|
+
]);
|
|
113
|
+
|
|
114
|
+
function cloneValue(value) {
|
|
115
|
+
return value === undefined ? undefined : structuredClone(value);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function assertNonEmptyString(value, label) {
|
|
119
|
+
const normalized = String(value ?? "").trim();
|
|
120
|
+
if (!normalized) {
|
|
121
|
+
throw new TypeError(`${label} is required.`);
|
|
122
|
+
}
|
|
123
|
+
return normalized;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function assertNonNegativeInteger(value, label) {
|
|
127
|
+
const normalized = Number(value);
|
|
128
|
+
if (!Number.isInteger(normalized) || normalized < 0) {
|
|
129
|
+
throw new TypeError(`${label} must be a non-negative integer.`);
|
|
130
|
+
}
|
|
131
|
+
return normalized;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function normalizeGrantedCapabilities(options) {
|
|
135
|
+
const source =
|
|
136
|
+
options.grantedCapabilities ??
|
|
137
|
+
options.capabilities ??
|
|
138
|
+
options.manifest?.capabilities ??
|
|
139
|
+
NodeHostSupportedCapabilities;
|
|
140
|
+
if (!Array.isArray(source)) {
|
|
141
|
+
throw new TypeError(
|
|
142
|
+
"Node host capabilities must be an array when provided.",
|
|
143
|
+
);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const normalized = new Set();
|
|
147
|
+
for (const capability of source) {
|
|
148
|
+
const id = assertNonEmptyString(capability, "Capability id");
|
|
149
|
+
normalized.add(id);
|
|
150
|
+
}
|
|
151
|
+
return normalized;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function normalizeAllowedOrigins(origins) {
|
|
155
|
+
if (origins === undefined || origins === null) {
|
|
156
|
+
return null;
|
|
157
|
+
}
|
|
158
|
+
if (!Array.isArray(origins)) {
|
|
159
|
+
throw new TypeError("allowedHttpOrigins must be an array of origin strings.");
|
|
160
|
+
}
|
|
161
|
+
const normalized = new Set();
|
|
162
|
+
for (const origin of origins) {
|
|
163
|
+
normalized.add(new URL(assertNonEmptyString(origin, "HTTP origin")).origin);
|
|
164
|
+
}
|
|
165
|
+
return normalized;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function normalizeAllowedWebSocketOrigins(origins) {
|
|
169
|
+
if (origins === undefined || origins === null) {
|
|
170
|
+
return null;
|
|
171
|
+
}
|
|
172
|
+
if (!Array.isArray(origins)) {
|
|
173
|
+
throw new TypeError(
|
|
174
|
+
"allowedWebSocketOrigins must be an array of WebSocket origin strings.",
|
|
175
|
+
);
|
|
176
|
+
}
|
|
177
|
+
const normalized = new Set();
|
|
178
|
+
for (const origin of origins) {
|
|
179
|
+
const url = new URL(assertNonEmptyString(origin, "WebSocket origin"));
|
|
180
|
+
if (!["ws:", "wss:"].includes(url.protocol)) {
|
|
181
|
+
throw new TypeError(
|
|
182
|
+
`WebSocket origin "${origin}" must use ws: or wss:.`,
|
|
183
|
+
);
|
|
184
|
+
}
|
|
185
|
+
normalized.add(url.origin);
|
|
186
|
+
}
|
|
187
|
+
return normalized;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function normalizeAllowedCommands(commands) {
|
|
191
|
+
if (commands === undefined || commands === null) {
|
|
192
|
+
return null;
|
|
193
|
+
}
|
|
194
|
+
if (!Array.isArray(commands)) {
|
|
195
|
+
throw new TypeError("allowedCommands must be an array of executable paths.");
|
|
196
|
+
}
|
|
197
|
+
const normalized = new Set();
|
|
198
|
+
for (const command of commands) {
|
|
199
|
+
normalized.add(assertNonEmptyString(command, "Executable path"));
|
|
200
|
+
}
|
|
201
|
+
return normalized;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function normalizeAllowedHosts(hosts, label) {
|
|
205
|
+
if (hosts === undefined || hosts === null) {
|
|
206
|
+
return null;
|
|
207
|
+
}
|
|
208
|
+
if (!Array.isArray(hosts)) {
|
|
209
|
+
throw new TypeError(`${label} must be an array of host strings.`);
|
|
210
|
+
}
|
|
211
|
+
const normalized = new Set();
|
|
212
|
+
for (const host of hosts) {
|
|
213
|
+
normalized.add(assertNonEmptyString(host, label).toLowerCase());
|
|
214
|
+
}
|
|
215
|
+
return normalized;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function normalizeAllowedPorts(ports, label, { allowZero = false } = {}) {
|
|
219
|
+
if (ports === undefined || ports === null) {
|
|
220
|
+
return null;
|
|
221
|
+
}
|
|
222
|
+
if (!Array.isArray(ports)) {
|
|
223
|
+
throw new TypeError(`${label} must be an array of integer ports.`);
|
|
224
|
+
}
|
|
225
|
+
const normalized = new Set();
|
|
226
|
+
for (const port of ports) {
|
|
227
|
+
normalized.add(assertPort(port, label, { allowZero }));
|
|
228
|
+
}
|
|
229
|
+
return normalized;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function normalizeFilesystemRoot(rootPath) {
|
|
233
|
+
return path.resolve(String(rootPath ?? process.cwd()));
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
function headersToObject(headers) {
|
|
237
|
+
return Object.fromEntries(headers.entries());
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
function normalizeResponseType(responseType) {
|
|
241
|
+
const normalized = String(responseType ?? "bytes").trim().toLowerCase();
|
|
242
|
+
if (!["bytes", "text", "json"].includes(normalized)) {
|
|
243
|
+
throw new Error(
|
|
244
|
+
`Unsupported HTTP responseType "${responseType}". Expected "bytes", "text", or "json".`,
|
|
245
|
+
);
|
|
246
|
+
}
|
|
247
|
+
return normalized;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
function normalizeWebSocketResponseType(responseType) {
|
|
251
|
+
const normalized = String(responseType ?? "utf8").trim().toLowerCase();
|
|
252
|
+
if (!["bytes", "utf8", "json"].includes(normalized)) {
|
|
253
|
+
throw new Error(
|
|
254
|
+
`Unsupported WebSocket responseType "${responseType}". Expected "bytes", "utf8", or "json".`,
|
|
255
|
+
);
|
|
256
|
+
}
|
|
257
|
+
return normalized;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
function normalizeSocketEncoding(encoding, label = "Socket encoding") {
|
|
261
|
+
const normalized = String(encoding ?? "bytes").trim().toLowerCase();
|
|
262
|
+
if (!["utf8", "bytes"].includes(normalized)) {
|
|
263
|
+
throw new Error(
|
|
264
|
+
`${label} "${encoding}" is unsupported. Expected "utf8" or "bytes".`,
|
|
265
|
+
);
|
|
266
|
+
}
|
|
267
|
+
return normalized;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
function normalizeSocketPayload(value, label, textEncoding = "utf8") {
|
|
271
|
+
if (value === undefined || value === null) {
|
|
272
|
+
return null;
|
|
273
|
+
}
|
|
274
|
+
if (typeof value === "string") {
|
|
275
|
+
return Buffer.from(value, textEncoding);
|
|
276
|
+
}
|
|
277
|
+
return Buffer.from(toUint8Array(value));
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
function assertPort(value, label, { allowZero = false } = {}) {
|
|
281
|
+
const normalized = Number(value);
|
|
282
|
+
const min = allowZero ? 0 : 1;
|
|
283
|
+
if (!Number.isInteger(normalized) || normalized < min || normalized > 65535) {
|
|
284
|
+
throw new TypeError(`${label} must be an integer in range ${min}..65535.`);
|
|
285
|
+
}
|
|
286
|
+
return normalized;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
function assertNetworkTargetAllowed(
|
|
290
|
+
kind,
|
|
291
|
+
host,
|
|
292
|
+
port,
|
|
293
|
+
allowedHosts,
|
|
294
|
+
allowedPorts,
|
|
295
|
+
) {
|
|
296
|
+
const normalizedHost = assertNonEmptyString(host, `${kind} host`).toLowerCase();
|
|
297
|
+
const normalizedPort = assertPort(port, `${kind} port`);
|
|
298
|
+
if (allowedHosts && !allowedHosts.has(normalizedHost)) {
|
|
299
|
+
throw new Error(`${kind} host "${host}" is not permitted by this host.`);
|
|
300
|
+
}
|
|
301
|
+
if (allowedPorts && !allowedPorts.has(normalizedPort)) {
|
|
302
|
+
throw new Error(`${kind} port "${port}" is not permitted by this host.`);
|
|
303
|
+
}
|
|
304
|
+
return {
|
|
305
|
+
host: normalizedHost,
|
|
306
|
+
port: normalizedPort,
|
|
307
|
+
};
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
function createTimeoutSignal(timeoutMs) {
|
|
311
|
+
if (timeoutMs === undefined || timeoutMs === null) {
|
|
312
|
+
return { signal: undefined, dispose() {} };
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
const duration = assertNonNegativeInteger(timeoutMs, "HTTP timeoutMs");
|
|
316
|
+
const controller = new AbortController();
|
|
317
|
+
const timer = setTimeout(() => controller.abort(), duration);
|
|
318
|
+
return {
|
|
319
|
+
signal: controller.signal,
|
|
320
|
+
dispose() {
|
|
321
|
+
clearTimeout(timer);
|
|
322
|
+
},
|
|
323
|
+
};
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
function resolveSignals(primary, secondary) {
|
|
327
|
+
if (!primary) {
|
|
328
|
+
return secondary ?? undefined;
|
|
329
|
+
}
|
|
330
|
+
if (!secondary) {
|
|
331
|
+
return primary;
|
|
332
|
+
}
|
|
333
|
+
if (typeof AbortSignal.any === "function") {
|
|
334
|
+
return AbortSignal.any([primary, secondary]);
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
const controller = new AbortController();
|
|
338
|
+
const onAbort = () => controller.abort();
|
|
339
|
+
primary.addEventListener("abort", onAbort, { once: true });
|
|
340
|
+
secondary.addEventListener("abort", onAbort, { once: true });
|
|
341
|
+
return controller.signal;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
function normalizeRequestBody(body) {
|
|
345
|
+
if (body === undefined || body === null) {
|
|
346
|
+
return undefined;
|
|
347
|
+
}
|
|
348
|
+
if (typeof body === "string") {
|
|
349
|
+
return body;
|
|
350
|
+
}
|
|
351
|
+
return toUint8Array(body);
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
function normalizeWebSocketMessage(message) {
|
|
355
|
+
if (message === undefined || message === null) {
|
|
356
|
+
return undefined;
|
|
357
|
+
}
|
|
358
|
+
if (typeof message === "string") {
|
|
359
|
+
return message;
|
|
360
|
+
}
|
|
361
|
+
return toUint8Array(message);
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
function normalizeWebSocketUrl(url) {
|
|
365
|
+
const resolved = new URL(assertNonEmptyString(url, "WebSocket url"));
|
|
366
|
+
if (!["ws:", "wss:"].includes(resolved.protocol)) {
|
|
367
|
+
throw new Error(
|
|
368
|
+
`Unsupported WebSocket protocol "${resolved.protocol}". Expected ws: or wss:.`,
|
|
369
|
+
);
|
|
370
|
+
}
|
|
371
|
+
return resolved;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
async function decodeWebSocketMessage(data, responseType) {
|
|
375
|
+
if (typeof data === "string") {
|
|
376
|
+
if (responseType === "json") {
|
|
377
|
+
return JSON.parse(data);
|
|
378
|
+
}
|
|
379
|
+
if (responseType === "bytes") {
|
|
380
|
+
return textEncoder.encode(data);
|
|
381
|
+
}
|
|
382
|
+
return data;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
let bytes = null;
|
|
386
|
+
if (data instanceof ArrayBuffer) {
|
|
387
|
+
bytes = new Uint8Array(data);
|
|
388
|
+
} else if (ArrayBuffer.isView(data)) {
|
|
389
|
+
bytes = new Uint8Array(data.buffer, data.byteOffset, data.byteLength);
|
|
390
|
+
} else if (typeof Blob !== "undefined" && data instanceof Blob) {
|
|
391
|
+
bytes = new Uint8Array(await data.arrayBuffer());
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
if (!bytes) {
|
|
395
|
+
throw new TypeError("Unsupported WebSocket message payload type.");
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
if (responseType === "json") {
|
|
399
|
+
return JSON.parse(textDecoder.decode(bytes));
|
|
400
|
+
}
|
|
401
|
+
if (responseType === "utf8") {
|
|
402
|
+
return textDecoder.decode(bytes);
|
|
403
|
+
}
|
|
404
|
+
return new Uint8Array(bytes);
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
async function runTcpRequest(options) {
|
|
408
|
+
const responseEncoding = normalizeSocketEncoding(
|
|
409
|
+
options.responseEncoding,
|
|
410
|
+
"TCP responseEncoding",
|
|
411
|
+
);
|
|
412
|
+
const payload = normalizeSocketPayload(options.data, "TCP request data");
|
|
413
|
+
|
|
414
|
+
return new Promise((resolve, reject) => {
|
|
415
|
+
const socket = net.createConnection({
|
|
416
|
+
host: options.host,
|
|
417
|
+
port: options.port,
|
|
418
|
+
});
|
|
419
|
+
const chunks = [];
|
|
420
|
+
let settled = false;
|
|
421
|
+
|
|
422
|
+
function cleanup() {
|
|
423
|
+
socket.removeAllListeners();
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
function fail(error) {
|
|
427
|
+
if (settled) {
|
|
428
|
+
return;
|
|
429
|
+
}
|
|
430
|
+
settled = true;
|
|
431
|
+
cleanup();
|
|
432
|
+
socket.destroy();
|
|
433
|
+
reject(error);
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
function finish() {
|
|
437
|
+
if (settled) {
|
|
438
|
+
return;
|
|
439
|
+
}
|
|
440
|
+
settled = true;
|
|
441
|
+
const bodyBuffer = Buffer.concat(chunks);
|
|
442
|
+
const result = {
|
|
443
|
+
host: options.host,
|
|
444
|
+
port: options.port,
|
|
445
|
+
localAddress: socket.localAddress ?? null,
|
|
446
|
+
localPort: socket.localPort ?? null,
|
|
447
|
+
remoteAddress: socket.remoteAddress ?? options.host,
|
|
448
|
+
remotePort: socket.remotePort ?? options.port,
|
|
449
|
+
body:
|
|
450
|
+
responseEncoding === "bytes"
|
|
451
|
+
? new Uint8Array(bodyBuffer)
|
|
452
|
+
: bodyBuffer.toString("utf8"),
|
|
453
|
+
};
|
|
454
|
+
cleanup();
|
|
455
|
+
socket.destroy();
|
|
456
|
+
resolve(result);
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
socket.once("error", fail);
|
|
460
|
+
socket.on("data", (chunk) => {
|
|
461
|
+
chunks.push(Buffer.from(chunk));
|
|
462
|
+
});
|
|
463
|
+
socket.once("end", finish);
|
|
464
|
+
socket.once("close", (hadError) => {
|
|
465
|
+
if (!hadError) {
|
|
466
|
+
finish();
|
|
467
|
+
}
|
|
468
|
+
});
|
|
469
|
+
|
|
470
|
+
if (options.timeoutMs !== undefined && options.timeoutMs !== null) {
|
|
471
|
+
socket.setTimeout(
|
|
472
|
+
assertNonNegativeInteger(options.timeoutMs, "TCP timeoutMs"),
|
|
473
|
+
() => fail(new Error("TCP request timed out.")),
|
|
474
|
+
);
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
socket.once("connect", () => {
|
|
478
|
+
if (payload) {
|
|
479
|
+
socket.write(payload);
|
|
480
|
+
}
|
|
481
|
+
socket.end();
|
|
482
|
+
});
|
|
483
|
+
});
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
async function runUdpRequest(options) {
|
|
487
|
+
const responseEncoding = normalizeSocketEncoding(
|
|
488
|
+
options.responseEncoding,
|
|
489
|
+
"UDP responseEncoding",
|
|
490
|
+
);
|
|
491
|
+
const payload = normalizeSocketPayload(options.data, "UDP request data");
|
|
492
|
+
if (!payload || payload.length === 0) {
|
|
493
|
+
throw new TypeError("UDP request data is required.");
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
return new Promise((resolve, reject) => {
|
|
497
|
+
const socket = dgram.createSocket(options.type ?? "udp4");
|
|
498
|
+
let settled = false;
|
|
499
|
+
let timeout = null;
|
|
500
|
+
|
|
501
|
+
function cleanup() {
|
|
502
|
+
if (timeout) {
|
|
503
|
+
clearTimeout(timeout);
|
|
504
|
+
}
|
|
505
|
+
socket.removeAllListeners();
|
|
506
|
+
socket.close();
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
function fail(error) {
|
|
510
|
+
if (settled) {
|
|
511
|
+
return;
|
|
512
|
+
}
|
|
513
|
+
settled = true;
|
|
514
|
+
cleanup();
|
|
515
|
+
reject(error);
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
function finish(messageBytes = Buffer.alloc(0), rinfo = null) {
|
|
519
|
+
if (settled) {
|
|
520
|
+
return;
|
|
521
|
+
}
|
|
522
|
+
settled = true;
|
|
523
|
+
const local = socket.address();
|
|
524
|
+
const responseBuffer = Buffer.from(messageBytes);
|
|
525
|
+
const result = {
|
|
526
|
+
host: options.host,
|
|
527
|
+
port: options.port,
|
|
528
|
+
localAddress: typeof local === "string" ? local : local.address,
|
|
529
|
+
localPort: typeof local === "string" ? null : local.port,
|
|
530
|
+
remoteAddress: rinfo?.address ?? options.host,
|
|
531
|
+
remotePort: rinfo?.port ?? options.port,
|
|
532
|
+
body:
|
|
533
|
+
responseEncoding === "bytes"
|
|
534
|
+
? new Uint8Array(responseBuffer)
|
|
535
|
+
: responseBuffer.toString("utf8"),
|
|
536
|
+
};
|
|
537
|
+
cleanup();
|
|
538
|
+
resolve(result);
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
socket.once("error", fail);
|
|
542
|
+
socket.once("message", (message, rinfo) => finish(message, rinfo));
|
|
543
|
+
|
|
544
|
+
if (options.timeoutMs !== undefined && options.timeoutMs !== null) {
|
|
545
|
+
timeout = setTimeout(
|
|
546
|
+
() => fail(new Error("UDP request timed out.")),
|
|
547
|
+
assertNonNegativeInteger(options.timeoutMs, "UDP timeoutMs"),
|
|
548
|
+
);
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
socket.bind(
|
|
552
|
+
options.bindPort ?? 0,
|
|
553
|
+
options.bindAddress,
|
|
554
|
+
() => {
|
|
555
|
+
socket.send(payload, options.port, options.host, (error) => {
|
|
556
|
+
if (error) {
|
|
557
|
+
fail(error);
|
|
558
|
+
return;
|
|
559
|
+
}
|
|
560
|
+
if (options.expectResponse === false) {
|
|
561
|
+
finish();
|
|
562
|
+
}
|
|
563
|
+
});
|
|
564
|
+
},
|
|
565
|
+
);
|
|
566
|
+
});
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
async function runTlsRequest(options) {
|
|
570
|
+
const responseEncoding = normalizeSocketEncoding(
|
|
571
|
+
options.responseEncoding,
|
|
572
|
+
"TLS responseEncoding",
|
|
573
|
+
);
|
|
574
|
+
const payload = normalizeSocketPayload(options.data, "TLS request data");
|
|
575
|
+
|
|
576
|
+
return new Promise((resolve, reject) => {
|
|
577
|
+
const socket = nodeTls.connect({
|
|
578
|
+
host: options.host,
|
|
579
|
+
port: options.port,
|
|
580
|
+
ca: options.ca,
|
|
581
|
+
cert: options.cert,
|
|
582
|
+
key: options.key,
|
|
583
|
+
rejectUnauthorized: options.rejectUnauthorized ?? true,
|
|
584
|
+
servername:
|
|
585
|
+
options.servername ??
|
|
586
|
+
(net.isIP(options.host) ? undefined : options.host),
|
|
587
|
+
});
|
|
588
|
+
const chunks = [];
|
|
589
|
+
let settled = false;
|
|
590
|
+
|
|
591
|
+
function cleanup() {
|
|
592
|
+
socket.removeAllListeners();
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
function fail(error) {
|
|
596
|
+
if (settled) {
|
|
597
|
+
return;
|
|
598
|
+
}
|
|
599
|
+
settled = true;
|
|
600
|
+
cleanup();
|
|
601
|
+
socket.destroy();
|
|
602
|
+
reject(error);
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
function finish() {
|
|
606
|
+
if (settled) {
|
|
607
|
+
return;
|
|
608
|
+
}
|
|
609
|
+
settled = true;
|
|
610
|
+
const bodyBuffer = Buffer.concat(chunks);
|
|
611
|
+
const result = {
|
|
612
|
+
host: options.host,
|
|
613
|
+
port: options.port,
|
|
614
|
+
localAddress: socket.localAddress ?? null,
|
|
615
|
+
localPort: socket.localPort ?? null,
|
|
616
|
+
remoteAddress: socket.remoteAddress ?? options.host,
|
|
617
|
+
remotePort: socket.remotePort ?? options.port,
|
|
618
|
+
authorized: socket.authorized,
|
|
619
|
+
authorizationError: socket.authorizationError ?? null,
|
|
620
|
+
body:
|
|
621
|
+
responseEncoding === "bytes"
|
|
622
|
+
? new Uint8Array(bodyBuffer)
|
|
623
|
+
: bodyBuffer.toString("utf8"),
|
|
624
|
+
};
|
|
625
|
+
cleanup();
|
|
626
|
+
socket.destroy();
|
|
627
|
+
resolve(result);
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
socket.once("error", fail);
|
|
631
|
+
socket.on("data", (chunk) => {
|
|
632
|
+
chunks.push(Buffer.from(chunk));
|
|
633
|
+
});
|
|
634
|
+
socket.once("end", finish);
|
|
635
|
+
socket.once("close", (hadError) => {
|
|
636
|
+
if (!hadError) {
|
|
637
|
+
finish();
|
|
638
|
+
}
|
|
639
|
+
});
|
|
640
|
+
|
|
641
|
+
if (options.timeoutMs !== undefined && options.timeoutMs !== null) {
|
|
642
|
+
socket.setTimeout(
|
|
643
|
+
assertNonNegativeInteger(options.timeoutMs, "TLS timeoutMs"),
|
|
644
|
+
() => fail(new Error("TLS request timed out.")),
|
|
645
|
+
);
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
socket.once("secureConnect", () => {
|
|
649
|
+
if (payload) {
|
|
650
|
+
socket.write(payload);
|
|
651
|
+
}
|
|
652
|
+
socket.end();
|
|
653
|
+
});
|
|
654
|
+
});
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
async function runWebSocketExchange(options) {
|
|
658
|
+
const responseType = normalizeWebSocketResponseType(options.responseType);
|
|
659
|
+
const WebSocketImpl = options.WebSocketImpl ?? globalThis.WebSocket;
|
|
660
|
+
if (typeof WebSocketImpl !== "function") {
|
|
661
|
+
throw new TypeError("A WebSocket implementation is required.");
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
return new Promise((resolve, reject) => {
|
|
665
|
+
const websocket = new WebSocketImpl(
|
|
666
|
+
options.url,
|
|
667
|
+
options.protocols,
|
|
668
|
+
);
|
|
669
|
+
let settled = false;
|
|
670
|
+
let timeout = null;
|
|
671
|
+
|
|
672
|
+
function cleanup() {
|
|
673
|
+
if (timeout) {
|
|
674
|
+
clearTimeout(timeout);
|
|
675
|
+
}
|
|
676
|
+
websocket.removeEventListener("open", onOpen);
|
|
677
|
+
websocket.removeEventListener("message", onMessage);
|
|
678
|
+
websocket.removeEventListener("error", onError);
|
|
679
|
+
websocket.removeEventListener("close", onClose);
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
function finish(result) {
|
|
683
|
+
if (settled) {
|
|
684
|
+
return;
|
|
685
|
+
}
|
|
686
|
+
settled = true;
|
|
687
|
+
cleanup();
|
|
688
|
+
try {
|
|
689
|
+
if (websocket.readyState === WebSocketImpl.OPEN) {
|
|
690
|
+
websocket.close(1000, "completed");
|
|
691
|
+
}
|
|
692
|
+
} catch {}
|
|
693
|
+
resolve(result);
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
function fail(error) {
|
|
697
|
+
if (settled) {
|
|
698
|
+
return;
|
|
699
|
+
}
|
|
700
|
+
settled = true;
|
|
701
|
+
cleanup();
|
|
702
|
+
try {
|
|
703
|
+
websocket.close(1011, "error");
|
|
704
|
+
} catch {}
|
|
705
|
+
reject(error);
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
async function onMessage(event) {
|
|
709
|
+
try {
|
|
710
|
+
const body = await decodeWebSocketMessage(event.data, responseType);
|
|
711
|
+
finish({
|
|
712
|
+
url: websocket.url ?? options.url,
|
|
713
|
+
protocol: websocket.protocol ?? "",
|
|
714
|
+
extensions: websocket.extensions ?? "",
|
|
715
|
+
closeCode: null,
|
|
716
|
+
closeReason: "",
|
|
717
|
+
body,
|
|
718
|
+
});
|
|
719
|
+
} catch (error) {
|
|
720
|
+
fail(error);
|
|
721
|
+
}
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
function onOpen() {
|
|
725
|
+
try {
|
|
726
|
+
const message = normalizeWebSocketMessage(options.message);
|
|
727
|
+
if (message !== undefined) {
|
|
728
|
+
websocket.send(message);
|
|
729
|
+
}
|
|
730
|
+
if (options.expectResponse === false) {
|
|
731
|
+
finish({
|
|
732
|
+
url: websocket.url ?? options.url,
|
|
733
|
+
protocol: websocket.protocol ?? "",
|
|
734
|
+
extensions: websocket.extensions ?? "",
|
|
735
|
+
closeCode: null,
|
|
736
|
+
closeReason: "",
|
|
737
|
+
body: null,
|
|
738
|
+
});
|
|
739
|
+
}
|
|
740
|
+
} catch (error) {
|
|
741
|
+
fail(error);
|
|
742
|
+
}
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
function onError() {
|
|
746
|
+
fail(new Error("WebSocket exchange failed."));
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
function onClose(event) {
|
|
750
|
+
if (settled) {
|
|
751
|
+
return;
|
|
752
|
+
}
|
|
753
|
+
if (options.expectResponse === false) {
|
|
754
|
+
finish({
|
|
755
|
+
url: websocket.url ?? options.url,
|
|
756
|
+
protocol: websocket.protocol ?? "",
|
|
757
|
+
extensions: websocket.extensions ?? "",
|
|
758
|
+
closeCode: event.code ?? null,
|
|
759
|
+
closeReason: event.reason ?? "",
|
|
760
|
+
body: null,
|
|
761
|
+
});
|
|
762
|
+
return;
|
|
763
|
+
}
|
|
764
|
+
fail(
|
|
765
|
+
new Error(
|
|
766
|
+
`WebSocket closed before response (code ${event.code ?? "unknown"}).`,
|
|
767
|
+
),
|
|
768
|
+
);
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
websocket.addEventListener("open", onOpen);
|
|
772
|
+
websocket.addEventListener("message", onMessage);
|
|
773
|
+
websocket.addEventListener("error", onError);
|
|
774
|
+
websocket.addEventListener("close", onClose);
|
|
775
|
+
|
|
776
|
+
if (options.timeoutMs !== undefined && options.timeoutMs !== null) {
|
|
777
|
+
timeout = setTimeout(
|
|
778
|
+
() => fail(new Error("WebSocket exchange timed out.")),
|
|
779
|
+
assertNonNegativeInteger(options.timeoutMs, "WebSocket timeoutMs"),
|
|
780
|
+
);
|
|
781
|
+
}
|
|
782
|
+
});
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
function encodeMqttRemainingLength(length) {
|
|
786
|
+
const bytes = [];
|
|
787
|
+
let value = assertNonNegativeInteger(length, "MQTT remaining length");
|
|
788
|
+
do {
|
|
789
|
+
let encoded = value % 128;
|
|
790
|
+
value = Math.floor(value / 128);
|
|
791
|
+
if (value > 0) {
|
|
792
|
+
encoded |= 0x80;
|
|
793
|
+
}
|
|
794
|
+
bytes.push(encoded);
|
|
795
|
+
} while (value > 0);
|
|
796
|
+
return Buffer.from(bytes);
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
function encodeMqttString(value) {
|
|
800
|
+
const bytes = Buffer.from(assertNonEmptyString(value, "MQTT string"), "utf8");
|
|
801
|
+
const length = Buffer.alloc(2);
|
|
802
|
+
length.writeUInt16BE(bytes.length, 0);
|
|
803
|
+
return Buffer.concat([length, bytes]);
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
function decodeMqttString(buffer, offset = 0) {
|
|
807
|
+
const length = buffer.readUInt16BE(offset);
|
|
808
|
+
const start = offset + 2;
|
|
809
|
+
const end = start + length;
|
|
810
|
+
return {
|
|
811
|
+
value: buffer.subarray(start, end).toString("utf8"),
|
|
812
|
+
nextOffset: end,
|
|
813
|
+
};
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
function encodeMqttPacket(headerByte, ...parts) {
|
|
817
|
+
const body = Buffer.concat(parts.filter(Boolean));
|
|
818
|
+
return Buffer.concat([
|
|
819
|
+
Buffer.from([headerByte]),
|
|
820
|
+
encodeMqttRemainingLength(body.length),
|
|
821
|
+
body,
|
|
822
|
+
]);
|
|
823
|
+
}
|
|
824
|
+
|
|
825
|
+
function createMqttConnectPacket(options = {}) {
|
|
826
|
+
const flags =
|
|
827
|
+
0x02 |
|
|
828
|
+
(options.username ? 0x80 : 0) |
|
|
829
|
+
(options.password ? 0x40 : 0);
|
|
830
|
+
const keepAlive = Buffer.alloc(2);
|
|
831
|
+
keepAlive.writeUInt16BE(
|
|
832
|
+
assertNonNegativeInteger(options.keepAliveSeconds ?? 30, "MQTT keepAliveSeconds"),
|
|
833
|
+
0,
|
|
834
|
+
);
|
|
835
|
+
|
|
836
|
+
const payload = [
|
|
837
|
+
encodeMqttString(options.clientId ?? "space-data-module-sdk"),
|
|
838
|
+
];
|
|
839
|
+
if (options.username) {
|
|
840
|
+
payload.push(encodeMqttString(options.username));
|
|
841
|
+
}
|
|
842
|
+
if (options.password) {
|
|
843
|
+
payload.push(encodeMqttString(options.password));
|
|
844
|
+
}
|
|
845
|
+
|
|
846
|
+
return encodeMqttPacket(
|
|
847
|
+
0x10,
|
|
848
|
+
encodeMqttString("MQTT"),
|
|
849
|
+
Buffer.from([0x04, flags]),
|
|
850
|
+
keepAlive,
|
|
851
|
+
...payload,
|
|
852
|
+
);
|
|
853
|
+
}
|
|
854
|
+
|
|
855
|
+
function createMqttPublishPacket(options = {}) {
|
|
856
|
+
const payload =
|
|
857
|
+
typeof options.payload === "string"
|
|
858
|
+
? Buffer.from(options.payload, "utf8")
|
|
859
|
+
: Buffer.from(toUint8Array(options.payload ?? new Uint8Array()));
|
|
860
|
+
return encodeMqttPacket(0x30, encodeMqttString(options.topic), payload);
|
|
861
|
+
}
|
|
862
|
+
|
|
863
|
+
function createMqttSubscribePacket(options = {}) {
|
|
864
|
+
const packetId = Buffer.alloc(2);
|
|
865
|
+
packetId.writeUInt16BE(assertPort(options.packetId ?? 1, "MQTT packetId", {
|
|
866
|
+
allowZero: false,
|
|
867
|
+
}), 0);
|
|
868
|
+
return encodeMqttPacket(
|
|
869
|
+
0x82,
|
|
870
|
+
packetId,
|
|
871
|
+
encodeMqttString(options.topic),
|
|
872
|
+
Buffer.from([options.qos ?? 0]),
|
|
873
|
+
);
|
|
874
|
+
}
|
|
875
|
+
|
|
876
|
+
function createMqttDisconnectPacket() {
|
|
877
|
+
return Buffer.from([0xe0, 0x00]);
|
|
878
|
+
}
|
|
879
|
+
|
|
880
|
+
function parseMqttPackets(buffer) {
|
|
881
|
+
const packets = [];
|
|
882
|
+
let offset = 0;
|
|
883
|
+
|
|
884
|
+
while (offset < buffer.length) {
|
|
885
|
+
const packetStart = offset;
|
|
886
|
+
if (offset + 2 > buffer.length) {
|
|
887
|
+
break;
|
|
888
|
+
}
|
|
889
|
+
const header = buffer[offset++];
|
|
890
|
+
let multiplier = 1;
|
|
891
|
+
let remainingLength = 0;
|
|
892
|
+
let encodedBytes = 0;
|
|
893
|
+
let encodedByte = 0;
|
|
894
|
+
|
|
895
|
+
do {
|
|
896
|
+
if (offset >= buffer.length) {
|
|
897
|
+
return {
|
|
898
|
+
packets,
|
|
899
|
+
remaining: buffer.subarray(packetStart),
|
|
900
|
+
};
|
|
901
|
+
}
|
|
902
|
+
encodedByte = buffer[offset++];
|
|
903
|
+
remainingLength += (encodedByte & 0x7f) * multiplier;
|
|
904
|
+
multiplier *= 128;
|
|
905
|
+
encodedBytes += 1;
|
|
906
|
+
} while ((encodedByte & 0x80) !== 0 && encodedBytes < 4);
|
|
907
|
+
|
|
908
|
+
if (offset + remainingLength > buffer.length) {
|
|
909
|
+
return {
|
|
910
|
+
packets,
|
|
911
|
+
remaining: buffer.subarray(packetStart),
|
|
912
|
+
};
|
|
913
|
+
}
|
|
914
|
+
|
|
915
|
+
const body = buffer.subarray(offset, offset + remainingLength);
|
|
916
|
+
packets.push({
|
|
917
|
+
header,
|
|
918
|
+
type: header >> 4,
|
|
919
|
+
flags: header & 0x0f,
|
|
920
|
+
body,
|
|
921
|
+
});
|
|
922
|
+
offset += remainingLength;
|
|
923
|
+
}
|
|
924
|
+
|
|
925
|
+
return {
|
|
926
|
+
packets,
|
|
927
|
+
remaining: buffer.subarray(offset),
|
|
928
|
+
};
|
|
929
|
+
}
|
|
930
|
+
|
|
931
|
+
function parseMqttPublishPacket(packet) {
|
|
932
|
+
const topic = decodeMqttString(packet.body, 0);
|
|
933
|
+
return {
|
|
934
|
+
topic: topic.value,
|
|
935
|
+
payload: packet.body.subarray(topic.nextOffset),
|
|
936
|
+
};
|
|
937
|
+
}
|
|
938
|
+
|
|
939
|
+
async function runMqttPublish(options) {
|
|
940
|
+
return new Promise((resolve, reject) => {
|
|
941
|
+
const socket = net.createConnection({
|
|
942
|
+
host: options.host,
|
|
943
|
+
port: options.port,
|
|
944
|
+
});
|
|
945
|
+
let buffer = Buffer.alloc(0);
|
|
946
|
+
let settled = false;
|
|
947
|
+
|
|
948
|
+
function cleanup() {
|
|
949
|
+
socket.removeAllListeners();
|
|
950
|
+
}
|
|
951
|
+
|
|
952
|
+
function fail(error) {
|
|
953
|
+
if (settled) {
|
|
954
|
+
return;
|
|
955
|
+
}
|
|
956
|
+
settled = true;
|
|
957
|
+
cleanup();
|
|
958
|
+
socket.destroy();
|
|
959
|
+
reject(error);
|
|
960
|
+
}
|
|
961
|
+
|
|
962
|
+
function finish(result) {
|
|
963
|
+
if (settled) {
|
|
964
|
+
return;
|
|
965
|
+
}
|
|
966
|
+
settled = true;
|
|
967
|
+
cleanup();
|
|
968
|
+
socket.end();
|
|
969
|
+
resolve(result);
|
|
970
|
+
}
|
|
971
|
+
|
|
972
|
+
socket.once("connect", () => {
|
|
973
|
+
socket.write(
|
|
974
|
+
createMqttConnectPacket({
|
|
975
|
+
clientId: options.clientId,
|
|
976
|
+
username: options.username,
|
|
977
|
+
password: options.password,
|
|
978
|
+
keepAliveSeconds: options.keepAliveSeconds,
|
|
979
|
+
}),
|
|
980
|
+
);
|
|
981
|
+
});
|
|
982
|
+
|
|
983
|
+
socket.on("data", (chunk) => {
|
|
984
|
+
buffer = Buffer.concat([buffer, Buffer.from(chunk)]);
|
|
985
|
+
const parsed = parseMqttPackets(buffer);
|
|
986
|
+
buffer = parsed.remaining;
|
|
987
|
+
for (const packet of parsed.packets) {
|
|
988
|
+
if (packet.type === 2) {
|
|
989
|
+
if (packet.body.length < 2 || packet.body[1] !== 0) {
|
|
990
|
+
fail(new Error(`MQTT CONNACK rejected with code ${packet.body[1] ?? "unknown"}.`));
|
|
991
|
+
return;
|
|
992
|
+
}
|
|
993
|
+
socket.write(
|
|
994
|
+
createMqttPublishPacket({
|
|
995
|
+
topic: options.topic,
|
|
996
|
+
payload: options.payload,
|
|
997
|
+
}),
|
|
998
|
+
);
|
|
999
|
+
socket.write(createMqttDisconnectPacket());
|
|
1000
|
+
finish({
|
|
1001
|
+
host: options.host,
|
|
1002
|
+
port: options.port,
|
|
1003
|
+
clientId: options.clientId,
|
|
1004
|
+
topic: options.topic,
|
|
1005
|
+
payloadBytes:
|
|
1006
|
+
typeof options.payload === "string"
|
|
1007
|
+
? Buffer.byteLength(options.payload)
|
|
1008
|
+
: toUint8Array(options.payload).length,
|
|
1009
|
+
});
|
|
1010
|
+
return;
|
|
1011
|
+
}
|
|
1012
|
+
}
|
|
1013
|
+
});
|
|
1014
|
+
|
|
1015
|
+
socket.once("error", fail);
|
|
1016
|
+
socket.once("close", (hadError) => {
|
|
1017
|
+
if (!settled && !hadError) {
|
|
1018
|
+
fail(new Error("MQTT connection closed before publish completed."));
|
|
1019
|
+
}
|
|
1020
|
+
});
|
|
1021
|
+
|
|
1022
|
+
if (options.timeoutMs !== undefined && options.timeoutMs !== null) {
|
|
1023
|
+
socket.setTimeout(
|
|
1024
|
+
assertNonNegativeInteger(options.timeoutMs, "MQTT timeoutMs"),
|
|
1025
|
+
() => fail(new Error("MQTT publish timed out.")),
|
|
1026
|
+
);
|
|
1027
|
+
}
|
|
1028
|
+
});
|
|
1029
|
+
}
|
|
1030
|
+
|
|
1031
|
+
async function runMqttSubscribeOnce(options) {
|
|
1032
|
+
const responseType = normalizeWebSocketResponseType(options.responseType);
|
|
1033
|
+
return new Promise((resolve, reject) => {
|
|
1034
|
+
const socket = net.createConnection({
|
|
1035
|
+
host: options.host,
|
|
1036
|
+
port: options.port,
|
|
1037
|
+
});
|
|
1038
|
+
let buffer = Buffer.alloc(0);
|
|
1039
|
+
let settled = false;
|
|
1040
|
+
const packetId = options.packetId ?? 1;
|
|
1041
|
+
|
|
1042
|
+
function cleanup() {
|
|
1043
|
+
socket.removeAllListeners();
|
|
1044
|
+
}
|
|
1045
|
+
|
|
1046
|
+
function fail(error) {
|
|
1047
|
+
if (settled) {
|
|
1048
|
+
return;
|
|
1049
|
+
}
|
|
1050
|
+
settled = true;
|
|
1051
|
+
cleanup();
|
|
1052
|
+
socket.destroy();
|
|
1053
|
+
reject(error);
|
|
1054
|
+
}
|
|
1055
|
+
|
|
1056
|
+
function finish(result) {
|
|
1057
|
+
if (settled) {
|
|
1058
|
+
return;
|
|
1059
|
+
}
|
|
1060
|
+
settled = true;
|
|
1061
|
+
cleanup();
|
|
1062
|
+
socket.end(createMqttDisconnectPacket());
|
|
1063
|
+
resolve(result);
|
|
1064
|
+
}
|
|
1065
|
+
|
|
1066
|
+
socket.once("connect", () => {
|
|
1067
|
+
socket.write(
|
|
1068
|
+
createMqttConnectPacket({
|
|
1069
|
+
clientId: options.clientId,
|
|
1070
|
+
username: options.username,
|
|
1071
|
+
password: options.password,
|
|
1072
|
+
keepAliveSeconds: options.keepAliveSeconds,
|
|
1073
|
+
}),
|
|
1074
|
+
);
|
|
1075
|
+
});
|
|
1076
|
+
|
|
1077
|
+
socket.on("data", (chunk) => {
|
|
1078
|
+
buffer = Buffer.concat([buffer, Buffer.from(chunk)]);
|
|
1079
|
+
const parsed = parseMqttPackets(buffer);
|
|
1080
|
+
buffer = parsed.remaining;
|
|
1081
|
+
for (const packet of parsed.packets) {
|
|
1082
|
+
if (packet.type === 2) {
|
|
1083
|
+
if (packet.body.length < 2 || packet.body[1] !== 0) {
|
|
1084
|
+
fail(new Error(`MQTT CONNACK rejected with code ${packet.body[1] ?? "unknown"}.`));
|
|
1085
|
+
return;
|
|
1086
|
+
}
|
|
1087
|
+
socket.write(
|
|
1088
|
+
createMqttSubscribePacket({
|
|
1089
|
+
packetId,
|
|
1090
|
+
topic: options.topic,
|
|
1091
|
+
qos: 0,
|
|
1092
|
+
}),
|
|
1093
|
+
);
|
|
1094
|
+
continue;
|
|
1095
|
+
}
|
|
1096
|
+
|
|
1097
|
+
if (packet.type === 9) {
|
|
1098
|
+
continue;
|
|
1099
|
+
}
|
|
1100
|
+
|
|
1101
|
+
if (packet.type === 3) {
|
|
1102
|
+
const published = parseMqttPublishPacket(packet);
|
|
1103
|
+
let body = null;
|
|
1104
|
+
if (responseType === "json") {
|
|
1105
|
+
body = JSON.parse(textDecoder.decode(published.payload));
|
|
1106
|
+
} else if (responseType === "utf8") {
|
|
1107
|
+
body = textDecoder.decode(published.payload);
|
|
1108
|
+
} else {
|
|
1109
|
+
body = new Uint8Array(published.payload);
|
|
1110
|
+
}
|
|
1111
|
+
finish({
|
|
1112
|
+
host: options.host,
|
|
1113
|
+
port: options.port,
|
|
1114
|
+
clientId: options.clientId,
|
|
1115
|
+
topic: published.topic,
|
|
1116
|
+
body,
|
|
1117
|
+
});
|
|
1118
|
+
return;
|
|
1119
|
+
}
|
|
1120
|
+
}
|
|
1121
|
+
});
|
|
1122
|
+
|
|
1123
|
+
socket.once("error", fail);
|
|
1124
|
+
socket.once("close", (hadError) => {
|
|
1125
|
+
if (!settled && !hadError) {
|
|
1126
|
+
fail(new Error("MQTT connection closed before subscribe completed."));
|
|
1127
|
+
}
|
|
1128
|
+
});
|
|
1129
|
+
|
|
1130
|
+
if (options.timeoutMs !== undefined && options.timeoutMs !== null) {
|
|
1131
|
+
socket.setTimeout(
|
|
1132
|
+
assertNonNegativeInteger(options.timeoutMs, "MQTT timeoutMs"),
|
|
1133
|
+
() => fail(new Error("MQTT subscribe timed out.")),
|
|
1134
|
+
);
|
|
1135
|
+
}
|
|
1136
|
+
});
|
|
1137
|
+
}
|
|
1138
|
+
|
|
1139
|
+
function normalizeExecEncoding(encoding) {
|
|
1140
|
+
const normalized = String(encoding ?? "utf8").trim().toLowerCase();
|
|
1141
|
+
if (!["utf8", "bytes"].includes(normalized)) {
|
|
1142
|
+
throw new Error(
|
|
1143
|
+
`Unsupported exec encoding "${encoding}". Expected "utf8" or "bytes".`,
|
|
1144
|
+
);
|
|
1145
|
+
}
|
|
1146
|
+
return normalized;
|
|
1147
|
+
}
|
|
1148
|
+
|
|
1149
|
+
function normalizeExecArgs(args) {
|
|
1150
|
+
if (args === undefined || args === null) {
|
|
1151
|
+
return [];
|
|
1152
|
+
}
|
|
1153
|
+
if (!Array.isArray(args)) {
|
|
1154
|
+
throw new TypeError("Executable args must be an array of strings.");
|
|
1155
|
+
}
|
|
1156
|
+
return args.map((arg) => String(arg));
|
|
1157
|
+
}
|
|
1158
|
+
|
|
1159
|
+
async function runExecFile(options) {
|
|
1160
|
+
const encoding = normalizeExecEncoding(options.encoding);
|
|
1161
|
+
const stdoutChunks = [];
|
|
1162
|
+
const stderrChunks = [];
|
|
1163
|
+
const spawnOptions = {
|
|
1164
|
+
cwd: options.cwd,
|
|
1165
|
+
env: options.env,
|
|
1166
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
1167
|
+
};
|
|
1168
|
+
|
|
1169
|
+
return new Promise((resolve, reject) => {
|
|
1170
|
+
const child = spawn(options.file, options.args, spawnOptions);
|
|
1171
|
+
let timeout = null;
|
|
1172
|
+
|
|
1173
|
+
child.once("error", reject);
|
|
1174
|
+
|
|
1175
|
+
if (options.timeoutMs !== undefined && options.timeoutMs !== null) {
|
|
1176
|
+
const duration = assertNonNegativeInteger(options.timeoutMs, "Exec timeoutMs");
|
|
1177
|
+
timeout = setTimeout(() => {
|
|
1178
|
+
child.kill("SIGTERM");
|
|
1179
|
+
}, duration);
|
|
1180
|
+
}
|
|
1181
|
+
|
|
1182
|
+
child.stdout.on("data", (chunk) => stdoutChunks.push(Buffer.from(chunk)));
|
|
1183
|
+
child.stderr.on("data", (chunk) => stderrChunks.push(Buffer.from(chunk)));
|
|
1184
|
+
child.once("close", (exitCode, signal) => {
|
|
1185
|
+
if (timeout) {
|
|
1186
|
+
clearTimeout(timeout);
|
|
1187
|
+
}
|
|
1188
|
+
const stdoutBuffer = Buffer.concat(stdoutChunks);
|
|
1189
|
+
const stderrBuffer = Buffer.concat(stderrChunks);
|
|
1190
|
+
resolve({
|
|
1191
|
+
exitCode,
|
|
1192
|
+
signal,
|
|
1193
|
+
stdout:
|
|
1194
|
+
encoding === "bytes"
|
|
1195
|
+
? new Uint8Array(stdoutBuffer)
|
|
1196
|
+
: stdoutBuffer.toString("utf8"),
|
|
1197
|
+
stderr:
|
|
1198
|
+
encoding === "bytes"
|
|
1199
|
+
? new Uint8Array(stderrBuffer)
|
|
1200
|
+
: stderrBuffer.toString("utf8"),
|
|
1201
|
+
});
|
|
1202
|
+
});
|
|
1203
|
+
|
|
1204
|
+
if (options.input !== undefined && options.input !== null) {
|
|
1205
|
+
if (typeof options.input === "string") {
|
|
1206
|
+
child.stdin.end(options.input);
|
|
1207
|
+
} else {
|
|
1208
|
+
child.stdin.end(Buffer.from(toUint8Array(options.input)));
|
|
1209
|
+
}
|
|
1210
|
+
} else {
|
|
1211
|
+
child.stdin.end();
|
|
1212
|
+
}
|
|
1213
|
+
});
|
|
1214
|
+
}
|
|
1215
|
+
|
|
1216
|
+
function createInMemoryContextStore() {
|
|
1217
|
+
const scopes = new Map();
|
|
1218
|
+
|
|
1219
|
+
function getScope(scopeId) {
|
|
1220
|
+
if (!scopes.has(scopeId)) {
|
|
1221
|
+
scopes.set(scopeId, new Map());
|
|
1222
|
+
}
|
|
1223
|
+
return scopes.get(scopeId);
|
|
1224
|
+
}
|
|
1225
|
+
|
|
1226
|
+
return {
|
|
1227
|
+
async get(scope, key) {
|
|
1228
|
+
return cloneValue(getScope(scope).get(key));
|
|
1229
|
+
},
|
|
1230
|
+
async set(scope, key, value) {
|
|
1231
|
+
getScope(scope).set(key, cloneValue(value));
|
|
1232
|
+
},
|
|
1233
|
+
async delete(scope, key) {
|
|
1234
|
+
return getScope(scope).delete(key);
|
|
1235
|
+
},
|
|
1236
|
+
async listKeys(scope) {
|
|
1237
|
+
return Array.from(getScope(scope).keys()).sort();
|
|
1238
|
+
},
|
|
1239
|
+
async listScopes() {
|
|
1240
|
+
return Array.from(scopes.keys()).sort();
|
|
1241
|
+
},
|
|
1242
|
+
};
|
|
1243
|
+
}
|
|
1244
|
+
|
|
1245
|
+
function assertContextStore(store) {
|
|
1246
|
+
if (!store || typeof store !== "object") {
|
|
1247
|
+
throw new TypeError("contextStore must be an object when provided.");
|
|
1248
|
+
}
|
|
1249
|
+
|
|
1250
|
+
const requiredMethods = ["get", "set", "delete", "listKeys", "listScopes"];
|
|
1251
|
+
for (const methodName of requiredMethods) {
|
|
1252
|
+
if (typeof store[methodName] !== "function") {
|
|
1253
|
+
throw new TypeError(
|
|
1254
|
+
`contextStore must implement an async ${methodName}() method.`,
|
|
1255
|
+
);
|
|
1256
|
+
}
|
|
1257
|
+
}
|
|
1258
|
+
|
|
1259
|
+
return store;
|
|
1260
|
+
}
|
|
1261
|
+
|
|
1262
|
+
function createFileContextStore(filePath) {
|
|
1263
|
+
const resolvedFilePath = path.resolve(String(filePath));
|
|
1264
|
+
let cache = null;
|
|
1265
|
+
let writeChain = Promise.resolve();
|
|
1266
|
+
|
|
1267
|
+
async function loadState() {
|
|
1268
|
+
if (cache !== null) {
|
|
1269
|
+
return cache;
|
|
1270
|
+
}
|
|
1271
|
+
|
|
1272
|
+
try {
|
|
1273
|
+
const file = await readFile(resolvedFilePath, "utf8");
|
|
1274
|
+
const parsed = JSON.parse(file);
|
|
1275
|
+
cache = parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {};
|
|
1276
|
+
} catch (error) {
|
|
1277
|
+
if (error?.code === "ENOENT") {
|
|
1278
|
+
cache = {};
|
|
1279
|
+
} else {
|
|
1280
|
+
throw error;
|
|
1281
|
+
}
|
|
1282
|
+
}
|
|
1283
|
+
|
|
1284
|
+
return cache;
|
|
1285
|
+
}
|
|
1286
|
+
|
|
1287
|
+
async function flushState(state) {
|
|
1288
|
+
const payload = JSON.stringify(state, null, 2) + "\n";
|
|
1289
|
+
await mkdir(path.dirname(resolvedFilePath), { recursive: true });
|
|
1290
|
+
await writeFile(resolvedFilePath, payload, "utf8");
|
|
1291
|
+
}
|
|
1292
|
+
|
|
1293
|
+
function getScope(state, scopeId) {
|
|
1294
|
+
if (!state[scopeId] || typeof state[scopeId] !== "object") {
|
|
1295
|
+
state[scopeId] = {};
|
|
1296
|
+
}
|
|
1297
|
+
return state[scopeId];
|
|
1298
|
+
}
|
|
1299
|
+
|
|
1300
|
+
return {
|
|
1301
|
+
async get(scope, key) {
|
|
1302
|
+
const state = await loadState();
|
|
1303
|
+
return cloneValue(getScope(state, scope)[key]);
|
|
1304
|
+
},
|
|
1305
|
+
async set(scope, key, value) {
|
|
1306
|
+
writeChain = writeChain.then(async () => {
|
|
1307
|
+
const state = await loadState();
|
|
1308
|
+
getScope(state, scope)[key] = cloneValue(value);
|
|
1309
|
+
await flushState(state);
|
|
1310
|
+
});
|
|
1311
|
+
return writeChain;
|
|
1312
|
+
},
|
|
1313
|
+
async delete(scope, key) {
|
|
1314
|
+
let deleted = false;
|
|
1315
|
+
writeChain = writeChain.then(async () => {
|
|
1316
|
+
const state = await loadState();
|
|
1317
|
+
const scopeState = getScope(state, scope);
|
|
1318
|
+
deleted = delete scopeState[key];
|
|
1319
|
+
await flushState(state);
|
|
1320
|
+
});
|
|
1321
|
+
await writeChain;
|
|
1322
|
+
return deleted;
|
|
1323
|
+
},
|
|
1324
|
+
async listKeys(scope) {
|
|
1325
|
+
const state = await loadState();
|
|
1326
|
+
return Object.keys(getScope(state, scope)).sort();
|
|
1327
|
+
},
|
|
1328
|
+
async listScopes() {
|
|
1329
|
+
const state = await loadState();
|
|
1330
|
+
return Object.keys(state).sort();
|
|
1331
|
+
},
|
|
1332
|
+
};
|
|
1333
|
+
}
|
|
1334
|
+
|
|
1335
|
+
export class HostCapabilityError extends Error {
|
|
1336
|
+
constructor(message, options = {}) {
|
|
1337
|
+
super(message);
|
|
1338
|
+
this.name = "HostCapabilityError";
|
|
1339
|
+
this.code = options.code ?? "host-capability-error";
|
|
1340
|
+
this.capability = options.capability ?? null;
|
|
1341
|
+
this.operation = options.operation ?? null;
|
|
1342
|
+
}
|
|
1343
|
+
}
|
|
1344
|
+
|
|
1345
|
+
export class HostFilesystemScopeError extends Error {
|
|
1346
|
+
constructor(message, options = {}) {
|
|
1347
|
+
super(message);
|
|
1348
|
+
this.name = "HostFilesystemScopeError";
|
|
1349
|
+
this.code = options.code ?? "filesystem-scope-violation";
|
|
1350
|
+
this.requestedPath = options.requestedPath ?? null;
|
|
1351
|
+
this.filesystemRoot = options.filesystemRoot ?? null;
|
|
1352
|
+
}
|
|
1353
|
+
}
|
|
1354
|
+
|
|
1355
|
+
export class NodeHost {
|
|
1356
|
+
constructor(options = {}) {
|
|
1357
|
+
this.runtimeTarget = RuntimeTarget.NODE;
|
|
1358
|
+
this.filesystemRoot = normalizeFilesystemRoot(
|
|
1359
|
+
options.filesystemRoot ?? options.fsRoot,
|
|
1360
|
+
);
|
|
1361
|
+
this.allowedHttpOrigins = normalizeAllowedOrigins(options.allowedHttpOrigins);
|
|
1362
|
+
this.allowedWebSocketOrigins = normalizeAllowedWebSocketOrigins(
|
|
1363
|
+
options.allowedWebSocketOrigins,
|
|
1364
|
+
);
|
|
1365
|
+
this.allowedCommands = normalizeAllowedCommands(options.allowedCommands);
|
|
1366
|
+
this.allowedMqttHosts = normalizeAllowedHosts(
|
|
1367
|
+
options.allowedMqttHosts,
|
|
1368
|
+
"allowedMqttHosts",
|
|
1369
|
+
);
|
|
1370
|
+
this.allowedMqttPorts = normalizeAllowedPorts(
|
|
1371
|
+
options.allowedMqttPorts,
|
|
1372
|
+
"allowedMqttPorts",
|
|
1373
|
+
);
|
|
1374
|
+
this.allowedTcpHosts = normalizeAllowedHosts(
|
|
1375
|
+
options.allowedTcpHosts,
|
|
1376
|
+
"allowedTcpHosts",
|
|
1377
|
+
);
|
|
1378
|
+
this.allowedTcpPorts = normalizeAllowedPorts(
|
|
1379
|
+
options.allowedTcpPorts,
|
|
1380
|
+
"allowedTcpPorts",
|
|
1381
|
+
);
|
|
1382
|
+
this.allowedUdpHosts = normalizeAllowedHosts(
|
|
1383
|
+
options.allowedUdpHosts,
|
|
1384
|
+
"allowedUdpHosts",
|
|
1385
|
+
);
|
|
1386
|
+
this.allowedUdpPorts = normalizeAllowedPorts(
|
|
1387
|
+
options.allowedUdpPorts,
|
|
1388
|
+
"allowedUdpPorts",
|
|
1389
|
+
);
|
|
1390
|
+
this.allowedTlsHosts = normalizeAllowedHosts(
|
|
1391
|
+
options.allowedTlsHosts,
|
|
1392
|
+
"allowedTlsHosts",
|
|
1393
|
+
);
|
|
1394
|
+
this.allowedTlsPorts = normalizeAllowedPorts(
|
|
1395
|
+
options.allowedTlsPorts,
|
|
1396
|
+
"allowedTlsPorts",
|
|
1397
|
+
);
|
|
1398
|
+
this.fetch = options.fetch ?? globalThis.fetch;
|
|
1399
|
+
this.WebSocket = options.WebSocket ?? globalThis.WebSocket;
|
|
1400
|
+
if (typeof this.fetch !== "function") {
|
|
1401
|
+
throw new TypeError(
|
|
1402
|
+
"A fetch implementation is required to create a Node host.",
|
|
1403
|
+
);
|
|
1404
|
+
}
|
|
1405
|
+
|
|
1406
|
+
this._supportedCapabilities = new Set(NodeHostSupportedCapabilities);
|
|
1407
|
+
this._grantedCapabilities = normalizeGrantedCapabilities(options);
|
|
1408
|
+
this._contextStore = options.contextStore
|
|
1409
|
+
? assertContextStore(options.contextStore)
|
|
1410
|
+
: options.contextFilePath
|
|
1411
|
+
? createFileContextStore(options.contextFilePath)
|
|
1412
|
+
: createInMemoryContextStore();
|
|
1413
|
+
|
|
1414
|
+
this.clock = Object.freeze({
|
|
1415
|
+
now: () => this.#withCapability("clock", "clock.now", () => Date.now()),
|
|
1416
|
+
monotonicNow: () =>
|
|
1417
|
+
this.#withCapability("clock", "clock.monotonicNow", () => performance.now()),
|
|
1418
|
+
nowIso: () =>
|
|
1419
|
+
this.#withCapability("clock", "clock.nowIso", () =>
|
|
1420
|
+
new Date(Date.now()).toISOString(),
|
|
1421
|
+
),
|
|
1422
|
+
});
|
|
1423
|
+
|
|
1424
|
+
this.random = Object.freeze({
|
|
1425
|
+
bytes: async (length) =>
|
|
1426
|
+
this.#withCapability("random", "random.bytes", async () => {
|
|
1427
|
+
const size = assertNonNegativeInteger(length, "Random byte length");
|
|
1428
|
+
return randomBytes(size);
|
|
1429
|
+
}),
|
|
1430
|
+
});
|
|
1431
|
+
|
|
1432
|
+
this.timers = Object.freeze({
|
|
1433
|
+
delay: async (ms, options = {}) =>
|
|
1434
|
+
this.#withCapability("timers", "timers.delay", async () => {
|
|
1435
|
+
const duration = assertNonNegativeInteger(ms, "Timer duration");
|
|
1436
|
+
return new Promise((resolve, reject) => {
|
|
1437
|
+
const onAbort = () => {
|
|
1438
|
+
clearTimeout(timer);
|
|
1439
|
+
reject(new Error("Timer delay aborted."));
|
|
1440
|
+
};
|
|
1441
|
+
const signal = options.signal ?? null;
|
|
1442
|
+
if (signal?.aborted) {
|
|
1443
|
+
reject(new Error("Timer delay aborted."));
|
|
1444
|
+
return;
|
|
1445
|
+
}
|
|
1446
|
+
const timer = setTimeout(() => {
|
|
1447
|
+
signal?.removeEventListener("abort", onAbort);
|
|
1448
|
+
resolve();
|
|
1449
|
+
}, duration);
|
|
1450
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
1451
|
+
});
|
|
1452
|
+
}),
|
|
1453
|
+
setTimeout: (callback, ms, ...args) =>
|
|
1454
|
+
this.#withCapability("timers", "timers.setTimeout", () => {
|
|
1455
|
+
const duration = assertNonNegativeInteger(ms, "Timeout duration");
|
|
1456
|
+
return setTimeout(callback, duration, ...args);
|
|
1457
|
+
}),
|
|
1458
|
+
clearTimeout: (handle) => clearTimeout(handle),
|
|
1459
|
+
setInterval: (callback, ms, ...args) =>
|
|
1460
|
+
this.#withCapability("timers", "timers.setInterval", () => {
|
|
1461
|
+
const duration = assertNonNegativeInteger(ms, "Interval duration");
|
|
1462
|
+
return setInterval(callback, duration, ...args);
|
|
1463
|
+
}),
|
|
1464
|
+
clearInterval: (handle) => clearInterval(handle),
|
|
1465
|
+
});
|
|
1466
|
+
|
|
1467
|
+
this.schedule = Object.freeze({
|
|
1468
|
+
parse: (expression) =>
|
|
1469
|
+
this.#withCapability("schedule_cron", "schedule.parse", () =>
|
|
1470
|
+
parseCronExpression(expression),
|
|
1471
|
+
),
|
|
1472
|
+
matches: (expression, date = Date.now()) =>
|
|
1473
|
+
this.#withCapability("schedule_cron", "schedule.matches", () =>
|
|
1474
|
+
matchesCronExpression(expression, date),
|
|
1475
|
+
),
|
|
1476
|
+
next: (expression, from = Date.now()) =>
|
|
1477
|
+
this.#withCapability("schedule_cron", "schedule.next", () =>
|
|
1478
|
+
nextCronOccurrence(expression, from),
|
|
1479
|
+
),
|
|
1480
|
+
});
|
|
1481
|
+
|
|
1482
|
+
this.http = Object.freeze({
|
|
1483
|
+
request: async (requestOptions = {}) =>
|
|
1484
|
+
this.#withCapability("http", "http.request", async () => {
|
|
1485
|
+
const url = new URL(assertNonEmptyString(requestOptions.url, "HTTP url"));
|
|
1486
|
+
if (!["http:", "https:"].includes(url.protocol)) {
|
|
1487
|
+
throw new Error(
|
|
1488
|
+
`Unsupported HTTP protocol "${url.protocol}". Expected http: or https:.`,
|
|
1489
|
+
);
|
|
1490
|
+
}
|
|
1491
|
+
if (
|
|
1492
|
+
this.allowedHttpOrigins &&
|
|
1493
|
+
!this.allowedHttpOrigins.has(url.origin)
|
|
1494
|
+
) {
|
|
1495
|
+
throw new Error(`HTTP origin "${url.origin}" is not permitted by this host.`);
|
|
1496
|
+
}
|
|
1497
|
+
|
|
1498
|
+
const timeout = createTimeoutSignal(requestOptions.timeoutMs);
|
|
1499
|
+
try {
|
|
1500
|
+
const response = await this.fetch(url, {
|
|
1501
|
+
method: requestOptions.method ?? "GET",
|
|
1502
|
+
headers: requestOptions.headers,
|
|
1503
|
+
body: normalizeRequestBody(requestOptions.body),
|
|
1504
|
+
signal: resolveSignals(requestOptions.signal, timeout.signal),
|
|
1505
|
+
});
|
|
1506
|
+
const responseType = normalizeResponseType(requestOptions.responseType);
|
|
1507
|
+
let body;
|
|
1508
|
+
if (responseType === "json") {
|
|
1509
|
+
body = await response.json();
|
|
1510
|
+
} else if (responseType === "text") {
|
|
1511
|
+
body = await response.text();
|
|
1512
|
+
} else {
|
|
1513
|
+
body = new Uint8Array(await response.arrayBuffer());
|
|
1514
|
+
}
|
|
1515
|
+
return {
|
|
1516
|
+
url: response.url || url.toString(),
|
|
1517
|
+
status: response.status,
|
|
1518
|
+
statusText: response.statusText,
|
|
1519
|
+
ok: response.ok,
|
|
1520
|
+
headers: headersToObject(response.headers),
|
|
1521
|
+
body,
|
|
1522
|
+
};
|
|
1523
|
+
} finally {
|
|
1524
|
+
timeout.dispose();
|
|
1525
|
+
}
|
|
1526
|
+
}),
|
|
1527
|
+
});
|
|
1528
|
+
|
|
1529
|
+
this.websocket = Object.freeze({
|
|
1530
|
+
exchange: async (websocketOptions = {}) =>
|
|
1531
|
+
this.#withCapability("websocket", "websocket.exchange", async () => {
|
|
1532
|
+
const url = normalizeWebSocketUrl(websocketOptions.url);
|
|
1533
|
+
if (
|
|
1534
|
+
this.allowedWebSocketOrigins &&
|
|
1535
|
+
!this.allowedWebSocketOrigins.has(url.origin)
|
|
1536
|
+
) {
|
|
1537
|
+
throw new Error(
|
|
1538
|
+
`WebSocket origin "${url.origin}" is not permitted by this host.`,
|
|
1539
|
+
);
|
|
1540
|
+
}
|
|
1541
|
+
return runWebSocketExchange({
|
|
1542
|
+
url: url.toString(),
|
|
1543
|
+
protocols: websocketOptions.protocols,
|
|
1544
|
+
message: websocketOptions.message,
|
|
1545
|
+
responseType: websocketOptions.responseType,
|
|
1546
|
+
timeoutMs: websocketOptions.timeoutMs,
|
|
1547
|
+
expectResponse: websocketOptions.expectResponse,
|
|
1548
|
+
WebSocketImpl: websocketOptions.WebSocketImpl ?? this.WebSocket,
|
|
1549
|
+
});
|
|
1550
|
+
}),
|
|
1551
|
+
});
|
|
1552
|
+
|
|
1553
|
+
this.mqtt = Object.freeze({
|
|
1554
|
+
publish: async (mqttOptions = {}) =>
|
|
1555
|
+
this.#withCapability("mqtt", "mqtt.publish", async () => {
|
|
1556
|
+
const { host, port } = assertNetworkTargetAllowed(
|
|
1557
|
+
"MQTT",
|
|
1558
|
+
mqttOptions.host,
|
|
1559
|
+
mqttOptions.port,
|
|
1560
|
+
this.allowedMqttHosts,
|
|
1561
|
+
this.allowedMqttPorts,
|
|
1562
|
+
);
|
|
1563
|
+
return runMqttPublish({
|
|
1564
|
+
host,
|
|
1565
|
+
port,
|
|
1566
|
+
clientId:
|
|
1567
|
+
mqttOptions.clientId ?? `space-data-module-sdk-${Date.now()}`,
|
|
1568
|
+
topic: assertNonEmptyString(mqttOptions.topic, "MQTT topic"),
|
|
1569
|
+
payload: mqttOptions.payload ?? "",
|
|
1570
|
+
username: mqttOptions.username,
|
|
1571
|
+
password: mqttOptions.password,
|
|
1572
|
+
keepAliveSeconds: mqttOptions.keepAliveSeconds,
|
|
1573
|
+
timeoutMs: mqttOptions.timeoutMs,
|
|
1574
|
+
});
|
|
1575
|
+
}),
|
|
1576
|
+
subscribeOnce: async (mqttOptions = {}) =>
|
|
1577
|
+
this.#withCapability("mqtt", "mqtt.subscribeOnce", async () => {
|
|
1578
|
+
const { host, port } = assertNetworkTargetAllowed(
|
|
1579
|
+
"MQTT",
|
|
1580
|
+
mqttOptions.host,
|
|
1581
|
+
mqttOptions.port,
|
|
1582
|
+
this.allowedMqttHosts,
|
|
1583
|
+
this.allowedMqttPorts,
|
|
1584
|
+
);
|
|
1585
|
+
return runMqttSubscribeOnce({
|
|
1586
|
+
host,
|
|
1587
|
+
port,
|
|
1588
|
+
clientId:
|
|
1589
|
+
mqttOptions.clientId ?? `space-data-module-sdk-${Date.now()}`,
|
|
1590
|
+
topic: assertNonEmptyString(mqttOptions.topic, "MQTT topic"),
|
|
1591
|
+
username: mqttOptions.username,
|
|
1592
|
+
password: mqttOptions.password,
|
|
1593
|
+
keepAliveSeconds: mqttOptions.keepAliveSeconds,
|
|
1594
|
+
timeoutMs: mqttOptions.timeoutMs,
|
|
1595
|
+
responseType: mqttOptions.responseType,
|
|
1596
|
+
packetId: mqttOptions.packetId,
|
|
1597
|
+
});
|
|
1598
|
+
}),
|
|
1599
|
+
});
|
|
1600
|
+
|
|
1601
|
+
this.filesystem = Object.freeze({
|
|
1602
|
+
resolvePath: (targetPath) =>
|
|
1603
|
+
this.#withCapability("filesystem", "filesystem.resolvePath", () =>
|
|
1604
|
+
this.#resolveFilesystemPath(targetPath),
|
|
1605
|
+
),
|
|
1606
|
+
readFile: async (targetPath, options = {}) =>
|
|
1607
|
+
this.#withCapability("filesystem", "filesystem.readFile", async () => {
|
|
1608
|
+
const resolvedPath = this.#resolveFilesystemPath(targetPath);
|
|
1609
|
+
const file = await readFile(resolvedPath);
|
|
1610
|
+
if (options.encoding) {
|
|
1611
|
+
return file.toString(options.encoding);
|
|
1612
|
+
}
|
|
1613
|
+
return new Uint8Array(file);
|
|
1614
|
+
}),
|
|
1615
|
+
writeFile: async (targetPath, value, options = {}) =>
|
|
1616
|
+
this.#withCapability("filesystem", "filesystem.writeFile", async () => {
|
|
1617
|
+
const resolvedPath = this.#resolveFilesystemPath(targetPath);
|
|
1618
|
+
await mkdir(path.dirname(resolvedPath), { recursive: true });
|
|
1619
|
+
await writeFile(
|
|
1620
|
+
resolvedPath,
|
|
1621
|
+
typeof value === "string" ? value : Buffer.from(toUint8Array(value)),
|
|
1622
|
+
options.encoding && typeof value === "string" ? options.encoding : undefined,
|
|
1623
|
+
);
|
|
1624
|
+
return { path: resolvedPath };
|
|
1625
|
+
}),
|
|
1626
|
+
appendFile: async (targetPath, value, options = {}) =>
|
|
1627
|
+
this.#withCapability("filesystem", "filesystem.appendFile", async () => {
|
|
1628
|
+
const resolvedPath = this.#resolveFilesystemPath(targetPath);
|
|
1629
|
+
await mkdir(path.dirname(resolvedPath), { recursive: true });
|
|
1630
|
+
const existing = await readFile(resolvedPath).catch((error) => {
|
|
1631
|
+
if (error?.code === "ENOENT") {
|
|
1632
|
+
return Buffer.alloc(0);
|
|
1633
|
+
}
|
|
1634
|
+
throw error;
|
|
1635
|
+
});
|
|
1636
|
+
const nextChunk =
|
|
1637
|
+
typeof value === "string"
|
|
1638
|
+
? Buffer.from(value, options.encoding ?? "utf8")
|
|
1639
|
+
: Buffer.from(toUint8Array(value));
|
|
1640
|
+
await writeFile(resolvedPath, Buffer.concat([existing, nextChunk]));
|
|
1641
|
+
return { path: resolvedPath };
|
|
1642
|
+
}),
|
|
1643
|
+
deleteFile: async (targetPath) =>
|
|
1644
|
+
this.#withCapability("filesystem", "filesystem.deleteFile", async () => {
|
|
1645
|
+
const resolvedPath = this.#resolveFilesystemPath(targetPath);
|
|
1646
|
+
await rm(resolvedPath, { force: true });
|
|
1647
|
+
return { path: resolvedPath };
|
|
1648
|
+
}),
|
|
1649
|
+
mkdir: async (targetPath, options = {}) =>
|
|
1650
|
+
this.#withCapability("filesystem", "filesystem.mkdir", async () => {
|
|
1651
|
+
const resolvedPath = this.#resolveFilesystemPath(targetPath);
|
|
1652
|
+
await mkdir(resolvedPath, {
|
|
1653
|
+
recursive: options.recursive ?? true,
|
|
1654
|
+
});
|
|
1655
|
+
return { path: resolvedPath };
|
|
1656
|
+
}),
|
|
1657
|
+
readdir: async (targetPath = ".") =>
|
|
1658
|
+
this.#withCapability("filesystem", "filesystem.readdir", async () => {
|
|
1659
|
+
const resolvedPath = this.#resolveFilesystemPath(targetPath);
|
|
1660
|
+
const entries = await readdir(resolvedPath, { withFileTypes: true });
|
|
1661
|
+
return entries
|
|
1662
|
+
.map((entry) => ({
|
|
1663
|
+
name: entry.name,
|
|
1664
|
+
isFile: entry.isFile(),
|
|
1665
|
+
isDirectory: entry.isDirectory(),
|
|
1666
|
+
}))
|
|
1667
|
+
.sort((left, right) => left.name.localeCompare(right.name));
|
|
1668
|
+
}),
|
|
1669
|
+
stat: async (targetPath) =>
|
|
1670
|
+
this.#withCapability("filesystem", "filesystem.stat", async () => {
|
|
1671
|
+
const resolvedPath = this.#resolveFilesystemPath(targetPath);
|
|
1672
|
+
const metadata = await stat(resolvedPath);
|
|
1673
|
+
return {
|
|
1674
|
+
path: resolvedPath,
|
|
1675
|
+
size: metadata.size,
|
|
1676
|
+
isFile: metadata.isFile(),
|
|
1677
|
+
isDirectory: metadata.isDirectory(),
|
|
1678
|
+
ctimeMs: metadata.ctimeMs,
|
|
1679
|
+
mtimeMs: metadata.mtimeMs,
|
|
1680
|
+
};
|
|
1681
|
+
}),
|
|
1682
|
+
rename: async (fromPath, toPath) =>
|
|
1683
|
+
this.#withCapability("filesystem", "filesystem.rename", async () => {
|
|
1684
|
+
const resolvedFrom = this.#resolveFilesystemPath(fromPath);
|
|
1685
|
+
const resolvedTo = this.#resolveFilesystemPath(toPath);
|
|
1686
|
+
await mkdir(path.dirname(resolvedTo), { recursive: true });
|
|
1687
|
+
await rename(resolvedFrom, resolvedTo);
|
|
1688
|
+
return { from: resolvedFrom, to: resolvedTo };
|
|
1689
|
+
}),
|
|
1690
|
+
});
|
|
1691
|
+
|
|
1692
|
+
this.tcp = Object.freeze({
|
|
1693
|
+
request: async (tcpOptions = {}) =>
|
|
1694
|
+
this.#withCapability("tcp", "tcp.request", async () => {
|
|
1695
|
+
const { host, port } = assertNetworkTargetAllowed(
|
|
1696
|
+
"TCP",
|
|
1697
|
+
tcpOptions.host,
|
|
1698
|
+
tcpOptions.port,
|
|
1699
|
+
this.allowedTcpHosts,
|
|
1700
|
+
this.allowedTcpPorts,
|
|
1701
|
+
);
|
|
1702
|
+
return runTcpRequest({
|
|
1703
|
+
host,
|
|
1704
|
+
port,
|
|
1705
|
+
data: tcpOptions.data,
|
|
1706
|
+
timeoutMs: tcpOptions.timeoutMs,
|
|
1707
|
+
responseEncoding: tcpOptions.responseEncoding,
|
|
1708
|
+
});
|
|
1709
|
+
}),
|
|
1710
|
+
});
|
|
1711
|
+
|
|
1712
|
+
this.udp = Object.freeze({
|
|
1713
|
+
request: async (udpOptions = {}) =>
|
|
1714
|
+
this.#withCapability("udp", "udp.request", async () => {
|
|
1715
|
+
const { host, port } = assertNetworkTargetAllowed(
|
|
1716
|
+
"UDP",
|
|
1717
|
+
udpOptions.host,
|
|
1718
|
+
udpOptions.port,
|
|
1719
|
+
this.allowedUdpHosts,
|
|
1720
|
+
this.allowedUdpPorts,
|
|
1721
|
+
);
|
|
1722
|
+
return runUdpRequest({
|
|
1723
|
+
host,
|
|
1724
|
+
port,
|
|
1725
|
+
data: udpOptions.data,
|
|
1726
|
+
timeoutMs: udpOptions.timeoutMs,
|
|
1727
|
+
responseEncoding: udpOptions.responseEncoding,
|
|
1728
|
+
bindAddress: udpOptions.bindAddress,
|
|
1729
|
+
bindPort:
|
|
1730
|
+
udpOptions.bindPort === undefined
|
|
1731
|
+
? undefined
|
|
1732
|
+
: assertPort(udpOptions.bindPort, "UDP bindPort", {
|
|
1733
|
+
allowZero: true,
|
|
1734
|
+
}),
|
|
1735
|
+
type: udpOptions.type,
|
|
1736
|
+
expectResponse: udpOptions.expectResponse,
|
|
1737
|
+
});
|
|
1738
|
+
}),
|
|
1739
|
+
});
|
|
1740
|
+
|
|
1741
|
+
this.tls = Object.freeze({
|
|
1742
|
+
request: async (tlsOptions = {}) =>
|
|
1743
|
+
this.#withCapability("tls", "tls.request", async () => {
|
|
1744
|
+
const { host, port } = assertNetworkTargetAllowed(
|
|
1745
|
+
"TLS",
|
|
1746
|
+
tlsOptions.host,
|
|
1747
|
+
tlsOptions.port,
|
|
1748
|
+
this.allowedTlsHosts,
|
|
1749
|
+
this.allowedTlsPorts,
|
|
1750
|
+
);
|
|
1751
|
+
return runTlsRequest({
|
|
1752
|
+
host,
|
|
1753
|
+
port,
|
|
1754
|
+
data: tlsOptions.data,
|
|
1755
|
+
timeoutMs: tlsOptions.timeoutMs,
|
|
1756
|
+
responseEncoding: tlsOptions.responseEncoding,
|
|
1757
|
+
ca: tlsOptions.ca,
|
|
1758
|
+
cert: tlsOptions.cert,
|
|
1759
|
+
key: tlsOptions.key,
|
|
1760
|
+
rejectUnauthorized: tlsOptions.rejectUnauthorized,
|
|
1761
|
+
servername: tlsOptions.servername,
|
|
1762
|
+
});
|
|
1763
|
+
}),
|
|
1764
|
+
});
|
|
1765
|
+
|
|
1766
|
+
this.exec = Object.freeze({
|
|
1767
|
+
execFile: async (execOptions = {}) =>
|
|
1768
|
+
this.#withCapability("process_exec", "exec.execFile", async () => {
|
|
1769
|
+
const file = assertNonEmptyString(execOptions.file, "Executable path");
|
|
1770
|
+
if (
|
|
1771
|
+
this.allowedCommands &&
|
|
1772
|
+
!this.allowedCommands.has(file)
|
|
1773
|
+
) {
|
|
1774
|
+
throw new Error(
|
|
1775
|
+
`Executable "${file}" is not permitted by this host.`,
|
|
1776
|
+
);
|
|
1777
|
+
}
|
|
1778
|
+
|
|
1779
|
+
return runExecFile({
|
|
1780
|
+
file,
|
|
1781
|
+
args: normalizeExecArgs(execOptions.args),
|
|
1782
|
+
cwd: execOptions.cwd ?? this.filesystemRoot,
|
|
1783
|
+
env:
|
|
1784
|
+
execOptions.env && typeof execOptions.env === "object"
|
|
1785
|
+
? { ...process.env, ...execOptions.env }
|
|
1786
|
+
: process.env,
|
|
1787
|
+
input: execOptions.input,
|
|
1788
|
+
timeoutMs: execOptions.timeoutMs,
|
|
1789
|
+
encoding: execOptions.encoding,
|
|
1790
|
+
});
|
|
1791
|
+
}),
|
|
1792
|
+
});
|
|
1793
|
+
|
|
1794
|
+
this.context = Object.freeze({
|
|
1795
|
+
get: async (scope, key) =>
|
|
1796
|
+
this.#withCapability("context_read", "context.get", async () =>
|
|
1797
|
+
this._contextStore.get(
|
|
1798
|
+
assertNonEmptyString(scope ?? "global", "Context scope"),
|
|
1799
|
+
assertNonEmptyString(key, "Context key"),
|
|
1800
|
+
),
|
|
1801
|
+
),
|
|
1802
|
+
set: async (scope, key, value) =>
|
|
1803
|
+
this.#withCapability("context_write", "context.set", async () =>
|
|
1804
|
+
this._contextStore.set(
|
|
1805
|
+
assertNonEmptyString(scope ?? "global", "Context scope"),
|
|
1806
|
+
assertNonEmptyString(key, "Context key"),
|
|
1807
|
+
value,
|
|
1808
|
+
),
|
|
1809
|
+
),
|
|
1810
|
+
delete: async (scope, key) =>
|
|
1811
|
+
this.#withCapability("context_write", "context.delete", async () =>
|
|
1812
|
+
this._contextStore.delete(
|
|
1813
|
+
assertNonEmptyString(scope ?? "global", "Context scope"),
|
|
1814
|
+
assertNonEmptyString(key, "Context key"),
|
|
1815
|
+
),
|
|
1816
|
+
),
|
|
1817
|
+
listKeys: async (scope = "global") =>
|
|
1818
|
+
this.#withCapability("context_read", "context.listKeys", async () =>
|
|
1819
|
+
this._contextStore.listKeys(
|
|
1820
|
+
assertNonEmptyString(scope, "Context scope"),
|
|
1821
|
+
),
|
|
1822
|
+
),
|
|
1823
|
+
listScopes: async () =>
|
|
1824
|
+
this.#withCapability("context_read", "context.listScopes", async () =>
|
|
1825
|
+
this._contextStore.listScopes(),
|
|
1826
|
+
),
|
|
1827
|
+
});
|
|
1828
|
+
}
|
|
1829
|
+
|
|
1830
|
+
listCapabilities() {
|
|
1831
|
+
return Array.from(this._grantedCapabilities).sort();
|
|
1832
|
+
}
|
|
1833
|
+
|
|
1834
|
+
listSupportedCapabilities() {
|
|
1835
|
+
return Array.from(this._supportedCapabilities).sort();
|
|
1836
|
+
}
|
|
1837
|
+
|
|
1838
|
+
listOperations() {
|
|
1839
|
+
return [...NodeHostSupportedOperations];
|
|
1840
|
+
}
|
|
1841
|
+
|
|
1842
|
+
hasCapability(capability) {
|
|
1843
|
+
return this._grantedCapabilities.has(String(capability ?? "").trim());
|
|
1844
|
+
}
|
|
1845
|
+
|
|
1846
|
+
assertCapability(capability, operation = null) {
|
|
1847
|
+
const normalized = assertNonEmptyString(capability, "Capability id");
|
|
1848
|
+
if (!this._supportedCapabilities.has(normalized)) {
|
|
1849
|
+
throw new HostCapabilityError(
|
|
1850
|
+
`Capability "${normalized}" is not supported by the reference Node host.`,
|
|
1851
|
+
{
|
|
1852
|
+
code: "host-capability-unsupported",
|
|
1853
|
+
capability: normalized,
|
|
1854
|
+
operation,
|
|
1855
|
+
},
|
|
1856
|
+
);
|
|
1857
|
+
}
|
|
1858
|
+
if (!this._grantedCapabilities.has(normalized)) {
|
|
1859
|
+
throw new HostCapabilityError(
|
|
1860
|
+
`Capability "${normalized}" is not granted for this Node host.`,
|
|
1861
|
+
{
|
|
1862
|
+
code: "host-capability-denied",
|
|
1863
|
+
capability: normalized,
|
|
1864
|
+
operation,
|
|
1865
|
+
},
|
|
1866
|
+
);
|
|
1867
|
+
}
|
|
1868
|
+
return normalized;
|
|
1869
|
+
}
|
|
1870
|
+
|
|
1871
|
+
async invoke(operation, params = {}) {
|
|
1872
|
+
const normalized = assertNonEmptyString(operation, "Host operation");
|
|
1873
|
+
switch (normalized) {
|
|
1874
|
+
case "clock.now":
|
|
1875
|
+
return this.clock.now();
|
|
1876
|
+
case "clock.monotonicNow":
|
|
1877
|
+
return this.clock.monotonicNow();
|
|
1878
|
+
case "clock.nowIso":
|
|
1879
|
+
return this.clock.nowIso();
|
|
1880
|
+
case "random.bytes":
|
|
1881
|
+
return this.random.bytes(params.length);
|
|
1882
|
+
case "timers.delay":
|
|
1883
|
+
return this.timers.delay(params.ms ?? params.delayMs, {
|
|
1884
|
+
signal: params.signal,
|
|
1885
|
+
});
|
|
1886
|
+
case "schedule.parse":
|
|
1887
|
+
return this.schedule.parse(params.expression);
|
|
1888
|
+
case "schedule.matches":
|
|
1889
|
+
return this.schedule.matches(params.expression, params.date);
|
|
1890
|
+
case "schedule.next":
|
|
1891
|
+
return this.schedule.next(params.expression, params.from);
|
|
1892
|
+
case "http.request":
|
|
1893
|
+
return this.http.request(params);
|
|
1894
|
+
case "websocket.exchange":
|
|
1895
|
+
return this.websocket.exchange(params);
|
|
1896
|
+
case "mqtt.publish":
|
|
1897
|
+
return this.mqtt.publish(params);
|
|
1898
|
+
case "mqtt.subscribeOnce":
|
|
1899
|
+
return this.mqtt.subscribeOnce(params);
|
|
1900
|
+
case "filesystem.resolvePath":
|
|
1901
|
+
return this.filesystem.resolvePath(params.path);
|
|
1902
|
+
case "filesystem.readFile":
|
|
1903
|
+
return this.filesystem.readFile(params.path, {
|
|
1904
|
+
encoding: params.encoding,
|
|
1905
|
+
});
|
|
1906
|
+
case "filesystem.writeFile":
|
|
1907
|
+
return this.filesystem.writeFile(params.path, params.value, {
|
|
1908
|
+
encoding: params.encoding,
|
|
1909
|
+
});
|
|
1910
|
+
case "filesystem.appendFile":
|
|
1911
|
+
return this.filesystem.appendFile(params.path, params.value, {
|
|
1912
|
+
encoding: params.encoding,
|
|
1913
|
+
});
|
|
1914
|
+
case "filesystem.deleteFile":
|
|
1915
|
+
return this.filesystem.deleteFile(params.path);
|
|
1916
|
+
case "filesystem.mkdir":
|
|
1917
|
+
return this.filesystem.mkdir(params.path, {
|
|
1918
|
+
recursive: params.recursive,
|
|
1919
|
+
});
|
|
1920
|
+
case "filesystem.readdir":
|
|
1921
|
+
return this.filesystem.readdir(params.path);
|
|
1922
|
+
case "filesystem.stat":
|
|
1923
|
+
return this.filesystem.stat(params.path);
|
|
1924
|
+
case "filesystem.rename":
|
|
1925
|
+
return this.filesystem.rename(params.fromPath, params.toPath);
|
|
1926
|
+
case "tcp.request":
|
|
1927
|
+
return this.tcp.request(params);
|
|
1928
|
+
case "udp.request":
|
|
1929
|
+
return this.udp.request(params);
|
|
1930
|
+
case "tls.request":
|
|
1931
|
+
return this.tls.request(params);
|
|
1932
|
+
case "exec.execFile":
|
|
1933
|
+
return this.exec.execFile(params);
|
|
1934
|
+
case "context.get":
|
|
1935
|
+
return this.context.get(params.scope, params.key);
|
|
1936
|
+
case "context.set":
|
|
1937
|
+
return this.context.set(params.scope, params.key, params.value);
|
|
1938
|
+
case "context.delete":
|
|
1939
|
+
return this.context.delete(params.scope, params.key);
|
|
1940
|
+
case "context.listKeys":
|
|
1941
|
+
return this.context.listKeys(params.scope);
|
|
1942
|
+
case "context.listScopes":
|
|
1943
|
+
return this.context.listScopes();
|
|
1944
|
+
case "crypto.sha256":
|
|
1945
|
+
return this.crypto.sha256(params.value ?? params.bytes);
|
|
1946
|
+
case "crypto.sha512":
|
|
1947
|
+
return this.crypto.sha512(params.value ?? params.bytes);
|
|
1948
|
+
case "crypto.hkdf":
|
|
1949
|
+
return this.crypto.hkdf(params);
|
|
1950
|
+
case "crypto.aesGcmEncrypt":
|
|
1951
|
+
return this.crypto.aesGcmEncrypt(params);
|
|
1952
|
+
case "crypto.aesGcmDecrypt":
|
|
1953
|
+
return this.crypto.aesGcmDecrypt(params);
|
|
1954
|
+
case "crypto.x25519.generateKeypair":
|
|
1955
|
+
return this.crypto.generateX25519Keypair();
|
|
1956
|
+
case "crypto.x25519.publicKey":
|
|
1957
|
+
return this.crypto.x25519PublicKey(params.privateKey);
|
|
1958
|
+
case "crypto.x25519.sharedSecret":
|
|
1959
|
+
return this.crypto.x25519SharedSecret(
|
|
1960
|
+
params.privateKey,
|
|
1961
|
+
params.publicKey,
|
|
1962
|
+
);
|
|
1963
|
+
case "crypto.sealedBox.encryptForRecipient":
|
|
1964
|
+
return this.crypto.encryptForRecipient(params);
|
|
1965
|
+
case "crypto.sealedBox.decryptFromEnvelope":
|
|
1966
|
+
return this.crypto.decryptFromEnvelope(params);
|
|
1967
|
+
case "crypto.secp256k1.publicKeyFromPrivate":
|
|
1968
|
+
return this.crypto.secp256k1.publicKeyFromPrivate(params.privateKey);
|
|
1969
|
+
case "crypto.secp256k1.signDigest":
|
|
1970
|
+
return this.crypto.secp256k1.signDigest(
|
|
1971
|
+
params.digest,
|
|
1972
|
+
params.privateKey,
|
|
1973
|
+
);
|
|
1974
|
+
case "crypto.secp256k1.verifyDigest":
|
|
1975
|
+
return this.crypto.secp256k1.verifyDigest(
|
|
1976
|
+
params.digest,
|
|
1977
|
+
params.signature,
|
|
1978
|
+
params.publicKey,
|
|
1979
|
+
);
|
|
1980
|
+
case "crypto.ed25519.publicKeyFromSeed":
|
|
1981
|
+
return this.crypto.ed25519.publicKeyFromSeed(params.seed);
|
|
1982
|
+
case "crypto.ed25519.sign":
|
|
1983
|
+
return this.crypto.ed25519.sign(params.message, params.seed);
|
|
1984
|
+
case "crypto.ed25519.verify":
|
|
1985
|
+
return this.crypto.ed25519.verify(
|
|
1986
|
+
params.message,
|
|
1987
|
+
params.signature,
|
|
1988
|
+
params.publicKey,
|
|
1989
|
+
);
|
|
1990
|
+
default:
|
|
1991
|
+
throw new Error(`Unknown Node host operation "${normalized}".`);
|
|
1992
|
+
}
|
|
1993
|
+
}
|
|
1994
|
+
|
|
1995
|
+
crypto = Object.freeze({
|
|
1996
|
+
sha256: async (value) =>
|
|
1997
|
+
this.#withCapability("crypto_hash", "crypto.sha256", async () =>
|
|
1998
|
+
sha256Bytes(toUint8Array(value)),
|
|
1999
|
+
),
|
|
2000
|
+
sha512: async (value) =>
|
|
2001
|
+
this.#withCapability("crypto_hash", "crypto.sha512", async () =>
|
|
2002
|
+
sha512Bytes(toUint8Array(value)),
|
|
2003
|
+
),
|
|
2004
|
+
hkdf: async (options = {}) =>
|
|
2005
|
+
this.#withCapability("crypto_kdf", "crypto.hkdf", async () =>
|
|
2006
|
+
hkdfBytes(
|
|
2007
|
+
toUint8Array(options.ikm),
|
|
2008
|
+
toUint8Array(options.salt),
|
|
2009
|
+
toUint8Array(options.info ?? new Uint8Array()),
|
|
2010
|
+
assertNonNegativeInteger(options.length, "HKDF length"),
|
|
2011
|
+
),
|
|
2012
|
+
),
|
|
2013
|
+
aesGcmEncrypt: async (options = {}) =>
|
|
2014
|
+
this.#withCapability("crypto_encrypt", "crypto.aesGcmEncrypt", async () =>
|
|
2015
|
+
aesGcmEncrypt(
|
|
2016
|
+
toUint8Array(options.key),
|
|
2017
|
+
toUint8Array(options.plaintext),
|
|
2018
|
+
toUint8Array(options.iv),
|
|
2019
|
+
options.aad === undefined || options.aad === null
|
|
2020
|
+
? null
|
|
2021
|
+
: toUint8Array(options.aad),
|
|
2022
|
+
),
|
|
2023
|
+
),
|
|
2024
|
+
aesGcmDecrypt: async (options = {}) =>
|
|
2025
|
+
this.#withCapability("crypto_decrypt", "crypto.aesGcmDecrypt", async () =>
|
|
2026
|
+
aesGcmDecrypt(
|
|
2027
|
+
toUint8Array(options.key),
|
|
2028
|
+
toUint8Array(options.ciphertext),
|
|
2029
|
+
toUint8Array(options.tag),
|
|
2030
|
+
toUint8Array(options.iv),
|
|
2031
|
+
options.aad === undefined || options.aad === null
|
|
2032
|
+
? null
|
|
2033
|
+
: toUint8Array(options.aad),
|
|
2034
|
+
),
|
|
2035
|
+
),
|
|
2036
|
+
generateX25519Keypair: async () =>
|
|
2037
|
+
this.#withCapability(
|
|
2038
|
+
"crypto_key_agreement",
|
|
2039
|
+
"crypto.x25519.generateKeypair",
|
|
2040
|
+
async () => generateX25519Keypair(),
|
|
2041
|
+
),
|
|
2042
|
+
x25519PublicKey: async (privateKey) =>
|
|
2043
|
+
this.#withCapability(
|
|
2044
|
+
"crypto_key_agreement",
|
|
2045
|
+
"crypto.x25519.publicKey",
|
|
2046
|
+
async () => x25519PublicKey(toUint8Array(privateKey)),
|
|
2047
|
+
),
|
|
2048
|
+
x25519SharedSecret: async (privateKey, publicKey) =>
|
|
2049
|
+
this.#withCapability(
|
|
2050
|
+
"crypto_key_agreement",
|
|
2051
|
+
"crypto.x25519.sharedSecret",
|
|
2052
|
+
async () =>
|
|
2053
|
+
x25519SharedSecret(
|
|
2054
|
+
toUint8Array(privateKey),
|
|
2055
|
+
toUint8Array(publicKey),
|
|
2056
|
+
),
|
|
2057
|
+
),
|
|
2058
|
+
encryptForRecipient: async (options = {}) =>
|
|
2059
|
+
this.#withCapability(
|
|
2060
|
+
"crypto_encrypt",
|
|
2061
|
+
"crypto.sealedBox.encryptForRecipient",
|
|
2062
|
+
async () =>
|
|
2063
|
+
encryptBytesForRecipient({
|
|
2064
|
+
plaintext: toUint8Array(options.plaintext),
|
|
2065
|
+
recipientPublicKey: toUint8Array(options.recipientPublicKey),
|
|
2066
|
+
context: options.context,
|
|
2067
|
+
senderKeyPair: options.senderKeyPair,
|
|
2068
|
+
}),
|
|
2069
|
+
),
|
|
2070
|
+
decryptFromEnvelope: async (options = {}) =>
|
|
2071
|
+
this.#withCapability(
|
|
2072
|
+
"crypto_decrypt",
|
|
2073
|
+
"crypto.sealedBox.decryptFromEnvelope",
|
|
2074
|
+
async () =>
|
|
2075
|
+
decryptBytesFromEnvelope({
|
|
2076
|
+
envelope: options.envelope,
|
|
2077
|
+
recipientPrivateKey: toUint8Array(options.recipientPrivateKey),
|
|
2078
|
+
}),
|
|
2079
|
+
),
|
|
2080
|
+
secp256k1: Object.freeze({
|
|
2081
|
+
publicKeyFromPrivate: async (privateKey) =>
|
|
2082
|
+
this.#withCapability(
|
|
2083
|
+
"crypto_sign",
|
|
2084
|
+
"crypto.secp256k1.publicKeyFromPrivate",
|
|
2085
|
+
async () => secp256k1PublicKey(toUint8Array(privateKey)),
|
|
2086
|
+
),
|
|
2087
|
+
signDigest: async (digest, privateKey) =>
|
|
2088
|
+
this.#withCapability(
|
|
2089
|
+
"crypto_sign",
|
|
2090
|
+
"crypto.secp256k1.signDigest",
|
|
2091
|
+
async () =>
|
|
2092
|
+
secp256k1SignDigest(
|
|
2093
|
+
toUint8Array(digest),
|
|
2094
|
+
toUint8Array(privateKey),
|
|
2095
|
+
),
|
|
2096
|
+
),
|
|
2097
|
+
verifyDigest: async (digest, signature, publicKey) =>
|
|
2098
|
+
this.#withCapability(
|
|
2099
|
+
"crypto_verify",
|
|
2100
|
+
"crypto.secp256k1.verifyDigest",
|
|
2101
|
+
async () =>
|
|
2102
|
+
secp256k1VerifyDigest(
|
|
2103
|
+
toUint8Array(digest),
|
|
2104
|
+
toUint8Array(signature),
|
|
2105
|
+
toUint8Array(publicKey),
|
|
2106
|
+
),
|
|
2107
|
+
),
|
|
2108
|
+
}),
|
|
2109
|
+
ed25519: Object.freeze({
|
|
2110
|
+
publicKeyFromSeed: async (seed) =>
|
|
2111
|
+
this.#withCapability(
|
|
2112
|
+
"crypto_sign",
|
|
2113
|
+
"crypto.ed25519.publicKeyFromSeed",
|
|
2114
|
+
async () => ed25519PublicKey(toUint8Array(seed)),
|
|
2115
|
+
),
|
|
2116
|
+
sign: async (message, seed) =>
|
|
2117
|
+
this.#withCapability(
|
|
2118
|
+
"crypto_sign",
|
|
2119
|
+
"crypto.ed25519.sign",
|
|
2120
|
+
async () => ed25519Sign(toUint8Array(message), toUint8Array(seed)),
|
|
2121
|
+
),
|
|
2122
|
+
verify: async (message, signature, publicKey) =>
|
|
2123
|
+
this.#withCapability(
|
|
2124
|
+
"crypto_verify",
|
|
2125
|
+
"crypto.ed25519.verify",
|
|
2126
|
+
async () =>
|
|
2127
|
+
ed25519Verify(
|
|
2128
|
+
toUint8Array(message),
|
|
2129
|
+
toUint8Array(signature),
|
|
2130
|
+
toUint8Array(publicKey),
|
|
2131
|
+
),
|
|
2132
|
+
),
|
|
2133
|
+
}),
|
|
2134
|
+
});
|
|
2135
|
+
|
|
2136
|
+
#resolveFilesystemPath(targetPath) {
|
|
2137
|
+
const requestedPath = assertNonEmptyString(targetPath, "Filesystem path");
|
|
2138
|
+
const resolvedPath = path.resolve(this.filesystemRoot, requestedPath);
|
|
2139
|
+
const rootWithSeparator = this.filesystemRoot.endsWith(path.sep)
|
|
2140
|
+
? this.filesystemRoot
|
|
2141
|
+
: `${this.filesystemRoot}${path.sep}`;
|
|
2142
|
+
if (
|
|
2143
|
+
resolvedPath !== this.filesystemRoot &&
|
|
2144
|
+
!resolvedPath.startsWith(rootWithSeparator)
|
|
2145
|
+
) {
|
|
2146
|
+
throw new HostFilesystemScopeError(
|
|
2147
|
+
`Path "${requestedPath}" escapes the configured filesystem root.`,
|
|
2148
|
+
{
|
|
2149
|
+
requestedPath,
|
|
2150
|
+
filesystemRoot: this.filesystemRoot,
|
|
2151
|
+
},
|
|
2152
|
+
);
|
|
2153
|
+
}
|
|
2154
|
+
return resolvedPath;
|
|
2155
|
+
}
|
|
2156
|
+
|
|
2157
|
+
#withCapability(capability, operation, callback) {
|
|
2158
|
+
this.assertCapability(capability, operation);
|
|
2159
|
+
return callback();
|
|
2160
|
+
}
|
|
2161
|
+
}
|
|
2162
|
+
|
|
2163
|
+
export function createNodeHost(options = {}) {
|
|
2164
|
+
return new NodeHost(options);
|
|
2165
|
+
}
|