@fedify/fedify 2.4.0-dev.2164 → 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-BqRmnx7p.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-CxDBdtNz.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-CizGaYeG.mjs → deno-EopndaNb.mjs} +1 -1
  10. package/dist/{docloader-BBFAItNB.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-DFCnp94y.cjs → federation-BSS2dUO7.cjs} +38 -21
  41. package/dist/{http-Baovr6xq.js → http-BqN5kyUf.js} +426 -141
  42. package/dist/{http-DC1cqAQ2.d.cts → http-BrwKINQn.d.cts} +28 -0
  43. package/dist/{http-R4g1McwE.cjs → http-CCr2_ipG.cjs} +443 -140
  44. package/dist/{http-Cok5-vIC.d.ts → http-DGx0KOws.d.ts} +28 -0
  45. package/dist/{http-DRH0n8TD.mjs → http-Y1ObT1A8.mjs} +180 -92
  46. package/dist/{key-D5awqp7u.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-CZiBDLPi.js → kv-cache-C02XitwK.js} +24 -16
  50. package/dist/{kv-cache-C7RWL7hz.mjs → kv-cache-C8YjNAi_.mjs} +1 -1
  51. package/dist/{kv-cache-Ct-KWRta.cjs → kv-cache-DUXluAc0.cjs} +23 -15
  52. package/dist/{ld-CyliEU48.mjs → ld-oQj6j_hP.mjs} +3 -3
  53. package/dist/{metrics-CFBEehTc.mjs → metrics-zMe834ID.mjs} +1 -1
  54. package/dist/{middleware-B99JjvFn.mjs → middleware-Bdu2-OPD.mjs} +37 -25
  55. package/dist/{middleware-CdpTFsa2.js → middleware-CaR2MRWY.js} +39 -22
  56. package/dist/{middleware-BmqEWsWP.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-DKdtsrEq.mjs → owner-Dem4fbwc.mjs} +5 -5
  69. package/dist/{portable-collection-uv4CuQXm.js → portable-collection-BEqLrMW3.js} +1 -1
  70. package/dist/{portable-collection-Cb5cdPIU.mjs → portable-collection-BL6aZlQ0.mjs} +2 -2
  71. package/dist/{portable-collection-UajqDrSL.cjs → portable-collection-DJe_bk50.cjs} +1 -1
  72. package/dist/{portable-key-Bv1RdQ0n.mjs → portable-key-Duue9Y2Y.mjs} +209 -56
  73. package/dist/{portable-key-id-Bg259c4Z.mjs → portable-key-id-Ca30-hOa.mjs} +45 -1
  74. package/dist/{proof-CRTPWbVh.mjs → proof-C9115DX_.mjs} +5 -5
  75. package/dist/{send-2dULlZc8.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 +253 -7
  93. package/dist/sig/proof.test.mjs +3 -3
  94. package/dist/{temporal-Dhsk6aUT.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 +6 -6
@@ -6,7 +6,7 @@ import { t as assertFalse } from "../assert_false-CPdRnc3E.mjs";
6
6
  import "../std__assert-DvMDRPes.mjs";
7
7
  import { t as assert } from "../assert-D1Eafmd_.mjs";
8
8
  import { o as rsaPublicKey1, s as rsaPublicKey2 } from "../keys-DIJNHW-Y.mjs";
9
- import { n as getKeyOwner, t as doesActorOwnKey } from "../owner-DKdtsrEq.mjs";
9
+ import { n as getKeyOwner, t as doesActorOwnKey } from "../owner-Dem4fbwc.mjs";
10
10
  import { Create, CryptographicKey, lookupObject } from "@fedify/vocab";
11
11
  import { createTestTracerProvider, mockDocumentLoader, test } from "@fedify/fixture";
12
12
  //#region src/sig/owner.test.ts
@@ -2,7 +2,7 @@ import "temporal-polyfill";
2
2
  import "urlpattern-polyfill";
3
3
  globalThis.addEventListener = () => {};
4
4
  import { n as ed25519PrivateKey, r as ed25519PublicKey } from "../keys-DIJNHW-Y.mjs";
5
- import { t as verifyPortableObject } from "../portable-collection-Cb5cdPIU.mjs";
5
+ import { t as verifyPortableObject } from "../portable-collection-BL6aZlQ0.mjs";
6
6
  import { Collection, CollectionPage, Note, OrderedCollection, Person, traverseCollection } from "@fedify/vocab";
7
7
  import { test } from "@fedify/fixture";
8
8
  import { FetchError, encodeMultibase, exportDidKey, preloadedContexts } from "@fedify/vocab-runtime";
@@ -0,0 +1,2 @@
1
+ import { Temporal } from "temporal-polyfill";
2
+ import { URLPattern } from "urlpattern-polyfill";
@@ -0,0 +1,67 @@
1
+ import "temporal-polyfill";
2
+ import "urlpattern-polyfill";
3
+ globalThis.addEventListener = () => {};
4
+ import { t as assertEquals } from "../assert_equals-Z6IcEbWU.mjs";
5
+ import { t as assertFalse } from "../assert_false-CPdRnc3E.mjs";
6
+ import "../std__assert-DvMDRPes.mjs";
7
+ import { t as assert } from "../assert-D1Eafmd_.mjs";
8
+ import { f as isSameObjectId, t as getAuthenticationOrigin } from "../portable-key-id-Ca30-hOa.mjs";
9
+ import { r as ed25519PublicKey } from "../keys-DIJNHW-Y.mjs";
10
+ import { test } from "@fedify/fixture";
11
+ import { exportDidKey, parseIri } from "@fedify/vocab-runtime";
12
+ //#region src/sig/portable-key-id.test.ts
13
+ const did = await exportDidKey(ed25519PublicKey.publicKey);
14
+ const otherDid = await exportDidKey((await crypto.subtle.generateKey("Ed25519", true, ["sign", "verify"])).publicKey);
15
+ test("getAuthenticationOrigin()", () => {
16
+ for (const id of [
17
+ parseIri(`ap://${did}/actor`),
18
+ parseIri(`ap+ef61://${did}/actor`),
19
+ parseIri(`ap+ef61://${encodeURIComponent(did)}/actor#main-key`),
20
+ parseIri(`ap://${did}/actor?@gateway=https%3A%2F%2Fgw.example`),
21
+ new URL(`https://gw.example/.well-known/apgateway/${did}/actor`),
22
+ new URL(`https://gw2.example/.well-known/apgateway/${did}/objects/1`),
23
+ new URL(`${did}#${did.slice(8)}`)
24
+ ]) assertEquals(getAuthenticationOrigin(id), did, id.href);
25
+ assertEquals(getAuthenticationOrigin(parseIri(`ap+ef61://${otherDid}/actor`)), otherDid);
26
+ assertEquals(getAuthenticationOrigin(new URL("https://example.com:443/users/alice")), "https://example.com");
27
+ assertEquals(getAuthenticationOrigin(new URL("http://example.com:8080/users/alice")), "http://example.com:8080");
28
+ for (const id of [
29
+ new URL(`https://gw.example/.well-known/apgateway/${did}/actor?@gateway=https%3A%2F%2Fgw.example`),
30
+ new URL("https://gw.example/.well-known/apgateway/did:key:invalid!/a"),
31
+ new URL(`https://gw.example/.well-known/apgateway/${did}`),
32
+ new URL(`https://gw.example/.well-known/apgateway/${did}/actor%ZZ`),
33
+ new URL("urn:uuid:3d6f4c1e-2f8b-4d1a-9c6e-7b8a9d0e1f2a"),
34
+ new URL("tag:example.com,2026:actor")
35
+ ]) assertEquals(getAuthenticationOrigin(id), null, id.href);
36
+ });
37
+ test("isSameObjectId()", () => {
38
+ const portable = parseIri(`ap+ef61://${did}/activities/1`);
39
+ for (const id of [
40
+ parseIri(`ap://${did}/activities/1`),
41
+ parseIri(`ap+ef61://${encodeURIComponent(did)}/activities/1`),
42
+ parseIri(`ap://${did}/activities/1?@gateway=https%3A%2F%2Fgw.example`),
43
+ new URL(`https://gw.example/.well-known/apgateway/${did}/activities/1`),
44
+ new URL(`https://gw2.example/.well-known/apgateway/${did}/activities/1`)
45
+ ]) {
46
+ assert(isSameObjectId(portable, id), id.href);
47
+ assert(isSameObjectId(id, portable), id.href);
48
+ }
49
+ for (const id of [
50
+ parseIri(`ap+ef61://${otherDid}/activities/1`),
51
+ parseIri(`ap+ef61://${did}/activities/2`),
52
+ parseIri(`ap+ef61://${did}/activities/1#fragment`),
53
+ new URL(`https://gw.example/.well-known/apgateway/${did}/activities/1%ZZ`),
54
+ new URL("https://gw.example/activities/1")
55
+ ]) {
56
+ assertFalse(isSameObjectId(portable, id), id.href);
57
+ assertFalse(isSameObjectId(id, portable), id.href);
58
+ }
59
+ const malformed = new URL(`https://gw.example/.well-known/apgateway/${did}/activities/1%ZZ`);
60
+ assertFalse(isSameObjectId(malformed, malformed));
61
+ const http = new URL("https://example.com/activities/1");
62
+ assert(isSameObjectId(http, new URL("https://example.com/activities/1")));
63
+ assertFalse(isSameObjectId(http, new URL("https://example.com/activities/1#a")));
64
+ assertFalse(isSameObjectId(http, new URL("https://example.com/activities/1?a")));
65
+ });
66
+ //#endregion
67
+ export {};
@@ -2,13 +2,14 @@ import { Temporal } from "temporal-polyfill";
2
2
  import "urlpattern-polyfill";
3
3
  globalThis.addEventListener = () => {};
4
4
  import { t as MemoryKvStore } from "../kv-DCHkaJ9-.mjs";
5
- import { i as fetchKey } from "../key-D5awqp7u.mjs";
6
- import { h as verifyJsonLd, p as signJsonLd } from "../ld-CyliEU48.mjs";
7
- import { d as verifyProof, r as createProof } from "../proof-CRTPWbVh.mjs";
8
- import { d as verifyRequest, f as verifyRequestDetailed, l as signRequest } from "../http-DRH0n8TD.mjs";
5
+ import { a as fetchKeyDetailed, i as fetchKey } from "../key-DyXf88bq.mjs";
6
+ import { h as verifyJsonLd, p as signJsonLd } from "../ld-oQj6j_hP.mjs";
7
+ import { d as verifyProof, r as createProof } from "../proof-C9115DX_.mjs";
8
+ import { n as resolvePortableActorKey } from "../portable-key-Duue9Y2Y.mjs";
9
+ import { f as verifyRequest, l as signRequest, p as verifyRequestDetailed } from "../http-Y1ObT1A8.mjs";
9
10
  import { a as rsaPrivateKey3, c as rsaPublicKey3, i as rsaPrivateKey2, n as ed25519PrivateKey, r as ed25519PublicKey, s as rsaPublicKey2 } from "../keys-DIJNHW-Y.mjs";
10
- import { n as getKeyOwner, t as doesActorOwnKey } from "../owner-DKdtsrEq.mjs";
11
- import { t as KvKeyCache } from "../keycache-PiOM9pM0.mjs";
11
+ import { n as getKeyOwner, t as doesActorOwnKey } from "../owner-Dem4fbwc.mjs";
12
+ import { t as KvKeyCache } from "../keycache-Da8A8mZa.mjs";
12
13
  import { Create, CryptographicKey, Multikey, Note, Person } from "@fedify/vocab";
13
14
  import { test } from "@fedify/fixture";
14
15
  import { FetchError, encodeMultibase, exportDidKey, exportSpki, parseIri, preloadedContexts } from "@fedify/vocab-runtime";
@@ -965,7 +966,21 @@ test("verifyRequest() tries the gateways of ap: key IDs in order", async () => {
965
966
  documentLoader: many,
966
967
  contextLoader
967
968
  })).verified);
