@fedify/vocab-runtime 2.0.7 → 2.0.9

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.
Files changed (49) hide show
  1. package/deno.json +1 -1
  2. package/dist/{chunk-CUT6urMc.cjs → chunk-CKQMccvm.cjs} +7 -9
  3. package/dist/jsonld.cjs +4 -5
  4. package/dist/jsonld.d.cts +3 -4
  5. package/dist/jsonld.d.ts +2 -2
  6. package/dist/jsonld.js +1 -3
  7. package/dist/mod.cjs +83 -131
  8. package/dist/mod.d.cts +1 -5
  9. package/dist/mod.d.ts +1 -5
  10. package/dist/mod.js +59 -110
  11. package/dist/tests/{chunk-DWy1uDak.cjs → chunk-Do9eywBl.cjs} +13 -17
  12. package/dist/tests/docloader.test.cjs +137 -114
  13. package/dist/tests/{docloader.test.js → docloader.test.mjs} +132 -115
  14. package/dist/tests/internal/multicodec.test.cjs +5 -7
  15. package/dist/tests/internal/{multicodec.test.js → multicodec.test.mjs} +3 -4
  16. package/dist/tests/key.test.cjs +39 -70
  17. package/dist/tests/{key.test.js → key.test.mjs} +32 -62
  18. package/dist/tests/langstr.test.cjs +6 -8
  19. package/dist/tests/{langstr.test.js → langstr.test.mjs} +2 -4
  20. package/dist/tests/{link-CdFPEo9O.cjs → link-B6ZWBZhf.cjs} +6 -8
  21. package/dist/tests/{link-Ck2yj4dH.js → link-B8JGXSS2.mjs} +1 -2
  22. package/dist/tests/link.test.cjs +5 -7
  23. package/dist/tests/{link.test.js → link.test.mjs} +3 -4
  24. package/dist/tests/multibase/multibase.test.cjs +10 -17
  25. package/dist/tests/multibase/{multibase.test.js → multibase.test.mjs} +10 -18
  26. package/dist/tests/{multibase-B2D6B0V4.cjs → multibase-CgYqpk4Z.cjs} +43 -49
  27. package/dist/tests/{multibase-BdHCGO4H.js → multibase-jcKrOpuU.mjs} +5 -12
  28. package/dist/tests/{multicodec-mHcRzSGY.cjs → multicodec-DeYop8xg.cjs} +16 -18
  29. package/dist/tests/{multicodec-DvC5xnX2.js → multicodec-aqbZnrNi.mjs} +1 -2
  30. package/dist/tests/{request-BZixuWv5.js → request-AitXfW_2.mjs} +4 -45
  31. package/dist/tests/{request-78UEYyIx.cjs → request-C6iSYeYi.cjs} +38 -72
  32. package/dist/tests/request.test.cjs +23 -24
  33. package/dist/tests/request.test.mjs +42 -0
  34. package/dist/tests/{url-C5Vs9nYh.cjs → url-Cr2K-wzd.cjs} +31 -35
  35. package/dist/tests/{url-fW_DHbih.js → url-Djghaq0m.mjs} +3 -7
  36. package/dist/tests/url.test.cjs +5 -7
  37. package/dist/tests/{url.test.js → url.test.mjs} +3 -4
  38. package/package.json +3 -3
  39. package/src/docloader.test.ts +67 -0
  40. package/src/docloader.ts +43 -11
  41. package/dist/tests/request.test.js +0 -43
  42. /package/dist/tests/{docloader.test.d.ts → docloader.test.d.mts} +0 -0
  43. /package/dist/tests/internal/{multicodec.test.d.ts → multicodec.test.d.mts} +0 -0
  44. /package/dist/tests/{key.test.d.ts → key.test.d.mts} +0 -0
  45. /package/dist/tests/{langstr.test.d.ts → langstr.test.d.mts} +0 -0
  46. /package/dist/tests/{link.test.d.ts → link.test.d.mts} +0 -0
  47. /package/dist/tests/multibase/{multibase.test.d.ts → multibase.test.d.mts} +0 -0
  48. /package/dist/tests/{request.test.d.ts → request.test.d.mts} +0 -0
  49. /package/dist/tests/{url.test.d.ts → url.test.d.mts} +0 -0
