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

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 (68) hide show
  1. package/dist/{builder-i3jxVe9m.mjs → builder-BGlpUocK.mjs} +2 -2
  2. package/dist/compat/transformers.test.mjs +1 -1
  3. package/dist/{compound-proof-Cayy-ECj.mjs → compound-proof-B6Aj2kGY.mjs} +1 -1
  4. package/dist/{deno-Djkzd-G_.mjs → deno-Dn-py9Zy.mjs} +1 -1
  5. package/dist/{docloader-dA0RswUV.mjs → docloader-BUq5bYJr.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 +5 -5
  17. package/dist/federation/portable-gateway-keys.test.mjs +3 -3
  18. package/dist/federation/portable-inbox.test.mjs +401 -6
  19. package/dist/federation/portable.test.mjs +3 -3
  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 +1 -1
  25. package/dist/{federation-CFd0Jhhv.cjs → federation-BLGx8JUH.cjs} +115 -12
  26. package/dist/{http-aqh3yFdM.js → http-4PCCvhsU.js} +1 -1
  27. package/dist/{http-CCJ97Jql.mjs → http-B0VRNipE.mjs} +4 -4
  28. package/dist/{http-Dvy_IwRa.cjs → http-B5q1CHtL.cjs} +1 -1
  29. package/dist/{key-IxTTC8Yf.mjs → key-BJrp524f.mjs} +2 -2
  30. package/dist/{kv-cache-Y6SDRSX6.js → kv-cache-1KUmAPH7.js} +1 -1
  31. package/dist/{kv-cache-Bwwbh-xR.mjs → kv-cache-C4C3LyYW.mjs} +1 -1
  32. package/dist/{kv-cache-BP69NV0P.cjs → kv-cache-Dhhhrryv.cjs} +1 -1
  33. package/dist/{ld-l0B9nREb.mjs → ld-BmFz5NLA.mjs} +3 -3
  34. package/dist/{metrics-D1p6hv4z.mjs → metrics-Beod6tzO.mjs} +1 -1
  35. package/dist/{middleware-BDjCUHMS.mjs → middleware-B7CNsyqt.mjs} +1 -1
  36. package/dist/{middleware-BMgzqBtr.mjs → middleware-BKeQYPm0.mjs} +125 -22
  37. package/dist/{middleware-SSfryVzY.js → middleware-D_9pNYe9.js} +115 -12
  38. package/dist/mod.cjs +5 -5
  39. package/dist/mod.js +5 -5
  40. package/dist/nodeinfo/handler.test.mjs +1 -1
  41. package/dist/{owner-BpOxL5PG.cjs → owner-BQFZtFV4.cjs} +1 -1
  42. package/dist/{owner-DGbYmXqU.mjs → owner-CsKVgMzk.mjs} +3 -3
  43. package/dist/{owner-jH1z_YfD.js → owner-_pbbgLJK.js} +1 -1
  44. package/dist/{portable-key-CCwJGSsU.mjs → portable-key-B4R-CiIV.mjs} +2 -2
  45. package/dist/{proof-CVL7JnZd.mjs → proof-ozG4HfcQ.mjs} +4 -4
  46. package/dist/{send-Ejy1jOz9.mjs → send-ClPt8nYu.mjs} +3 -3
  47. package/dist/sig/compound-proof-discovery.test.mjs +1 -1
  48. package/dist/sig/compound-proof-producer.test.mjs +1 -1
  49. package/dist/sig/compound-proof-verification.test.mjs +2 -2
  50. package/dist/sig/compound-proof.test.mjs +2 -2
  51. package/dist/sig/http.test.mjs +2 -2
  52. package/dist/sig/key.test.mjs +1 -1
  53. package/dist/sig/ld.test.mjs +2 -2
  54. package/dist/sig/mod.cjs +3 -3
  55. package/dist/sig/mod.js +3 -3
  56. package/dist/sig/owner.test.mjs +1 -1
  57. package/dist/sig/portable-collection.test.mjs +2 -2
  58. package/dist/sig/portable-key.test.mjs +3 -3
  59. package/dist/sig/proof.test.mjs +3 -3
  60. package/dist/{sig-Dpy1B1Ad.js → sig-DhdomAN0.js} +2 -2
  61. package/dist/{sig-xoqy3ATq.cjs → sig-k3C8sF7g.cjs} +2 -2
  62. package/dist/{temporal-DNJ1cNpr.mjs → temporal-Ba0EAZAI.mjs} +1 -1
  63. package/dist/testing/kv.test.mjs +1 -1
  64. package/dist/utils/docloader.test.mjs +2 -2
  65. package/dist/utils/kv-cache.test.mjs +1 -1
  66. package/dist/utils/mod.cjs +1 -1
  67. package/dist/utils/mod.js +1 -1
  68. package/package.json +6 -6
@@ -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-BmFz5NLA.mjs";
12
+ import { r as signObject } from "../proof-ozG4HfcQ.mjs";
13
+ import { s as signRequest, u as verifyRequestDetailed } from "../http-B0VRNipE.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 { c as forwardPortableInboxActivity, l as resolvePortableInboxForwardingOptions, o as createFederation } from "../middleware-BKeQYPm0.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,399 @@ test("forwardPortableInboxActivity() does not wait for slow gateways", async ()
520
522
  esm_default.hardReset();
521
523
  }
522
524
  });
