@fedify/fedify 2.1.28 → 2.1.30

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 (62) hide show
  1. package/dist/{assert_rejects-B-qJtC9Z.mjs → assert_rejects-WyYSTDDX.mjs} +2 -20
  2. package/dist/{assert_throws-4NwKEy2q.mjs → assert_throws-D4DNWrux.mjs} +19 -1
  3. package/dist/{builder-CW1BrLbm.mjs → builder-CTpfuuVp.mjs} +3 -3
  4. package/dist/compat/transformers.test.mjs +1 -1
  5. package/dist/{deno-CJgU2mcr.mjs → deno-BATJ1hq6.mjs} +1 -1
  6. package/dist/{docloader-o3fCIlIa.mjs → docloader-OLWc7P2D.mjs} +6 -2
  7. package/dist/federation/builder.test.mjs +3 -3
  8. package/dist/federation/collection.test.mjs +1 -1
  9. package/dist/federation/handler.test.mjs +262 -9
  10. package/dist/federation/idempotency.test.mjs +3 -3
  11. package/dist/federation/inbox.test.mjs +25 -2
  12. package/dist/federation/keycache.test.mjs +18 -7
  13. package/dist/federation/kv.test.mjs +1 -1
  14. package/dist/federation/middleware.test.mjs +128 -10
  15. package/dist/federation/mod.cjs +1 -1
  16. package/dist/federation/mod.js +1 -1
  17. package/dist/federation/mq.test.mjs +2 -2
  18. package/dist/federation/negotiation.test.mjs +2 -2
  19. package/dist/federation/retry.test.mjs +1 -1
  20. package/dist/federation/router.test.mjs +2 -3
  21. package/dist/federation/send.test.mjs +7 -6
  22. package/dist/federation/temporal.test.mjs +3 -3
  23. package/dist/federation/webfinger.test.mjs +2 -2
  24. package/dist/{http-Dcj1ulAh.mjs → http-Bjd6I92u.mjs} +2 -2
  25. package/dist/{http-EEfKB8Qb.cjs → http-BoR1M3W7.cjs} +17 -3
  26. package/dist/{http-DtypHuuK.js → http-CK3efjgM.js} +17 -3
  27. package/dist/{inbox-CVYVJI0M.mjs → inbox-9MlBGgs9.mjs} +23 -10
  28. package/dist/{key-DRLvwDTh.mjs → key-ClQh9Qqh.mjs} +17 -3
  29. package/dist/{keycache-RFlZObU3.mjs → keycache-DahCaG_m.mjs} +1 -1
  30. package/dist/{kv-cache-BWVKAAmc.cjs → kv-cache-BxSx_Iyp.cjs} +5 -1
  31. package/dist/{kv-cache-CEJCaF4E.js → kv-cache-ImApO425.js} +5 -1
  32. package/dist/{ld-C5BZ98YK.mjs → ld-Crg0Xmwr.mjs} +2 -2
  33. package/dist/{middleware-CqQoREZ9.mjs → middleware-Bi5D6DBT.mjs} +1 -1
  34. package/dist/{middleware-wCgWm4Eh.js → middleware-C2AJFk8W.js} +96 -26
  35. package/dist/{middleware-SSrXiJIV.mjs → middleware-CE0b69gj.mjs} +83 -26
  36. package/dist/{middleware-Dw9sPiec.cjs → middleware-CmD3lpy4.cjs} +97 -27
  37. package/dist/{middleware-DZxmsbPB.cjs → middleware-jL9p9V9G.cjs} +1 -1
  38. package/dist/mod.cjs +4 -4
  39. package/dist/mod.js +4 -4
  40. package/dist/nodeinfo/client.test.mjs +1 -1
  41. package/dist/nodeinfo/handler.test.mjs +2 -2
  42. package/dist/nodeinfo/types.test.mjs +2 -2
  43. package/dist/otel/exporter.test.mjs +1 -1
  44. package/dist/{owner-cPOoPvmr.mjs → owner-BRoKojK1.mjs} +2 -2
  45. package/dist/{proof-DPU3poip.js → proof-7R4s10TJ.js} +1 -1
  46. package/dist/{proof-De1y2j-e.cjs → proof-CHXVlsH_.cjs} +1 -1
  47. package/dist/{proof-BTknCsN7.mjs → proof-DMIAezxz.mjs} +2 -2
  48. package/dist/{send-DDuHNgHz.mjs → send-BHIt_8ia.mjs} +2 -2
  49. package/dist/sig/http.test.mjs +6 -6
  50. package/dist/sig/key.test.mjs +199 -5
  51. package/dist/sig/ld.test.mjs +49 -4
  52. package/dist/sig/mod.cjs +2 -2
  53. package/dist/sig/mod.js +2 -2
  54. package/dist/sig/owner.test.mjs +48 -3
  55. package/dist/sig/proof.test.mjs +54 -3
  56. package/dist/{std__assert-BUOJXi-Z.mjs → std__assert-DflMZha5.mjs} +2 -2
  57. package/dist/{temporal-CxXY--ZL.mjs → temporal-DR0JahMu.mjs} +1 -1
  58. package/dist/utils/docloader.test.mjs +178 -39
  59. package/dist/utils/mod.cjs +1 -1
  60. package/dist/utils/mod.js +1 -1
  61. package/package.json +5 -5
  62. /package/dist/{assert_not_equals--wG9hV7u.mjs → assert_not_equals-BkxW5QJI.mjs} +0 -0
