@fedify/fedify 2.2.18 → 2.2.20

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 (86) hide show
  1. package/dist/{builder-DAbQyxmb.mjs → builder-BolQ56ab.mjs} +2 -2
  2. package/dist/{client-CAM_bQXx.d.cts → client-BIBzqEIp.d.cts} +0 -1
  3. package/dist/{client-9F_u23dc.mjs → client-DhxaVgOv.mjs} +113 -64
  4. package/dist/{client-CSddvgWN.d.ts → client-jE7oGvRh.d.ts} +0 -1
  5. package/dist/compat/mod.d.cts +1 -1
  6. package/dist/compat/mod.d.ts +1 -1
  7. package/dist/compat/transformers.test.mjs +1 -1
  8. package/dist/{context-tJbL_Hkw.d.cts → context-CFQGaq5x.d.cts} +28 -2
  9. package/dist/{context-OkwEpc3-.d.ts → context-DdbLVV_k.d.ts} +28 -2
  10. package/dist/{deno-JUTr6LYC.mjs → deno-BUj4roJc.mjs} +1 -1
  11. package/dist/docloader-CbtF_0cD.mjs +125 -0
  12. package/dist/federation/builder.test.mjs +1 -1
  13. package/dist/federation/handler.test.mjs +5 -5
  14. package/dist/federation/idempotency.test.mjs +2 -2
  15. package/dist/federation/inbox.test.mjs +1 -1
  16. package/dist/federation/keycache.test.mjs +18 -7
  17. package/dist/federation/middleware.test.mjs +389 -28
  18. package/dist/federation/mod.cjs +1 -1
  19. package/dist/federation/mod.d.cts +2 -2
  20. package/dist/federation/mod.d.ts +2 -2
  21. package/dist/federation/mod.js +1 -1
  22. package/dist/federation/send.test.mjs +445 -70
  23. package/dist/federation/temporal.test.mjs +1 -1
  24. package/dist/federation/webfinger.test.mjs +1 -1
  25. package/dist/{http-Dl00HXr4.d.ts → http-9IFUxdkp.d.cts} +1 -2
  26. package/dist/{http-OSYYhKM8.js → http-C5g_VrA_.js} +188 -105
  27. package/dist/{http-lCmKCZBI.mjs → http-CQMVisM0.mjs} +195 -97
  28. package/dist/{http-DEH_eppX.cjs → http-D8I7LBfx.cjs} +189 -106
  29. package/dist/{http-DqZhyEso.d.cts → http-SbfezERn.d.ts} +1 -2
  30. package/dist/{inbox-DMATfdEI.mjs → inbox-CD8IDILN.mjs} +1 -1
  31. package/dist/{key-D0ahOVjf.mjs → key-C3o3DGap.mjs} +17 -3
  32. package/dist/{keycache-Dkc_sflV.mjs → keycache-BimEQucx.mjs} +1 -1
  33. package/dist/{kv-cache-B7X4p1y7.js → kv-cache-CSKXXleR.js} +60 -16
  34. package/dist/{kv-cache-CPEV_4nZ.cjs → kv-cache-DC9LcfoC.cjs} +59 -15
  35. package/dist/{ld-CJOXIehz.mjs → ld-ijSr-0AB.mjs} +2 -2
  36. package/dist/{middleware-CHoUjmJm.mjs → middleware-CYAaUTi5.mjs} +79 -39
  37. package/dist/{middleware-QcuVRA-q.cjs → middleware-DNueR0GD.cjs} +185 -60
  38. package/dist/{middleware-D3hx1aiI.cjs → middleware-DmcY2TWS.cjs} +1 -1
  39. package/dist/{middleware-aNUajYPY.js → middleware-sjhz30sm.js} +185 -60
  40. package/dist/{middleware-BgGJEcdp.mjs → middleware-yqqfI7oJ.mjs} +1 -1
  41. package/dist/{mod-B6LmugJ4.d.ts → mod-BRnwh2oy.d.ts} +2 -8
  42. package/dist/{mod-D2JyWtW3.d.cts → mod-BYWp77-z.d.cts} +1 -1
  43. package/dist/{mod-DQGjRJUG.d.ts → mod-BuRRdsfD.d.ts} +1 -1
  44. package/dist/{mod-ej_rLx8h.d.ts → mod-CM5_J_cx.d.ts} +1 -1
  45. package/dist/{mod-a6kmB8xD.d.cts → mod-CVSKw-5H.d.cts} +1 -1
  46. package/dist/{mod-V2vDnYNu.d.cts → mod-Co7LEK9X.d.cts} +2 -8
  47. package/dist/mod.cjs +5 -5
  48. package/dist/mod.d.cts +6 -6
  49. package/dist/mod.d.ts +6 -6
  50. package/dist/mod.js +5 -5
  51. package/dist/nodeinfo/client.test.mjs +337 -19
  52. package/dist/nodeinfo/handler.test.mjs +1 -1
  53. package/dist/nodeinfo/mod.cjs +1 -1
  54. package/dist/nodeinfo/mod.d.cts +1 -1
  55. package/dist/nodeinfo/mod.d.ts +1 -1
  56. package/dist/nodeinfo/mod.js +1 -1
  57. package/dist/otel/exporter.test.mjs +292 -30
  58. package/dist/otel/mod.cjs +36 -30
  59. package/dist/otel/mod.d.cts +13 -0
  60. package/dist/otel/mod.d.ts +13 -0
  61. package/dist/otel/mod.js +36 -30
  62. package/dist/{owner-BUTMEkbn.mjs → owner-B8kXzD0q.mjs} +2 -2
  63. package/dist/{proof-DS1Zl6BX.mjs → proof-CBQOjOgA.mjs} +2 -2
  64. package/dist/{proof-Bm3xyn3D.js → proof-DKX6nASL.js} +1 -1
  65. package/dist/{proof-BeY7TsiS.cjs → proof-qiuGJCD7.cjs} +1 -1
  66. package/dist/{send-fnPrh9Zm.mjs → send-DRpisX_f.mjs} +117 -32
  67. package/dist/sig/http.test.mjs +529 -3
  68. package/dist/sig/key.test.mjs +197 -3
  69. package/dist/sig/ld.test.mjs +47 -2
  70. package/dist/sig/mod.cjs +2 -2
  71. package/dist/sig/mod.d.cts +2 -2
  72. package/dist/sig/mod.d.ts +2 -2
  73. package/dist/sig/mod.js +2 -2
  74. package/dist/sig/owner.test.mjs +46 -1
  75. package/dist/sig/proof.test.mjs +52 -1
  76. package/dist/{temporal-Biz6zK6e.mjs → temporal-BQGEINad.mjs} +1 -1
  77. package/dist/testing/mod.d.mts +26 -1
  78. package/dist/{types-miAYfiGO.js → types-B1_pRFE_.js} +112 -63
  79. package/dist/{types-DRPjmHVV.cjs → types-DOvyymM4.cjs} +111 -62
  80. package/dist/utils/docloader.test.mjs +382 -34
  81. package/dist/utils/mod.cjs +1 -1
  82. package/dist/utils/mod.d.cts +1 -1
  83. package/dist/utils/mod.d.ts +1 -1
  84. package/dist/utils/mod.js +1 -1
  85. package/package.json +6 -6
  86. package/dist/docloader-BGjq1F5U.mjs +0 -81
