@fedify/vocab-runtime 2.4.0-dev.1570 → 2.4.0-dev.1573

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 (48) hide show
  1. package/deno.json +2 -1
  2. package/dist/docloader-DnUMWHaJ.d.cts +202 -0
  3. package/dist/docloader-xRGn1azD.d.ts +202 -0
  4. package/dist/internal/jsonld-cache.cjs +279 -0
  5. package/dist/internal/jsonld-cache.d.cts +48 -0
  6. package/dist/internal/jsonld-cache.d.ts +48 -0
  7. package/dist/internal/jsonld-cache.js +275 -0
  8. package/dist/mod.cjs +178 -190
  9. package/dist/mod.d.cts +59 -190
  10. package/dist/mod.d.ts +59 -190
  11. package/dist/mod.js +165 -184
  12. package/dist/tests/decimal.test.cjs +3 -3
  13. package/dist/tests/decimal.test.mjs +3 -3
  14. package/dist/tests/{docloader-C76ldE5C.mjs → docloader-B5Upa3Ox.mjs} +97 -10
  15. package/dist/tests/{docloader-mvgIWKI7.cjs → docloader-C69O-D5T.cjs} +102 -9
  16. package/dist/tests/docloader.test.cjs +58 -6
  17. package/dist/tests/docloader.test.mjs +58 -6
  18. package/dist/tests/jsonld-cache.test.cjs +652 -0
  19. package/dist/tests/jsonld-cache.test.d.cts +1 -0
  20. package/dist/tests/jsonld-cache.test.d.mts +1 -0
  21. package/dist/tests/jsonld-cache.test.mjs +651 -0
  22. package/dist/tests/{key-CrrK9mYh.mjs → key-CDGDH_vC.mjs} +69 -1
  23. package/dist/tests/{key-pMmqUKuo.cjs → key-_wXwomh_.cjs} +86 -0
  24. package/dist/tests/key.test.cjs +48 -1
  25. package/dist/tests/key.test.mjs +48 -1
  26. package/dist/tests/{request-BEXkv1ul.mjs → request-BGoZTy5L.mjs} +1 -1
  27. package/dist/tests/{request-SworbvLc.cjs → request-DX4gki5x.cjs} +1 -1
  28. package/dist/tests/request.test.cjs +1 -1
  29. package/dist/tests/request.test.mjs +1 -1
  30. package/dist/tests/{url-C20FhC7p.cjs → url-2XwVbUS_.cjs} +108 -0
  31. package/dist/tests/{url-m9Qzxy-Y.mjs → url-YWJbnRlf.mjs} +85 -1
  32. package/dist/tests/url.test.cjs +104 -1
  33. package/dist/tests/url.test.mjs +105 -2
  34. package/dist/url-BAdyyqAa.cjs +315 -0
  35. package/dist/url-BuxPHxK2.js +261 -0
  36. package/package.json +11 -1
  37. package/src/contexts/fep-7aa9.json +24 -0
  38. package/src/contexts.ts +2 -0
  39. package/src/docloader.test.ts +102 -6
  40. package/src/docloader.ts +76 -8
  41. package/src/internal/jsonld-cache.ts +538 -0
  42. package/src/jsonld-cache.test.ts +554 -0
  43. package/src/key.test.ts +86 -0
  44. package/src/key.ts +125 -0
  45. package/src/mod.ts +8 -0
  46. package/src/url.test.ts +252 -1
  47. package/src/url.ts +141 -0
  48. package/tsdown.config.ts +6 -1
package/dist/mod.js CHANGED
@@ -1,9 +1,8 @@
1
1
 
2
+ import { a as isValidPublicIPv4Address, c as parseJsonLdId, i as haveSameIriOrigin, l as validatePublicUrl, n as expandIPv6Address, o as isValidPublicIPv6Address, r as formatIri, s as parseIri, t as UrlError } from "./url-BuxPHxK2.js";
2
3
  import { getLogger } from "@logtape/logtape";
3
4
  import { SpanKind, SpanStatusCode, trace } from "@opentelemetry/api";
4
5
  import process from "node:process";
5
- import { lookup } from "node:dns/promises";
6
- import { isIP } from "node:net";
7
6
  import { Integer, Sequence } from "asn1js";
8
7
  import { decodeBase64, encodeBase64 } from "byte-encodings/base64";
9
8
  import { decodeBase64Url } from "byte-encodings/base64url";
@@ -4284,6 +4283,28 @@ const preloadedContexts = {
4284
4283
  "@type": "@id"
4285
4284
  }
4286
4285
  } },
