@fedify/fedify 2.4.0-dev.2169 → 2.4.0-dev.2190

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 (103) hide show
  1. package/dist/{builder-B2d_Pt0j.mjs → builder-CFF0YNTp.mjs} +3 -3
  2. package/dist/{circuit-breaker-C5NqoLBJ.mjs → circuit-breaker-XJ53md4S.mjs} +16 -11
  3. package/dist/compat/mod.d.cts +1 -1
  4. package/dist/compat/mod.d.ts +1 -1
  5. package/dist/compat/transformers.test.mjs +1 -1
  6. package/dist/{compound-proof-D28gMFqP.mjs → compound-proof-F2jjkrt0.mjs} +3 -3
  7. package/dist/{context-BykNUf04.d.ts → context-9T--NSo4.d.ts} +55 -1
  8. package/dist/{context-L0Fnib9j.d.cts → context-CDcAgm4l.d.cts} +55 -1
  9. package/dist/{deno-DU8dvnWk.mjs → deno-EopndaNb.mjs} +1 -1
  10. package/dist/{docloader-CghLH7Zi.mjs → docloader-u6zzOExF.mjs} +25 -17
  11. package/dist/federation/builder.test.mjs +1 -1
  12. package/dist/federation/circuit-breaker.test.mjs +50 -1
  13. package/dist/federation/compound-fanout.test.mjs +3 -3
  14. package/dist/federation/handler-compound-proof.test.mjs +3 -3
  15. package/dist/federation/handler.test.mjs +5 -5
  16. package/dist/federation/hashlink-media.test.mjs +2 -2
  17. package/dist/federation/idempotency.test.mjs +2 -2
  18. package/dist/federation/keycache.test.mjs +1 -1
  19. package/dist/federation/metrics.test.mjs +1 -1
  20. package/dist/federation/middleware.test.mjs +137 -13
  21. package/dist/federation/mod.cjs +1 -1
  22. package/dist/federation/mod.d.cts +2 -2
  23. package/dist/federation/mod.d.ts +2 -2
  24. package/dist/federation/mod.js +1 -1
  25. package/dist/federation/outgoing-proof.test.mjs +5 -5
  26. package/dist/federation/portable-actor.test.mjs +3 -3
  27. package/dist/federation/portable-chain.test.mjs +3 -3
  28. package/dist/federation/portable-collection.test.mjs +4 -4
  29. package/dist/federation/portable-gateway-keys.test.mjs +6 -6
  30. package/dist/federation/portable-inbox.test.mjs +35 -5
  31. package/dist/federation/portable-parse-uri.test.mjs +1 -1
  32. package/dist/federation/portable-route-activity.test.d.mts +2 -0
  33. package/dist/federation/portable-route-activity.test.mjs +259 -0
  34. package/dist/federation/portable.test.mjs +3 -3
  35. package/dist/federation/send.test.mjs +3 -3
  36. package/dist/federation/tasks/enqueue.test.mjs +1 -1
  37. package/dist/federation/tasks/tasks.test.mjs +2 -2
  38. package/dist/federation/temporal.test.mjs +1 -1
  39. package/dist/federation/webfinger.test.mjs +1 -1
  40. package/dist/{federation-DrNgkLfJ.cjs → federation-BSS2dUO7.cjs} +38 -21
  41. package/dist/{http-BGpexPO7.js → http-BqN5kyUf.js} +219 -87
  42. package/dist/{http-Cok5-vIC.d.ts → http-BrwKINQn.d.cts} +29 -1
  43. package/dist/{http-5CJBdU8n.cjs → http-CCr2_ipG.cjs} +236 -86
  44. package/dist/{http-DC1cqAQ2.d.cts → http-DGx0KOws.d.ts} +29 -1
  45. package/dist/{http-Bd6rZlJX.mjs → http-Y1ObT1A8.mjs} +180 -92
  46. package/dist/{key-gIhVZO3p.mjs → key-DyXf88bq.mjs} +4 -4
  47. package/dist/{key-owner-evidence-Bo3yaeL5.mjs → key-owner-evidence-APB6NpSk.mjs} +1 -1
  48. package/dist/{keycache-PiOM9pM0.mjs → keycache-Da8A8mZa.mjs} +1 -1
  49. package/dist/{kv-cache-iR55fqlk.js → kv-cache-C02XitwK.js} +24 -16
  50. package/dist/{kv-cache-D0qjSeWY.mjs → kv-cache-C8YjNAi_.mjs} +1 -1
  51. package/dist/{kv-cache-YRi_g8GB.cjs → kv-cache-DUXluAc0.cjs} +23 -15
  52. package/dist/{ld-BsTkRrwF.mjs → ld-oQj6j_hP.mjs} +3 -3
  53. package/dist/{metrics-jUDFfVgi.mjs → metrics-zMe834ID.mjs} +1 -1
  54. package/dist/{middleware-Coqgp-K0.mjs → middleware-Bdu2-OPD.mjs} +37 -25
  55. package/dist/{middleware-DlxMxVp5.js → middleware-CaR2MRWY.js} +39 -22
  56. package/dist/{middleware-Dq-OCmb6.mjs → middleware-Ck9yghxs.mjs} +1 -1
  57. package/dist/{mod-DK0y9fd-.d.ts → mod-B3OBLj5g.d.ts} +1 -1
  58. package/dist/{mod-B5z_q9TH.d.ts → mod-C5Mwx5ty.d.ts} +5 -1
  59. package/dist/{mod--jv6vyrP.d.ts → mod-Ck9_lSJB.d.ts} +1 -1
  60. package/dist/{mod-BLOXGs6m.d.cts → mod-D58KCqUg.d.cts} +1 -1
  61. package/dist/{mod-CPPqkyV_.d.cts → mod-DHTuu3l8.d.cts} +1 -1
  62. package/dist/{mod-BA_F2x53.d.cts → mod-Dp3Ij7sw.d.cts} +5 -1
  63. package/dist/mod.cjs +4 -4
  64. package/dist/mod.d.cts +5 -5
  65. package/dist/mod.d.ts +5 -5
  66. package/dist/mod.js +4 -4
  67. package/dist/nodeinfo/handler.test.mjs +1 -1
  68. package/dist/{owner-D3GoetXS.mjs → owner-Dem4fbwc.mjs} +5 -5
  69. package/dist/{portable-collection-Bkdh_WpL.js → portable-collection-BEqLrMW3.js} +1 -1
  70. package/dist/{portable-collection-C60_acmx.mjs → portable-collection-BL6aZlQ0.mjs} +2 -2
  71. package/dist/{portable-collection-CnlCamup.cjs → portable-collection-DJe_bk50.cjs} +1 -1
  72. package/dist/{portable-key-DyF5AnZo.mjs → portable-key-Duue9Y2Y.mjs} +2 -2
  73. package/dist/{portable-key-id-Bg259c4Z.mjs → portable-key-id-Ca30-hOa.mjs} +45 -1
  74. package/dist/{proof-CQAYqWdj.mjs → proof-C9115DX_.mjs} +5 -5
  75. package/dist/{send-DjkQrxt5.mjs → send-D0hxFFRB.mjs} +3 -3
  76. package/dist/sig/compound-proof-discovery.test.mjs +1 -1
  77. package/dist/sig/compound-proof-producer.test.mjs +1 -1
  78. package/dist/sig/compound-proof-verification.test.mjs +2 -2
  79. package/dist/sig/compound-proof.test.mjs +2 -2
  80. package/dist/sig/http.test.mjs +495 -23
  81. package/dist/sig/key-owner-evidence.test.mjs +2 -2
  82. package/dist/sig/key.test.mjs +1 -1
  83. package/dist/sig/ld.test.mjs +2 -2
  84. package/dist/sig/mod.cjs +2 -2
  85. package/dist/sig/mod.d.cts +2 -2
  86. package/dist/sig/mod.d.ts +2 -2
  87. package/dist/sig/mod.js +2 -2
  88. package/dist/sig/owner.test.mjs +1 -1
  89. package/dist/sig/portable-collection.test.mjs +1 -1
  90. package/dist/sig/portable-key-id.test.d.mts +2 -0
  91. package/dist/sig/portable-key-id.test.mjs +67 -0
  92. package/dist/sig/portable-key.test.mjs +7 -7
  93. package/dist/sig/proof.test.mjs +3 -3
  94. package/dist/{temporal-oRKP803M.mjs → temporal-DKchpKyp.mjs} +1 -1
  95. package/dist/testing/kv.test.mjs +1 -1
  96. package/dist/testing/mod.d.mts +52 -0
  97. package/dist/utils/docloader.test.mjs +93 -2
  98. package/dist/utils/kv-cache.test.mjs +1 -1
  99. package/dist/utils/mod.cjs +1 -1
  100. package/dist/utils/mod.d.cts +1 -1
  101. package/dist/utils/mod.d.ts +1 -1
  102. package/dist/utils/mod.js +1 -1
  103. package/package.json +7 -7