968
- deepStrictEqual(many.fetched, gateways.slice(0, 5).map((g) => compatibleId(g)));
969
+ deepStrictEqual(many.fetched, gateways.slice(0, 3).map((g) => compatibleId(g)));
970
+ for (const [index, expected] of [[2, true], [3, false]]) {
971
+ const documentLoader = createLoader({ [compatibleId(gateways[index])]: signed });
972
+ strictEqual(await verifyRequest(await signedRequest(void 0, hinted(...gateways)), {
973
+ documentLoader,
974
+ contextLoader
975
+ }) != null, expected, `gateway ${index + 1}`);
976
+ deepStrictEqual(documentLoader.fetched, gateways.slice(0, 3).map((g) => compatibleId(g)), `gateway ${index + 1}`);
977
+ }
978
+ const sparse = createLoader({ [compatibleId(gateways[2])]: signed });
979
+ ok(await verifyRequest(await signedRequest(void 0, hinted("not a gateway", gateways[0], gateways[0].toUpperCase(), "https://gw.example/path", gateways[1], gateways[2])), {
980
+ documentLoader: sparse,
981
+ contextLoader
982
+ }) != null);
983
+ deepStrictEqual(sparse.fetched, gateways.slice(0, 3).map((g) => compatibleId(g)));
969
984
  });