@@ -1,7 +1,6 @@
1
- const require_chunk = require('./chunk-DWy1uDak.cjs');
2
- const node_dns_promises = require_chunk.__toESM(require("node:dns/promises"));
3
- const node_net = require_chunk.__toESM(require("node:net"));
4
-
1
+ require("./chunk-Do9eywBl.cjs");
2
+ let node_dns_promises = require("node:dns/promises");
3
+ let node_net = require("node:net");
5
4
  //#region src/url.ts
6
5
  var UrlError = class extends Error {
7
6
  constructor(message) {
@@ -19,8 +18,7 @@ async function validatePublicUrl(url) {
19
18
  if (hostname.startsWith("[") && hostname.endsWith("]")) hostname = hostname.substring(1, hostname.length - 2);
20
19
  if (hostname === "localhost") throw new UrlError("Localhost is not allowed");
21
20
  if ("Deno" in globalThis && !(0, node_net.isIP)(hostname)) {
22
- const netPermission = await Deno.permissions.query({ name: "net" });
23
- if (netPermission.state !== "granted") return;
21
+ if ((await Deno.permissions.query({ name: "net" })).state !== "granted") return;
24
22
  }
25
23
  if ("Bun" in globalThis) {
26
24
  if (hostname === "example.com" || hostname.endsWith(".example.com")) return;
@@ -56,38 +54,36 @@ function expandIPv6Address(address) {
56
54
  if (address.startsWith("::")) address = "0000" + address;
57
55
  if (address.endsWith("::")) address = address + "0000";
58
56
  address = address.replace("::", ":0000".repeat(8 - (address.match(/:/g) || []).length) + ":");
59
- const parts = address.split(":");
60
- return parts.map((part) => part.padStart(4, "0")).join(":");
57
+ return address.split(":").map((part) => part.padStart(4, "0")).join(":");
61
58
  }
62
-
63
59
  //#endregion
64
- Object.defineProperty(exports, 'UrlError', {
65
- enumerable: true,
66
- get: function () {
67
- return UrlError;
68
- }
60
+ Object.defineProperty(exports, "UrlError", {
61
+ enumerable: true,
62
+ get: function() {
63
+ return UrlError;
64
+ }
65
+ });
66
+ Object.defineProperty(exports, "expandIPv6Address", {
67
+ enumerable: true,
68
+ get: function() {
69
+ return expandIPv6Address;
70
+ }
69
71
  });
70
- Object.defineProperty(exports, 'expandIPv6Address', {
71
- enumerable: true,
72
- get: function () {
73
- return expandIPv6Address;
74
- }
72
+ Object.defineProperty(exports, "isValidPublicIPv4Address", {
73
+ enumerable: true,
74
+ get: function() {
75
+ return isValidPublicIPv4Address;
76
+ }
75
77
  });
76
- Object.defineProperty(exports, 'isValidPublicIPv4Address', {
77
- enumerable: true,
78
- get: function () {
79
- return isValidPublicIPv4Address;
80
- }
78
+ Object.defineProperty(exports, "isValidPublicIPv6Address", {
79
+ enumerable: true,
80
+ get: function() {
81
+ return isValidPublicIPv6Address;
82
+ }
81
83
  });
82
- Object.defineProperty(exports, 'isValidPublicIPv6Address', {
83
- enumerable: true,
84
- get: function () {
85
- return isValidPublicIPv6Address;
86
- }
84
+ Object.defineProperty(exports, "validatePublicUrl", {
85
+ enumerable: true,
86
+ get: function() {
87
+ return validatePublicUrl;
88
+ }
87
89
  });
88
- Object.defineProperty(exports, 'validatePublicUrl', {
89
- enumerable: true,
90
- get: function () {
91
- return validatePublicUrl;
92
- }
93
- });
@@ -1,6 +1,5 @@
1
1
  import { lookup } from "node:dns/promises";
2
2
  import { isIP } from "node:net";
3
-
4
3
  //#region src/url.ts
5
4
  var UrlError = class extends Error {
6
5
  constructor(message) {
@@ -18,8 +17,7 @@ async function validatePublicUrl(url) {
18
17
  if (hostname.startsWith("[") && hostname.endsWith("]")) hostname = hostname.substring(1, hostname.length - 2);
19
18
  if (hostname === "localhost") throw new UrlError("Localhost is not allowed");
20
19
  if ("Deno" in globalThis && !isIP(hostname)) {
21
- const netPermission = await Deno.permissions.query({ name: "net" });
22
- if (netPermission.state !== "granted") return;
20
+ if ((await Deno.permissions.query({ name: "net" })).state !== "granted") return;
23
21
  }
24
22
  if ("Bun" in globalThis) {
25
23
  if (hostname === "example.com" || hostname.endsWith(".example.com")) return;
@@ -55,9 +53,7 @@ function expandIPv6Address(address) {
55
53
  if (address.startsWith("::")) address = "0000" + address;
56
54
  if (address.endsWith("::")) address = address + "0000";
57
55
  address = address.replace("::", ":0000".repeat(8 - (address.match(/:/g) || []).length) + ":");
58
- const parts = address.split(":");
59
- return parts.map((part) => part.padStart(4, "0")).join(":");
56
+ return address.split(":").map((part) => part.padStart(4, "0")).join(":");
60
57
  }
61
-
62
58
  //#endregion
63
- export { UrlError, expandIPv6Address, isValidPublicIPv4Address, isValidPublicIPv6Address, validatePublicUrl };
59
+ export { validatePublicUrl as a, isValidPublicIPv6Address as i, expandIPv6Address as n, isValidPublicIPv4Address as r, UrlError as t };
@@ -1,8 +1,7 @@
1
- const require_chunk = require('./chunk-DWy1uDak.cjs');
2
- const require_url = require('./url-C5Vs9nYh.cjs');
3
- const node_assert = require_chunk.__toESM(require("node:assert"));
4
- const node_test = require_chunk.__toESM(require("node:test"));
5
-
1
+ require("./chunk-Do9eywBl.cjs");
2
+ const require_url = require("./url-Cr2K-wzd.cjs");
3
+ let node_assert = require("node:assert");
4
+ let node_test = require("node:test");
6
5
  //#region src/url.test.ts
7
6
  (0, node_test.test)("validatePublicUrl()", async () => {
8
7
  await (0, node_assert.rejects)(() => require_url.validatePublicUrl("ftp://localhost"), require_url.UrlError);
@@ -33,5 +32,4 @@ const node_test = require_chunk.__toESM(require("node:test"));
33
32
  (0, node_assert.deepStrictEqual)(require_url.expandIPv6Address("2001:db8::"), "2001:0db8:0000:0000:0000:0000:0000:0000");
34
33
  (0, node_assert.deepStrictEqual)(require_url.expandIPv6Address("2001:db8::1"), "2001:0db8:0000:0000:0000:0000:0000:0001");
35
34
  });
36
-
37
- //#endregion
35
+ //#endregion
@@ -1,7 +1,6 @@
1
- import { UrlError, expandIPv6Address, isValidPublicIPv4Address, isValidPublicIPv6Address, validatePublicUrl } from "./url-fW_DHbih.js";
1
+ import { a as validatePublicUrl, i as isValidPublicIPv6Address, n as expandIPv6Address, r as isValidPublicIPv4Address, t as UrlError } from "./url-Djghaq0m.mjs";
2
2
  import { deepStrictEqual, ok, rejects } from "node:assert";
3
3
  import { test } from "node:test";
4
-
5
4
  //#region src/url.test.ts
6
5
  test("validatePublicUrl()", async () => {
7
6
  await rejects(() => validatePublicUrl("ftp://localhost"), UrlError);
@@ -32,5 +31,5 @@ test("expandIPv6Address()", () => {
32
31
  deepStrictEqual(expandIPv6Address("2001:db8::"), "2001:0db8:0000:0000:0000:0000:0000:0000");
33
32
  deepStrictEqual(expandIPv6Address("2001:db8::1"), "2001:0db8:0000:0000:0000:0000:0000:0001");
34
33
  });
35
-
36
- //#endregion
34
+ //#endregion
35
+ export {};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fedify/vocab-runtime",
3
- "version": "2.0.7",
3
+ "version": "2.0.9",
4
4
  "homepage": "https://fedify.dev/",
5
5
  "repository": {
6
6
  "type": "git",
@@ -61,8 +61,8 @@
61
61
  "devDependencies": {
62
62
  "@types/node": "^24.2.1",
63
63
  "fetch-mock": "^12.5.4",
64
- "tsdown": "^0.12.9",
65
- "typescript": "^5.9.3"
64
+ "tsdown": "^0.21.6",
65
+ "typescript": "^5.9.2"
66
66
  },
67
67
  "dependencies": {
68
68
  "@logtape/logtape": "^2.0.0",
@@ -361,6 +361,73 @@ test("getDocumentLoader()", async (t) => {
361
361
  );
362
362
  });
363
363
 
364
+ let redirectAttempts = 0;
365
+ fetchMock.get("begin:https://example.com/too-many-redirects/", (cl) => {
366
+ redirectAttempts++;
367
+ const index = Number(cl.url.split("/").at(-1));
368
+ return {
369
+ status: 302,
370
+ headers: {
371
+ Location: `https://example.com/too-many-redirects/${index + 1}`,
372
+ },
373
+ };
374
+ });
375
+
376
+ await t.test("too many redirects", async () => {
377
+ redirectAttempts = 0;
378
+ await rejects(
379
+ () => fetchDocumentLoader("https://example.com/too-many-redirects/0"),
380
+ FetchError,
381
+ "Too many redirections",
382
+ );
383
+ deepStrictEqual(redirectAttempts, 21);
384
+ });
385
+
386
+ let loopAttempts = 0;
387
+ fetchMock.get("https://example.com/redirect-loop-a", () => {
388
+ loopAttempts++;
389
+ return {
390
+ status: 302,
391
+ headers: { Location: "https://example.com/redirect-loop-b" },
392
+ };
393
+ });
394
+ fetchMock.get("https://example.com/redirect-loop-b", () => {
395
+ loopAttempts++;
396
+ return {
397
+ status: 302,
398
+ headers: { Location: "https://example.com/redirect-loop-a" },
399
+ };
400
+ });
401
+
402
+ await t.test("redirect loop", async () => {
403
+ loopAttempts = 0;
404
+ await rejects(
405
+ () => fetchDocumentLoader("https://example.com/redirect-loop-a"),
406
+ FetchError,
407
+ "Redirect loop detected",
408
+ );
409
+ deepStrictEqual(loopAttempts, 2);
410
+ });
411
+
412
+ let relativeLoopAttempts = 0;
413
+ fetchMock.get("https://example.com/redirect-loop-relative", () => {
414
+ relativeLoopAttempts++;
415
+ return {
416
+ status: 302,
417
+ headers: { Location: "/redirect-loop-relative" },
418
+ };
419
+ });
420
+
421
+ await t.test("redirect loop with relative location", async () => {
422
+ relativeLoopAttempts = 0;
423
+ await rejects(
424
+ () => fetchDocumentLoader("https://example.com/redirect-loop-relative"),
425
+ FetchError,
426
+ "Redirect loop detected",
427
+ );
428
+ deepStrictEqual(relativeLoopAttempts, 1);
429
+ });
430
+
364
431
  // Regression test for ReDoS vulnerability (CVE-2025-68475)
365
432
  // Malicious HTML payload: <a a="b" a="b" ... (unclosed tag)
366
433
  // With the vulnerable regex, this causes catastrophic backtracking
package/src/docloader.ts CHANGED
@@ -12,6 +12,7 @@ import {
12
12
  import { UrlError, validatePublicUrl } from "./url.ts";
13
13
 
14
14
  const logger = getLogger(["fedify", "runtime", "docloader"]);
15
+ const DEFAULT_MAX_REDIRECTION = 20;
15
16
 
16
17
  /**
17
18
  * A remote JSON-LD document and its context fetched by
@@ -292,38 +293,45 @@ export function getDocumentLoader(
292
293
  async function load(
293
294
  url: string,
294
295
  options?: DocumentLoaderOptions,
296
+ redirected = 0,
297
+ visited = new Set<string>(),
295
298
  ): Promise<RemoteDocument> {
296
299
  options?.signal?.throwIfAborted();
297
- if (!skipPreloadedContexts && url in preloadedContexts) {
298
- logger.debug("Using preloaded context: {url}.", { url });
300
+ const currentUrl = new URL(url).href;
301
+ if (!skipPreloadedContexts && currentUrl in preloadedContexts) {
302
+ logger.debug("Using preloaded context: {url}.", { url: currentUrl });
299
303
  return {
300
304
  contextUrl: null,
301
- document: preloadedContexts[url],
302
- documentUrl: url,
305
+ document: preloadedContexts[currentUrl],
306
+ documentUrl: currentUrl,
303
307
  };
304
308
  }
305
309
  if (!allowPrivateAddress) {
306
310
  try {
307
- await validatePublicUrl(url);
311
+ await validatePublicUrl(currentUrl);
308
312
  } catch (error) {
309
313
  if (error instanceof UrlError) {
310
- logger.error("Disallowed private URL: {url}", { url, error });
314
+ logger.error("Disallowed private URL: {url}", {
315
+ url: currentUrl,
316
+ error,
317
+ });
311
318
  }
312
319
  throw error;
313
320
  }
314
321
  }
322
+ visited.add(currentUrl);
315
323
 
316
324
  return await tracer.startActiveSpan(
317
325
  "activitypub.fetch_document",
318
326
  {
319
327
  kind: SpanKind.CLIENT,
320
328
  attributes: {
321
- "url.full": url,
329
+ "url.full": currentUrl,
322
330
  },
323
331
  },
324
332
  async (span) => {
325
333
  try {
326
- const request = createActivityPubRequest(url, { userAgent });
334
+ const request = createActivityPubRequest(currentUrl, { userAgent });
327
335
  logRequest(logger, request);
328
336
  const response = await fetch(request, {
329
337
  // Since Bun has a bug that ignores the `Request.redirect` option,
@@ -339,12 +347,36 @@ export function getDocumentLoader(
339
347
  response.status >= 300 && response.status < 400 &&
340
348
  response.headers.has("Location")
341
349
  ) {
342
- const redirectUrl = response.headers.get("Location")!;
350
+ if (redirected >= DEFAULT_MAX_REDIRECTION) {
351
+ logger.error(
352
+ "Too many redirections ({redirections}) while fetching document.",
353
+ { redirections: redirected + 1, url: currentUrl },
354
+ );
355
+ throw new FetchError(
356
+ currentUrl,
357
+ `Too many redirections (${redirected + 1})`,
358
+ );
359
+ }
360
+ const redirectUrl = new URL(
361
+ response.headers.get("Location")!,
362
+ response.url === "" ? currentUrl : response.url,
363
+ ).href;
343
364
  span.setAttribute("http.redirect.url", redirectUrl);
344
- return await load(redirectUrl, options);
365
+ if (visited.has(redirectUrl)) {
366
+ logger.error(
367
+ "Detected a redirect loop while fetching document: {url} -> " +
368
+ "{redirectUrl}",
369
+ { url: currentUrl, redirectUrl },
370
+ );
371
+ throw new FetchError(
372
+ currentUrl,
373
+ `Redirect loop detected: ${redirectUrl}`,
374
+ );
375
+ }
376
+ return await load(redirectUrl, options, redirected + 1, visited);
345
377
  }
346
378
 
347
- const result = await getRemoteDocument(url, response, load);
379
+ const result = await getRemoteDocument(currentUrl, response, load);
348
380
  span.setAttribute("docloader.document_url", result.documentUrl);
349
381
  if (result.contextUrl != null) {
350
382
  span.setAttribute("docloader.context_url", result.contextUrl);
@@ -1,43 +0,0 @@
1
- import { deno_default, getUserAgent } from "./request-BZixuWv5.js";
2
- import { deepStrictEqual } from "node:assert";
3
- import { test } from "node:test";
4
- import process from "node:process";
5
-
6
- //#region src/request.test.ts
7
- test("getUserAgent()", () => {
8
- if ("Deno" in globalThis) {
9
- deepStrictEqual(getUserAgent(), `Fedify/${deno_default.version} (Deno/${Deno.version.deno})`);
10
- deepStrictEqual(getUserAgent({ software: "MyApp/1.0.0" }), `MyApp/1.0.0 (Fedify/${deno_default.version}; Deno/${Deno.version.deno})`);
11
- deepStrictEqual(getUserAgent({ url: "https://example.com/" }), `Fedify/${deno_default.version} (Deno/${Deno.version.deno}; +https://example.com/)`);
12
- deepStrictEqual(getUserAgent({
13
- software: "MyApp/1.0.0",
14
- url: new URL("https://example.com/")
15
- }), `MyApp/1.0.0 (Fedify/${deno_default.version}; Deno/${Deno.version.deno}; +https://example.com/)`);
16
- } else if ("Bun" in globalThis) {
17
- deepStrictEqual(getUserAgent(), `Fedify/${deno_default.version} (Bun/${Bun.version})`);
18
- deepStrictEqual(getUserAgent({ software: "MyApp/1.0.0" }), `MyApp/1.0.0 (Fedify/${deno_default.version}; Bun/${Bun.version})`);
19
- deepStrictEqual(getUserAgent({ url: "https://example.com/" }), `Fedify/${deno_default.version} (Bun/${Bun.version}; +https://example.com/)`);
20
- deepStrictEqual(getUserAgent({
21
- software: "MyApp/1.0.0",
22
- url: new URL("https://example.com/")
23
- }), `MyApp/1.0.0 (Fedify/${deno_default.version}; Bun/${Bun.version}; +https://example.com/)`);
24
- } else if (navigator.userAgent === "Cloudflare-Workers") {
25
- deepStrictEqual(getUserAgent(), `Fedify/${deno_default.version} (Cloudflare-Workers)`);
26
- deepStrictEqual(getUserAgent({ software: "MyApp/1.0.0" }), `MyApp/1.0.0 (Fedify/${deno_default.version}; Cloudflare-Workers)`);
27
- deepStrictEqual(getUserAgent({ url: "https://example.com/" }), `Fedify/${deno_default.version} (Cloudflare-Workers; +https://example.com/)`);
28
- deepStrictEqual(getUserAgent({
29
- software: "MyApp/1.0.0",
30
- url: new URL("https://example.com/")
31
- }), `MyApp/1.0.0 (Fedify/${deno_default.version}; Cloudflare-Workers; +https://example.com/)`);
32
- } else {
33
- deepStrictEqual(getUserAgent(), `Fedify/${deno_default.version} (Node.js/${process.versions.node})`);
34
- deepStrictEqual(getUserAgent({ software: "MyApp/1.0.0" }), `MyApp/1.0.0 (Fedify/${deno_default.version}; Node.js/${process.versions.node})`);
35
- deepStrictEqual(getUserAgent({ url: "https://example.com/" }), `Fedify/${deno_default.version} (Node.js/${process.versions.node}; +https://example.com/)`);
36
- deepStrictEqual(getUserAgent({
37
- software: "MyApp/1.0.0",
38
- url: new URL("https://example.com/")
39
- }), `MyApp/1.0.0 (Fedify/${deno_default.version}; Node.js/${process.versions.node}; +https://example.com/)`);
40
- }
41
- });
42
-
43
- //#endregion
File without changes
File without changes
File without changes