@fedify/vocab-runtime 2.3.15-dev.2482 → 2.3.15

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 (114) hide show
  1. package/deno.json +5 -2
  2. package/dist/internal.cjs +6 -0
  3. package/dist/internal.d.cts +3 -0
  4. package/dist/internal.d.ts +3 -0
  5. package/dist/internal.js +3 -0
  6. package/dist/mod.cjs +105 -253
  7. package/dist/mod.d.cts +5 -28
  8. package/dist/mod.d.ts +6 -28
  9. package/dist/mod.js +98 -245
  10. package/dist/public-fetch-BC-nWJNM.d.ts +143 -0
  11. package/dist/public-fetch-Bk32IaYB.cjs +821 -0
  12. package/dist/public-fetch-DBpt3xxf.d.cts +143 -0
  13. package/dist/public-fetch-Dfmc_Kfy.js +772 -0
  14. package/dist/tests/{body-BmS3KJE1.cjs → body-BtW9TZGZ.cjs} +1 -1
  15. package/dist/tests/{body-xrlsEUPK.mjs → body-CmdrGt1w.mjs} +1 -1
  16. package/dist/tests/body.test.cjs +1 -1
  17. package/dist/tests/body.test.mjs +1 -1
  18. package/dist/tests/decimal.test.cjs +6 -6
  19. package/dist/tests/decimal.test.mjs +6 -6
  20. package/dist/tests/{docloader-CETMi5su.cjs → docloader-D34pSxKd.cjs} +106 -35
  21. package/dist/tests/{docloader-BJXlwNXs.mjs → docloader-zoX3z1bj.mjs} +102 -37
  22. package/dist/tests/docloader.test.cjs +523 -1273
  23. package/dist/tests/docloader.test.mjs +469 -1238
  24. package/dist/tests/esm-C_ftyt-G.cjs +1208 -0
  25. package/dist/tests/esm-LaZ1p80h.mjs +1222 -0
  26. package/dist/tests/internal/multicodec.test.cjs +1 -1
  27. package/dist/tests/internal/multicodec.test.mjs +1 -1
  28. package/dist/tests/{key-Qj7u1qmH.mjs → key-2lZR0Cht.mjs} +2 -2
  29. package/dist/tests/{key-CWDxavIR.cjs → key-CnsHrQSU.cjs} +2 -2
  30. package/dist/tests/key.test.cjs +2 -2
  31. package/dist/tests/key.test.mjs +2 -2
  32. package/dist/tests/{langstr-BsqFJupv.cjs → langstr-CZpWmpo5.cjs} +1 -1
  33. package/dist/tests/{langstr-ABdAOKlF.mjs → langstr-DTB4sl0a.mjs} +1 -1
  34. package/dist/tests/langstr.test.cjs +1 -1
  35. package/dist/tests/langstr.test.mjs +1 -1
  36. package/dist/tests/langtag.test.cjs +1 -1
  37. package/dist/tests/langtag.test.mjs +1 -1
  38. package/dist/tests/link.test.mjs +1 -1
  39. package/dist/tests/multibase/multibase.test.cjs +1 -1
  40. package/dist/tests/multibase/multibase.test.mjs +1 -1
  41. package/dist/tests/node-C0iftNaR.cjs +90 -0
  42. package/dist/tests/node-C3bjFjL1.mjs +84 -0
  43. package/dist/tests/pin-grant-CU-o367k.cjs +70 -0
  44. package/dist/tests/pin-grant-HW3B6SKv.mjs +53 -0
  45. package/dist/tests/pin-grant.test.cjs +88 -0
  46. package/dist/tests/pin-grant.test.d.cts +1 -0
  47. package/dist/tests/pin-grant.test.d.mts +1 -0
  48. package/dist/tests/pin-grant.test.mjs +88 -0
  49. package/dist/tests/pinners/bun.test.cjs +167 -0
  50. package/dist/tests/pinners/bun.test.d.cts +1 -0
  51. package/dist/tests/pinners/bun.test.d.mts +1 -0
  52. package/dist/tests/pinners/bun.test.mjs +167 -0
  53. package/dist/tests/pinners/node.test.cjs +269 -0
  54. package/dist/tests/pinners/node.test.d.cts +1 -0
  55. package/dist/tests/pinners/node.test.d.mts +1 -0
  56. package/dist/tests/pinners/node.test.mjs +268 -0
  57. package/dist/tests/pinners/pinners.test.cjs +197 -0
  58. package/dist/tests/pinners/pinners.test.d.cts +1 -0
  59. package/dist/tests/pinners/pinners.test.d.mts +1 -0
  60. package/dist/tests/pinners/pinners.test.mjs +197 -0
  61. package/dist/tests/pinners/testdata/node-child.cjs +135 -0
  62. package/dist/tests/pinners/testdata/node-child.d.cts +1 -0
  63. package/dist/tests/pinners/testdata/node-child.d.mts +1 -0
  64. package/dist/tests/pinners/testdata/node-child.mjs +134 -0
  65. package/dist/tests/pinners-D4UQ-HXm.cjs +221 -0
  66. package/dist/tests/pinners-ZgZDx9y6.mjs +203 -0
  67. package/dist/tests/public-fetch-BtiBjQPL.cjs +187 -0
  68. package/dist/tests/public-fetch-Yi7Vks9s.mjs +170 -0
  69. package/dist/tests/public-fetch.test.cjs +1074 -0
  70. package/dist/tests/public-fetch.test.d.cts +1 -0
  71. package/dist/tests/public-fetch.test.d.mts +1 -0
  72. package/dist/tests/public-fetch.test.mjs +1074 -0
  73. package/dist/tests/{request-rtQTKXfB.mjs → request-BKFirRrO.mjs} +1 -1
  74. package/dist/tests/{request-pbEw07NE.cjs → request-CqKju-Ll.cjs} +1 -1
  75. package/dist/tests/request.test.cjs +1 -1
  76. package/dist/tests/request.test.mjs +1 -1
  77. package/dist/tests/support-Cj0T6TP_.cjs +363 -0
  78. package/dist/tests/support-M5ClN-no.mjs +272 -0
  79. package/dist/tests/{url-4PAkYhZC.cjs → url-BR7UVTkW.cjs} +88 -12
  80. package/dist/tests/{url-3fyJuqC0.mjs → url-DQWrMGhu.mjs} +65 -13
  81. package/dist/tests/url.test.cjs +4 -1
  82. package/dist/tests/url.test.mjs +4 -1
  83. package/package.json +14 -3
  84. package/src/docloader.test.ts +700 -32
  85. package/src/docloader.ts +174 -67
  86. package/src/internal.ts +13 -0
  87. package/src/pin-grant.test.ts +118 -0
  88. package/src/pin-grant.ts +99 -0
  89. package/src/pinners/bun.test.ts +278 -0
  90. package/src/pinners/bun.ts +223 -0
  91. package/src/pinners/mod.ts +59 -0
  92. package/src/pinners/node.test.ts +415 -0
  93. package/src/pinners/node.ts +144 -0
  94. package/src/pinners/pinners.test.ts +316 -0
  95. package/src/pinners/testdata/node-child.ts +147 -0
  96. package/src/pinners/testdata/proxy-child.ts +82 -0
  97. package/src/pinners/testdata/support.ts +343 -0
  98. package/src/pinners/testdata/tls-cert.pem +12 -0
  99. package/src/pinners/testdata/tls-child.ts +88 -0
  100. package/src/pinners/testdata/tls-key.pem +5 -0
  101. package/src/pinners/types.ts +48 -0
  102. package/src/public-fetch.test.ts +1286 -0
  103. package/src/public-fetch.ts +338 -0
  104. package/src/testdata/public-fetch-deno-worker.ts +74 -0
  105. package/src/url.test.ts +11 -0
  106. package/src/url.ts +113 -22
  107. package/tsdown.config.ts +12 -3
  108. /package/dist/tests/{langtag-LDBqlxHv.mjs → langtag-D8FWxjv8.mjs} +0 -0
  109. /package/dist/tests/{langtag-DgySESmk.cjs → langtag-a2-s9uyi.cjs} +0 -0
  110. /package/dist/tests/{link-CYszIegt.mjs → link-CHMtH2-k.mjs} +0 -0
  111. /package/dist/tests/{multibase-BL45eko3.cjs → multibase-CPwWBzL3.cjs} +0 -0
  112. /package/dist/tests/{multibase-Da9VJCw8.mjs → multibase-D85wf3ZR.mjs} +0 -0
  113. /package/dist/tests/{multicodec-ByS9XLsw.cjs → multicodec-B-_CSt1M.cjs} +0 -0
  114. /package/dist/tests/{multicodec-BBPZjRfg.mjs → multicodec-caWdG98t.mjs} +0 -0