970
985
  function failingLoader(responses) {
971
986
  const fetched = [];
@@ -1189,5 +1204,236 @@ test("verifyRequest() does not cache keys at ap: key IDs it could not process",
1189
1204
  strictEqual((await verifyRequest(await signedRequest(void 0, hintedKeyId), options))?.ownerId?.href, parseIri(actorId).href);
1190
1205
  strictEqual(documentLoader.fetched.length, 2);
1191
1206
  });
1207
+ /**
1208
+ * A loader that serves the given responses, and never responds for the URLs
1209
+ * in `hanging`. A hanging response is given up when its signal aborts,
1210
+ * unless `ignoreSignal` is set.
1211
+ */
1212
+ function controlledLoader(responses, hanging, { ignoreSignal = false, delays = {} } = {}) {
1213
+ const fetched = [];
1214
+ const signals = [];
1215
+ const respond = failingLoader(responses);
1216
+ const loader = async (url, options) => {
1217
+ fetched.push(url);
1218
+ signals.push(options?.signal);
1219
+ const signal = ignoreSignal ? void 0 : options?.signal;
1220
+ const delay = delays[url];
1221
+ if (delay != null) await new Promise((resolve) => setTimeout(resolve, delay));
1222
+ if (hanging.includes(url)) return await new Promise((_, reject) => {
1223
+ signal?.addEventListener("abort", () => reject(signal.reason));
1224
+ });
1225
+ return await respond(url);
1226
+ };
1227
+ return Object.assign(loader, {
1228
+ fetched,
1229
+ signals
1230
+ });
1231
+ }
1232
+ function isTimeoutError(error) {
1233
+ return error instanceof DOMException && error.name === "TimeoutError";
1234
+ }
1235
+ test("resolvePortableActorKey() gives up gateways that do not respond in time", async () => {
1236
+ const signed = await sign(await apActorJson());
1237
+ for (const ignoreSignal of [false, true]) {
1238
+ const documentLoader = controlledLoader({ [compatibleId(gw2)]: signed }, [compatibleId(gw1)], { ignoreSignal });
1239
+ const resolution = await resolvePortableActorKey(hinted(gw1, gw2), {
1240
+ documentLoader,
1241
+ contextLoader,
1242
+ lookupTimeout: 50
1243
+ });
1244
+ strictEqual(resolution.type, "unavailable", `ignoreSignal: ${ignoreSignal}`);
1245
+ if (resolution.type === "unavailable") strictEqual(resolution.cacheable, false);
1246
+ deepStrictEqual(documentLoader.fetched, [compatibleId(gw1)]);
1247
+ const [signal] = documentLoader.signals;
1248
+ ok(signal?.aborted);
1249
+ ok(isTimeoutError(signal.reason));
1250
+ }
1251
+ });
1252
+ test("resolvePortableActorKey() caches timeouts only as failures to fetch", async () => {
1253
+ const invalid = await apActorJson();
1254
+ const cases = [
1255
+ {
1256
+ name: "the only gateway times out",
1257
+ responses: {},
1258
+ hanging: [compatibleId(gw1)],
1259
+ hints: [gw1],
1260
+ cacheable: true
1261
+ },
1262
+ {
1263
+ name: "a gateway responds with 404, and the last one times out",
1264
+ responses: { [compatibleId(gw1)]: 404 },
1265
+ hanging: [compatibleId(gw2)],
1266
+ hints: [gw1, gw2],
1267
+ cacheable: true
1268
+ },
1269
+ {
1270
+ name: "a gateway serves an invalid document, and the last one times out",
1271
+ responses: { [compatibleId(gw1)]: invalid },
1272
+ hanging: [compatibleId(gw2)],
1273
+ hints: [gw1, gw2],
1274
+ cacheable: false
1275
+ },
1276
+ {
1277
+ name: "the document loader cannot resolve the ap: URI in time",
1278
+ responses: {},
1279
+ hanging: [actorId],
1280
+ hints: [],
1281
+ cacheable: true
1282
+ }
1283
+ ];
1284
+ for (const { name, responses, hanging, hints, cacheable } of cases) {
1285
+ const documentLoader = controlledLoader(responses, hanging);
1286
+ const resolution = await resolvePortableActorKey(hinted(...hints), {
1287
+ documentLoader,
1288
+ contextLoader,
1289
+ lookupTimeout: 50
1290
+ });
1291
+ strictEqual(resolution.type, "unavailable", name);
1292
+ if (resolution.type !== "unavailable") continue;
1293
+ strictEqual(resolution.cacheable, cacheable, name);
1294
+ ok(!(resolution.error instanceof FetchError), name);
1295
+ if (hanging.length === 1 && hints.length < 2) ok(isTimeoutError(resolution.error), name);
1296
+ }
1297
+ });
1298
+ test("resolvePortableActorKey() shares one timeout among all gateways", async () => {
1299
+ const documentLoader = controlledLoader({ [compatibleId(gw1)]: 404 }, [compatibleId(gw2)], { delays: { [compatibleId(gw1)]: 250 } });
1300
+ const started = Date.now();
1301
+ const resolution = await resolvePortableActorKey(hinted(gw1, gw2), {
1302
+ documentLoader,
1303
+ contextLoader,
1304
+ lookupTimeout: 300
1305
+ });
1306
+ const elapsed = Date.now() - started;
1307
+ ok(elapsed < 500, `took ${elapsed} ms`);
1308
+ strictEqual(resolution.type, "unavailable");
1309
+ deepStrictEqual(documentLoader.fetched, [compatibleId(gw1), compatibleId(gw2)]);
1310
+ ok(documentLoader.signals[1]?.aborted);
1311
+ });
1312
+ test("resolvePortableActorKey() bounds loading the contexts of actor documents", async () => {
1313
+ const documentLoader = controlledLoader({ [compatibleId(gw1)]: await sign(await apActorJson()) }, []);
1314
+ const hangingContextLoader = (_url, options) => new Promise((_, reject) => {
1315
+ options?.signal?.addEventListener("abort", () => reject(options.signal.reason));
1316
+ });
1317
+ const resolution = await resolvePortableActorKey(hinted(gw1, gw2), {
1318
+ documentLoader,
1319
+ contextLoader: hangingContextLoader,
1320
+ lookupTimeout: 50
1321
+ });
1322
+ strictEqual(resolution.type, "unavailable");
1323
+ if (resolution.type === "unavailable") strictEqual(resolution.cacheable, false);
1324
+ deepStrictEqual(documentLoader.fetched, [compatibleId(gw1)]);
1325
+ });
1326
+ test("resolvePortableActorKey() ignores what is resolved after the timeout", async () => {
1327
+ let verifiedLate = () => {};
1328
+ const late = new Promise((resolve) => verifiedLate = resolve);
1329
+ let lookupDone = () => {};
1330
+ const done = new Promise((resolve) => lookupDone = resolve);
1331
+ const slowKeyCache = {
1332
+ async get() {
1333
+ await done;
1334
+ },
1335
+ set() {
1336
+ verifiedLate();
1337
+ return Promise.resolve();
1338
+ }
1339
+ };
1340
+ const documentLoader = controlledLoader({ [compatibleId(gw1)]: await sign(await apActorJson()) }, []);
1341
+ const resolution = await resolvePortableActorKey(hinted(gw1), {
1342
+ documentLoader,
1343
+ contextLoader,
1344
+ keyCache: slowKeyCache,
1345
+ lookupTimeout: 300
1346
+ });
1347
+ lookupDone();
1348
+ strictEqual(resolution.type, "unavailable");
1349
+ if (resolution.type === "unavailable") strictEqual(resolution.cacheable, false);
1350
+ await late;
1351
+ const lateRejection = controlledLoader({}, [], { delays: { [compatibleId(gw1)]: 60 } });
1352
+ strictEqual((await resolvePortableActorKey(hinted(gw1), {
1353
+ documentLoader: lateRejection,
1354
+ contextLoader,
1355
+ lookupTimeout: 20
1356
+ })).type, "unavailable");
1357
+ await new Promise((resolve) => setTimeout(resolve, 80));
1358
+ });
1359
+ test("resolvePortableActorKey() validates the lookup timeout", async () => {
1360
+ const documentLoader = controlledLoader({}, []);
1361
+ for (const lookupTimeout of [
1362
+ -1,
1363
+ NaN,
1364
+ Infinity,
1365
+ 2 ** 31
1366
+ ]) {
1367
+ let error;
1368
+ try {
1369
+ await resolvePortableActorKey(hinted(gw1), {
1370
+ documentLoader,
1371
+ contextLoader,
1372
+ lookupTimeout
1373
+ });
1374
+ } catch (e) {
1375
+ error = e;
1376
+ }
1377
+ ok(error instanceof RangeError, String(lookupTimeout));
1378
+ }
1379
+ const resolution = await resolvePortableActorKey(hinted(gw1), {
1380
+ documentLoader,
1381
+ contextLoader,
1382
+ lookupTimeout: 0
1383
+ });
1384
+ strictEqual(resolution.type, "unavailable");
1385
+ if (resolution.type === "unavailable") strictEqual(resolution.cacheable, false);
1386
+ deepStrictEqual(documentLoader.fetched, []);
1387
+ });
1388
+ test("fetchKeyDetailed() caches keys at ap: key IDs that time out as fetch failures", async () => {
1389
+ const cases = [{
1390
+ responses: { [compatibleId(gw1)]: 404 },
1391
+ hanging: [compatibleId(gw2)],
1392
+ cached: true
1393
+ }, {
1394
+ responses: {},
1395
+ hanging: [compatibleId(gw1)],
1396
+ cached: false
1397
+ }];
1398
+ for (const { responses, hanging, cached } of cases) {
1399
+ const documentLoader = controlledLoader(responses, hanging);
1400
+ const keyCache = new KvKeyCache(new MemoryKvStore(), ["pk"]);
1401
+ const options = {
1402
+ documentLoader,
1403
+ contextLoader,
1404
+ keyCache,
1405
+ portableKeyResolvers: {
1406
+ gatewayKey: () => Promise.resolve({ type: "legacy" }),
1407
+ actorKey: (keyId) => resolvePortableActorKey(keyId, {
1408
+ documentLoader,
1409
+ contextLoader,
1410
+ lookupTimeout: 50
1411
+ })
1412
+ }
1413
+ };
1414
+ const hintedKeyId = hinted(gw1, gw2);
1415
+ for (let i = 0; i < 2; i++) {
1416
+ const result = await fetchKeyDetailed(hintedKeyId, CryptographicKey, options);
1417
+ strictEqual(result.key, null);
1418
+ strictEqual(result.cached, cached && i > 0, `${cached}, ${i}`);
1419
+ ok(result.fetchError == null || !("status" in result.fetchError), "a timeout has no HTTP status");
1420
+ }
1421
+ strictEqual(documentLoader.fetched.filter((url) => url === compatibleId(gw1)).length, cached ? 1 : 2);
1422
+ }
1423
+ });
1424
+ test("verifyRequestDetailed() reports keys at ap: key IDs that time out as fetch errors", async () => {
1425
+ const timeout = new DOMException("Timed out", "TimeoutError");
1426
+ const documentLoader = () => Promise.reject(timeout);
1427
+ for (const spec of specs) {
1428
+ const result = await verifyRequestDetailed(await signedRequest(spec, hinted(gw1)), {
1429
+ documentLoader,
1430
+ contextLoader,
1431
+ spec
1432
+ });
1433
+ ok(!result.verified, spec);
1434
+ strictEqual(result.reason.type, "keyFetchError", spec);
1435
+ if (result.reason.type === "keyFetchError") ok(!("status" in result.reason.result), spec);
1436
+ }
1437
+ });
1192
1438
  //#endregion
