@fedify/fedify 2.4.2 → 2.4.4

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 (83) hide show
  1. package/dist/{builder-9ovCSqLp.mjs → builder-DdY0E0w9.mjs} +2 -2
  2. package/dist/compat/transformers.test.mjs +1 -1
  3. package/dist/{compound-proof-G2J_bU0u.mjs → compound-proof-fxkyiXYM.mjs} +2 -2
  4. package/dist/{deno-BZ4z-lJJ.mjs → deno-CO1X_K_q.mjs} +1 -1
  5. package/dist/{docloader-DUXXI-7G.mjs → docloader-DFB_TNVp.mjs} +6 -2
  6. package/dist/federation/builder.test.mjs +1 -1
  7. package/dist/federation/compound-fanout.test.mjs +3 -3
  8. package/dist/federation/handler-compound-proof.test.mjs +5 -5
  9. package/dist/federation/handler.test.mjs +258 -5
  10. package/dist/federation/hashlink-media.test.mjs +2 -2
  11. package/dist/federation/idempotency.test.mjs +2 -2
  12. package/dist/federation/inbox-report.test.mjs +5 -5
  13. package/dist/federation/inbox.test.mjs +43 -0
  14. package/dist/federation/keycache.test.mjs +20 -9
  15. package/dist/federation/metrics.test.mjs +1 -1
  16. package/dist/federation/middleware.test.mjs +173 -11
  17. package/dist/federation/mod.cjs +1 -1
  18. package/dist/federation/mod.js +1 -1
  19. package/dist/federation/outgoing-proof.test.mjs +4 -4
  20. package/dist/federation/portable-actor.test.mjs +3 -3
  21. package/dist/federation/portable-chain.test.mjs +3 -3
  22. package/dist/federation/portable-collection.test.mjs +4 -4
  23. package/dist/federation/portable-delivery.test.mjs +2 -2
  24. package/dist/federation/portable-gateway-keys.test.mjs +5 -5
  25. package/dist/federation/portable-inbox.test.mjs +4 -4
  26. package/dist/federation/portable-parse-uri.test.mjs +1 -1
  27. package/dist/federation/portable-route-activity.test.mjs +2 -2
  28. package/dist/federation/portable.test.mjs +3 -3
  29. package/dist/federation/send.test.mjs +3 -3
  30. package/dist/federation/tasks/enqueue.test.mjs +1 -1
  31. package/dist/federation/tasks/tasks.test.mjs +2 -2
  32. package/dist/federation/temporal.test.mjs +1 -1
  33. package/dist/federation/webfinger.test.mjs +1 -1
  34. package/dist/{federation-DpT8bDM6.cjs → federation-Bf4FEork.cjs} +122 -21
  35. package/dist/{http-Qq7jR2rC.js → http-D_AGUmSq.js} +17 -3
  36. package/dist/{http-B5w_qcIg.mjs → http-DkLKT8Vp.mjs} +4 -4
  37. package/dist/{http-x1satM4y.cjs → http-g5f1D2tm.cjs} +17 -3
  38. package/dist/inbox-Di1-EKqa.mjs +188 -0
  39. package/dist/{keycache-D-MNltOk.mjs → keycache-N2opu4-3.mjs} +1 -1
  40. package/dist/{kv-cache-BgQMAKIg.mjs → kv-cache-BxfOpUP9.mjs} +1 -1
  41. package/dist/{kv-cache-B0SZfzFb.js → kv-cache-C0AsMZ4U.js} +5 -1
  42. package/dist/{kv-cache-D4throaQ.cjs → kv-cache-HsXADkl9.cjs} +5 -1
  43. package/dist/{ld-Dkkyk65e.mjs → ld-Cy86r-Ty.mjs} +18 -4
  44. package/dist/{metrics-CRv6_IBg.mjs → metrics-BguhdXt-.mjs} +1 -1
  45. package/dist/{middleware-BqefXH6B.js → middleware-BOBF7req.js} +122 -21
  46. package/dist/{middleware-DFtNaBcH.mjs → middleware-BYojyPi4.mjs} +1 -1
  47. package/dist/{middleware-C-swet0z.mjs → middleware-C_gtR3fV.mjs} +104 -180
  48. package/dist/mod.cjs +4 -4
  49. package/dist/mod.js +4 -4
  50. package/dist/nodeinfo/handler.test.mjs +1 -1
  51. package/dist/{owner-BHUhedVc.mjs → owner-Dc-Lg_Xn.mjs} +3 -3
  52. package/dist/{portable-collection-DL-1Wy1s.cjs → portable-collection-Bl2UkqcO.cjs} +1 -1
  53. package/dist/{portable-collection-CfIc190c.js → portable-collection-CjdP7fQ4.js} +1 -1
  54. package/dist/{portable-collection-DONEPTXV.mjs → portable-collection-DSKqMgvr.mjs} +2 -2
  55. package/dist/{portable-key-Cdw3vfl-.mjs → portable-key-ClXZ25LT.mjs} +1 -1
  56. package/dist/{proof-DuSE1ABD.mjs → proof-kJtQ49gr.mjs} +3 -3
  57. package/dist/{send-D9oTjA6A.mjs → send-D6hcL5qf.mjs} +3 -3
  58. package/dist/sig/compound-proof-discovery.test.mjs +1 -1
  59. package/dist/sig/compound-proof-producer.test.mjs +1 -1
  60. package/dist/sig/compound-proof-verification.test.mjs +2 -2
  61. package/dist/sig/compound-proof.test.mjs +2 -2
  62. package/dist/sig/http.test.mjs +3 -3
  63. package/dist/sig/key.test.mjs +196 -1
  64. package/dist/sig/ld.test.mjs +46 -1
  65. package/dist/sig/mitra-receive-interop.test.mjs +4 -4
  66. package/dist/sig/mitra-send-interop.test.mjs +3 -3
  67. package/dist/sig/mod.cjs +2 -2
  68. package/dist/sig/mod.js +2 -2
  69. package/dist/sig/owner.test.mjs +46 -1
  70. package/dist/sig/portable-collection.test.mjs +1 -1
  71. package/dist/sig/portable-interop.test.mjs +3 -3
  72. package/dist/sig/portable-key.test.mjs +6 -6
  73. package/dist/sig/proof.test.mjs +54 -3
  74. package/dist/sig/tootik-request-interop.test.mjs +4 -4
  75. package/dist/sig/tootik-send-interop.test.mjs +3 -3
  76. package/dist/sig/verification.test.mjs +3 -3
  77. package/dist/{temporal-iyAxBq0I.mjs → temporal-C8WqyM34.mjs} +1 -1
  78. package/dist/testing/kv.test.mjs +1 -1
  79. package/dist/utils/docloader.test.mjs +176 -37
  80. package/dist/utils/kv-cache.test.mjs +1 -1
  81. package/dist/utils/mod.cjs +1 -1
  82. package/dist/utils/mod.js +1 -1
  83. package/package.json +6 -6