package/deno.json CHANGED
@@ -1,9 +1,10 @@
1
1
  {
2
2
  "name": "@fedify/vocab-runtime",
3
- "version": "2.3.15-dev.2482+41d364a2",
3
+ "version": "2.3.15",
4
4
  "license": "MIT",
5
5
  "exports": {
6
6
  ".": "./src/mod.ts",
7
+ "./internal": "./src/internal.ts",
7
8
  "./jsonld": "./src/jsonld.ts",
8
9
  "./temporal": "./src/temporal.ts"
9
10
  },
@@ -19,7 +20,8 @@
19
20
  "byte-encodings": "npm:byte-encodings@^1.0.11",
20
21
  "fetch-mock": "npm:fetch-mock@^12.5.4",
21
22
  "jsonld": "npm:jsonld@^9.0.0",
22
- "pkijs": "npm:pkijs@^3.2.5"
23
+ "pkijs": "npm:pkijs@^3.2.5",
24
+ "undici": "npm:undici@^7.30.1"
23
25
  },
24
26
  "exclude": [
25
27
  "dist",
@@ -28,6 +30,7 @@
28
30
  "publish": {
29
31
  "exclude": [
30
32
  "**/*.test.ts",
33
+ "src/**/testdata/",
31
34
  "tsdown.config.ts"
32
35
  ]
33
36
  },
@@ -0,0 +1,6 @@
1
+
2
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
3
+ const require_public_fetch = require("./public-fetch-Bk32IaYB.cjs");
4
+ exports.openDirectFetch = require_public_fetch.openDirectFetch;
5
+ exports.openPublicFetch = require_public_fetch.openPublicFetch;
6
+ exports.withPublicFetch = require_public_fetch.withPublicFetch;
@@ -0,0 +1,3 @@
1
+ /// <reference lib="esnext.temporal" />
2
+ import { a as withPublicFetch, i as openPublicFetch, n as PublicFetchSession, r as openDirectFetch, t as PublicFetchOptions } from "./public-fetch-DBpt3xxf.cjs";
3
+ export { type PublicFetchOptions, type PublicFetchSession, openDirectFetch, openPublicFetch, withPublicFetch };
@@ -0,0 +1,3 @@
1
+ /// <reference lib="esnext.temporal" />
2
+ import { a as withPublicFetch, i as openPublicFetch, n as PublicFetchSession, r as openDirectFetch, t as PublicFetchOptions } from "./public-fetch-BC-nWJNM.js";
3
+ export { type PublicFetchOptions, type PublicFetchSession, openDirectFetch, openPublicFetch, withPublicFetch };
@@ -0,0 +1,3 @@
1
+
2
+ import { n as openPublicFetch, r as withPublicFetch, t as openDirectFetch } from "./public-fetch-Dfmc_Kfy.js";
3
+ export { openDirectFetch, openPublicFetch, withPublicFetch };
package/dist/mod.cjs CHANGED
@@ -1,13 +1,11 @@
1
1
 
2
2
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
3
3
  const require_chunk = require("./chunk-M78iaK0I.cjs");
4
+ const require_public_fetch = require("./public-fetch-Bk32IaYB.cjs");
4
5
  let _logtape_logtape = require("@logtape/logtape");
5
6
  let _opentelemetry_api = require("@opentelemetry/api");
6
7
  let node_process = require("node:process");
7
8
  node_process = require_chunk.__toESM(node_process, 1);
8
- let node_dns_promises = require("node:dns/promises");
9
- node_dns_promises = require_chunk.__toESM(node_dns_promises, 1);
10
- let node_net = require("node:net");
11
9
  let asn1js = require("asn1js");
12
10
  let byte_encodings_base64 = require("byte-encodings/base64");
13
11
  let byte_encodings_base64url = require("byte-encodings/base64url");
@@ -4537,7 +4535,7 @@ const preloadedContexts = {
4537
4535
  //#endregion
4538
4536
  //#region deno.json
4539
4537
  var name = "@fedify/vocab-runtime";
4540
- var version = "2.3.15-dev.2482+41d364a2";
4538
+ var version = "2.3.15";
4541
4539
  //#endregion
4542
4540
  //#region src/request.ts
4543
4541
  /**
@@ -4864,216 +4862,6 @@ var HttpHeaderLink = class HttpHeaderLink {
4864
4862
  }
4865
4863
  };
4866
4864
  //#endregion
4867
- //#region src/url.ts
4868
- var UrlError = class extends Error {
4869
- /**
4870
- * The failure category. Defaults to `"disallowed"`.
4871
- * `"dns"` includes lookup errors and results with no usable IP addresses.
4872
- * @since 2.0.29
4873
- */
4874
- reason;
4875
- constructor(message, options) {
4876
- super(message, options);
4877
- this.name = "UrlError";
4878
- this.reason = options?.reason ?? "disallowed";
4879
- }
4880
- };
4881
- /**
4882
- * Validates a URL to prevent SSRF attacks.
4883
- */
4884
- async function validatePublicUrl(url) {
4885
- const parsed = new URL(url);
4886
- if (parsed.protocol !== "http:" && parsed.protocol !== "https:") throw new UrlError(`Unsupported protocol: ${parsed.protocol}`);
4887
- let hostname = parsed.hostname;
4888
- if (hostname.startsWith("[") && hostname.endsWith("]")) hostname = hostname.slice(1, -1);
4889
- if (hostname === "localhost") throw new UrlError("Localhost is not allowed");
4890
- const hostnameFamily = (0, node_net.isIP)(hostname);
4891
- if (hostnameFamily !== 0) {
4892
- validatePublicIpAddress(hostname, hostnameFamily);
4893
- return;
4894
- }
4895
- if ("Deno" in globalThis && !(0, node_net.isIP)(hostname)) {
4896
- if ((await Deno.permissions.query({ name: "net" })).state !== "granted") return;
4897
- }
4898
- if ("Bun" in globalThis) {
4899
- if (hostname === "example.com" || hostname.endsWith(".example.com")) return;
4900
- else if (hostname === "fedify-test.internal") throw new UrlError("Invalid or private address: fedify-test.internal");
4901
- }
4902
- let addresses;
4903
- try {
4904
- addresses = await node_dns_promises.default.lookup(hostname, { all: true });
4905
- } catch (error) {
4906
- throw new UrlError("DNS lookup failed", {
4907
- cause: error,
4908
- reason: "dns"
4909
- });
4910
- }
4911
- validateLookupAddresses(addresses);
4912
- }
4913
- /**
4914
- * Validates the IP addresses returned by `node:dns.lookup()`.
4915
- *
4916
- * Cloudflare Workers' `node:dns` implementation currently maps every record
4917
- * in a DNS-over-HTTPS `Answer` array—including CNAME records—to a
4918
- * `LookupAddress`, even though Node.js specifies that the `address` field must
4919
- * contain an IPv4 or IPv6 literal. See:
4920
- * https://github.com/cloudflare/workerd/issues/6886
4921
- *
4922
- * Work around that bug by ignoring non-IP entries only when the lookup also
4923
- * returns at least one actual IP address. This remains fail-closed: a result
4924
- * containing no IP addresses is rejected, and every returned IP address is
4925
- * still validated and must be public. This workaround can be revisited once
4926
- * the Workerd issue is fixed in supported Cloudflare Workers runtimes.
4927
- *
4928
- * @internal
4929
- */
4930
- function validateLookupAddresses(addresses) {
4931
- let ipAddressCount = 0;
4932
- for (const { address } of addresses) {
4933
- const family = (0, node_net.isIP)(address);
4934
- if (family === 0) continue;
4935
- ipAddressCount++;
4936
- validatePublicIpAddress(address, family);
4937
- }
4938
- if (ipAddressCount === 0) throw new UrlError("DNS lookup did not return any IP address", { reason: "dns" });
4939
- }
4940
- function validatePublicIpAddress(address, family) {
4941
- if (family === 4 && isValidPublicIPv4Address(address) || family === 6 && isValidPublicIPv6Address(address)) return;
4942
- throw new UrlError(`Invalid or private address: ${address}`);
4943
- }
4944
- function isValidPublicIPv4Address(address) {
4945
- const parts = parseIPv4Address(address);
4946
- if (parts == null) return false;
4947
- const value = ipv4PartsToNumber(parts);
4948
- return !nonPublicIPv4Prefixes.some(({ base, prefix }) => matchesIPv4Prefix(value, base, prefix));
4949
- }
4950
- function isValidPublicIPv6Address(address) {
4951
- const words = parseIPv6Address(address);
4952
- if (words == null) return false;
4953
- if (nonPublicIPv6Prefixes.some(({ words: prefixWords, prefix }) => matchesIPv6Prefix(words, prefixWords, prefix))) return false;
4954
- for (const { extractIPv4, prefix, words: prefixWords } of ipv6WithIPv4Prefixes) {
4955
- if (!matchesIPv6Prefix(words, prefixWords, prefix)) continue;
4956
- const ipv4Address = extractIPv4(words);
4957
- if (ipv4Address != null && !isValidPublicIPv4Address(ipv4Address)) return false;
4958
- }
4959
- return true;
4960
- }
4961
- function expandIPv6Address(address) {
4962
- address = address.toLowerCase();
4963
- const ipv4Delimiter = address.lastIndexOf(":");
4964
- if (address.includes(".") && ipv4Delimiter >= 0) {
4965
- const ipv4Parts = parseIPv4Address(address.substring(ipv4Delimiter + 1));
4966
- if (ipv4Parts == null) return address;
4967
- const high = (ipv4Parts[0] << 8) + ipv4Parts[1];
4968
- const low = (ipv4Parts[2] << 8) + ipv4Parts[3];
4969
- address = address.substring(0, ipv4Delimiter + 1) + high.toString(16) + ":" + low.toString(16);
4970
- }
4971
- if (address === "::") return "0000:0000:0000:0000:0000:0000:0000:0000";
4972
- if (address.startsWith("::")) address = "0000" + address;
4973
- if (address.endsWith("::")) address = address + "0000";
4974
- address = address.replace("::", ":0000".repeat(8 - (address.match(/:/g) || []).length) + ":");
4975
- return address.split(":").map((part) => part.padStart(4, "0")).join(":");
4976
- }
4977
- const nonPublicIPv4Prefixes = [
4978
- ipv4Prefix("0.0.0.0/8", "RFC 6890"),
4979
- ipv4Prefix("10.0.0.0/8", "RFC 1918"),
4980
- ipv4Prefix("100.64.0.0/10", "RFC 6598"),
4981
- ipv4Prefix("127.0.0.0/8", "RFC 1122"),
4982
- ipv4Prefix("169.254.0.0/16", "RFC 3927"),
4983
- ipv4Prefix("172.16.0.0/12", "RFC 1918"),
4984
- ipv4Prefix("192.0.0.0/24", "RFC 6890"),
4985
- ipv4Prefix("192.0.2.0/24", "RFC 5737"),
4986
- ipv4Prefix("192.88.99.0/24", "RFC 7526"),
4987
- ipv4Prefix("192.168.0.0/16", "RFC 1918"),
4988
- ipv4Prefix("198.18.0.0/15", "RFC 2544"),
4989
- ipv4Prefix("198.51.100.0/24", "RFC 5737"),
4990
- ipv4Prefix("203.0.113.0/24", "RFC 5737"),
4991
- ipv4Prefix("224.0.0.0/4", "RFC 5771"),
4992
- ipv4Prefix("240.0.0.0/4", "RFC 1112")
4993
- ];
4994
- const nonPublicIPv6Prefixes = [
4995
- ipv6Prefix("::/16", "RFC 4291"),
4996
- ipv6Prefix("2001::/32", "RFC 4380"),
4997
- ipv6Prefix("2002::/16", "RFC 3056"),
4998
- ipv6Prefix("64:ff9b:1::/48", "RFC 8215"),
4999
- ipv6Prefix("fc00::/7", "RFC 4193"),
5000
- ipv6Prefix("fe80::/10", "RFC 4291"),
5001
- ipv6Prefix("ff00::/8", "RFC 4291")
5002
- ];
5003
- const ipv6WithIPv4Prefixes = [{
5004
- ...ipv6Prefix("64:ff9b::/96", "RFC 6052"),
5005
- extractIPv4: (words) => ipv4FromWords(words[6], words[7])
5006
- }];
5007
- function ipv4Prefix(cidr, rfc) {
5008
- const [address, prefixText] = cidr.split("/");
5009
- const prefix = parseInt(prefixText, 10);
5010
- const parts = parseIPv4Address(address);
5011
- if (parts == null || !Number.isInteger(prefix) || prefix < 0 || prefix > 32) throw new Error(`Invalid IPv4 prefix: ${cidr}`);
5012
- return {
5013
- cidr,
5014
- base: ipv4PartsToNumber(parts),
5015
- prefix,
5016
- rfc
5017
- };
5018
- }
5019
- function ipv6Prefix(cidr, rfc) {
5020
- const [address, prefixText] = cidr.split("/");
5021
- const prefix = parseInt(prefixText, 10);
5022
- const words = parseIPv6Address(address);
5023
- if (words == null || !Number.isInteger(prefix) || prefix < 0 || prefix > 128) throw new Error(`Invalid IPv6 prefix: ${cidr}`);
5024
- return {
5025
- cidr,
5026
- words,
5027
- prefix,
5028
- rfc
5029
- };
5030
- }
5031
- function parseIPv4Address(address) {
5032
- const parts = address.split(".").map((part) => {
5033
- if (!/^\d+$/.test(part)) return NaN;
5034
- return parseInt(part, 10);
5035
- });
5036
- if (parts.length !== 4 || parts.some((part) => !Number.isInteger(part) || part < 0 || part > 255)) return null;
5037
- return parts;
5038
- }
5039
- function parseIPv6Address(address) {
5040
- const parts = expandIPv6Address(address).split(":");
5041
- if (parts.length !== 8) return null;
5042
- const words = parts.map((part) => {
5043
- if (!/^[0-9a-f]{1,4}$/i.test(part)) return NaN;
5044
- return parseInt(part, 16);
5045
- });
5046
- if (words.some((word) => !Number.isInteger(word) || word < 0 || word > 65535)) return null;
5047
- return words;
5048
- }
5049
- function ipv4PartsToNumber(parts) {
5050
- return parts[0] * 2 ** 24 + parts[1] * 2 ** 16 + parts[2] * 2 ** 8 + parts[3];
5051
- }
5052
- function ipv4FromWords(highWord, lowWord) {
5053
- return [
5054
- highWord >> 8,
5055
- highWord & 255,
5056
- lowWord >> 8,
5057
- lowWord & 255
5058
- ].join(".");
5059
- }
5060
- function matchesIPv4Prefix(address, prefixBase, prefixLength) {
5061
- const blockSize = 2 ** (32 - prefixLength);
5062
- return Math.floor(address / blockSize) === Math.floor(prefixBase / blockSize);
5063
- }
5064
- function matchesIPv6Prefix(address, prefixWords, prefixLength) {
5065
- let remaining = prefixLength;
5066
- for (let i = 0; i < 8 && remaining > 0; i++) if (remaining >= 16) {
5067
- if (address[i] !== prefixWords[i]) return false;
5068
- remaining -= 16;
5069
- } else {
5070
- const mask = 65535 << 16 - remaining & 65535;
5071
- if ((address[i] & mask) !== (prefixWords[i] & mask)) return false;
5072
- remaining = 0;
5073
- }
5074
- return true;
5075
- }
5076
- //#endregion
5077
4865
  //#region src/docloader.ts
5078
4866
  const logger = (0, _logtape_logtape.getLogger)([
5079
4867
  "fedify",
@@ -5085,13 +4873,47 @@ const DEFAULT_MAX_REDIRECTION = 20;
5085
4873
  * Gets a {@link RemoteDocument} from the given response.
5086
4874
  * @param url The URL of the document to load.
5087
4875
  * @param response The response to get the document from.
5088
- * @param fetch The function to fetch the document.
4876
+ * @param fetch The function to fetch the document. It is called with the URL
4877
+ * of an alternate document, if any, after the response body has
4878
+ * been either read or its cancellation has been started.
5089
4879
  * @returns The loaded remote document.
5090
4880
  * @throws {FetchError} If the response is not OK.
5091
4881
  * @internal
5092
4882
  */
5093
4883
  async function getRemoteDocument(url, response, fetch) {
5094
- const documentUrl = response.url === "" ? url : response.url;
4884
+ const result = await parseRemoteDocument(url, response);
4885
+ if ("alternate" in result) return await fetch(result.alternate);
4886
+ return result.document;
4887
+ }
4888
+ /**
4889
+ * Starts cancelling the body of a response without waiting for it to settle.
4890
+ * The cancellation of a branch of a tee (e.g., a body cloned by a fetch mock)
4891
+ * settles only after the other branch is cancelled too, per the Streams
4892
+ * standard, so awaiting it could block forever. Failures are only logged.
4893
+ * @param response The response whose body is cancelled.
4894
+ * @param url The URL of the response, for logging.
4895
+ */
4896
+ function cancelResponseBody(response, url) {
4897
+ response.body?.cancel().catch((error) => {
4898
+ logger.debug("Failed to cancel the response body from {url}: {error}", {
4899
+ url,
4900
+ error
4901
+ });
4902
+ });
4903
+ }
4904
+ /**
4905
+ * Gets a {@link RemoteDocument} from the given response, or the URL of an
4906
+ * alternate document to load instead. When an alternate document is found,
4907
+ * the response body has been either read or its cancellation has been started
4908
+ * (without waiting for it), so that the alternate document can be loaded after
4909
+ * the response's session is closed.
4910
+ * @param url The URL of the document to load.
4911
+ * @param response The response to get the document from.
4912
+ * @returns The loaded remote document, or the alternate document URL.
4913
+ * @throws {FetchError} If the response is not OK.
4914
+ */
4915
+ async function parseRemoteDocument(url, response) {
4916
+ const documentUrl = url;
5095
4917
  const docUrl = new URL(documentUrl);
5096
4918
  if (!response.ok) {
5097
4919
  logger.error("Failed to fetch document: {status} {url} {headers}", {
@@ -5128,7 +4950,8 @@ async function getRemoteDocument(url, response, fetch) {
5128
4950
  alternateUrl: altUri.href,
5129
4951
  url: documentUrl
5130
4952
  });
5131
- return await fetch(altUri.href);
4953
+ cancelResponseBody(response, documentUrl);
4954
+ return { alternate: altUri.href };
5132
4955
  }
5133
4956
  }
5134
4957
  }
@@ -5153,7 +4976,7 @@ async function getRemoteDocument(url, response, fetch) {
5153
4976
  alternateUrl: attribs.href,
5154
4977
  url: documentUrl
5155
4978
  });
5156
- return await fetch(new URL(attribs.href, docUrl).href);
4979
+ return { alternate: new URL(attribs.href, docUrl).href };
5157
4980
  }
5158
4981
  }