1193
1439
  export {};
@@ -10,10 +10,10 @@ import "../std__assert-DvMDRPes.mjs";
10
10
  import { t as assertRejects } from "../assert_rejects-CTptdYck.mjs";
11
11
  import { t as assert } from "../assert-D1Eafmd_.mjs";
12
12
  import { r as normalizeOutgoingActivityJsonLd } from "../outgoing-jsonld-4IJqPYos.mjs";
13
- import { a as signObject, c as verifyPortableObjectProof, d as verifyProof, i as hasProofLike, r as createProof, s as verifyObject } from "../proof-CRTPWbVh.mjs";
14
- import { d as verifyRequest } from "../http-DRH0n8TD.mjs";
13
+ import { a as signObject, c as verifyPortableObjectProof, d as verifyProof, i as hasProofLike, r as createProof, s as verifyObject } from "../proof-C9115DX_.mjs";
14
+ import { f as verifyRequest } from "../http-Y1ObT1A8.mjs";
15
15
  import { i as rsaPrivateKey2, n as ed25519PrivateKey, r as ed25519PublicKey, s as rsaPublicKey2, t as ed25519Multikey } from "../keys-DIJNHW-Y.mjs";
16
- import { t as getAuthenticatedDocumentLoader } from "../docloader-BBFAItNB.mjs";
16
+ import { t as getAuthenticatedDocumentLoader } from "../docloader-u6zzOExF.mjs";
17
17
  import { Create, DataIntegrityProof, Document, Multikey, Note, PUBLIC_COLLECTION, Place, Translation } from "@fedify/vocab";