@@ -3,11 +3,11 @@ import "urlpattern-polyfill";
3
3
  globalThis.addEventListener = () => {};
4
4
  import { t as esm_default } from "../esm-DVILvP5e.mjs";
5
5
  import { t as assertEquals } from "../assert_equals-Ew3jOFa3.mjs";
6
- import "../std__assert-BUOJXi-Z.mjs";
7
- import { t as assertRejects } from "../assert_rejects-B-qJtC9Z.mjs";
8
- import { l as verifyRequest } from "../http-Dcj1ulAh.mjs";
6
+ import "../std__assert-DflMZha5.mjs";
7
+ import { t as assertRejects } from "../assert_rejects-WyYSTDDX.mjs";
8
+ import { l as verifyRequest } from "../http-Bjd6I92u.mjs";
9
9
  import { i as rsaPrivateKey2 } from "../keys-CtZLJq76.mjs";
10
- import { t as getAuthenticatedDocumentLoader } from "../docloader-o3fCIlIa.mjs";
10
+ import { t as getAuthenticatedDocumentLoader } from "../docloader-OLWc7P2D.mjs";
11
11
  import { mockDocumentLoader, test } from "@fedify/fixture";
12
12
  import { FetchError, UrlError } from "@fedify/vocab-runtime";
13
13
  import { configure, reset } from "@logtape/logtape";
@@ -83,45 +83,50 @@ test("getAuthenticatedDocumentLoader() validates redirects", async (t) => {
83
83
  });
84
84
  esm_default.hardReset();
85
85
  });