@@ -11,7 +11,7 @@ let _opentelemetry_semantic_conventions = require("@opentelemetry/semantic-conve
11
11
  let byte_encodings_base64 = require("byte-encodings/base64");
12
12
  //#region deno.json
13
13
  var name = "@fedify/fedify";
14
- var version = "2.2.18";
14
+ var version = "2.2.20";
15
15
  //#endregion
16
16
  //#region src/sig/accept.ts
17
17
  /**
@@ -292,16 +292,23 @@ async function importJwk(jwk, type) {
292
292
  /**
293
293
  * Fetches the document that the given actor URI dereferences to, and returns
294
294
  * the actor only if the host that served the document is authoritative for
295
- * the id the document claims.
295
+ * the id the document claims, and that id is the actor that was asked for.
296
296
  *
297
297
  * Only the origin that serves an actor id can speak for it. Without that
298
298
  * rule any host could serve a document describing somebody else's actor—and
299
299
  * listing its own keys as that actor's.
300
300
  *
301
+ * The returned actor always has exactly the requested `actorId` as its `id`.
302
+ * A document loader follows redirects and alternate document links, so the
303
+ * document an actor id leads to may describe a different actor altogether,
304
+ * on the same origin or on another; accepting it would attribute that other
305
+ * actor's keys to the requested one. See GHSA-8863-8468-7hq7.
306
+ *
301
307
  * @param actorId The URI of the actor to fetch.
302
308
  * @param options Options for fetching the document.
303
309
  * @returns The actor, or `null` if the document cannot be fetched, is not an
304
- * actor, or belongs to another origin.
310
+ * actor, belongs to another origin, or describes an actor other
311
+ * than the requested one.
305
312
  * @internal
306
313
  */
307
314
  async function fetchActorDocument(actorId, options = {}) {
@@ -356,6 +363,13 @@ async function fetchActorDocumentInternal(actorId, options, propagateContextFetc
356
363
  });
357
364
  return null;
358
365
  }
