@fedify/fedify 2.0.35-dev.2480 → 2.0.35-dev.2485

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 (79) hide show
  1. package/dist/{builder-cEjyBwpD.mjs → builder-CKbQrOH1.mjs} +3 -3
  2. package/dist/{client-CAM_bQXx.d.cts → client-BIBzqEIp.d.cts} +0 -1
  3. package/dist/{client-BUqbea1d.mjs → client-DGNacEx_.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-r8sjhltN.d.cts → context-CnoWxuoB.d.cts} +2 -2
  9. package/dist/{context-B2iX3heK.d.ts → context-lxjaUTAI.d.ts} +2 -2
  10. package/dist/{deno-BcnGeGr2.mjs → deno-DL_ifPJk.mjs} +1 -1
  11. package/dist/docloader-B0_0Fjqo.mjs +124 -0
  12. package/dist/federation/builder.test.mjs +1 -1
  13. package/dist/federation/handler.test.mjs +6 -6
  14. package/dist/federation/idempotency.test.mjs +2 -2
  15. package/dist/federation/inbox.test.mjs +1 -1
  16. package/dist/federation/middleware.test.mjs +42 -23
  17. package/dist/federation/mod.cjs +1 -1
  18. package/dist/federation/mod.d.cts +2 -2
  19. package/dist/federation/mod.d.ts +2 -2
  20. package/dist/federation/mod.js +1 -1
  21. package/dist/federation/send.test.mjs +445 -70
  22. package/dist/federation/temporal.test.mjs +1 -1
  23. package/dist/federation/webfinger.test.mjs +1 -1
  24. package/dist/{http-Bs6thuCC.d.cts → http-Cd8UD1ym.d.ts} +1 -2
  25. package/dist/{http-BD9izRp9.cjs → http-CjkqnlR2.cjs} +114 -56
  26. package/dist/{http-CDupyfOc.d.ts → http-DFqAb11b.d.cts} +1 -2
  27. package/dist/{http-IYRDhhnT.js → http-DVEBwq6e.js} +113 -55
  28. package/dist/{http-D0cjeL8a.mjs → http-Dvsq4uCu.mjs} +136 -49
  29. package/dist/{inbox-D2fsxjqC.mjs → inbox-D6YbZFrZ.mjs} +1 -1
  30. package/dist/{key-BFzQ3Hef.mjs → key-CgcWGw6W.mjs} +1 -1
  31. package/dist/{kv-cache-C-RyNMt6.js → kv-cache-DC_F2WSH.js} +60 -18
  32. package/dist/{kv-cache-B4sDuLjb.cjs → kv-cache-nN9RaiYp.cjs} +59 -17
  33. package/dist/{ld-D2bkYBIz.mjs → ld-CfOIwHup.mjs} +2 -2
  34. package/dist/{middleware-DRZp7Odv.js → middleware-B0eqDUmD.js} +118 -33
  35. package/dist/{middleware-CXIBpN4y.mjs → middleware-CA7egMDR.mjs} +12 -12
  36. package/dist/{middleware-B2UC8mtN.cjs → middleware-Cu_yKroA.cjs} +1 -1
  37. package/dist/{middleware-3mc-kq3v.mjs → middleware-DEwj7jGj.mjs} +1 -1
  38. package/dist/{middleware-DxX8ZL-q.cjs → middleware-DZvWX5af.cjs} +118 -33
  39. package/dist/{mod-BkPB4hPe.d.cts → mod-BPDfu4Ey.d.cts} +2 -7
  40. package/dist/{mod-E7YM72JZ.d.ts → mod-BS-jg3Wx.d.ts} +1 -1
  41. package/dist/{mod-BKJyD7nk.d.ts → mod-DGP89q0u.d.ts} +2 -7
  42. package/dist/{mod-IOWUF5Yr.d.cts → mod-DVPS4yam.d.cts} +1 -1
  43. package/dist/{mod-CM_eR8hQ.d.ts → mod-I-34OMN3.d.ts} +1 -1
  44. package/dist/{mod-DDTLbfce.d.cts → mod-w1oyHseQ.d.cts} +1 -1
  45. package/dist/mod.cjs +5 -5
  46. package/dist/mod.d.cts +6 -6
  47. package/dist/mod.d.ts +6 -6
  48. package/dist/mod.js +5 -5
  49. package/dist/nodeinfo/client.test.mjs +337 -19
  50. package/dist/nodeinfo/handler.test.mjs +1 -1
  51. package/dist/nodeinfo/mod.cjs +1 -1
  52. package/dist/nodeinfo/mod.d.cts +1 -1
  53. package/dist/nodeinfo/mod.d.ts +1 -1
  54. package/dist/nodeinfo/mod.js +1 -1
  55. package/dist/{owner-BuJXJDhl.mjs → owner-BvVwCoj2.mjs} +2 -2
  56. package/dist/{proof-DfmHAmtr.cjs → proof-BssSZpMi.cjs} +1 -1
  57. package/dist/{proof-JxVhLZA3.mjs → proof-C9TBSmUR.mjs} +2 -2
  58. package/dist/{proof-BJ_6nKhb.js → proof-Cp62GFJ3.js} +1 -1
  59. package/dist/{send-CDaWmeqV.mjs → send-ZWmjk2Zf.mjs} +117 -32
  60. package/dist/sig/http.test.mjs +411 -2
  61. package/dist/sig/key.test.mjs +2 -2
  62. package/dist/sig/ld.test.mjs +2 -2
  63. package/dist/sig/mod.cjs +2 -2
  64. package/dist/sig/mod.d.cts +2 -2
  65. package/dist/sig/mod.d.ts +2 -2
  66. package/dist/sig/mod.js +2 -2
  67. package/dist/sig/owner.test.mjs +1 -1
  68. package/dist/sig/proof.test.mjs +1 -1
  69. package/dist/{temporal-Bv6Zn1C5.mjs → temporal-BNtFi_ut.mjs} +1 -1
  70. package/dist/testing/mod.d.mts +0 -1
  71. package/dist/{types-miAYfiGO.js → types-B1_pRFE_.js} +112 -63
  72. package/dist/{types-DRPjmHVV.cjs → types-DOvyymM4.cjs} +111 -62
  73. package/dist/utils/docloader.test.mjs +316 -32
  74. package/dist/utils/mod.cjs +1 -1
  75. package/dist/utils/mod.d.cts +1 -1
  76. package/dist/utils/mod.d.ts +1 -1
  77. package/dist/utils/mod.js +1 -1
  78. package/package.json +5 -5
  79. package/dist/docloader-BqDq3fDF.mjs +0 -82
