@fedify/fedify 2.4.0-dev.2084 → 2.4.0-dev.2093

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 (75) hide show
  1. package/dist/{builder-BGlpUocK.mjs → builder-Ld4Y9ymR.mjs} +34 -3
  2. package/dist/compat/transformers.test.mjs +1 -1
  3. package/dist/{compound-proof-B6Aj2kGY.mjs → compound-proof-CTx_lewQ.mjs} +191 -13
  4. package/dist/{deno-Dn-py9Zy.mjs → deno-8t8k_6Yv.mjs} +1 -1
  5. package/dist/{docloader-BUq5bYJr.mjs → docloader-DJKcsAiF.mjs} +2 -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 +3 -3
  9. package/dist/federation/handler.test.mjs +5 -5
  10. package/dist/federation/hashlink-media.test.mjs +2 -2
  11. package/dist/federation/idempotency.test.mjs +2 -2
  12. package/dist/federation/metrics.test.mjs +1 -1
  13. package/dist/federation/middleware.test.mjs +7 -7
  14. package/dist/federation/mod.cjs +1 -1
  15. package/dist/federation/mod.js +1 -1
  16. package/dist/federation/outgoing-proof.test.mjs +254 -5
  17. package/dist/federation/portable-gateway-keys.test.mjs +72 -11
  18. package/dist/federation/portable-inbox.test.mjs +116 -4
  19. package/dist/federation/portable.test.mjs +182 -4
  20. package/dist/federation/send.test.mjs +3 -3
  21. package/dist/federation/tasks/enqueue.test.mjs +1 -1
  22. package/dist/federation/tasks/tasks.test.mjs +2 -2
  23. package/dist/federation/temporal.test.mjs +1 -1
  24. package/dist/federation/webfinger.test.mjs +89 -1
  25. package/dist/{federation-BLGx8JUH.cjs → federation-aSHO04ob.cjs} +360 -65
  26. package/dist/{http-B0VRNipE.mjs → http-B2ftY0Ha.mjs} +4 -4
  27. package/dist/{http-B5q1CHtL.cjs → http-DSaSAN7X.cjs} +1609 -1457
  28. package/dist/{http-4PCCvhsU.js → http-DyT7DP1Y.js} +1518 -1390
  29. package/dist/{key-BJrp524f.mjs → key-Dwm4VswT.mjs} +39 -99
  30. package/dist/{kv-cache-Dhhhrryv.cjs → kv-cache-BSJAlP4C.cjs} +1 -1
  31. package/dist/{kv-cache-C4C3LyYW.mjs → kv-cache-D0yYukqk.mjs} +1 -1
  32. package/dist/{kv-cache-1KUmAPH7.js → kv-cache-DmroAz3M.js} +1 -1
  33. package/dist/{ld-BmFz5NLA.mjs → ld-BYZOoDzZ.mjs} +3 -3
  34. package/dist/{metrics-Beod6tzO.mjs → metrics-B7YwKTFr.mjs} +1 -1
  35. package/dist/{middleware-B7CNsyqt.mjs → middleware-BNeqKSsL.mjs} +1 -1
  36. package/dist/{middleware-D_9pNYe9.js → middleware-Blq0882d.js} +368 -73
  37. package/dist/{middleware-BKeQYPm0.mjs → middleware-E_0IH72m.mjs} +166 -71
  38. package/dist/{mod-HDSvS-ww.d.cts → mod-C8LieBzF.d.cts} +16 -3
  39. package/dist/{mod-C9WSpZWm.d.ts → mod-Coacostr.d.ts} +16 -3
  40. package/dist/mod.cjs +5 -5
  41. package/dist/mod.d.cts +1 -1
  42. package/dist/mod.d.ts +1 -1
  43. package/dist/mod.js +5 -5
  44. package/dist/nodeinfo/handler.test.mjs +1 -1
  45. package/dist/{owner-CsKVgMzk.mjs → owner-Cgx2Dc7I.mjs} +5 -4
  46. package/dist/{owner-_pbbgLJK.js → owner-DAGSC5gX.js} +2 -2
  47. package/dist/{owner-BQFZtFV4.cjs → owner-szIZl4qI.cjs} +2 -2
  48. package/dist/{portable-key-B4R-CiIV.mjs → portable-key-Cn1WJSN1.mjs} +60 -41
  49. package/dist/portable-key-id-Bva5iT3W.mjs +136 -0
  50. package/dist/{proof-ozG4HfcQ.mjs → proof-DVXC6nYz.mjs} +59 -18
  51. package/dist/{send-ClPt8nYu.mjs → send-D-hOvYKy.mjs} +3 -3
  52. package/dist/sig/compound-proof-discovery.test.mjs +1 -1
  53. package/dist/sig/compound-proof-producer.test.mjs +1 -1
  54. package/dist/sig/compound-proof-verification.test.mjs +160 -3
  55. package/dist/sig/compound-proof.test.mjs +2 -2
  56. package/dist/sig/http.test.mjs +2 -2
  57. package/dist/sig/key.test.mjs +1 -1
  58. package/dist/sig/ld.test.mjs +2 -2
  59. package/dist/sig/mod.cjs +3 -3
  60. package/dist/sig/mod.d.cts +1 -1
  61. package/dist/sig/mod.d.ts +1 -1
  62. package/dist/sig/mod.js +3 -3
  63. package/dist/sig/owner.test.mjs +1 -1
  64. package/dist/sig/portable-collection.test.mjs +69 -5
  65. package/dist/sig/portable-key.test.mjs +176 -10
  66. package/dist/sig/proof.test.mjs +107 -4
  67. package/dist/{sig-DhdomAN0.js → sig-B5Ell7pO.js} +19 -3
  68. package/dist/{sig-k3C8sF7g.cjs → sig-CXOKN4Br.cjs} +19 -3
  69. package/dist/{temporal-Ba0EAZAI.mjs → temporal-CqpKGEPk.mjs} +1 -1
  70. package/dist/testing/kv.test.mjs +1 -1
  71. package/dist/utils/docloader.test.mjs +2 -2
  72. package/dist/utils/kv-cache.test.mjs +1 -1
  73. package/dist/utils/mod.cjs +1 -1
  74. package/dist/utils/mod.js +1 -1
  75. package/package.json +7 -7