525
+ const aliceId = parseIri(`ap+ef61://${did}/users/alice`);
526
+ const rsaGatewayKeyPair = await crypto.subtle.generateKey({
527
+ name: "RSASSA-PKCS1-v1_5",
528
+ modulusLength: 2048,
529
+ publicExponent: new Uint8Array([
530
+ 1,
531
+ 0,
532
+ 1
533
+ ]),
534
+ hash: "SHA-256"
535
+ }, true, ["sign", "verify"]);
536
+ function compatibleAliceId(origin = LOCAL) {
537
+ return `${origin}/.well-known/apgateway/${did}/users/alice`;
538
+ }
539
+ function mapAlice(identifier) {
540
+ return identifier === "alice" ? aliceId : null;
541
+ }
542
+ function signatureKeyId(headers) {
543
+ return headers.get("Signature-Input")?.match(/keyid="([^"]+)"/)?.[1] ?? headers.get("Signature")?.match(/keyId="([^"]+)"/)?.[1];
544
+ }
545
+ /**
546
+ * Makes a document loader that serves Alice's actor document as the gateway
547
+ * at {@link LOCAL} does: signed by her DID, and embedding the gateway keys in
548
+ * its `assertionMethod`.
549
+ */
550
+ async function createGatewayLoader(keyPairs) {
551
+ const { federation } = setup({
552
+ aliceKeyPairs: keyPairs,
553
+ mapPortableActorId: mapAlice
554
+ });
555
+ const keys = await federation.createContext(new URL(LOCAL)).getActorKeyPairs("alice");
556
+ const document = await (await signObject(new Person({
557
+ id: aliceId,
558
+ inbox: parseIri(`${aliceId.href}/inbox`),
559
+ gateways: [
560
+ new URL(LOCAL),
561
+ new URL(GATEWAY2),
562
+ new URL(GATEWAY3)
563
+ ],
564
+ assertionMethods: keys.map((k) => k.multikey)
565
+ }), ed25519PrivateKey, keyId, { contextLoader: mockDocumentLoader })).toJsonLd({ contextLoader: mockDocumentLoader });
566
+ return (url) => {
567
+ if (url.replace(/#.*$/, "") !== compatibleAliceId()) return mockDocumentLoader(url);
568
+ return Promise.resolve({
569
+ contextUrl: null,
570
+ documentUrl: url,
571
+ document: structuredClone(document)
572
+ });
573
+ };
574
+ }
575
+ function toRequest({ url, headers, body }) {
576
+ return new Request(url, {
577
+ method: "POST",
578
+ headers,
579
+ body
580
+ });
581
+ }
582
+ async function assertSignedBy(request, expectedKeyId, documentLoader) {
583
+ assertEquals(signatureKeyId(request.headers), expectedKeyId);
584
+ const result = await verifyRequestDetailed(toRequest(request), {
585
+ documentLoader,
586
+ contextLoader: mockDocumentLoader
587
+ });
588
+ assert(result.verified, JSON.stringify(result));
589
+ assertEquals(result.key.id?.href, expectedKeyId);
590
+ }
591
+ /** Records the requests made to the gateways other than {@link LOCAL}. */
592
+ function captureGatewayRequests(respond = () => new Response(null, { status: 202 })) {
593
+ const requests = [];
594
+ esm_default.spyGlobal();
595
+ for (const gateway of [GATEWAY2, GATEWAY3]) esm_default.post(`begin:${gateway}/`, async (callLog) => {
596
+ const request = callLog.request;
597
+ const captured = {
598
+ url: request.url,
599
+ headers: new Headers(request.headers),
600
+ body: await request.text()
601
+ };
602
+ requests.push(captured);
603
+ return respond(captured);
604
+ });
605
+ return requests;
606
+ }
607
+ test("Federation.fetch() signs forwarded portable inbox deliveries with gateway keys", async (t) => {
608
+ const keyOrders = [[
609
+ "RSA key first",
610
+ [rsaGatewayKeyPair, otherKeyPair],
611
+ "#main-key"
612
+ ], [
613
+ "Ed25519 key first",
614
+ [otherKeyPair, rsaGatewayKeyPair],
615
+ "#key-2"
616
+ ]];
617
+ for (const [name, aliceKeyPairs, fragment] of keyOrders) await t.step(name, async () => {
618
+ const requests = captureGatewayRequests();
619
+ try {
620
+ const kv = new MemoryKvStore();
621
+ const { federation, received } = setup({
622
+ kv,
623
+ aliceKeyPairs,
624
+ mapPortableActorId: mapAlice,
625
+ options: { allowPrivateAddress: true }
626
+ });
627
+ const json = await signedFollow();
628
+ assertEquals((await federation.fetch(post(inboxUrl(), json), { contextData: void 0 })).status, 202);
629
+ assertEquals(received.length, 1);
630
+ assertEquals(requests.map((r) => r.url).sort(), [inboxUrl(did, "/users/alice/inbox", GATEWAY3), inboxUrl(did, "/users/alice/inbox", GATEWAY2)]);
631
+ const loader = await createGatewayLoader(aliceKeyPairs);
632
+ for (const request of requests) {
633
+ await assertSignedBy(request, compatibleAliceId() + fragment, loader);
634
+ assertEquals(JSON.parse(request.body), json);
635
+ }
636
+ assertEquals(await kv.get([
637
+ "_fedify",
638
+ "httpMessageSignaturesSpec",
639
+ GATEWAY2
640
+ ]), "rfc9421");
641
+ } finally {
642
+ esm_default.hardReset();
643
+ }
644
+ });
645
+ });
646
+ test("Federation.fetch() signs queued forwarded portable inbox deliveries with gateway keys", async () => {
647
+ const queue = new RecordingQueue();
648
+ const aliceKeyPairs = [otherKeyPair, rsaGatewayKeyPair];
649
+ const { federation } = setup({
650
+ queue,
651
+ aliceKeyPairs,
652
+ mapPortableActorId: mapAlice,
653
+ options: { allowPrivateAddress: true }
654
+ });
655
+ assertEquals((await federation.fetch(post(inboxUrl(), await signedFollow()), { contextData: void 0 })).status, 202);
656
+ assertEquals(queue.outbox.length, 2);
657
+ const expectedKeyId = `${compatibleAliceId()}#key-2`;
658
+ for (const message of queue.outbox) {
659
+ assertEquals(message.keys.map((k) => k.keyId), [expectedKeyId]);
660
+ assertEquals(message.keys[0].privateKey.kty, "RSA");
661
+ }
662
+ const requests = captureGatewayRequests();
663
+ try {
664
+ for (const message of queue.outbox) await federation.processQueuedTask(void 0, message);
665
+ } finally {
666
+ esm_default.hardReset();
667
+ }
668
+ assertEquals(requests.length, 2);
669
+ const loader = await createGatewayLoader(aliceKeyPairs);
670
+ for (const request of requests) await assertSignedBy(request, expectedKeyId, loader);
671
+ });
672
+ test("Federation.fetch() forwards portable inbox deliveries unsigned without gateway keys", async (t) => {
673
+ const otherActorId = parseIri(`ap+ef61://${otherDid}/users/alice`);
674
+ const cases = [
675
+ ["without a mapper", { aliceKeyPairs: [rsaGatewayKeyPair] }],
676
+ ["the mapper returns null", {
677
+ aliceKeyPairs: [rsaGatewayKeyPair],
678
+ mapPortableActorId: () => null
679
+ }],
680
+ ["the mapper returns another actor", {
681
+ aliceKeyPairs: [rsaGatewayKeyPair],
682
+ mapPortableActorId: () => otherActorId
683
+ }],
684
+ ["the mapper returns another actor of the same DID", {
685
+ aliceKeyPairs: [rsaGatewayKeyPair],
686
+ mapPortableActorId: () => parseIri(`ap+ef61://${did}/users/bob`)
687
+ }],
688
+ ["without a key pairs dispatcher", {
689
+ keyPairs: false,
690
+ mapPortableActorId: mapAlice
691
+ }],
692
+ ["without key pairs", { mapPortableActorId: mapAlice }],
693
+ ["without an RSA key", {
694
+ aliceKeyPairs: [otherKeyPair],
695
+ mapPortableActorId: mapAlice
696
+ }]
697
+ ];
698
+ for (const [name, options] of cases) await t.step(name, async () => {
699
+ const queue = new RecordingQueue();
700
+ const { federation, received } = setup({
701
+ ...options,
702
+ queue
703
+ });
704
+ assertEquals((await federation.fetch(post(inboxUrl(), await signedFollow()), { contextData: void 0 })).status, 202);
705
+ assertEquals(received.length, 1);
706
+ assertEquals(queue.outbox.length, 2);
707
+ for (const message of queue.outbox) assertEquals(message.keys, []);
708
+ });
709
+ });
710
+ test("Federation.fetch() signs forwarded portable inbox deliveries if the mapper returns an equivalent ID", async (t) => {
711
+ const ids = [
712
+ ["ap: URI", parseIri(`ap://${did}/users/alice`)],
713
+ ["percent-encoded DID", parseIri(`ap+ef61://${did.replaceAll(":", "%3A")}/users/alice`)],
714
+ ["compatible identifier", new URL(compatibleAliceId(GATEWAY2))]
715
+ ];
716
+ for (const [name, id] of ids) await t.step(name, async () => {
717
+ const queue = new RecordingQueue();
718
+ const { federation } = setup({
719
+ queue,
720
+ aliceKeyPairs: [rsaGatewayKeyPair],
721
+ mapPortableActorId: (identifier) => identifier === "alice" ? id : null
722
+ });
723
+ assertEquals((await federation.fetch(post(inboxUrl(), await signedFollow()), { contextData: void 0 })).status, 202);
724
+ assertEquals(queue.outbox.length, 2);
725
+ for (const message of queue.outbox) assertEquals(message.keys.map((k) => k.keyId), [`${compatibleAliceId()}#main-key`]);
726
+ });
727
+ });
728
+ test("Federation.fetch() signs forwarded portable inbox deliveries with the gateway keys of the canonical origin", async () => {
729
+ const queue = new RecordingQueue();
730
+ const { federation } = setup({
731
+ queue,
732
+ gateways: [
733
+ GATEWAY2,
734
+ LOCAL,
735
+ GATEWAY3
736
+ ],
737
+ aliceKeyPairs: [rsaGatewayKeyPair],
738
+ mapPortableActorId: mapAlice,
739
+ options: { origin: GATEWAY2 }
740
+ });
741
+ assertEquals((await federation.fetch(post(inboxUrl(), await signedFollow()), { contextData: void 0 })).status, 202);
742
+ assertEquals(queue.outbox.length, 1);
743
+ assertEquals(queue.outbox[0].keys.map((k) => k.keyId), [`${compatibleAliceId(GATEWAY2)}#main-key`]);
744
+ });
745
+ test("Federation.fetch() double-knocks forwarded portable inbox deliveries", async (t) => {
746
+ await t.step("firstKnock", async () => {
747
+ const requests = captureGatewayRequests();
748
+ try {
749
+ const { federation } = setup({
750
+ aliceKeyPairs: [rsaGatewayKeyPair],
751
+ mapPortableActorId: mapAlice,
752
+ options: {
753
+ allowPrivateAddress: true,
754
+ firstKnock: "draft-cavage-http-signatures-12"
755
+ }
756
+ });
757
+ assertEquals((await federation.fetch(post(inboxUrl(), await signedFollow()), { contextData: void 0 })).status, 202);
758
+ assertEquals(requests.length, 2);
759
+ for (const request of requests) {
760
+ assert(request.headers.has("Signature"));
761
+ assertFalse(request.headers.has("Signature-Input"));
762
+ }
763
+ } finally {
764
+ esm_default.hardReset();
765
+ }
766
+ });
767
+ await t.step("falls back to the other spec", async () => {
768
+ const requests = captureGatewayRequests((request) => new Response(null, { status: request.headers.has("Signature-Input") ? 401 : 202 }));
769
+ try {
770
+ const kv = new MemoryKvStore();
771
+ const { federation } = setup({
772
+ kv,
773
+ aliceKeyPairs: [rsaGatewayKeyPair],
774
+ mapPortableActorId: mapAlice,
775
+ options: { allowPrivateAddress: true }
776
+ });
777
+ assertEquals((await federation.fetch(post(inboxUrl(), await signedFollow()), { contextData: void 0 })).status, 202);
778
+ assertEquals(requests.length, 4);
779
+ const loader = await createGatewayLoader([rsaGatewayKeyPair]);
780
+ for (const request of requests) {
781
+ if (request.headers.has("Signature-Input")) continue;
782
+ await assertSignedBy(request, `${compatibleAliceId()}#main-key`, loader);
783
+ }
784
+ assertEquals(await kv.get([
785
+ "_fedify",
786
+ "httpMessageSignaturesSpec",
787
+ GATEWAY2
788
+ ]), "draft-cavage-http-signatures-12");
789
+ } finally {
790
+ esm_default.hardReset();
791
+ }
792
+ });
793
+ });
794
+ test("A Fedify gateway accepts signed forwarded portable inbox deliveries", async () => {
795
+ const requests = captureGatewayRequests();
796
+ let forwarded;
797
+ try {
798
+ const { federation } = setup({
799
+ aliceKeyPairs: [rsaGatewayKeyPair],
800
+ mapPortableActorId: mapAlice,
801
+ options: { allowPrivateAddress: true }
802
+ });
803
+ assertEquals((await federation.fetch(post(inboxUrl(), await signedFollow()), { contextData: void 0 })).status, 202);
804
+ forwarded = requests.find((r) => r.url.startsWith(`${GATEWAY2}/`));
805
+ } finally {
806
+ esm_default.hardReset();
807
+ }
808
+ await assertSignedBy(forwarded, `${compatibleAliceId()}#main-key`, await createGatewayLoader([rsaGatewayKeyPair]));
809
+ const queue = new RecordingQueue();
810
+ const { federation, received } = setup({
811
+ queue,
812
+ options: { origin: GATEWAY2 }
813
+ });
814
+ assertEquals((await federation.fetch(toRequest(forwarded), { contextData: void 0 })).status, 202);
815
+ assertEquals(received.length, 1);
816
+ assertEquals(received[0].recipient, "alice");
817
+ assertEquals(queue.outbox.map((m) => m.inbox).sort(), [inboxUrl(did, "/users/alice/inbox", LOCAL), inboxUrl(did, "/users/alice/inbox", GATEWAY3)]);
818
+ });
819
+ function forwardingParameters(overrides = {}) {
820
+ const activityId = parseIri(`ap+ef61://${did}/follows/${++activityCounter}`);
821
+ return {
822
+ recipient: {
823
+ actorId: aliceId,
824
+ inboxId: parseIri(`ap+ef61://${did}/users/alice/inbox`),
825
+ canonicalInboxId: `ap://${did}/users/alice/inbox`,
826
+ gateways: [new URL(LOCAL), new URL(GATEWAY2)]
827
+ },
828
+ activity: { id: formatIri(activityId) },
829
+ activityId,
830
+ activityType: "https://www.w3.org/ns/activitystreams#Follow",
831
+ excludedOrigins: [LOCAL],
832
+ baseUrl: LOCAL,
833
+ kv: new MemoryKvStore(),
834
+ kvPrefix: ["_fedify", "portableInboxForwarding"],
835
+ allowPrivateAddress: true,
836
+ ...overrides
837
+ };
838
+ }
839
+ const gatewayKey = {
840
+ keyId: new URL(`${compatibleAliceId()}#main-key`),
841
+ privateKey: rsaGatewayKeyPair.privateKey
842
+ };
843
+ test("forwardPortableInboxActivity() gets keys only when it forwards", async () => {
844
+ let calls = 0;
845
+ const parameters = forwardingParameters({
846
+ outboxQueue: new RecordingQueue(),
847
+ getKeys: () => {
848
+ calls++;
849
+ return Promise.resolve([gatewayKey]);
850
+ }
851
+ });
852
+ assertEquals((await forwardPortableInboxActivity(parameters)).length, 1);
853
+ assertEquals(calls, 1);
854
+ assertEquals(await forwardPortableInboxActivity(parameters), []);
855
+ assertEquals(calls, 1);
856
+ });
857
+ test("forwardPortableInboxActivity() forwards unsigned if it cannot get keys", async (t) => {
858
+ const nonExtractable = await crypto.subtle.generateKey({
859
+ name: "RSASSA-PKCS1-v1_5",
860
+ modulusLength: 2048,
861
+ publicExponent: new Uint8Array([
862
+ 1,
863
+ 0,
864
+ 1
865
+ ]),
866
+ hash: "SHA-256"
867
+ }, false, ["sign", "verify"]);
868
+ const getKeysCases = [["getKeys() rejects", () => Promise.reject(/* @__PURE__ */ new Error("Unavailable."))], ["the key is not extractable", () => Promise.resolve([{
869
+ keyId: gatewayKey.keyId,
870
+ privateKey: nonExtractable.privateKey
871
+ }])]];
872
+ for (const [name, getKeys] of getKeysCases) {
873
+ await t.step(`${name} (queued)`, async () => {
874
+ const queue = new RecordingQueue();
875
+ assertEquals((await forwardPortableInboxActivity(forwardingParameters({
876
+ outboxQueue: queue,
877
+ getKeys
878
+ }))).length, 1);
879
+ assertEquals(queue.outbox.length, 1);
880
+ assertEquals(queue.outbox[0].keys, []);
881
+ });
882
+ await t.step(`${name} (immediate)`, async () => {
883
+ const requests = captureGatewayRequests();
884
+ try {
885
+ assertEquals((await forwardPortableInboxActivity(forwardingParameters({ getKeys }))).length, 1);
886
+ } finally {
887
+ esm_default.hardReset();
888
+ }
889
+ assertEquals(requests.length, 1);
890
+ assertEquals(signatureKeyId(requests[0].headers), void 0);
891
+ });
892
+ }
893
+ });
894
+ test("forwardPortableInboxActivity() does not wait for slow keys", async () => {
895
+ let release = () => {};
896
+ const pending = new Promise((resolve) => release = resolve);
897
+ const requests = captureGatewayRequests();
898
+ try {
899
+ const started = performance.now();
900
+ const forwarded = await forwardPortableInboxActivity(forwardingParameters({
901
+ options: resolvePortableInboxForwardingOptions({ deadline: { milliseconds: 10 } }),
902
+ getKeys: async () => {
903
+ await pending;
904
+ throw new Error("Unavailable.");
905
+ }
906
+ }));
907
+ assert(performance.now() - started < 5e3);
908
+ assertEquals(forwarded.length, 1);
909
+ assertEquals(requests.length, 0);
910
+ release();
911
+ for (let i = 0; i < 100 && requests.length < 1; i++) await new Promise((resolve) => setTimeout(resolve, 10));
912
+ assertEquals(requests.length, 1);
913
+ assertEquals(signatureKeyId(requests[0].headers), void 0);
914
+ } finally {
915
+ esm_default.hardReset();
916
+ }
917
+ });
523
918
  test("Context.getPortableInboxUri()", async (t) => {
524
919
  const { federation } = setup();
525
920
  await t.step("Context", () => {
@@ -7,10 +7,10 @@ import { t as assertInstanceOf } from "../assert_instance_of-mrXWWNY-.mjs";
7
7
  import { t as assertThrows } from "../assert_throws-BYITOvPO.mjs";
8
8
  import "../std__assert-DvMDRPes.mjs";
9
9
  import { t as assertStrictEquals } from "../assert_strict_equals-CYvBmkpC.mjs";
10
- import { o as verifyPortableObjectProof, r as signObject } from "../proof-CVL7JnZd.mjs";
11
- import { s as signRequest } from "../http-CCJ97Jql.mjs";
10
+ import { o as verifyPortableObjectProof, r as signObject } from "../proof-ozG4HfcQ.mjs";
11
+ import { s as signRequest } from "../http-B0VRNipE.mjs";
12
12
  import { a as rsaPrivateKey3, c as rsaPublicKey3, i as rsaPrivateKey2, n as ed25519PrivateKey, r as ed25519PublicKey, s as rsaPublicKey2 } from "../keys-DIJNHW-Y.mjs";
13
- import { o as createFederation, v as PORTABLE_OBJECT_CONTENT_TYPE } from "../middleware-BMgzqBtr.mjs";
13
+ import { o as createFederation, v as PORTABLE_OBJECT_CONTENT_TYPE } from "../middleware-BKeQYPm0.mjs";
14
14
  import { Collection, Create, Note, Object as Object$1, Person, Tombstone, lookupObject } from "@fedify/vocab";
15
15
  import { mockDocumentLoader, test } from "@fedify/fixture";
16
16
  import { exportDidKey, formatIri, parseIri } from "@fedify/vocab-runtime";
@@ -10,10 +10,10 @@ import { t as assertNotEquals } from "../assert_not_equals-CpPK2t89.mjs";
10
10
  import "../std__assert-DvMDRPes.mjs";
11
11
  import { t as assertRejects } from "../assert_rejects-CTptdYck.mjs";
12
12
  import { t as assert } from "../assert-D1Eafmd_.mjs";
13
- import { l as verifyRequest } from "../http-CCJ97Jql.mjs";
13
+ import { l as verifyRequest } from "../http-B0VRNipE.mjs";
14
14
  import { i as rsaPrivateKey2, n as ed25519PrivateKey, s as rsaPublicKey2, t as ed25519Multikey } from "../keys-DIJNHW-Y.mjs";
15
- import { t as doesActorOwnKey } from "../owner-DGbYmXqU.mjs";
16
- import { n as extractInboxes, r as sendActivity, t as SendActivityError } from "../send-Ejy1jOz9.mjs";
15
+ import { t as doesActorOwnKey } from "../owner-CsKVgMzk.mjs";
16
+ import { n as extractInboxes, r as sendActivity, t as SendActivityError } from "../send-ClPt8nYu.mjs";
17
17
  import { Activity, Application, Endpoints, Group, Person, Service } from "@fedify/vocab";
18
18
  import { createTestMeterProvider, createTestTracerProvider, mockDocumentLoader, test } from "@fedify/fixture";
19
19
  import { FetchError, UrlError } from "@fedify/vocab-runtime";
@@ -4,7 +4,7 @@ globalThis.addEventListener = () => {};
4
4
  import { t as MemoryKvStore } from "../../kv-DCHkaJ9-.mjs";
5
5
  import { a as numberSchema, i as makeSchema, n as baseOptions, o as stringSchema, t as MockQueue } from "../../tasks-dbCShlCb.mjs";
6
6
  import "../../testing-CO_cEvQc.mjs";
7
- import { o as createFederation } from "../../middleware-BMgzqBtr.mjs";
7
+ import { o as createFederation } from "../../middleware-BKeQYPm0.mjs";
8
8
  import { n as ParallelMessageQueue } from "../../mq-Dm1Wmtm-.mjs";
9
9
  import { configure, reset } from "@logtape/logtape";
10
10
  import { delay } from "es-toolkit";
@@ -4,8 +4,8 @@ globalThis.addEventListener = () => {};
4
4
  import { t as MemoryKvStore } from "../../kv-DCHkaJ9-.mjs";
5
5
  import { a as numberSchema, i as makeSchema, n as baseOptions, o as stringSchema, t as MockQueue } from "../../tasks-dbCShlCb.mjs";
6
6
  import "../../testing-CO_cEvQc.mjs";
7
- import { o as createFederation } from "../../middleware-BMgzqBtr.mjs";
8
- import { r as createFederationBuilder } from "../../builder-i3jxVe9m.mjs";
7
+ import { o as createFederation } from "../../middleware-BKeQYPm0.mjs";
8
+ import { r as createFederationBuilder } from "../../builder-BGlpUocK.mjs";
9
9
  import { t as TaskCodec } from "../../codec-C6rTrpIp.mjs";
10
10
  import { t as InProcessMessageQueue } from "../../mq-Dm1Wmtm-.mjs";
11
11
  import { W3CTraceContextPropagator } from "@opentelemetry/core";
@@ -4,7 +4,7 @@ globalThis.addEventListener = () => {};
4
4
  import { t as assertFalse } from "../assert_false-CPdRnc3E.mjs";
5
5
  import "../std__assert-DvMDRPes.mjs";
6
6
  import { t as assert } from "../assert-D1Eafmd_.mjs";
7
- import { t as hasMalformedKnownTemporalLiteral } from "../temporal-DNJ1cNpr.mjs";
7
+ import { t as hasMalformedKnownTemporalLiteral } from "../temporal-Ba0EAZAI.mjs";
8
8
  import { test } from "@fedify/fixture";
9
9
  //#region src/federation/temporal.test.ts
10
10
  test("hasMalformedKnownTemporalLiteral() detects expanded proof timestamps", async () => {
@@ -6,7 +6,7 @@ import { t as MemoryKvStore } from "../kv-DCHkaJ9-.mjs";
6
6
  import { t as assertEquals } from "../assert_equals-Z6IcEbWU.mjs";
7
7
  import { t as assertNotEquals } from "../assert_not_equals-CpPK2t89.mjs";
8
8
  import "../std__assert-DvMDRPes.mjs";
9
- import { o as createFederation, s as handleWebFinger } from "../middleware-BMgzqBtr.mjs";
9
+ import { o as createFederation, s as handleWebFinger } from "../middleware-BKeQYPm0.mjs";
10
10
  import { Image, Link, Person, Tombstone } from "@fedify/vocab";
11
11
  import { createTestMeterProvider, test } from "@fedify/fixture";
12
12
  import { parseIri } from "@fedify/vocab-runtime";