366
+ if (object.id.href !== actorId.href) {
367
+ logger.debug("The document fetched for the actor {actorId} is the actor {documentActorId} instead; refusing to treat it as the requested actor's document.", {
368
+ actorId: actorId.href,
369
+ documentActorId: object.id.href
370
+ });
371
+ return null;
372
+ }
359
373
  return object;
360
374
  }
361
375
  /**
@@ -1584,10 +1598,10 @@ async function createRedirectRequest(request, location, body, validateRedirect)
1584
1598
  cache: request.cache
1585
1599
  });
1586
1600
  }
1587
- async function fetchDoubleKnockRequest(request, signedRequest, signal) {
1601
+ async function fetchDoubleKnockRequest(session, request, signedRequest, signal) {
1588
1602
  const maxAttempts = request.method === "GET" || request.method === "HEAD" ? 2 : 1;
1589
1603
  for (let attempt = 1;; attempt++) try {
1590
- return await fetch(signedRequest, {
1604
+ return await session.fetch(signedRequest, {
1591
1605
  redirect: "manual",
1592
1606
  signal
1593
1607
  });
@@ -1638,118 +1652,187 @@ function getAbortReason(signal) {
1638
1652
  return signal.reason ?? new DOMException("The operation was aborted.", "AbortError");
1639
1653
  }
1640
1654
  /**
1641
- * Performs a double-knock request to the given URL. For the details of
1642
- * double-knocking, see
1643
- * <https://swicg.github.io/activitypub-http-signature/#how-to-upgrade-supported-versions>.
1655
+ * Performs a double-knock request like {@link doubleKnock}, opening a session
1656
+ * for every hop with the given function, and passes the final response to
1657
+ * the given function. The final session is closed after `consume` settles,
1658
+ * so `consume` must read or cancel the response body before it returns.
1659
+ * This is not exported from any entry point.
1644
1660
  * @param request The request to send.
1645
1661
  * @param identity The identity to use for signing the request.
1646
1662
  * @param options The options for double-knock requests.
1647
- * @returns The response to the request.
1648
- * @since 1.6.0
1663
+ * @param openHop The function to open a session for each hop. The session of
1664
+ * a hop is closed before the next hop is opened.
1665
+ * @param consume The function to consume the final response.
1666
+ * @returns The value returned by `consume`.
1649
1667
  */
