@fedify/fedify 2.4.0-dev.2077 → 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-i3jxVe9m.mjs → builder-Ld4Y9ymR.mjs} +34 -3
  2. package/dist/compat/transformers.test.mjs +1 -1
  3. package/dist/{compound-proof-Cayy-ECj.mjs → compound-proof-CTx_lewQ.mjs} +191 -13
  4. package/dist/{deno-Djkzd-G_.mjs → deno-8t8k_6Yv.mjs} +1 -1
  5. package/dist/{docloader-dA0RswUV.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 +513 -6
  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-CFd0Jhhv.cjs → federation-aSHO04ob.cjs} +472 -74
  26. package/dist/{http-CCJ97Jql.mjs → http-B2ftY0Ha.mjs} +4 -4
  27. package/dist/{http-Dvy_IwRa.cjs → http-DSaSAN7X.cjs} +1609 -1457
  28. package/dist/{http-aqh3yFdM.js → http-DyT7DP1Y.js} +1518 -1390
  29. package/dist/{key-IxTTC8Yf.mjs → key-Dwm4VswT.mjs} +39 -99
  30. package/dist/{kv-cache-BP69NV0P.cjs → kv-cache-BSJAlP4C.cjs} +1 -1
  31. package/dist/{kv-cache-Bwwbh-xR.mjs → kv-cache-D0yYukqk.mjs} +1 -1
  32. package/dist/{kv-cache-Y6SDRSX6.js → kv-cache-DmroAz3M.js} +1 -1
  33. package/dist/{ld-l0B9nREb.mjs → ld-BYZOoDzZ.mjs} +3 -3
  34. package/dist/{metrics-D1p6hv4z.mjs → metrics-B7YwKTFr.mjs} +1 -1
  35. package/dist/{middleware-BDjCUHMS.mjs → middleware-BNeqKSsL.mjs} +1 -1
  36. package/dist/{middleware-SSfryVzY.js → middleware-Blq0882d.js} +480 -82
  37. package/dist/{middleware-BMgzqBtr.mjs → middleware-E_0IH72m.mjs} +278 -80
  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-DGbYmXqU.mjs → owner-Cgx2Dc7I.mjs} +5 -4
  46. package/dist/{owner-jH1z_YfD.js → owner-DAGSC5gX.js} +2 -2
  47. package/dist/{owner-BpOxL5PG.cjs → owner-szIZl4qI.cjs} +2 -2
  48. package/dist/{portable-key-CCwJGSsU.mjs → portable-key-Cn1WJSN1.mjs} +60 -41
  49. package/dist/portable-key-id-Bva5iT3W.mjs +136 -0
  50. package/dist/{proof-CVL7JnZd.mjs → proof-DVXC6nYz.mjs} +59 -18
  51. package/dist/{send-Ejy1jOz9.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-Dpy1B1Ad.js → sig-B5Ell7pO.js} +19 -3
  68. package/dist/{sig-xoqy3ATq.cjs → sig-CXOKN4Br.cjs} +19 -3
  69. package/dist/{temporal-DNJ1cNpr.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
@@ -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-l0B9nREb.mjs";
12
- import { r as signObject } from "../proof-CVL7JnZd.mjs";
13
- import { s as signRequest } from "../http-CCJ97Jql.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-BMgzqBtr.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";
@@ -56,7 +56,7 @@ function setup({ gateways = [
56
56
  LOCAL,
57
57
  GATEWAY2,
58
58
  GATEWAY3
59
- ], kv = new MemoryKvStore(), queue, options = {}, actor, listenerError = false, keyPairs = true, onSharedInboxKey } = {}) {
59
+ ], kv = new MemoryKvStore(), queue, options = {}, actor, listenerError = false, keyPairs = true, aliceKeyPairs = [], mapPortableActorId, onSharedInboxKey } = {}) {
60
60
  const federation = createFederation({
61
61
  kv,
62
62
  ...options.allowPrivateAddress ? {} : {
@@ -83,7 +83,8 @@ function setup({ gateways = [
83
83
  if (keyPairs) actorCallbacks.setKeyPairsDispatcher((_ctx, identifier) => identifier === "ordinary" ? [{
84
84
  privateKey: rsaPrivateKey2,
85
85
  publicKey: rsaPublicKey2.publicKey
86
- }] : []);
86
+ }] : identifier === "alice" ? aliceKeyPairs : []);
87
+ if (mapPortableActorId != null) actorCallbacks.mapPortableActorId((_ctx, identifier) => mapPortableActorId(identifier));
87
88
  const inboxListeners = federation.setInboxListeners("/users/{identifier}/inbox", "/inbox");
88
89
  if (onSharedInboxKey != null) inboxListeners.setSharedKeyDispatcher(() => {
89
90
  onSharedInboxKey();
@@ -494,6 +495,7 @@ test("forwardPortableInboxActivity() does not wait for slow gateways", async ()
494
495
  const activityId = parseIri(`ap+ef61://${did}/follows/slow`);
495
496
  const parameters = {
496
497
  recipient: {
498
+ actorId: parseIri(`ap+ef61://${did}/users/alice`),
497
499
  inboxId: parseIri(`ap+ef61://${did}/users/alice/inbox`),
498
500
  canonicalInboxId: `ap://${did}/users/alice/inbox`,
499
501
  gateways: [new URL(LOCAL), new URL(GATEWAY2)]
@@ -520,6 +522,511 @@ test("forwardPortableInboxActivity() does not wait for slow gateways", async ()
520
522
  esm_default.hardReset();
521
523
  }
522
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
+ });
637
+ const aliceId = parseIri(`ap+ef61://${did}/users/alice`);
638
+ const rsaGatewayKeyPair = await crypto.subtle.generateKey({
639
+ name: "RSASSA-PKCS1-v1_5",
640
+ modulusLength: 2048,
641
+ publicExponent: new Uint8Array([
642
+ 1,
643
+ 0,
644
+ 1
645
+ ]),
646
+ hash: "SHA-256"
647
+ }, true, ["sign", "verify"]);
648
+ function compatibleAliceId(origin = LOCAL) {
649
+ return `${origin}/.well-known/apgateway/${did}/users/alice`;
650
+ }
651
+ function mapAlice(identifier) {
652
+ return identifier === "alice" ? aliceId : null;
653
+ }
654
+ function signatureKeyId(headers) {
655
+ return headers.get("Signature-Input")?.match(/keyid="([^"]+)"/)?.[1] ?? headers.get("Signature")?.match(/keyId="([^"]+)"/)?.[1];
656
+ }
657
+ /**
658
+ * Makes a document loader that serves Alice's actor document as the gateway
659
+ * at {@link LOCAL} does: signed by her DID, and embedding the gateway keys in
660
+ * its `assertionMethod`.
661
+ */
662
+ async function createGatewayLoader(keyPairs) {
663
+ const { federation } = setup({
664
+ aliceKeyPairs: keyPairs,
665
+ mapPortableActorId: mapAlice
666
+ });
667
+ const keys = await federation.createContext(new URL(LOCAL)).getActorKeyPairs("alice");
668
+ const document = await (await signObject(new Person({
669
+ id: aliceId,
670
+ inbox: parseIri(`${aliceId.href}/inbox`),
671
+ gateways: [
672
+ new URL(LOCAL),
673
+ new URL(GATEWAY2),
674
+ new URL(GATEWAY3)
675
+ ],
676
+ assertionMethods: keys.map((k) => k.multikey)
677
+ }), ed25519PrivateKey, keyId, { contextLoader: mockDocumentLoader })).toJsonLd({ contextLoader: mockDocumentLoader });
678
+ return (url) => {
679
+ if (url.replace(/#.*$/, "") !== compatibleAliceId()) return mockDocumentLoader(url);
680
+ return Promise.resolve({
681
+ contextUrl: null,
682
+ documentUrl: url,
683
+ document: structuredClone(document)
684
+ });
685
+ };
686
+ }
687
+ function toRequest({ url, headers, body }) {
688
+ return new Request(url, {
689
+ method: "POST",
690
+ headers,
691
+ body
692
+ });
693
+ }
694
+ async function assertSignedBy(request, expectedKeyId, documentLoader) {
695
+ assertEquals(signatureKeyId(request.headers), expectedKeyId);
696
+ const result = await verifyRequestDetailed(toRequest(request), {
697
+ documentLoader,
698
+ contextLoader: mockDocumentLoader
699
+ });
700
+ assert(result.verified, JSON.stringify(result));
701
+ assertEquals(result.key.id?.href, expectedKeyId);
702
+ }
703
+ /** Records the requests made to the gateways other than {@link LOCAL}. */
704
+ function captureGatewayRequests(respond = () => new Response(null, { status: 202 })) {
705
+ const requests = [];
706
+ esm_default.spyGlobal();
707
+ for (const gateway of [GATEWAY2, GATEWAY3]) esm_default.post(`begin:${gateway}/`, async (callLog) => {
708
+ const request = callLog.request;
709
+ const captured = {
710
+ url: request.url,
711
+ headers: new Headers(request.headers),
712
+ body: await request.text()
713
+ };
714
+ requests.push(captured);
715
+ return respond(captured);
716
+ });
717
+ return requests;
718
+ }
719
+ test("Federation.fetch() signs forwarded portable inbox deliveries with gateway keys", async (t) => {
720
+ const keyOrders = [[
721
+ "RSA key first",
722
+ [rsaGatewayKeyPair, otherKeyPair],
723
+ "#main-key"
724
+ ], [
725
+ "Ed25519 key first",
726
+ [otherKeyPair, rsaGatewayKeyPair],
727
+ "#key-2"
728
+ ]];
729
+ for (const [name, aliceKeyPairs, fragment] of keyOrders) await t.step(name, async () => {
730
+ const requests = captureGatewayRequests();
731
+ try {
732
+ const kv = new MemoryKvStore();
733
+ const { federation, received } = setup({
734
+ kv,
735
+ aliceKeyPairs,
736
+ mapPortableActorId: mapAlice,
737
+ options: { allowPrivateAddress: true }
738
+ });
739
+ const json = await signedFollow();
740
+ assertEquals((await federation.fetch(post(inboxUrl(), json), { contextData: void 0 })).status, 202);
741
+ assertEquals(received.length, 1);
742
+ assertEquals(requests.map((r) => r.url).sort(), [inboxUrl(did, "/users/alice/inbox", GATEWAY3), inboxUrl(did, "/users/alice/inbox", GATEWAY2)]);
743
+ const loader = await createGatewayLoader(aliceKeyPairs);
744
+ for (const request of requests) {
745
+ await assertSignedBy(request, compatibleAliceId() + fragment, loader);
746
+ assertEquals(JSON.parse(request.body), json);
747
+ }
748
+ assertEquals(await kv.get([
749
+ "_fedify",
750
+ "httpMessageSignaturesSpec",
751
+ GATEWAY2
752
+ ]), "rfc9421");
753
+ } finally {
754
+ esm_default.hardReset();
755
+ }
756
+ });
757
+ });
758
+ test("Federation.fetch() signs queued forwarded portable inbox deliveries with gateway keys", async () => {
759
+ const queue = new RecordingQueue();
760
+ const aliceKeyPairs = [otherKeyPair, rsaGatewayKeyPair];
761
+ const { federation } = setup({
762
+ queue,
763
+ aliceKeyPairs,
764
+ mapPortableActorId: mapAlice,
765
+ options: { allowPrivateAddress: true }
766
+ });
767
+ assertEquals((await federation.fetch(post(inboxUrl(), await signedFollow()), { contextData: void 0 })).status, 202);
768
+ assertEquals(queue.outbox.length, 2);
769
+ const expectedKeyId = `${compatibleAliceId()}#key-2`;
770
+ for (const message of queue.outbox) {
771
+ assertEquals(message.keys.map((k) => k.keyId), [expectedKeyId]);
772
+ assertEquals(message.keys[0].privateKey.kty, "RSA");
773
+ }
774
+ const requests = captureGatewayRequests();
775
+ try {
776
+ for (const message of queue.outbox) await federation.processQueuedTask(void 0, message);
777
+ } finally {
778
+ esm_default.hardReset();
779
+ }
780
+ assertEquals(requests.length, 2);
781
+ const loader = await createGatewayLoader(aliceKeyPairs);
782
+ for (const request of requests) await assertSignedBy(request, expectedKeyId, loader);
783
+ });
784
+ test("Federation.fetch() forwards portable inbox deliveries unsigned without gateway keys", async (t) => {
785
+ const otherActorId = parseIri(`ap+ef61://${otherDid}/users/alice`);
786
+ const cases = [
787
+ ["without a mapper", { aliceKeyPairs: [rsaGatewayKeyPair] }],
788
+ ["the mapper returns null", {
789
+ aliceKeyPairs: [rsaGatewayKeyPair],
790
+ mapPortableActorId: () => null
791
+ }],
792
+ ["the mapper returns another actor", {
793
+ aliceKeyPairs: [rsaGatewayKeyPair],
794
+ mapPortableActorId: () => otherActorId
795
+ }],
796
+ ["the mapper returns another actor of the same DID", {
797
+ aliceKeyPairs: [rsaGatewayKeyPair],
798
+ mapPortableActorId: () => parseIri(`ap+ef61://${did}/users/bob`)
799
+ }],
800
+ ["without a key pairs dispatcher", {
801
+ keyPairs: false,
802
+ mapPortableActorId: mapAlice
803
+ }],
804
+ ["without key pairs", { mapPortableActorId: mapAlice }],
805
+ ["without an RSA key", {
806
+ aliceKeyPairs: [otherKeyPair],
807
+ mapPortableActorId: mapAlice
808
+ }]
809
+ ];
810
+ for (const [name, options] of cases) await t.step(name, async () => {
811
+ const queue = new RecordingQueue();
812
+ const { federation, received } = setup({
813
+ ...options,
814
+ queue
815
+ });
816
+ assertEquals((await federation.fetch(post(inboxUrl(), await signedFollow()), { contextData: void 0 })).status, 202);
817
+ assertEquals(received.length, 1);
818
+ assertEquals(queue.outbox.length, 2);
819
+ for (const message of queue.outbox) assertEquals(message.keys, []);
820
+ });
821
+ });
822
+ test("Federation.fetch() signs forwarded portable inbox deliveries if the mapper returns an equivalent ID", async (t) => {
823
+ const ids = [
824
+ ["ap: URI", parseIri(`ap://${did}/users/alice`)],
825
+ ["percent-encoded DID", parseIri(`ap+ef61://${did.replaceAll(":", "%3A")}/users/alice`)],
826
+ ["compatible identifier", new URL(compatibleAliceId(GATEWAY2))]
827
+ ];
828
+ for (const [name, id] of ids) await t.step(name, async () => {
829
+ const queue = new RecordingQueue();
830
+ const { federation } = setup({
831
+ queue,
832
+ aliceKeyPairs: [rsaGatewayKeyPair],
833
+ mapPortableActorId: (identifier) => identifier === "alice" ? id : null
834
+ });
835
+ assertEquals((await federation.fetch(post(inboxUrl(), await signedFollow()), { contextData: void 0 })).status, 202);
836
+ assertEquals(queue.outbox.length, 2);
837
+ for (const message of queue.outbox) assertEquals(message.keys.map((k) => k.keyId), [`${compatibleAliceId()}#main-key`]);
838
+ });
839
+ });
840
+ test("Federation.fetch() signs forwarded portable inbox deliveries with the gateway keys of the canonical origin", async () => {
841
+ const queue = new RecordingQueue();
842
+ const { federation } = setup({
843
+ queue,
844
+ gateways: [
845
+ GATEWAY2,
846
+ LOCAL,
847
+ GATEWAY3
848
+ ],
849
+ aliceKeyPairs: [rsaGatewayKeyPair],
850
+ mapPortableActorId: mapAlice,
851
+ options: { origin: GATEWAY2 }
852
+ });
853
+ assertEquals((await federation.fetch(post(inboxUrl(), await signedFollow()), { contextData: void 0 })).status, 202);
854
+ assertEquals(queue.outbox.length, 1);
855
+ assertEquals(queue.outbox[0].keys.map((k) => k.keyId), [`${compatibleAliceId(GATEWAY2)}#main-key`]);
856
+ });
857
+ test("Federation.fetch() double-knocks forwarded portable inbox deliveries", async (t) => {
858
+ await t.step("firstKnock", async () => {
859
+ const requests = captureGatewayRequests();
860
+ try {
861
+ const { federation } = setup({
862
+ aliceKeyPairs: [rsaGatewayKeyPair],
863
+ mapPortableActorId: mapAlice,
864
+ options: {
865
+ allowPrivateAddress: true,
866
+ firstKnock: "draft-cavage-http-signatures-12"
867
+ }
868
+ });
869
+ assertEquals((await federation.fetch(post(inboxUrl(), await signedFollow()), { contextData: void 0 })).status, 202);
870
+ assertEquals(requests.length, 2);
871
+ for (const request of requests) {
872
+ assert(request.headers.has("Signature"));
873
+ assertFalse(request.headers.has("Signature-Input"));
874
+ }
875
+ } finally {
876
+ esm_default.hardReset();
877
+ }
878
+ });
879
+ await t.step("falls back to the other spec", async () => {
880
+ const requests = captureGatewayRequests((request) => new Response(null, { status: request.headers.has("Signature-Input") ? 401 : 202 }));
881
+ try {
882
+ const kv = new MemoryKvStore();
883
+ const { federation } = setup({
884
+ kv,
885
+ aliceKeyPairs: [rsaGatewayKeyPair],
886
+ mapPortableActorId: mapAlice,
887
+ options: { allowPrivateAddress: true }
888
+ });
889
+ assertEquals((await federation.fetch(post(inboxUrl(), await signedFollow()), { contextData: void 0 })).status, 202);
890
+ assertEquals(requests.length, 4);
891
+ const loader = await createGatewayLoader([rsaGatewayKeyPair]);
892
+ for (const request of requests) {
893
+ if (request.headers.has("Signature-Input")) continue;
894
+ await assertSignedBy(request, `${compatibleAliceId()}#main-key`, loader);
895
+ }
896
+ assertEquals(await kv.get([
897
+ "_fedify",
898
+ "httpMessageSignaturesSpec",
899
+ GATEWAY2
900
+ ]), "draft-cavage-http-signatures-12");
901
+ } finally {
902
+ esm_default.hardReset();
903
+ }
904
+ });
905
+ });
906
+ test("A Fedify gateway accepts signed forwarded portable inbox deliveries", async () => {
907
+ const requests = captureGatewayRequests();
908
+ let forwarded;
909
+ try {
910
+ const { federation } = setup({
911
+ aliceKeyPairs: [rsaGatewayKeyPair],
912
+ mapPortableActorId: mapAlice,
913
+ options: { allowPrivateAddress: true }
914
+ });
915
+ assertEquals((await federation.fetch(post(inboxUrl(), await signedFollow()), { contextData: void 0 })).status, 202);
916
+ forwarded = requests.find((r) => r.url.startsWith(`${GATEWAY2}/`));
917
+ } finally {
918
+ esm_default.hardReset();
919
+ }
920
+ await assertSignedBy(forwarded, `${compatibleAliceId()}#main-key`, await createGatewayLoader([rsaGatewayKeyPair]));
921
+ const queue = new RecordingQueue();
922
+ const { federation, received } = setup({
923
+ queue,
924
+ options: { origin: GATEWAY2 }
925
+ });
926
+ assertEquals((await federation.fetch(toRequest(forwarded), { contextData: void 0 })).status, 202);
927
+ assertEquals(received.length, 1);
928
+ assertEquals(received[0].recipient, "alice");
929
+ assertEquals(queue.outbox.map((m) => m.inbox).sort(), [inboxUrl(did, "/users/alice/inbox", LOCAL), inboxUrl(did, "/users/alice/inbox", GATEWAY3)]);
930
+ });
931
+ function forwardingParameters(overrides = {}) {
932
+ const activityId = parseIri(`ap+ef61://${did}/follows/${++activityCounter}`);
933
+ return {
934
+ recipient: {
935
+ actorId: aliceId,
936
+ inboxId: parseIri(`ap+ef61://${did}/users/alice/inbox`),
937
+ canonicalInboxId: `ap://${did}/users/alice/inbox`,
938
+ gateways: [new URL(LOCAL), new URL(GATEWAY2)]
939
+ },
940
+ activity: { id: formatIri(activityId) },
941
+ activityId,
942
+ activityType: "https://www.w3.org/ns/activitystreams#Follow",
943
+ excludedOrigins: [LOCAL],
944
+ baseUrl: LOCAL,
945
+ kv: new MemoryKvStore(),
946
+ kvPrefix: ["_fedify", "portableInboxForwarding"],
947
+ allowPrivateAddress: true,
948
+ ...overrides
949
+ };
950
+ }
951
+ const gatewayKey = {
952
+ keyId: new URL(`${compatibleAliceId()}#main-key`),
953
+ privateKey: rsaGatewayKeyPair.privateKey
954
+ };
955
+ test("forwardPortableInboxActivity() gets keys only when it forwards", async () => {
956
+ let calls = 0;
957
+ const parameters = forwardingParameters({
958
+ outboxQueue: new RecordingQueue(),
959
+ getKeys: () => {
960
+ calls++;
961
+ return Promise.resolve([gatewayKey]);
962
+ }
963
+ });
964
+ assertEquals((await forwardPortableInboxActivity(parameters)).length, 1);
965
+ assertEquals(calls, 1);
966
+ assertEquals(await forwardPortableInboxActivity(parameters), []);
967
+ assertEquals(calls, 1);
968
+ });
969
+ test("forwardPortableInboxActivity() forwards unsigned if it cannot get keys", async (t) => {
970
+ const nonExtractable = await crypto.subtle.generateKey({
971
+ name: "RSASSA-PKCS1-v1_5",
972
+ modulusLength: 2048,
973
+ publicExponent: new Uint8Array([
974
+ 1,
975
+ 0,
976
+ 1
977
+ ]),
978
+ hash: "SHA-256"
979
+ }, false, ["sign", "verify"]);
980
+ const getKeysCases = [["getKeys() rejects", () => Promise.reject(/* @__PURE__ */ new Error("Unavailable."))], ["the key is not extractable", () => Promise.resolve([{
981
+ keyId: gatewayKey.keyId,
982
+ privateKey: nonExtractable.privateKey
983
+ }])]];
984
+ for (const [name, getKeys] of getKeysCases) {
985
+ await t.step(`${name} (queued)`, async () => {
986
+ const queue = new RecordingQueue();
987
+ assertEquals((await forwardPortableInboxActivity(forwardingParameters({
988
+ outboxQueue: queue,
989
+ getKeys
990
+ }))).length, 1);
991
+ assertEquals(queue.outbox.length, 1);
992
+ assertEquals(queue.outbox[0].keys, []);
993
+ });
994
+ await t.step(`${name} (immediate)`, async () => {
995
+ const requests = captureGatewayRequests();
996
+ try {
997
+ assertEquals((await forwardPortableInboxActivity(forwardingParameters({ getKeys }))).length, 1);
998
+ } finally {
999
+ esm_default.hardReset();
1000
+ }
1001
+ assertEquals(requests.length, 1);
1002
+ assertEquals(signatureKeyId(requests[0].headers), void 0);
1003
+ });
1004
+ }
1005
+ });
1006
+ test("forwardPortableInboxActivity() does not wait for slow keys", async () => {
1007
+ let release = () => {};
1008
+ const pending = new Promise((resolve) => release = resolve);
1009
+ const requests = captureGatewayRequests();
1010
+ try {
1011
+ const started = performance.now();
1012
+ const forwarded = await forwardPortableInboxActivity(forwardingParameters({
1013
+ options: resolvePortableInboxForwardingOptions({ deadline: { milliseconds: 10 } }),
1014
+ getKeys: async () => {
1015
+ await pending;
1016
+ throw new Error("Unavailable.");
1017
+ }
1018
+ }));
1019
+ assert(performance.now() - started < 5e3);
1020
+ assertEquals(forwarded.length, 1);
1021
+ assertEquals(requests.length, 0);
1022
+ release();
1023
+ for (let i = 0; i < 100 && requests.length < 1; i++) await new Promise((resolve) => setTimeout(resolve, 10));
1024
+ assertEquals(requests.length, 1);
1025
+ assertEquals(signatureKeyId(requests[0].headers), void 0);
1026
+ } finally {
1027
+ esm_default.hardReset();
1028
+ }
1029
+ });
523
1030
  test("Context.getPortableInboxUri()", async (t) => {
524
1031
  const { federation } = setup();
525
1032
  await t.step("Context", () => {