@workglow/tasks 0.2.2 → 0.2.3

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/bun.js CHANGED
@@ -3,16 +3,70 @@ var __require = import.meta.require;
3
3
 
4
4
  // src/task/image/imageRasterCodecNode.ts
5
5
  import { parseDataUri } from "@workglow/util/media";
6
- function normalizeMimeType(mimeType) {
7
- const m = mimeType.toLowerCase();
8
- if (m.includes("jpeg") || m.includes("jpg")) {
6
+
7
+ // src/task/image/imageCodecLimits.ts
8
+ var MAX_DECODED_PIXELS = 1e8;
9
+ var MAX_INPUT_BYTES_NODE = 64 * 1024 * 1024;
10
+ var MAX_INPUT_BYTES_BROWSER = 8 * 1024 * 1024;
11
+ var REJECTED_DECODE_MIME_TYPES = new Set([
12
+ "image/svg+xml",
13
+ "image/svg",
14
+ "image/gif",
15
+ "image/apng"
16
+ ]);
17
+ var SUPPORTED_OUTPUT_MIME_TYPES = ["image/jpeg", "image/png", "image/webp"];
18
+ function assertWithinPixelBudget(width, height) {
19
+ if (!Number.isFinite(width) || !Number.isFinite(height) || width <= 0 || height <= 0) {
20
+ throw new Error(`Image raster codec: invalid dimensions ${width}x${height}`);
21
+ }
22
+ const pixels = width * height;
23
+ if (pixels > MAX_DECODED_PIXELS) {
24
+ throw new Error(`Image raster codec: decoded image exceeds pixel budget ` + `(${width}x${height} = ${pixels} > ${MAX_DECODED_PIXELS})`);
25
+ }
26
+ }
27
+ function assertWithinByteBudget(byteLength, limit) {
28
+ if (byteLength > limit) {
29
+ throw new Error(`Image raster codec: input exceeds byte budget (${byteLength} > ${limit})`);
30
+ }
31
+ }
32
+ function formatDataUriErrorPreview(value) {
33
+ if (typeof value !== "string") {
34
+ return String(value);
35
+ }
36
+ if (value.startsWith("data:")) {
37
+ const commaIndex = value.indexOf(",");
38
+ if (commaIndex >= 0) {
39
+ return `${value.slice(0, commaIndex)},[REDACTED]`;
40
+ }
41
+ return `${value.slice(0, 80)}${value.length > 80 ? "\u2026" : ""}`;
42
+ }
43
+ return `${value.slice(0, 80)}${value.length > 80 ? "\u2026" : ""}`;
44
+ }
45
+ function assertIsDataUri(value) {
46
+ if (typeof value !== "string" || !value.startsWith("data:")) {
47
+ const preview = formatDataUriErrorPreview(value);
48
+ throw new Error(`Image raster codec: expected a data: URI but received "${preview}"`);
49
+ }
50
+ }
51
+ function extractDataUriMimeType(dataUri) {
52
+ const match = dataUri.match(/^data:([^;,]+)/);
53
+ return match?.[1]?.trim().toLowerCase();
54
+ }
55
+ function normalizeOutputMimeType(mimeType) {
56
+ const m = mimeType.toLowerCase().trim();
57
+ if (m === "image/jpeg" || m === "image/jpg") {
9
58
  return "image/jpeg";
10
59
  }
11
- if (m.includes("webp")) {
60
+ if (m === "image/png") {
61
+ return "image/png";
62
+ }
63
+ if (m === "image/webp") {
12
64
  return "image/webp";
13
65
  }
14
- return "image/png";
66
+ throw new Error(`Image raster codec: unsupported output mime type "${mimeType}". ` + `Supported: ${SUPPORTED_OUTPUT_MIME_TYPES.join(", ")}.`);
15
67
  }
68
+
69
+ // src/task/image/imageRasterCodecNode.ts
16
70
  function expandGrayAlphaToRgba(src, width, height) {
17
71
  const n = width * height;
18
72
  const dst = new Uint8ClampedArray(n * 4);
@@ -38,13 +92,24 @@ async function getSharp() {
38
92
  return _sharp;
39
93
  }
40
94
  async function decodeDataUri(dataUri) {
95
+ assertIsDataUri(dataUri);
96
+ const declaredMime = extractDataUriMimeType(dataUri);
97
+ if (declaredMime && REJECTED_DECODE_MIME_TYPES.has(declaredMime)) {
98
+ throw new Error(`Image raster codec: refusing to rasterize "${declaredMime}". Vector and animated formats lose information when converted to pixels. Convert to PNG, JPEG, or WebP before passing to the codec.`);
99
+ }
41
100
  const sharp = await getSharp();
42
101
  const { base64 } = parseDataUri(dataUri);
102
+ const estimatedBytes = Math.ceil(base64.length * 3 / 4);
103
+ assertWithinByteBudget(estimatedBytes, MAX_INPUT_BYTES_NODE);
43
104
  const buffer = Buffer.from(base64, "base64");
44
- const { data, info } = await sharp(buffer).raw().toBuffer({ resolveWithObject: true });
105
+ const { data, info } = await sharp(buffer, {
106
+ limitInputPixels: MAX_DECODED_PIXELS,
107
+ sequentialRead: true
108
+ }).raw().toBuffer({ resolveWithObject: true });
45
109
  const width = info.width;
46
110
  const height = info.height;
47
111
  const ch = info.channels;
112
+ assertWithinPixelBudget(width, height);
48
113
  if (ch === 2) {
49
114
  return {
50
115
  data: expandGrayAlphaToRgba(data, width, height),
@@ -55,7 +120,7 @@ async function decodeDataUri(dataUri) {
55
120
  }
56
121
  if (ch === 1 || ch === 3 || ch === 4) {
57
122
  return {
58
- data: new Uint8ClampedArray(data.buffer, data.byteOffset, data.byteLength),
123
+ data: new Uint8ClampedArray(data),
59
124
  width,
60
125
  height,
61
126
  channels: ch
@@ -66,7 +131,7 @@ async function decodeDataUri(dataUri) {
66
131
  async function encodeDataUri(image, mimeType) {
67
132
  const sharp = await getSharp();
68
133
  const { data, width, height, channels } = image;
69
- const fmt = normalizeMimeType(mimeType);
134
+ const fmt = normalizeOutputMimeType(mimeType);
70
135
  const base = sharp(Buffer.from(data), { raw: { width, height, channels } });
71
136
  const out = fmt === "image/jpeg" ? await base.jpeg({ quality: 92, mozjpeg: true }).toBuffer() : fmt === "image/webp" ? await base.webp({ quality: 92 }).toBuffer() : await base.png({ compressionLevel: 6 }).toBuffer();
72
137
  return `data:${fmt};base64,${out.toString("base64")}`;
@@ -90,6 +155,405 @@ function getImageRasterCodec() {
90
155
  // src/task/image/registerImageRasterCodec.node.ts
91
156
  registerImageRasterCodec(createNodeImageRasterCodec());
92
157
 
158
+ // src/util/SafeFetch.server.ts
159
+ import { PermanentJobError as PermanentJobError2 } from "@workglow/job-queue";
160
+ import { lookup as dnsLookup } from "dns/promises";
161
+ import { Agent, fetch as undiciFetch } from "undici";
162
+
163
+ // src/util/UrlClassifier.ts
164
+ import ipaddr from "ipaddr.js";
165
+ var PRIVATE_EXACT_HOSTS = new Set([
166
+ "localhost",
167
+ "localhost.localdomain",
168
+ "ip6-localhost",
169
+ "ip6-loopback",
170
+ "metadata.google.internal",
171
+ "metadata.internal",
172
+ "metadata.azure.com",
173
+ "instance-data"
174
+ ]);
175
+ var PRIVATE_DOMAIN_SUFFIXES = [
176
+ "local",
177
+ "localhost",
178
+ "internal",
179
+ "lan",
180
+ "home.arpa",
181
+ "corp",
182
+ "intranet",
183
+ "private",
184
+ "localdomain"
185
+ ];
186
+ var PRIVATE_IPV4_RANGES = new Set([
187
+ "unspecified",
188
+ "broadcast",
189
+ "multicast",
190
+ "linkLocal",
191
+ "loopback",
192
+ "carrierGradeNat",
193
+ "private",
194
+ "reserved",
195
+ "benchmarking"
196
+ ]);
197
+ var PRIVATE_IPV6_RANGES = new Set([
198
+ "unspecified",
199
+ "linkLocal",
200
+ "multicast",
201
+ "loopback",
202
+ "uniqueLocal",
203
+ "ipv4Mapped",
204
+ "ipv4Compat",
205
+ "rfc6145",
206
+ "rfc6052",
207
+ "6to4",
208
+ "teredo",
209
+ "reserved",
210
+ "benchmarking",
211
+ "amt",
212
+ "as112v6",
213
+ "deprecated",
214
+ "orchid2",
215
+ "droneRemoteIdProtocolEntityTags"
216
+ ]);
217
+ function tryNormalizeIPv4(host) {
218
+ if (host.length === 0)
219
+ return;
220
+ if (/^\d{1,3}(\.\d{1,3}){3}$/.test(host)) {
221
+ const octets = host.split(".").map((s) => parseInt(s, 10));
222
+ if (octets.every((n) => n >= 0 && n <= 255)) {
223
+ return octets.join(".");
224
+ }
225
+ return;
226
+ }
227
+ const parts = host.split(".");
228
+ if (parts.length < 1 || parts.length > 4)
229
+ return;
230
+ const nums = [];
231
+ for (const p of parts) {
232
+ if (p.length === 0)
233
+ return;
234
+ let n;
235
+ if (/^0[xX][0-9a-fA-F]+$/.test(p)) {
236
+ n = parseInt(p.slice(2), 16);
237
+ } else if (/^0[0-7]+$/.test(p)) {
238
+ n = parseInt(p, 8);
239
+ } else if (/^\d+$/.test(p)) {
240
+ n = parseInt(p, 10);
241
+ } else {
242
+ return;
243
+ }
244
+ if (!Number.isFinite(n) || n < 0)
245
+ return;
246
+ nums.push(n);
247
+ }
248
+ let addr;
249
+ if (nums.length === 1) {
250
+ if (nums[0] > 4294967295)
251
+ return;
252
+ addr = nums[0];
253
+ } else if (nums.length === 2) {
254
+ if (nums[0] > 255 || nums[1] > 16777215)
255
+ return;
256
+ addr = nums[0] * 16777216 + nums[1];
257
+ } else if (nums.length === 3) {
258
+ if (nums[0] > 255 || nums[1] > 255 || nums[2] > 65535)
259
+ return;
260
+ addr = nums[0] * 16777216 + nums[1] * 65536 + nums[2];
261
+ } else {
262
+ if (nums.some((n) => n > 255))
263
+ return;
264
+ addr = nums[0] * 16777216 + nums[1] * 65536 + nums[2] * 256 + nums[3];
265
+ }
266
+ const o1 = Math.floor(addr / 16777216) & 255;
267
+ const o2 = Math.floor(addr / 65536) & 255;
268
+ const o3 = Math.floor(addr / 256) & 255;
269
+ const o4 = addr & 255;
270
+ return `${o1}.${o2}.${o3}.${o4}`;
271
+ }
272
+ function classifyIpLiteral(host) {
273
+ if (host.includes(":")) {
274
+ let ipv6;
275
+ try {
276
+ ipv6 = ipaddr.IPv6.parse(host);
277
+ } catch {
278
+ return;
279
+ }
280
+ if (ipv6.isIPv4MappedAddress()) {
281
+ const v4 = ipv6.toIPv4Address();
282
+ const range3 = v4.range();
283
+ const canonical3 = v4.toNormalizedString();
284
+ if (PRIVATE_IPV4_RANGES.has(range3)) {
285
+ return { kind: "private", reason: `IPv4-mapped IPv6 in ${range3} range`, canonical: canonical3 };
286
+ }
287
+ return { kind: "public", canonical: canonical3 };
288
+ }
289
+ const range2 = ipv6.range();
290
+ const canonical2 = ipv6.toNormalizedString();
291
+ if (PRIVATE_IPV6_RANGES.has(range2)) {
292
+ return { kind: "private", reason: `IPv6 in ${range2} range`, canonical: canonical2 };
293
+ }
294
+ return { kind: "public", canonical: canonical2 };
295
+ }
296
+ const canonical = tryNormalizeIPv4(host);
297
+ if (canonical === undefined)
298
+ return;
299
+ let ipv4;
300
+ try {
301
+ ipv4 = ipaddr.IPv4.parse(canonical);
302
+ } catch {
303
+ return;
304
+ }
305
+ const range = ipv4.range();
306
+ if (PRIVATE_IPV4_RANGES.has(range)) {
307
+ return { kind: "private", reason: `IPv4 in ${range} range`, canonical };
308
+ }
309
+ return { kind: "public", canonical };
310
+ }
311
+ function normalizeHost(host) {
312
+ let h = host;
313
+ if (h.startsWith("[") && h.endsWith("]")) {
314
+ h = h.slice(1, -1);
315
+ }
316
+ while (h.endsWith(".")) {
317
+ h = h.slice(0, -1);
318
+ }
319
+ return h.toLowerCase();
320
+ }
321
+ function matchesPrivateHostnamePattern(host) {
322
+ if (PRIVATE_EXACT_HOSTS.has(host)) {
323
+ return `host '${host}' is a reserved private name`;
324
+ }
325
+ for (const suffix of PRIVATE_DOMAIN_SUFFIXES) {
326
+ if (host === suffix || host.endsWith("." + suffix)) {
327
+ return `host matches private suffix '.${suffix}'`;
328
+ }
329
+ }
330
+ return;
331
+ }
332
+ function classifyUrl(urlStr) {
333
+ if (typeof urlStr !== "string" || urlStr.length === 0) {
334
+ return { kind: "invalid", reason: "empty or non-string URL" };
335
+ }
336
+ let parsed;
337
+ try {
338
+ parsed = new URL(urlStr);
339
+ } catch {
340
+ return { kind: "invalid", reason: "malformed URL" };
341
+ }
342
+ if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
343
+ return { kind: "invalid", reason: `unsupported protocol '${parsed.protocol}'` };
344
+ }
345
+ if (parsed.username.length > 0 || parsed.password.length > 0) {
346
+ return { kind: "invalid", reason: "URL credentials are not allowed" };
347
+ }
348
+ const host = normalizeHost(parsed.hostname);
349
+ if (host.length === 0) {
350
+ return { kind: "invalid", reason: "empty host" };
351
+ }
352
+ const ipClassification = classifyIpLiteral(host);
353
+ if (ipClassification !== undefined) {
354
+ return {
355
+ kind: ipClassification.kind,
356
+ reason: ipClassification.reason,
357
+ host,
358
+ literalIp: ipClassification.canonical
359
+ };
360
+ }
361
+ const hostnameReason = matchesPrivateHostnamePattern(host);
362
+ if (hostnameReason !== undefined) {
363
+ return { kind: "private", reason: hostnameReason, host };
364
+ }
365
+ return { kind: "public", host };
366
+ }
367
+ function urlResourcePattern(urlStr) {
368
+ let parsed;
369
+ try {
370
+ parsed = new URL(urlStr);
371
+ } catch {
372
+ return urlStr;
373
+ }
374
+ const origin = parsed.port.length > 0 ? `${parsed.protocol}//${parsed.host}` : `${parsed.protocol}//${parsed.hostname}`;
375
+ return `${origin}/*`;
376
+ }
377
+
378
+ // src/util/SafeFetch.ts
379
+ import { PermanentJobError } from "@workglow/job-queue";
380
+ var MAX_REDIRECT_HOPS = 20;
381
+ function assertAllowedUrl(url, allowPrivate) {
382
+ const classification = classifyUrl(url);
383
+ if (classification.kind === "invalid") {
384
+ throw new PermanentJobError(`Refusing to fetch invalid URL: ${classification.reason}`);
385
+ }
386
+ if (classification.kind === "private" && !allowPrivate) {
387
+ throw new PermanentJobError(`Refusing to fetch private/internal URL ${url}: ${classification.reason}. ` + `Grant the 'network:private' entitlement to allow this request.`);
388
+ }
389
+ }
390
+ function isRedirectStatus(status) {
391
+ return status === 301 || status === 302 || status === 303 || status === 307 || status === 308;
392
+ }
393
+ async function defaultSafeFetch(url, options) {
394
+ const requestedRedirectMode = options.redirect ?? "follow";
395
+ const { allowPrivate, redirect: _redirect, ...fetchOptions } = options;
396
+ let currentUrl = url;
397
+ for (let hops = 0;hops <= MAX_REDIRECT_HOPS; hops += 1) {
398
+ assertAllowedUrl(currentUrl, allowPrivate);
399
+ const response = await globalThis.fetch(currentUrl, {
400
+ ...fetchOptions,
401
+ redirect: "manual"
402
+ });
403
+ if (!isRedirectStatus(response.status)) {
404
+ return response;
405
+ }
406
+ if (requestedRedirectMode === "manual") {
407
+ return response;
408
+ }
409
+ if (requestedRedirectMode === "error") {
410
+ throw new TypeError(`Fetch for ${currentUrl} failed because redirect mode was set to 'error'.`);
411
+ }
412
+ const location = response.headers.get("location");
413
+ if (!location) {
414
+ throw new PermanentJobError(`Refusing to follow redirect from ${currentUrl}: missing Location header.`);
415
+ }
416
+ currentUrl = new URL(location, currentUrl).toString();
417
+ }
418
+ throw new PermanentJobError(`Refusing to fetch ${url}: too many redirects.`);
419
+ }
420
+ var currentImpl = defaultSafeFetch;
421
+ function registerSafeFetch(fn) {
422
+ const previousImpl = currentImpl;
423
+ currentImpl = fn;
424
+ return previousImpl;
425
+ }
426
+ function getSafeFetchImpl() {
427
+ return currentImpl;
428
+ }
429
+ function resetSafeFetch() {
430
+ currentImpl = defaultSafeFetch;
431
+ }
432
+ function safeFetch(url, options = {}) {
433
+ return currentImpl(url, options);
434
+ }
435
+
436
+ // src/util/SafeFetch.server.ts
437
+ var MAX_REDIRECT_HOPS2 = 20;
438
+ async function resolveAll(hostname) {
439
+ try {
440
+ const addrs = await dnsLookup(hostname, { all: true, verbatim: true });
441
+ if (!Array.isArray(addrs) || addrs.length === 0) {
442
+ throw new PermanentJobError2(`DNS lookup returned no addresses for '${hostname}'`);
443
+ }
444
+ return addrs.map((a) => ({ address: a.address, family: a.family }));
445
+ } catch (err) {
446
+ if (err instanceof PermanentJobError2)
447
+ throw err;
448
+ const msg = err instanceof Error ? err.message : String(err);
449
+ throw new PermanentJobError2(`DNS lookup failed for '${hostname}': ${msg}`);
450
+ }
451
+ }
452
+ function isLiteralHost(host) {
453
+ if (host.includes(":"))
454
+ return true;
455
+ return /^[0-9a-fA-FxX.]+$/.test(host);
456
+ }
457
+ function isRedirectStatus2(status) {
458
+ return status === 301 || status === 302 || status === 303 || status === 307 || status === 308;
459
+ }
460
+ async function fetchOneHop(url, opts, fetchInit) {
461
+ const classification = classifyUrl(url);
462
+ if (classification.kind === "invalid") {
463
+ throw new PermanentJobError2(`Refusing to fetch invalid URL: ${classification.reason}`);
464
+ }
465
+ if (classification.kind === "private" && !opts.allowPrivate) {
466
+ throw new PermanentJobError2(`Refusing to fetch private/internal URL ${url}: ${classification.reason}. ` + `Grant the 'network:private' entitlement to allow this request.`);
467
+ }
468
+ const parsed = new URL(url);
469
+ const host = classification.host ?? parsed.hostname.toLowerCase();
470
+ let pinned;
471
+ if (isLiteralHost(host) && classification.literalIp !== undefined) {
472
+ pinned = {
473
+ address: classification.literalIp,
474
+ family: classification.literalIp.includes(":") ? 6 : 4
475
+ };
476
+ } else {
477
+ const addrs = await resolveAll(host);
478
+ for (const addr of addrs) {
479
+ const ipClass = classifyIpLiteral(addr.address);
480
+ if (ipClass === undefined) {
481
+ throw new PermanentJobError2(`DNS resolved '${host}' to an unparseable address '${addr.address}'`);
482
+ }
483
+ if (ipClass.kind === "private" && !opts.allowPrivate) {
484
+ throw new PermanentJobError2(`Refusing to fetch ${url}: hostname '${host}' resolved to private address ` + `${addr.address} (${ipClass.reason}). This may indicate DNS rebinding. ` + `Grant the 'network:private' entitlement to allow this request.`);
485
+ }
486
+ }
487
+ pinned = addrs[0];
488
+ }
489
+ const pinnedAddress = pinned.address;
490
+ const pinnedFamily = pinned.family;
491
+ const dispatcher = new Agent({
492
+ connect: {
493
+ lookup: (_hostname, _lookupOptions, cb) => {
494
+ cb(null, pinnedAddress, pinnedFamily);
495
+ }
496
+ }
497
+ });
498
+ try {
499
+ const response = await undiciFetch(url, {
500
+ ...fetchInit,
501
+ dispatcher,
502
+ redirect: "manual"
503
+ });
504
+ return { response, dispatcher };
505
+ } catch (err) {
506
+ await dispatcher.close().catch(() => {});
507
+ throw err;
508
+ }
509
+ }
510
+ var serverSafeFetch = async (url, options) => {
511
+ const opts = options ?? {};
512
+ const requestedRedirectMode = opts.redirect ?? "follow";
513
+ const { allowPrivate: _allowPrivate, redirect: _redirect, ...fetchInit } = opts;
514
+ let currentUrl = url;
515
+ let prevDispatcher;
516
+ for (let hops = 0;hops <= MAX_REDIRECT_HOPS2; hops += 1) {
517
+ const { response, dispatcher } = await fetchOneHop(currentUrl, opts, fetchInit);
518
+ if (prevDispatcher !== undefined) {
519
+ prevDispatcher.close().catch(() => {});
520
+ }
521
+ if (!isRedirectStatus2(response.status)) {
522
+ const body = response.body;
523
+ if (body !== null) {
524
+ const { readable, writable } = new TransformStream;
525
+ body.pipeTo(writable).finally(() => {
526
+ dispatcher.close().catch(() => {});
527
+ });
528
+ return new Response(readable, {
529
+ status: response.status,
530
+ statusText: response.statusText,
531
+ headers: response.headers
532
+ });
533
+ }
534
+ dispatcher.close().catch(() => {});
535
+ return response;
536
+ }
537
+ if (requestedRedirectMode === "manual") {
538
+ dispatcher.close().catch(() => {});
539
+ return response;
540
+ }
541
+ if (requestedRedirectMode === "error") {
542
+ dispatcher.close().catch(() => {});
543
+ throw new TypeError(`Fetch for ${currentUrl} failed because redirect mode was set to 'error'.`);
544
+ }
545
+ const location = response.headers.get("location");
546
+ if (!location) {
547
+ dispatcher.close().catch(() => {});
548
+ throw new PermanentJobError2(`Refusing to follow redirect from ${currentUrl}: missing Location header.`);
549
+ }
550
+ prevDispatcher = dispatcher;
551
+ currentUrl = new URL(location, currentUrl).toString();
552
+ }
553
+ throw new PermanentJobError2(`Refusing to fetch ${url}: too many redirects.`);
554
+ };
555
+ registerSafeFetch(serverSafeFetch);
556
+
93
557
  // src/task/adaptive.ts
94
558
  import { CreateAdaptiveWorkflow, Workflow as Workflow10 } from "@workglow/task-graph";
95
559
 
@@ -625,7 +1089,6 @@ Workflow10.prototype.subtract = CreateAdaptiveWorkflow(ScalarSubtractTask, Vecto
625
1089
  Workflow10.prototype.multiply = CreateAdaptiveWorkflow(ScalarMultiplyTask, VectorMultiplyTask);
626
1090
  Workflow10.prototype.divide = CreateAdaptiveWorkflow(ScalarDivideTask, VectorDivideTask);
627
1091
  Workflow10.prototype.sum = CreateAdaptiveWorkflow(ScalarSumTask, VectorSumTask);
628
-
629
1092
  // src/mcp-server/getMcpServerConfig.ts
630
1093
  function getMcpServerConfig(configOrInput) {
631
1094
  const server = configOrInput.server;
@@ -1813,7 +2276,7 @@ Workflow13.prototype.delay = CreateWorkflow12(DelayTask);
1813
2276
  import {
1814
2277
  AbortSignalJobError,
1815
2278
  Job,
1816
- PermanentJobError,
2279
+ PermanentJobError as PermanentJobError3,
1817
2280
  RetryableJobError
1818
2281
  } from "@workglow/job-queue";
1819
2282
  import {
@@ -1822,52 +2285,13 @@ import {
1822
2285
  getJobQueueFactory,
1823
2286
  getTaskQueueRegistry,
1824
2287
  JobTaskFailedError,
2288
+ mergeEntitlements,
1825
2289
  Task as Task13,
1826
2290
  TaskConfigSchema as TaskConfigSchema3,
1827
2291
  TaskConfigurationError,
1828
2292
  TaskInvalidInputError as TaskInvalidInputError2,
1829
2293
  Workflow as Workflow14
1830
2294
  } from "@workglow/task-graph";
1831
- var PRIVATE_IP_RANGES = [
1832
- /^127\./,
1833
- /^10\./,
1834
- /^172\.(1[6-9]|2\d|3[01])\./,
1835
- /^192\.168\./,
1836
- /^169\.254\./,
1837
- /^0\./,
1838
- /^fc00:/i,
1839
- /^fe80:/i,
1840
- /^::1$/,
1841
- /^::$/
1842
- ];
1843
- var PRIVATE_HOSTNAMES = new Set(["localhost", "metadata.google.internal", "metadata.internal"]);
1844
- function isAllowPrivateUrlsEnvSet() {
1845
- if (globalThis?.process?.env?.WORKGLOW_ALLOW_PRIVATE_URLS === "true") {
1846
- return true;
1847
- }
1848
- const viteEnv = import.meta.env;
1849
- return viteEnv?.VITE_WORKGLOW_ALLOW_PRIVATE_URLS === "true";
1850
- }
1851
- function isPrivateUrl(urlStr) {
1852
- if (isAllowPrivateUrlsEnvSet()) {
1853
- return false;
1854
- }
1855
- try {
1856
- const parsed = new URL(urlStr);
1857
- const hostname = parsed.hostname.toLowerCase().replace(/^\[|\]$/g, "");
1858
- if (PRIVATE_HOSTNAMES.has(hostname) || hostname.endsWith(".local")) {
1859
- return true;
1860
- }
1861
- for (const range of PRIVATE_IP_RANGES) {
1862
- if (range.test(hostname)) {
1863
- return true;
1864
- }
1865
- }
1866
- return false;
1867
- } catch {
1868
- return false;
1869
- }
1870
- }
1871
2295
  var inputSchema13 = {
1872
2296
  type: "object",
1873
2297
  properties: {
@@ -1955,7 +2379,7 @@ async function fetchWithProgress(url, options = {}, onProgress) {
1955
2379
  if (!options.signal) {
1956
2380
  throw new TaskConfigurationError("An AbortSignal must be provided.");
1957
2381
  }
1958
- const response = await globalThis.fetch(url, options);
2382
+ const response = await safeFetch(url, options);
1959
2383
  if (!response.body) {
1960
2384
  throw new Error("ReadableStream not supported in this environment.");
1961
2385
  }
@@ -2004,14 +2428,16 @@ async function fetchWithProgress(url, options = {}, onProgress) {
2004
2428
  class FetchUrlJob extends Job {
2005
2429
  static type = "FetchUrlJob";
2006
2430
  async execute(input, context) {
2007
- if (isPrivateUrl(input.url)) {
2008
- throw new PermanentJobError(`Requests to private/internal networks are not allowed: ${input.url}. ` + `Set WORKGLOW_ALLOW_PRIVATE_URLS=true to override.`);
2431
+ const classification = classifyUrl(input.url);
2432
+ if (classification.kind === "invalid") {
2433
+ throw new PermanentJobError3(`Refusing to fetch invalid URL ${input.url}: ${classification.reason ?? "malformed"}`);
2009
2434
  }
2010
2435
  const response = await fetchWithProgress(input.url, {
2011
2436
  method: input.method,
2012
2437
  headers: input.headers,
2013
2438
  body: input.body,
2014
- signal: context.signal
2439
+ signal: context.signal,
2440
+ allowPrivate: classification.kind === "private"
2015
2441
  }, async (progress) => await context.updateProgress(progress));
2016
2442
  if (response.ok) {
2017
2443
  const contentType = response.headers.get("content-type") ?? "";
@@ -2064,7 +2490,7 @@ class FetchUrlJob extends Job {
2064
2490
  }
2065
2491
  throw new RetryableJobError(`Failed to fetch ${input.url}: ${response.status} ${response.statusText}`, retryDate);
2066
2492
  } else {
2067
- throw new PermanentJobError(`Failed to fetch ${input.url}: ${response.status} ${response.statusText}`);
2493
+ throw new PermanentJobError3(`Failed to fetch ${input.url}: ${response.status} ${response.statusText}`);
2068
2494
  }
2069
2495
  }
2070
2496
  }
@@ -2088,6 +2514,7 @@ class FetchUrlTask extends Task13 {
2088
2514
  static title = "Fetch";
2089
2515
  static description = "Fetches data from a URL with progress tracking and automatic retry handling";
2090
2516
  static hasDynamicSchemas = true;
2517
+ static hasDynamicEntitlements = true;
2091
2518
  static entitlements() {
2092
2519
  return {
2093
2520
  entitlements: [
@@ -2100,6 +2527,33 @@ class FetchUrlTask extends Task13 {
2100
2527
  ]
2101
2528
  };
2102
2529
  }
2530
+ entitlements() {
2531
+ const base = FetchUrlTask.entitlements();
2532
+ const url = this.runInputData?.url;
2533
+ if (typeof url !== "string" || url.length === 0) {
2534
+ return mergeEntitlements(base, {
2535
+ entitlements: [
2536
+ {
2537
+ id: Entitlements.NETWORK_PRIVATE,
2538
+ reason: "Runtime URL is not yet available during entitlement evaluation; private/internal destinations must be explicitly allowed"
2539
+ }
2540
+ ]
2541
+ });
2542
+ }
2543
+ const classification = classifyUrl(url);
2544
+ if (classification.kind !== "private") {
2545
+ return base;
2546
+ }
2547
+ return mergeEntitlements(base, {
2548
+ entitlements: [
2549
+ {
2550
+ id: Entitlements.NETWORK_PRIVATE,
2551
+ reason: `URL targets private/internal host: ${classification.reason ?? classification.host ?? "unknown"}`,
2552
+ resources: [urlResourcePattern(url)]
2553
+ }
2554
+ ]
2555
+ });
2556
+ }
2103
2557
  static configSchema() {
2104
2558
  return fetchUrlTaskConfigSchema;
2105
2559
  }
@@ -8799,7 +9253,7 @@ Workflow38.prototype.lambda = CreateWorkflow37(LambdaTask);
8799
9253
  import {
8800
9254
  CreateWorkflow as CreateWorkflow38,
8801
9255
  Entitlements as Entitlements3,
8802
- mergeEntitlements,
9256
+ mergeEntitlements as mergeEntitlements2,
8803
9257
  Task as Task37,
8804
9258
  Workflow as Workflow39
8805
9259
  } from "@workglow/task-graph";
@@ -8994,13 +9448,13 @@ class McpListTask extends Task37 {
8994
9448
  const base = McpListTask.entitlements();
8995
9449
  const transport = getMcpServerTransport(this);
8996
9450
  if (transport === "stdio") {
8997
- return mergeEntitlements(base, {
9451
+ return mergeEntitlements2(base, {
8998
9452
  entitlements: [
8999
9453
  { id: Entitlements3.MCP_STDIO, reason: "Uses stdio transport to spawn local process" }
9000
9454
  ]
9001
9455
  });
9002
9456
  }
9003
- return mergeEntitlements(base, {
9457
+ return mergeEntitlements2(base, {
9004
9458
  entitlements: [{ id: Entitlements3.NETWORK_HTTP, reason: "Connects to MCP server over HTTP" }]
9005
9459
  });
9006
9460
  }
@@ -9087,7 +9541,7 @@ Workflow39.prototype.mcpList = CreateWorkflow38(McpListTask);
9087
9541
  import {
9088
9542
  CreateWorkflow as CreateWorkflow39,
9089
9543
  Entitlements as Entitlements4,
9090
- mergeEntitlements as mergeEntitlements2,
9544
+ mergeEntitlements as mergeEntitlements3,
9091
9545
  Task as Task38,
9092
9546
  TaskConfigSchema as TaskConfigSchema8,
9093
9547
  Workflow as Workflow40
@@ -9243,13 +9697,13 @@ class McpPromptGetTask extends Task38 {
9243
9697
  const base = McpPromptGetTask.entitlements();
9244
9698
  const transport = getMcpServerTransport(this);
9245
9699
  if (transport === "stdio") {
9246
- return mergeEntitlements2(base, {
9700
+ return mergeEntitlements3(base, {
9247
9701
  entitlements: [
9248
9702
  { id: Entitlements4.MCP_STDIO, reason: "Uses stdio transport to spawn local process" }
9249
9703
  ]
9250
9704
  });
9251
9705
  }
9252
- return mergeEntitlements2(base, {
9706
+ return mergeEntitlements3(base, {
9253
9707
  entitlements: [
9254
9708
  { id: Entitlements4.NETWORK_HTTP, reason: "Connects to MCP server over HTTP" },
9255
9709
  { id: Entitlements4.CREDENTIAL, reason: "May require authentication", optional: true }
@@ -9353,7 +9807,7 @@ Workflow40.prototype.mcpPromptGet = CreateWorkflow39(McpPromptGetTask);
9353
9807
  import {
9354
9808
  CreateWorkflow as CreateWorkflow40,
9355
9809
  Entitlements as Entitlements5,
9356
- mergeEntitlements as mergeEntitlements3,
9810
+ mergeEntitlements as mergeEntitlements4,
9357
9811
  Task as Task39,
9358
9812
  TaskConfigSchema as TaskConfigSchema9,
9359
9813
  Workflow as Workflow41
@@ -9422,13 +9876,13 @@ class McpResourceReadTask extends Task39 {
9422
9876
  const base = McpResourceReadTask.entitlements();
9423
9877
  const transport = getMcpServerTransport(this);
9424
9878
  if (transport === "stdio") {
9425
- return mergeEntitlements3(base, {
9879
+ return mergeEntitlements4(base, {
9426
9880
  entitlements: [
9427
9881
  { id: Entitlements5.MCP_STDIO, reason: "Uses stdio transport to spawn local process" }
9428
9882
  ]
9429
9883
  });
9430
9884
  }
9431
- return mergeEntitlements3(base, {
9885
+ return mergeEntitlements4(base, {
9432
9886
  entitlements: [
9433
9887
  { id: Entitlements5.NETWORK_HTTP, reason: "Connects to MCP server over HTTP" },
9434
9888
  { id: Entitlements5.CREDENTIAL, reason: "May require authentication", optional: true }
@@ -9654,7 +10108,7 @@ Workflow42.prototype.mcpSearch = CreateWorkflow41(McpSearchTask);
9654
10108
  import {
9655
10109
  CreateWorkflow as CreateWorkflow42,
9656
10110
  Entitlements as Entitlements7,
9657
- mergeEntitlements as mergeEntitlements4,
10111
+ mergeEntitlements as mergeEntitlements5,
9658
10112
  Task as Task41,
9659
10113
  TaskConfigSchema as TaskConfigSchema10,
9660
10114
  Workflow as Workflow43
@@ -9802,13 +10256,13 @@ class McpToolCallTask extends Task41 {
9802
10256
  const base = McpToolCallTask.entitlements();
9803
10257
  const transport = getMcpServerTransport(this);
9804
10258
  if (transport === "stdio") {
9805
- return mergeEntitlements4(base, {
10259
+ return mergeEntitlements5(base, {
9806
10260
  entitlements: [
9807
10261
  { id: Entitlements7.MCP_STDIO, reason: "Uses stdio transport to spawn local process" }
9808
10262
  ]
9809
10263
  });
9810
10264
  }
9811
- return mergeEntitlements4(base, {
10265
+ return mergeEntitlements5(base, {
9812
10266
  entitlements: [
9813
10267
  { id: Entitlements7.NETWORK_HTTP, reason: "Connects to MCP server over HTTP" },
9814
10268
  { id: Entitlements7.CREDENTIAL, reason: "May require authentication", optional: true }
@@ -12099,18 +12553,24 @@ var registerCommonTasks2 = () => {
12099
12553
  return [...tasks, FileLoaderTask2];
12100
12554
  };
12101
12555
  export {
12556
+ urlResourcePattern,
12557
+ tryNormalizeIPv4,
12102
12558
  split,
12103
12559
  setGlobalMcpServerRepository,
12104
12560
  searchMcpRegistryPage,
12105
12561
  searchMcpRegistry,
12562
+ safeFetch,
12106
12563
  resolveImageInput,
12107
12564
  resolveHumanConnector,
12108
12565
  resolveAuthSecrets,
12566
+ resetSafeFetch,
12567
+ registerSafeFetch,
12109
12568
  registerMcpTaskDeps,
12110
12569
  registerMcpServer,
12111
12570
  registerImageRasterCodec,
12112
12571
  registerCommonTasks2 as registerCommonTasks,
12113
12572
  produceImageOutput,
12573
+ normalizeOutputMimeType,
12114
12574
  merge,
12115
12575
  mcpTransportTypes,
12116
12576
  mcpToolCall,
@@ -12128,6 +12588,7 @@ export {
12128
12588
  json,
12129
12589
  javaScript,
12130
12590
  isDataUriImage,
12591
+ getSafeFetchImpl,
12131
12592
  getMcpTaskDeps,
12132
12593
  getMcpServerConfig,
12133
12594
  getMcpServer,
@@ -12137,11 +12598,17 @@ export {
12137
12598
  formatImageOutput,
12138
12599
  fileLoader,
12139
12600
  fetchUrl,
12601
+ extractDataUriMimeType,
12140
12602
  delay,
12141
12603
  debugLog,
12142
12604
  createMcpClient,
12143
12605
  createAuthProvider,
12606
+ classifyUrl,
12607
+ classifyIpLiteral,
12144
12608
  buildAuthConfig,
12609
+ assertWithinPixelBudget,
12610
+ assertWithinByteBudget,
12611
+ assertIsDataUri,
12145
12612
  VectorSumTask,
12146
12613
  VectorSubtractTask,
12147
12614
  VectorScaleTask,
@@ -12176,7 +12643,9 @@ export {
12176
12643
  ScalarCeilTask,
12177
12644
  ScalarAddTask,
12178
12645
  ScalarAbsTask,
12646
+ SUPPORTED_OUTPUT_MIME_TYPES,
12179
12647
  RegexTask,
12648
+ REJECTED_DECODE_MIME_TYPES,
12180
12649
  OutputTask,
12181
12650
  MergeTask,
12182
12651
  McpToolCallTask,
@@ -12191,6 +12660,9 @@ export {
12191
12660
  MCP_TASK_DEPS,
12192
12661
  MCP_SERVER_REPOSITORY,
12193
12662
  MCP_SERVERS,
12663
+ MAX_INPUT_BYTES_NODE,
12664
+ MAX_INPUT_BYTES_BROWSER,
12665
+ MAX_DECODED_PIXELS,
12194
12666
  LambdaTask,
12195
12667
  JsonTask,
12196
12668
  JsonPathTask,
@@ -12231,4 +12703,4 @@ export {
12231
12703
  ArrayTask
12232
12704
  };
12233
12705
 
12234
- //# debugId=D5802074607EFED764756E2164756E21
12706
+ //# debugId=141DEE75E36B39CB64756E2164756E21