@revenexx/integrations-node-sdk 1.1.0 → 1.3.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/README.md +5 -4
- package/dist/index.cjs +198 -60
- package/dist/index.d.cts +86 -7
- package/dist/index.d.ts +86 -7
- package/dist/index.js +193 -60
- package/package.json +4 -1
package/README.md
CHANGED
|
@@ -4,9 +4,10 @@ The shared TypeScript contract library for **Revenexx integration nodes**. It
|
|
|
4
4
|
defines the interfaces every integration node and credential must implement, and
|
|
5
5
|
ships the small helper toolchain the node registry uses to build manifests.
|
|
6
6
|
|
|
7
|
-
It
|
|
8
|
-
|
|
9
|
-
|
|
7
|
+
It carries **one runtime dependency** — `ipaddr.js`, which the SSRF guard defers
|
|
8
|
+
its address classification to (PO-183) — and no business logic beyond the manifest
|
|
9
|
+
helpers: it exists purely to share one contract between the node packages and the
|
|
10
|
+
workflow engine that runs them.
|
|
10
11
|
|
|
11
12
|
```
|
|
12
13
|
integrations-node-sdk ← this package (types & helpers only)
|
|
@@ -34,7 +35,7 @@ configuration or auth token needed.
|
|
|
34
35
|
| `credentialType` | `normalizeCredentialType` — normalise a credential-type reference (`string \| string[] \| undefined`) to `string[]`. |
|
|
35
36
|
| `errors` | `NodeError` — typed error class for unexpected/system-level failures thrown inside `execute`. |
|
|
36
37
|
| `fetch` | `safeFetch` and the `read*` body helpers — timeout, retry, response size-cap, unified error form, and an always-on **SSRF guard** (blocks private/loopback/link-local/metadata targets, re-checked on every redirect hop). Prefer it over raw `fetch`. See [`docs/overview.md`](docs/overview.md#safefetch). |
|
|
37
|
-
| `ssrf` | `assertPublicUrl` / `isBlockedAddress` — the SSRF guard that backs `safeFetch`, exported for reuse.
|
|
38
|
+
| `ssrf` | `assertPublicUrl` / `isBlockedAddress` — the SSRF guard that backs `safeFetch`, exported for reuse. An address is refused unless it is public unicast — `ipaddr.js` classifies the range, and for IPv6 the address must also sit inside IANA's allocated `2000::/3`, since that classification's *default* verdict is `unicast`. So reserved and unallocated bands nobody enumerated are refused too (PO-183). These two are the pre-flight check; `safeFetch` additionally judges the address each connection lands on, so a DNS answer that changes between check and connect gets no request (PO-184). Errors surface as `NodeError('BLOCKED_ADDRESS')`. Set `RVNXX_SSRF_ALLOW_PRIVATE=1` (off by default; local dev only) to reach `localhost`/internal targets while testing — see [`docs/overview.md`](docs/overview.md#ssrf-guard). |
|
|
38
39
|
| `extract` | `extractManifest` / `extractManifests` (nodes) and `extractCredentialManifest` / `extractCredentialManifests` (credentials) — pull descriptions off instances without executing them. |
|
|
39
40
|
| `manifest` | `buildManifest` / `MANIFEST_VERSION` — wrap node, credential and template descriptions in the `{ manifestVersion, nodes, credentials, templates }` envelope the registry expects. |
|
|
40
41
|
|
package/dist/index.cjs
CHANGED
|
@@ -54,11 +54,14 @@ __export(index_exports, {
|
|
|
54
54
|
backoffDelay: () => backoffDelay,
|
|
55
55
|
buildManifest: () => buildManifest,
|
|
56
56
|
clampResponseBytes: () => clampResponseBytes,
|
|
57
|
+
errorPort: () => errorPort,
|
|
58
|
+
errorResult: () => errorResult,
|
|
57
59
|
evaluate: () => evaluate,
|
|
58
60
|
extractCredentialManifest: () => extractCredentialManifest,
|
|
59
61
|
extractCredentialManifests: () => extractCredentialManifests,
|
|
60
62
|
extractManifest: () => extractManifest,
|
|
61
63
|
extractManifests: () => extractManifests,
|
|
64
|
+
httpErrorResult: () => httpErrorResult,
|
|
62
65
|
isBlockedAddress: () => isBlockedAddress,
|
|
63
66
|
isNodeWithIteration: () => isNodeWithIteration,
|
|
64
67
|
isOAuthAuthorizeCredential: () => isOAuthAuthorizeCredential,
|
|
@@ -76,6 +79,8 @@ __export(index_exports, {
|
|
|
76
79
|
sleepWithSignal: () => sleepWithSignal,
|
|
77
80
|
takesValue: () => takesValue,
|
|
78
81
|
timeoutConfigField: () => timeoutConfigField,
|
|
82
|
+
toErrorOutput: () => toErrorOutput,
|
|
83
|
+
toErrorResult: () => toErrorResult,
|
|
79
84
|
withRetry: () => withRetry
|
|
80
85
|
});
|
|
81
86
|
module.exports = __toCommonJS(index_exports);
|
|
@@ -203,6 +208,76 @@ var NodeError = class extends Error {
|
|
|
203
208
|
}
|
|
204
209
|
};
|
|
205
210
|
|
|
211
|
+
// src/nodeErrors.ts
|
|
212
|
+
function toErrorOutput(err) {
|
|
213
|
+
if (err instanceof NodeError) {
|
|
214
|
+
const raw = err.meta?.["status"];
|
|
215
|
+
const status = typeof raw === "number" && Number.isFinite(raw) ? raw : 0;
|
|
216
|
+
return { code: err.code, message: err.message, status };
|
|
217
|
+
}
|
|
218
|
+
if (err instanceof Error && err.name === "AbortError") {
|
|
219
|
+
return { code: "TIMEOUT", message: "Request timed out", status: 0 };
|
|
220
|
+
}
|
|
221
|
+
if (err instanceof Error) {
|
|
222
|
+
return { code: "REQUEST_FAILED", message: err.message, status: 0 };
|
|
223
|
+
}
|
|
224
|
+
return { code: "REQUEST_FAILED", message: String(err), status: 0 };
|
|
225
|
+
}
|
|
226
|
+
function errorResult(output, port = "error") {
|
|
227
|
+
return { outputs: { [port]: output }, branch: port };
|
|
228
|
+
}
|
|
229
|
+
function toErrorResult(err, port = "error") {
|
|
230
|
+
return errorResult(toErrorOutput(err), port);
|
|
231
|
+
}
|
|
232
|
+
function httpErrorResult(err, port = "error") {
|
|
233
|
+
const body = err instanceof NodeError ? err.meta?.["body"] ?? null : null;
|
|
234
|
+
return errorResult({ ...toErrorOutput(err), body }, port);
|
|
235
|
+
}
|
|
236
|
+
function errorPort(opts = {}) {
|
|
237
|
+
return {
|
|
238
|
+
name: "error",
|
|
239
|
+
kind: "error",
|
|
240
|
+
dataType: "object",
|
|
241
|
+
label: { en: "Error", de: "Fehler" },
|
|
242
|
+
description: {
|
|
243
|
+
en: "Taken when the attempt fails in a way the workflow can carry on from. A node that cannot run as configured fails the run instead of taking this path.",
|
|
244
|
+
de: "Wird genommen, wenn der Versuch auf eine Weise fehlschl\xE4gt, mit der der Workflow weiterarbeiten kann. Ein Node, der so konfiguriert nicht laufen kann, l\xE4sst den Run scheitern, statt diesen Pfad zu nehmen."
|
|
245
|
+
},
|
|
246
|
+
fields: {
|
|
247
|
+
code: {
|
|
248
|
+
dataType: "string",
|
|
249
|
+
description: {
|
|
250
|
+
en: "Machine-readable name of the failure \u2014 branch on this rather than on the message. A node that talks to a remote system shares `TIMEOUT` and `REQUEST_FAILED` with every other one; the remaining codes belong to this node alone.",
|
|
251
|
+
de: "Maschinenlesbarer Name des Fehlers \u2014 hierauf verzweigen, nicht auf die Meldung. Ein Node, der mit einem Fremdsystem spricht, teilt `TIMEOUT` und `REQUEST_FAILED` mit allen anderen; die \xFCbrigen Codes geh\xF6ren allein zu diesem Node."
|
|
252
|
+
}
|
|
253
|
+
},
|
|
254
|
+
message: {
|
|
255
|
+
dataType: "string",
|
|
256
|
+
description: {
|
|
257
|
+
en: "Wording for whoever reads the run afterwards, frequently the wording the far end used. It is not a stable value to branch on.",
|
|
258
|
+
de: "Wortlaut f\xFCr den, der den Run sp\xE4ter liest, h\xE4ufig der des Gegen\xFCbers. Kein stabiler Wert, auf den sich Verzweigungen st\xFCtzen sollten."
|
|
259
|
+
}
|
|
260
|
+
},
|
|
261
|
+
status: {
|
|
262
|
+
dataType: "number",
|
|
263
|
+
description: {
|
|
264
|
+
en: "How the far end answered, as an HTTP status code, where one answered with a status at all. `0` otherwise \u2014 a timeout, a refused connection, an input the node could not use, a node that never leaves the workflow, or a failure the far end reported in its own answer rather than in a status, as a bulk job that ends as failed does. So `0` means there is no status to read, not that nothing was reached.",
|
|
265
|
+
de: "Wie das Gegen\xFCber geantwortet hat, als HTTP-Statuscode \u2014 sofern eines \xFCberhaupt mit einem Status geantwortet hat. Sonst `0`: Timeout, abgelehnte Verbindung, eine unbrauchbare Eingabe, ein Node, der den Workflow nicht verl\xE4sst, oder ein Fehler, den das Gegen\xFCber in seiner Antwort statt im Status gemeldet hat \u2014 wie ein Bulk-Job, der als fehlgeschlagen endet. `0` hei\xDFt also: kein Status zu lesen, nicht: nichts erreicht."
|
|
266
|
+
}
|
|
267
|
+
},
|
|
268
|
+
...opts.body ? {
|
|
269
|
+
body: {
|
|
270
|
+
dataType: "any",
|
|
271
|
+
description: {
|
|
272
|
+
en: "The answer the far end gave, where the failure got far enough to have one: the response body, or the job the platform reported as failed.",
|
|
273
|
+
de: "Die Antwort des Gegen\xFCbers, sofern der Fehler weit genug kam, um eine zu haben: der Response-Body oder der von der Plattform als fehlgeschlagen gemeldete Job."
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
} : {}
|
|
277
|
+
}
|
|
278
|
+
};
|
|
279
|
+
}
|
|
280
|
+
|
|
206
281
|
// src/extract.ts
|
|
207
282
|
function extractManifest(node) {
|
|
208
283
|
return node.description;
|
|
@@ -218,7 +293,9 @@ function extractCredentialManifests(credentials) {
|
|
|
218
293
|
}
|
|
219
294
|
|
|
220
295
|
// src/ssrf.ts
|
|
296
|
+
var import_node_diagnostics_channel = require("diagnostics_channel");
|
|
221
297
|
var import_node_net = require("net");
|
|
298
|
+
var import_ipaddr = __toESM(require("ipaddr.js"), 1);
|
|
222
299
|
var ssrfResolver = {
|
|
223
300
|
lookup: async (hostname) => {
|
|
224
301
|
const { lookup } = await import("dns/promises");
|
|
@@ -226,66 +303,27 @@ var ssrfResolver = {
|
|
|
226
303
|
return results.map((r) => ({ address: r.address, family: r.family }));
|
|
227
304
|
}
|
|
228
305
|
};
|
|
229
|
-
function
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
let s = input;
|
|
238
|
-
const zone = s.indexOf("%");
|
|
239
|
-
if (zone !== -1) s = s.slice(0, zone);
|
|
240
|
-
if (s.includes(".")) {
|
|
241
|
-
const idx = s.lastIndexOf(":");
|
|
242
|
-
if (idx === -1) return null;
|
|
243
|
-
const v4 = parseIpv4(s.slice(idx + 1));
|
|
244
|
-
if (!v4) return null;
|
|
245
|
-
const hi = (v4[0] << 8 | v4[1]).toString(16);
|
|
246
|
-
const lo = (v4[2] << 8 | v4[3]).toString(16);
|
|
247
|
-
s = `${s.slice(0, idx + 1)}${hi}:${lo}`;
|
|
248
|
-
}
|
|
249
|
-
const halves = s.split("::");
|
|
250
|
-
if (halves.length > 2) return null;
|
|
251
|
-
const parseGroups = (part) => part === "" ? [] : part.split(":").map((h) => /^[0-9a-fA-F]{1,4}$/.test(h) ? parseInt(h, 16) : Number.NaN);
|
|
252
|
-
const head = parseGroups(halves[0] ?? "");
|
|
253
|
-
const back = halves.length === 2 ? parseGroups(halves[1] ?? "") : null;
|
|
254
|
-
const declared = [...head, ...back ?? []];
|
|
255
|
-
if (declared.some((h) => !Number.isInteger(h) || h < 0 || h > 65535)) return null;
|
|
256
|
-
let hextets;
|
|
257
|
-
if (back === null) {
|
|
258
|
-
hextets = head;
|
|
259
|
-
} else {
|
|
260
|
-
const zeros = 8 - (head.length + back.length);
|
|
261
|
-
if (zeros < 1) return null;
|
|
262
|
-
hextets = [...head, ...new Array(zeros).fill(0), ...back];
|
|
263
|
-
}
|
|
264
|
-
return hextets.length === 8 ? hextets : null;
|
|
265
|
-
}
|
|
266
|
-
function isBlockedIpv4(o) {
|
|
267
|
-
const [a, b] = o;
|
|
268
|
-
return a === 0 || // 0.0.0.0/8 "this network" (incl. 0.0.0.0)
|
|
269
|
-
a === 127 || // 127.0.0.0/8 loopback
|
|
270
|
-
a === 10 || // 10.0.0.0/8 private
|
|
271
|
-
a === 172 && b >= 16 && b <= 31 || // 172.16.0.0/12 private
|
|
272
|
-
a === 192 && b === 168 || // 192.168.0.0/16 private
|
|
273
|
-
a === 169 && b === 254;
|
|
306
|
+
function isIPv6(addr) {
|
|
307
|
+
return addr.kind() === "ipv6";
|
|
308
|
+
}
|
|
309
|
+
var PUBLIC_RANGE = "unicast";
|
|
310
|
+
var GLOBAL_UNICAST_V6 = import_ipaddr.default.parseCIDR("2000::/3");
|
|
311
|
+
var V4_EMBEDDING_RANGES = /* @__PURE__ */ new Set(["ipv4Mapped", "rfc6052"]);
|
|
312
|
+
function embeddedIpv4(addr) {
|
|
313
|
+
return import_ipaddr.default.fromByteArray(addr.toByteArray().slice(12));
|
|
274
314
|
}
|
|
275
315
|
function isBlockedAddress(ip) {
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
}
|
|
286
|
-
|
|
287
|
-
if ((h[0] & 65472) === 65152) return true;
|
|
288
|
-
return false;
|
|
316
|
+
let addr;
|
|
317
|
+
try {
|
|
318
|
+
addr = import_ipaddr.default.parse(ip);
|
|
319
|
+
} catch {
|
|
320
|
+
return true;
|
|
321
|
+
}
|
|
322
|
+
if (isIPv6(addr)) {
|
|
323
|
+
if (V4_EMBEDDING_RANGES.has(addr.range())) return embeddedIpv4(addr).range() !== PUBLIC_RANGE;
|
|
324
|
+
if (!addr.match(GLOBAL_UNICAST_V6)) return true;
|
|
325
|
+
}
|
|
326
|
+
return addr.range() !== PUBLIC_RANGE;
|
|
289
327
|
}
|
|
290
328
|
var bypassNoticeLogged = false;
|
|
291
329
|
function guardRelaxedForLocalDev() {
|
|
@@ -310,6 +348,82 @@ function blockedError(host, address) {
|
|
|
310
348
|
status: 0
|
|
311
349
|
});
|
|
312
350
|
}
|
|
351
|
+
var guardedTargets = /* @__PURE__ */ new Map();
|
|
352
|
+
var connectGuardInstalled = false;
|
|
353
|
+
function normalizeHost(hostname) {
|
|
354
|
+
const host = hostname.startsWith("[") && hostname.endsWith("]") ? hostname.slice(1, -1) : hostname;
|
|
355
|
+
return host.toLowerCase();
|
|
356
|
+
}
|
|
357
|
+
function targetKey(hostname, port, protocol) {
|
|
358
|
+
const host = normalizeHost(hostname);
|
|
359
|
+
const effective = port === "" ? protocol === "https:" ? "443" : "80" : port;
|
|
360
|
+
return `${host}:${effective}`;
|
|
361
|
+
}
|
|
362
|
+
var unreadableMessageLogged = false;
|
|
363
|
+
function reportUnreadableMessage() {
|
|
364
|
+
if (unreadableMessageLogged) return;
|
|
365
|
+
unreadableMessageLogged = true;
|
|
366
|
+
console.warn(
|
|
367
|
+
"[ssrf] undici:client:connected published a message this guard cannot read: the connect-time half of the SSRF guard is not judging connections. The pre-flight check still applies."
|
|
368
|
+
);
|
|
369
|
+
}
|
|
370
|
+
function connectBlockedError(host, address) {
|
|
371
|
+
if (address == null) {
|
|
372
|
+
console.warn(`[ssrf] dropped connection to ${host}: the address it landed on could not be read`);
|
|
373
|
+
return new NodeError(
|
|
374
|
+
"BLOCKED_ADDRESS",
|
|
375
|
+
`Blocked request to ${host}: the address the connection landed on could not be read`,
|
|
376
|
+
{ status: 0 }
|
|
377
|
+
);
|
|
378
|
+
}
|
|
379
|
+
if (host === address) {
|
|
380
|
+
return new NodeError("BLOCKED_ADDRESS", `Blocked connection to private or reserved address ${address}`, {
|
|
381
|
+
status: 0
|
|
382
|
+
});
|
|
383
|
+
}
|
|
384
|
+
console.warn(`[ssrf] dropped connection to ${host}: connected to private/reserved address ${address}`);
|
|
385
|
+
return new NodeError(
|
|
386
|
+
"BLOCKED_ADDRESS",
|
|
387
|
+
`Blocked request to ${host}: the connection landed on a private or reserved address`,
|
|
388
|
+
{ status: 0 }
|
|
389
|
+
);
|
|
390
|
+
}
|
|
391
|
+
function judgeConnection(message) {
|
|
392
|
+
const { connectParams, socket } = message ?? {};
|
|
393
|
+
const hostname = connectParams?.hostname;
|
|
394
|
+
const port = connectParams?.port;
|
|
395
|
+
const protocol = connectParams?.protocol;
|
|
396
|
+
if (hostname == null || port == null || protocol == null || typeof socket?.destroy !== "function") {
|
|
397
|
+
reportUnreadableMessage();
|
|
398
|
+
return;
|
|
399
|
+
}
|
|
400
|
+
if (!guardedTargets.has(targetKey(hostname, port, protocol))) return;
|
|
401
|
+
if (guardRelaxedForLocalDev()) return;
|
|
402
|
+
const peer = socket.remoteAddress;
|
|
403
|
+
if (peer != null && !isBlockedAddress(peer)) return;
|
|
404
|
+
socket.destroy(connectBlockedError(normalizeHost(hostname), peer));
|
|
405
|
+
}
|
|
406
|
+
function guardConnectionsTo(url) {
|
|
407
|
+
if (!connectGuardInstalled) {
|
|
408
|
+
connectGuardInstalled = true;
|
|
409
|
+
(0, import_node_diagnostics_channel.subscribe)("undici:client:connected", judgeConnection);
|
|
410
|
+
}
|
|
411
|
+
const key = targetKey(url.hostname, url.port, url.protocol);
|
|
412
|
+
guardedTargets.set(key, (guardedTargets.get(key) ?? 0) + 1);
|
|
413
|
+
let released = false;
|
|
414
|
+
return () => {
|
|
415
|
+
if (released) return;
|
|
416
|
+
released = true;
|
|
417
|
+
const count = (guardedTargets.get(key) ?? 1) - 1;
|
|
418
|
+
if (count > 0) guardedTargets.set(key, count);
|
|
419
|
+
else guardedTargets.delete(key);
|
|
420
|
+
};
|
|
421
|
+
}
|
|
422
|
+
function connectionRefusal(err) {
|
|
423
|
+
const cause = err?.cause;
|
|
424
|
+
if (cause instanceof NodeError && cause.code === "BLOCKED_ADDRESS") return cause;
|
|
425
|
+
return void 0;
|
|
426
|
+
}
|
|
313
427
|
async function resolveHost(lookup, host, signal) {
|
|
314
428
|
if (!signal) return lookup(host);
|
|
315
429
|
if (signal.aborted) throw signal.reason;
|
|
@@ -376,6 +490,8 @@ async function timedFetch(url, fetchOptions, ctxSignal, effectiveMs) {
|
|
|
376
490
|
try {
|
|
377
491
|
return await fetch(url, { ...fetchOptions, signal });
|
|
378
492
|
} catch (err) {
|
|
493
|
+
const blocked = connectionRefusal(err);
|
|
494
|
+
if (blocked) throw blocked;
|
|
379
495
|
if (ctxSignal?.aborted) throw ctxSignal.reason;
|
|
380
496
|
if (ac.signal.aborted) throw new NodeError("TIMEOUT", `Request timed out after ${effectiveMs}ms`);
|
|
381
497
|
throw err;
|
|
@@ -402,7 +518,15 @@ async function guardedFetch(url, fetchOptions, ctxSignal, effectiveMs) {
|
|
|
402
518
|
let currentInit = { ...fetchOptions, redirect: "manual" };
|
|
403
519
|
await guardUrl(currentUrl, ctxSignal, effectiveMs);
|
|
404
520
|
for (let hop = 0; ; hop++) {
|
|
405
|
-
const
|
|
521
|
+
const release = guardConnectionsTo(
|
|
522
|
+
currentUrl instanceof URL ? currentUrl : new URL(currentUrl)
|
|
523
|
+
);
|
|
524
|
+
let res;
|
|
525
|
+
try {
|
|
526
|
+
res = await timedFetch(currentUrl, currentInit, ctxSignal, effectiveMs);
|
|
527
|
+
} finally {
|
|
528
|
+
release();
|
|
529
|
+
}
|
|
406
530
|
const location = res.headers.get("location");
|
|
407
531
|
if (!REDIRECT_STATUSES.has(res.status) || !location) return res;
|
|
408
532
|
const redirectStatus = res.status;
|
|
@@ -486,6 +610,8 @@ async function readArrayBuffer(res, maxBytes = DEFAULT_MAX_RESPONSE_BYTES) {
|
|
|
486
610
|
const cap = clampResponseBytes(maxBytes);
|
|
487
611
|
const declared = Number(res.headers.get("content-length"));
|
|
488
612
|
if (Number.isFinite(declared) && declared > cap) {
|
|
613
|
+
await res.body?.cancel().catch(() => {
|
|
614
|
+
});
|
|
489
615
|
throw tooLargeError(res.status, declared, cap);
|
|
490
616
|
}
|
|
491
617
|
const body = res.body;
|
|
@@ -694,6 +820,7 @@ function expiryFromExpiresIn(expiresIn) {
|
|
|
694
820
|
}
|
|
695
821
|
return new Date(Date.now() + seconds * 1e3).toISOString();
|
|
696
822
|
}
|
|
823
|
+
var MAX_TOKEN_RESPONSE_BYTES = 1024 * 1024;
|
|
697
824
|
var BaseCredential = class {
|
|
698
825
|
async test(_ctx, config) {
|
|
699
826
|
for (const field of this.description.fields) {
|
|
@@ -721,6 +848,12 @@ var BaseCredential = class {
|
|
|
721
848
|
* Note that a 301/302 answer to this POST is downgraded to a bodiless GET —
|
|
722
849
|
* per the redirect rules, not a quirk of ours. Real token endpoints do not
|
|
723
850
|
* redirect; one that does was never going to complete the exchange anyway.
|
|
851
|
+
*
|
|
852
|
+
* The third protection is the caller's own: safeFetch hands back a `Response`
|
|
853
|
+
* and reading it is not its job, so the answer goes through {@link readText}
|
|
854
|
+
* under {@link MAX_TOKEN_RESPONSE_BYTES} rather than a bare `res.text()`.
|
|
855
|
+
* Both branches below read that same string, so the cap covers the refusal as
|
|
856
|
+
* well as the token — an error body is the less predictable of the two.
|
|
724
857
|
*/
|
|
725
858
|
async postForm(ctx, url, form, headers = {}) {
|
|
726
859
|
const body = new URLSearchParams();
|
|
@@ -735,7 +868,7 @@ var BaseCredential = class {
|
|
|
735
868
|
headers: { "Content-Type": "application/x-www-form-urlencoded", Accept: "application/json", ...headers },
|
|
736
869
|
body
|
|
737
870
|
});
|
|
738
|
-
const text = await res
|
|
871
|
+
const text = await readText(res, MAX_TOKEN_RESPONSE_BYTES);
|
|
739
872
|
if (!res.ok) {
|
|
740
873
|
throw new Error(`token endpoint ${url} -> ${res.status}${formatOAuthError(text)}`);
|
|
741
874
|
}
|
|
@@ -922,11 +1055,14 @@ function base64Url(buf) {
|
|
|
922
1055
|
backoffDelay,
|
|
923
1056
|
buildManifest,
|
|
924
1057
|
clampResponseBytes,
|
|
1058
|
+
errorPort,
|
|
1059
|
+
errorResult,
|
|
925
1060
|
evaluate,
|
|
926
1061
|
extractCredentialManifest,
|
|
927
1062
|
extractCredentialManifests,
|
|
928
1063
|
extractManifest,
|
|
929
1064
|
extractManifests,
|
|
1065
|
+
httpErrorResult,
|
|
930
1066
|
isBlockedAddress,
|
|
931
1067
|
isNodeWithIteration,
|
|
932
1068
|
isOAuthAuthorizeCredential,
|
|
@@ -944,5 +1080,7 @@ function base64Url(buf) {
|
|
|
944
1080
|
sleepWithSignal,
|
|
945
1081
|
takesValue,
|
|
946
1082
|
timeoutConfigField,
|
|
1083
|
+
toErrorOutput,
|
|
1084
|
+
toErrorResult,
|
|
947
1085
|
withRetry
|
|
948
1086
|
});
|
package/dist/index.d.cts
CHANGED
|
@@ -650,6 +650,67 @@ declare class NodeError extends Error {
|
|
|
650
650
|
constructor(code: string, message: string, meta?: Record<string, unknown>);
|
|
651
651
|
}
|
|
652
652
|
|
|
653
|
+
/**
|
|
654
|
+
* Canonical error-output shape emitted on a node's `error` port. It lives here
|
|
655
|
+
* rather than in a node package because all four of them were building it
|
|
656
|
+
* separately — `integrations-nodes-core` in an internal module, `deepl` in a
|
|
657
|
+
* copy of that module, Business Central in `bc-fetch.ts` and Pipedrive in
|
|
658
|
+
* `pipedrive-client.ts` — so the workflow editor's guarantee of one
|
|
659
|
+
* `{ code, message, status }` payload rested on four files agreeing by hand
|
|
660
|
+
* (PO-442; asked for by PO-134 in June and not done then).
|
|
661
|
+
*/
|
|
662
|
+
interface NodeErrorOutput {
|
|
663
|
+
/** Machine-readable error code, e.g. `HTTP_ERROR`, `TIMEOUT`, `REQUEST_FAILED`. */
|
|
664
|
+
code: string;
|
|
665
|
+
message: string;
|
|
666
|
+
/** HTTP status code, or `0` for timeouts / network / non-HTTP errors. */
|
|
667
|
+
status: number;
|
|
668
|
+
/** Optional response body — HTTP-specific. */
|
|
669
|
+
body?: unknown;
|
|
670
|
+
}
|
|
671
|
+
/**
|
|
672
|
+
* Normalize any caught value into the canonical {@link NodeErrorOutput} shape.
|
|
673
|
+
* Use this in a node's `catch` block before routing to the `error` port.
|
|
674
|
+
*/
|
|
675
|
+
declare function toErrorOutput(err: unknown): NodeErrorOutput;
|
|
676
|
+
/** Wrap an error output into an `INodeResult` routed to the `error` port. */
|
|
677
|
+
declare function errorResult(output: NodeErrorOutput, port?: string): INodeResult;
|
|
678
|
+
/**
|
|
679
|
+
* Catch-block shortcut: normalize any thrown value and route it to the failure
|
|
680
|
+
* output. The one helper every node's `catch` funnels into, so the
|
|
681
|
+
* `{ code, message, status }` mapping lives in a single place instead of being
|
|
682
|
+
* re-implemented per protocol (FTP/SFTP) or per package.
|
|
683
|
+
*
|
|
684
|
+
* `port` is the output taken, `error` unless the node declares its failure
|
|
685
|
+
* output under another name (`{ ...errorPort(), name: 'failed' }`) — pass the
|
|
686
|
+
* same name here, or the branch names a port that node does not have.
|
|
687
|
+
*/
|
|
688
|
+
declare function toErrorResult(err: unknown, port?: string): INodeResult;
|
|
689
|
+
/**
|
|
690
|
+
* Catch-block shortcut for the HTTP-style nodes whose failure output additionally
|
|
691
|
+
* declares `body`: the canonical `{ code, message, status }` mapping plus the
|
|
692
|
+
* response body a {@link NodeError} carries in `meta.body` (`null` when the
|
|
693
|
+
* failure never reached a response). `port` names the output as above.
|
|
694
|
+
*/
|
|
695
|
+
declare function httpErrorResult(err: unknown, port?: string): INodeResult;
|
|
696
|
+
/**
|
|
697
|
+
* Shared `error` output port — the uniform `{ code, message, status }` triple
|
|
698
|
+
* declared on every node that has an `error` branch. Pass `body: true` for
|
|
699
|
+
* HTTP-style nodes that additionally emit a response body on the port.
|
|
700
|
+
*
|
|
701
|
+
* The sentences travel with the port. The three values mean the same thing
|
|
702
|
+
* behind every node, so they are written here once instead of per node; a node
|
|
703
|
+
* whose failures are worth naming more precisely spreads the port and replaces
|
|
704
|
+
* only its `description` (`{ ...errorPort(), description: … }`).
|
|
705
|
+
*
|
|
706
|
+
* The second sentence of the description is the rule this port exists under,
|
|
707
|
+
* and it is promised in `specs/error-handling.md`: a fault in the author's own
|
|
708
|
+
* configuration ends the run, a fault met while running is offered here.
|
|
709
|
+
*/
|
|
710
|
+
declare function errorPort(opts?: {
|
|
711
|
+
body?: boolean;
|
|
712
|
+
}): IOutputPort;
|
|
713
|
+
|
|
653
714
|
declare function extractManifest(node: INode): INodeDescription;
|
|
654
715
|
declare function extractManifests(nodes: INode[]): INodeDescription[];
|
|
655
716
|
declare function extractCredentialManifest(credential: ICredential): ICredentialDescription;
|
|
@@ -694,7 +755,8 @@ declare function safeFetch(url: string | URL, options?: SafeFetchOptions): Promi
|
|
|
694
755
|
* The `Content-Length` header is used as a fast-reject (bail before downloading
|
|
695
756
|
* anything), but the limit is *also* enforced while streaming, since the header
|
|
696
757
|
* can be absent or lie. On overrun the stream is cancelled and a
|
|
697
|
-
* `NodeError('RESPONSE_TOO_LARGE', …, { status })` is thrown.
|
|
758
|
+
* `NodeError('RESPONSE_TOO_LARGE', …, { status })` is thrown. Both refusal paths
|
|
759
|
+
* cancel the body first, so neither leaves the connection held.
|
|
698
760
|
*/
|
|
699
761
|
declare function readArrayBuffer(res: Response, maxBytes?: number): Promise<ArrayBuffer>;
|
|
700
762
|
/** Read a response body as UTF-8 text, capped at `maxBytes` (see {@link readArrayBuffer}). */
|
|
@@ -738,10 +800,19 @@ interface LookupAddress {
|
|
|
738
800
|
type LookupFn = (hostname: string) => Promise<LookupAddress[]>;
|
|
739
801
|
/**
|
|
740
802
|
* Return `true` when `ip` (a literal IPv4/IPv6 address) points at a private,
|
|
741
|
-
* loopback, link-local
|
|
742
|
-
*
|
|
743
|
-
*
|
|
803
|
+
* loopback, link-local, carrier-grade NAT, multicast, broadcast, reserved,
|
|
804
|
+
* unallocated or otherwise non-public target that a server-side fetch must never be
|
|
805
|
+
* steered to.
|
|
806
|
+
*
|
|
807
|
+
* The verdict is an allow-list: the address is refused unless the classification
|
|
808
|
+
* calls its range public unicast and — for IPv6, where that verdict is a default
|
|
809
|
+
* rather than a determination — it also sits inside the block IANA has allocated
|
|
810
|
+
* (see {@link GLOBAL_UNICAST_V6}). The IPv6 forms that embed an IPv4 address are
|
|
811
|
+
* unwrapped and the embedded address judged in its place, so a public host stays
|
|
812
|
+
* reachable when it is written that way. An address we cannot parse is treated as
|
|
744
813
|
* blocked (fail-closed).
|
|
814
|
+
*
|
|
815
|
+
* Promised behaviour: specs/ssrf-guard.md (AC-9, AC-11, AC-19).
|
|
745
816
|
*/
|
|
746
817
|
declare function isBlockedAddress(ip: string): boolean;
|
|
747
818
|
/**
|
|
@@ -752,8 +823,10 @@ declare function isBlockedAddress(ip: string): boolean;
|
|
|
752
823
|
* rejects if **any** resolved address is private/reserved. Throws
|
|
753
824
|
* `NodeError('BLOCKED_ADDRESS', …, { status: 0 })` on rejection.
|
|
754
825
|
*
|
|
755
|
-
*
|
|
756
|
-
*
|
|
826
|
+
* This is the pre-flight half of the guard: the connection that follows resolves
|
|
827
|
+
* the name again on its own, so `safeFetch` also judges the address the socket
|
|
828
|
+
* actually reached (PO-184). A caller that opens its own request after
|
|
829
|
+
* `assertPublicUrl` gets the check and not that second half.
|
|
757
830
|
*
|
|
758
831
|
* Pass `signal` (the per-request timeout/cancellation budget) so a hung or
|
|
759
832
|
* hostile DNS resolve cannot block the guard past that budget — see
|
|
@@ -952,6 +1025,12 @@ declare abstract class BaseCredential implements ICredential {
|
|
|
952
1025
|
* Note that a 301/302 answer to this POST is downgraded to a bodiless GET —
|
|
953
1026
|
* per the redirect rules, not a quirk of ours. Real token endpoints do not
|
|
954
1027
|
* redirect; one that does was never going to complete the exchange anyway.
|
|
1028
|
+
*
|
|
1029
|
+
* The third protection is the caller's own: safeFetch hands back a `Response`
|
|
1030
|
+
* and reading it is not its job, so the answer goes through {@link readText}
|
|
1031
|
+
* under {@link MAX_TOKEN_RESPONSE_BYTES} rather than a bare `res.text()`.
|
|
1032
|
+
* Both branches below read that same string, so the cap covers the refusal as
|
|
1033
|
+
* well as the token — an error body is the less predictable of the two.
|
|
955
1034
|
*/
|
|
956
1035
|
protected postForm(ctx: ICredentialContext, url: string, form: Record<string, string | undefined>, headers?: Record<string, string>): Promise<OAuthTokenResponse>;
|
|
957
1036
|
}
|
|
@@ -1028,4 +1107,4 @@ declare abstract class OAuth2AuthCodeCredential extends BaseCredential implement
|
|
|
1028
1107
|
test(_ctx: ICredentialContext, config: Config): Promise<ICredentialTestResult>;
|
|
1029
1108
|
}
|
|
1030
1109
|
|
|
1031
|
-
export { ApiKeyCredential, BaseCredential, BasicAuthCredential, type ConfigType, type CredentialAuthKind, type CredentialFieldType, DEFAULT_MAX_RESPONSE_BYTES, DEFAULT_RETRY_ATTEMPTS, DEFAULT_RETRY_DELAY_MS, DEFAULT_RETRY_POLICY, DEFAULT_TIMEOUT_MS, type DataType, type IConfigField, type IConfigFieldBase, type IConfigOption, type IConfigValidation, type ICredential, type ICredentialContext, type ICredentialDescription, type ICredentialField, type ICredentialOAuthAuthorize, type ICredentialResolveResult, type ICredentialTestResult, type IImage, type IInputPort, type INode, type INodeAuthorContext, type INodeContext, type INodeDescription, type INodeResult, type INodeState, type INodeWithIteration, type IOutputField, type IOutputPort, type IShowIfCondition, type ITemplateDescription, type ITemplateTrigger, type ImageCategory, type LocalizedString, type LookupAddress, type LookupFn, MANIFEST_VERSION, MAX_REDIRECTS, MAX_RESPONSE_BYTES, MAX_RETRY_ATTEMPTS, MAX_TIMEOUT_MS, type NodeCategory, NodeError, type NodeManifest, type NodePackageMeta, OAuth2AuthCodeCredential, OAuth2ClientCredentialsCredential, type OAuthTokenResponse, OPERATORS, type Operator, type OutputKind, type RetryHooks, type RetryPolicy, RetryableError, type SafeFetchOptions, type SafeFetchRetry, SimpleValueCredential, type TemplateLevel, type TemplateTriggerType, VALUELESS_OPERATORS, assertPublicUrl, backoffDelay, buildManifest, clampResponseBytes, evaluate, extractCredentialManifest, extractCredentialManifests, extractManifest, extractManifests, isBlockedAddress, isNodeWithIteration, isOAuthAuthorizeCredential, isOperator, maxBytesConfigField, normalizeCredentialType, normalizeLocalized, parsePackageMeta, readArrayBuffer, readJsonOrText, readText, retryConfigFields, safeFetch, settingApplies, sleepWithSignal, takesValue, timeoutConfigField, withRetry };
|
|
1110
|
+
export { ApiKeyCredential, BaseCredential, BasicAuthCredential, type ConfigType, type CredentialAuthKind, type CredentialFieldType, DEFAULT_MAX_RESPONSE_BYTES, DEFAULT_RETRY_ATTEMPTS, DEFAULT_RETRY_DELAY_MS, DEFAULT_RETRY_POLICY, DEFAULT_TIMEOUT_MS, type DataType, type IConfigField, type IConfigFieldBase, type IConfigOption, type IConfigValidation, type ICredential, type ICredentialContext, type ICredentialDescription, type ICredentialField, type ICredentialOAuthAuthorize, type ICredentialResolveResult, type ICredentialTestResult, type IImage, type IInputPort, type INode, type INodeAuthorContext, type INodeContext, type INodeDescription, type INodeResult, type INodeState, type INodeWithIteration, type IOutputField, type IOutputPort, type IShowIfCondition, type ITemplateDescription, type ITemplateTrigger, type ImageCategory, type LocalizedString, type LookupAddress, type LookupFn, MANIFEST_VERSION, MAX_REDIRECTS, MAX_RESPONSE_BYTES, MAX_RETRY_ATTEMPTS, MAX_TIMEOUT_MS, type NodeCategory, NodeError, type NodeErrorOutput, type NodeManifest, type NodePackageMeta, OAuth2AuthCodeCredential, OAuth2ClientCredentialsCredential, type OAuthTokenResponse, OPERATORS, type Operator, type OutputKind, type RetryHooks, type RetryPolicy, RetryableError, type SafeFetchOptions, type SafeFetchRetry, SimpleValueCredential, type TemplateLevel, type TemplateTriggerType, VALUELESS_OPERATORS, assertPublicUrl, backoffDelay, buildManifest, clampResponseBytes, errorPort, errorResult, evaluate, extractCredentialManifest, extractCredentialManifests, extractManifest, extractManifests, httpErrorResult, isBlockedAddress, isNodeWithIteration, isOAuthAuthorizeCredential, isOperator, maxBytesConfigField, normalizeCredentialType, normalizeLocalized, parsePackageMeta, readArrayBuffer, readJsonOrText, readText, retryConfigFields, safeFetch, settingApplies, sleepWithSignal, takesValue, timeoutConfigField, toErrorOutput, toErrorResult, withRetry };
|
package/dist/index.d.ts
CHANGED
|
@@ -650,6 +650,67 @@ declare class NodeError extends Error {
|
|
|
650
650
|
constructor(code: string, message: string, meta?: Record<string, unknown>);
|
|
651
651
|
}
|
|
652
652
|
|
|
653
|
+
/**
|
|
654
|
+
* Canonical error-output shape emitted on a node's `error` port. It lives here
|
|
655
|
+
* rather than in a node package because all four of them were building it
|
|
656
|
+
* separately — `integrations-nodes-core` in an internal module, `deepl` in a
|
|
657
|
+
* copy of that module, Business Central in `bc-fetch.ts` and Pipedrive in
|
|
658
|
+
* `pipedrive-client.ts` — so the workflow editor's guarantee of one
|
|
659
|
+
* `{ code, message, status }` payload rested on four files agreeing by hand
|
|
660
|
+
* (PO-442; asked for by PO-134 in June and not done then).
|
|
661
|
+
*/
|
|
662
|
+
interface NodeErrorOutput {
|
|
663
|
+
/** Machine-readable error code, e.g. `HTTP_ERROR`, `TIMEOUT`, `REQUEST_FAILED`. */
|
|
664
|
+
code: string;
|
|
665
|
+
message: string;
|
|
666
|
+
/** HTTP status code, or `0` for timeouts / network / non-HTTP errors. */
|
|
667
|
+
status: number;
|
|
668
|
+
/** Optional response body — HTTP-specific. */
|
|
669
|
+
body?: unknown;
|
|
670
|
+
}
|
|
671
|
+
/**
|
|
672
|
+
* Normalize any caught value into the canonical {@link NodeErrorOutput} shape.
|
|
673
|
+
* Use this in a node's `catch` block before routing to the `error` port.
|
|
674
|
+
*/
|
|
675
|
+
declare function toErrorOutput(err: unknown): NodeErrorOutput;
|
|
676
|
+
/** Wrap an error output into an `INodeResult` routed to the `error` port. */
|
|
677
|
+
declare function errorResult(output: NodeErrorOutput, port?: string): INodeResult;
|
|
678
|
+
/**
|
|
679
|
+
* Catch-block shortcut: normalize any thrown value and route it to the failure
|
|
680
|
+
* output. The one helper every node's `catch` funnels into, so the
|
|
681
|
+
* `{ code, message, status }` mapping lives in a single place instead of being
|
|
682
|
+
* re-implemented per protocol (FTP/SFTP) or per package.
|
|
683
|
+
*
|
|
684
|
+
* `port` is the output taken, `error` unless the node declares its failure
|
|
685
|
+
* output under another name (`{ ...errorPort(), name: 'failed' }`) — pass the
|
|
686
|
+
* same name here, or the branch names a port that node does not have.
|
|
687
|
+
*/
|
|
688
|
+
declare function toErrorResult(err: unknown, port?: string): INodeResult;
|
|
689
|
+
/**
|
|
690
|
+
* Catch-block shortcut for the HTTP-style nodes whose failure output additionally
|
|
691
|
+
* declares `body`: the canonical `{ code, message, status }` mapping plus the
|
|
692
|
+
* response body a {@link NodeError} carries in `meta.body` (`null` when the
|
|
693
|
+
* failure never reached a response). `port` names the output as above.
|
|
694
|
+
*/
|
|
695
|
+
declare function httpErrorResult(err: unknown, port?: string): INodeResult;
|
|
696
|
+
/**
|
|
697
|
+
* Shared `error` output port — the uniform `{ code, message, status }` triple
|
|
698
|
+
* declared on every node that has an `error` branch. Pass `body: true` for
|
|
699
|
+
* HTTP-style nodes that additionally emit a response body on the port.
|
|
700
|
+
*
|
|
701
|
+
* The sentences travel with the port. The three values mean the same thing
|
|
702
|
+
* behind every node, so they are written here once instead of per node; a node
|
|
703
|
+
* whose failures are worth naming more precisely spreads the port and replaces
|
|
704
|
+
* only its `description` (`{ ...errorPort(), description: … }`).
|
|
705
|
+
*
|
|
706
|
+
* The second sentence of the description is the rule this port exists under,
|
|
707
|
+
* and it is promised in `specs/error-handling.md`: a fault in the author's own
|
|
708
|
+
* configuration ends the run, a fault met while running is offered here.
|
|
709
|
+
*/
|
|
710
|
+
declare function errorPort(opts?: {
|
|
711
|
+
body?: boolean;
|
|
712
|
+
}): IOutputPort;
|
|
713
|
+
|
|
653
714
|
declare function extractManifest(node: INode): INodeDescription;
|
|
654
715
|
declare function extractManifests(nodes: INode[]): INodeDescription[];
|
|
655
716
|
declare function extractCredentialManifest(credential: ICredential): ICredentialDescription;
|
|
@@ -694,7 +755,8 @@ declare function safeFetch(url: string | URL, options?: SafeFetchOptions): Promi
|
|
|
694
755
|
* The `Content-Length` header is used as a fast-reject (bail before downloading
|
|
695
756
|
* anything), but the limit is *also* enforced while streaming, since the header
|
|
696
757
|
* can be absent or lie. On overrun the stream is cancelled and a
|
|
697
|
-
* `NodeError('RESPONSE_TOO_LARGE', …, { status })` is thrown.
|
|
758
|
+
* `NodeError('RESPONSE_TOO_LARGE', …, { status })` is thrown. Both refusal paths
|
|
759
|
+
* cancel the body first, so neither leaves the connection held.
|
|
698
760
|
*/
|
|
699
761
|
declare function readArrayBuffer(res: Response, maxBytes?: number): Promise<ArrayBuffer>;
|
|
700
762
|
/** Read a response body as UTF-8 text, capped at `maxBytes` (see {@link readArrayBuffer}). */
|
|
@@ -738,10 +800,19 @@ interface LookupAddress {
|
|
|
738
800
|
type LookupFn = (hostname: string) => Promise<LookupAddress[]>;
|
|
739
801
|
/**
|
|
740
802
|
* Return `true` when `ip` (a literal IPv4/IPv6 address) points at a private,
|
|
741
|
-
* loopback, link-local
|
|
742
|
-
*
|
|
743
|
-
*
|
|
803
|
+
* loopback, link-local, carrier-grade NAT, multicast, broadcast, reserved,
|
|
804
|
+
* unallocated or otherwise non-public target that a server-side fetch must never be
|
|
805
|
+
* steered to.
|
|
806
|
+
*
|
|
807
|
+
* The verdict is an allow-list: the address is refused unless the classification
|
|
808
|
+
* calls its range public unicast and — for IPv6, where that verdict is a default
|
|
809
|
+
* rather than a determination — it also sits inside the block IANA has allocated
|
|
810
|
+
* (see {@link GLOBAL_UNICAST_V6}). The IPv6 forms that embed an IPv4 address are
|
|
811
|
+
* unwrapped and the embedded address judged in its place, so a public host stays
|
|
812
|
+
* reachable when it is written that way. An address we cannot parse is treated as
|
|
744
813
|
* blocked (fail-closed).
|
|
814
|
+
*
|
|
815
|
+
* Promised behaviour: specs/ssrf-guard.md (AC-9, AC-11, AC-19).
|
|
745
816
|
*/
|
|
746
817
|
declare function isBlockedAddress(ip: string): boolean;
|
|
747
818
|
/**
|
|
@@ -752,8 +823,10 @@ declare function isBlockedAddress(ip: string): boolean;
|
|
|
752
823
|
* rejects if **any** resolved address is private/reserved. Throws
|
|
753
824
|
* `NodeError('BLOCKED_ADDRESS', …, { status: 0 })` on rejection.
|
|
754
825
|
*
|
|
755
|
-
*
|
|
756
|
-
*
|
|
826
|
+
* This is the pre-flight half of the guard: the connection that follows resolves
|
|
827
|
+
* the name again on its own, so `safeFetch` also judges the address the socket
|
|
828
|
+
* actually reached (PO-184). A caller that opens its own request after
|
|
829
|
+
* `assertPublicUrl` gets the check and not that second half.
|
|
757
830
|
*
|
|
758
831
|
* Pass `signal` (the per-request timeout/cancellation budget) so a hung or
|
|
759
832
|
* hostile DNS resolve cannot block the guard past that budget — see
|
|
@@ -952,6 +1025,12 @@ declare abstract class BaseCredential implements ICredential {
|
|
|
952
1025
|
* Note that a 301/302 answer to this POST is downgraded to a bodiless GET —
|
|
953
1026
|
* per the redirect rules, not a quirk of ours. Real token endpoints do not
|
|
954
1027
|
* redirect; one that does was never going to complete the exchange anyway.
|
|
1028
|
+
*
|
|
1029
|
+
* The third protection is the caller's own: safeFetch hands back a `Response`
|
|
1030
|
+
* and reading it is not its job, so the answer goes through {@link readText}
|
|
1031
|
+
* under {@link MAX_TOKEN_RESPONSE_BYTES} rather than a bare `res.text()`.
|
|
1032
|
+
* Both branches below read that same string, so the cap covers the refusal as
|
|
1033
|
+
* well as the token — an error body is the less predictable of the two.
|
|
955
1034
|
*/
|
|
956
1035
|
protected postForm(ctx: ICredentialContext, url: string, form: Record<string, string | undefined>, headers?: Record<string, string>): Promise<OAuthTokenResponse>;
|
|
957
1036
|
}
|
|
@@ -1028,4 +1107,4 @@ declare abstract class OAuth2AuthCodeCredential extends BaseCredential implement
|
|
|
1028
1107
|
test(_ctx: ICredentialContext, config: Config): Promise<ICredentialTestResult>;
|
|
1029
1108
|
}
|
|
1030
1109
|
|
|
1031
|
-
export { ApiKeyCredential, BaseCredential, BasicAuthCredential, type ConfigType, type CredentialAuthKind, type CredentialFieldType, DEFAULT_MAX_RESPONSE_BYTES, DEFAULT_RETRY_ATTEMPTS, DEFAULT_RETRY_DELAY_MS, DEFAULT_RETRY_POLICY, DEFAULT_TIMEOUT_MS, type DataType, type IConfigField, type IConfigFieldBase, type IConfigOption, type IConfigValidation, type ICredential, type ICredentialContext, type ICredentialDescription, type ICredentialField, type ICredentialOAuthAuthorize, type ICredentialResolveResult, type ICredentialTestResult, type IImage, type IInputPort, type INode, type INodeAuthorContext, type INodeContext, type INodeDescription, type INodeResult, type INodeState, type INodeWithIteration, type IOutputField, type IOutputPort, type IShowIfCondition, type ITemplateDescription, type ITemplateTrigger, type ImageCategory, type LocalizedString, type LookupAddress, type LookupFn, MANIFEST_VERSION, MAX_REDIRECTS, MAX_RESPONSE_BYTES, MAX_RETRY_ATTEMPTS, MAX_TIMEOUT_MS, type NodeCategory, NodeError, type NodeManifest, type NodePackageMeta, OAuth2AuthCodeCredential, OAuth2ClientCredentialsCredential, type OAuthTokenResponse, OPERATORS, type Operator, type OutputKind, type RetryHooks, type RetryPolicy, RetryableError, type SafeFetchOptions, type SafeFetchRetry, SimpleValueCredential, type TemplateLevel, type TemplateTriggerType, VALUELESS_OPERATORS, assertPublicUrl, backoffDelay, buildManifest, clampResponseBytes, evaluate, extractCredentialManifest, extractCredentialManifests, extractManifest, extractManifests, isBlockedAddress, isNodeWithIteration, isOAuthAuthorizeCredential, isOperator, maxBytesConfigField, normalizeCredentialType, normalizeLocalized, parsePackageMeta, readArrayBuffer, readJsonOrText, readText, retryConfigFields, safeFetch, settingApplies, sleepWithSignal, takesValue, timeoutConfigField, withRetry };
|
|
1110
|
+
export { ApiKeyCredential, BaseCredential, BasicAuthCredential, type ConfigType, type CredentialAuthKind, type CredentialFieldType, DEFAULT_MAX_RESPONSE_BYTES, DEFAULT_RETRY_ATTEMPTS, DEFAULT_RETRY_DELAY_MS, DEFAULT_RETRY_POLICY, DEFAULT_TIMEOUT_MS, type DataType, type IConfigField, type IConfigFieldBase, type IConfigOption, type IConfigValidation, type ICredential, type ICredentialContext, type ICredentialDescription, type ICredentialField, type ICredentialOAuthAuthorize, type ICredentialResolveResult, type ICredentialTestResult, type IImage, type IInputPort, type INode, type INodeAuthorContext, type INodeContext, type INodeDescription, type INodeResult, type INodeState, type INodeWithIteration, type IOutputField, type IOutputPort, type IShowIfCondition, type ITemplateDescription, type ITemplateTrigger, type ImageCategory, type LocalizedString, type LookupAddress, type LookupFn, MANIFEST_VERSION, MAX_REDIRECTS, MAX_RESPONSE_BYTES, MAX_RETRY_ATTEMPTS, MAX_TIMEOUT_MS, type NodeCategory, NodeError, type NodeErrorOutput, type NodeManifest, type NodePackageMeta, OAuth2AuthCodeCredential, OAuth2ClientCredentialsCredential, type OAuthTokenResponse, OPERATORS, type Operator, type OutputKind, type RetryHooks, type RetryPolicy, RetryableError, type SafeFetchOptions, type SafeFetchRetry, SimpleValueCredential, type TemplateLevel, type TemplateTriggerType, VALUELESS_OPERATORS, assertPublicUrl, backoffDelay, buildManifest, clampResponseBytes, errorPort, errorResult, evaluate, extractCredentialManifest, extractCredentialManifests, extractManifest, extractManifests, httpErrorResult, isBlockedAddress, isNodeWithIteration, isOAuthAuthorizeCredential, isOperator, maxBytesConfigField, normalizeCredentialType, normalizeLocalized, parsePackageMeta, readArrayBuffer, readJsonOrText, readText, retryConfigFields, safeFetch, settingApplies, sleepWithSignal, takesValue, timeoutConfigField, toErrorOutput, toErrorResult, withRetry };
|
package/dist/index.js
CHANGED
|
@@ -131,8 +131,80 @@ var NodeError = class extends Error {
|
|
|
131
131
|
}
|
|
132
132
|
};
|
|
133
133
|
|
|
134
|
+
// src/nodeErrors.ts
|
|
135
|
+
function toErrorOutput(err) {
|
|
136
|
+
if (err instanceof NodeError) {
|
|
137
|
+
const raw = err.meta?.["status"];
|
|
138
|
+
const status = typeof raw === "number" && Number.isFinite(raw) ? raw : 0;
|
|
139
|
+
return { code: err.code, message: err.message, status };
|
|
140
|
+
}
|
|
141
|
+
if (err instanceof Error && err.name === "AbortError") {
|
|
142
|
+
return { code: "TIMEOUT", message: "Request timed out", status: 0 };
|
|
143
|
+
}
|
|
144
|
+
if (err instanceof Error) {
|
|
145
|
+
return { code: "REQUEST_FAILED", message: err.message, status: 0 };
|
|
146
|
+
}
|
|
147
|
+
return { code: "REQUEST_FAILED", message: String(err), status: 0 };
|
|
148
|
+
}
|
|
149
|
+
function errorResult(output, port = "error") {
|
|
150
|
+
return { outputs: { [port]: output }, branch: port };
|
|
151
|
+
}
|
|
152
|
+
function toErrorResult(err, port = "error") {
|
|
153
|
+
return errorResult(toErrorOutput(err), port);
|
|
154
|
+
}
|
|
155
|
+
function httpErrorResult(err, port = "error") {
|
|
156
|
+
const body = err instanceof NodeError ? err.meta?.["body"] ?? null : null;
|
|
157
|
+
return errorResult({ ...toErrorOutput(err), body }, port);
|
|
158
|
+
}
|
|
159
|
+
function errorPort(opts = {}) {
|
|
160
|
+
return {
|
|
161
|
+
name: "error",
|
|
162
|
+
kind: "error",
|
|
163
|
+
dataType: "object",
|
|
164
|
+
label: { en: "Error", de: "Fehler" },
|
|
165
|
+
description: {
|
|
166
|
+
en: "Taken when the attempt fails in a way the workflow can carry on from. A node that cannot run as configured fails the run instead of taking this path.",
|
|
167
|
+
de: "Wird genommen, wenn der Versuch auf eine Weise fehlschl\xE4gt, mit der der Workflow weiterarbeiten kann. Ein Node, der so konfiguriert nicht laufen kann, l\xE4sst den Run scheitern, statt diesen Pfad zu nehmen."
|
|
168
|
+
},
|
|
169
|
+
fields: {
|
|
170
|
+
code: {
|
|
171
|
+
dataType: "string",
|
|
172
|
+
description: {
|
|
173
|
+
en: "Machine-readable name of the failure \u2014 branch on this rather than on the message. A node that talks to a remote system shares `TIMEOUT` and `REQUEST_FAILED` with every other one; the remaining codes belong to this node alone.",
|
|
174
|
+
de: "Maschinenlesbarer Name des Fehlers \u2014 hierauf verzweigen, nicht auf die Meldung. Ein Node, der mit einem Fremdsystem spricht, teilt `TIMEOUT` und `REQUEST_FAILED` mit allen anderen; die \xFCbrigen Codes geh\xF6ren allein zu diesem Node."
|
|
175
|
+
}
|
|
176
|
+
},
|
|
177
|
+
message: {
|
|
178
|
+
dataType: "string",
|
|
179
|
+
description: {
|
|
180
|
+
en: "Wording for whoever reads the run afterwards, frequently the wording the far end used. It is not a stable value to branch on.",
|
|
181
|
+
de: "Wortlaut f\xFCr den, der den Run sp\xE4ter liest, h\xE4ufig der des Gegen\xFCbers. Kein stabiler Wert, auf den sich Verzweigungen st\xFCtzen sollten."
|
|
182
|
+
}
|
|
183
|
+
},
|
|
184
|
+
status: {
|
|
185
|
+
dataType: "number",
|
|
186
|
+
description: {
|
|
187
|
+
en: "How the far end answered, as an HTTP status code, where one answered with a status at all. `0` otherwise \u2014 a timeout, a refused connection, an input the node could not use, a node that never leaves the workflow, or a failure the far end reported in its own answer rather than in a status, as a bulk job that ends as failed does. So `0` means there is no status to read, not that nothing was reached.",
|
|
188
|
+
de: "Wie das Gegen\xFCber geantwortet hat, als HTTP-Statuscode \u2014 sofern eines \xFCberhaupt mit einem Status geantwortet hat. Sonst `0`: Timeout, abgelehnte Verbindung, eine unbrauchbare Eingabe, ein Node, der den Workflow nicht verl\xE4sst, oder ein Fehler, den das Gegen\xFCber in seiner Antwort statt im Status gemeldet hat \u2014 wie ein Bulk-Job, der als fehlgeschlagen endet. `0` hei\xDFt also: kein Status zu lesen, nicht: nichts erreicht."
|
|
189
|
+
}
|
|
190
|
+
},
|
|
191
|
+
...opts.body ? {
|
|
192
|
+
body: {
|
|
193
|
+
dataType: "any",
|
|
194
|
+
description: {
|
|
195
|
+
en: "The answer the far end gave, where the failure got far enough to have one: the response body, or the job the platform reported as failed.",
|
|
196
|
+
de: "Die Antwort des Gegen\xFCbers, sofern der Fehler weit genug kam, um eine zu haben: der Response-Body oder der von der Plattform als fehlgeschlagen gemeldete Job."
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
} : {}
|
|
200
|
+
}
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
|
|
134
204
|
// src/ssrf.ts
|
|
205
|
+
import { subscribe } from "diagnostics_channel";
|
|
135
206
|
import { isIP } from "net";
|
|
207
|
+
import ipaddr from "ipaddr.js";
|
|
136
208
|
var ssrfResolver = {
|
|
137
209
|
lookup: async (hostname) => {
|
|
138
210
|
const { lookup } = await import("dns/promises");
|
|
@@ -140,66 +212,27 @@ var ssrfResolver = {
|
|
|
140
212
|
return results.map((r) => ({ address: r.address, family: r.family }));
|
|
141
213
|
}
|
|
142
214
|
};
|
|
143
|
-
function
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
let s = input;
|
|
152
|
-
const zone = s.indexOf("%");
|
|
153
|
-
if (zone !== -1) s = s.slice(0, zone);
|
|
154
|
-
if (s.includes(".")) {
|
|
155
|
-
const idx = s.lastIndexOf(":");
|
|
156
|
-
if (idx === -1) return null;
|
|
157
|
-
const v4 = parseIpv4(s.slice(idx + 1));
|
|
158
|
-
if (!v4) return null;
|
|
159
|
-
const hi = (v4[0] << 8 | v4[1]).toString(16);
|
|
160
|
-
const lo = (v4[2] << 8 | v4[3]).toString(16);
|
|
161
|
-
s = `${s.slice(0, idx + 1)}${hi}:${lo}`;
|
|
162
|
-
}
|
|
163
|
-
const halves = s.split("::");
|
|
164
|
-
if (halves.length > 2) return null;
|
|
165
|
-
const parseGroups = (part) => part === "" ? [] : part.split(":").map((h) => /^[0-9a-fA-F]{1,4}$/.test(h) ? parseInt(h, 16) : Number.NaN);
|
|
166
|
-
const head = parseGroups(halves[0] ?? "");
|
|
167
|
-
const back = halves.length === 2 ? parseGroups(halves[1] ?? "") : null;
|
|
168
|
-
const declared = [...head, ...back ?? []];
|
|
169
|
-
if (declared.some((h) => !Number.isInteger(h) || h < 0 || h > 65535)) return null;
|
|
170
|
-
let hextets;
|
|
171
|
-
if (back === null) {
|
|
172
|
-
hextets = head;
|
|
173
|
-
} else {
|
|
174
|
-
const zeros = 8 - (head.length + back.length);
|
|
175
|
-
if (zeros < 1) return null;
|
|
176
|
-
hextets = [...head, ...new Array(zeros).fill(0), ...back];
|
|
177
|
-
}
|
|
178
|
-
return hextets.length === 8 ? hextets : null;
|
|
179
|
-
}
|
|
180
|
-
function isBlockedIpv4(o) {
|
|
181
|
-
const [a, b] = o;
|
|
182
|
-
return a === 0 || // 0.0.0.0/8 "this network" (incl. 0.0.0.0)
|
|
183
|
-
a === 127 || // 127.0.0.0/8 loopback
|
|
184
|
-
a === 10 || // 10.0.0.0/8 private
|
|
185
|
-
a === 172 && b >= 16 && b <= 31 || // 172.16.0.0/12 private
|
|
186
|
-
a === 192 && b === 168 || // 192.168.0.0/16 private
|
|
187
|
-
a === 169 && b === 254;
|
|
215
|
+
function isIPv6(addr) {
|
|
216
|
+
return addr.kind() === "ipv6";
|
|
217
|
+
}
|
|
218
|
+
var PUBLIC_RANGE = "unicast";
|
|
219
|
+
var GLOBAL_UNICAST_V6 = ipaddr.parseCIDR("2000::/3");
|
|
220
|
+
var V4_EMBEDDING_RANGES = /* @__PURE__ */ new Set(["ipv4Mapped", "rfc6052"]);
|
|
221
|
+
function embeddedIpv4(addr) {
|
|
222
|
+
return ipaddr.fromByteArray(addr.toByteArray().slice(12));
|
|
188
223
|
}
|
|
189
224
|
function isBlockedAddress(ip) {
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
if ((h[0] & 65472) === 65152) return true;
|
|
202
|
-
return false;
|
|
225
|
+
let addr;
|
|
226
|
+
try {
|
|
227
|
+
addr = ipaddr.parse(ip);
|
|
228
|
+
} catch {
|
|
229
|
+
return true;
|
|
230
|
+
}
|
|
231
|
+
if (isIPv6(addr)) {
|
|
232
|
+
if (V4_EMBEDDING_RANGES.has(addr.range())) return embeddedIpv4(addr).range() !== PUBLIC_RANGE;
|
|
233
|
+
if (!addr.match(GLOBAL_UNICAST_V6)) return true;
|
|
234
|
+
}
|
|
235
|
+
return addr.range() !== PUBLIC_RANGE;
|
|
203
236
|
}
|
|
204
237
|
var bypassNoticeLogged = false;
|
|
205
238
|
function guardRelaxedForLocalDev() {
|
|
@@ -224,6 +257,82 @@ function blockedError(host, address) {
|
|
|
224
257
|
status: 0
|
|
225
258
|
});
|
|
226
259
|
}
|
|
260
|
+
var guardedTargets = /* @__PURE__ */ new Map();
|
|
261
|
+
var connectGuardInstalled = false;
|
|
262
|
+
function normalizeHost(hostname) {
|
|
263
|
+
const host = hostname.startsWith("[") && hostname.endsWith("]") ? hostname.slice(1, -1) : hostname;
|
|
264
|
+
return host.toLowerCase();
|
|
265
|
+
}
|
|
266
|
+
function targetKey(hostname, port, protocol) {
|
|
267
|
+
const host = normalizeHost(hostname);
|
|
268
|
+
const effective = port === "" ? protocol === "https:" ? "443" : "80" : port;
|
|
269
|
+
return `${host}:${effective}`;
|
|
270
|
+
}
|
|
271
|
+
var unreadableMessageLogged = false;
|
|
272
|
+
function reportUnreadableMessage() {
|
|
273
|
+
if (unreadableMessageLogged) return;
|
|
274
|
+
unreadableMessageLogged = true;
|
|
275
|
+
console.warn(
|
|
276
|
+
"[ssrf] undici:client:connected published a message this guard cannot read: the connect-time half of the SSRF guard is not judging connections. The pre-flight check still applies."
|
|
277
|
+
);
|
|
278
|
+
}
|
|
279
|
+
function connectBlockedError(host, address) {
|
|
280
|
+
if (address == null) {
|
|
281
|
+
console.warn(`[ssrf] dropped connection to ${host}: the address it landed on could not be read`);
|
|
282
|
+
return new NodeError(
|
|
283
|
+
"BLOCKED_ADDRESS",
|
|
284
|
+
`Blocked request to ${host}: the address the connection landed on could not be read`,
|
|
285
|
+
{ status: 0 }
|
|
286
|
+
);
|
|
287
|
+
}
|
|
288
|
+
if (host === address) {
|
|
289
|
+
return new NodeError("BLOCKED_ADDRESS", `Blocked connection to private or reserved address ${address}`, {
|
|
290
|
+
status: 0
|
|
291
|
+
});
|
|
292
|
+
}
|
|
293
|
+
console.warn(`[ssrf] dropped connection to ${host}: connected to private/reserved address ${address}`);
|
|
294
|
+
return new NodeError(
|
|
295
|
+
"BLOCKED_ADDRESS",
|
|
296
|
+
`Blocked request to ${host}: the connection landed on a private or reserved address`,
|
|
297
|
+
{ status: 0 }
|
|
298
|
+
);
|
|
299
|
+
}
|
|
300
|
+
function judgeConnection(message) {
|
|
301
|
+
const { connectParams, socket } = message ?? {};
|
|
302
|
+
const hostname = connectParams?.hostname;
|
|
303
|
+
const port = connectParams?.port;
|
|
304
|
+
const protocol = connectParams?.protocol;
|
|
305
|
+
if (hostname == null || port == null || protocol == null || typeof socket?.destroy !== "function") {
|
|
306
|
+
reportUnreadableMessage();
|
|
307
|
+
return;
|
|
308
|
+
}
|
|
309
|
+
if (!guardedTargets.has(targetKey(hostname, port, protocol))) return;
|
|
310
|
+
if (guardRelaxedForLocalDev()) return;
|
|
311
|
+
const peer = socket.remoteAddress;
|
|
312
|
+
if (peer != null && !isBlockedAddress(peer)) return;
|
|
313
|
+
socket.destroy(connectBlockedError(normalizeHost(hostname), peer));
|
|
314
|
+
}
|
|
315
|
+
function guardConnectionsTo(url) {
|
|
316
|
+
if (!connectGuardInstalled) {
|
|
317
|
+
connectGuardInstalled = true;
|
|
318
|
+
subscribe("undici:client:connected", judgeConnection);
|
|
319
|
+
}
|
|
320
|
+
const key = targetKey(url.hostname, url.port, url.protocol);
|
|
321
|
+
guardedTargets.set(key, (guardedTargets.get(key) ?? 0) + 1);
|
|
322
|
+
let released = false;
|
|
323
|
+
return () => {
|
|
324
|
+
if (released) return;
|
|
325
|
+
released = true;
|
|
326
|
+
const count = (guardedTargets.get(key) ?? 1) - 1;
|
|
327
|
+
if (count > 0) guardedTargets.set(key, count);
|
|
328
|
+
else guardedTargets.delete(key);
|
|
329
|
+
};
|
|
330
|
+
}
|
|
331
|
+
function connectionRefusal(err) {
|
|
332
|
+
const cause = err?.cause;
|
|
333
|
+
if (cause instanceof NodeError && cause.code === "BLOCKED_ADDRESS") return cause;
|
|
334
|
+
return void 0;
|
|
335
|
+
}
|
|
227
336
|
async function resolveHost(lookup, host, signal) {
|
|
228
337
|
if (!signal) return lookup(host);
|
|
229
338
|
if (signal.aborted) throw signal.reason;
|
|
@@ -290,6 +399,8 @@ async function timedFetch(url, fetchOptions, ctxSignal, effectiveMs) {
|
|
|
290
399
|
try {
|
|
291
400
|
return await fetch(url, { ...fetchOptions, signal });
|
|
292
401
|
} catch (err) {
|
|
402
|
+
const blocked = connectionRefusal(err);
|
|
403
|
+
if (blocked) throw blocked;
|
|
293
404
|
if (ctxSignal?.aborted) throw ctxSignal.reason;
|
|
294
405
|
if (ac.signal.aborted) throw new NodeError("TIMEOUT", `Request timed out after ${effectiveMs}ms`);
|
|
295
406
|
throw err;
|
|
@@ -316,7 +427,15 @@ async function guardedFetch(url, fetchOptions, ctxSignal, effectiveMs) {
|
|
|
316
427
|
let currentInit = { ...fetchOptions, redirect: "manual" };
|
|
317
428
|
await guardUrl(currentUrl, ctxSignal, effectiveMs);
|
|
318
429
|
for (let hop = 0; ; hop++) {
|
|
319
|
-
const
|
|
430
|
+
const release = guardConnectionsTo(
|
|
431
|
+
currentUrl instanceof URL ? currentUrl : new URL(currentUrl)
|
|
432
|
+
);
|
|
433
|
+
let res;
|
|
434
|
+
try {
|
|
435
|
+
res = await timedFetch(currentUrl, currentInit, ctxSignal, effectiveMs);
|
|
436
|
+
} finally {
|
|
437
|
+
release();
|
|
438
|
+
}
|
|
320
439
|
const location = res.headers.get("location");
|
|
321
440
|
if (!REDIRECT_STATUSES.has(res.status) || !location) return res;
|
|
322
441
|
const redirectStatus = res.status;
|
|
@@ -400,6 +519,8 @@ async function readArrayBuffer(res, maxBytes = DEFAULT_MAX_RESPONSE_BYTES) {
|
|
|
400
519
|
const cap = clampResponseBytes(maxBytes);
|
|
401
520
|
const declared = Number(res.headers.get("content-length"));
|
|
402
521
|
if (Number.isFinite(declared) && declared > cap) {
|
|
522
|
+
await res.body?.cancel().catch(() => {
|
|
523
|
+
});
|
|
403
524
|
throw tooLargeError(res.status, declared, cap);
|
|
404
525
|
}
|
|
405
526
|
const body = res.body;
|
|
@@ -581,6 +702,7 @@ function expiryFromExpiresIn(expiresIn) {
|
|
|
581
702
|
}
|
|
582
703
|
return new Date(Date.now() + seconds * 1e3).toISOString();
|
|
583
704
|
}
|
|
705
|
+
var MAX_TOKEN_RESPONSE_BYTES = 1024 * 1024;
|
|
584
706
|
var BaseCredential = class {
|
|
585
707
|
async test(_ctx, config) {
|
|
586
708
|
for (const field of this.description.fields) {
|
|
@@ -608,6 +730,12 @@ var BaseCredential = class {
|
|
|
608
730
|
* Note that a 301/302 answer to this POST is downgraded to a bodiless GET —
|
|
609
731
|
* per the redirect rules, not a quirk of ours. Real token endpoints do not
|
|
610
732
|
* redirect; one that does was never going to complete the exchange anyway.
|
|
733
|
+
*
|
|
734
|
+
* The third protection is the caller's own: safeFetch hands back a `Response`
|
|
735
|
+
* and reading it is not its job, so the answer goes through {@link readText}
|
|
736
|
+
* under {@link MAX_TOKEN_RESPONSE_BYTES} rather than a bare `res.text()`.
|
|
737
|
+
* Both branches below read that same string, so the cap covers the refusal as
|
|
738
|
+
* well as the token — an error body is the less predictable of the two.
|
|
611
739
|
*/
|
|
612
740
|
async postForm(ctx, url, form, headers = {}) {
|
|
613
741
|
const body = new URLSearchParams();
|
|
@@ -622,7 +750,7 @@ var BaseCredential = class {
|
|
|
622
750
|
headers: { "Content-Type": "application/x-www-form-urlencoded", Accept: "application/json", ...headers },
|
|
623
751
|
body
|
|
624
752
|
});
|
|
625
|
-
const text = await res
|
|
753
|
+
const text = await readText(res, MAX_TOKEN_RESPONSE_BYTES);
|
|
626
754
|
if (!res.ok) {
|
|
627
755
|
throw new Error(`token endpoint ${url} -> ${res.status}${formatOAuthError(text)}`);
|
|
628
756
|
}
|
|
@@ -808,11 +936,14 @@ export {
|
|
|
808
936
|
backoffDelay,
|
|
809
937
|
buildManifest,
|
|
810
938
|
clampResponseBytes,
|
|
939
|
+
errorPort,
|
|
940
|
+
errorResult,
|
|
811
941
|
evaluate,
|
|
812
942
|
extractCredentialManifest,
|
|
813
943
|
extractCredentialManifests,
|
|
814
944
|
extractManifest,
|
|
815
945
|
extractManifests,
|
|
946
|
+
httpErrorResult,
|
|
816
947
|
isBlockedAddress,
|
|
817
948
|
isNodeWithIteration,
|
|
818
949
|
isOAuthAuthorizeCredential,
|
|
@@ -830,5 +961,7 @@ export {
|
|
|
830
961
|
sleepWithSignal,
|
|
831
962
|
takesValue,
|
|
832
963
|
timeoutConfigField,
|
|
964
|
+
toErrorOutput,
|
|
965
|
+
toErrorResult,
|
|
833
966
|
withRetry
|
|
834
967
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@revenexx/integrations-node-sdk",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.3.0",
|
|
4
4
|
"description": "TypeScript interfaces and utilities for Revenexx integration nodes",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "revenexx GmbH",
|
|
@@ -61,5 +61,8 @@
|
|
|
61
61
|
"tsup": "^8.0.0",
|
|
62
62
|
"tsx": "^4.0.0",
|
|
63
63
|
"typescript": "^5.0.0"
|
|
64
|
+
},
|
|
65
|
+
"dependencies": {
|
|
66
|
+
"ipaddr.js": "2.5.0"
|
|
64
67
|
}
|
|
65
68
|
}
|