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

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 (84) hide show
  1. package/dist/{builder-BGlpUocK.mjs → builder-Bno54o_-.mjs} +34 -3
  2. package/dist/compat/mod.d.cts +1 -1
  3. package/dist/compat/mod.d.ts +1 -1
  4. package/dist/compat/transformers.test.mjs +1 -1
  5. package/dist/{compound-proof-B6Aj2kGY.mjs → compound-proof-X3SxXVbc.mjs} +191 -13
  6. package/dist/{context-CZjZYXFC.d.cts → context-C43spTGI.d.cts} +5 -2
  7. package/dist/{context-BSywYspi.d.ts → context-MORIQJIh.d.ts} +5 -2
  8. package/dist/{deno-Dn-py9Zy.mjs → deno-dxdvl2XZ.mjs} +1 -1
  9. package/dist/{docloader-BUq5bYJr.mjs → docloader-eBxQDsk9.mjs} +2 -2
  10. package/dist/federation/builder.test.mjs +1 -1
  11. package/dist/federation/compound-fanout.test.mjs +3 -3
  12. package/dist/federation/handler-compound-proof.test.mjs +3 -3
  13. package/dist/federation/handler.test.mjs +5 -5
  14. package/dist/federation/hashlink-media.test.mjs +2 -2
  15. package/dist/federation/idempotency.test.mjs +2 -2
  16. package/dist/federation/metrics.test.mjs +1 -1
  17. package/dist/federation/middleware.test.mjs +7 -7
  18. package/dist/federation/mod.cjs +1 -1
  19. package/dist/federation/mod.d.cts +2 -2
  20. package/dist/federation/mod.d.ts +2 -2
  21. package/dist/federation/mod.js +1 -1
  22. package/dist/federation/outgoing-proof.test.mjs +254 -5
  23. package/dist/federation/portable-gateway-keys.test.mjs +72 -11
  24. package/dist/federation/portable-inbox.test.mjs +487 -10
  25. package/dist/federation/portable.test.mjs +182 -4
  26. package/dist/federation/send.test.mjs +3 -3
  27. package/dist/federation/tasks/enqueue.test.mjs +1 -1
  28. package/dist/federation/tasks/tasks.test.mjs +2 -2
  29. package/dist/federation/temporal.test.mjs +1 -1
  30. package/dist/federation/webfinger.test.mjs +89 -1
  31. package/dist/{federation-BLGx8JUH.cjs → federation-C4Ef3JKa.cjs} +1487 -1070
  32. package/dist/{http-B5q1CHtL.cjs → http-C8cZTE3Y.cjs} +1224 -985
  33. package/dist/{http-4PCCvhsU.js → http-D-rbr_Sy.js} +1663 -1460
  34. package/dist/{http-B0VRNipE.mjs → http-DsgawTd8.mjs} +82 -7
  35. package/dist/{key-BJrp524f.mjs → key-DYfmjnmp.mjs} +39 -99
  36. package/dist/{kv-cache-1KUmAPH7.js → kv-cache-DIxwMeNi.js} +1 -1
  37. package/dist/{kv-cache-Dhhhrryv.cjs → kv-cache-Dvd-Q-E2.cjs} +1 -1
  38. package/dist/{kv-cache-C4C3LyYW.mjs → kv-cache-EvOgrFsS.mjs} +1 -1
  39. package/dist/{ld-BmFz5NLA.mjs → ld-B00wWHzc.mjs} +3 -3
  40. package/dist/{metrics-Beod6tzO.mjs → metrics-BP3vdBpb.mjs} +1 -1
  41. package/dist/{middleware-BKeQYPm0.mjs → middleware-Br1_d-Ga.mjs} +606 -389
  42. package/dist/{middleware-D_9pNYe9.js → middleware-CcXMe400.js} +1602 -1185
  43. package/dist/{middleware-B7CNsyqt.mjs → middleware-CeBVBa61.mjs} +1 -1
  44. package/dist/{mod-B2h2KY0q.d.ts → mod-BL5BQHCq.d.ts} +1 -1
  45. package/dist/{mod-HDSvS-ww.d.cts → mod-C8LieBzF.d.cts} +16 -3
  46. package/dist/{mod-C9WSpZWm.d.ts → mod-Coacostr.d.ts} +16 -3
  47. package/dist/{mod-JzkaEXsi.d.cts → mod-DMI4DbOo.d.cts} +1 -1
  48. package/dist/mod.cjs +5 -5
  49. package/dist/mod.d.cts +3 -3
  50. package/dist/mod.d.ts +3 -3
  51. package/dist/mod.js +5 -5
  52. package/dist/nodeinfo/handler.test.mjs +1 -1
  53. package/dist/{owner-CsKVgMzk.mjs → owner-BJPDRDRq.mjs} +5 -4
  54. package/dist/{owner-BQFZtFV4.cjs → owner-DSzH7wro.cjs} +2 -2
  55. package/dist/{owner-_pbbgLJK.js → owner-U12fDpQW.js} +2 -2
  56. package/dist/{portable-key-B4R-CiIV.mjs → portable-key-D0mWbV9G.mjs} +60 -41
  57. package/dist/portable-key-id-Bva5iT3W.mjs +136 -0
  58. package/dist/{proof-ozG4HfcQ.mjs → proof-ChjBwnep.mjs} +59 -18
  59. package/dist/{send-ClPt8nYu.mjs → send-yFX0E-ye.mjs} +3 -3
  60. package/dist/sig/compound-proof-discovery.test.mjs +1 -1
  61. package/dist/sig/compound-proof-producer.test.mjs +1 -1
  62. package/dist/sig/compound-proof-verification.test.mjs +160 -3
  63. package/dist/sig/compound-proof.test.mjs +2 -2
  64. package/dist/sig/http.test.mjs +72 -2
  65. package/dist/sig/key.test.mjs +1 -1
  66. package/dist/sig/ld.test.mjs +2 -2
  67. package/dist/sig/mod.cjs +3 -3
  68. package/dist/sig/mod.d.cts +1 -1
  69. package/dist/sig/mod.d.ts +1 -1
  70. package/dist/sig/mod.js +3 -3
  71. package/dist/sig/owner.test.mjs +1 -1
  72. package/dist/sig/portable-collection.test.mjs +69 -5
  73. package/dist/sig/portable-key.test.mjs +176 -10
  74. package/dist/sig/proof.test.mjs +107 -4
  75. package/dist/{sig-k3C8sF7g.cjs → sig-B689Nj7y.cjs} +19 -3
  76. package/dist/{sig-DhdomAN0.js → sig-C85KqAcx.js} +19 -3
  77. package/dist/{temporal-Ba0EAZAI.mjs → temporal-Dl9x-Qw4.mjs} +1 -1
  78. package/dist/testing/kv.test.mjs +1 -1
  79. package/dist/testing/mod.d.mts +5 -2
  80. package/dist/utils/docloader.test.mjs +2 -2
  81. package/dist/utils/kv-cache.test.mjs +1 -1
  82. package/dist/utils/mod.cjs +1 -1
  83. package/dist/utils/mod.js +1 -1
  84. package/package.json +7 -7
