@workglow/tasks 0.2.3 → 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/dist/browser.js +26 -6
- package/dist/browser.js.map +5 -5
- package/dist/bun.js +35 -7
- package/dist/bun.js.map +6 -6
- package/dist/node.js +35 -7
- package/dist/node.js.map +6 -6
- package/dist/task/FetchUrlTask.d.ts.map +1 -1
- package/dist/util/SafeFetch.d.ts +16 -0
- package/dist/util/SafeFetch.d.ts.map +1 -1
- package/dist/util/SafeFetch.server.d.ts.map +1 -1
- package/dist/util/UrlClassifier.d.ts +15 -0
- package/dist/util/UrlClassifier.d.ts.map +1 -1
- package/package.json +10 -12
package/dist/node.js
CHANGED
|
@@ -161,6 +161,7 @@ import { lookup as dnsLookup } from "node:dns/promises";
|
|
|
161
161
|
import { Agent, fetch as undiciFetch } from "undici";
|
|
162
162
|
|
|
163
163
|
// src/util/UrlClassifier.ts
|
|
164
|
+
import { resourcePatternMatches } from "@workglow/task-graph";
|
|
164
165
|
import ipaddr from "ipaddr.js";
|
|
165
166
|
var PRIVATE_EXACT_HOSTS = new Set([
|
|
166
167
|
"localhost",
|
|
@@ -374,28 +375,44 @@ function urlResourcePattern(urlStr) {
|
|
|
374
375
|
const origin = parsed.port.length > 0 ? `${parsed.protocol}//${parsed.host}` : `${parsed.protocol}//${parsed.hostname}`;
|
|
375
376
|
return `${origin}/*`;
|
|
376
377
|
}
|
|
378
|
+
function urlMatchesScope(url, patterns) {
|
|
379
|
+
let canonical;
|
|
380
|
+
try {
|
|
381
|
+
const parsed = new URL(url);
|
|
382
|
+
parsed.hostname = normalizeHost(parsed.hostname);
|
|
383
|
+
canonical = parsed.toString();
|
|
384
|
+
} catch {
|
|
385
|
+
return false;
|
|
386
|
+
}
|
|
387
|
+
return patterns.some((pat) => resourcePatternMatches(pat, canonical));
|
|
388
|
+
}
|
|
377
389
|
|
|
378
390
|
// src/util/SafeFetch.ts
|
|
379
391
|
import { PermanentJobError } from "@workglow/job-queue";
|
|
380
392
|
var MAX_REDIRECT_HOPS = 20;
|
|
381
|
-
function assertAllowedUrl(url, allowPrivate) {
|
|
393
|
+
function assertAllowedUrl(url, allowPrivate, privateResourceScopes) {
|
|
382
394
|
const classification = classifyUrl(url);
|
|
383
395
|
if (classification.kind === "invalid") {
|
|
384
396
|
throw new PermanentJobError(`Refusing to fetch invalid URL: ${classification.reason}`);
|
|
385
397
|
}
|
|
386
|
-
if (classification.kind
|
|
398
|
+
if (classification.kind !== "private")
|
|
399
|
+
return;
|
|
400
|
+
if (!allowPrivate) {
|
|
387
401
|
throw new PermanentJobError(`Refusing to fetch private/internal URL ${url}: ${classification.reason}. ` + `Grant the 'network:private' entitlement to allow this request.`);
|
|
388
402
|
}
|
|
403
|
+
if (privateResourceScopes !== undefined && !urlMatchesScope(url, privateResourceScopes)) {
|
|
404
|
+
throw new PermanentJobError(`Refusing to fetch private/internal URL ${url}: outside granted network:private scope ` + `[${privateResourceScopes.join(", ")}]. A compromised upstream may be attempting ` + `to escape the task's authorized private-host origin.`);
|
|
405
|
+
}
|
|
389
406
|
}
|
|
390
407
|
function isRedirectStatus(status) {
|
|
391
408
|
return status === 301 || status === 302 || status === 303 || status === 307 || status === 308;
|
|
392
409
|
}
|
|
393
410
|
async function defaultSafeFetch(url, options) {
|
|
394
411
|
const requestedRedirectMode = options.redirect ?? "follow";
|
|
395
|
-
const { allowPrivate, redirect: _redirect, ...fetchOptions } = options;
|
|
412
|
+
const { allowPrivate, privateResourceScopes, redirect: _redirect, ...fetchOptions } = options;
|
|
396
413
|
let currentUrl = url;
|
|
397
414
|
for (let hops = 0;hops <= MAX_REDIRECT_HOPS; hops += 1) {
|
|
398
|
-
assertAllowedUrl(currentUrl, allowPrivate);
|
|
415
|
+
assertAllowedUrl(currentUrl, allowPrivate, privateResourceScopes);
|
|
399
416
|
const response = await globalThis.fetch(currentUrl, {
|
|
400
417
|
...fetchOptions,
|
|
401
418
|
redirect: "manual"
|
|
@@ -465,6 +482,9 @@ async function fetchOneHop(url, opts, fetchInit) {
|
|
|
465
482
|
if (classification.kind === "private" && !opts.allowPrivate) {
|
|
466
483
|
throw new PermanentJobError2(`Refusing to fetch private/internal URL ${url}: ${classification.reason}. ` + `Grant the 'network:private' entitlement to allow this request.`);
|
|
467
484
|
}
|
|
485
|
+
if (classification.kind === "private" && opts.privateResourceScopes !== undefined && !urlMatchesScope(url, opts.privateResourceScopes)) {
|
|
486
|
+
throw new PermanentJobError2(`Refusing to fetch ${url}: outside granted network:private scope ` + `[${opts.privateResourceScopes.join(", ")}]. A compromised upstream may be attempting ` + `to escape the task's authorized private-host origin.`);
|
|
487
|
+
}
|
|
468
488
|
const parsed = new URL(url);
|
|
469
489
|
const host = classification.host ?? parsed.hostname.toLowerCase();
|
|
470
490
|
let pinned;
|
|
@@ -510,7 +530,12 @@ async function fetchOneHop(url, opts, fetchInit) {
|
|
|
510
530
|
var serverSafeFetch = async (url, options) => {
|
|
511
531
|
const opts = options ?? {};
|
|
512
532
|
const requestedRedirectMode = opts.redirect ?? "follow";
|
|
513
|
-
const {
|
|
533
|
+
const {
|
|
534
|
+
allowPrivate: _allowPrivate,
|
|
535
|
+
privateResourceScopes: _privateResourceScopes,
|
|
536
|
+
redirect: _redirect,
|
|
537
|
+
...fetchInit
|
|
538
|
+
} = opts;
|
|
514
539
|
let currentUrl = url;
|
|
515
540
|
let prevDispatcher;
|
|
516
541
|
for (let hops = 0;hops <= MAX_REDIRECT_HOPS2; hops += 1) {
|
|
@@ -2432,12 +2457,14 @@ class FetchUrlJob extends Job {
|
|
|
2432
2457
|
if (classification.kind === "invalid") {
|
|
2433
2458
|
throw new PermanentJobError3(`Refusing to fetch invalid URL ${input.url}: ${classification.reason ?? "malformed"}`);
|
|
2434
2459
|
}
|
|
2460
|
+
const isPrivate = classification.kind === "private";
|
|
2435
2461
|
const response = await fetchWithProgress(input.url, {
|
|
2436
2462
|
method: input.method,
|
|
2437
2463
|
headers: input.headers,
|
|
2438
2464
|
body: input.body,
|
|
2439
2465
|
signal: context.signal,
|
|
2440
|
-
allowPrivate:
|
|
2466
|
+
allowPrivate: isPrivate,
|
|
2467
|
+
privateResourceScopes: isPrivate ? [urlResourcePattern(input.url)] : undefined
|
|
2441
2468
|
}, async (progress) => await context.updateProgress(progress));
|
|
2442
2469
|
if (response.ok) {
|
|
2443
2470
|
const contentType = response.headers.get("content-type") ?? "";
|
|
@@ -12554,6 +12581,7 @@ var registerCommonTasks2 = () => {
|
|
|
12554
12581
|
};
|
|
12555
12582
|
export {
|
|
12556
12583
|
urlResourcePattern,
|
|
12584
|
+
urlMatchesScope,
|
|
12557
12585
|
tryNormalizeIPv4,
|
|
12558
12586
|
split,
|
|
12559
12587
|
setGlobalMcpServerRepository,
|
|
@@ -12703,4 +12731,4 @@ export {
|
|
|
12703
12731
|
ArrayTask
|
|
12704
12732
|
};
|
|
12705
12733
|
|
|
12706
|
-
//# debugId=
|
|
12734
|
+
//# debugId=265ADF95A5E9A9D764756E2164756E21
|