@@ -7,12 +7,13 @@ import { t as assertEquals } from "../assert_equals-Z6IcEbWU.mjs";
7
7
  import "../std__assert-DvMDRPes.mjs";
8
8
  import { t as assertRejects } from "../assert_rejects-CTptdYck.mjs";
9
9
  import { t as assert } from "../assert-D1Eafmd_.mjs";
10
- import { t as exportJwk } from "../key-BJrp524f.mjs";
11
- import { s as detachSignature } from "../ld-BmFz5NLA.mjs";
12
- import { r as signObject } from "../proof-ozG4HfcQ.mjs";
10
+ import { t as exportJwk } from "../key-Dwm4VswT.mjs";
11
+ import { s as detachSignature } from "../ld-BYZOoDzZ.mjs";
12
+ import { i as signObject } from "../proof-DVXC6nYz.mjs";
13
13
  import { i as rsaPrivateKey2, n as ed25519PrivateKey, r as ed25519PublicKey, s as rsaPublicKey2 } from "../keys-DIJNHW-Y.mjs";
14
- import { n as FederationImpl, o as createFederation } from "../middleware-BKeQYPm0.mjs";
15
- import { a as verifyCompoundPortableObjectProofs } from "../compound-proof-B6Aj2kGY.mjs";
14
+ import { c as assertPortableActorActivity, n as FederationImpl, o as createFederation } from "../middleware-E_0IH72m.mjs";
15
+ import { a as verifyCompoundPortableObjectProofs } from "../compound-proof-CTx_lewQ.mjs";
16
+ import { configure, reset } from "@logtape/logtape";
16
17
  import { Create, Note, Person } from "@fedify/vocab";
17
18
  import { mockDocumentLoader, test } from "@fedify/fixture";
18
19
  import { exportDidKey, parseIri } from "@fedify/vocab-runtime";
@@ -222,6 +223,29 @@ test("a non-portable activity embedding portable objects needs one key", async (
222
223
  assertEquals(accepted.bodies.length, 1);
223
224
  assertEquals(proofOf(accepted.bodies[0]).verificationMethod, ed25519PublicKey.id.href);
224
225
  });