@@ -8,14 +8,15 @@ 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-B00wWHzc.mjs";
12
+ import { i as signObject } from "../proof-ChjBwnep.mjs";
13
+ import { f as verifyRequestDetailed, l as signRequest } from "../http-DsgawTd8.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";
16
- import { Create, Follow, Note, Person, Tombstone } from "@fedify/vocab";
15
+ import { _ as coversDelivery, o as createFederation, v as forwardPortableInboxActivity, y as resolvePortableInboxForwardingOptions } from "../middleware-Br1_d-Ga.mjs";
16
+ import { Create, CryptographicKey, 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";
19
+ import { encodeBase64 } from "byte-encodings/base64";
19
20
  //#region src/federation/portable-inbox.test.ts
20
21
  const did = await exportDidKey(ed25519PublicKey.publicKey);
21
22
  const keyId = new URL(`${did}#${did.substring(8)}`);
@@ -522,6 +523,118 @@ test("forwardPortableInboxActivity() does not wait for slow gateways", async ()
522
523
  esm_default.hardReset();
523
524
  }
524
525
  });
526
+ function compatibleAlice(path = "/users/alice", { authority = did, origin = LOCAL, gateways = [
527
+ LOCAL,
528
+ GATEWAY2,
529
+ GATEWAY3
530
+ ] } = {}) {
531
+ return {
532
+ id: new URL(`${origin}/.well-known/apgateway/${authority}${path}`),
533
+ inbox: new URL(inboxUrl(authority, `${path}/inbox`, origin)),
534
+ gateways: gateways.map((g) => new URL(g))
535
+ };
536
+ }
537
+ test("Federation.fetch() delivers to portable inboxes of compatible-ID actors", async (t) => {
538
+ const apAlice = parseIri(`ap+ef61://${did}/users/alice`);
539
+ const apInbox = parseIri(`ap+ef61://${did}/users/alice/inbox`);
540
+ const cases = [
541
+ [
542
+ "compatible actor and inbox",
543
+ compatibleAlice(),
544
+ ""
545
+ ],
546
+ [
547
+ "on another gateway",
548
+ compatibleAlice(void 0, { origin: GATEWAY2 }),
549
+ ""
550
+ ],
551
+ [
552
+ "compatible actor and ap: inbox",
553
+ {
554
+ ...compatibleAlice(),
555
+ inbox: apInbox
556
+ },
557
+ ""
558
+ ],
559
+ [
560
+ "ap: actor and compatible inbox",
561
+ {
562
+ ...compatibleAlice(),
563
+ id: apAlice
564
+ },
565
+ ""
566
+ ],
567
+ [
568
+ "compatible inbox with a query",
569
+ {
570
+ ...compatibleAlice(),
571
+ inbox: new URL(inboxUrl() + "?page=1")
572
+ },
573
+ "?page=1"
574
+ ]
575
+ ];
576
+ for (const [name, alice, query] of cases) await t.step(name, async () => {
577
+ const queue = new RecordingQueue();
578
+ const { federation, received } = setup({
579
+ queue,
580
+ actor: (_ctx, identifier) => identifier === "alice" ? new Person(alice) : null
581
+ });
582
+ assertEquals((await federation.fetch(post(inboxUrl(), await signedFollow()), { contextData: void 0 })).status, 202);
583
+ assertEquals(received.length, 1);
584
+ assertEquals(received[0].recipient, "alice");
585
+ assertEquals(queue.outbox.map((m) => m.inbox).sort(), [inboxUrl(did, "/users/alice/inbox", GATEWAY3) + query, inboxUrl(did, "/users/alice/inbox", GATEWAY2) + query]);
586
+ });
587
+ const json = await signedFollow();
588
+ const rejections = [
589
+ ["DID mismatch", new Person(compatibleAlice(void 0, { authority: otherDid }))],
590
+ ["inbox mismatch", new Person({
591
+ ...compatibleAlice(),
592
+ inbox: new URL(inboxUrl(did, "/users/alice/other-inbox"))
593
+ })],
594
+ ["this server is not a gateway of the actor", new Person(compatibleAlice(void 0, { gateways: [GATEWAY2] }))],
595
+ ["malformed actor ID", new Person({
596
+ ...compatibleAlice(),
597
+ id: new URL(compatibleAlice().id.href + "?@gateway=https%3A%2F%2Fexample.com")
598
+ })],
599
+ ["malformed inbox ID", new Person({
600
+ ...compatibleAlice(),
601
+ inbox: new URL(inboxUrl() + "?@gateway=https%3A%2F%2Fexample.com")
602
+ })]
603
+ ];
604
+ for (const [name, actor] of rejections) await t.step(name, async () => {
605
+ const { federation, received } = setup({ actor: (_ctx, identifier) => identifier === "alice" ? actor : null });
606
+ assertEquals((await federation.fetch(post(inboxUrl(), json), { contextData: void 0 })).status, 404);
607
+ assertEquals(received.length, 0);
608
+ });
609
+ });
610
+ test("forwardPortableInboxActivity() deduplicates compatible activity IDs", async () => {
611
+ const kv = new MemoryKvStore();
612
+ const queue = new RecordingQueue();
613
+ const forward = (activityId) => forwardPortableInboxActivity({
614
+ recipient: {
615
+ actorId: compatibleAlice().id,
616
+ inboxId: parseIri(`ap+ef61://${did}/users/alice/inbox`),
617
+ canonicalInboxId: `ap+ef61://${did}/users/alice/inbox`,
618
+ gateways: [new URL(LOCAL), new URL(GATEWAY2)]
619
+ },
620
+ activity: { id: activityId.href },
621
+ activityId,
622
+ activityType: "https://www.w3.org/ns/activitystreams#Follow",
623
+ excludedOrigins: [LOCAL],
624
+ baseUrl: LOCAL,
625
+ kv,
626
+ kvPrefix: ["_fedify", "portableInboxForwarding"],
627
+ outboxQueue: queue
628
+ });
629
+ const path = `${did}/follows/compatible`;
630
+ assertEquals((await forward(new URL(`${LOCAL}/.well-known/apgateway/${path}`))).length, 1);
631
+ for (const id of [
632
+ new URL(`${GATEWAY2}/.well-known/apgateway/${path}`),
633
+ new URL(`${LOCAL}/.well-known/apgateway/${path}?x=1`),
634
+ parseIri(`ap://${path}`)
635
+ ]) assertEquals(await forward(id), [], id.href);
636
+ assertEquals(queue.outbox.length, 1);
637
+ });
525
638
  const aliceId = parseIri(`ap+ef61://${did}/users/alice`);