@@ -1,13 +1,13 @@
1
1
  import { Temporal } from "temporal-polyfill";
2
2
  import "urlpattern-polyfill";
3
3
  globalThis.addEventListener = () => {};
4
- import { n as version, t as name } from "./deno-DU8dvnWk.mjs";
5
- import { n as getDurationMs, r as getFederationMetrics, s as measureSignatureKeyFetch } from "./metrics-jUDFfVgi.mjs";
4
+ import { n as version, t as name } from "./deno-EopndaNb.mjs";
5
+ import { n as getDurationMs, r as getFederationMetrics, s as measureSignatureKeyFetch } from "./metrics-zMe834ID.mjs";
6
6
  import { i as validateAcceptSignature, n as fulfillAcceptSignature, r as parseAcceptSignature } from "./accept-CPVqki78.mjs";
7
- import { f as parseKeyIdString } from "./portable-key-id-Bg259c4Z.mjs";
8
- import { a as promoteKeyOwnerEvidence } from "./key-owner-evidence-Bo3yaeL5.mjs";
9
- import { a as fetchKeyDetailed, c as validateCryptoKey, t as bypassKeyCacheReads } from "./key-gIhVZO3p.mjs";
10
- import { n as resolvePortableActorKey, r as verifyPortableGatewayKeyDocument } from "./portable-key-DyF5AnZo.mjs";
7
+ import { m as parseKeyIdString } from "./portable-key-id-Ca30-hOa.mjs";
8
+ import { a as promoteKeyOwnerEvidence } from "./key-owner-evidence-APB6NpSk.mjs";
9
+ import { a as fetchKeyDetailed, c as validateCryptoKey, t as bypassKeyCacheReads } from "./key-DyXf88bq.mjs";
10
+ import { n as resolvePortableActorKey, r as verifyPortableGatewayKeyDocument } from "./portable-key-Duue9Y2Y.mjs";
11
11
  import { getLogger } from "@logtape/logtape";
12
12
  import { CryptographicKey } from "@fedify/vocab";
13
13
  import { SpanStatusCode, trace } from "@opentelemetry/api";