226
+ test("a non-portable activity embedding compatible-ID objects needs one key", async () => {
227
+ const owner = await didKey();
228
+ const compatibleId = (path) => new URL(`https://gw.example/.well-known/apgateway/${owner.did}${path}`);
229
+ const child = await signObject(new Note({
230
+ id: compatibleId(`/objects/${crypto.randomUUID()}`),
231
+ attribution: compatibleId("/actor"),
232
+ content: "A portable note"
233
+ }), owner.privateKey, owner.keyId, {
234
+ ...options,
235
+ context: childContext,
236
+ created
237
+ });
238
+ const ctx = createTestFederation().createContext(new URL("https://example.com/"));
239
+ const keys = [sender(await didKey()), sender(await didKey())];
240
+ assertEquals((await capture(() => assertRejects(() => ctx.sendActivity([rsaKey, ...keys], recipient, httpCreate(child)), TypeError, "exactly one Ed25519 key"))).bodies.length, 0);
241
+ const key = {
242
+ keyId: ed25519PublicKey.id,
243
+ privateKey: ed25519PrivateKey
244
+ };
245
+ const accepted = await capture(() => ctx.sendActivity([rsaKey, key], recipient, httpCreate(child)));
246
+ assertEquals(accepted.bodies.length, 1);
247
+ await assertCompoundVerifies(accepted.bodies[0]);
248
+ });
225
249
  test("an activity without portable objects is still signed by every key", async () => {
226
250
  const ctx = createTestFederation().createContext(new URL("https://example.com/"));
227
251
  const keys = [await didKey(), await didKey()];
@@ -392,5 +416,230 @@ test("a Fedify inbox accepts a portable activity Fedify produced", async () => {
392
416
  });
393
417
  assertEquals([(await receiving.fetch(requests[0], { contextData: void 0 })).status, received], [202, 1]);
394
418
  });