86
- test("getAuthenticatedDocumentLoader() cancellation", {
87
- sanitizeResources: false,
88
- sanitizeOps: false
89
- }, async (t) => {
86
+ test("getAuthenticatedDocumentLoader() cancellation", async (t) => {
90
87
  esm_default.spyGlobal();
91
- await t.step("document loader cancellation", async () => {
92
- esm_default.get("https://example.com/slow-object", () => new Promise((resolve) => {
93
- setTimeout(() => {
94
- resolve({
95
- status: 200,
96
- headers: { "Content-Type": "application/activity+json" },
97
- body: {
98
- "@context": "https://www.w3.org/ns/activitystreams",
99
- type: "Note",
100
- content: "Slow response"
101
- }
88
+ try {
89
+ await t.step("document loader cancellation", async () => {
90
+ let responseTimer;
91
+ try {
92
+ esm_default.get("https://example.com/slow-object", () => new Promise((resolve) => {
93
+ responseTimer = setTimeout(() => {
94
+ resolve({
95
+ status: 200,
96
+ headers: { "Content-Type": "application/activity+json" },
97
+ body: {
98
+ "@context": "https://www.w3.org/ns/activitystreams",
99
+ type: "Note",
100
+ content: "Slow response"
101
+ }
102
+ });
103
+ }, 1e3);
104
+ }));
105
+ const loader = getAuthenticatedDocumentLoader({
106
+ keyId: new URL("https://example.com/key2"),
107
+ privateKey: rsaPrivateKey2
102
108
  });
103
- }, 1e3);
104
- }));
105
- const loader = getAuthenticatedDocumentLoader({
106
- keyId: new URL("https://example.com/key2"),
107
- privateKey: rsaPrivateKey2
109
+ const controller = new AbortController();
110
+ const promise = loader("https://example.com/slow-object", { signal: controller.signal });
111
+ controller.abort();
112
+ await assertRejects(() => promise, Error);
113
+ await assertRejects(() => loader("https://example.com/object", { signal: controller.signal }), Error);
114
+ } finally {
115
+ clearTimeout(responseTimer);
116
+ }
108
117
  });
109
- const controller = new AbortController();
110
- const promise = loader("https://example.com/slow-object", { signal: controller.signal });
111
- controller.abort();
112
- await assertRejects(() => promise, Error);
113
- await assertRejects(() => loader("https://example.com/object", { signal: controller.signal }), Error);
114
- });
115
- await t.step("immediate cancellation", async () => {
116
- const loader = getAuthenticatedDocumentLoader({
117
- keyId: new URL("https://example.com/key2"),
118
- privateKey: rsaPrivateKey2
118
+ await t.step("immediate cancellation", async () => {
119
+ const loader = getAuthenticatedDocumentLoader({
120
+ keyId: new URL("https://example.com/key2"),
121
+ privateKey: rsaPrivateKey2
122
+ });
123
+ const controller = new AbortController();
124
+ controller.abort();
125
+ await assertRejects(() => loader("https://example.com/object", { signal: controller.signal }), Error);
119
126
  });
120
- const controller = new AbortController();
121
- controller.abort();
122
- await assertRejects(() => loader("https://example.com/object", { signal: controller.signal }), Error);
123
- });
124
- esm_default.hardReset();
127
+ } finally {
128
+ esm_default.hardReset();
129
+ }
125
130
  });
126
131
  test("getAuthenticatedDocumentLoader() bounds JSON after redirects", async () => {
127
132
  esm_default.mockGlobal();
@@ -440,5 +445,139 @@ test("getAuthenticatedDocumentLoader() rejects cancellation before fetching", as
440
445
  esm_default.hardReset();
441
446
  }
442
447
  });
448
+ const unsupportedUrls = [
449
+ "ftp://example.com/collection",
450
+ "file:///collection",
451
+ `data:application/ld+json,${encodeURIComponent(JSON.stringify({ id: "https://example.com/data" }))}`
452
+ ];
453
+ function isCredentialUrlError(error) {
454
+ return error instanceof UrlError && error.reason === "disallowed" && error.message === "URLs with credentials are not allowed";
455
+ }
456
+ test("getAuthenticatedDocumentLoader() rejects URLs with credentials", async (t) => {
457
+ const identity = {
458
+ keyId: new URL("https://example.com/key2"),
459
+ privateKey: rsaPrivateKey2
460
+ };
461
+ const start = "https://8.8.8.8/start";
462
+ const target = "https://user:pass@8.8.8.8/document";
463
+ for (const allowPrivateAddress of [false, true]) {
464
+ const loader = getAuthenticatedDocumentLoader(identity, { allowPrivateAddress });
465
+ for (const path of [
466
+ "direct",
467
+ "redirect",
468
+ "Link",
469
+ "HTML"
470
+ ]) await t.step(`${path}, allowPrivateAddress=${allowPrivateAddress}`, async () => {
471
+ await withFetchStub((request) => {
472
+ if (request.url !== start) return Response.json({});
473
+ if (path === "redirect") return Response.redirect(target, 302);
474
+ if (path === "HTML") return new Response(`<link rel="alternate" type="application/activity+json" href="${target}">`, { headers: { "Content-Type": "text/html" } });
475
+ return new Response("", { headers: {
476
+ "Content-Type": "text/plain",
477
+ Link: `<${target}>; rel="alternate"; type="application/activity+json"`
478
+ } });
479
+ }, async (fetched) => {
480
+ await rejects(loader(path === "direct" ? target : start), isCredentialUrlError);
481
+ deepStrictEqual(fetched.map((r) => r.url), path === "direct" ? [] : [start]);
482
+ });
483
+ });
484
+ for (const url of [
485
+ "https://user@8.8.8.8/document",
486
+ "https://:pass@8.8.8.8/document",
487
+ "https://%75ser:p%40ss@8.8.8.8/document",
488
+ "http://user:pass@8.8.8.8/document"
489
+ ]) await t.step(`${url}, allowPrivateAddress=${allowPrivateAddress}`, async () => {
490
+ await withFetchStub(() => Response.json({}), async (fetched) => {
491
+ await rejects(loader(url), isCredentialUrlError);
492
+ deepStrictEqual(fetched, []);
493
+ });
494
+ });
495
+ 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 () => {
496
+ await withFetchStub(() => Response.json({ done: true }), async (fetched) => {
497
+ deepStrictEqual((await loader(url)).document, { done: true });
498
+ deepStrictEqual(fetched.map((r) => r.url), [new URL(url).href]);
499
+ });
500
+ });
501
+ }
502
+ for (const firstSpec of ["rfc9421", "draft-cavage-http-signatures-12"]) await t.step(`redirect after fallback from ${firstSpec}`, async () => {
503
+ const loader = getAuthenticatedDocumentLoader(identity, {
504
+ allowPrivateAddress: true,
505
+ specDeterminer: {
506
+ determineSpec: () => firstSpec,
507
+ rememberSpec: () => {}
508
+ }
509
+ });
510
+ 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) => {
511
+ await rejects(loader(start), isCredentialUrlError);
512
+ deepStrictEqual(fetched.map((r) => r.url), [start, start]);
513
+ deepStrictEqual(fetched.map((r) => r.headers.has("Signature-Input")), [firstSpec === "rfc9421", firstSpec !== "rfc9421"]);
514
+ });
515
+ });
516
+ });
517
+ function isUnsupportedProtocolError(error, url) {
518
+ return error instanceof UrlError && error.message === `Unsupported protocol: ${new URL(url).protocol}` && error.reason === "disallowed";
519
+ }
520
+ async function withFetchStub(respond, callback) {
521
+ const fetched = [];
522
+ const originalFetch = globalThis.fetch;
523
+ globalThis.fetch = ((input, init) => {
524
+ const request = input instanceof Request && init == null ? input : new Request(input, init);
525
+ fetched.push(request);
526
+ return Promise.resolve(respond(request));
527
+ });
528
+ try {
529
+ await callback(fetched);
530
+ } finally {
531
+ globalThis.fetch = originalFetch;
532
+ }
533
+ }
534
+ for (const url of unsupportedUrls) {
535
+ const scheme = new URL(url).protocol;
536
+ const loader = getAuthenticatedDocumentLoader({
537
+ keyId: new URL("https://example.com/key2"),
538
+ privateKey: rsaPrivateKey2
539
+ }, { allowPrivateAddress: true });
540
+ test(`getAuthenticatedDocumentLoader() rejects ${scheme} URLs with private addresses allowed before fetching`, async () => {
541
+ await withFetchStub(() => Response.json({}), async (fetched) => {
542
+ await rejects(loader(url), (e) => isUnsupportedProtocolError(e, url));
543
+ deepStrictEqual(fetched, []);
544
+ });
545
+ });
546
+ test(`getAuthenticatedDocumentLoader() rejects redirects to ${scheme} URLs with private addresses allowed`, async () => {
547
+ const start = "https://example.com/redirect-to-unsupported";
548
+ await withFetchStub(() => Response.redirect(url, 302), async (fetched) => {
549
+ await rejects(loader(start), (e) => isUnsupportedProtocolError(e, url));
550
+ deepStrictEqual(fetched.map((r) => r.url), [start]);
551
+ });
552
+ });
553
+ for (const firstSpec of ["rfc9421", "draft-cavage-http-signatures-12"]) test(`getAuthenticatedDocumentLoader() rejects redirects to ${scheme} URLs after signature fallback from ${firstSpec}`, async () => {
554
+ const start = "https://example.com/fallback-redirect-to-unsupported";
555
+ const fallbackLoader = getAuthenticatedDocumentLoader({
556
+ keyId: new URL("https://example.com/key2"),
557
+ privateKey: rsaPrivateKey2
558
+ }, {
559
+ allowPrivateAddress: true,
560
+ specDeterminer: {
561
+ determineSpec: () => firstSpec,
562
+ rememberSpec: () => {}
563
+ }
564
+ });
565
+ await withFetchStub((request) => request.headers.has("Signature-Input") === (firstSpec === "rfc9421") ? new Response("Unauthorized", { status: 401 }) : Response.redirect(url, 302), async (fetched) => {
566
+ await rejects(fallbackLoader(start), (e) => isUnsupportedProtocolError(e, url));
567
+ deepStrictEqual(fetched.map((r) => r.url), [start, start]);
568
+ deepStrictEqual(fetched.map((r) => r.headers.has("Signature-Input")), [firstSpec === "rfc9421", firstSpec !== "rfc9421"]);
569
+ });
570
+ });
571
+ for (const html of [false, true]) test(`getAuthenticatedDocumentLoader() rejects ${html ? "HTML" : "Link header"} alternates to ${scheme} URLs with private addresses allowed`, async () => {
572
+ const start = "https://example.com/alternate-to-unsupported";
573
+ await withFetchStub(() => html ? new Response(`<link rel="alternate" type="application/activity+json" href="${url}">`, { headers: { "Content-Type": "text/html" } }) : new Response("", { headers: {
574
+ "Content-Type": "text/plain",
575
+ Link: `<${url}>; rel="alternate"; type="application/activity+json"`
576
+ } }), async (fetched) => {
577
+ await rejects(loader(start), (e) => isUnsupportedProtocolError(e, url));
578
+ deepStrictEqual(fetched.map((r) => r.url), [start]);
579
+ });
580
+ });
581
+ }
443
582
  //#endregion
444
583
  export {};
@@ -1,6 +1,6 @@
1
1
  const { Temporal } = require("@js-temporal/polyfill");
2
2
  const { URLPattern } = require("urlpattern-polyfill");
3
3
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
4
- const require_kv_cache = require("../kv-cache-BWVKAAmc.cjs");
4
+ const require_kv_cache = require("../kv-cache-BxSx_Iyp.cjs");
5
5
  exports.getAuthenticatedDocumentLoader = require_kv_cache.getAuthenticatedDocumentLoader;
6
6
  exports.kvCache = require_kv_cache.kvCache;
package/dist/utils/mod.js CHANGED
@@ -1,4 +1,4 @@
1
1
  import { Temporal } from "@js-temporal/polyfill";
2
2
  import { URLPattern } from "urlpattern-polyfill";
3
- import { n as getAuthenticatedDocumentLoader, t as kvCache } from "../kv-cache-CEJCaF4E.js";
3
+ import { n as getAuthenticatedDocumentLoader, t as kvCache } from "../kv-cache-ImApO425.js";
4
4
  export { getAuthenticatedDocumentLoader, kvCache };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fedify/fedify",
3
- "version": "2.1.28",
3
+ "version": "2.1.30",
4
4
  "description": "An ActivityPub server framework",
5
5
  "keywords": [
6
6
  "ActivityPub",
@@ -144,9 +144,9 @@
144
144
  "uri-template-router": "^1.0.0",
145
145
  "url-template": "^3.1.1",
146
146
  "urlpattern-polyfill": "^10.1.0",
147
- "@fedify/vocab": "2.1.28",
148
- "@fedify/webfinger": "2.1.28",
149
- "@fedify/vocab-runtime": "2.1.28"
147
+ "@fedify/vocab": "2.1.30",
148
+ "@fedify/webfinger": "2.1.30",
149
+ "@fedify/vocab-runtime": "2.1.30"
150
150
  },
151
151
  "devDependencies": {
152
152
  "@std/assert": "npm:@jsr/std__assert@^0.226.0",
@@ -159,7 +159,7 @@
159
159
  "typescript": "^6.0.0",
160
160
  "wrangler": "^4.17.0",
161
161
  "@fedify/fixture": "2.0.0",
162
- "@fedify/vocab-tools": "^2.1.28"
162
+ "@fedify/vocab-tools": "^2.1.30"
163
163
  },
164
164
  "scripts": {
165
165
  "build:self": "tsdown",