18
18
  import { createTestMeterProvider, mockDocumentLoader, test } from "@fedify/fixture";
19
19
  import { decodeMultibase, encodeMultibase, exportDidKey, exportMultibaseKey, formatIri, importMultibaseKey, parseIri } from "@fedify/vocab-runtime";
@@ -1,7 +1,7 @@
1
1
  import { Temporal } from "temporal-polyfill";
2
2
  import "urlpattern-polyfill";
3
3
  globalThis.addEventListener = () => {};
4
- import { c as getNormalizationContextLoader } from "./ld-CyliEU48.mjs";
4
+ import { c as getNormalizationContextLoader } from "./ld-oQj6j_hP.mjs";
5
5
  import jsonld from "@fedify/vocab-runtime/jsonld";
6
6
  //#region src/federation/temporal.ts
7
7
  function isPlainObject(value) {
@@ -6,7 +6,7 @@ import { t as MemoryKvStore } from "../kv-DCHkaJ9-.mjs";
6
6
  import { t as ManualClockKvStore } from "../kv-DBRWVx3Q.mjs";
7
7
  import { n as baseOptions, o as stringSchema, t as MockQueue } from "../tasks-IG3pHfvZ.mjs";
8
8
  import { t as assertEquals } from "../assert_equals-Z6IcEbWU.mjs";
9
- import { o as createFederation } from "../middleware-B99JjvFn.mjs";
9
+ import { o as createFederation } from "../middleware-Bdu2-OPD.mjs";
10
10
  //#region src/testing/kv.test.ts
11
11
  test("ManualClockKvStore keeps an entry at its exact expiration instant", async () => {
12
12
  const kv = new ManualClockKvStore();
@@ -2275,6 +2275,35 @@ interface FederationOptions<TContextData> {
2275
2275
  * @since 1.3.0
2276
2276
  */
2277
2277
  userAgent?: GetUserAgentOptions | string;
2278
+ /**
2279
+ * The timeout for each call of the built-in document loader, context
2280
+ * loader, and authenticated document loader, e.g., when Fedify fetches
2281
+ * a key to verify a signature. The timeout is shared by all the steps of
2282
+ * a call, including every redirect it follows, retries, and reading the
2283
+ * response body. It does not cover the time spent reading from or
2284
+ * writing to the cache.
2285
+ *
2286
+ * A timed-out call throws a `FetchError` without a response, whose
2287
+ * `cause` is a `DOMException` named `"TimeoutError"`, so that, e.g.,
2288
+ * a key fetch that times out is reported as a `keyFetchError` and cached
2289
+ * like other failures to fetch a key.
2290
+ *
2291
+ * It does not affect loaders made by
2292
+ * {@link FederationOptions.documentLoaderFactory},
2293
+ * {@link FederationOptions.contextLoaderFactory}, or
2294
+ * {@link FederationOptions.authenticatedDocumentLoaderFactory}.
2295
+ * Note that if only {@link FederationOptions.documentLoaderFactory} is
2296
+ * set, it also makes context loaders.
2297
+ *
2298
+ * Set it to `null` to turn off the timeout.
2299
+ *
2300
+ * 10 seconds by default.
2301
+ * @throws {RangeError} If the duration is not positive, is longer than
2302
+ * about 24.8 days, or is given in calendar units
2303
+ * such as months.
2304
+ * @since 2.4.0
2305
+ */
2306
+ documentLoaderTimeout?: Temporal.Duration | Temporal.DurationLike | null;
2278
2307
  /**
2279
2308
  * A callback that handles errors during outbox processing. Note that this
2280
2309
  * callback can be called multiple times for the same activity, because
@@ -2303,6 +2332,29 @@ interface FederationOptions<TContextData> {
2303
2332
  * By default, the window is an hour.
2304
2333
  */
2305
2334
  signatureTimeWindow?: Temporal.Duration | Temporal.DurationLike | false;
2335
+ /**
2336
+ * The maximum number of [RFC 9421] signatures of an incoming request to
2337
+ * verify, e.g., in the inbox and in
2338
+ * {@link RequestContext.getSignedKey}. A request can carry several
2339
+ * signatures, each of which may make Fedify fetch the key that it names,
2340
+ * so only the first ones in the order of the `Signature-Input` header are
2341
+ * verified, and the rest are ignored as if they were absent. Every
2342
+ * signature among the first ones counts, even if it fails before its key
2343
+ * is fetched. See also the `maxSignatures` option of `verifyRequest()`.
2344
+ *
2345
+ * It has to be a positive integer, or `Infinity` to verify every
2346
+ * signature, which lets a single request make Fedify fetch any number of
2347
+ * keys. Draft-cavage HTTP Signatures carry a single signature, so this
2348
+ * option does not affect them.
2349
+ *
2350
+ * Three by default.
2351
+ *
2352
+ * [RFC 9421]: https://www.rfc-editor.org/rfc/rfc9421
2353
+ * @throws {RangeError} Thrown when the federation is created if the value
2354
+ * is not a positive integer or `Infinity`.
2355
+ * @since 2.4.0
2356
+ */
2357
+ maxHttpSignatures?: number;
2306
2358
  /**
2307
2359
  * Whether to skip HTTP Signatures verification for incoming activities.
2308
2360
  * This is useful for testing purposes, but should not be used in production.
@@ -5,9 +5,9 @@ import { t as esm_default } from "../esm-BHXXx4Wx.mjs";
5
5
  import { t as assertEquals } from "../assert_equals-Z6IcEbWU.mjs";
6
6
  import "../std__assert-DvMDRPes.mjs";
7
7
  import { t as assertRejects } from "../assert_rejects-CTptdYck.mjs";
8
- import { d as verifyRequest } from "../http-DRH0n8TD.mjs";
8
+ import { f as verifyRequest } from "../http-Y1ObT1A8.mjs";
9
9
  import { i as rsaPrivateKey2 } from "../keys-DIJNHW-Y.mjs";
10
- import { t as getAuthenticatedDocumentLoader } from "../docloader-BBFAItNB.mjs";
10
+ import { t as getAuthenticatedDocumentLoader } from "../docloader-u6zzOExF.mjs";
11
11
  import { configure, reset } from "@logtape/logtape";
12
12
  import { mockDocumentLoader, test } from "@fedify/fixture";
13
13
  import { FetchError, UrlError } from "@fedify/vocab-runtime";
@@ -534,5 +534,96 @@ test("getAuthenticatedDocumentLoader() rejects cancellation before fetching", as
534
534
  esm_default.hardReset();
535
535
  }
536
536
  });
537
+ function assertTimeoutFetchError(error, url) {
538
+ ok(error instanceof FetchError, String(error));
539
+ deepStrictEqual(error.url.href, url);
540
+ deepStrictEqual(error.response, void 0);
541
+ ok(error.cause instanceof DOMException);
542
+ deepStrictEqual(error.cause.name, "TimeoutError");
543
+ return true;
544
+ }
545
+ test("getAuthenticatedDocumentLoader() times out", async (t) => {
546
+ const identity = {
547
+ keyId: new URL("https://example.com/key2"),
548
+ privateKey: rsaPrivateKey2
549
+ };
550
+ const delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
551
+ await t.step("a request without a response", async () => {
552
+ esm_default.mockGlobal();
553
+ const url = "https://example.com/hang";
554
+ esm_default.get(url, () => new Promise(() => {}));
555
+ try {
556
+ await rejects(getAuthenticatedDocumentLoader(identity, {
557
+ allowPrivateAddress: true,
558
+ timeout: 100
559
+ })(url), (e) => assertTimeoutFetchError(e, url));
560
+ } finally {
561
+ esm_default.hardReset();
562
+ }
563
+ });
564
+ await t.step("double-knocking shares the timeout", async () => {
565
+ esm_default.mockGlobal();
566
+ const url = "https://example.com/double-knock";
567
+ let requests = 0;
568
+ esm_default.get(url, async () => {
569
+ requests++;
570
+ await delay(80);
571
+ return new Response(null, { status: 401 });
572
+ });
573
+ try {
574
+ await rejects(getAuthenticatedDocumentLoader(identity, {
575
+ allowPrivateAddress: true,
576
+ timeout: 120
577
+ })(url), (e) => assertTimeoutFetchError(e, url));
578
+ deepStrictEqual(requests, 2);
579
+ await delay(100);
580
+ deepStrictEqual(requests, 2);
581
+ } finally {
582
+ esm_default.hardReset();
583
+ }
584
+ });
585
+ for (const step of ["determineSpec", "rememberSpec"]) await t.step(`a slow ${step}()`, async () => {
586
+ esm_default.mockGlobal();
587
+ const url = `https://example.com/slow-${step}`;
588
+ let requests = 0;
589
+ esm_default.get(url, () => {
590
+ requests++;
591
+ return Response.json({ parsed: true });
592
+ });
593
+ try {
594
+ await rejects(getAuthenticatedDocumentLoader(identity, {
595
+ allowPrivateAddress: true,
596
+ timeout: 50,
597
+ specDeterminer: {
598
+ async determineSpec() {
599
+ if (step === "determineSpec") await delay(150);
600
+ return "rfc9421";
601
+ },
602
+ async rememberSpec() {
603
+ if (step === "rememberSpec") await delay(150);
604
+ }
605
+ }
606
+ })(url), (e) => assertTimeoutFetchError(e, url));
607
+ await delay(200);
608
+ deepStrictEqual(requests, step === "determineSpec" ? 0 : 1);
609
+ } finally {
610
+ esm_default.hardReset();
611
+ }
612
+ });
613
+ await t.step("the caller's signal still aborts", async () => {
614
+ esm_default.mockGlobal();
615
+ const url = "https://example.com/hang";
616
+ esm_default.get(url, () => new Promise(() => {}));
617
+ try {
618
+ const loader = getAuthenticatedDocumentLoader(identity, { allowPrivateAddress: true });
619
+ const controller = new AbortController();
620
+ const reason = /* @__PURE__ */ new Error("Canceled by the caller");
621
+ setTimeout(() => controller.abort(reason), 50);
622
+ await rejects(loader(url, { signal: controller.signal }), (e) => e === reason);
623
+ } finally {
624
+ esm_default.hardReset();
625
+ }
626
+ });
627
+ });
537
628
  //#endregion