419
+ const gateway = "https://gw.example";
420
+ function compatibleId(did, path, origin = gateway) {
421
+ return new URL(`${origin}/.well-known/apgateway/${did}${path}`);
422
+ }
423
+ function compatibleCreate(owner, object, { id, actor } = {}) {
424
+ return new Create({
425
+ id: id ?? compatibleId(owner.did, `/activities/${crypto.randomUUID()}`),
426
+ actor: actor ?? compatibleId(owner.did, "/actor"),
427
+ object
428
+ });
429
+ }
430
+ test("a compatible-ID activity is signed only by the key matching its DID", async () => {
431
+ const owner = await didKey();
432
+ const childOwner = await didKey();
433
+ const child = await signedChild(childOwner);
434
+ const ctx = createTestFederation().createContext(new URL("https://example.com/"));
435
+ const { bodies, requests } = await capture(async () => ctx.sendActivity([
436
+ rsaKey,
437
+ sender(await didKey()),
438
+ sender(owner),
439
+ sender(childOwner)
440
+ ], recipient, compatibleCreate(owner, child)));
441
+ assertEquals(bodies.length, 1);
442
+ assertEquals(proofOf(bodies[0]).verificationMethod, owner.keyId.href);
443
+ assert(requests[0].headers.has("Signature"));
444
+ assertEquals(bodies[0].signature, void 0);
445
+ await assertCompoundVerifies(bodies[0]);
446
+ });
447
+ test("a compatible-ID actor's activity needs an ID of the actor's DID", async () => {
448
+ const owner = await didKey();
449
+ const other = await didKey();
450
+ const cases = [
451
+ [
452
+ "a compatible activity ID of another DID",
453
+ compatibleCreate(owner, void 0, { id: compatibleId(other.did, "/activities/1") }),
454
+ "with the same DID"
455
+ ],
456
+ [
457
+ "an ap: activity ID of another DID",
458
+ compatibleCreate(owner, void 0, { id: parseIri(`ap://${other.did}/activities/1`) }),
459
+ "with the same DID"
460
+ ],
461
+ [
462
+ "an ordinary activity ID",
463
+ compatibleCreate(owner, void 0, { id: new URL("https://gw.example/activities/1") }),
464
+ "with the same DID"
465
+ ],
466
+ [
467
+ "a malformed compatible activity ID",
468
+ compatibleCreate(owner, void 0, { id: new URL(compatibleId(owner.did, "/activities/1").href + "?@gateway=https%3A%2F%2Fgw.example") }),
469
+ "with the same DID"
470
+ ],
471
+ [
472
+ "a malformed compatible actor ID",
473
+ compatibleCreate(owner, void 0, { actor: new URL(compatibleId(owner.did, "/actor").href + "?@gateway=https%3A%2F%2Fgw.example") }),
474
+ "malformed FEP-ef61 portable ID"
475
+ ],
476
+ [
477
+ "compatible actors of different DIDs",
478
+ new Create({
479
+ id: compatibleId(owner.did, "/activities/1"),
480
+ actors: [compatibleId(owner.did, "/actor"), compatibleId(other.did, "/actor")]
481
+ }),
482
+ "different DIDs"
483
+ ]
484
+ ];
485
+ for (const [name, activity, message] of cases) {
486
+ const ctx = createTestFederation().createContext(new URL("https://example.com/"));
487
+ const { bodies } = await capture(() => assertRejects(() => ctx.sendActivity([rsaKey, sender(owner)], recipient, activity), TypeError, message));
488
+ assertEquals(bodies.length, 0, name);
489
+ }
490
+ });
491
+ test("a compatible-ID activity without exactly one DID-matching key is rejected", async () => {
492
+ const owner = await didKey();
493
+ const cases = [
494
+ [
495
+ "no key matches",
496
+ [sender(await didKey())],
497
+ "none of its 1 Ed25519 keys"
498
+ ],
499
+ [
500
+ "only a gateway key",
501
+ [{
502
+ keyId: compatibleId(owner.did, "/actor#main-key"),
503
+ privateKey: owner.privateKey
504
+ }],
505
+ "none of its 0 Ed25519 keys"
506
+ ],
507
+ [
508
+ "several keys match",
509
+ [sender(owner), {
510
+ keyId: new URL(`${owner.did}#second`),
511
+ privateKey: owner.privateKey
512
+ }],
513
+ "2 of its Ed25519 keys"
514
+ ]
515
+ ];
516
+ for (const [name, keys, message] of cases) {
517
+ const ctx = createTestFederation().createContext(new URL("https://example.com/"));
518
+ const { bodies } = await capture(() => assertRejects(() => ctx.sendActivity([rsaKey, ...keys], recipient, compatibleCreate(owner)), TypeError, message));
519
+ assertEquals(bodies.length, 0, name);
520
+ }
521
+ });
522
+ test("compatible and ap: IDs of the same DID can be mixed", async () => {
523
+ const owner = await didKey();
524
+ const activities = [
525
+ compatibleCreate(owner, void 0, { id: parseIri(`ap://${owner.did}/activities/${crypto.randomUUID()}`) }),
526
+ compatibleCreate(owner, void 0, { actor: parseIri(`ap+ef61://${owner.did}/actor`) }),
527
+ compatibleCreate(owner, void 0, { id: compatibleId(owner.did, "/activities/1", "https://other.example") })
528
+ ];
529
+ for (const activity of activities) {
530
+ const ctx = createTestFederation().createContext(new URL("https://example.com/"));
531
+ const { bodies } = await capture(async () => ctx.sendActivity([
532
+ rsaKey,
533
+ sender(await didKey()),
534
+ sender(owner)
535
+ ], recipient, activity));
536
+ assertEquals(bodies.length, 1, activity.id?.href);
537
+ assertEquals(proofOf(bodies[0]).verificationMethod, owner.keyId.href);
538
+ }
539
+ });
540
+ test("a pre-signed compatible-ID activity is not re-signed", async () => {
541
+ const owner = await didKey();
542
+ const activity = await signObject(compatibleCreate(owner, await signedChild(await didKey())), owner.privateKey, owner.keyId, {
543
+ ...options,
544
+ created
545
+ });
546
+ const ctx = createTestFederation().createContext(new URL("https://example.com/"));
547
+ const { bodies } = await capture(async () => ctx.sendActivity([
548
+ rsaKey,
549
+ sender(await didKey()),
550
+ sender(await didKey())
551
+ ], recipient, activity));
552
+ assertEquals(bodies.length, 1);
553
+ assertEquals(proofOf(bodies[0]).verificationMethod, owner.keyId.href);
554
+ await assertCompoundVerifies(bodies[0]);
555
+ });
556
+ test("forced fanout selects the DID-matching key for a compatible-ID activity", async () => {
557
+ const owner = await didKey();
558
+ const { queue, queued } = createQueue();
559
+ const federation = createTestFederation(queue);
560
+ const ctx = federation.createContext(new URL("https://example.com/"));
561
+ const { bodies } = await capture(async () => {
562
+ await ctx.sendActivity([
563
+ rsaKey,
564
+ sender(await didKey()),
565
+ sender(owner)
566
+ ], recipient, compatibleCreate(owner, await signedChild(await didKey())), { fanout: "force" });
567
+ assertEquals(queued.length, 1);
568
+ for (let i = 0; i < queued.length; i++) await federation.processQueuedTask(void 0, queued[i]);
569
+ });
570
+ assertEquals(bodies.length, 1);
571
+ assertEquals(proofOf(bodies[0]).verificationMethod, owner.keyId.href);
572
+ await assertCompoundVerifies(bodies[0]);
573
+ });
574
+ test("a Fedify inbox accepts an activity Fedify produced for a compatible-ID actor", async () => {
575
+ const owner = await didKey();
576
+ const child = await signedChild(await didKey());
577
+ const ctx = createTestFederation().createContext(new URL("https://example.com/"));
578
+ const { requests } = await capture(async () => ctx.sendActivity([sender(await didKey()), sender(owner)], recipient, compatibleCreate(owner, child)));
579
+ assertEquals(requests.length, 1);
580
+ let received = 0;
581
+ const receiving = createFederation({
582
+ kv: new MemoryKvStore(),
583
+ contextLoaderFactory: () => mockDocumentLoader,
584
+ documentLoaderFactory: () => mockDocumentLoader
585
+ });
586
+ receiving.setActorDispatcher("/users/{identifier}", () => null);
587
+ receiving.setInboxListeners("/users/{identifier}/inbox", "/inbox").on(Create, () => {
588
+ received++;
589
+ });
590
+ const body = await requests[0].text();
591
+ const deliver = (json) => receiving.fetch(new Request(requests[0].url, {
592
+ method: "POST",
593
+ headers: requests[0].headers,
594
+ body: json
595
+ }), { contextData: void 0 });
596
+ assertEquals([(await deliver(body)).status, received], [202, 1]);
597
+ const forged = JSON.parse(body);
598
+ forged.actor = compatibleId((await didKey()).did, "/actor").href;
599
+ assertEquals([(await deliver(JSON.stringify(forged))).status, received], [401, 1]);
600
+ });
601
+ async function captureLogs(run) {
602
+ const records = [];
603
+ await reset();
604
+ try {
605
+ await configure({
606
+ sinks: { buffer: (record) => records.push(record) },
607
+ filters: {},
608
+ loggers: [{
609
+ category: ["logtape", "meta"],
610
+ sinks: []
611
+ }, {
612
+ category: [],
613
+ sinks: ["buffer"],
614
+ lowestLevel: "warning"
615
+ }]
616
+ });
617
+ await run();
618
+ } finally {
619
+ await reset();
620
+ }
621
+ return records.filter((record) => record.category[0] === "fedify");
622
+ }
623
+ test("an activity on another gateway than its compatible-ID actor is warned about", async () => {
624
+ const owner = await didKey();
625
+ const warnings = (activity) => captureLogs(() => assertPortableActorActivity(activity)).then((records) => records.map((r) => r.properties.activityId));
626
+ const other = compatibleId(owner.did, "/activities/1", "https://other.example");
627
+ assertEquals(await warnings(compatibleCreate(owner, void 0, { id: other })), [other.href]);
628
+ assertEquals(await warnings(compatibleCreate(owner)), []);
629
+ assertEquals(await warnings(compatibleCreate(owner, void 0, { id: parseIri(`ap://${owner.did}/activities/1`) })), []);
630
+ assertEquals(await warnings(compatibleCreate(owner, void 0, {
631
+ id: other,
632
+ actor: parseIri(`ap://${owner.did}/actor`)
633
+ })), []);
634
+ });
635
+ test("an unsigned activity with a malformed compatible ID is rejected", async () => {
636
+ const owner = await didKey();
637
+ const ctx = createTestFederation().createContext(new URL("https://example.com/"));
638
+ const { bodies } = await capture(() => assertRejects(() => ctx.sendActivity([rsaKey, sender(owner)], recipient, new Create({
639
+ id: new URL(compatibleId(owner.did, "/activities/1").href + "?@gateway=https%3A%2F%2Fgw.example"),
640
+ actor: new URL("https://example.com/users/alice")
641
+ })), TypeError, "malformed FEP-ef61 portable ID"));
642
+ assertEquals(bodies.length, 0);
643
+ });
395
644
  //#endregion
