@workglow/tasks 0.2.4 → 0.2.6

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/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 === "private" && !allowPrivate) {
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 { allowPrivate: _allowPrivate, redirect: _redirect, ...fetchInit } = opts;
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: classification.kind === "private"
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=4A1D9A65F809EEAD64756E2164756E21
12734
+ //# debugId=265ADF95A5E9A9D764756E2164756E21