538
629
  export {};
@@ -1,7 +1,7 @@
1
1
  import { Temporal } from "temporal-polyfill";
2
2
  import { URLPattern } from "urlpattern-polyfill";
3
3
  globalThis.addEventListener = () => {};
4
- import { n as kvCache, t as MockKvStore } from "../kv-cache-C7RWL7hz.mjs";
4
+ import { n as kvCache, t as MockKvStore } from "../kv-cache-C8YjNAi_.mjs";
5
5
  import { deepStrictEqual, throws } from "node:assert";
6
6
  import { createTestMeterProvider, mockDocumentLoader, test } from "@fedify/fixture";
7
7
  import { preloadedContexts } from "@fedify/vocab-runtime";
@@ -1,5 +1,5 @@
1
1
  const { URLPattern } = require("urlpattern-polyfill");
2
2
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
3
- const require_kv_cache = require("../kv-cache-Ct-KWRta.cjs");
3
+ const require_kv_cache = require("../kv-cache-DUXluAc0.cjs");
4
4
  exports.getAuthenticatedDocumentLoader = require_kv_cache.getAuthenticatedDocumentLoader;
5
5
  exports.kvCache = require_kv_cache.kvCache;
@@ -1,3 +1,3 @@
1
1
  /// <reference lib="esnext.temporal" />