4286
+ "https://w3id.org/fep/7aa9": { "@context": {
4287
+ "FeaturedCollection": "https://w3id.org/fep/7aa9#FeaturedCollection",
4288
+ "FeaturedItem": "https://w3id.org/fep/7aa9#FeaturedItem",
4289
+ "FeatureRequest": "https://w3id.org/fep/7aa9#FeatureRequest",
4290
+ "FeatureAuthorization": "https://w3id.org/fep/7aa9#FeatureAuthorization",
4291
+ "topic": {
4292
+ "@id": "https://w3id.org/fep/7aa9#topic",
4293
+ "@type": "@id"
4294
+ },
4295
+ "featuredObject": {
4296
+ "@id": "https://w3id.org/fep/7aa9#featuredObject",
4297
+ "@type": "@id"
4298
+ },
4299
+ "canFeature": {
4300
+ "@id": "https://w3id.org/fep/7aa9#canFeature",
4301
+ "@type": "@id"
4302
+ },
4303
+ "featureAuthorization": {
4304
+ "@id": "https://w3id.org/fep/7aa9#featureAuthorization",
4305
+ "@type": "@id"
4306
+ }
4307
+ } },
4287
4308
  "https://join-lemmy.org/context.json": { "@context": ["https://w3id.org/security/v1", {
4288
4309
  "as": "https://www.w3.org/ns/activitystreams#",
4289
4310
  "lemmy": "https://join-lemmy.org/ns#",
@@ -4342,7 +4363,7 @@ const preloadedContexts = {
4342
4363
  //#endregion
4343
4364
  //#region deno.json
4344
4365
  var name = "@fedify/vocab-runtime";
4345
- var version = "2.4.0-dev.1570+f1b6aaa3";
4366
+ var version = "2.4.0-dev.1573+246bd0b2";
4346
4367
  //#endregion
4347
4368
  //#region src/link.ts
4348
4369
  const parametersNeedLowerCase = ["rel", "type"];
@@ -4595,179 +4616,6 @@ function logRequest(logger, request) {
4595
4616
  });
4596
4617
  }
4597
4618
  //#endregion
4598
- //#region src/url.ts
4599
- var UrlError = class extends Error {
4600
- constructor(message) {
4601
- super(message);
4602
- this.name = "UrlError";
4603
- }
4604
- };
4605
- /**
4606
- * Validates a URL to prevent SSRF attacks.
4607
- */
4608
- async function validatePublicUrl(url) {
4609
- const parsed = new URL(url);
4610
- if (parsed.protocol !== "http:" && parsed.protocol !== "https:") throw new UrlError(`Unsupported protocol: ${parsed.protocol}`);
4611
- let hostname = parsed.hostname;
4612
- if (hostname.startsWith("[") && hostname.endsWith("]")) hostname = hostname.slice(1, -1);
4613
- if (hostname === "localhost") throw new UrlError("Localhost is not allowed");
4614
- const hostnameFamily = isIP(hostname);
4615
- if (hostnameFamily !== 0) {
4616
- validatePublicIpAddress(hostname, hostnameFamily);
4617
- return;
4618
- }
4619
- if ("Deno" in globalThis && !isIP(hostname)) {
4620
- if ((await Deno.permissions.query({ name: "net" })).state !== "granted") return;
4621
- }
4622
- if ("Bun" in globalThis) {
4623
- if (hostname === "example.com" || hostname.endsWith(".example.com")) return;
4624
- else if (hostname === "fedify-test.internal") throw new UrlError("Invalid or private address: fedify-test.internal");
4625
- }
4626
- let addresses;
4627
- try {
4628
- addresses = await lookup(hostname, { all: true });
4629
- } catch {
4630
- addresses = [];
4631
- }
4632
- for (const { address, family } of addresses) validatePublicIpAddress(address, family);
4633
- }
4634
- function validatePublicIpAddress(address, family) {
4635
- if (family === 4 && isValidPublicIPv4Address(address) || family === 6 && isValidPublicIPv6Address(address)) return;
4636
- throw new UrlError(`Invalid or private address: ${address}`);
4637
- }
4638
- function isValidPublicIPv4Address(address) {
4639
- const parts = parseIPv4Address(address);
4640
- if (parts == null) return false;
4641
- const value = ipv4PartsToNumber(parts);
4642
- return !nonPublicIPv4Prefixes.some(({ base, prefix }) => matchesIPv4Prefix(value, base, prefix));
4643
- }
4644
- function isValidPublicIPv6Address(address) {
4645
- const words = parseIPv6Address(address);
4646
- if (words == null) return false;
4647
- if (nonPublicIPv6Prefixes.some(({ words: prefixWords, prefix }) => matchesIPv6Prefix(words, prefixWords, prefix))) return false;
4648
- for (const { extractIPv4, prefix, words: prefixWords } of ipv6WithIPv4Prefixes) {
4649
- if (!matchesIPv6Prefix(words, prefixWords, prefix)) continue;
4650
- const ipv4Address = extractIPv4(words);
4651
- if (ipv4Address != null && !isValidPublicIPv4Address(ipv4Address)) return false;
4652
- }
4653
- return true;
4654
- }
4655
- function expandIPv6Address(address) {
4656
- address = address.toLowerCase();
4657
- const ipv4Delimiter = address.lastIndexOf(":");
4658
- if (address.includes(".") && ipv4Delimiter >= 0) {
4659
- const ipv4Parts = parseIPv4Address(address.substring(ipv4Delimiter + 1));
4660
- if (ipv4Parts == null) return address;
4661
- const high = (ipv4Parts[0] << 8) + ipv4Parts[1];
4662
- const low = (ipv4Parts[2] << 8) + ipv4Parts[3];
4663
- address = address.substring(0, ipv4Delimiter + 1) + high.toString(16) + ":" + low.toString(16);
4664
- }
4665
- if (address === "::") return "0000:0000:0000:0000:0000:0000:0000:0000";
4666
- if (address.startsWith("::")) address = "0000" + address;
4667
- if (address.endsWith("::")) address = address + "0000";
4668
- address = address.replace("::", ":0000".repeat(8 - (address.match(/:/g) || []).length) + ":");
4669
- return address.split(":").map((part) => part.padStart(4, "0")).join(":");
4670
- }
4671
- const nonPublicIPv4Prefixes = [
4672
- ipv4Prefix("0.0.0.0/8", "RFC 6890"),
4673
- ipv4Prefix("10.0.0.0/8", "RFC 1918"),
4674
- ipv4Prefix("100.64.0.0/10", "RFC 6598"),
4675
- ipv4Prefix("127.0.0.0/8", "RFC 1122"),
4676
- ipv4Prefix("169.254.0.0/16", "RFC 3927"),
4677
- ipv4Prefix("172.16.0.0/12", "RFC 1918"),
4678
- ipv4Prefix("192.0.0.0/24", "RFC 6890"),
4679
- ipv4Prefix("192.0.2.0/24", "RFC 5737"),
4680
- ipv4Prefix("192.88.99.0/24", "RFC 7526"),
4681
- ipv4Prefix("192.168.0.0/16", "RFC 1918"),
4682
- ipv4Prefix("198.18.0.0/15", "RFC 2544"),
4683
- ipv4Prefix("198.51.100.0/24", "RFC 5737"),
4684
- ipv4Prefix("203.0.113.0/24", "RFC 5737"),
4685
- ipv4Prefix("224.0.0.0/4", "RFC 5771"),
4686
- ipv4Prefix("240.0.0.0/4", "RFC 1112")
4687
- ];
4688
- const nonPublicIPv6Prefixes = [
4689
- ipv6Prefix("::/16", "RFC 4291"),
4690
- ipv6Prefix("2001::/32", "RFC 4380"),
4691
- ipv6Prefix("2002::/16", "RFC 3056"),
4692
- ipv6Prefix("64:ff9b:1::/48", "RFC 8215"),
4693
- ipv6Prefix("fc00::/7", "RFC 4193"),
4694
- ipv6Prefix("fe80::/10", "RFC 4291"),
4695
- ipv6Prefix("ff00::/8", "RFC 4291")
4696
- ];
4697
- const ipv6WithIPv4Prefixes = [{
4698
- ...ipv6Prefix("64:ff9b::/96", "RFC 6052"),
4699
- extractIPv4: (words) => ipv4FromWords(words[6], words[7])
4700
- }];
4701
- function ipv4Prefix(cidr, rfc) {
4702
- const [address, prefixText] = cidr.split("/");
4703
- const prefix = parseInt(prefixText, 10);
4704
- const parts = parseIPv4Address(address);
4705
- if (parts == null || !Number.isInteger(prefix) || prefix < 0 || prefix > 32) throw new Error(`Invalid IPv4 prefix: ${cidr}`);
4706
- return {
4707
- cidr,
4708
- base: ipv4PartsToNumber(parts),
4709
- prefix,
4710
- rfc
4711
- };
4712
- }
4713
- function ipv6Prefix(cidr, rfc) {
4714
- const [address, prefixText] = cidr.split("/");
4715
- const prefix = parseInt(prefixText, 10);
4716
- const words = parseIPv6Address(address);
4717
- if (words == null || !Number.isInteger(prefix) || prefix < 0 || prefix > 128) throw new Error(`Invalid IPv6 prefix: ${cidr}`);
4718
- return {
4719
- cidr,
4720
- words,
4721
- prefix,
4722
- rfc
4723
- };
4724
- }
4725
- function parseIPv4Address(address) {
4726
- const parts = address.split(".").map((part) => {
4727
- if (!/^\d+$/.test(part)) return NaN;
4728
- return parseInt(part, 10);
4729
- });
4730
- if (parts.length !== 4 || parts.some((part) => !Number.isInteger(part) || part < 0 || part > 255)) return null;
4731
- return parts;
4732
- }
4733
- function parseIPv6Address(address) {
4734
- const parts = expandIPv6Address(address).split(":");
4735
- if (parts.length !== 8) return null;
4736
- const words = parts.map((part) => {
4737
- if (!/^[0-9a-f]{1,4}$/i.test(part)) return NaN;
4738
- return parseInt(part, 16);
4739
- });
4740
- if (words.some((word) => !Number.isInteger(word) || word < 0 || word > 65535)) return null;
4741
- return words;
4742
- }
4743
- function ipv4PartsToNumber(parts) {
4744
- return parts[0] * 2 ** 24 + parts[1] * 2 ** 16 + parts[2] * 2 ** 8 + parts[3];
4745
- }
4746
- function ipv4FromWords(highWord, lowWord) {
4747
- return [
4748
- highWord >> 8,
4749
- highWord & 255,
4750
- lowWord >> 8,
4751
- lowWord & 255
4752
- ].join(".");
4753
- }
4754
- function matchesIPv4Prefix(address, prefixBase, prefixLength) {
4755
- const blockSize = 2 ** (32 - prefixLength);
4756
- return Math.floor(address / blockSize) === Math.floor(prefixBase / blockSize);
4757
- }
4758
- function matchesIPv6Prefix(address, prefixWords, prefixLength) {
4759
- let remaining = prefixLength;
4760
- for (let i = 0; i < 8 && remaining > 0; i++) if (remaining >= 16) {
4761
- if (address[i] !== prefixWords[i]) return false;
4762
- remaining -= 16;
4763
- } else {
4764
- const mask = 65535 << 16 - remaining & 65535;
4765
- if ((address[i] & mask) !== (prefixWords[i] & mask)) return false;
4766
- remaining = 0;
4767
- }
4768
- return true;
4769
- }
4770
- //#endregion
4771
4619
  //#region src/docloader.ts
4772
4620
  const logger = getLogger([
4773
4621
  "fedify",
@@ -4775,6 +4623,66 @@ const logger = getLogger([
4775
4623
  "docloader"
4776
4624
  ]);
4777
4625
  const DEFAULT_MAX_REDIRECTION = 20;
4626
+ const MAX_HTML_SIZE = 1024 * 1024;
4627
+ function createResponseMetadata(response) {
4628
+ return new Response(null, {
4629
+ headers: response.headers,
4630
+ status: response.status,
4631
+ statusText: response.statusText
4632
+ });
4633
+ }
4634
+ async function cancelResponseBody(response) {
4635
+ if (response.body != null) await response.body.cancel();
4636
+ }
4637
+ async function readBoundedText(response, maxBytes) {
4638
+ const contentLength = response.headers.get("Content-Length");
4639
+ if (contentLength != null) {
4640
+ const size = Number(contentLength);
4641
+ if (size > maxBytes) {
4642
+ await cancelResponseBody(response);
4643
+ return {
4644
+ text: "",
4645
+ size,
4646
+ tooLarge: true
4647
+ };
4648
+ }
4649
+ }
4650
+ if (response.body == null) return {
4651
+ text: "",
4652
+ size: 0,
4653
+ tooLarge: false
4654
+ };
4655
+ const reader = response.body.getReader();
4656
+ const decoder = new TextDecoder();
4657
+ let text = "";
4658
+ let size = 0;
4659
+ try {
4660
+ while (true) {
4661
+ const result = await reader.read();
4662
+ if (result.done) break;
4663
+ const chunkSize = result.value.byteLength;
4664
+ if (size + chunkSize > maxBytes) {
4665
+ size += chunkSize;
4666
+ await reader.cancel();
4667
+ return {
4668
+ text: "",
4669
+ size,
4670
+ tooLarge: true
4671
+ };
4672
+ }
4673
+ size += chunkSize;
4674
+ text += decoder.decode(result.value, { stream: true });
4675
+ }
4676
+ text += decoder.decode();
4677
+ return {
4678
+ text,
4679
+ size,
4680
+ tooLarge: false
4681
+ };
4682
+ } finally {
4683
+ reader.releaseLock();
4684
+ }
4685
+ }
4778
4686
  /**
4779
4687
  * Gets a {@link RemoteDocument} from the given response.
4780
4688
  * @param url The URL of the document to load.
@@ -4829,19 +4737,19 @@ async function getRemoteDocument(url, response, fetch) {
4829
4737
  }
4830
4738
  let document;
4831
4739
  if (!jsonLd && (contentType === "text/html" || contentType?.startsWith("text/html;") || contentType === "application/xhtml+xml" || contentType?.startsWith("application/xhtml+xml;"))) {
4832
- const MAX_HTML_SIZE = 1024 * 1024;
4833
- const html = await response.text();
4834
- if (html.length > MAX_HTML_SIZE) {
4740
+ const errorResponse = createResponseMetadata(response);
4741
+ const html = await readBoundedText(response, MAX_HTML_SIZE);
4742
+ if (html.tooLarge) {
4835
4743
  logger.warn("HTML response too large, skipping alternate link discovery: {url}", {
4836
4744
  url: documentUrl,
4837
- size: html.length
4745
+ size: html.size
4838
4746
  });
4839
- document = JSON.parse(html);
4747
+ throw new FetchError(documentUrl, `HTML document is too large to scan for an ActivityPub alternate link (Content-Type: ${contentType})`, errorResponse);
4840
4748
  } else {
4841
4749
  const tagPattern = /<(a|link)\s+([^>]*?)\s*\/?>/gi;
4842
4750
  const attrPattern = /([a-z][a-z:_-]*)=(?:"([^"]*)"|'([^']*)'|([^\s>]+))/gi;
4843
4751
  let tagMatch;
4844
- while ((tagMatch = tagPattern.exec(html)) !== null) {
4752
+ while ((tagMatch = tagPattern.exec(html.text)) !== null) {
4845
4753
  const tagContent = tagMatch[2];
4846
4754
  let attrMatch;
4847
4755
  const attribs = {};
@@ -4858,7 +4766,12 @@ async function getRemoteDocument(url, response, fetch) {
4858
4766
  return await fetch(new URL(attribs.href, docUrl).href);
4859
4767
  }
4860
4768
  }
4861
- document = JSON.parse(html);
4769
+ try {
4770
+ document = JSON.parse(html.text);
4771
+ } catch (error) {
4772
+ if (!(error instanceof SyntaxError)) throw error;
4773
+ throw new FetchError(documentUrl, `HTML document has no ActivityPub alternate link (Content-Type: ${contentType})`, errorResponse);
4774
+ }
4862
4775
  }
4863
4776
  } else document = await response.json();
4864
4777
  logger.debug("Fetched document: {status} {url} {headers}", {
@@ -5336,6 +5249,11 @@ const algorithms = {
5336
5249
  },
5337
5250
  "1.3.101.112": "Ed25519"
5338
5251
  };
5252
+ const DID_KEY_PREFIX = "did:key:";
5253
+ const ED25519_PUBLIC_KEY_MULTICODEC = 237;
5254
+ const ED25519_PUBLIC_KEY_LENGTH = 32;
5255
+ const DID_KEY_PATTERN = /^did:key:([^/?#]+)$/;
5256
+ const DID_KEY_VERIFICATION_METHOD_PATTERN = /^did:key:([^/?#]+)#([^/?#]+)$/;
5339
5257
  /**
5340
5258
  * Imports a PEM-SPKI formatted public key.
5341
5259
  * @param pem The PEM-SPKI formatted public key.
@@ -5397,6 +5315,69 @@ const PKCS1_HEADER = /^\s*-----BEGIN\s+RSA\s+PUBLIC\s+KEY-----\s*\n/;
5397
5315
  function importPem(pem) {
5398
5316
  return PKCS1_HEADER.test(pem) ? importPkcs1(pem) : importSpki(pem);
5399
5317
  }
5318
+ function decodeEd25519DidKeyMultibase(multibaseKey) {
5319
+ if (!multibaseKey.startsWith("z")) throw new TypeError("did:key must use base58-btc Multibase encoding.");
5320
+ let decoded;
5321
+ try {
5322
+ decoded = decodeMultibase(multibaseKey);
5323
+ } catch (error) {
5324
+ throw new TypeError("Invalid did:key Multibase encoding.", { cause: error });
5325
+ }
5326
+ const { code } = getMulticodecPrefix(decoded);
5327
+ if (code !== ED25519_PUBLIC_KEY_MULTICODEC) throw new TypeError("Unsupported did:key type: 0x" + code.toString(16));
5328
+ const content = removeMulticodecPrefix(decoded);
5329
+ if (content.length !== ED25519_PUBLIC_KEY_LENGTH) throw new TypeError("Invalid Ed25519 did:key length.");
5330
+ return content;
5331
+ }
5332
+ /**
5333
+ * Imports an Ed25519 `did:key` DID.
5334
+ *
5335
+ * @param did The `did:key` DID.
5336
+ * @returns The imported Ed25519 public key.
5337
+ * @throws {TypeError} If the DID is malformed or uses an unsupported key type.
5338
+ * @since 2.4.0
5339
+ */
5340
+ async function importDidKey(did) {
5341
+ const match = (did instanceof URL ? did.href : did).match(DID_KEY_PATTERN);
5342
+ if (match == null) throw new TypeError("Invalid did:key DID.");
5343
+ const content = decodeEd25519DidKeyMultibase(match[1]);
5344
+ return await crypto.subtle.importKey("raw", content.slice(), "Ed25519", true, ["verify"]);
5345
+ }
5346
+ /**
5347
+ * Exports an Ed25519 public key as a `did:key` DID.
5348
+ *
5349
+ * @param key The Ed25519 public key.
5350
+ * @returns The `did:key` DID.
5351
+ * @throws {TypeError} If the key is invalid or unsupported.
5352
+ * @since 2.4.0
5353
+ */
5354
+ async function exportDidKey(key) {
5355
+ if (key.algorithm.name !== "Ed25519") throw new TypeError("Unsupported key type: " + JSON.stringify(key.algorithm));
5356
+ return DID_KEY_PREFIX + await exportMultibaseKey(key);
5357
+ }
5358
+ /**
5359
+ * Parses an Ed25519 `did:key` verification method DID URL.
5360
+ *
5361
+ * @param didUrl The `did:key` DID URL.
5362
+ * @returns The parsed verification method.
5363
+ * @throws {TypeError} If the DID URL is malformed, unsupported, or its
5364
+ * fragment does not identify the same key as the DID.
5365
+ * @since 2.4.0
5366
+ */
5367
+ async function parseDidKeyVerificationMethod(didUrl) {
5368
+ const didUrlString = didUrl instanceof URL ? didUrl.href : didUrl;
5369
+ const match = didUrlString.match(DID_KEY_VERIFICATION_METHOD_PATTERN);
5370
+ if (match == null) throw new TypeError("Invalid did:key verification method.");
5371
+ const [, publicKeyMultibase, fragment] = match;
5372
+ if (publicKeyMultibase !== fragment) throw new TypeError("Invalid did:key verification method fragment.");
5373
+ const publicKey = await importDidKey(DID_KEY_PREFIX + publicKeyMultibase);
5374
+ return {
5375
+ id: new URL(didUrlString),
5376
+ controller: new URL(DID_KEY_PREFIX + publicKeyMultibase),
5377
+ publicKeyMultibase,
5378
+ publicKey
5379
+ };
5380
+ }
5400
5381
  /**
5401
5382
  * Imports a [Multibase]-encoded public key.
5402
5383
  *
@@ -5563,4 +5544,4 @@ LanguageString.prototype[Symbol.for("nodejs.util.inspect.custom")] = function(_d
5563
5544
  return `<${this.locale.baseName}> ${inspect(this.toString(), options)}`;
5564
5545
  };
5565
5546
  //#endregion
5566
- export { FetchError, LanguageString, UrlError, canParseDecimal, createActivityPubRequest, decodeMultibase, encodeMultibase, encodingFromBaseData, expandIPv6Address, exportMultibaseKey, exportSpki, getDocumentLoader, getRemoteDocument, getUserAgent, importMultibaseKey, importPem, importPkcs1, importSpki, isDecimal, isValidPublicIPv4Address, isValidPublicIPv6Address, logRequest, parseDecimal, preloadedContexts, validatePublicUrl };
5547
+ export { FetchError, LanguageString, UrlError, canParseDecimal, createActivityPubRequest, decodeMultibase, encodeMultibase, encodingFromBaseData, expandIPv6Address, exportDidKey, exportMultibaseKey, exportSpki, formatIri, getDocumentLoader, getRemoteDocument, getUserAgent, haveSameIriOrigin, importDidKey, importMultibaseKey, importPem, importPkcs1, importSpki, isDecimal, isValidPublicIPv4Address, isValidPublicIPv6Address, logRequest, parseDecimal, parseDidKeyVerificationMethod, parseIri, parseJsonLdId, preloadedContexts, validatePublicUrl };
@@ -1,7 +1,7 @@
1
1
  require("./chunk-C2EiDwsr.cjs");
2
- require("./docloader-mvgIWKI7.cjs");
3
- require("./url-C20FhC7p.cjs");
4
- require("./key-pMmqUKuo.cjs");
2
+ require("./docloader-C69O-D5T.cjs");
3
+ require("./url-2XwVbUS_.cjs");
4
+ require("./key-_wXwomh_.cjs");
5
5
  require("./multibase-Bz_UUDtL.cjs");
6
6
  require("./langstr-CbAxaeEZ.cjs");
7
7
  let node_assert = require("node:assert");
@@ -1,6 +1,6 @@
1
- import "./docloader-C76ldE5C.mjs";
2
- import "./url-m9Qzxy-Y.mjs";
3
- import "./key-CrrK9mYh.mjs";
1
+ import "./docloader-B5Upa3Ox.mjs";
2
+ import "./url-YWJbnRlf.mjs";
3
+ import "./key-CDGDH_vC.mjs";
4
4
  import "./multibase-B4bvakyA.mjs";
5
5
  import "./langstr-Di5AvKpB.mjs";
6
6
  import { deepStrictEqual, throws } from "node:assert";
@@ -1,6 +1,6 @@
1
- import { a as name, i as logRequest, n as createActivityPubRequest, o as version, t as FetchError } from "./request-BEXkv1ul.mjs";
1
+ import { a as name, i as logRequest, n as createActivityPubRequest, o as version, t as FetchError } from "./request-BGoZTy5L.mjs";
2
2
  import { t as HttpHeaderLink } from "./link-NUUWCdnK.mjs";
3
- import { a as validatePublicUrl, t as UrlError } from "./url-m9Qzxy-Y.mjs";
3
+ import { l as validatePublicUrl, t as UrlError } from "./url-YWJbnRlf.mjs";
4
4
  import { getLogger } from "@logtape/logtape";
5
5
  import { SpanKind, SpanStatusCode, trace } from "@opentelemetry/api";
6
6
  //#endregion
@@ -4277,6 +4277,28 @@ const preloadedContexts = {
4277
4277
  "@type": "@id"
4278
4278
  }
4279
4279
  } },
4280
+ "https://w3id.org/fep/7aa9": { "@context": {
4281
+ "FeaturedCollection": "https://w3id.org/fep/7aa9#FeaturedCollection",
4282
+ "FeaturedItem": "https://w3id.org/fep/7aa9#FeaturedItem",
4283
+ "FeatureRequest": "https://w3id.org/fep/7aa9#FeatureRequest",
4284
+ "FeatureAuthorization": "https://w3id.org/fep/7aa9#FeatureAuthorization",
4285
+ "topic": {
4286
+ "@id": "https://w3id.org/fep/7aa9#topic",
4287
+ "@type": "@id"
4288
+ },
4289
+ "featuredObject": {
4290
+ "@id": "https://w3id.org/fep/7aa9#featuredObject",
4291
+ "@type": "@id"
4292
+ },
4293
+ "canFeature": {
4294
+ "@id": "https://w3id.org/fep/7aa9#canFeature",
4295
+ "@type": "@id"
4296
+ },
4297
+ "featureAuthorization": {
4298
+ "@id": "https://w3id.org/fep/7aa9#featureAuthorization",
4299
+ "@type": "@id"
4300
+ }
4301
+ } },
4280
4302
  "https://join-lemmy.org/context.json": { "@context": ["https://w3id.org/security/v1", {
4281
4303
  "as": "https://www.w3.org/ns/activitystreams#",
4282
4304
  "lemmy": "https://join-lemmy.org/ns#",
@@ -4340,6 +4362,66 @@ const logger = getLogger([
4340
4362
  "docloader"
4341
4363
  ]);
4342
4364
  const DEFAULT_MAX_REDIRECTION = 20;
4365
+ const MAX_HTML_SIZE = 1024 * 1024;
4366
+ function createResponseMetadata(response) {
4367
+ return new Response(null, {
4368
+ headers: response.headers,
4369
+ status: response.status,
4370
+ statusText: response.statusText
4371
+ });
4372
+ }
4373
+ async function cancelResponseBody(response) {
4374
+ if (response.body != null) await response.body.cancel();
4375
+ }
4376
+ async function readBoundedText(response, maxBytes) {
4377
+ const contentLength = response.headers.get("Content-Length");
4378
+ if (contentLength != null) {
4379
+ const size = Number(contentLength);
4380
+ if (size > maxBytes) {
4381
+ await cancelResponseBody(response);
4382
+ return {
4383
+ text: "",
4384
+ size,
4385
+ tooLarge: true
4386
+ };
4387
+ }
4388
+ }
4389
+ if (response.body == null) return {
4390
+ text: "",
4391
+ size: 0,
4392
+ tooLarge: false
4393
+ };
4394
+ const reader = response.body.getReader();
4395
+ const decoder = new TextDecoder();
4396
+ let text = "";
4397
+ let size = 0;
4398
+ try {
4399
+ while (true) {
4400
+ const result = await reader.read();
4401
+ if (result.done) break;
4402
+ const chunkSize = result.value.byteLength;
4403
+ if (size + chunkSize > maxBytes) {
4404
+ size += chunkSize;
4405
+ await reader.cancel();
4406
+ return {
4407
+ text: "",
4408
+ size,
4409
+ tooLarge: true
4410
+ };
4411
+ }
4412
+ size += chunkSize;
4413
+ text += decoder.decode(result.value, { stream: true });
4414
+ }
4415
+ text += decoder.decode();
4416
+ return {
4417
+ text,
4418
+ size,
4419
+ tooLarge: false
4420
+ };
4421
+ } finally {
4422
+ reader.releaseLock();
4423
+ }
4424
+ }
4343
4425
  /**
4344
4426
  * Gets a {@link RemoteDocument} from the given response.
4345
4427
  * @param url The URL of the document to load.
@@ -4394,19 +4476,19 @@ async function getRemoteDocument(url, response, fetch) {
4394
4476
  }
4395
4477
  let document;
4396
4478
  if (!jsonLd && (contentType === "text/html" || contentType?.startsWith("text/html;") || contentType === "application/xhtml+xml" || contentType?.startsWith("application/xhtml+xml;"))) {
4397
- const MAX_HTML_SIZE = 1024 * 1024;
4398
- const html = await response.text();
4399
- if (html.length > MAX_HTML_SIZE) {
4479
+ const errorResponse = createResponseMetadata(response);
4480
+ const html = await readBoundedText(response, MAX_HTML_SIZE);
4481
+ if (html.tooLarge) {
4400
4482
  logger.warn("HTML response too large, skipping alternate link discovery: {url}", {
4401
4483
  url: documentUrl,
4402
- size: html.length
4484
+ size: html.size
4403
4485
  });
4404
- document = JSON.parse(html);
4486
+ throw new FetchError(documentUrl, `HTML document is too large to scan for an ActivityPub alternate link (Content-Type: ${contentType})`, errorResponse);
4405
4487
  } else {
4406
4488
  const tagPattern = /<(a|link)\s+([^>]*?)\s*\/?>/gi;
4407
4489
  const attrPattern = /([a-z][a-z:_-]*)=(?:"([^"]*)"|'([^']*)'|([^\s>]+))/gi;
4408
4490
  let tagMatch;
4409
- while ((tagMatch = tagPattern.exec(html)) !== null) {
4491
+ while ((tagMatch = tagPattern.exec(html.text)) !== null) {
4410
4492
  const tagContent = tagMatch[2];
4411
4493
  let attrMatch;
4412
4494
  const attribs = {};
@@ -4423,7 +4505,12 @@ async function getRemoteDocument(url, response, fetch) {
4423
4505
  return await fetch(new URL(attribs.href, docUrl).href);
4424
4506
  }
4425
4507
  }
4426
- document = JSON.parse(html);
4508
+ try {
4509
+ document = JSON.parse(html.text);
4510
+ } catch (error) {
4511
+ if (!(error instanceof SyntaxError)) throw error;
4512
+ throw new FetchError(documentUrl, `HTML document has no ActivityPub alternate link (Content-Type: ${contentType})`, errorResponse);
4513
+ }
4427
4514
  }
4428
4515
  } else document = await response.json();
4429
4516
  logger.debug("Fetched document: {status} {url} {headers}", {
@@ -4528,4 +4615,4 @@ function getDocumentLoader({ allowPrivateAddress, maxRedirection, skipPreloadedC
4528
4615
  return load;
4529
4616
  }
4530
4617
  //#endregion
4531
- export { preloadedContexts as n, getDocumentLoader as t };
4618
+ export { getRemoteDocument as n, preloadedContexts as r, getDocumentLoader as t };