@@ -215,16 +215,19 @@ function parseDraftSignature(signature) {
215
215
  * Lists the HTTP Signatures that a request claims to carry, without verifying
216
216
  * any of them. Signatures whose key IDs are not URLs are left out.
217
217
  * @param request The request.
218
+ * @param maxSignatures The maximum number of RFC 9421 signatures to consider,
219
+ * counted the same way as by {@link verifyRequest}, so
220
+ * that the signatures it ignores are not listed either.
218
221
  * @returns The signatures; RFC 9421 ones if the request has
219
222
  * a `Signature-Input` header, or a draft-cavage one otherwise.
220
223
  * @internal
221
224
  */
222
- function listRequestSignatures(request) {
225
+ function listRequestSignatures(request, maxSignatures = 3) {
223
226
  const signatureInput = request.headers.get("Signature-Input");
224
227
  if (signatureInput != null) {
225
228
  const signatures = [];
226
- const inputs = parseRfc9421SignatureInput(signatureInput);
227
- for (const [label, input] of Object.entries(inputs)) {
229
+ const inputs = Object.entries(parseRfc9421SignatureInput(signatureInput));
230
+ for (const [label, input] of inputs.slice(0, maxSignatures)) {
228
231
  const keyId = parseKeyId(input.keyId);
229
232
  if (keyId == null) continue;
230
233
  signatures.push({
@@ -352,6 +355,18 @@ async function signRequestRfc9421(request, privateKey, keyId, span, currentTime,
352
355
  body
353
356
  });
354
357
  }
358
+ /**
359
+ * Checks that a maximum number of signatures to verify is a positive safe
360
+ * integer or `Infinity`.
361
+ * @param value The value to check.
362
+ * @param name The name of the option, used in the error message.
363
+ * @throws {RangeError} Thrown if the value is not valid.
364
+ * @internal
365
+ */
366
+ function validateMaxSignatures(value, name) {
367
+ if (value === Infinity || Number.isSafeInteger(value) && value >= 1) return;
368
+ throw new RangeError(`${name} must be a positive integer or Infinity.`);
369
+ }
355
370
  const supportedHashAlgorithms = {
356
371
  "sha": "SHA-1",
357
372
  "sha-256": "SHA-256",
@@ -444,6 +459,8 @@ function recordVerificationResult(span, result) {
444
459
  * @param options Options for verifying the request.
445
460
  * @returns The public key of the verified signature, or `null` if the signature
446
461
  * could not be verified.
462
+ * @throws {RangeError} Thrown if {@link VerifyRequestOptions.maxSignatures}
463
+ * is not a positive integer or `Infinity`.
447
464
  */
448
465
  async function verifyRequest(request, options = {}) {
449
466
  const result = await verifyRequestDetailed(request, options);
@@ -456,9 +473,12 @@ async function verifyRequest(request, options = {}) {
456
473
  * @param request The request to verify.
457
474
  * @param options Options for verifying the request.
458
475
  * @returns The verified public key, or a structured verification failure.
476
+ * @throws {RangeError} Thrown if {@link VerifyRequestOptions.maxSignatures}
477
+ * is not a positive integer or `Infinity`.
459
478
  * @since 2.1.0
460
479
  */
461
480
  async function verifyRequestDetailed(request, options = {}) {
481
+ if (options.maxSignatures !== void 0) validateMaxSignatures(options.maxSignatures, "maxSignatures");
462
482
  return await (options.tracerProvider ?? trace.getTracerProvider()).getTracer(name, version).startActiveSpan("http_signatures.verify", async (span) => {
463
483
  if (span.isRecording()) {
464
484
  span.setAttribute(ATTR_HTTP_REQUEST_METHOD, request.method);
@@ -767,7 +787,100 @@ async function verifyRfc9421ContentDigest(digestHeader, body) {
767
787
  }
768
788
  return false;
769
789
  }
770
- async function verifyRequestRfc9421(request, span, metricsContext, { documentLoader, contextLoader, timeWindow, currentTime, keyCache, meterProvider, tracerProvider } = {}) {
790
+ /**
791
+ * Verifies one RFC 9421 signature of a request with a key.
792
+ * @param request The request, whose body may be consumed already.
793
+ * @param sigInput The parsed `Signature-Input` member of the signature.
794
+ * @param sigBytes The bytes of the signature.
795
+ * @param key The public key named by the signature.
796
+ * @param span The span to record the algorithm and the signature on.
797
+ * @returns Whether the signature is verified, and the algorithm of the
798
+ * signature if it is supported.
799
+ */
800
+ async function verifyRfc9421SignatureWithKey(request, sigInput, sigBytes, key, span) {
801
+ const logger = getLogger([
802
+ "fedify",
803
+ "sig",
804
+ "http"
805
+ ]);
806
+ let alg = sigInput.alg?.toLowerCase();
807
+ if (alg == null) {
808
+ if (key.publicKey.algorithm.name === "RSASSA-PKCS1-v1_5") alg = "hash" in key.publicKey.algorithm ? key.publicKey.algorithm.hash === "SHA-512" ? "rsa-v1_5-sha512" : "rsa-v1_5-sha256" : "rsa-v1_5-sha256";
809
+ else if (key.publicKey.algorithm.name === "RSA-PSS") alg = "rsa-pss-sha512";
810
+ else if (key.publicKey.algorithm.name === "ECDSA") alg = "namedCurve" in key.publicKey.algorithm && key.publicKey.algorithm.namedCurve === "P-256" ? "ecdsa-p256-sha256" : "ecdsa-p384-sha384";
811
+ else if (key.publicKey.algorithm.name === "Ed25519") alg = "ed25519";
812
+ }
813
+ if (alg) span?.setAttribute("http_signatures.algorithm", alg);
814
+ const algorithm = alg && rfc9421AlgorithmMap[alg];
815
+ if (!algorithm) {
816
+ logger.debug("Failed to verify; unsupported algorithm: {algorithm}", {
817
+ algorithm: sigInput.alg,
818
+ supported: Object.keys(rfc9421AlgorithmMap)
819
+ });
820
+ return {
821
+ verified: false,
822
+ mismatched: false
823
+ };
824
+ }
825
+ const metricAlgorithm = alg;
826
+ let signatureBase;
827
+ try {
828
+ signatureBase = createRfc9421SignatureBase(request, sigInput.components, sigInput.parameters);
829
+ } catch (error) {
830
+ logger.debug("Failed to create signature base for verification: {error}", {
831
+ error,
832
+ signatureInput: sigInput
833
+ });
834
+ return {
835
+ verified: false,
836
+ algorithm: metricAlgorithm,
837
+ mismatched: false
838
+ };
839
+ }
840
+ const signatureBaseBytes = new TextEncoder().encode(signatureBase);
841
+ span?.setAttribute("http_signatures.signature", encodeHex(sigBytes));
842
+ const algorithmName = typeof algorithm === "string" ? algorithm : algorithm.name;
843
+ if (key.publicKey.algorithm.name !== algorithmName) {
844
+ logger.debug("Failed to verify; key {keyId} is not a key for algorithm {algorithm}.", {
845
+ keyId: sigInput.keyId,
846
+ algorithm: alg
847
+ });
848
+ return {
849
+ verified: false,
850
+ algorithm: metricAlgorithm,
851
+ mismatched: true
852
+ };
853
+ }
854
+ let verified;
855
+ try {
856
+ verified = await crypto.subtle.verify(algorithm, key.publicKey, sigBytes.slice(), signatureBaseBytes);
857
+ } catch (error) {
858
+ logger.debug("Error during signature verification: {error}", {
859
+ error,
860
+ keyId: sigInput.keyId,
861
+ algorithm: sigInput.alg
862
+ });
863
+ return {
864
+ verified: false,
865
+ algorithm: metricAlgorithm,
866
+ mismatched: false
867
+ };
868
+ }
869
+ if (verified) return {
870
+ verified: true,
871
+ algorithm: metricAlgorithm
872
+ };
873
+ logger.debug("Failed to verify signature with key {keyId}; signature invalid.", {
874
+ keyId: sigInput.keyId,
875
+ signatureBase
876
+ });
877
+ return {
878
+ verified: false,
879
+ algorithm: metricAlgorithm,
880
+ mismatched: true
881
+ };
882
+ }
883
+ async function verifyRequestRfc9421(request, span, metricsContext, { documentLoader, contextLoader, timeWindow, currentTime, keyCache, maxSignatures = 3, meterProvider, tracerProvider } = {}) {
771
884
  const logger = getLogger([
772
885
  "fedify",
773
886
  "sig",
@@ -780,7 +893,6 @@ async function verifyRequestRfc9421(request, span, metricsContext, { documentLoa
780
893
  logger.error("Failed to verify; the request body is locked.", { url: request.url });
781
894
  return noSignatureResult();
782
895
  }
783
- const originalRequest = request;
784
896
  request = request.clone();
785
897
  const signatureInputHeader = request.headers.get("Signature-Input");
786
898
  if (!signatureInputHeader) {
@@ -800,6 +912,29 @@ async function verifyRequestRfc9421(request, span, metricsContext, { documentLoa
800
912
  logger.debug("Failed to verify; no valid signatures found in Signature-Input header.", { header: signatureInputHeader });
801
913
  return invalidSignatureResult(null);
802
914
  }
915
+ if (signatureNames.length > maxSignatures) {
916
+ logger.debug("Verifying only the first {maxSignatures} of the {count} signatures in the Signature-Input header; the rest are ignored.", {
917
+ maxSignatures,
918
+ count: signatureNames.length
919
+ });
920
+ signatureNames.splice(maxSignatures);
921
+ }
922
+ const keyLookups = /* @__PURE__ */ new Map();
923
+ const refreshedKeyIds = /* @__PURE__ */ new Set();
924
+ const lookUpKey = (keyId, keyCache) => measureSignatureKeyFetch(meterProvider, "http", () => fetchKeyDetailed(keyId, CryptographicKey, {
925
+ documentLoader,
926
+ contextLoader,
927
+ keyCache,
928
+ tracerProvider,
929
+ meterProvider,
930
+ portableKeyResolvers: createPortableKeyResolvers({
931
+ documentLoader,
932
+ contextLoader,
933
+ keyCache,
934
+ tracerProvider,
935
+ meterProvider
936
+ })
937
+ }));
803
938
  let failure = noSignatureResult();
804
939
  let failureAlgorithm;
805
940
  const setFailure = (result, algorithm) => {
@@ -873,94 +1008,40 @@ async function verifyRequestRfc9421(request, span, metricsContext, { documentLoa
873
1008
  setFailure(invalidSignatureResult(null));
874
1009
  continue;
875
1010
  }
876
- const { key, cached, fetchError } = await measureSignatureKeyFetch(meterProvider, "http", () => fetchKeyDetailed(keyId, CryptographicKey, {
877
- documentLoader,
878
- contextLoader,
879
- keyCache,
880
- tracerProvider,
881
- meterProvider,
882
- portableKeyResolvers: createPortableKeyResolvers({
883
- documentLoader,
884
- contextLoader,
885
- keyCache,
886
- tracerProvider,
887
- meterProvider
888
- })
889
- }));
890
- if (fetchError != null) {
891
- setFailure(keyFetchErrorResult(keyId, fetchError));
892
- continue;
1011
+ let lookup = keyLookups.get(keyId.href);
1012
+ if (lookup == null) {
1013
+ lookup = await lookUpKey(keyId, keyCache);
1014
+ keyLookups.set(keyId.href, lookup);
893
1015
  }
894
- if (!key) {
895
- logger.debug("Failed to fetch key: {keyId}", { keyId: sigInput.keyId });
896
- setFailure(invalidSignatureResult(keyId));
897
- continue;
898
- }
899
- let alg = sigInput.alg?.toLowerCase();
900
- if (alg == null) {
901
- if (key.publicKey.algorithm.name === "RSASSA-PKCS1-v1_5") alg = "hash" in key.publicKey.algorithm ? key.publicKey.algorithm.hash === "SHA-512" ? "rsa-v1_5-sha512" : "rsa-v1_5-sha256" : "rsa-v1_5-sha256";
902
- else if (key.publicKey.algorithm.name === "RSA-PSS") alg = "rsa-pss-sha512";
903
- else if (key.publicKey.algorithm.name === "ECDSA") alg = "namedCurve" in key.publicKey.algorithm && key.publicKey.algorithm.namedCurve === "P-256" ? "ecdsa-p256-sha256" : "ecdsa-p384-sha384";
904
- else if (key.publicKey.algorithm.name === "Ed25519") alg = "ed25519";
905
- }
906
- if (alg) span?.setAttribute("http_signatures.algorithm", alg);
907
- const algorithm = alg && rfc9421AlgorithmMap[alg];
908
- const candidateAlgorithm = algorithm ? alg : void 0;
909
- if (!algorithm) {
910
- logger.debug("Failed to verify; unsupported algorithm: {algorithm}", {
911
- algorithm: sigInput.alg,
912
- supported: Object.keys(rfc9421AlgorithmMap)
913
- });
914
- setFailure(invalidSignatureResult(keyId));
915
- continue;
916
- }
917
- let signatureBase;
918
- try {
919
- signatureBase = createRfc9421SignatureBase(request, sigInput.components, sigInput.parameters);
920
- } catch (error) {
921
- logger.debug("Failed to create signature base for verification: {error}", {
922
- error,
923
- signatureInput: sigInput
924
- });
925
- setFailure(invalidSignatureResult(keyId), candidateAlgorithm);
926
- continue;
927
- }
928
- const signatureBaseBytes = new TextEncoder().encode(signatureBase);
929
- span?.setAttribute("http_signatures.signature", encodeHex(sigBytes));
930
- try {
931
- if (await crypto.subtle.verify(algorithm, key.publicKey, sigBytes.slice(), signatureBaseBytes)) {
932
- metricsContext.algorithm = candidateAlgorithm;
1016
+ while (true) {
1017
+ const { key, cached, fetchError } = lookup;
1018
+ if (fetchError != null) {
1019
+ setFailure(keyFetchErrorResult(keyId, fetchError));
1020
+ break;
1021
+ }
1022
+ if (!key) {
1023
+ logger.debug("Failed to fetch key: {keyId}", { keyId: sigInput.keyId });
1024
+ setFailure(invalidSignatureResult(keyId));
1025
+ break;
1026
+ }
1027
+ const result = await verifyRfc9421SignatureWithKey(request, sigInput, sigBytes, key, span);
1028
+ if (result.verified) {
1029
+ metricsContext.algorithm = result.algorithm;
933
1030
  return {
934
1031
  verified: true,
935
1032
  key,
936
1033
  signatureLabel: sigName
937
1034
  };
938
- } else if (cached) {
1035
+ }
1036
+ if (result.mismatched && cached && !refreshedKeyIds.has(keyId.href)) {
939
1037
  logger.debug("Failed to verify with cached key {keyId}; retrying with fresh key...", { keyId: sigInput.keyId });
940
- return await verifyRequestRfc9421(originalRequest, span, metricsContext, {
941
- documentLoader,
942
- contextLoader,
943
- timeWindow,
944
- currentTime,
945
- keyCache: bypassKeyCacheReads(keyCache),
946
- spec: "rfc9421",
947
- meterProvider,
948
- tracerProvider
949
- });
950
- } else {
951
- logger.debug("Failed to verify signature with fetched key {keyId}; signature invalid.", {
952
- keyId: sigInput.keyId,
953
- signatureBase
954
- });
955
- setFailure(invalidSignatureResult(keyId), candidateAlgorithm);
1038
+ refreshedKeyIds.add(keyId.href);
1039
+ lookup = await lookUpKey(keyId, bypassKeyCacheReads(keyCache));
1040
+ keyLookups.set(keyId.href, lookup);
1041
+ continue;
956
1042
  }
957
- } catch (error) {
958
- logger.debug("Error during signature verification: {error}", {
959
- error,
960
- keyId: sigInput.keyId,
961
- algorithm: sigInput.alg
962
- });
963
- setFailure(invalidSignatureResult(keyId), candidateAlgorithm);
1043
+ setFailure(invalidSignatureResult(keyId), result.algorithm);
1044
+ break;
964
1045
  }
965
1046
  }
966
1047
  metricsContext.algorithm = failureAlgorithm;
@@ -1060,16 +1141,19 @@ async function doubleKnock(request, identity, options = {}) {
1060
1141
  }
1061
1142
  async function doubleKnockInternal(request, identity, options, redirected = 0, visited = /* @__PURE__ */ new Set()) {
1062
1143
  const { specDeterminer, log, tracerProvider, signal } = options;
1144
+ signal?.throwIfAborted();
1063
1145
  const maximumRedirection = options.maxRedirection ?? DEFAULT_MAX_REDIRECTION;
1064
1146
  visited.add(request.url);
1065
1147
  const origin = new URL(request.url).origin;
1066
1148
  const firstTrySpec = specDeterminer == null ? "rfc9421" : await specDeterminer.determineSpec(origin);
1149
+ signal?.throwIfAborted();
1067
1150
  const body = options.body !== void 0 ? options.body : request.method !== "GET" && request.method !== "HEAD" ? await request.clone().arrayBuffer() : null;
1068
1151
  let signedRequest = await signRequest(request, identity.privateKey, identity.keyId, {
1069
1152
  spec: firstTrySpec,
1070
1153
  tracerProvider,
1071
1154
  body
1072
1155
  });
1156
+ signal?.throwIfAborted();
1073
1157
  log?.(signedRequest);
1074
1158
  let response = await fetchDoubleKnockRequest(request, signedRequest, signal);
1075
1159
  if (response.status >= 300 && response.status < 400 && response.headers.has("Location")) {
@@ -1094,6 +1178,7 @@ async function doubleKnockInternal(request, identity, options, redirected = 0, v
1094
1178
  let fulfilled = false;
1095
1179
  let challengeRequest;
1096
1180
  for (const entry of entries) {
1181
+ signal?.throwIfAborted();
1097
1182
  const rfc9421 = fulfillAcceptSignature(entry, localKeyId, localAlg);
1098
1183
  if (rfc9421 == null) continue;
1099
1184
  logger.debug("Received Accept-Signature challenge; accumulating label {label} and components {components}.", {
@@ -1116,6 +1201,7 @@ async function doubleKnockInternal(request, identity, options, redirected = 0, v
1116
1201
  }
1117
1202
  }
1118
1203
  if (fulfilled && challengeRequest != null) {
1204
+ signal?.throwIfAborted();
1119
1205
  signedRequest = challengeRequest;
1120
1206
  log?.(signedRequest);
1121
1207
  response = await fetch(signedRequest, {
@@ -1140,11 +1226,13 @@ async function doubleKnockInternal(request, identity, options, redirected = 0, v
1140
1226
  status: response.status,
1141
1227
  statusText: response.statusText
1142
1228
  });
1229
+ signal?.throwIfAborted();
1143
1230
  signedRequest = await signRequest(request, identity.privateKey, identity.keyId, {
1144
1231
  spec,
1145
1232
  tracerProvider,
1146
1233
  body
1147
1234
  });
1235
+ signal?.throwIfAborted();
1148
1236
  log?.(signedRequest);
1149
1237
  response = await fetchDoubleKnockRequest(request, signedRequest, signal);
1150
1238
  if (response.status >= 300 && response.status < 400 && response.headers.has("Location")) {
@@ -1187,4 +1275,4 @@ function timingSafeEqual(a, b) {
1187
1275
  return result === 0;
1188
1276
  }
1189
1277
  //#endregion
1190
- export { listRequestSignatures as a, selectRequestSignature as c, verifyRequest as d, verifyRequestDetailed as f, formatRfc9421SignatureParameters as i, signRequest as l, doubleKnock as n, parseRfc9421Signature as o, formatRfc9421Signature as r, parseRfc9421SignatureInput as s, createRfc9421SignatureBase as t, timingSafeEqual as u };
1278
+ export { listRequestSignatures as a, selectRequestSignature as c, validateMaxSignatures as d, verifyRequest as f, formatRfc9421SignatureParameters as i, signRequest as l, doubleKnock as n, parseRfc9421Signature as o, verifyRequestDetailed as p, formatRfc9421Signature as r, parseRfc9421SignatureInput as s, createRfc9421SignatureBase as t, timingSafeEqual as u };
@@ -1,10 +1,10 @@
1
1
  import { Temporal } from "temporal-polyfill";
2
2
  import "urlpattern-polyfill";
3
3
  globalThis.addEventListener = () => {};
4
- import { n as version, t as name } from "./deno-DU8dvnWk.mjs";
5
- import { _ as recordKeyLookup, n as getDurationMs, t as classifyFetchError } from "./metrics-jUDFfVgi.mjs";
6
- import { c as isPortableId, f as parseKeyIdString, o as isCompatibleKeyId, s as isPortableActorDocument, u as isPortableUri } from "./portable-key-id-Bg259c4Z.mjs";
7
- import { n as attachKeyOwnerEvidence, t as MAX_PORTABLE_KEY_TTL } from "./key-owner-evidence-Bo3yaeL5.mjs";
4
+ import { n as version, t as name } from "./deno-EopndaNb.mjs";
5
+ import { _ as recordKeyLookup, n as getDurationMs, t as classifyFetchError } from "./metrics-zMe834ID.mjs";
6
+ import { c as isPortableActorDocument, d as isPortableUri, l as isPortableId, m as parseKeyIdString, s as isCompatibleKeyId } from "./portable-key-id-Ca30-hOa.mjs";
7
+ import { n as attachKeyOwnerEvidence, t as MAX_PORTABLE_KEY_TTL } from "./key-owner-evidence-APB6NpSk.mjs";
8
8
  import { getLogger } from "@logtape/logtape";
9
9
  import { CryptographicKey, Multikey, Object as Object$1, isActor } from "@fedify/vocab";
10
10
  import { SpanKind, SpanStatusCode, trace } from "@opentelemetry/api";
@@ -1,7 +1,7 @@
1
1
  import { Temporal } from "temporal-polyfill";
2
2
  import "urlpattern-polyfill";
3
3
  globalThis.addEventListener = () => {};
4
- import { i as getRawCanonicalPortableId, s as isPortableActorDocument, t as getCanonicalPortableId } from "./portable-key-id-Bg259c4Z.mjs";
4
+ import { a as getRawCanonicalPortableId, c as isPortableActorDocument, n as getCanonicalPortableId } from "./portable-key-id-Ca30-hOa.mjs";
5
5
  import { CryptographicKey, Object as Object$1 } from "@fedify/vocab";
6
6
  //#region src/sig/key-owner-evidence.ts
7
7
  /**
@@ -1,7 +1,7 @@
1
1
  import { Temporal } from "temporal-polyfill";
2
2
  import "urlpattern-polyfill";
3
3
  globalThis.addEventListener = () => {};
4
- import { t as MAX_PORTABLE_KEY_TTL } from "./key-owner-evidence-Bo3yaeL5.mjs";
4
+ import { t as MAX_PORTABLE_KEY_TTL } from "./key-owner-evidence-APB6NpSk.mjs";
5
5
  import { CryptographicKey, Multikey } from "@fedify/vocab";
6
6
  //#region src/federation/keycache.ts
7
7
  const KEY_CACHE_GENERATION = "2";
@@ -1,8 +1,8 @@
1
1
  import { Temporal } from "temporal-polyfill";
2
2
  import { URLPattern } from "urlpattern-polyfill";
3
- import { W as validateCryptoKey, at as recordDocumentCache, t as doubleKnock } from "./http-BGpexPO7.js";
3
+ import { G as validateCryptoKey, ot as recordDocumentCache, t as doubleKnock } from "./http-BqN5kyUf.js";
4
4
  import { getLogger } from "@logtape/logtape";
5
- import { FetchError, UrlError, createActivityPubRequest, getRemoteDocument, logRequest, preloadedContexts, validatePublicUrl } from "@fedify/vocab-runtime";
5
+ import { FetchError, UrlError, createActivityPubRequest, getRemoteDocument, logRequest, preloadedContexts, resolveDocumentLoaderTimeout, validatePublicUrl, withDocumentLoaderTimeout } from "@fedify/vocab-runtime";
6
6
  import { curry } from "es-toolkit";
7
7
  //#region src/utils/docloader.ts
8
8
  const logger$1 = getLogger([
@@ -17,15 +17,19 @@ const DEFAULT_MAX_REDIRECTION = 20;
17
17
  * the fetched documents.
18
18
  * At most 20 HTTP redirects and alternate document links are followed in total
19
19
  * per call. Revisiting a URL within that chain throws a {@link FetchError}.
20
+ * Each call, including its double-knocking retries, times out after
21
+ * 10 seconds by default; see {@link DocumentLoaderFactoryOptions.timeout}.
20
22
  * @param identity The identity to get the document loader for.
21
23
  * The actor's key pair.
22
24
  * @param options The options for the document loader.
23
25
  * @returns The authenticated document loader.
24
26
  * @throws {TypeError} If the key is invalid or unsupported.
27
+ * @throws {RangeError} If the `timeout` option is invalid.
25
28
  * @since 0.4.0
26
29
  */
27
- function getAuthenticatedDocumentLoader(identity, { allowPrivateAddress, maxRedirection, userAgent, specDeterminer, tracerProvider } = {}) {
30
+ function getAuthenticatedDocumentLoader(identity, { allowPrivateAddress, maxRedirection, userAgent, specDeterminer, timeout, tracerProvider } = {}) {
28
31
  validateCryptoKey(identity.privateKey);
32
+ const resolvedTimeout = resolveDocumentLoaderTimeout(timeout);
29
33
  async function load(url, options, redirected = 0, visited = /* @__PURE__ */ new Set()) {
30
34
  options?.signal?.throwIfAborted();
31
35
  let currentUrl = new URL(url).href;
@@ -51,27 +55,31 @@ function getAuthenticatedDocumentLoader(identity, { allowPrivateAddress, maxRedi
51
55
  currentUrl = nextUrl;
52
56
  }
53
57
  });
58
+ options?.signal?.throwIfAborted();
54
59
  return getRemoteDocument(currentUrl, response, (alternateUrl) => {
55
60
  follow(alternateUrl);
56
61
  return load(alternateUrl, options, redirected, visited);
57
62
  }, options);
58
63
  }
59
64
  async function validateUrl(url, options) {
60
- if (!allowPrivateAddress) try {
61
- await validatePublicUrl(url);
62
- } catch (error) {
63
- if (error instanceof UrlError) if (error.reason === "dns") logger$1.debug("DNS lookup failed for {url}", {
64
- url,
65
- error
66
- });
67
- else logger$1[options?.suppressError ? "warn" : "error"]("Disallowed private URL: {url}", {
68
- url,
69
- error
70
- });
71
- throw error;
65
+ if (!allowPrivateAddress) {
66
+ try {
67
+ await validatePublicUrl(url);
68
+ } catch (error) {
69
+ if (error instanceof UrlError) if (error.reason === "dns") logger$1.debug("DNS lookup failed for {url}", {
70
+ url,
71
+ error
72
+ });
73
+ else logger$1[options?.suppressError ? "warn" : "error"]("Disallowed private URL: {url}", {
74
+ url,
75
+ error
76
+ });
77
+ throw error;
78
+ }
79
+ options?.signal?.throwIfAborted();
72
80
  }
73
81
  }
74
- return (url, options) => load(url, options);
82
+ return withDocumentLoaderTimeout((url, options) => load(url, options), resolvedTimeout);
75
83
  }
76
84
  //#endregion
77
85
  //#region src/utils/kv-cache.ts
@@ -1,7 +1,7 @@
1
1
  import { Temporal } from "temporal-polyfill";
2
2
  import { URLPattern } from "urlpattern-polyfill";
3
3
  globalThis.addEventListener = () => {};
4
- import { p as recordDocumentCache } from "./metrics-jUDFfVgi.mjs";
4
+ import { p as recordDocumentCache } from "./metrics-zMe834ID.mjs";
5
5
  import { getLogger } from "@logtape/logtape";
6
6
  import { preloadedContexts } from "@fedify/vocab-runtime";
7
7
  //#region src/utils/kv-cache.ts
@@ -1,5 +1,5 @@
1
1
  const { URLPattern } = require("urlpattern-polyfill");
2
- const require_http = require("./http-5CJBdU8n.cjs");
2
+ const require_http = require("./http-CCr2_ipG.cjs");
3
3
  let _logtape_logtape = require("@logtape/logtape");
4
4
  let _fedify_vocab_runtime = require("@fedify/vocab-runtime");
5
5
  let es_toolkit = require("es-toolkit");
@@ -16,15 +16,19 @@ const DEFAULT_MAX_REDIRECTION = 20;
16
16
  * the fetched documents.
17
17
  * At most 20 HTTP redirects and alternate document links are followed in total
18
18
  * per call. Revisiting a URL within that chain throws a {@link FetchError}.
19
+ * Each call, including its double-knocking retries, times out after
20
+ * 10 seconds by default; see {@link DocumentLoaderFactoryOptions.timeout}.
19
21
  * @param identity The identity to get the document loader for.
20
22
  * The actor's key pair.
21
23
  * @param options The options for the document loader.
22
24
  * @returns The authenticated document loader.
23
25
  * @throws {TypeError} If the key is invalid or unsupported.
26
+ * @throws {RangeError} If the `timeout` option is invalid.
24
27
  * @since 0.4.0
25
28
  */
26
- function getAuthenticatedDocumentLoader(identity, { allowPrivateAddress, maxRedirection, userAgent, specDeterminer, tracerProvider } = {}) {
29
+ function getAuthenticatedDocumentLoader(identity, { allowPrivateAddress, maxRedirection, userAgent, specDeterminer, timeout, tracerProvider } = {}) {
27
30
  require_http.validateCryptoKey(identity.privateKey);
31
+ const resolvedTimeout = (0, _fedify_vocab_runtime.resolveDocumentLoaderTimeout)(timeout);
28
32
  async function load(url, options, redirected = 0, visited = /* @__PURE__ */ new Set()) {
29
33
  options?.signal?.throwIfAborted();
30
34
  let currentUrl = new URL(url).href;
@@ -50,27 +54,31 @@ function getAuthenticatedDocumentLoader(identity, { allowPrivateAddress, maxRedi
50
54
  currentUrl = nextUrl;
51
55
  }
52
56
  });
57
+ options?.signal?.throwIfAborted();
53
58
  return (0, _fedify_vocab_runtime.getRemoteDocument)(currentUrl, response, (alternateUrl) => {
54
59
  follow(alternateUrl);
55
60
  return load(alternateUrl, options, redirected, visited);
56
61
  }, options);
57
62
  }
58
63
  async function validateUrl(url, options) {
59
- if (!allowPrivateAddress) try {
60
- await (0, _fedify_vocab_runtime.validatePublicUrl)(url);
61
- } catch (error) {
62
- if (error instanceof _fedify_vocab_runtime.UrlError) if (error.reason === "dns") logger$1.debug("DNS lookup failed for {url}", {
63
- url,
64
- error
65
- });
66
- else logger$1[options?.suppressError ? "warn" : "error"]("Disallowed private URL: {url}", {
67
- url,
68
- error
69
- });
70
- throw error;
64
+ if (!allowPrivateAddress) {
65
+ try {
66
+ await (0, _fedify_vocab_runtime.validatePublicUrl)(url);
67
+ } catch (error) {
68
+ if (error instanceof _fedify_vocab_runtime.UrlError) if (error.reason === "dns") logger$1.debug("DNS lookup failed for {url}", {
69
+ url,
70
+ error
71
+ });
72
+ else logger$1[options?.suppressError ? "warn" : "error"]("Disallowed private URL: {url}", {
73
+ url,
74
+ error
75
+ });
76
+ throw error;
77
+ }
78
+ options?.signal?.throwIfAborted();
71
79
  }
72
80
  }
73
- return (url, options) => load(url, options);
81
+ return (0, _fedify_vocab_runtime.withDocumentLoaderTimeout)((url, options) => load(url, options), resolvedTimeout);
74
82
  }
75
83
  //#endregion
76
84
  //#region src/utils/kv-cache.ts
@@ -1,9 +1,9 @@
1
1
  import "temporal-polyfill";
2
2
  import "urlpattern-polyfill";
3
3
  globalThis.addEventListener = () => {};
4
- import { n as version, t as name } from "./deno-DU8dvnWk.mjs";
5
- import { n as getDurationMs, r as getFederationMetrics, s as measureSignatureKeyFetch } from "./metrics-jUDFfVgi.mjs";
6
- import { c as validateCryptoKey, i as fetchKey, t as bypassKeyCacheReads } from "./key-gIhVZO3p.mjs";
4
+ import { n as version, t as name } from "./deno-EopndaNb.mjs";
5
+ import { n as getDurationMs, r as getFederationMetrics, s as measureSignatureKeyFetch } from "./metrics-zMe834ID.mjs";
6
+ import { c as validateCryptoKey, i as fetchKey, t as bypassKeyCacheReads } from "./key-DyXf88bq.mjs";
7
7
  import { getLogger } from "@logtape/logtape";
8
8
  import { Activity, CryptographicKey, Object as Object$1, getTypeId } from "@fedify/vocab";
9
9
  import { SpanStatusCode, trace } from "@opentelemetry/api";
@@ -1,7 +1,7 @@
1
1
  import "temporal-polyfill";
2
2
  import "urlpattern-polyfill";
3
3
  globalThis.addEventListener = () => {};
4
- import { n as version, t as name } from "./deno-DU8dvnWk.mjs";
4
+ import { n as version, t as name } from "./deno-EopndaNb.mjs";
5
5
  import { metrics } from "@opentelemetry/api";
6
6
  import { FetchError } from "@fedify/vocab-runtime";
7
7
  //#region src/federation/metrics.ts