2
- import { n as getAuthenticatedDocumentLoader, t as kvCache } from "../mod-BA_F2x53.cjs";
2
+ import { n as getAuthenticatedDocumentLoader, t as kvCache } from "../mod-Dp3Ij7sw.cjs";
3
3
  export { getAuthenticatedDocumentLoader, kvCache };
@@ -1,3 +1,3 @@
1
1
  /// <reference lib="esnext.temporal" />
2
- import { n as getAuthenticatedDocumentLoader, t as kvCache } from "../mod-B5z_q9TH.js";
2
+ import { n as getAuthenticatedDocumentLoader, t as kvCache } from "../mod-C5Mwx5ty.js";
3
3
  export { getAuthenticatedDocumentLoader, kvCache };
package/dist/utils/mod.js CHANGED
@@ -1,4 +1,4 @@
1
1
  import { Temporal } from "temporal-polyfill";
2
2
  import { URLPattern } from "urlpattern-polyfill";
3
- import { n as getAuthenticatedDocumentLoader, t as kvCache } from "../kv-cache-CZiBDLPi.js";
3
+ import { n as getAuthenticatedDocumentLoader, t as kvCache } from "../kv-cache-C02XitwK.js";
4
4
  export { getAuthenticatedDocumentLoader, kvCache };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fedify/fedify",