@@ -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-BcnGeGr2.mjs";
4
+ import { n as version, t as name } from "./deno-DL_ifPJk.mjs";
5
5
  import { Activity, 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-BcnGeGr2.mjs";
4
+ import { n as version, t as name } from "./deno-DL_ifPJk.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";
@@ -1,9 +1,10 @@
1
1
  import { Temporal } from "@js-temporal/polyfill";
2
2
  import { URLPattern } from "urlpattern-polyfill";
3
- import { l as validateCryptoKey, t as doubleKnock } from "./http-IYRDhhnT.js";
3
+ import { l as validateCryptoKey, t as doubleKnockScoped } from "./http-DVEBwq6e.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, 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, 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,30 +61,43 @@ function getAuthenticatedDocumentLoader(identity, { allowPrivateAddress, userAge
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
  specDeterminer,
43
66
  log: curry(logRequest)(logger$1),
44
67
  tracerProvider,
45
68
  signal: options?.signal,
46
- validateRedirect: async (nextUrl) => {
69
+ validateRedirect: (nextUrl) => {
47
70
  follow(nextUrl);
48
- await validateUrl(nextUrl);
71
+ validateUrl(nextUrl);
49
72
  visited.add(nextUrl);
50
73
  currentUrl = nextUrl;
51
74
  }
75
+ }, (hopUrl) => openHop(hopUrl, options?.signal), async (response) => {
76
+ let alternateUrl;
77
+ const document = await getRemoteDocument(currentUrl, response, (url) => {
78
+ alternateUrl = url;
79
+ return Promise.resolve(ALTERNATE_PLACEHOLDER);
80
+ });
81
+ if (alternateUrl == null) return { document };
82
+ if (!response.bodyUsed && response.body?.locked !== true) response.body?.cancel().catch((error) => {
83
+ logger$1.debug("Failed to cancel the response body from {url}: {error}", {
84
+ url: currentUrl,
85
+ error
86
+ });
87
+ });
88
+ return { alternateUrl };
52
89
  });
53
- return getRemoteDocument(currentUrl, response, (alternateUrl) => {
54
- follow(alternateUrl);
55
- return load(alternateUrl, options, redirected, visited);
56
- });
90
+ if ("document" in result) return result.document;
91
+ follow(result.alternateUrl);
92
+ return load(result.alternateUrl, options, redirected, visited);
57
93
  }
