@fedify/fedify 2.1.30 → 2.1.31

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 (84) hide show
  1. package/dist/{builder-CTpfuuVp.mjs → builder-isMRh_I6.mjs} +3 -3
  2. package/dist/{client-CAM_bQXx.d.cts → client-BIBzqEIp.d.cts} +0 -1
  3. package/dist/{client-CjDMsn4a.mjs → client-BZr7TGyu.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-DLLaDau8.d.ts → context-C0uGVzgk.d.ts} +28 -2
  9. package/dist/{context-BNwv9Q1v.d.cts → context-CLaLxi49.d.cts} +28 -2
  10. package/dist/{deno-BATJ1hq6.mjs → deno-CiaJqTcu.mjs} +1 -1
  11. package/dist/docloader-DOrwedT7.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 +380 -24
  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-Dl00HXr4.d.ts → http-9IFUxdkp.d.cts} +1 -2
  25. package/dist/{http-Bjd6I92u.mjs → http-A9mP2uM7.mjs} +194 -96
  26. package/dist/{http-BoR1M3W7.cjs → http-CRiVlZd5.cjs} +172 -103
  27. package/dist/{http-CK3efjgM.js → http-Crml3gei.js} +171 -102
  28. package/dist/{http-DqZhyEso.d.cts → http-SbfezERn.d.ts} +1 -2
  29. package/dist/{inbox-9MlBGgs9.mjs → inbox-KESLudq4.mjs} +1 -1
  30. package/dist/{key-ClQh9Qqh.mjs → key-Dg6-DV_O.mjs} +1 -1
  31. package/dist/{kv-cache-BxSx_Iyp.cjs → kv-cache-Cqmm-yQa.cjs} +59 -17
  32. package/dist/{kv-cache-ImApO425.js → kv-cache-DQcyZL5T.js} +60 -18
  33. package/dist/{ld-Crg0Xmwr.mjs → ld-COjDN6Nb.mjs} +2 -2
  34. package/dist/{middleware-CmD3lpy4.cjs → middleware-BbgR7xOI.cjs} +184 -59
  35. package/dist/{middleware-jL9p9V9G.cjs → middleware-BtDQUCcz.cjs} +1 -1
  36. package/dist/{middleware-CE0b69gj.mjs → middleware-COFlKeYn.mjs} +78 -38
  37. package/dist/{middleware-Bi5D6DBT.mjs → middleware-CuQvczZd.mjs} +1 -1
  38. package/dist/{middleware-C2AJFk8W.js → middleware-DIW5S_5L.js} +184 -59
  39. package/dist/{mod-DNEgTI8M.d.ts → mod-BRnwh2oy.d.ts} +2 -7
  40. package/dist/{mod-BLDHhuW0.d.ts → mod-C7w-QVUv.d.ts} +1 -1
  41. package/dist/{mod-CWJNbyju.d.ts → mod-CFRocFeQ.d.ts} +1 -1
  42. package/dist/{mod-BmlfRBmU.d.cts → mod-CGUH8Ujp.d.cts} +1 -1
  43. package/dist/{mod-Cn9GJ5ex.d.cts → mod-Co7LEK9X.d.cts} +2 -7
  44. package/dist/{mod-DiuFTYUt.d.cts → mod-LOEOKbYd.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/otel/exporter.test.mjs +292 -30
  56. package/dist/otel/mod.cjs +36 -30
  57. package/dist/otel/mod.d.cts +13 -0
  58. package/dist/otel/mod.d.ts +13 -0
  59. package/dist/otel/mod.js +36 -30
  60. package/dist/{owner-BRoKojK1.mjs → owner-CFHq61cq.mjs} +2 -2
  61. package/dist/{proof-CHXVlsH_.cjs → proof-0JkSC_pT.cjs} +1 -1
  62. package/dist/{proof-7R4s10TJ.js → proof-DzksJFK7.js} +1 -1
  63. package/dist/{proof-DMIAezxz.mjs → proof-KSHlbPCG.mjs} +2 -2
  64. package/dist/{send-BHIt_8ia.mjs → send-DaDe2YCb.mjs} +117 -32
  65. package/dist/sig/http.test.mjs +506 -2
  66. package/dist/sig/key.test.mjs +2 -2
  67. package/dist/sig/ld.test.mjs +2 -2
  68. package/dist/sig/mod.cjs +2 -2
  69. package/dist/sig/mod.d.cts +2 -2
  70. package/dist/sig/mod.d.ts +2 -2
  71. package/dist/sig/mod.js +2 -2
  72. package/dist/sig/owner.test.mjs +1 -1
  73. package/dist/sig/proof.test.mjs +1 -1
  74. package/dist/{temporal-DR0JahMu.mjs → temporal-DNGH8o2E.mjs} +1 -1
  75. package/dist/testing/mod.d.mts +26 -1
  76. package/dist/{types-miAYfiGO.js → types-B1_pRFE_.js} +112 -63
  77. package/dist/{types-DRPjmHVV.cjs → types-DOvyymM4.cjs} +111 -62
  78. package/dist/utils/docloader.test.mjs +316 -32
  79. package/dist/utils/mod.cjs +1 -1
  80. package/dist/utils/mod.d.cts +1 -1
  81. package/dist/utils/mod.d.ts +1 -1
  82. package/dist/utils/mod.js +1 -1
  83. package/package.json +6 -6
  84. package/dist/docloader-OLWc7P2D.mjs +0 -82
@@ -1,9 +1,9 @@
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-BATJ1hq6.mjs";
4
+ import { n as version, t as name } from "./deno-CiaJqTcu.mjs";
5
5
  import { i as validateAcceptSignature, n as fulfillAcceptSignature, r as parseAcceptSignature } from "./accept-BavzedPB.mjs";
6
- import { i as fetchKeyDetailed, s as validateCryptoKey } from "./key-ClQh9Qqh.mjs";
6
+ import { i as fetchKeyDetailed, s as validateCryptoKey } from "./key-Dg6-DV_O.mjs";
7
7
  import { CryptographicKey } from "@fedify/vocab";
8
8
  import { SpanStatusCode, trace } from "@opentelemetry/api";
9
9
  import { FetchError } from "@fedify/vocab-runtime";
@@ -849,10 +849,10 @@ async function createRedirectRequest(request, location, body, validateRedirect)
849
849
  cache: request.cache
850
850
  });