3
- "version": "2.4.0-dev.2164+db2b8571",
3
+ "version": "2.4.0-dev.2190+be3a8be0",
4
4
  "description": "An ActivityPub server framework",
5
5
  "keywords": [
6
6
  "ActivityPub",
@@ -151,10 +151,10 @@
151
151
  "structured-field-values": "^2.0.4",
152
152
  "temporal-polyfill": "^1.0.1",
153
153
  "urlpattern-polyfill": "^10.1.0",
154
- "@fedify/webfinger": "2.4.0-dev.2164+db2b8571",
155
- "@fedify/uri-template": "2.4.0-dev.2164+db2b8571",
156
- "@fedify/vocab": "2.4.0-dev.2164+db2b8571",
157
- "@fedify/vocab-runtime": "2.4.0-dev.2164+db2b8571"
154
+ "@fedify/uri-template": "2.4.0-dev.2190+be3a8be0",
155
+ "@fedify/vocab-runtime": "2.4.0-dev.2190+be3a8be0",
156
+ "@fedify/vocab": "2.4.0-dev.2190+be3a8be0",
157
+ "@fedify/webfinger": "2.4.0-dev.2190+be3a8be0"
158
158
  },
159
159
  "devDependencies": {
160
160
  "@std/assert": "npm:@jsr/std__assert@^0.226.0",
@@ -167,7 +167,7 @@
167
167
  "typescript": "^6.0.0",
168
168
  "wrangler": "^4.17.0",
169
169
  "@fedify/fixture": "2.0.0",
170
- "@fedify/vocab-tools": "^2.4.0-dev.2164+db2b8571"
170
+ "@fedify/vocab-tools": "^2.4.0-dev.2190+be3a8be0"
171
171
  },
172
172
  "scripts": {
173
173
  "build:self": "tsdown",