@@ -11,10 +11,10 @@ import { t as assertRejects } from "../assert_rejects-CTptdYck.mjs";
11
11
  import { t as assert } from "../assert-D1Eafmd_.mjs";
12
12
  import { a as verificationObservation } from "../verification-Dpfcw8ml.mjs";
13
13
  import { r as normalizeOutgoingActivityJsonLd } from "../outgoing-jsonld-4IJqPYos.mjs";
14
- import { a as signObject, c as verifyPortableObjectProof, d as verifyProof, i as hasProofLike, o as verifyMapLocalProof, r as createProof, s as verifyObject } from "../proof-DuSE1ABD.mjs";
15
- import { f as verifyRequest } from "../http-B5w_qcIg.mjs";
14
+ import { a as signObject, c as verifyPortableObjectProof, d as verifyProof, i as hasProofLike, o as verifyMapLocalProof, r as createProof, s as verifyObject } from "../proof-kJtQ49gr.mjs";
15
+ import { f as verifyRequest } from "../http-DkLKT8Vp.mjs";
16
16
  import { i as rsaPrivateKey2, n as ed25519PrivateKey, r as ed25519PublicKey, s as rsaPublicKey2, t as ed25519Multikey } from "../keys-DIJNHW-Y.mjs";
17
- import { t as getAuthenticatedDocumentLoader } from "../docloader-DUXXI-7G.mjs";
17
+ import { t as getAuthenticatedDocumentLoader } from "../docloader-DFB_TNVp.mjs";
18
18
  import { Create, DataIntegrityProof, Document, Multikey, Note, PUBLIC_COLLECTION, Place, Translation } from "@fedify/vocab";