5159
4982
  document = JSON.parse(html);
@@ -5163,11 +4986,11 @@ async function getRemoteDocument(url, response, fetch) {
5163
4986
  url: documentUrl,
5164
4987
  headers: Object.fromEntries(response.headers.entries())
5165
4988
  });
5166
- return {
4989
+ return { document: {
5167
4990
  contextUrl,
5168
4991
  document,
5169
4992
  documentUrl
5170
- };
4993
+ } };
5171
4994
  }
5172
4995
  /**
5173
4996
  * Creates a JSON-LD document loader that utilizes the browser's `fetch` API.
@@ -5193,14 +5016,59 @@ async function getRemoteDocument(url, response, fetch) {
5193
5016
  * @returns The document loader.
5194
5017
  * @since 1.3.0
5195
5018
  */
5196
- function getDocumentLoader({ allowPrivateAddress, maxRedirection, skipPreloadedContexts, userAgent } = {}) {
5019
+ function getDocumentLoader(options = {}) {
5020
+ return createDocumentLoader(options);
5021
+ }
5022
+ /**
5023
+ * The implementation of {@link getDocumentLoader}, with injectable
5024
+ * dependencies. This is not exported from any entry point.
5025
+ * @param options Options for the document loader.
5026
+ * @param deps The dependencies.
5027
+ * @returns The document loader.
5028
+ */
5029
+ function createDocumentLoader({ allowPrivateAddress, maxRedirection, skipPreloadedContexts, userAgent } = {}, deps = {}) {
5197
5030
  const tracer = _opentelemetry_api.trace.getTracerProvider().getTracer(name, version);
5198
5031
  const maximumRedirection = maxRedirection ?? DEFAULT_MAX_REDIRECTION;
5032
+ async function openHop(url, signal) {
5033
+ try {
5034
+ if (deps.openHop != null) return await deps.openHop(url);
5035
+ return allowPrivateAddress ? await require_public_fetch.openDirectFetch(url) : await require_public_fetch.openPublicFetch(url, { signal });
5036
+ } catch (error) {
5037
+ if (error instanceof require_public_fetch.UrlError) if (error.reason === "dns") logger.debug("DNS lookup failed for {url}", {
5038
+ url,
5039
+ error
5040
+ });
5041
+ else logger.error("Disallowed private URL: {url}", {
5042
+ url,
5043
+ error
5044
+ });
5045
+ throw error;
5046
+ }
5047
+ }
5048
+ async function fetchHop(session, url, span, options) {
5049
+ try {
5050
+ const request = createActivityPubRequest(url, { userAgent });
5051
+ logRequest(logger, request);
5052
+ const response = await session.fetch(request, {
5053
+ redirect: "manual",
5054
+ signal: options?.signal
5055
+ });
5056
+ span.setAttribute("http.response.status_code", response.status);
5057
+ if (response.status >= 300 && response.status < 400 && response.headers.has("Location")) {
5058
+ const location = response.headers.get("Location");
5059
+ cancelResponseBody(response, url);
5060
+ return { location };
5061
+ }
5062
+ return await parseRemoteDocument(url, response);
5063
+ } finally {
5064
+ await session.close();
5065
+ }
5066
+ }
5199
5067
  async function load(url, options, redirected = 0, visited = /* @__PURE__ */ new Set()) {
5200
5068
  options?.signal?.throwIfAborted();
5201
5069
  const parsedUrl = new URL(url);
5202
- if (parsedUrl.protocol !== "http:" && parsedUrl.protocol !== "https:") throw new UrlError(`Unsupported protocol: ${parsedUrl.protocol}`);
5203
- if (parsedUrl.username !== "" || parsedUrl.password !== "") throw new UrlError("URLs with credentials are not allowed");
5070
+ if (parsedUrl.protocol !== "http:" && parsedUrl.protocol !== "https:") throw new require_public_fetch.UrlError(`Unsupported protocol: ${parsedUrl.protocol}`);
5071
+ if (parsedUrl.username !== "" || parsedUrl.password !== "") throw new require_public_fetch.UrlError("URLs with credentials are not allowed");
5204
5072
  const currentUrl = parsedUrl.href;
5205
5073
  if (!skipPreloadedContexts && currentUrl in preloadedContexts) {
5206
5074
  logger.debug("Using preloaded context: {url}.", { url: currentUrl });
@@ -5210,33 +5078,15 @@ function getDocumentLoader({ allowPrivateAddress, maxRedirection, skipPreloadedC
5210
5078
  documentUrl: currentUrl
5211
5079
  };
5212
5080
  }
5213
- if (!allowPrivateAddress) try {
5214
- await validatePublicUrl(currentUrl);
5215
- } catch (error) {
5216
- if (error instanceof UrlError) if (error.reason === "dns") logger.debug("DNS lookup failed for {url}", {
5217
- url: currentUrl,
5218
- error
5219
- });
5220
- else logger.error("Disallowed private URL: {url}", {
5221
- url: currentUrl,
5222
- error
5223
- });
5224
- throw error;
5225
- }
5081
+ const session = await openHop(currentUrl, options?.signal);
5226
5082
  visited.add(currentUrl);
5227
5083
  return await tracer.startActiveSpan("activitypub.fetch_document", {
5228
5084
  kind: _opentelemetry_api.SpanKind.CLIENT,
5229
5085
  attributes: { "url.full": currentUrl }
5230
5086
  }, async (span) => {
5231
5087
  try {
5232
- const request = createActivityPubRequest(currentUrl, { userAgent });
5233
- logRequest(logger, request);
5234
- const response = await fetch(request, {
5235
- redirect: "manual",
5236
- signal: options?.signal
5237
- });
5238
- span.setAttribute("http.response.status_code", response.status);
5239
- if (response.status >= 300 && response.status < 400 && response.headers.has("Location")) {
5088
+ const outcome = await fetchHop(session, currentUrl, span, options);
5089
+ if ("location" in outcome) {
5240
5090
  if (redirected >= maximumRedirection) {
5241
5091
  logger.error("Too many redirections ({redirections}) while fetching document.", {
5242
5092
  redirections: redirected + 1,
@@ -5244,7 +5094,7 @@ function getDocumentLoader({ allowPrivateAddress, maxRedirection, skipPreloadedC
5244
5094
  });
5245
5095
  throw new FetchError(currentUrl, `Too many redirections (${redirected + 1})`);
5246
5096
  }
5247
- const redirectUrl = new URL(response.headers.get("Location"), response.url === "" ? currentUrl : response.url).href;
5097
+ const redirectUrl = new URL(outcome.location, currentUrl).href;
5248
5098
  span.setAttribute("http.redirect.url", redirectUrl);
5249
5099
  if (visited.has(redirectUrl)) {
5250
5100
  logger.error("Detected a redirect loop while fetching document: {url} -> {redirectUrl}", {
@@ -5255,12 +5105,14 @@ function getDocumentLoader({ allowPrivateAddress, maxRedirection, skipPreloadedC
5255
5105
  }
5256
5106
  return await load(redirectUrl, options, redirected + 1, visited);
5257
5107
  }
5258
- const result = await getRemoteDocument(currentUrl, response, async (alternateUrl) => {
5108
+ let result;
5109
+ if ("alternate" in outcome) {
5110
+ const alternateUrl = outcome.alternate;
5259
5111
  options?.signal?.throwIfAborted();
5260
5112
  if (redirected >= DEFAULT_MAX_REDIRECTION) throw new FetchError(currentUrl, `Too many redirections (${redirected + 1})`);
5261
5113
  if (visited.has(alternateUrl)) throw new FetchError(currentUrl, `Redirect loop detected: ${alternateUrl}`);
5262
- return await load(alternateUrl, options, redirected + 1, visited);
5263
- });
5114
+ result = await load(alternateUrl, options, redirected + 1, visited);
5115
+ } else result = outcome.document;
5264
5116
  span.setAttribute("docloader.document_url", result.documentUrl);
5265
5117
  if (result.contextUrl != null) span.setAttribute("docloader.context_url", result.contextUrl);
5266
5118
  return result;
@@ -5927,13 +5779,13 @@ LanguageString.prototype[Symbol.for("nodejs.util.inspect.custom")] = function(_d
5927
5779
  //#endregion
5928
5780
  exports.FetchError = FetchError;
5929
5781
  exports.LanguageString = LanguageString;
5930
- exports.UrlError = UrlError;
5782
+ exports.UrlError = require_public_fetch.UrlError;
5931
5783
  exports.canParseDecimal = canParseDecimal;
5932
5784
  exports.createActivityPubRequest = createActivityPubRequest;
5933
5785
  exports.decodeMultibase = decodeMultibase;
5934
5786
  exports.encodeMultibase = encodeMultibase;
5935
5787
  exports.encodingFromBaseData = encodingFromBaseData;
5936
- exports.expandIPv6Address = expandIPv6Address;
5788
+ exports.expandIPv6Address = require_public_fetch.expandIPv6Address;
5937
5789
  exports.exportMultibaseKey = exportMultibaseKey;
5938
5790
  exports.exportSpki = exportSpki;
5939
5791
  exports.getDocumentLoader = getDocumentLoader;
@@ -5944,10 +5796,10 @@ exports.importPem = importPem;
5944
5796
  exports.importPkcs1 = importPkcs1;
5945
5797
  exports.importSpki = importSpki;
5946
5798
  exports.isDecimal = isDecimal;
5947
- exports.isValidPublicIPv4Address = isValidPublicIPv4Address;
5948
- exports.isValidPublicIPv6Address = isValidPublicIPv6Address;
5799
+ exports.isValidPublicIPv4Address = require_public_fetch.isValidPublicIPv4Address;
5800
+ exports.isValidPublicIPv6Address = require_public_fetch.isValidPublicIPv6Address;
5949
5801
  exports.logRequest = logRequest;
5950
5802
  exports.normalizeLanguageTag = normalizeLanguageTag;
5951
5803
  exports.parseDecimal = parseDecimal;
5952
5804
  exports.preloadedContexts = preloadedContexts;
5953
- exports.validatePublicUrl = validatePublicUrl;
5805
+ exports.validatePublicUrl = require_public_fetch.validatePublicUrl;
package/dist/mod.d.cts CHANGED
@@ -1,4 +1,5 @@
1
1
  /// <reference lib="esnext.temporal" />
2
+ import { c as isValidPublicIPv4Address, l as isValidPublicIPv6Address, o as UrlError, s as expandIPv6Address, u as validatePublicUrl } from "./public-fetch-DBpt3xxf.cjs";
2
3
  import { Logger } from "@logtape/logtape";
3
4
  import { TracerProvider } from "@opentelemetry/api";
4
5
 
@@ -163,7 +164,9 @@ type AuthenticatedDocumentLoaderFactory = (identity: {
163
164
  * Gets a {@link RemoteDocument} from the given response.
164
165
  * @param url The URL of the document to load.
165
166
  * @param response The response to get the document from.
166
- * @param fetch The function to fetch the document.
167
+ * @param fetch The function to fetch the document. It is called with the URL
168
+ * of an alternate document, if any, after the response body has
169
+ * been either read or its cancellation has been started.
167
170
  * @returns The loaded remote document.
168
171
  * @throws {FetchError} If the response is not OK.
169
172
  * @internal
@@ -203,12 +206,7 @@ interface GetDocumentLoaderOptions extends DocumentLoaderFactoryOptions {
203
206
  * @returns The document loader.
204
207
  * @since 1.3.0
205
208
  */
206
- declare function getDocumentLoader({
207
- allowPrivateAddress,
208
- maxRedirection,
209
- skipPreloadedContexts,
210
- userAgent
211
- }?: GetDocumentLoaderOptions): DocumentLoader;
209
+ declare function getDocumentLoader(options?: GetDocumentLoaderOptions): DocumentLoader;
212
210
  //#endregion
213
211
  //#region src/key.d.ts
214
212
  /**
@@ -489,25 +487,4 @@ interface PropertyPreprocessorContext {
489
487
  */
490
488
  type PropertyPreprocessor<T = unknown> = (value: Json, context: PropertyPreprocessorContext) => T | undefined | Error | Promise<T | undefined | Error>;
491
489
  //#endregion
492
- //#region src/url.d.ts
493
- declare class UrlError extends Error {
494
- /**
495
- * The failure category. Defaults to `"disallowed"`.
496
- * `"dns"` includes lookup errors and results with no usable IP addresses.
497
- * @since 2.0.29
498
- */
499
- readonly reason: "dns" | "disallowed";
500
- constructor(message: string, options?: {
501
- cause?: unknown;
502
- reason?: "dns" | "disallowed";
503
- });
504
- }
505
- /**
506
- * Validates a URL to prevent SSRF attacks.
507
- */
508
- declare function validatePublicUrl(url: string): Promise<void>;
509
- declare function isValidPublicIPv4Address(address: string): boolean;
510
- declare function isValidPublicIPv6Address(address: string): boolean;
511
- declare function expandIPv6Address(address: string): string;
512
- //#endregion
513
490
  export { type AuthenticatedDocumentLoaderFactory, type CreateRequestOptions, type Decimal, type DocumentLoader, type DocumentLoaderFactory, type DocumentLoaderFactoryOptions, type DocumentLoaderOptions, FetchError, type GetDocumentLoaderOptions, type GetUserAgentOptions, type Json, LanguageString, type PropertyPreprocessor, type PropertyPreprocessorContext, type RemoteDocument, UrlError, canParseDecimal, createActivityPubRequest, decodeMultibase, encodeMultibase, encodingFromBaseData, expandIPv6Address, exportMultibaseKey, exportSpki, getDocumentLoader, getRemoteDocument, getUserAgent, importMultibaseKey, importPem, importPkcs1, importSpki, isDecimal, isValidPublicIPv4Address, isValidPublicIPv6Address, logRequest, normalizeLanguageTag, parseDecimal, preloadedContexts, validatePublicUrl };
package/dist/mod.d.ts CHANGED
@@ -1,6 +1,8 @@
1
1
  /// <reference lib="esnext.temporal" />
2
+ import { c as isValidPublicIPv4Address, l as isValidPublicIPv6Address, o as UrlError, s as expandIPv6Address, u as validatePublicUrl } from "./public-fetch-BC-nWJNM.js";
2
3
  import { Logger } from "@logtape/logtape";
3
4
  import { TracerProvider } from "@opentelemetry/api";
5
+
4
6
  //#region src/contexts.d.ts
5
7
  declare const preloadedContexts: Record<string, unknown>;
6
8
  //#endregion
@@ -162,7 +164,9 @@ type AuthenticatedDocumentLoaderFactory = (identity: {
162
164
  * Gets a {@link RemoteDocument} from the given response.
163
165
  * @param url The URL of the document to load.
164
166
  * @param response The response to get the document from.
165
- * @param fetch The function to fetch the document.
167
+ * @param fetch The function to fetch the document. It is called with the URL
168
+ * of an alternate document, if any, after the response body has
169
+ * been either read or its cancellation has been started.
166
170
  * @returns The loaded remote document.
167
171
  * @throws {FetchError} If the response is not OK.
168
172
  * @internal
@@ -202,12 +206,7 @@ interface GetDocumentLoaderOptions extends DocumentLoaderFactoryOptions {
202
206
  * @returns The document loader.
203
207
  * @since 1.3.0
204
208
  */
205
- declare function getDocumentLoader({
206
- allowPrivateAddress,
207
- maxRedirection,
208
- skipPreloadedContexts,
209
- userAgent
210
- }?: GetDocumentLoaderOptions): DocumentLoader;
209
+ declare function getDocumentLoader(options?: GetDocumentLoaderOptions): DocumentLoader;
211
210
  //#endregion
212
211
  //#region src/key.d.ts
213
212
  /**
@@ -488,25 +487,4 @@ interface PropertyPreprocessorContext {
488
487
  */
489
488
  type PropertyPreprocessor<T = unknown> = (value: Json, context: PropertyPreprocessorContext) => T | undefined | Error | Promise<T | undefined | Error>;
490
489
  //#endregion
491
- //#region src/url.d.ts
492
- declare class UrlError extends Error {
493
- /**
494
- * The failure category. Defaults to `"disallowed"`.
495
- * `"dns"` includes lookup errors and results with no usable IP addresses.
496
- * @since 2.0.29
497
- */
498
- readonly reason: "dns" | "disallowed";
499
- constructor(message: string, options?: {
500
- cause?: unknown;
501
- reason?: "dns" | "disallowed";
502
- });
503
- }
504
- /**
505
- * Validates a URL to prevent SSRF attacks.
506
- */
507
- declare function validatePublicUrl(url: string): Promise<void>;
508
- declare function isValidPublicIPv4Address(address: string): boolean;
509
- declare function isValidPublicIPv6Address(address: string): boolean;
510
- declare function expandIPv6Address(address: string): string;
511
- //#endregion
512
490
  export { type AuthenticatedDocumentLoaderFactory, type CreateRequestOptions, type Decimal, type DocumentLoader, type DocumentLoaderFactory, type DocumentLoaderFactoryOptions, type DocumentLoaderOptions, FetchError, type GetDocumentLoaderOptions, type GetUserAgentOptions, type Json, LanguageString, type PropertyPreprocessor, type PropertyPreprocessorContext, type RemoteDocument, UrlError, canParseDecimal, createActivityPubRequest, decodeMultibase, encodeMultibase, encodingFromBaseData, expandIPv6Address, exportMultibaseKey, exportSpki, getDocumentLoader, getRemoteDocument, getUserAgent, importMultibaseKey, importPem, importPkcs1, importSpki, isDecimal, isValidPublicIPv4Address, isValidPublicIPv6Address, logRequest, normalizeLanguageTag, parseDecimal, preloadedContexts, validatePublicUrl };