396
645
  export {};
@@ -7,10 +7,10 @@ import { t as MemoryKvStore } from "../kv-DCHkaJ9-.mjs";
7
7
  import { t as assertEquals } from "../assert_equals-Z6IcEbWU.mjs";
8
8
  import "../std__assert-DvMDRPes.mjs";
9
9
  import { t as assertRejects } from "../assert_rejects-CTptdYck.mjs";
10
- import { p as signJsonLd } from "../ld-BmFz5NLA.mjs";
11
- import { s as signRequest } from "../http-B0VRNipE.mjs";
10
+ import { p as signJsonLd } from "../ld-BYZOoDzZ.mjs";
11
+ import { s as signRequest } from "../http-B2ftY0Ha.mjs";
12
12
  import { i as rsaPrivateKey2, n as ed25519PrivateKey, r as ed25519PublicKey, s as rsaPublicKey2 } from "../keys-DIJNHW-Y.mjs";
13
- import { n as FederationImpl, o as createFederation, p as handleInbox } from "../middleware-BKeQYPm0.mjs";
13
+ import { m as handleInbox, n as FederationImpl, o as createFederation } from "../middleware-E_0IH72m.mjs";
14
14
  import { t as ActivityListenerSet } from "../activity-listener-C-BSbKI7.mjs";