58
- async function validateUrl(url) {
59
- const parsedUrl = new URL(url);
60
- const protocol = parsedUrl.protocol;
61
- if (protocol !== "http:" && protocol !== "https:") throw new UrlError(`Unsupported protocol: ${protocol}`);
62
- if (parsedUrl.username !== "" || parsedUrl.password !== "") throw new UrlError("URLs with credentials are not allowed");
63
- if (!allowPrivateAddress) try {
64
- await validatePublicUrl(url);
94
+ function openUnvalidatedHop(url, signal) {
95
+ if (deps.openHop != null) return deps.openHop(url);
96
+ return allowPrivateAddress ? openDirectFetch(url) : openPublicFetch(url, { signal });
97
+ }
98
+ async function openHop(url, signal) {
99
+ try {
100
+ return await openUnvalidatedHop(url, signal);
65
101
  } catch (error) {
66
102
  if (error instanceof UrlError) if (error.reason === "dns") logger$1.debug("DNS lookup failed for {url}", {
67
103
  url,
@@ -74,6 +110,12 @@ function getAuthenticatedDocumentLoader(identity, { allowPrivateAddress, userAge
74
110
  throw error;
75
111
  }
76
112
  }
113
+ function validateUrl(url) {
114
+ const parsedUrl = new URL(url);
115
+ const protocol = parsedUrl.protocol;
116
+ if (protocol !== "http:" && protocol !== "https:") throw new UrlError(`Unsupported protocol: ${protocol}`);
117
+ if (parsedUrl.username !== "" || parsedUrl.password !== "") throw new UrlError("URLs with credentials are not allowed");
118
+ }
77
119
  return (url, options) => load(url, options);
78
120
  }
79
121
  //#endregion
@@ -1,10 +1,11 @@
1
1
  const { Temporal } = require("@js-temporal/polyfill");
2
2
  const { URLPattern } = require("urlpattern-polyfill");
3
3
  require("./chunk-DDcVe30Y.cjs");
4
- const require_http = require("./http-BD9izRp9.cjs");
4
+ const require_http = require("./http-CjkqnlR2.cjs");
5
5
  let _logtape_logtape = require("@logtape/logtape");
6
6
  let es_toolkit = require("es-toolkit");
7
7
  let _fedify_vocab_runtime = require("@fedify/vocab-runtime");
8
+ let _fedify_vocab_runtime_internal = require("@fedify/vocab-runtime/internal");
8
9
  //#region src/utils/docloader.ts
9
10
  const logger$1 = (0, _logtape_logtape.getLogger)([
10
11
  "fedify",
@@ -25,12 +26,34 @@ const DEFAULT_MAX_REDIRECTION = 20;
25
26
  * @throws {TypeError} If the key is invalid or unsupported.
26
27
  * @since 0.4.0
27
28
  */
28
- function getAuthenticatedDocumentLoader(identity, { allowPrivateAddress, userAgent, specDeterminer, tracerProvider } = {}) {
29
+ function getAuthenticatedDocumentLoader(identity, options = {}) {
30
+ return createAuthenticatedDocumentLoader(identity, options);
31
+ }
32
+ /**
33
+ * The placeholder that the alternate document callback of
34
+ * `getRemoteDocument()` returns, so that the alternate document is loaded
35
+ * after the current hop is closed.
36
+ */
37
+ const ALTERNATE_PLACEHOLDER = Object.freeze({
38
+ contextUrl: null,
39
+ document: null,
40
+ documentUrl: "about:blank"
41
+ });
42
+ /**
43
+ * The implementation of {@link getAuthenticatedDocumentLoader}. This is not
44
+ * exported from any entry point.
45
+ * @param identity The identity to get the document loader for.
46
+ * @param options The options for the document loader.
47
+ * @param deps The dependencies, for tests.
48
+ * @returns The authenticated document loader.
49
+ * @throws {TypeError} If the key is invalid or unsupported.
50
+ */
51
+ function createAuthenticatedDocumentLoader(identity, { allowPrivateAddress, userAgent, specDeterminer, tracerProvider } = {}, deps = {}) {
29
52
  require_http.validateCryptoKey(identity.privateKey);
30
53
  async function load(url, options, redirected = 0, visited = /* @__PURE__ */ new Set()) {
31
54
  options?.signal?.throwIfAborted();
32
55
  let currentUrl = new URL(url).href;
33
- await validateUrl(currentUrl);
56
+ validateUrl(currentUrl);
34
57
  visited.add(currentUrl);
35
58
  const originalRequest = (0, _fedify_vocab_runtime.createActivityPubRequest)(currentUrl, { userAgent });
36
59
  function follow(nextUrl) {
@@ -39,30 +62,43 @@ function getAuthenticatedDocumentLoader(identity, { allowPrivateAddress, userAge
39
62
  if (visited.has(nextUrl)) throw new _fedify_vocab_runtime.FetchError(currentUrl, `Redirect loop detected: ${nextUrl}`);
40
63
  redirected++;
41
64
  }
42
- const response = await require_http.doubleKnock(originalRequest, identity, {
65
+ const result = await require_http.doubleKnockScoped(originalRequest, identity, {
43
66
  specDeterminer,
44
67
  log: (0, es_toolkit.curry)(_fedify_vocab_runtime.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 (0, _fedify_vocab_runtime.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 (0, _fedify_vocab_runtime.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 parsedUrl = new URL(url);
61
- const protocol = parsedUrl.protocol;
62
- if (protocol !== "http:" && protocol !== "https:") throw new _fedify_vocab_runtime.UrlError(`Unsupported protocol: ${protocol}`);
63
- if (parsedUrl.username !== "" || parsedUrl.password !== "") throw new _fedify_vocab_runtime.UrlError("URLs with credentials are not allowed");
64
- if (!allowPrivateAddress) try {
65
- await (0, _fedify_vocab_runtime.validatePublicUrl)(url);
95
+ function openUnvalidatedHop(url, signal) {
96
+ if (deps.openHop != null) return deps.openHop(url);
97
+ return allowPrivateAddress ? (0, _fedify_vocab_runtime_internal.openDirectFetch)(url) : (0, _fedify_vocab_runtime_internal.openPublicFetch)(url, { signal });
98
+ }
99
+ async function openHop(url, signal) {
100
+ try {
101
+ return await openUnvalidatedHop(url, signal);
66
102
  } catch (error) {
67
103
  if (error instanceof _fedify_vocab_runtime.UrlError) if (error.reason === "dns") logger$1.debug("DNS lookup failed for {url}", {
68
104
  url,
@@ -75,6 +111,12 @@ function getAuthenticatedDocumentLoader(identity, { allowPrivateAddress, userAge
75
111
  throw error;
76
112
  }
77
113
  }
114
+ function validateUrl(url) {
115
+ const parsedUrl = new URL(url);
116
+ const protocol = parsedUrl.protocol;
117
+ if (protocol !== "http:" && protocol !== "https:") throw new _fedify_vocab_runtime.UrlError(`Unsupported protocol: ${protocol}`);
118
+ if (parsedUrl.username !== "" || parsedUrl.password !== "") throw new _fedify_vocab_runtime.UrlError("URLs with credentials are not allowed");
119
+ }
78
120
  return (url, options) => load(url, options);
79
121
  }
80
122
  //#endregion
@@ -1,8 +1,8 @@
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-BcnGeGr2.mjs";
5
- import { o as validateCryptoKey, r as fetchKey } from "./key-BFzQ3Hef.mjs";
4
+ import { n as version, t as name } from "./deno-DL_ifPJk.mjs";
5
+ import { o as validateCryptoKey, r as fetchKey } from "./key-CgcWGw6W.mjs";
6
6
  import { Activity, CryptographicKey, Object as Object$1, getTypeId } from "@fedify/vocab";
7
7
  import { SpanStatusCode, trace } from "@opentelemetry/api";
8
8
  import { getDocumentLoader } from "@fedify/vocab-runtime";
@@ -2,10 +2,10 @@ import { Temporal } from "@js-temporal/polyfill";
2
2
  import { URLPattern } from "urlpattern-polyfill";
3
3
  import { t as __exportAll } from "./chunk-CRNNMoPX.js";
4
4
  import { r as getDefaultActivityTransformers } from "./transformers-BGMIq1cs.js";
5
- import { c as importJwk, d as name, f as version, i as exportJwk, l as validateCryptoKey, r as verifyRequest, t as doubleKnock } from "./http-IYRDhhnT.js";
6
- import { a as MAX_BODY_SIZE, i as BodyTooLargeError, n as getNodeInfo, o as readBoundedText, t as nodeInfoToJson } from "./types-miAYfiGO.js";
7
- import { _ as signJsonLd, a as doesActorOwnKey, c as assertSafeJsonLd, f as detachSignature, g as isInvalidUrlTypeError, h as isClearlyMalformedContextReference, m as hasSignature, n as signObject, o as getKeyOwner, p as getNormalizationContextLoader, r as verifyObject, s as InvalidContextReferenceError, u as compactJsonLd, v as verifyCompactJsonLd, x as wrapContextLoaderForJsonLd } from "./proof-BJ_6nKhb.js";
8
- import { n as getAuthenticatedDocumentLoader, t as kvCache } from "./kv-cache-C-RyNMt6.js";
5
+ import { c as importJwk, d as name, f as version, i as exportJwk, l as validateCryptoKey, r as verifyRequest, t as doubleKnockScoped } from "./http-DVEBwq6e.js";
6
+ import { a as MAX_BODY_SIZE, i as BodyTooLargeError, n as getNodeInfo, o as readBoundedText, t as nodeInfoToJson } from "./types-B1_pRFE_.js";
7
+ import { _ as signJsonLd, a as doesActorOwnKey, c as assertSafeJsonLd, f as detachSignature, g as isInvalidUrlTypeError, h as isClearlyMalformedContextReference, m as hasSignature, n as signObject, o as getKeyOwner, p as getNormalizationContextLoader, r as verifyObject, s as InvalidContextReferenceError, u as compactJsonLd, v as verifyCompactJsonLd, x as wrapContextLoaderForJsonLd } from "./proof-Cp62GFJ3.js";
8
+ import { n as getAuthenticatedDocumentLoader, t as kvCache } from "./kv-cache-DC_F2WSH.js";
9
9
  import { getLogger, withContext } from "@logtape/logtape";
10
10
  import { Activity, Collection, CollectionPage, CryptographicKey, Link, Multikey, Object as Object$1, OrderedCollection, OrderedCollectionPage, getTypeId, lookupObject, traverseCollection } from "@fedify/vocab";
11
11
  import { SpanKind, SpanStatusCode, context, propagation, trace } from "@opentelemetry/api";
@@ -13,10 +13,11 @@ import { cloneDeep } from "es-toolkit";
13
13
  import { Router } from "uri-template-router";
14
14
  import { parseTemplate } from "url-template";
15
15
  import { encodeHex } from "byte-encodings/hex";
16
- import { FetchError, UrlError, getDocumentLoader, validatePublicUrl } from "@fedify/vocab-runtime";
16
+ import { FetchError, UrlError, getDocumentLoader } from "@fedify/vocab-runtime";
17
17
  import { ATTR_HTTP_REQUEST_HEADER, ATTR_HTTP_REQUEST_METHOD, ATTR_HTTP_RESPONSE_HEADER, ATTR_HTTP_RESPONSE_STATUS_CODE, ATTR_URL_FULL } from "@opentelemetry/semantic-conventions";
18
18
  import jsonld from "@fedify/vocab-runtime/jsonld";
19
19
  import { lookupWebFinger } from "@fedify/webfinger";
20
+ import { openDirectFetch, openPublicFetch } from "@fedify/vocab-runtime/internal";
20
21
  import { domainToASCII } from "node:url";
21
22
  //#region src/federation/inbox.ts
22
23
  var InboxListenerSet = class InboxListenerSet {
@@ -2262,9 +2263,10 @@ function extractInboxes({ recipients, preferSharedInbox, excludeBaseUris }) {
2262
2263
  *
2263
2264
  * @param parameters The parameters for sending the activity.
2264
2265
  * See also {@link SendActivityParameters}.
2266
+ * @param deps The dependencies, for tests.
2265
2267
  * @throws {Error} If the activity fails to send.
2266
2268
  */
2267
- function sendActivity(options) {
2269
+ function sendActivity(options, deps = {}) {
2268
2270
  const tracerProvider = options.tracerProvider ?? trace.getTracerProvider();
2269
2271
  return tracerProvider.getTracer(name, version).startActiveSpan("activitypub.send_activity", {
2270
2272
  kind: SpanKind.CLIENT,
@@ -2276,7 +2278,7 @@ function sendActivity(options) {
2276
2278
  await sendActivityInternal({
2277
2279
  ...options,
2278
2280
  tracerProvider
2279
- }, span);
2281
+ }, span, deps);
2280
2282
  } catch (e) {
2281
2283
  span.setStatus({
2282
2284
  code: SpanStatusCode.ERROR,
@@ -2310,21 +2312,25 @@ async function readLimitedResponseBody(response, maxBytes) {
2310
2312
  totalBytes += value.length;
2311
2313
  }
2312
2314
  } finally {
2315
+ if (truncated) reader.cancel().catch(() => {});
2313
2316
  reader.releaseLock();
2314
2317
  }
2315
2318
  let result = chunks.join("");
2316
2319
  if (truncated) result += "… (truncated)";
2317
2320
  return result;
2318
2321
  }
2319
- async function sendActivityInternal({ activity, activityId, keys, inbox, allowPrivateAddress, headers, specDeterminer, tracerProvider }, span) {
2322
+ async function sendActivityInternal({ activity, activityId, keys, inbox, allowPrivateAddress, headers, specDeterminer, tracerProvider }, span, deps) {
2320
2323
  const logger = getLogger([
2321
2324
  "fedify",
2322
2325
  "federation",
2323
2326
  "outbox"
2324
2327
  ]);
2325
- async function validateUrl(url) {
2326
- if (!allowPrivateAddress) try {
2327
- await validatePublicUrl(url);
2328
+ const openUnvalidatedHop = deps.openHop ?? (allowPrivateAddress ? openDirectFetch : (url) => openPublicFetch(url));
2329
+ let opened = false;
2330
+ async function openHop(url) {
2331
+ let session;
2332
+ try {
2333
+ session = await openUnvalidatedHop(url);
2328
2334
  } catch (error) {
2329
2335
  if (error instanceof UrlError && error.reason === "dns") {
2330
2336
  logger.error("DNS resolution failed for URL: {url}", {
@@ -2341,8 +2347,9 @@ async function sendActivityInternal({ activity, activityId, keys, inbox, allowPr
2341
2347
  });
2342
2348
  throw error;
2343
2349
  }
2350
+ opened = true;
2351
+ return session;
2344
2352
  }
2345
- await validateUrl(inbox.href);
2346
2353
  headers = new Headers(headers);
2347
2354
  headers.set("Content-Type", "application/activity+json");
2348
2355
  const request = new Request(inbox, {
@@ -2363,36 +2370,30 @@ async function sendActivityInternal({ activity, activityId, keys, inbox, allowPr
2363
2370
  privateKey: pair.privateKey
2364
2371
  }))
2365
2372
  });
2366
- let response;
2373
+ let outcome;
2367
2374
  try {
2368
- response = rsaKey == null ? await fetchWithValidatedRedirects(request, validateUrl) : await doubleKnock(request, rsaKey, {
2375
+ outcome = rsaKey == null ? await fetchWithValidatedRedirects(request, openHop, (response) => consumeSendActivityResponse(response, inbox)) : await doubleKnockScoped(request, rsaKey, {
2369
2376
  tracerProvider,
2370
- specDeterminer,
2371
- validateRedirect: validateUrl
2372
- });
2377
+ specDeterminer
2378
+ }, openHop, (response) => consumeSendActivityResponse(response, inbox));
2373
2379
  } catch (error) {
2374
- logger.error("Failed to send activity {activityId} to {inbox}:\n{error}", {
2380
+ if (opened) logger.error("Failed to send activity {activityId} to {inbox}:\n{error}", {
2375
2381
  activityId,
2376
2382
  inbox: inbox.href,
2377
2383
  error
2378
2384
  });
2379
2385
  throw error;
2380
2386
  }
2381
- if (!response.ok) {
2382
- let error;
2383
- try {
2384
- error = await readLimitedResponseBody(response, MAX_ERROR_RESPONSE_BODY_BYTES);
2385
- } catch (_) {
2386
- error = "";
2387
- }
2387
+ if (!outcome.ok) {
2388
+ const { status, statusText, error } = outcome;
2388
2389
  logger.error("Failed to send activity {activityId} to {inbox} ({status} {statusText}):\n{error}", {
2389
2390
  activityId,
2390
2391
  inbox: inbox.href,
2391
- status: response.status,
2392
- statusText: response.statusText,
2392
+ status,
2393
+ statusText,
2393
2394
  error
2394
2395
  });
2395
- throw new SendActivityError(inbox, response.status, `Failed to send activity ${activityId} to ${inbox.href} (${response.status} ${response.statusText}):\n${error}`, error);
2396
+ throw new SendActivityError(inbox, status, `Failed to send activity ${activityId} to ${inbox.href} (${status} ${statusText}):\n${error}`, error);
2396
2397
  }
2397
2398
  span.addEvent("activitypub.activity.sent", {
2398
2399
  "activitypub.activity.json": JSON.stringify(activity),
@@ -2400,10 +2401,74 @@ async function sendActivityInternal({ activity, activityId, keys, inbox, allowPr
2400
2401
  "activitypub.activity.id": activityId ?? ""
2401
2402
  });
2402
2403
  }
2403
- async function fetchWithValidatedRedirects(request, validateUrl) {
2404
+ /**
2405
+ * Starts cancelling the body of a response without waiting for it to settle.
2406
+ * The cancellation of a branch of a tee (e.g., a body cloned by a fetch mock)
2407
+ * settles only after the other branch is cancelled too, per the Streams
2408
+ * standard, so awaiting it could block forever. Failures are only logged.
2409
+ */
2410
+ function cancelResponseBody(response, url) {
2411
+ response.body?.cancel().catch((error) => {
2412
+ getLogger([
2413
+ "fedify",
2414
+ "federation",
2415
+ "outbox"
2416
+ ]).debug("Failed to cancel the response body from {url}: {error}", {
2417
+ url,
2418
+ error
2419
+ });
2420
+ });
2421
+ }
2422
+ /**
2423
+ * Reads what is needed from the final response of a delivery while its
2424
+ * session is open: the body of a successful response is cancelled, and at
2425
+ * most 1 KiB of an error response body is read.
2426
+ */
2427
+ async function consumeSendActivityResponse(response, inbox) {
2428
+ if (response.ok) {
2429
+ cancelResponseBody(response, response.url || inbox.href);
2430
+ return { ok: true };
2431
+ }
2432
+ let error;
2433
+ try {
2434
+ error = await readLimitedResponseBody(response, MAX_ERROR_RESPONSE_BODY_BYTES);
2435
+ } catch (_) {
2436
+ error = "";
2437
+ }
2438
+ return {
2439
+ ok: false,
2440
+ status: response.status,
2441
+ statusText: response.statusText,
2442
+ error
2443
+ };
2444
+ }
2445
+ /**
2446
+ * Sends a request without signing it, following redirects manually like the
2447
+ * Fetch standard does. Every hop is opened with `openHop()`, which validates
2448
+ * the destination, after the previous hop is closed. The final response is
2449
+ * passed to `consume()`, and its session is closed after that.
2450
+ * This is exported only for tests; it is not exported from any entry point.
2451
+ * @param request The request to send.
2452
+ * @param openHop The function to open a session for each hop.
2453
+ * @param consume The function to consume the final response.
2454
+ * @returns The value returned by `consume`.
2455
+ */
2456
+ async function fetchWithValidatedRedirects(request, openHop, consume) {
2457
+ const logger = getLogger([
2458
+ "fedify",
2459
+ "federation",
2460
+ "outbox"
2461
+ ]);
2404
2462
  let body = await request.clone().text();
2405
2463
  for (let redirects = 0;; redirects++) {
2406
- const response = await fetch(request, { redirect: "manual" });
2464
+ const session = await openHop(request.url);
2465
+ let response;
2466
+ try {
2467
+ response = await session.fetch(request, { redirect: "manual" });
2468
+ } catch (error) {
2469
+ await closeAfterError(session, logger);
2470
+ throw error;
2471
+ }
2407
2472
  const location = response.headers.get("Location");
2408
2473
  if (![
2409
2474
  301,
@@ -2411,11 +2476,21 @@ async function fetchWithValidatedRedirects(request, validateUrl) {
2411
2476
  303,
2412
2477
  307,
2413
2478
  308
2414
- ].includes(response.status) || location == null) return response;
2415
- await response.body?.cancel();
2479
+ ].includes(response.status) || location == null) try {
2480
+ return await consume(response);
2481
+ } catch (error) {
2482
+ logger.debug("Failed to consume the response from {url}; closing the session: {error}", {
2483
+ url: request.url,
2484
+ error
2485
+ });
2486
+ throw error;
2487
+ } finally {
2488
+ await session.close();
2489
+ }
2490
+ cancelResponseBody(response, request.url);
2491
+ await session.close();
2416
2492
  if (redirects >= 20) throw new TypeError("Too many redirects");
2417
2493
  const url = new URL(location, request.url);
2418
- await validateUrl(url.href);
2419
2494
  const headers = new Headers(request.headers);
2420
2495
  if (url.origin !== new URL(request.url).origin) {
2421
2496
  headers.delete("Authorization");
@@ -2443,6 +2518,16 @@ async function fetchWithValidatedRedirects(request, validateUrl) {
2443
2518
  });
2444
2519
  }
2445
2520
  }
2521
+ async function closeAfterError(session, logger) {
2522
+ try {
2523
+ await session.close();
2524
+ } catch (error) {
2525
+ logger.debug("Failed to close the session for {url}: {error}", {
2526
+ url: session.url.href,
2527
+ error
2528
+ });
2529
+ }
2530
+ }
2446
2531
  /**
2447
2532
  * An error that is thrown when an activity fails to send to a remote inbox.
2448
2533
  * It contains structured information about the failure, including the HTTP
@@ -2,24 +2,24 @@ import { Temporal } from "@js-temporal/polyfill";
2
2
  import "urlpattern-polyfill";
3
3
  globalThis.addEventListener = () => {};
4
4
  import { n as RouterError } from "./router-CrMLXoOr.mjs";
5
- import { n as version, t as name } from "./deno-BcnGeGr2.mjs";
6
- import { a as importJwk, o as validateCryptoKey, t as exportJwk } from "./key-BFzQ3Hef.mjs";
7
- import { l as verifyRequest } from "./http-D0cjeL8a.mjs";
8
- import { t as getAuthenticatedDocumentLoader } from "./docloader-BqDq3fDF.mjs";
5
+ import { n as version, t as name } from "./deno-DL_ifPJk.mjs";
6
+ import { a as importJwk, o as validateCryptoKey, t as exportJwk } from "./key-CgcWGw6W.mjs";
7
+ import { u as verifyRequest } from "./http-Dvsq4uCu.mjs";
8
+ import { n as getAuthenticatedDocumentLoader } from "./docloader-B0_0Fjqo.mjs";
9
9
  import { n as kvCache } from "./kv-cache-BrjuJue8.mjs";
10
- import { a as compactJsonLd, c as getNormalizationContextLoader, d as isInvalidUrlTypeError, f as signJsonLd, g as wrapContextLoaderForJsonLd, l as hasSignature, p as verifyCompactJsonLd, r as assertSafeJsonLd, s as detachSignature, t as InvalidContextReferenceError, u as isClearlyMalformedContextReference } from "./ld-D2bkYBIz.mjs";
11
- import { n as getKeyOwner, t as doesActorOwnKey } from "./owner-BuJXJDhl.mjs";
12
- import { n as signObject, r as verifyObject } from "./proof-JxVhLZA3.mjs";
13
- import { d as readBoundedText, l as BodyTooLargeError, t as getNodeInfo, u as MAX_BODY_SIZE } from "./client-BUqbea1d.mjs";
10
+ import { a as compactJsonLd, c as getNormalizationContextLoader, d as isInvalidUrlTypeError, f as signJsonLd, g as wrapContextLoaderForJsonLd, l as hasSignature, p as verifyCompactJsonLd, r as assertSafeJsonLd, s as detachSignature, t as InvalidContextReferenceError, u as isClearlyMalformedContextReference } from "./ld-CfOIwHup.mjs";
11
+ import { n as getKeyOwner, t as doesActorOwnKey } from "./owner-BvVwCoj2.mjs";
12
+ import { n as signObject, r as verifyObject } from "./proof-C9TBSmUR.mjs";
13
+ import { d as MAX_BODY_SIZE, f as readBoundedText, t as getNodeInfo, u as BodyTooLargeError } from "./client-DGNacEx_.mjs";
14
14
  import { t as nodeInfoToJson } from "./types-MNlFfxjv.mjs";
15
- import { n as actorsHaveSameOrigin, r as routeActivity } from "./inbox-D2fsxjqC.mjs";
16
- import { t as FederationBuilderImpl } from "./builder-cEjyBwpD.mjs";
15
+ import { n as actorsHaveSameOrigin, r as routeActivity } from "./inbox-D6YbZFrZ.mjs";
16
+ import { t as FederationBuilderImpl } from "./builder-CKbQrOH1.mjs";
17
17
  import { t as buildCollectionSynchronizationHeader } from "./collection-DI6YzYpd.mjs";
18
18
  import { t as KvKeyCache } from "./keycache-DBD6fzl1.mjs";
19
19
  import { t as acceptsJsonLd } from "./negotiation-TWEs6Cl8.mjs";
20
- import { t as hasMalformedKnownTemporalLiteral } from "./temporal-Bv6Zn1C5.mjs";
20
+ import { t as hasMalformedKnownTemporalLiteral } from "./temporal-BNtFi_ut.mjs";
21
21
  import { t as createExponentialBackoffPolicy } from "./retry-Ddbq3AcK.mjs";
22
- import { n as extractInboxes, r as sendActivity, t as SendActivityError } from "./send-CDaWmeqV.mjs";
22
+ import { i as sendActivity, n as extractInboxes, t as SendActivityError } from "./send-ZWmjk2Zf.mjs";
23
23
  import { Activity, Collection, CollectionPage, CryptographicKey, Link, Multikey, Object as Object$1, OrderedCollection, OrderedCollectionPage, getTypeId, lookupObject, traverseCollection } from "@fedify/vocab";
24
24
  import { lookupWebFinger } from "@fedify/webfinger";
25
25
  import { SpanKind, SpanStatusCode, context, propagation, trace } from "@opentelemetry/api";
@@ -1,4 +1,4 @@
1
1
  const { Temporal } = require("@js-temporal/polyfill");
2
2
  const { URLPattern } = require("urlpattern-polyfill");
3
- const require_middleware = require("./middleware-DxX8ZL-q.cjs");
3
+ const require_middleware = require("./middleware-DZvWX5af.cjs");
4
4
  exports.FederationImpl = require_middleware.FederationImpl;
@@ -1,5 +1,5 @@
1
1
  import "@js-temporal/polyfill";
2
2
  import "urlpattern-polyfill";
3
3
  globalThis.addEventListener = () => {};
4
- import { n as FederationImpl } from "./middleware-CXIBpN4y.mjs";
4
+ import { n as FederationImpl } from "./middleware-CA7egMDR.mjs";
5
5
  export { FederationImpl };