851
851
  }
852
- async function fetchDoubleKnockRequest(request, signedRequest, signal) {
852
+ async function fetchDoubleKnockRequest(session, request, signedRequest, signal) {
853
853
  const maxAttempts = request.method === "GET" || request.method === "HEAD" ? 2 : 1;
854
854
  for (let attempt = 1;; attempt++) try {
855
- return await fetch(signedRequest, {
855
+ return await session.fetch(signedRequest, {
856
856
  redirect: "manual",
857
857
  signal
858
858
  });
@@ -913,107 +913,205 @@ function getAbortReason(signal) {
913
913
  * @since 1.6.0
914
914
  */
915
915
  async function doubleKnock(request, identity, options = {}) {
916
- return await doubleKnockInternal(request, identity, options);
916
+ const { response } = await doubleKnockCore(request, identity, options, openGlobalFetchHop);
917
+ return response;
917
918
  }
918
- async function doubleKnockInternal(request, identity, options, redirected = 0, visited = /* @__PURE__ */ new Set()) {
919
- const { specDeterminer, log, tracerProvider, signal } = options;
920
- visited.add(request.url);
921
- const origin = new URL(request.url).origin;
922
- const firstTrySpec = specDeterminer == null ? "rfc9421" : await specDeterminer.determineSpec(origin);
923
- const body = options.body !== void 0 ? options.body : request.method !== "GET" && request.method !== "HEAD" ? await request.clone().arrayBuffer() : null;
924
- let signedRequest = await signRequest(request, identity.privateKey, identity.keyId, {
925
- spec: firstTrySpec,
926
- tracerProvider,
927
- body
919
+ /**
920
+ * Performs a double-knock request like {@link doubleKnock}, opening a session
921
+ * for every hop with the given function, and passes the final response to
922
+ * the given function. The final session is closed after `consume` settles,
923
+ * so `consume` must read or cancel the response body before it returns.
924
+ * This is not exported from any entry point.
925
+ * @param request The request to send.
926
+ * @param identity The identity to use for signing the request.
927
+ * @param options The options for double-knock requests.
928
+ * @param openHop The function to open a session for each hop. The session of
929
+ * a hop is closed before the next hop is opened.
930
+ * @param consume The function to consume the final response.
931
+ * @returns The value returned by `consume`.
932
+ */
933
+ async function doubleKnockScoped(request, identity, options, openHop, consume) {
934
+ const { response, session } = await doubleKnockCore(request, identity, options, openHop);
935
+ try {
936
+ return await consume(response);
937
+ } catch (error) {
938
+ getLogger([
939
+ "fedify",
940
+ "sig",
941
+ "http"
942
+ ]).debug("Failed to consume the response from {url}; closing the session: {error}", {
943
+ url: request.url,
944
+ error
945
+ });
946
+ throw error;
947
+ } finally {
948
+ await session.close();
949
+ }
950
+ }
951
+ /**
952
+ * A hop session for {@link doubleKnock}: it uses the global `fetch()` without
953
+ * validating or pinning addresses, and its `close()` does nothing, so that
954
+ * the final response can be returned to the caller.
955
+ */
956
+ function openGlobalFetchHop(url) {
957
+ const sessionUrl = new URL(url);
958
+ return Promise.resolve({
959
+ get url() {
960
+ return new URL(sessionUrl);
961
+ },
962
+ fetch: (input, init) => fetch(input, init),
963
+ close: () => Promise.resolve()
928
964
  });
929
- log?.(signedRequest);
930
- let response = await fetchDoubleKnockRequest(request, signedRequest, signal);
931
- if (response.status >= 300 && response.status < 400 && response.headers.has("Location")) {
932
- if (redirected >= DEFAULT_MAX_REDIRECTION) throw new FetchError(request.url, `Too many redirections (${redirected + 1})`);
933
- const redirectRequest = await createRedirectRequest(request, response.headers.get("Location"), body, options.validateRedirect);
934
- if (visited.has(redirectRequest.url)) throw new FetchError(request.url, `Redirect loop detected: ${redirectRequest.url}`);
935
- return doubleKnockInternal(redirectRequest, identity, {
936
- ...options,
937
- body
938
- }, redirected + 1, visited);
939
- } else if (response.status === 400 || response.status === 401 || response.status > 401) {
940
- const logger = getLogger([
965
+ }
966
+ function isRedirectResponse(response) {
967
+ return response.status >= 300 && response.status < 400 && response.headers.has("Location");
968
+ }
969
+ /**
970
+ * Starts cancelling the body of a response that is not passed to the caller,
971
+ * without waiting for it to settle. The cancellation of a branch of a tee
972
+ * (e.g., a body cloned by a fetch mock) settles only after the other branch
973
+ * is cancelled too, per the Streams standard, so awaiting it could block
974
+ * forever. Failures are only logged.
975
+ */
976
+ function cancelResponseBody(response, url) {
977
+ response.body?.cancel().catch((error) => {
978
+ getLogger([
941
979
  "fedify",
942
980
  "sig",
943
981
  "http"
944
- ]);
945
- const acceptSigHeader = response.headers.get("Accept-Signature");
946
- if (acceptSigHeader != null) {
947
- const entries = validateAcceptSignature(parseAcceptSignature(acceptSigHeader));
948
- const localKeyId = identity.keyId.href;
949
- const localAlg = "rsa-v1_5-sha256";
950
- let fulfilled = false;
951
- let challengeRequest;
952
- for (const entry of entries) {
953
- const rfc9421 = fulfillAcceptSignature(entry, localKeyId, localAlg);
954
- if (rfc9421 == null) continue;
955
- logger.debug("Received Accept-Signature challenge; accumulating label {label} and components {components}.", {
956
- label: rfc9421.label,
957
- components: rfc9421.components
958
- });
959
- try {
960
- challengeRequest = await signRequest(challengeRequest ?? request, identity.privateKey, identity.keyId, {
961
- spec: "rfc9421",
962
- tracerProvider,
963
- body,
964
- rfc9421
982
+ ]).debug("Failed to cancel the response body from {url}: {error}", {
983
+ url,
984
+ error
985
+ });
986
+ });
987
+ }
988
+ async function doubleKnockCore(request, identity, options, openHop) {
989
+ const { specDeterminer, log, tracerProvider, signal } = options;
990
+ const visited = /* @__PURE__ */ new Set();
991
+ const body = options.body !== void 0 ? options.body : request.method !== "GET" && request.method !== "HEAD" ? await request.clone().arrayBuffer() : null;
992
+ let session = await openHop(request.url);
993
+ try {
994
+ for (let redirected = 0;; redirected++) {
995
+ visited.add(request.url);
996
+ const origin = new URL(request.url).origin;
997
+ const firstTrySpec = specDeterminer == null ? "rfc9421" : await specDeterminer.determineSpec(origin);
998
+ let signedRequest = await signRequest(request, identity.privateKey, identity.keyId, {
999
+ spec: firstTrySpec,
1000
+ tracerProvider,
1001
+ body
1002
+ });
1003
+ log?.(signedRequest);
1004
+ let response = await fetchDoubleKnockRequest(session, request, signedRequest, signal);
1005
+ const firstRedirected = isRedirectResponse(response);
1006
+ if (!firstRedirected && (response.status === 400 || response.status === 401 || response.status > 401)) {
1007
+ const logger = getLogger([
1008
+ "fedify",
1009
+ "sig",
1010
+ "http"
1011
+ ]);
1012
+ const acceptSigHeader = response.headers.get("Accept-Signature");
1013
+ if (acceptSigHeader != null) {
1014
+ const entries = validateAcceptSignature(parseAcceptSignature(acceptSigHeader));
1015
+ const localKeyId = identity.keyId.href;
1016
+ const localAlg = "rsa-v1_5-sha256";
1017
+ let fulfilled = false;
1018
+ let challengeRequest;
1019
+ for (const entry of entries) {
1020
+ const rfc9421 = fulfillAcceptSignature(entry, localKeyId, localAlg);
1021
+ if (rfc9421 == null) continue;
1022
+ logger.debug("Received Accept-Signature challenge; accumulating label {label} and components {components}.", {
1023
+ label: rfc9421.label,
1024
+ components: rfc9421.components
1025
+ });
1026
+ try {
1027
+ challengeRequest = await signRequest(challengeRequest ?? request, identity.privateKey, identity.keyId, {
1028
+ spec: "rfc9421",
1029
+ tracerProvider,
1030
+ body,
1031
+ rfc9421
1032
+ });
1033
+ fulfilled = true;
1034
+ } catch (error) {
1035
+ logger.debug("Failed to fulfill Accept-Signature challenge entry {label}: {error}", {
1036
+ label: entry.label,
1037
+ error
1038
+ });
1039
+ }
1040
+ }
1041
+ if (fulfilled && challengeRequest != null) {
1042
+ cancelResponseBody(response, request.url);
1043
+ signedRequest = challengeRequest;
1044
+ log?.(signedRequest);
1045
+ response = await session.fetch(signedRequest, {
1046
+ redirect: "manual",
1047
+ signal
1048
+ });
1049
+ }
1050
+ if (fulfilled && !isRedirectResponse(response)) {
1051
+ if (response.status < 300) {
1052
+ await specDeterminer?.rememberSpec(origin, "rfc9421");
1053
+ return {
1054
+ response,
1055
+ session
1056
+ };
1057
+ }
1058
+ if (response.status !== 400 && response.status !== 401) return {
1059
+ response,
1060
+ session
1061
+ };
1062
+ }
1063
+ }
1064
+ if (!isRedirectResponse(response)) {
1065
+ const spec = firstTrySpec === "draft-cavage-http-signatures-12" ? "rfc9421" : "draft-cavage-http-signatures-12";
1066
+ getLogger([
1067
+ "fedify",
1068
+ "sig",
1069
+ "http"
1070
+ ]).debug("Failed to verify with the spec {spec} ({status} {statusText}); retrying with spec {secondSpec}... (double-knocking)", {
1071
+ spec: firstTrySpec,
1072
+ secondSpec: spec,
1073
+ status: response.status,
1074
+ statusText: response.statusText
965
1075
  });
966
- fulfilled = true;
967
- } catch (error) {
968
- logger.debug("Failed to fulfill Accept-Signature challenge entry {label}: {error}", {
969
- label: entry.label,
970
- error
1076
+ cancelResponseBody(response, request.url);
1077
+ signedRequest = await signRequest(request, identity.privateKey, identity.keyId, {
1078
+ spec,
1079
+ tracerProvider,
1080
+ body
971
1081
  });
1082
+ log?.(signedRequest);
1083
+ response = await fetchDoubleKnockRequest(session, request, signedRequest, signal);
1084
+ if (!isRedirectResponse(response) && response.status !== 400 && response.status !== 401) await specDeterminer?.rememberSpec(origin, spec);
972
1085
  }
973
- }
974
- if (fulfilled && challengeRequest != null) {
975
- signedRequest = challengeRequest;
976
- log?.(signedRequest);
977
- response = await fetch(signedRequest, {
978
- redirect: "manual",
979
- signal
980
- });
981
- if (response.status >= 300 && response.status < 400 && response.headers.has("Location")) return doubleKnock(await createRedirectRequest(request, response.headers.get("Location"), body, options.validateRedirect), identity, {
982
- ...options,
983
- body
984
- });
985
- }
986
- if (fulfilled && response.status < 300) {
987
- await specDeterminer?.rememberSpec(origin, "rfc9421");
988
- return response;
989
- }
990
- if (fulfilled && response.status !== 400 && response.status !== 401) return response;
991
- }
992
- const spec = firstTrySpec === "draft-cavage-http-signatures-12" ? "rfc9421" : "draft-cavage-http-signatures-12";
993
- logger.debug("Failed to verify with the spec {spec} ({status} {statusText}); retrying with spec {secondSpec}... (double-knocking)", {
994
- spec: firstTrySpec,
995
- secondSpec: spec,
996
- status: response.status,
997
- statusText: response.statusText
998
- });
999
- signedRequest = await signRequest(request, identity.privateKey, identity.keyId, {
1000
- spec,
1001
- tracerProvider,
1002
- body
1003
- });
1004
- log?.(signedRequest);
1005
- response = await fetchDoubleKnockRequest(request, signedRequest, signal);
1006
- if (response.status >= 300 && response.status < 400 && response.headers.has("Location")) {
1086
+ } else if (!firstRedirected) await specDeterminer?.rememberSpec(origin, firstTrySpec);
1087
+ if (!isRedirectResponse(response)) return {
1088
+ response,
1089
+ session
1090
+ };
1091
+ cancelResponseBody(response, request.url);
1092
+ await session.close();
1007
1093
  if (redirected >= DEFAULT_MAX_REDIRECTION) throw new FetchError(request.url, `Too many redirections (${redirected + 1})`);
1008
- const redirectRequest = await createRedirectRequest(request, response.headers.get("Location"), body, options.validateRedirect);
1094
+ const location = response.headers.get("Location");
1095
+ const redirectRequest = await createRedirectRequest(request, location, body, options.validateRedirect);
1009
1096
  if (visited.has(redirectRequest.url)) throw new FetchError(request.url, `Redirect loop detected: ${redirectRequest.url}`);
1010
- return doubleKnockInternal(redirectRequest, identity, {
1011
- ...options,
1012
- body
1013
- }, redirected + 1, visited);
1014
- } else if (response.status !== 400 && response.status !== 401) await specDeterminer?.rememberSpec(origin, spec);
1015
- } else await specDeterminer?.rememberSpec(origin, firstTrySpec);
1016
- return response;
1097
+ request = redirectRequest;
1098
+ session = await openHop(request.url);
1099
+ }
1100
+ } catch (error) {
1101
+ try {
1102
+ await session.close();
1103
+ } catch (closeError) {
1104
+ getLogger([
1105
+ "fedify",
1106
+ "sig",
1107
+ "http"
1108
+ ]).debug("Failed to close the session for {url}: {error}", {
1109
+ url: request.url,
1110
+ error: closeError
1111
+ });
1112
+ }
1113
+ throw error;
1114
+ }
1017
1115
  }
1018
1116
  /**
1019
1117
  * Performs a timing-safe equality comparison between two `Uint8Array` values.
@@ -1043,4 +1141,4 @@ function timingSafeEqual(a, b) {
1043
1141
  return result === 0;
1044
1142
  }
1045
1143
  //#endregion
1046
- export { parseRfc9421Signature as a, timingSafeEqual as c, formatRfc9421SignatureParameters as i, verifyRequest as l, doubleKnock as n, parseRfc9421SignatureInput as o, formatRfc9421Signature as r, signRequest as s, createRfc9421SignatureBase as t, verifyRequestDetailed as u };
1144
+ export { formatRfc9421SignatureParameters as a, signRequest as c, verifyRequestDetailed as d, formatRfc9421Signature as i, timingSafeEqual as l, doubleKnock as n, parseRfc9421Signature as o, doubleKnockScoped as r, parseRfc9421SignatureInput as s, createRfc9421SignatureBase as t, verifyRequest as u };
@@ -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.1.30";
14
+ var version = "2.1.31";
15
15
  //#endregion
16
16
  //#region src/sig/accept.ts
17
17
  /**
@@ -1598,10 +1598,10 @@ async function createRedirectRequest(request, location, body, validateRedirect)
1598
1598
  cache: request.cache
1599
1599
  });
1600
1600
  }
1601
- async function fetchDoubleKnockRequest(request, signedRequest, signal) {
1601
+ async function fetchDoubleKnockRequest(session, request, signedRequest, signal) {
1602
1602
  const maxAttempts = request.method === "GET" || request.method === "HEAD" ? 2 : 1;
1603
1603
  for (let attempt = 1;; attempt++) try {
1604
- return await fetch(signedRequest, {
1604
+ return await session.fetch(signedRequest, {
1605
1605
  redirect: "manual",
1606
1606
  signal
1607
1607
  });
@@ -1652,117 +1652,186 @@ function getAbortReason(signal) {
1652
1652
  return signal.reason ?? new DOMException("The operation was aborted.", "AbortError");
1653
1653
  }
1654
1654
  /**
1655
- * Performs a double-knock request to the given URL. For the details of
1656
- * double-knocking, see
1657
- * <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.
1658
1660
  * @param request The request to send.
1659
1661
  * @param identity The identity to use for signing the request.
1660
1662
  * @param options The options for double-knock requests.
1661
- * @returns The response to the request.
1662
- * @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`.
1663
1667
  */
1664
- async function doubleKnock(request, identity, options = {}) {
1665
- 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
+ }
1666
1685
  }
1667
- async function doubleKnockInternal(request, identity, options, redirected = 0, visited = /* @__PURE__ */ new Set()) {
1668
- const { specDeterminer, log, tracerProvider, signal } = options;
1669
- visited.add(request.url);
1670
- const origin = new URL(request.url).origin;
1671
- const firstTrySpec = specDeterminer == null ? "rfc9421" : await specDeterminer.determineSpec(origin);
1672
- const body = options.body !== void 0 ? options.body : request.method !== "GET" && request.method !== "HEAD" ? await request.clone().arrayBuffer() : null;
1673
- let signedRequest = await signRequest(request, identity.privateKey, identity.keyId, {
1674
- spec: firstTrySpec,
1675
- tracerProvider,
1676
- body
1677
- });
1678
- log?.(signedRequest);
1679
- let response = await fetchDoubleKnockRequest(request, signedRequest, signal);
1680
- if (response.status >= 300 && response.status < 400 && response.headers.has("Location")) {
1681
- if (redirected >= DEFAULT_MAX_REDIRECTION) throw new _fedify_vocab_runtime.FetchError(request.url, `Too many redirections (${redirected + 1})`);
1682
- const redirectRequest = await createRedirectRequest(request, response.headers.get("Location"), body, options.validateRedirect);
1683
- if (visited.has(redirectRequest.url)) throw new _fedify_vocab_runtime.FetchError(request.url, `Redirect loop detected: ${redirectRequest.url}`);
1684
- return doubleKnockInternal(redirectRequest, identity, {
1685
- ...options,
1686
- body
1687
- }, redirected + 1, visited);
1688
- } else if (response.status === 400 || response.status === 401 || response.status > 401) {
1689
- 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)([
1690
1699
  "fedify",
1691
1700
  "sig",
1692
1701
  "http"
1693
- ]);
1694
- const acceptSigHeader = response.headers.get("Accept-Signature");
1695
- if (acceptSigHeader != null) {
1696
- const entries = validateAcceptSignature(parseAcceptSignature(acceptSigHeader));
1697
- const localKeyId = identity.keyId.href;
1698
- const localAlg = "rsa-v1_5-sha256";
1699
- let fulfilled = false;
1700
- let challengeRequest;
1701
- for (const entry of entries) {
1702
- const rfc9421 = fulfillAcceptSignature(entry, localKeyId, localAlg);
1703
- if (rfc9421 == null) continue;
1704
- logger.debug("Received Accept-Signature challenge; accumulating label {label} and components {components}.", {
1705
- label: rfc9421.label,
1706
- components: rfc9421.components
1707
- });
1708
- try {
1709
- challengeRequest = await signRequest(challengeRequest ?? request, identity.privateKey, identity.keyId, {
1710
- spec: "rfc9421",
1711
- tracerProvider,
1712
- body,
1713
- 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 visited = /* @__PURE__ */ new Set();
1711
+ const body = options.body !== void 0 ? options.body : request.method !== "GET" && request.method !== "HEAD" ? await request.clone().arrayBuffer() : null;
1712
+ let session = await openHop(request.url);
1713
+ try {
1714
+ for (let redirected = 0;; redirected++) {
1715
+ visited.add(request.url);
1716
+ const origin = new URL(request.url).origin;
1717
+ const firstTrySpec = specDeterminer == null ? "rfc9421" : await specDeterminer.determineSpec(origin);
1718
+ let signedRequest = await signRequest(request, identity.privateKey, identity.keyId, {
1719
+ spec: firstTrySpec,
1720
+ tracerProvider,
1721
+ body
1722
+ });
1723
+ log?.(signedRequest);
1724
+ let response = await fetchDoubleKnockRequest(session, request, signedRequest, signal);
1725
+ const firstRedirected = isRedirectResponse(response);
1726
+ if (!firstRedirected && (response.status === 400 || response.status === 401 || response.status > 401)) {
1727
+ const logger = (0, _logtape_logtape.getLogger)([
1728
+ "fedify",
1729
+ "sig",
1730
+ "http"
1731
+ ]);
1732
+ const acceptSigHeader = response.headers.get("Accept-Signature");
1733
+ if (acceptSigHeader != null) {
1734
+ const entries = validateAcceptSignature(parseAcceptSignature(acceptSigHeader));
1735
+ const localKeyId = identity.keyId.href;
1736
+ const localAlg = "rsa-v1_5-sha256";
1737
+ let fulfilled = false;
1738
+ let challengeRequest;
1739
+ for (const entry of entries) {
1740
+ const rfc9421 = fulfillAcceptSignature(entry, localKeyId, localAlg);
1741
+ if (rfc9421 == null) continue;
1742
+ logger.debug("Received Accept-Signature challenge; accumulating label {label} and components {components}.", {
1743
+ label: rfc9421.label,
1744
+ components: rfc9421.components
1745
+ });
1746
+ try {
1747
+ challengeRequest = await signRequest(challengeRequest ?? request, identity.privateKey, identity.keyId, {
1748
+ spec: "rfc9421",
1749
+ tracerProvider,
1750
+ body,
1751
+ rfc9421
1752
+ });
1753
+ fulfilled = true;
1754
+ } catch (error) {
1755
+ logger.debug("Failed to fulfill Accept-Signature challenge entry {label}: {error}", {
1756
+ label: entry.label,
1757
+ error
1758
+ });
1759
+ }
1760
+ }
1761
+ if (fulfilled && challengeRequest != null) {
1762
+ cancelResponseBody(response, request.url);
1763
+ signedRequest = challengeRequest;
1764
+ log?.(signedRequest);
1765
+ response = await session.fetch(signedRequest, {
1766
+ redirect: "manual",
1767
+ signal
1768
+ });
1769
+ }
1770
+ if (fulfilled && !isRedirectResponse(response)) {
1771
+ if (response.status < 300) {
1772
+ await specDeterminer?.rememberSpec(origin, "rfc9421");
1773
+ return {
1774
+ response,
1775
+ session
1776
+ };
1777
+ }
1778
+ if (response.status !== 400 && response.status !== 401) return {
1779
+ response,
1780
+ session
1781
+ };
1782
+ }
1783
+ }
1784
+ if (!isRedirectResponse(response)) {
1785
+ const spec = firstTrySpec === "draft-cavage-http-signatures-12" ? "rfc9421" : "draft-cavage-http-signatures-12";
1786
+ (0, _logtape_logtape.getLogger)([
1787
+ "fedify",
1788
+ "sig",
1789
+ "http"
1790
+ ]).debug("Failed to verify with the spec {spec} ({status} {statusText}); retrying with spec {secondSpec}... (double-knocking)", {
1791
+ spec: firstTrySpec,
1792
+ secondSpec: spec,
1793
+ status: response.status,
1794
+ statusText: response.statusText
1714
1795
  });
1715
- fulfilled = true;
1716
- } catch (error) {
1717
- logger.debug("Failed to fulfill Accept-Signature challenge entry {label}: {error}", {
1718
- label: entry.label,
1719
- error
1796
+ cancelResponseBody(response, request.url);
1797
+ signedRequest = await signRequest(request, identity.privateKey, identity.keyId, {
1798
+ spec,
1799
+ tracerProvider,
1800
+ body
1720
1801
  });
1802
+ log?.(signedRequest);
1803
+ response = await fetchDoubleKnockRequest(session, request, signedRequest, signal);
1804
+ if (!isRedirectResponse(response) && response.status !== 400 && response.status !== 401) await specDeterminer?.rememberSpec(origin, spec);
1721
1805
  }
1722
- }
1723
- if (fulfilled && challengeRequest != null) {
1724
- signedRequest = challengeRequest;
1725
- log?.(signedRequest);
1726
- response = await fetch(signedRequest, {
1727
- redirect: "manual",
1728
- signal
1729
- });
1730
- if (response.status >= 300 && response.status < 400 && response.headers.has("Location")) return doubleKnock(await createRedirectRequest(request, response.headers.get("Location"), body, options.validateRedirect), identity, {
1731
- ...options,
1732
- body
1733
- });
1734
- }
1735
- if (fulfilled && response.status < 300) {
1736
- await specDeterminer?.rememberSpec(origin, "rfc9421");
1737
- return response;
1738
- }
1739
- if (fulfilled && response.status !== 400 && response.status !== 401) return response;
1740
- }
1741
- const spec = firstTrySpec === "draft-cavage-http-signatures-12" ? "rfc9421" : "draft-cavage-http-signatures-12";
1742
- logger.debug("Failed to verify with the spec {spec} ({status} {statusText}); retrying with spec {secondSpec}... (double-knocking)", {
1743
- spec: firstTrySpec,
1744
- secondSpec: spec,
1745
- status: response.status,
1746
- statusText: response.statusText
1747
- });
1748
- signedRequest = await signRequest(request, identity.privateKey, identity.keyId, {
1749
- spec,
1750
- tracerProvider,
1751
- body
1752
- });
1753
- log?.(signedRequest);
1754
- response = await fetchDoubleKnockRequest(request, signedRequest, signal);
1755
- if (response.status >= 300 && response.status < 400 && response.headers.has("Location")) {
1806
+ } else if (!firstRedirected) await specDeterminer?.rememberSpec(origin, firstTrySpec);
1807
+ if (!isRedirectResponse(response)) return {
1808
+ response,
1809
+ session
1810
+ };
1811
+ cancelResponseBody(response, request.url);
1812
+ await session.close();
1756
1813
  if (redirected >= DEFAULT_MAX_REDIRECTION) throw new _fedify_vocab_runtime.FetchError(request.url, `Too many redirections (${redirected + 1})`);
1757
- const redirectRequest = await createRedirectRequest(request, response.headers.get("Location"), body, options.validateRedirect);
1814
+ const location = response.headers.get("Location");
1815
+ const redirectRequest = await createRedirectRequest(request, location, body, options.validateRedirect);
1758
1816
  if (visited.has(redirectRequest.url)) throw new _fedify_vocab_runtime.FetchError(request.url, `Redirect loop detected: ${redirectRequest.url}`);
1759
- return doubleKnockInternal(redirectRequest, identity, {
1760
- ...options,
1761
- body
1762
- }, redirected + 1, visited);
1763
- } else if (response.status !== 400 && response.status !== 401) await specDeterminer?.rememberSpec(origin, spec);
1764
- } else await specDeterminer?.rememberSpec(origin, firstTrySpec);
1765
- return response;
1817
+ request = redirectRequest;
1818
+ session = await openHop(request.url);
1819
+ }
1820
+ } catch (error) {
1821
+ try {
1822
+ await session.close();
1823
+ } catch (closeError) {
1824
+ (0, _logtape_logtape.getLogger)([
1825
+ "fedify",
1826
+ "sig",
1827
+ "http"
1828
+ ]).debug("Failed to close the session for {url}: {error}", {
1829
+ url: request.url,
1830
+ error: closeError
1831
+ });
1832
+ }
1833
+ throw error;
1834
+ }
1766
1835
  }
1767
1836
  /**
1768
1837
  * Performs a timing-safe equality comparison between two `Uint8Array` values.
@@ -1792,10 +1861,10 @@ function timingSafeEqual(a, b) {
1792
1861
  return result === 0;
1793
1862
  }
1794
1863
  //#endregion
1795
- Object.defineProperty(exports, "doubleKnock", {
1864
+ Object.defineProperty(exports, "doubleKnockScoped", {
1796
1865
  enumerable: true,
1797
1866
  get: function() {
1798
- return doubleKnock;
1867
+ return doubleKnockScoped;
1799
1868
  }
1800
1869
  });
1801
1870
  Object.defineProperty(exports, "exportJwk", {