15
15
  import { t as KvKeyCache } from "../keycache-BUDHB5rY.mjs";
16
16
  import { Create, CryptographicKey, Person } from "@fedify/vocab";
@@ -229,11 +229,11 @@ test("Context.getDocumentLoader() signs requests with gateway keys for portable
229
229
  }
230
230
  assertEquals(keyId, gatewayKeyId.href);
231
231
  });
232
- async function actorDocument(gateways) {
233
- const keys = await createTestFederation(() => actorId).createContext(new URL(gateway)).getActorKeyPairs("alice");
232
+ async function actorDocument(gateways, id = actorId, signed = true) {
233
+ const keys = await createTestFederation(() => id).createContext(new URL(gateway)).getActorKeyPairs("alice");
234
234
  const json = await new Person({
235
- id: actorId,
236
- inbox: parseIri(`${actorId.href}/inbox`),
235
+ id,
236
+ inbox: parseIri(`${id.href}/inbox`),
237
237
  gateways: gateways.map((g) => new URL(g)),
238
238
  publicKey: keys[0].cryptographicKey,
239
239
  assertionMethods: keys.map((k) => k.multikey)
@@ -242,7 +242,7 @@ async function actorDocument(gateways) {
242
242
  contextLoader: mockDocumentLoader
243
243
  });
244
244
  json["@context"] = [...json["@context"], "https://w3id.org/security/data-integrity/v1"];
245
- return await sign(json);
245
+ return signed ? await sign(json) : json;
246
246
  }
247
247
  function createLoader(responses) {
248
248
  return (url) => {
@@ -255,8 +255,8 @@ function createLoader(responses) {
255
255
  });
256
256
  };
257
257
  }
258
- async function deliver(body, gateways, { kv = new MemoryKvStore(), httpSignature = true } = {}) {
259
- const documentLoader = createLoader({ [compatibleActorId(gateway)]: await actorDocument(gateways) });
258
+ async function deliver(body, gateways, { kv = new MemoryKvStore(), httpSignature = true, actor } = {}) {
259
+ const documentLoader = createLoader({ [compatibleActorId(gateway)]: actor ?? await actorDocument(gateways) });
260
260
  const unsigned = new Request("https://local.example/inbox", {
261
261
  method: "POST",
262
262
  body: JSON.stringify(body),
@@ -357,7 +357,68 @@ test("handleInbox() does not take Linked Data Signatures for portable actors", a
357
357
  status: 401,
358
358
  dispatched: 0
359
359
  });
360
- assertEquals(lastResponseText, "Activities of portable actors must have valid Object Integrity Proofs.");
360
+ assertEquals(lastResponseText, "Failed to verify the request signature.");
361
+ });
362
+ const otherDid = await exportDidKey((await crypto.subtle.generateKey("Ed25519", true, ["sign", "verify"])).publicKey);
363
+ function compatibleCreateJson(id, activityDid = did) {
364
+ return {
365
+ "@context": portableContext,
366
+ id: `${gateway}/.well-known/apgateway/${activityDid}/activities/${id}`,
367
+ type: "Create",
368
+ actor: compatibleActorId(gateway),
369
+ object: "https://example.com/notes/1"
370
+ };
371
+ }
372
+ test("handleInbox() does not trust compatible-ID actors by web origin", async () => {
373
+ const id = new URL(compatibleActorId(gateway));
374
+ for (const signed of [true, false]) {
375
+ const actor = await actorDocument([gateway], id, signed);
376
+ assertEquals(await deliver(compatibleCreateJson(`unsigned-${signed}`), [gateway], { actor }), {
377
+ status: 401,
378
+ dispatched: 0
379
+ }, `actor document signed: ${signed}`);
380
+ }
381
+ assertEquals(await deliver(await sign(compatibleCreateJson("signed")), [gateway], { actor: await actorDocument([gateway], id) }), {
382
+ status: 202,
383
+ dispatched: 1
384
+ });
385
+ });
386
+ test("handleInbox() requires proofs of compatible-ID actors", async () => {
387
+ assertEquals(await deliver(compatibleCreateJson("unsigned"), [gateway]), {
388
+ status: 401,
389
+ dispatched: 0
390
+ });
391
+ assertEquals(await deliver(await sign(compatibleCreateJson("signed")), [gateway]), {
392
+ status: 202,
393
+ dispatched: 1
394
+ });
395
+ assertEquals(await deliver(await sign(compatibleCreateJson("unsigned-request")), [gateway], { httpSignature: false }), {
396
+ status: 202,
397
+ dispatched: 1
398
+ });
399
+ });
400
+ test("handleInbox() checks the DIDs of compatible activity IDs", async () => {
401
+ for (const actor of [actorId.href, compatibleActorId(gateway)]) assertEquals(await deliver(await sign({
402
+ ...compatibleCreateJson("forged", otherDid),
403
+ actor
404
+ }), [gateway]), {
405
+ status: 401,
406
+ dispatched: 0
407
+ }, actor);
408
+ assertEquals(await deliver(await sign({
409
+ ...compatibleCreateJson("forged-ap"),
410
+ id: `ap://${otherDid}/activities/forged-ap`
411
+ }), [gateway]), {
412
+ status: 401,
413
+ dispatched: 0
414
+ });
415
+ assertEquals(await deliver(await sign({
416
+ ...compatibleCreateJson("malformed"),
417
+ id: `https://user@example.com/.well-known/apgateway/${did}/activities/2`
418
+ }), [gateway]), {
419
+ status: 401,
420
+ dispatched: 0
421
+ });
361
422
  });
362
423
  //#endregion
363
424
  export {};
@@ -8,11 +8,11 @@ import { t as assertFalse } from "../assert_false-CPdRnc3E.mjs";
8
8
  import { t as assertThrows } from "../assert_throws-BYITOvPO.mjs";
9
9
  import "../std__assert-DvMDRPes.mjs";
10
10
  import { t as assert } from "../assert-D1Eafmd_.mjs";
11
- import { p as signJsonLd } from "../ld-BmFz5NLA.mjs";
12
- import { r as signObject } from "../proof-ozG4HfcQ.mjs";
13
- import { s as signRequest, u as verifyRequestDetailed } from "../http-B0VRNipE.mjs";
11
+ import { p as signJsonLd } from "../ld-BYZOoDzZ.mjs";
12
+ import { i as signObject } from "../proof-DVXC6nYz.mjs";
13
+ import { s as signRequest, u as verifyRequestDetailed } from "../http-B2ftY0Ha.mjs";
14
14
  import { a as rsaPrivateKey3, c as rsaPublicKey3, i as rsaPrivateKey2, n as ed25519PrivateKey, r as ed25519PublicKey, s as rsaPublicKey2 } from "../keys-DIJNHW-Y.mjs";
15
- import { c as forwardPortableInboxActivity, l as resolvePortableInboxForwardingOptions, o as createFederation } from "../middleware-BKeQYPm0.mjs";
15
+ import { l as forwardPortableInboxActivity, o as createFederation, u as resolvePortableInboxForwardingOptions } from "../middleware-E_0IH72m.mjs";
16
16
  import { Create, Follow, Note, Person, Tombstone } from "@fedify/vocab";
17
17
  import { mockDocumentLoader, test } from "@fedify/fixture";
18
18
  import { exportDidKey, formatIri, parseIri } from "@fedify/vocab-runtime";
@@ -522,6 +522,118 @@ test("forwardPortableInboxActivity() does not wait for slow gateways", async ()
522
522
  esm_default.hardReset();
523
523
  }
524
524
  });