526
639
  const rsaGatewayKeyPair = await crypto.subtle.generateKey({
527
640
  name: "RSASSA-PKCS1-v1_5",
@@ -547,12 +660,12 @@ function signatureKeyId(headers) {
547
660
  * at {@link LOCAL} does: signed by her DID, and embedding the gateway keys in
548
661
  * its `assertionMethod`.
549
662
  */
550
- async function createGatewayLoader(keyPairs) {
663
+ async function createGatewayLoader(keyPairs, origin = LOCAL) {
551
664
  const { federation } = setup({
552
665
  aliceKeyPairs: keyPairs,
553
666
  mapPortableActorId: mapAlice
554
667
  });
555
- const keys = await federation.createContext(new URL(LOCAL)).getActorKeyPairs("alice");
668
+ const keys = await federation.createContext(new URL(origin)).getActorKeyPairs("alice");
556
669
  const document = await (await signObject(new Person({
557
670
  id: aliceId,
558
671
  inbox: parseIri(`${aliceId.href}/inbox`),
@@ -564,7 +677,7 @@ async function createGatewayLoader(keyPairs) {
564
677
  assertionMethods: keys.map((k) => k.multikey)
565
678
  }), ed25519PrivateKey, keyId, { contextLoader: mockDocumentLoader })).toJsonLd({ contextLoader: mockDocumentLoader });
566
679
  return (url) => {
567
- if (url.replace(/#.*$/, "") !== compatibleAliceId()) return mockDocumentLoader(url);
680
+ if (url.replace(/#.*$/, "") !== compatibleAliceId(origin)) return mockDocumentLoader(url);
568
681
  return Promise.resolve({
569
682
  contextUrl: null,
570
683
  documentUrl: url,
@@ -807,14 +920,240 @@ test("A Fedify gateway accepts signed forwarded portable inbox deliveries", asyn
807
920
  }
808
921
  await assertSignedBy(forwarded, `${compatibleAliceId()}#main-key`, await createGatewayLoader([rsaGatewayKeyPair]));
809
922
  const queue = new RecordingQueue();
923
+ const loader = await createGatewayLoader([rsaGatewayKeyPair]);
810
924
  const { federation, received } = setup({
811
925
  queue,
812
- options: { origin: GATEWAY2 }
926
+ options: {
927
+ origin: GATEWAY2,
928
+ documentLoaderFactory: () => loader
929
+ }
813
930
  });
814
931
  assertEquals((await federation.fetch(toRequest(forwarded), { contextData: void 0 })).status, 202);
815
932
  assertEquals(received.length, 1);
816
933
  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)]);
934
+ assertEquals(queue.outbox.map((m) => m.inbox), [inboxUrl(did, "/users/alice/inbox", GATEWAY3)]);
935
+ });
936
+ /** Records the URLs that a document loader is asked for. */
937
+ function recordLoader(loader) {
938
+ const urls = [];
939
+ return {
940
+ urls,
941
+ loader: (url, options) => {
942
+ urls.push(url);
943
+ return loader(url, options);
944
+ }
945
+ };
946
+ }
947
+ const gateway2KeyId = new URL(`${compatibleAliceId(GATEWAY2)}#main-key`);
948
+ /** Signs a delivery to Alice's inbox on {@link LOCAL}. */
949
+ async function signDelivery(body, { keyId = gateway2KeyId, privateKey = rsaGatewayKeyPair.privateKey, spec = "draft-cavage-http-signatures-12", components, label, headers } = {}) {
950
+ const signed = await signRequest(post(inboxUrl(), body, headers), privateKey, keyId, {
951
+ spec,
952
+ ...components == null ? {} : { body: null },
953
+ rfc9421: {
954
+ label,
955
+ components: components?.map((value) => ({
956
+ value,
957
+ params: {}
958
+ }))
959
+ }
960
+ });
961
+ return new Request(signed.url, {
962
+ method: "POST",
963
+ headers: signed.headers,
964
+ body: JSON.stringify(body)
965
+ });
966
+ }
967
+ async function deliverToGateway(request, { loader, kv, maxTargets }) {
968
+ const queue = new RecordingQueue();
969
+ const { federation, received, kv: store } = setup({
970
+ queue,
971
+ kv,
972
+ options: {
973
+ documentLoaderFactory: () => loader,
974
+ ...maxTargets == null ? {} : { portableInboxForwarding: { maxTargets } }
975
+ }
976
+ });
977
+ const response = await federation.fetch(request, { contextData: void 0 });
978
+ assert(received.length <= 1);
979
+ return {
980
+ response,
981
+ queue,
982
+ kv: store
983
+ };
984
+ }
985
+ function forwardedGateways(queue) {
986
+ return queue.outbox.map((m) => new URL(m.inbox).origin).sort();
987
+ }
988
+ test("Federation.fetch() does not forward portable inbox deliveries back to the gateway that forwarded them", async (t) => {
989
+ const gatewayLoader = await createGatewayLoader([rsaGatewayKeyPair], GATEWAY2);
990
+ for (const spec of ["draft-cavage-http-signatures-12", "rfc9421"]) await t.step(spec, async () => {
991
+ const { loader, urls } = recordLoader(gatewayLoader);
992
+ const json = await signedFollow();
993
+ const { response, queue, kv } = await deliverToGateway(await signDelivery(json, { spec }), { loader });
994
+ assertEquals(response.status, 202);
995
+ assertEquals(forwardedGateways(queue), [GATEWAY3]);
996
+ assertEquals(urls.filter((u) => u.startsWith(GATEWAY2)), [gateway2KeyId.href]);
997
+ const { queue: queue2 } = await deliverToGateway(post(inboxUrl(), json), {
998
+ loader,
999
+ kv
1000
+ });
1001
+ assertEquals(queue2.outbox, []);
1002
+ });
1003
+ await t.step("identified before the targets are capped", async () => {
1004
+ const { queue } = await deliverToGateway(await signDelivery(await signedFollow()), {
1005
+ loader: gatewayLoader,
1006
+ maxTargets: 1
1007
+ });
1008
+ assertEquals(forwardedGateways(queue), [GATEWAY3]);
1009
+ });
1010
+ await t.step("with an unrelated signature first", async () => {
1011
+ const { loader, urls } = recordLoader(gatewayLoader);
1012
+ const json = await signedFollow();
1013
+ const unrelated = await signDelivery(json, {
1014
+ spec: "rfc9421",
1015
+ keyId: new URL("https://unrelated.example/key"),
1016
+ privateKey: rsaPrivateKey3,
1017
+ label: "sig1"
1018
+ });
1019
+ const gateway = await signDelivery(json, {
1020
+ spec: "rfc9421",
1021
+ label: "sig2"
1022
+ });
1023
+ const headers = new Headers(gateway.headers);
1024
+ for (const name of ["Signature-Input", "Signature"]) headers.set(name, `${unrelated.headers.get(name)}, ${gateway.headers.get(name)}`);
1025
+ const { queue } = await deliverToGateway(new Request(gateway.url, {
1026
+ method: "POST",
1027
+ headers,
1028
+ body: JSON.stringify(json)
1029
+ }), { loader });
1030
+ assertEquals(forwardedGateways(queue), [GATEWAY3]);
1031
+ assertFalse(urls.some((u) => u.startsWith("https://unrelated.example/")));
1032
+ });
1033
+ await t.step("activity authenticated by a Linked Data Signature", async () => {
1034
+ const { queue } = await deliverToGateway(await signDelivery(await signJsonLd({
1035
+ "@context": "https://www.w3.org/ns/activitystreams",
1036
+ id: `https://example.com/follows/lds-${++activityCounter}`,
1037
+ type: "Follow",
1038
+ actor: "https://example.com/person2",
1039
+ object: formatIri(aliceId)
1040
+ }, rsaPrivateKey3, rsaPublicKey3.id, { contextLoader: mockDocumentLoader })), { loader: gatewayLoader });
1041
+ assertEquals(forwardedGateways(queue), [GATEWAY3]);
1042
+ });
1043
+ });
1044
+ test("Federation.fetch() forwards portable inbox deliveries whose sending gateway is not identified", async (t) => {
1045
+ const gatewayLoader = await createGatewayLoader([rsaGatewayKeyPair], GATEWAY2);
1046
+ const bobKeyId = new URL(`${GATEWAY2}/.well-known/apgateway/${did}/users/bob#main-key`);
1047
+ const compatibleId = new URL(compatibleAliceId(GATEWAY2));
1048
+ const unsignedActor = await new Person({
1049
+ id: compatibleId,
1050
+ inbox: new URL(`${compatibleId.href}/inbox`),
1051
+ publicKey: rsaPublicKey2.clone({
1052
+ id: gateway2KeyId,
1053
+ owner: compatibleId
1054
+ })
1055
+ }).toJsonLd({ contextLoader: mockDocumentLoader });
1056
+ const unsignedLoader = (url) => {
1057
+ if (url.replace(/#.*$/, "") !== compatibleId.href) return mockDocumentLoader(url);
1058
+ return Promise.resolve({
1059
+ contextUrl: null,
1060
+ documentUrl: url,
1061
+ document: structuredClone(unsignedActor)
1062
+ });
1063
+ };
1064
+ const json = await signedFollow();
1065
+ const contentDigest = `sha-256=:${encodeBase64(await crypto.subtle.digest("SHA-256", new TextEncoder().encode(JSON.stringify(json))))}:`;
1066
+ const cases = [
1067
+ {
1068
+ name: "unsigned",
1069
+ unsigned: true,
1070
+ fetches: false
1071
+ },
1072
+ {
1073
+ name: "invalid signature",
1074
+ sign: { privateKey: rsaPrivateKey2 },
1075
+ fetches: true
1076
+ },
1077
+ {
1078
+ name: "unknown key",
1079
+ loader: mockDocumentLoader,
1080
+ fetches: true
1081
+ },
1082
+ {
1083
+ name: "gateway not in gateways",
1084
+ sign: { keyId: new URL(`https://unlisted.example/.well-known/apgateway/${did}/users/alice#main-key`) },
1085
+ fetches: false
1086
+ },
1087
+ {
1088
+ name: "key of another actor",
1089
+ sign: { keyId: bobKeyId },
1090
+ fetches: false
1091
+ },
1092
+ {
1093
+ name: "content-digest not covered",
1094
+ sign: {
1095
+ spec: "rfc9421",
1096
+ components: ["@method", "@target-uri"]
1097
+ },
1098
+ fetches: false
1099
+ },
1100
+ {
1101
+ name: "target URI not covered",
1102
+ sign: {
1103
+ spec: "rfc9421",
1104
+ components: [
1105
+ "@method",
1106
+ "@authority",
1107
+ "@path",
1108
+ "content-digest"
1109
+ ],
1110
+ headers: { "Content-Digest": contentDigest }
1111
+ },
1112
+ fetches: false
1113
+ },
1114
+ {
1115
+ name: "non-canonical spelling of content-digest",
1116
+ sign: {
1117
+ spec: "rfc9421",
1118
+ components: [
1119
+ "@method",
1120
+ "@target-uri",
1121
+ "Content-Digest"
1122
+ ],
1123
+ headers: { "Content-Digest": "sha-256=:AAAA:" }
1124
+ },
1125
+ fetches: false
1126
+ },
1127
+ {
1128
+ name: "key not vouched for by the DID",
1129
+ sign: { privateKey: rsaPrivateKey2 },
1130
+ loader: unsignedLoader,
1131
+ fetches: true
1132
+ }
1133
+ ];
1134
+ for (const { name, sign, unsigned, loader, fetches } of cases) await t.step(name, async () => {
1135
+ const recorded = recordLoader(loader ?? gatewayLoader);
1136
+ const { response, queue } = await deliverToGateway(unsigned ? post(inboxUrl(), json) : await signDelivery(json, sign), { loader: recorded.loader });
1137
+ assertEquals(response.status, 202);
1138
+ assertEquals(forwardedGateways(queue), [GATEWAY3, GATEWAY2].sort());
1139
+ assertEquals(recorded.urls.some((u) => u.includes("/.well-known/apgateway/")), fetches);
1140
+ });
1141
+ await t.step("already forwarded to the gateway", async () => {
1142
+ const recorded = recordLoader(gatewayLoader);
1143
+ const kv = new MemoryKvStore();
1144
+ const json = await signedFollow();
1145
+ const { queue } = await deliverToGateway(post(inboxUrl(), json), {
1146
+ loader: recorded.loader,
1147
+ kv
1148
+ });
1149
+ assertEquals(forwardedGateways(queue), [GATEWAY3, GATEWAY2].sort());
1150
+ const { queue: queue2 } = await deliverToGateway(await signDelivery(json), {
1151
+ loader: recorded.loader,
1152
+ kv
1153
+ });
1154
+ assertEquals(queue2.outbox, []);
1155
+ assertEquals(recorded.urls.filter((u) => u.startsWith(GATEWAY2)), []);
1156
+ });
818
1157
  });
819
1158
  function forwardingParameters(overrides = {}) {
820
1159
  const activityId = parseIri(`ap+ef61://${did}/follows/${++activityCounter}`);
@@ -915,6 +1254,144 @@ test("forwardPortableInboxActivity() does not wait for slow keys", async () => {
915
1254
  esm_default.hardReset();
916
1255
  }
917
1256
  });
1257
+ function gatewaySignature(keyId, verify) {
1258
+ return {
1259
+ keyId,
1260
+ coversDelivery: true,
1261
+ verify
1262
+ };
1263
+ }
1264
+ function gatewayKeyOf(keyId, owner = aliceId) {
1265
+ return new CryptographicKey({
1266
+ id: keyId,
1267
+ owner,
1268
+ publicKey: rsaGatewayKeyPair.publicKey
1269
+ });
1270
+ }
1271
+ test("forwardPortableInboxActivity() skips the gateway that forwarded the activity", async (t) => {
1272
+ const recipient = {
1273
+ actorId: parseIri(`ap://${did}/users/alice`),
1274
+ inboxId: parseIri(`ap+ef61://${did}/users/alice/inbox`),
1275
+ canonicalInboxId: `ap://${did}/users/alice/inbox`,
1276
+ gateways: [
1277
+ new URL(LOCAL),
1278
+ new URL(GATEWAY3),
1279
+ new URL(GATEWAY2)
1280
+ ]
1281
+ };
1282
+ const owners = [["identified", aliceId], ["identified by a key of a compatible identifier", new URL(compatibleAliceId())]];
1283
+ for (const [name, owner] of owners) await t.step(name, async () => {
1284
+ const queue = new RecordingQueue();
1285
+ assertEquals((await forwardPortableInboxActivity(forwardingParameters({
1286
+ recipient,
1287
+ outboxQueue: queue,
1288
+ signatures: [gatewaySignature(gateway2KeyId, () => Promise.resolve(gatewayKeyOf(gateway2KeyId, owner)))]
1289
+ }))).map((u) => u.origin), [GATEWAY3]);
1290
+ });
1291
+ const notIdentified = [
1292
+ ["verify() rejects", [gatewaySignature(gateway2KeyId, () => Promise.reject(/* @__PURE__ */ new Error("Failed.")))]],
1293
+ ["verify() throws", [gatewaySignature(gateway2KeyId, () => {
1294
+ throw new Error("Failed.");
1295
+ })]],
1296
+ ["not verified", [gatewaySignature(gateway2KeyId, () => Promise.resolve(null))]],
1297
+ ["the key is not owned by a portable actor", [gatewaySignature(gateway2KeyId, () => Promise.resolve(gatewayKeyOf(gateway2KeyId, new URL(`${GATEWAY2}/users/alice`))))]],
1298
+ ["the key is owned by another actor", [gatewaySignature(gateway2KeyId, () => Promise.resolve(gatewayKeyOf(gateway2KeyId, parseIri(`ap://${did}/users/bob`))))]],
1299
+ ["another key is verified", [gatewaySignature(gateway2KeyId, () => Promise.resolve(gatewayKeyOf(new URL(`${compatibleAliceId(GATEWAY3)}#main-key`))))]],
1300
+ ["the signature does not cover the delivery", [{
1301
+ ...gatewaySignature(gateway2KeyId, () => Promise.resolve(gatewayKeyOf(gateway2KeyId))),
1302
+ coversDelivery: false
1303
+ }]]
1304
+ ];
1305
+ for (const [name, signatures] of notIdentified) await t.step(name, async () => {
1306
+ const queue = new RecordingQueue();
1307
+ assertEquals((await forwardPortableInboxActivity(forwardingParameters({
1308
+ recipient,
1309
+ outboxQueue: queue,
1310
+ signatures
1311
+ }))).map((u) => u.origin), [GATEWAY3, GATEWAY2]);
1312
+ });
1313
+ await t.step("verified only if it may save a request", async (t) => {
1314
+ const cases = [
1315
+ ["forwarding is turned off", { options: resolvePortableInboxForwardingOptions({ maxTargets: 0 }) }],
1316
+ ["no compare-and-swap", { kv: {
1317
+ ...new MemoryKvStore(),
1318
+ cas: void 0
1319
+ } }],
1320
+ ["this server", { excludedOrigins: [LOCAL, GATEWAY2] }]
1321
+ ];
1322
+ for (const [name, overrides] of cases) await t.step(name, async () => {
1323
+ let calls = 0;
1324
+ await forwardPortableInboxActivity(forwardingParameters({
1325
+ recipient,
1326
+ outboxQueue: new RecordingQueue(),
1327
+ signatures: [gatewaySignature(gateway2KeyId, () => {
1328
+ calls++;
1329
+ return Promise.resolve(gatewayKeyOf(gateway2KeyId));
1330
+ })],
1331
+ ...overrides
1332
+ }));
1333
+ assertEquals(calls, 0);
1334
+ });
1335
+ });
1336
+ await t.step("verification does not finish in time", async () => {
1337
+ let release = () => {};
1338
+ const pending = new Promise((resolve) => release = resolve);
1339
+ const kv = new MemoryKvStore();
1340
+ const parameters = forwardingParameters({
1341
+ recipient,
1342
+ kv,
1343
+ outboxQueue: new RecordingQueue(),
1344
+ options: resolvePortableInboxForwardingOptions({
1345
+ maxTargets: 1,
1346
+ deadline: { milliseconds: 10 }
1347
+ }),
1348
+ signatures: [gatewaySignature(gateway2KeyId, () => pending)]
1349
+ });
1350
+ const started = performance.now();
1351
+ const forwarded = await forwardPortableInboxActivity(parameters);
1352
+ assert(performance.now() - started < 5e3);
1353
+ assertEquals(forwarded.map((u) => u.origin), [GATEWAY3]);
1354
+ release(gatewayKeyOf(gateway2KeyId));
1355
+ await new Promise((resolve) => setTimeout(resolve, 20));
1356
+ const claimed = [];
1357
+ for await (const { key } of kv.list(parameters.kvPrefix)) claimed.push(String(key.at(-1)));
1358
+ assertEquals(claimed, [GATEWAY3]);
1359
+ });
1360
+ });
1361
+ test("coversDelivery()", () => {
1362
+ const signature = (label, ...names) => ({
1363
+ keyId: gateway2KeyId,
1364
+ label,
1365
+ components: names.map((value) => ({
1366
+ value,
1367
+ params: {}
1368
+ }))
1369
+ });
1370
+ assert(coversDelivery(signature("sig1", "@method", "@target-uri", "@authority", "content-digest")));
1371
+ assert(coversDelivery(signature(null, "(request-target)", "host", "date", "digest")));
1372
+ assertFalse(coversDelivery(signature("sig1", "@method", "@target-uri")));
1373
+ assertFalse(coversDelivery(signature("sig1", "@method", "@target-uri", "Content-Digest")));
1374
+ assertFalse(coversDelivery(signature("sig1", "@method", "@authority", "@path", "content-digest")));
1375
+ assertFalse(coversDelivery(signature(null, "(request-target)", "digest")));
1376
+ assertFalse(coversDelivery({
1377
+ keyId: gateway2KeyId,
1378
+ label: "sig1",
1379
+ components: [
1380
+ {
1381
+ value: "@method",
1382
+ params: {}
1383
+ },
1384
+ {
1385
+ value: "@target-uri",
1386
+ params: {}
1387
+ },
1388
+ {
1389
+ value: "content-digest",
1390
+ params: { req: true }
1391
+ }
1392
+ ]
1393
+ }));
1394
+ });
918
1395
  test("Context.getPortableInboxUri()", async (t) => {
919
1396
  const { federation } = setup();
920
1397
  await t.step("Context", () => {