1650
- async function doubleKnock(request, identity, options = {}) {
1651
- return await doubleKnockInternal(request, identity, options);
1668
+ async function doubleKnockScoped(request, identity, options, openHop, consume) {
1669
+ const { response, session } = await doubleKnockCore(request, identity, options, openHop);
1670
+ try {
1671
+ return await consume(response);
1672
+ } catch (error) {
1673
+ (0, _logtape_logtape.getLogger)([
1674
+ "fedify",
1675
+ "sig",
1676
+ "http"
1677
+ ]).debug("Failed to consume the response from {url}; closing the session: {error}", {
1678
+ url: request.url,
1679
+ error
1680
+ });
1681
+ throw error;
1682
+ } finally {
1683
+ await session.close();
1684
+ }
1652
1685
  }
1653
- async function doubleKnockInternal(request, identity, options, redirected = 0, visited = /* @__PURE__ */ new Set()) {
1654
- const { specDeterminer, log, tracerProvider, signal } = options;
1655
- const maximumRedirection = options.maxRedirection ?? DEFAULT_MAX_REDIRECTION;
1656
- visited.add(request.url);
1657
- const origin = new URL(request.url).origin;
1658
- const firstTrySpec = specDeterminer == null ? "rfc9421" : await specDeterminer.determineSpec(origin);
1659
- const body = options.body !== void 0 ? options.body : request.method !== "GET" && request.method !== "HEAD" ? await request.clone().arrayBuffer() : null;
1660
- let signedRequest = await signRequest(request, identity.privateKey, identity.keyId, {
1661
- spec: firstTrySpec,
1662
- tracerProvider,
1663
- body
1664
- });
1665
- log?.(signedRequest);
1666
- let response = await fetchDoubleKnockRequest(request, signedRequest, signal);
1667
- if (response.status >= 300 && response.status < 400 && response.headers.has("Location")) {
1668
- if (redirected >= maximumRedirection) throw new _fedify_vocab_runtime.FetchError(request.url, `Too many redirections (${redirected + 1})`);
1669
- const redirectRequest = await createRedirectRequest(request, response.headers.get("Location"), body, options.validateRedirect);
1670
- if (visited.has(redirectRequest.url)) throw new _fedify_vocab_runtime.FetchError(request.url, `Redirect loop detected: ${redirectRequest.url}`);
1671
- return doubleKnockInternal(redirectRequest, identity, {
1672
- ...options,
1673
- body
1674
- }, redirected + 1, visited);
1675
- } else if (response.status === 400 || response.status === 401 || response.status > 401) {
1676
- const logger = (0, _logtape_logtape.getLogger)([
1686
+ function isRedirectResponse(response) {
1687
+ return response.status >= 300 && response.status < 400 && response.headers.has("Location");
1688
+ }
1689
+ /**
1690
+ * Starts cancelling the body of a response that is not passed to the caller,
1691
+ * without waiting for it to settle. The cancellation of a branch of a tee
1692
+ * (e.g., a body cloned by a fetch mock) settles only after the other branch
1693
+ * is cancelled too, per the Streams standard, so awaiting it could block
1694
+ * forever. Failures are only logged.
1695
+ */
1696
+ function cancelResponseBody(response, url) {
1697
+ response.body?.cancel().catch((error) => {
1698
+ (0, _logtape_logtape.getLogger)([
1677
1699
  "fedify",
1678
1700
  "sig",
1679
1701
  "http"
1680
- ]);
1681
- const acceptSigHeader = response.headers.get("Accept-Signature");
1682
- if (acceptSigHeader != null) {
1683
- const entries = validateAcceptSignature(parseAcceptSignature(acceptSigHeader));
1684
- const localKeyId = identity.keyId.href;
1685
- const localAlg = "rsa-v1_5-sha256";
1686
- let fulfilled = false;
1687
- let challengeRequest;
1688
- for (const entry of entries) {
1689
- const rfc9421 = fulfillAcceptSignature(entry, localKeyId, localAlg);
1690
- if (rfc9421 == null) continue;
1691
- logger.debug("Received Accept-Signature challenge; accumulating label {label} and components {components}.", {
1692
- label: rfc9421.label,
1693
- components: rfc9421.components
1694
- });
1695
- try {
1696
- challengeRequest = await signRequest(challengeRequest ?? request, identity.privateKey, identity.keyId, {
1697
- spec: "rfc9421",
1698
- tracerProvider,
1699
- body,
1700
- rfc9421
1702
+ ]).debug("Failed to cancel the response body from {url}: {error}", {
1703
+ url,
1704
+ error
1705
+ });
1706
+ });
1707
+ }
1708
+ async function doubleKnockCore(request, identity, options, openHop) {
1709
+ const { specDeterminer, log, tracerProvider, signal } = options;
1710
+ const maximumRedirection = options.maxRedirection ?? DEFAULT_MAX_REDIRECTION;
1711
+ const visited = /* @__PURE__ */ new Set();
1712
+ const body = options.body !== void 0 ? options.body : request.method !== "GET" && request.method !== "HEAD" ? await request.clone().arrayBuffer() : null;
1713
+ let session = await openHop(request.url);
1714
+ try {
1715
+ for (let redirected = 0;; redirected++) {
1716
+ visited.add(request.url);
1717
+ const origin = new URL(request.url).origin;
1718
+ const firstTrySpec = specDeterminer == null ? "rfc9421" : await specDeterminer.determineSpec(origin);
1719
+ let signedRequest = await signRequest(request, identity.privateKey, identity.keyId, {
1720
+ spec: firstTrySpec,
1721
+ tracerProvider,
1722
+ body
1723
+ });
1724
+ log?.(signedRequest);
1725
+ let response = await fetchDoubleKnockRequest(session, request, signedRequest, signal);
1726
+ const firstRedirected = isRedirectResponse(response);
1727
+ if (!firstRedirected && (response.status === 400 || response.status === 401 || response.status > 401)) {
1728
+ const logger = (0, _logtape_logtape.getLogger)([
1729
+ "fedify",
1730
+ "sig",
1731
+ "http"
1732
+ ]);
1733
+ const acceptSigHeader = response.headers.get("Accept-Signature");
1734
+ if (acceptSigHeader != null) {
1735
+ const entries = validateAcceptSignature(parseAcceptSignature(acceptSigHeader));
1736
+ const localKeyId = identity.keyId.href;
1737
+ const localAlg = "rsa-v1_5-sha256";
1738
+ let fulfilled = false;
1739
+ let challengeRequest;
1740
+ for (const entry of entries) {
1741
+ const rfc9421 = fulfillAcceptSignature(entry, localKeyId, localAlg);
1742
+ if (rfc9421 == null) continue;
1743
+ logger.debug("Received Accept-Signature challenge; accumulating label {label} and components {components}.", {
1744
+ label: rfc9421.label,
1745
+ components: rfc9421.components
1746
+ });
1747
+ try {
1748
+ challengeRequest = await signRequest(challengeRequest ?? request, identity.privateKey, identity.keyId, {
1749
+ spec: "rfc9421",
1750
+ tracerProvider,
1751
+ body,
1752
+ rfc9421
1753
+ });
1754
+ fulfilled = true;
1755
+ } catch (error) {
1756
+ logger.debug("Failed to fulfill Accept-Signature challenge entry {label}: {error}", {
1757
+ label: entry.label,
1758
+ error
1759
+ });
1760
+ }
1761
+ }
1762
+ if (fulfilled && challengeRequest != null) {
1763
+ cancelResponseBody(response, request.url);
1764
+ signedRequest = challengeRequest;
1765
+ log?.(signedRequest);
1766
+ response = await session.fetch(signedRequest, {
1767
+ redirect: "manual",
1768
+ signal
1769
+ });
1770
+ }
1771
+ if (fulfilled && !isRedirectResponse(response)) {
1772
+ if (response.status < 300) {
1773
+ await specDeterminer?.rememberSpec(origin, "rfc9421");
1774
+ return {
1775
+ response,
1776
+ session
1777
+ };
1778
+ }
1779
+ if (response.status !== 400 && response.status !== 401) return {
1780
+ response,
1781
+ session
1782
+ };
1783
+ }
1784
+ }
1785
+ if (!isRedirectResponse(response)) {
1786
+ const spec = firstTrySpec === "draft-cavage-http-signatures-12" ? "rfc9421" : "draft-cavage-http-signatures-12";
1787
+ (0, _logtape_logtape.getLogger)([
1788
+ "fedify",
1789
+ "sig",
1790
+ "http"
1791
+ ]).debug("Failed to verify with the spec {spec} ({status} {statusText}); retrying with spec {secondSpec}... (double-knocking)", {
1792
+ spec: firstTrySpec,
1793
+ secondSpec: spec,
1794
+ status: response.status,
1795
+ statusText: response.statusText
1701
1796
  });
1702
- fulfilled = true;
1703
- } catch (error) {
1704
- logger.debug("Failed to fulfill Accept-Signature challenge entry {label}: {error}", {
1705
- label: entry.label,
1706
- error
1797
+ cancelResponseBody(response, request.url);
1798
+ signedRequest = await signRequest(request, identity.privateKey, identity.keyId, {
1799
+ spec,
1800
+ tracerProvider,
1801
+ body
1707
1802
  });
1803
+ log?.(signedRequest);
1804
+ response = await fetchDoubleKnockRequest(session, request, signedRequest, signal);
1805
+ if (!isRedirectResponse(response) && response.status !== 400 && response.status !== 401) await specDeterminer?.rememberSpec(origin, spec);
1708
1806
  }
1709
- }
1710
- if (fulfilled && challengeRequest != null) {
1711
- signedRequest = challengeRequest;
1712
- log?.(signedRequest);
1713
- response = await fetch(signedRequest, {
1714
- redirect: "manual",
1715
- signal
1716
- });
1717
- if (response.status >= 300 && response.status < 400 && response.headers.has("Location")) return doubleKnock(await createRedirectRequest(request, response.headers.get("Location"), body, options.validateRedirect), identity, {
1718
- ...options,
1719
- body
1720
- });
1721
- }
1722
- if (fulfilled && response.status < 300) {
1723
- await specDeterminer?.rememberSpec(origin, "rfc9421");
1724
- return response;
1725
- }
1726
- if (fulfilled && response.status !== 400 && response.status !== 401) return response;
1727
- }
1728
- const spec = firstTrySpec === "draft-cavage-http-signatures-12" ? "rfc9421" : "draft-cavage-http-signatures-12";
1729
- logger.debug("Failed to verify with the spec {spec} ({status} {statusText}); retrying with spec {secondSpec}... (double-knocking)", {
1730
- spec: firstTrySpec,
1731
- secondSpec: spec,
1732
- status: response.status,
1733
- statusText: response.statusText
1734
- });
1735
- signedRequest = await signRequest(request, identity.privateKey, identity.keyId, {
1736
- spec,
1737
- tracerProvider,
1738
- body
1739
- });
1740
- log?.(signedRequest);
1741
- response = await fetchDoubleKnockRequest(request, signedRequest, signal);
1742
- if (response.status >= 300 && response.status < 400 && response.headers.has("Location")) {
1807
+ } else if (!firstRedirected) await specDeterminer?.rememberSpec(origin, firstTrySpec);
1808
+ if (!isRedirectResponse(response)) return {
1809
+ response,
1810
+ session
1811
+ };
1812
+ cancelResponseBody(response, request.url);
1813
+ await session.close();
1743
1814
  if (redirected >= maximumRedirection) throw new _fedify_vocab_runtime.FetchError(request.url, `Too many redirections (${redirected + 1})`);
1744
- const redirectRequest = await createRedirectRequest(request, response.headers.get("Location"), body, options.validateRedirect);
1815
+ const location = response.headers.get("Location");
1816
+ const redirectRequest = await createRedirectRequest(request, location, body, options.validateRedirect);
1745
1817
  if (visited.has(redirectRequest.url)) throw new _fedify_vocab_runtime.FetchError(request.url, `Redirect loop detected: ${redirectRequest.url}`);
1746
- return doubleKnockInternal(redirectRequest, identity, {
1747
- ...options,
1748
- body
1749
- }, redirected + 1, visited);
1750
- } else if (response.status !== 400 && response.status !== 401) await specDeterminer?.rememberSpec(origin, spec);
1751
- } else await specDeterminer?.rememberSpec(origin, firstTrySpec);
1752
- return response;
1818
+ request = redirectRequest;
1819
+ session = await openHop(request.url);
1820
+ }
1821
+ } catch (error) {
1822
+ try {
1823
+ await session.close();
1824
+ } catch (closeError) {
1825
+ (0, _logtape_logtape.getLogger)([
1826
+ "fedify",
1827
+ "sig",
1828
+ "http"
1829
+ ]).debug("Failed to close the session for {url}: {error}", {
1830
+ url: request.url,
1831
+ error: closeError
1832
+ });
1833
+ }
1834
+ throw error;
1835
+ }
1753
1836
  }
1754
1837
  /**
1755
1838
  * Performs a timing-safe equality comparison between two `Uint8Array` values.
@@ -1779,10 +1862,10 @@ function timingSafeEqual(a, b) {
1779
1862
  return result === 0;
1780
1863
  }
1781
1864
  //#endregion
1782
- Object.defineProperty(exports, "doubleKnock", {
1865
+ Object.defineProperty(exports, "doubleKnockScoped", {
1783
1866
  enumerable: true,
1784
1867
  get: function() {
1785
- return doubleKnock;
1868
+ return doubleKnockScoped;
1786
1869
  }
1787
1870
  });
1788
1871
  Object.defineProperty(exports, "exportJwk", {
@@ -1,8 +1,7 @@
1
1
  /// <reference lib="esnext.temporal" />
2
2
  import { Actor, CryptographicKey, Multikey } from "@fedify/vocab";
3
- import { DocumentLoader } from "@fedify/vocab-runtime";
4
3
  import { TracerProvider } from "@opentelemetry/api";
5
-
4
+ import { DocumentLoader } from "@fedify/vocab-runtime";
6
5
  //#region src/sig/key.d.ts
7
6
  /**
8
7
  * Generates a key pair which is appropriate for Fedify.
@@ -1,7 +1,7 @@
1
1
  import { Temporal } from "@js-temporal/polyfill";
2
2
  import "urlpattern-polyfill";
3
3
  globalThis.addEventListener = () => {};
4
- import { n as version, t as name } from "./deno-JUTr6LYC.mjs";
4
+ import { n as version, t as name } from "./deno-BUj4roJc.mjs";
5
5
  import { getTypeId } from "@fedify/vocab";
6
6
  import { SpanKind, SpanStatusCode, context, propagation, trace } from "@opentelemetry/api";
7
7
  import { getLogger } from "@logtape/logtape";
@@ -1,7 +1,7 @@
1
1
  import "@js-temporal/polyfill";
2
2
  import "urlpattern-polyfill";
3
3
  globalThis.addEventListener = () => {};
4
- import { n as version, t as name } from "./deno-JUTr6LYC.mjs";
4
+ import { n as version, t as name } from "./deno-BUj4roJc.mjs";
5
5
  import { CryptographicKey, Object as Object$1, isActor } from "@fedify/vocab";
6
6
  import { SpanKind, SpanStatusCode, trace } from "@opentelemetry/api";
7
7
  import { FetchError, UrlError, getDocumentLoader } from "@fedify/vocab-runtime";
@@ -146,16 +146,23 @@ async function importJwk(jwk, type) {
146
146
  /**
147
147
  * Fetches the document that the given actor URI dereferences to, and returns
148
148
  * the actor only if the host that served the document is authoritative for
149
- * the id the document claims.
149
+ * the id the document claims, and that id is the actor that was asked for.
150
150
  *
151
151
  * Only the origin that serves an actor id can speak for it. Without that
152
152
  * rule any host could serve a document describing somebody else's actor—and
153
153
  * listing its own keys as that actor's.
154
154
  *
155
+ * The returned actor always has exactly the requested `actorId` as its `id`.
156
+ * A document loader follows redirects and alternate document links, so the
157
+ * document an actor id leads to may describe a different actor altogether,
158
+ * on the same origin or on another; accepting it would attribute that other
159
+ * actor's keys to the requested one. See GHSA-8863-8468-7hq7.
160
+ *
155
161
  * @param actorId The URI of the actor to fetch.
156
162
  * @param options Options for fetching the document.
157
163
  * @returns The actor, or `null` if the document cannot be fetched, is not an
158
- * actor, or belongs to another origin.
164
+ * actor, belongs to another origin, or describes an actor other
165
+ * than the requested one.
159
166
  * @internal
160
167
  */
161
168
  async function fetchActorDocument(actorId, options = {}) {
@@ -210,6 +217,13 @@ async function fetchActorDocumentInternal(actorId, options, propagateContextFetc
210
217
  });
211
218
  return null;
212
219
  }
220
+ if (object.id.href !== actorId.href) {
221
+ logger.debug("The document fetched for the actor {actorId} is the actor {documentActorId} instead; refusing to treat it as the requested actor's document.", {
222
+ actorId: actorId.href,
223
+ documentActorId: object.id.href
224
+ });
225
+ return null;
226
+ }
213
227
  return object;
214
228
  }
215
229
  /**
@@ -4,7 +4,7 @@ globalThis.addEventListener = () => {};
4
4
  import { CryptographicKey, Multikey } from "@fedify/vocab";
5
5
  import { FetchError, UrlError } from "@fedify/vocab-runtime";
6
6
  //#region src/federation/keycache.ts
7
- const KEY_CACHE_GENERATION = "2";
7
+ const KEY_CACHE_GENERATION = "3";
8
8
  var KvKeyCache = class {
9
9
  kv;
10
10
  prefix;
@@ -1,9 +1,10 @@
1
1
  import { Temporal } from "@js-temporal/polyfill";
2
2
  import { URLPattern } from "urlpattern-polyfill";
3
- import { f as validateCryptoKey, t as doubleKnock } from "./http-OSYYhKM8.js";
3
+ import { f as validateCryptoKey, t as doubleKnockScoped } from "./http-C5g_VrA_.js";
4
4
  import { getLogger } from "@logtape/logtape";
5
5
  import { curry } from "es-toolkit";
6
- import { FetchError, UrlError, createActivityPubRequest, getRemoteDocument, logRequest, preloadedContexts, validatePublicUrl } from "@fedify/vocab-runtime";
6
+ import { FetchError, UrlError, createActivityPubRequest, getRemoteDocument, logRequest, preloadedContexts } from "@fedify/vocab-runtime";
7
+ import { openDirectFetch, openPublicFetch } from "@fedify/vocab-runtime/internal";
7
8
  //#region src/utils/docloader.ts
8
9
  const logger$1 = getLogger([
9
10
  "fedify",
@@ -24,12 +25,34 @@ const DEFAULT_MAX_REDIRECTION = 20;
24
25
  * @throws {TypeError} If the key is invalid or unsupported.
25
26
  * @since 0.4.0
26
27
  */
27
- function getAuthenticatedDocumentLoader(identity, { allowPrivateAddress, maxRedirection, userAgent, specDeterminer, tracerProvider } = {}) {
28
+ function getAuthenticatedDocumentLoader(identity, options = {}) {
29
+ return createAuthenticatedDocumentLoader(identity, options);
30
+ }
31
+ /**
32
+ * The placeholder that the alternate document callback of
33
+ * `getRemoteDocument()` returns, so that the alternate document is loaded
34
+ * after the current hop is closed.
35
+ */
36
+ const ALTERNATE_PLACEHOLDER = Object.freeze({
37
+ contextUrl: null,
38
+ document: null,
39
+ documentUrl: "about:blank"
40
+ });
41
+ /**
42
+ * The implementation of {@link getAuthenticatedDocumentLoader}. This is not
43
+ * exported from any entry point.
44
+ * @param identity The identity to get the document loader for.
45
+ * @param options The options for the document loader.
46
+ * @param deps The dependencies, for tests.
47
+ * @returns The authenticated document loader.
48
+ * @throws {TypeError} If the key is invalid or unsupported.
49
+ */
50
+ function createAuthenticatedDocumentLoader(identity, { allowPrivateAddress, maxRedirection, userAgent, specDeterminer, tracerProvider } = {}, deps = {}) {
28
51
  validateCryptoKey(identity.privateKey);
29
52
  async function load(url, options, redirected = 0, visited = /* @__PURE__ */ new Set()) {
30
53
  options?.signal?.throwIfAborted();
31
54
  let currentUrl = new URL(url).href;
32
- await validateUrl(currentUrl);
55
+ validateUrl(currentUrl);
33
56
  visited.add(currentUrl);
34
57
  const originalRequest = createActivityPubRequest(currentUrl, { userAgent });
35
58
  function follow(nextUrl) {
@@ -38,29 +61,44 @@ function getAuthenticatedDocumentLoader(identity, { allowPrivateAddress, maxRedi
38
61
  if (visited.has(nextUrl)) throw new FetchError(currentUrl, `Redirect loop detected: ${nextUrl}`);
39
62
  redirected++;
40
63
  }
41
- const response = await doubleKnock(originalRequest, identity, {
64
+ const result = await doubleKnockScoped(originalRequest, identity, {
42
65
  maxRedirection,
43
66
  specDeterminer,
44
67
  log: curry(logRequest)(logger$1),
45
68
  tracerProvider,
46
69
  signal: options?.signal,
47
- validateRedirect: async (nextUrl) => {
70
+ validateRedirect: (nextUrl) => {
48
71
  follow(nextUrl);
49
- await validateUrl(nextUrl);
72
+ validateUrl(nextUrl);
50
73
  visited.add(nextUrl);
51
74
  currentUrl = nextUrl;
52
75
  }
76
+ }, (hopUrl) => openHop(hopUrl, options?.signal), async (response) => {
77
+ let alternateUrl;
78
+ const document = await getRemoteDocument(currentUrl, response, (url) => {
79
+ alternateUrl = url;
80
+ return Promise.resolve(ALTERNATE_PLACEHOLDER);
81
+ });
82
+ if (alternateUrl == null) return { document };
83
+ if (!response.bodyUsed && response.body?.locked !== true) response.body?.cancel().catch((error) => {
84
+ logger$1.debug("Failed to cancel the response body from {url}: {error}", {
85
+ url: currentUrl,
86
+ error
87
+ });
88
+ });
89
+ return { alternateUrl };
53
90
  });
54
- return getRemoteDocument(currentUrl, response, (alternateUrl) => {
55
- follow(alternateUrl);
56
- return load(alternateUrl, options, redirected, visited);
57
- });
91
+ if ("document" in result) return result.document;
92
+ follow(result.alternateUrl);
93
+ return load(result.alternateUrl, options, redirected, visited);
58
94
  }
59
- async function validateUrl(url) {
60
- const protocol = new URL(url).protocol;
61
- if (protocol !== "http:" && protocol !== "https:") throw new UrlError(`Unsupported protocol: ${protocol}`);
62
- if (!allowPrivateAddress) try {
63
- await validatePublicUrl(url);
95
+ function openUnvalidatedHop(url, signal) {
96
+ if (deps.openHop != null) return deps.openHop(url);
97
+ return allowPrivateAddress ? openDirectFetch(url) : openPublicFetch(url, { signal });
98
+ }
99
+ async function openHop(url, signal) {
100
+ try {
101
+ return await openUnvalidatedHop(url, signal);
64
102
  } catch (error) {
65
103
  if (error instanceof UrlError) if (error.reason === "dns") logger$1.debug("DNS lookup failed for {url}", {
66
104
  url,
@@ -73,6 +111,12 @@ function getAuthenticatedDocumentLoader(identity, { allowPrivateAddress, maxRedi
73
111
  throw error;
74
112
  }
75
113
  }
114
+ function validateUrl(url) {
115
+ const parsedUrl = new URL(url);
116
+ const protocol = parsedUrl.protocol;
117
+ if (protocol !== "http:" && protocol !== "https:") throw new UrlError(`Unsupported protocol: ${protocol}`);
118
+ if (parsedUrl.username !== "" || parsedUrl.password !== "") throw new UrlError("URLs with credentials are not allowed");
119
+ }
76
120
  return (url, options) => load(url, options);
77
121
  }
78
122
  //#endregion