525
+ function compatibleAlice(path = "/users/alice", { authority = did, origin = LOCAL, gateways = [
526
+ LOCAL,
527
+ GATEWAY2,
528
+ GATEWAY3
529
+ ] } = {}) {
530
+ return {
531
+ id: new URL(`${origin}/.well-known/apgateway/${authority}${path}`),
532
+ inbox: new URL(inboxUrl(authority, `${path}/inbox`, origin)),
533
+ gateways: gateways.map((g) => new URL(g))
534
+ };
535
+ }
536
+ test("Federation.fetch() delivers to portable inboxes of compatible-ID actors", async (t) => {
537
+ const apAlice = parseIri(`ap+ef61://${did}/users/alice`);
538
+ const apInbox = parseIri(`ap+ef61://${did}/users/alice/inbox`);
539
+ const cases = [
540
+ [
541
+ "compatible actor and inbox",
542
+ compatibleAlice(),
543
+ ""
544
+ ],
545
+ [
546
+ "on another gateway",
547
+ compatibleAlice(void 0, { origin: GATEWAY2 }),
548
+ ""
549
+ ],
550
+ [
551
+ "compatible actor and ap: inbox",
552
+ {
553
+ ...compatibleAlice(),
554
+ inbox: apInbox
555
+ },
556
+ ""
557
+ ],
558
+ [
559
+ "ap: actor and compatible inbox",
560
+ {
561
+ ...compatibleAlice(),
562
+ id: apAlice
563
+ },
564
+ ""
565
+ ],
566
+ [
567
+ "compatible inbox with a query",
568
+ {
569
+ ...compatibleAlice(),
570
+ inbox: new URL(inboxUrl() + "?page=1")
571
+ },
572
+ "?page=1"
573
+ ]
574
+ ];
575
+ for (const [name, alice, query] of cases) await t.step(name, async () => {
576
+ const queue = new RecordingQueue();
577
+ const { federation, received } = setup({
578
+ queue,
579
+ actor: (_ctx, identifier) => identifier === "alice" ? new Person(alice) : null
580
+ });
581
+ assertEquals((await federation.fetch(post(inboxUrl(), await signedFollow()), { contextData: void 0 })).status, 202);
582
+ assertEquals(received.length, 1);
583
+ assertEquals(received[0].recipient, "alice");
584
+ assertEquals(queue.outbox.map((m) => m.inbox).sort(), [inboxUrl(did, "/users/alice/inbox", GATEWAY3) + query, inboxUrl(did, "/users/alice/inbox", GATEWAY2) + query]);
585
+ });
586
+ const json = await signedFollow();
587
+ const rejections = [
588
+ ["DID mismatch", new Person(compatibleAlice(void 0, { authority: otherDid }))],
589
+ ["inbox mismatch", new Person({
590
+ ...compatibleAlice(),
591
+ inbox: new URL(inboxUrl(did, "/users/alice/other-inbox"))
592
+ })],
593
+ ["this server is not a gateway of the actor", new Person(compatibleAlice(void 0, { gateways: [GATEWAY2] }))],
594
+ ["malformed actor ID", new Person({
595
+ ...compatibleAlice(),
596
+ id: new URL(compatibleAlice().id.href + "?@gateway=https%3A%2F%2Fexample.com")
597
+ })],
598
+ ["malformed inbox ID", new Person({
599
+ ...compatibleAlice(),
600
+ inbox: new URL(inboxUrl() + "?@gateway=https%3A%2F%2Fexample.com")
601
+ })]
602
+ ];
603
+ for (const [name, actor] of rejections) await t.step(name, async () => {
604
+ const { federation, received } = setup({ actor: (_ctx, identifier) => identifier === "alice" ? actor : null });
605
+ assertEquals((await federation.fetch(post(inboxUrl(), json), { contextData: void 0 })).status, 404);
606
+ assertEquals(received.length, 0);
607
+ });
608
+ });
609
+ test("forwardPortableInboxActivity() deduplicates compatible activity IDs", async () => {
610
+ const kv = new MemoryKvStore();
611
+ const queue = new RecordingQueue();
612
+ const forward = (activityId) => forwardPortableInboxActivity({
613
+ recipient: {
614
+ actorId: compatibleAlice().id,
615
+ inboxId: parseIri(`ap+ef61://${did}/users/alice/inbox`),
616
+ canonicalInboxId: `ap+ef61://${did}/users/alice/inbox`,
617
+ gateways: [new URL(LOCAL), new URL(GATEWAY2)]
618
+ },
619
+ activity: { id: activityId.href },
620
+ activityId,
621
+ activityType: "https://www.w3.org/ns/activitystreams#Follow",
622
+ excludedOrigins: [LOCAL],
623
+ baseUrl: LOCAL,
624
+ kv,
625
+ kvPrefix: ["_fedify", "portableInboxForwarding"],
626
+ outboxQueue: queue
627
+ });
628
+ const path = `${did}/follows/compatible`;
629
+ assertEquals((await forward(new URL(`${LOCAL}/.well-known/apgateway/${path}`))).length, 1);
630
+ for (const id of [
631
+ new URL(`${GATEWAY2}/.well-known/apgateway/${path}`),
632
+ new URL(`${LOCAL}/.well-known/apgateway/${path}?x=1`),
633
+ parseIri(`ap://${path}`)
634
+ ]) assertEquals(await forward(id), [], id.href);
635
+ assertEquals(queue.outbox.length, 1);
636
+ });
525
637
  const aliceId = parseIri(`ap+ef61://${did}/users/alice`);
526
638
  const rsaGatewayKeyPair = await crypto.subtle.generateKey({
527
639
  name: "RSASSA-PKCS1-v1_5",