19
19
  import { decodeMultibase, encodeMultibase, exportDidKey, exportMultibaseKey, formatIri, importMultibaseKey, parseIri } from "@fedify/vocab-runtime";
20
20
  import { createTestMeterProvider, mockDocumentLoader, test } from "@fedify/fixture";
@@ -2464,5 +2464,56 @@ test("getAuthenticatedDocumentLoader() signs portable object requests to gateway
2464
2464
  esm_default.hardReset();
2465
2465
  }
2466
2466
  });
2467
+ test("verifyObject() rejects a key whose controller redirects to another actor", async () => {
2468
+ const keyId = new URL("https://evil.example/keys/ed25519");
2469
+ const victim = "https://victim.example/users/alice";
2470
+ const mallory = "https://evil.example/users/mallory";
2471
+ const multikeyDocument = await ed25519Multikey.toJsonLd({ contextLoader: mockDocumentLoader });
2472
+ const signOptions = {
2473
+ format: "compact",
2474
+ contextLoader: mockDocumentLoader,
2475
+ documentLoader: mockDocumentLoader,
2476
+ context: ["https://www.w3.org/ns/activitystreams", "https://w3id.org/security/data-integrity/v1"]
2477
+ };
2478
+ assertEquals(await verifyObject(Create, await (await signObject(new Create({
2479
+ id: new URL("https://victim.example/activities/forged"),
2480
+ actor: new URL(victim),
2481
+ object: new Note({
2482
+ id: new URL("https://victim.example/notes/forged"),
2483
+ attribution: new URL(victim),
2484
+ content: "forged"
2485
+ })
2486
+ }), ed25519PrivateKey, keyId, signOptions)).toJsonLd(signOptions), {
2487
+ documentLoader(resource) {
2488
+ if (resource === keyId.href) return Promise.resolve({
2489
+ contextUrl: null,
2490
+ documentUrl: resource,
2491
+ document: {
2492
+ ...multikeyDocument,
2493
+ id: keyId.href,
2494
+ controller: victim
2495
+ }
2496
+ });
2497
+ if (resource === victim || resource === mallory) return Promise.resolve({
2498
+ contextUrl: null,
2499
+ documentUrl: mallory,
2500
+ document: {
2501
+ "@context": [
2502
+ "https://www.w3.org/ns/activitystreams",
2503
+ "https://w3id.org/security/v1",
2504
+ "https://w3id.org/security/multikey/v1",
2505
+ "https://w3id.org/security/data-integrity/v1",
2506
+ "https://www.w3.org/ns/did/v1"
2507
+ ],
2508
+ id: mallory,
2509
+ type: "Person",
2510
+ assertionMethod: [keyId.href]
2511
+ }
2512
+ });
2513
+ return mockDocumentLoader(resource);
2514
+ },
2515
+ contextLoader: mockDocumentLoader
2516
+ }), null);
2517
+ });
2467
2518
  //#endregion
2468
2519
  export {};
@@ -5,10 +5,10 @@ import { t as MemoryKvStore } from "../kv-DCHkaJ9-.mjs";
5
5
  import { t as assertEquals } from "../assert_equals-Z6IcEbWU.mjs";
6
6
  import "../std__assert-DvMDRPes.mjs";
7
7
  import { t as assert } from "../assert-D1Eafmd_.mjs";
8
- import { c as verifyPortableObjectProof } from "../proof-DuSE1ABD.mjs";
9
- import { p as verifyRequestDetailed } from "../http-B5w_qcIg.mjs";
10
- import { o as createFederation } from "../middleware-C-swet0z.mjs";
11
- import { t as doesActorOwnKey } from "../owner-BHUhedVc.mjs";
8
+ import { c as verifyPortableObjectProof } from "../proof-kJtQ49gr.mjs";
9
+ import { p as verifyRequestDetailed } from "../http-DkLKT8Vp.mjs";
10
+ import { o as createFederation } from "../middleware-C_gtR3fV.mjs";
11
+ import { t as doesActorOwnKey } from "../owner-Dc-Lg_Xn.mjs";
12
12
  import { Follow, Person } from "@fedify/vocab";
13
13
  import { mockDocumentLoader, test } from "@fedify/fixture";
14
14
  import { encodeHex } from "byte-encodings/hex";
@@ -4,9 +4,9 @@ globalThis.addEventListener = () => {};
4
4
  import { t as assertEquals } from "../assert_equals-Z6IcEbWU.mjs";
5
5
  import "../std__assert-DvMDRPes.mjs";
6
6
  import { t as assert } from "../assert-D1Eafmd_.mjs";
7
- import { c as verifyPortableObjectProof } from "../proof-DuSE1ABD.mjs";
8
- import { p as verifyRequestDetailed } from "../http-B5w_qcIg.mjs";
9
- import { t as doesActorOwnKey } from "../owner-BHUhedVc.mjs";
7
+ import { c as verifyPortableObjectProof } from "../proof-kJtQ49gr.mjs";
8
+ import { p as verifyRequestDetailed } from "../http-DkLKT8Vp.mjs";
9
+ import { t as doesActorOwnKey } from "../owner-Dc-Lg_Xn.mjs";
10
10
  import { Create } from "@fedify/vocab";
11
11
  import { decodeMultibase, importDidKey } from "@fedify/vocab-runtime";
12
12
  import { mockDocumentLoader, test } from "@fedify/fixture";
@@ -2,9 +2,9 @@ import "temporal-polyfill";
2
2
  import "urlpattern-polyfill";
3
3
  globalThis.addEventListener = () => {};
4
4
  import { a as verificationObservation } from "../verification-Dpfcw8ml.mjs";
5
- import { h as verifyJsonLd, p as signJsonLd } from "../ld-Dkkyk65e.mjs";
6
- import { a as signObject, s as verifyObject } from "../proof-DuSE1ABD.mjs";
7
- import { l as signRequest, p as verifyRequestDetailed } from "../http-B5w_qcIg.mjs";
5
+ import { h as verifyJsonLd, p as signJsonLd } from "../ld-Cy86r-Ty.mjs";
6
+ import { a as signObject, s as verifyObject } from "../proof-kJtQ49gr.mjs";
7
+ import { l as signRequest, p as verifyRequestDetailed } from "../http-DkLKT8Vp.mjs";
8
8
  import { i as rsaPrivateKey2, n as ed25519PrivateKey, o as rsaPublicKey1, s as rsaPublicKey2, t as ed25519Multikey } from "../keys-DIJNHW-Y.mjs";
9
9
  import { Activity, Create, CryptographicKey, Multikey } from "@fedify/vocab";
10
10
  import { FetchError } 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-Dkkyk65e.mjs";
4
+ import { c as getNormalizationContextLoader } from "./ld-Cy86r-Ty.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-C-swet0z.mjs";
9
+ import { o as createFederation } from "../middleware-C_gtR3fV.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();
@@ -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 { f as verifyRequest } from "../http-B5w_qcIg.mjs";
8
+ import { f as verifyRequest } from "../http-DkLKT8Vp.mjs";
9
9
  import { i as rsaPrivateKey2 } from "../keys-DIJNHW-Y.mjs";
10
- import { t as getAuthenticatedDocumentLoader } from "../docloader-DUXXI-7G.mjs";
10
+ import { t as getAuthenticatedDocumentLoader } from "../docloader-DFB_TNVp.mjs";
11
11
  import { configure, reset } from "@logtape/logtape";
12
12
  import { FetchError, UrlError } from "@fedify/vocab-runtime";
13
13
  import { mockDocumentLoader, test } from "@fedify/fixture";
@@ -99,45 +99,50 @@ test("getAuthenticatedDocumentLoader() validates redirects", async (t) => {
99
99
  });
100
100
  esm_default.hardReset();
101
101
  });
102
- test("getAuthenticatedDocumentLoader() cancellation", {
103
- sanitizeResources: false,
104
- sanitizeOps: false
105
- }, async (t) => {
102
+ test("getAuthenticatedDocumentLoader() cancellation", async (t) => {
106
103
  esm_default.spyGlobal();
107
- await t.step("document loader cancellation", async () => {
108
- esm_default.get("https://example.com/slow-object", () => new Promise((resolve) => {
109
- setTimeout(() => {
110
- resolve({
111
- status: 200,
112
- headers: { "Content-Type": "application/activity+json" },
113
- body: {
114
- "@context": "https://www.w3.org/ns/activitystreams",
115
- type: "Note",
116
- content: "Slow response"
117
- }
104
+ try {
105
+ await t.step("document loader cancellation", async () => {
106
+ let responseTimer;
107
+ try {
108
+ esm_default.get("https://example.com/slow-object", () => new Promise((resolve) => {
109
+ responseTimer = setTimeout(() => {
110
+ resolve({
111
+ status: 200,
112
+ headers: { "Content-Type": "application/activity+json" },
113
+ body: {
114
+ "@context": "https://www.w3.org/ns/activitystreams",
115
+ type: "Note",
116
+ content: "Slow response"
117
+ }
118
+ });
119
+ }, 1e3);
120
+ }));
121
+ const loader = getAuthenticatedDocumentLoader({
122
+ keyId: new URL("https://example.com/key2"),
123
+ privateKey: rsaPrivateKey2
118
124
  });
119
- }, 1e3);
120
- }));
121
- const loader = getAuthenticatedDocumentLoader({
122
- keyId: new URL("https://example.com/key2"),
123
- privateKey: rsaPrivateKey2
125
+ const controller = new AbortController();
126
+ const promise = loader("https://example.com/slow-object", { signal: controller.signal });
127
+ controller.abort();
128
+ await assertRejects(() => promise, Error);
129
+ await assertRejects(() => loader("https://example.com/object", { signal: controller.signal }), Error);
130
+ } finally {
131
+ clearTimeout(responseTimer);
132
+ }
124
133
  });
125
- const controller = new AbortController();
126
- const promise = loader("https://example.com/slow-object", { signal: controller.signal });
127
- controller.abort();
128
- await assertRejects(() => promise, Error);
129
- await assertRejects(() => loader("https://example.com/object", { signal: controller.signal }), Error);
130
- });
131
- await t.step("immediate cancellation", async () => {
132
- const loader = getAuthenticatedDocumentLoader({
133
- keyId: new URL("https://example.com/key2"),
134
- privateKey: rsaPrivateKey2
134
+ await t.step("immediate cancellation", async () => {
135
+ const loader = getAuthenticatedDocumentLoader({
136
+ keyId: new URL("https://example.com/key2"),
137
+ privateKey: rsaPrivateKey2
138
+ });
139
+ const controller = new AbortController();
140
+ controller.abort();
141
+ await assertRejects(() => loader("https://example.com/object", { signal: controller.signal }), Error);
135
142
  });
136
- const controller = new AbortController();
137
- controller.abort();
138
- await assertRejects(() => loader("https://example.com/object", { signal: controller.signal }), Error);
139
- });
140
- esm_default.hardReset();
143
+ } finally {
144
+ esm_default.hardReset();
145
+ }
141
146
  });
142
147
  test("getAuthenticatedDocumentLoader() bounds JSON after redirects", async () => {
143
148
  esm_default.mockGlobal();
@@ -625,5 +630,139 @@ test("getAuthenticatedDocumentLoader() times out", async (t) => {
625
630
  }
626
631
  });
627
632
  });
633
+ const unsupportedUrls = [
634
+ "ftp://example.com/collection",
635
+ "file:///collection",
636
+ `data:application/ld+json,${encodeURIComponent(JSON.stringify({ id: "https://example.com/data" }))}`
637
+ ];
638
+ function isCredentialUrlError(error) {
639
+ return error instanceof UrlError && error.reason === "disallowed" && error.message === "URLs with credentials are not allowed";
640
+ }
641
+ test("getAuthenticatedDocumentLoader() rejects URLs with credentials", async (t) => {
642
+ const identity = {
643
+ keyId: new URL("https://example.com/key2"),
644
+ privateKey: rsaPrivateKey2
645
+ };
646
+ const start = "https://8.8.8.8/start";
647
+ const target = "https://user:pass@8.8.8.8/document";
648
+ for (const allowPrivateAddress of [false, true]) {
649
+ const loader = getAuthenticatedDocumentLoader(identity, { allowPrivateAddress });
650
+ for (const path of [
651
+ "direct",
652
+ "redirect",
653
+ "Link",
654
+ "HTML"
655
+ ]) await t.step(`${path}, allowPrivateAddress=${allowPrivateAddress}`, async () => {
656
+ await withFetchStub((request) => {
657
+ if (request.url !== start) return Response.json({});
658
+ if (path === "redirect") return Response.redirect(target, 302);
659
+ if (path === "HTML") return new Response(`<link rel="alternate" type="application/activity+json" href="${target}">`, { headers: { "Content-Type": "text/html" } });
660
+ return new Response("", { headers: {
661
+ "Content-Type": "text/plain",
662
+ Link: `<${target}>; rel="alternate"; type="application/activity+json"`
663
+ } });
664
+ }, async (fetched) => {
665
+ await rejects(loader(path === "direct" ? target : start), isCredentialUrlError);
666
+ deepStrictEqual(fetched.map((r) => r.url), path === "direct" ? [] : [start]);
667
+ });
668
+ });
669
+ for (const url of [
670
+ "https://user@8.8.8.8/document",
671
+ "https://:pass@8.8.8.8/document",
672
+ "https://%75ser:p%40ss@8.8.8.8/document",
673
+ "http://user:pass@8.8.8.8/document"
674
+ ]) await t.step(`${url}, allowPrivateAddress=${allowPrivateAddress}`, async () => {
675
+ await withFetchStub(() => Response.json({}), async (fetched) => {
676
+ await rejects(loader(url), isCredentialUrlError);
677
+ deepStrictEqual(fetched, []);
678
+ });
679
+ });
680
+ for (const url of ["https://:@8.8.8.8/document", "https://8.8.8.8/@user?q=@pass"]) await t.step(`allows ${url}, allowPrivateAddress=${allowPrivateAddress}`, async () => {
681
+ await withFetchStub(() => Response.json({ done: true }), async (fetched) => {
682
+ deepStrictEqual((await loader(url)).document, { done: true });
683
+ deepStrictEqual(fetched.map((r) => r.url), [new URL(url).href]);
684
+ });
685
+ });
686
+ }
687
+ for (const firstSpec of ["rfc9421", "draft-cavage-http-signatures-12"]) await t.step(`redirect after fallback from ${firstSpec}`, async () => {
688
+ const loader = getAuthenticatedDocumentLoader(identity, {
689
+ allowPrivateAddress: true,
690
+ specDeterminer: {
691
+ determineSpec: () => firstSpec,
692
+ rememberSpec: () => {}
693
+ }
694
+ });
695
+ await withFetchStub((request) => request.url !== start ? Response.json({}) : request.headers.has("Signature-Input") === (firstSpec === "rfc9421") ? new Response(null, { status: 401 }) : Response.redirect(target, 302), async (fetched) => {
696
+ await rejects(loader(start), isCredentialUrlError);
697
+ deepStrictEqual(fetched.map((r) => r.url), [start, start]);
698
+ deepStrictEqual(fetched.map((r) => r.headers.has("Signature-Input")), [firstSpec === "rfc9421", firstSpec !== "rfc9421"]);
699
+ });
700
+ });
701
+ });
702
+ function isUnsupportedProtocolError(error, url) {
703
+ return error instanceof UrlError && error.message === `Unsupported protocol: ${new URL(url).protocol}` && error.reason === "disallowed";
704
+ }
705
+ async function withFetchStub(respond, callback) {
706
+ const fetched = [];
707
+ const originalFetch = globalThis.fetch;
708
+ globalThis.fetch = ((input, init) => {
709
+ const request = input instanceof Request && init == null ? input : new Request(input, init);
710
+ fetched.push(request);
711
+ return Promise.resolve(respond(request));
712
+ });
713
+ try {
714
+ await callback(fetched);
715
+ } finally {
716
+ globalThis.fetch = originalFetch;
717
+ }
718
+ }
719
+ for (const url of unsupportedUrls) {
720
+ const scheme = new URL(url).protocol;
721
+ const loader = getAuthenticatedDocumentLoader({
722
+ keyId: new URL("https://example.com/key2"),
723
+ privateKey: rsaPrivateKey2
724
+ }, { allowPrivateAddress: true });
725
+ test(`getAuthenticatedDocumentLoader() rejects ${scheme} URLs with private addresses allowed before fetching`, async () => {
726
+ await withFetchStub(() => Response.json({}), async (fetched) => {
727
+ await rejects(loader(url), (e) => isUnsupportedProtocolError(e, url));
728
+ deepStrictEqual(fetched, []);
729
+ });
730
+ });
731
+ test(`getAuthenticatedDocumentLoader() rejects redirects to ${scheme} URLs with private addresses allowed`, async () => {
732
+ const start = "https://example.com/redirect-to-unsupported";
733
+ await withFetchStub(() => Response.redirect(url, 302), async (fetched) => {
734
+ await rejects(loader(start), (e) => isUnsupportedProtocolError(e, url));
735
+ deepStrictEqual(fetched.map((r) => r.url), [start]);
736
+ });
737
+ });
738
+ for (const firstSpec of ["rfc9421", "draft-cavage-http-signatures-12"]) test(`getAuthenticatedDocumentLoader() rejects redirects to ${scheme} URLs after signature fallback from ${firstSpec}`, async () => {
739
+ const start = "https://example.com/fallback-redirect-to-unsupported";
740
+ const fallbackLoader = getAuthenticatedDocumentLoader({
741
+ keyId: new URL("https://example.com/key2"),
742
+ privateKey: rsaPrivateKey2
743
+ }, {
744
+ allowPrivateAddress: true,
745
+ specDeterminer: {
746
+ determineSpec: () => firstSpec,
747
+ rememberSpec: () => {}
748
+ }
749
+ });
750
+ await withFetchStub((request) => request.headers.has("Signature-Input") === (firstSpec === "rfc9421") ? new Response("Unauthorized", { status: 401 }) : Response.redirect(url, 302), async (fetched) => {
751
+ await rejects(fallbackLoader(start), (e) => isUnsupportedProtocolError(e, url));
752
+ deepStrictEqual(fetched.map((r) => r.url), [start, start]);
753
+ deepStrictEqual(fetched.map((r) => r.headers.has("Signature-Input")), [firstSpec === "rfc9421", firstSpec !== "rfc9421"]);
754
+ });
755
+ });
756
+ for (const html of [false, true]) test(`getAuthenticatedDocumentLoader() rejects ${html ? "HTML" : "Link header"} alternates to ${scheme} URLs with private addresses allowed`, async () => {
757
+ const start = "https://example.com/alternate-to-unsupported";
758
+ await withFetchStub(() => html ? new Response(`<link rel="alternate" type="application/activity+json" href="${url}">`, { headers: { "Content-Type": "text/html" } }) : new Response("", { headers: {
759
+ "Content-Type": "text/plain",
760
+ Link: `<${url}>; rel="alternate"; type="application/activity+json"`
761
+ } }), async (fetched) => {
762
+ await rejects(loader(start), (e) => isUnsupportedProtocolError(e, url));
763
+ deepStrictEqual(fetched.map((r) => r.url), [start]);
764
+ });
765
+ });
766
+ }
628
767
  //#endregion
629
768
  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-BgQMAKIg.mjs";
4
+ import { n as kvCache, t as MockKvStore } from "../kv-cache-BxfOpUP9.mjs";
5
5
  import { preloadedContexts } from "@fedify/vocab-runtime";
6
6
  import { deepStrictEqual, throws } from "node:assert";
7
7
  import { createTestMeterProvider, mockDocumentLoader, test } from "@fedify/fixture";
@@ -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-D4throaQ.cjs");
3
+ const require_kv_cache = require("../kv-cache-HsXADkl9.cjs");
4
4
  exports.getAuthenticatedDocumentLoader = require_kv_cache.getAuthenticatedDocumentLoader;
5
5
  exports.kvCache = require_kv_cache.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-B0SZfzFb.js";
3
+ import { n as getAuthenticatedDocumentLoader, t as kvCache } from "../kv-cache-C0AsMZ4U.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.2",
3
+ "version": "2.4.4",
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/uri-template": "2.4.2",
155
- "@fedify/vocab-runtime": "2.4.2",
156
- "@fedify/webfinger": "2.4.2",
157
- "@fedify/vocab": "2.4.2"
154
+ "@fedify/uri-template": "2.4.4",
155
+ "@fedify/vocab-runtime": "2.4.4",
156
+ "@fedify/webfinger": "2.4.4",
157
+ "@fedify/vocab": "2.4.4"
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.2"
170
+ "@fedify/vocab-tools": "^2.4.4"
171
171
  },
172
172
  "scripts": {
173
173
  "build:self": "tsdown",