@fedify/fedify 2.4.0-dev.2122 → 2.4.0-dev.2129

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 (98) hide show
  1. package/dist/{builder-BCXywsZF.mjs → builder-C0bxZDL0.mjs} +3 -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-Fxd45Oy5.mjs → compound-proof-D9Kljk_7.mjs} +3 -3
  6. package/dist/{context-tOiJfBoD.d.ts → context-Cz1w5Krc.d.ts} +31 -5
  7. package/dist/{context-CoNRxGYp.mjs → context-DfSVPcJ8.mjs} +6 -3
  8. package/dist/{context-SMNOiiY6.d.cts → context-Dvec_D_h.d.cts} +31 -5
  9. package/dist/{deno-BZrnW0Qz.mjs → deno-_o3lkDlG.mjs} +1 -1
  10. package/dist/{docloader-3BHaS20Z.mjs → docloader-BbS0rO-Y.mjs} +7 -7
  11. package/dist/federation/builder.test.mjs +1 -1
  12. package/dist/federation/compound-fanout.test.mjs +3 -3
  13. package/dist/federation/handler-compound-proof.test.mjs +4 -4
  14. package/dist/federation/handler.test.mjs +6 -6
  15. package/dist/federation/hashlink-media.test.mjs +2 -2
  16. package/dist/federation/idempotency.test.mjs +2 -2
  17. package/dist/federation/metrics.test.mjs +1 -1
  18. package/dist/federation/middleware.test.mjs +8 -8
  19. package/dist/federation/mod.cjs +1 -1
  20. package/dist/federation/mod.d.cts +2 -2
  21. package/dist/federation/mod.d.ts +2 -2
  22. package/dist/federation/mod.js +1 -1
  23. package/dist/federation/outgoing-proof.test.mjs +6 -6
  24. package/dist/federation/portable-actor.test.mjs +3 -3
  25. package/dist/federation/portable-chain.test.d.mts +2 -0
  26. package/dist/federation/portable-chain.test.mjs +358 -0
  27. package/dist/federation/portable-gateway-keys.test.mjs +77 -6
  28. package/dist/federation/portable-inbox.test.mjs +9 -4
  29. package/dist/federation/portable.test.mjs +3 -3
  30. package/dist/federation/send.test.mjs +3 -3
  31. package/dist/federation/tasks/enqueue.test.mjs +2 -2
  32. package/dist/federation/tasks/tasks.test.mjs +3 -3
  33. package/dist/federation/temporal.test.mjs +1 -1
  34. package/dist/federation/webfinger.test.mjs +2 -2
  35. package/dist/{federation-Ml6NrgRV.cjs → federation-DRY8KrkB.cjs} +46 -22
  36. package/dist/{http-4bmgw-mA.cjs → http-B1J6E-bT.cjs} +384 -33
  37. package/dist/{http-B9BQDNIy.mjs → http-BpOiZE09.mjs} +18 -12
  38. package/dist/{http-D76Py0M4.d.cts → http-C-x_OLSp.d.ts} +57 -14
  39. package/dist/{http-BETASGl0.js → http-C2M6wcs8.js} +374 -35
  40. package/dist/{http-BrQgkJay.d.ts → http-CjezPjFX.d.cts} +57 -14
  41. package/dist/{key-PyYvun0F.mjs → key-DolJjJab.mjs} +103 -16
  42. package/dist/{kv-cache-CPqUxRL3.mjs → kv-cache-Bq2cah0c.mjs} +1 -1
  43. package/dist/{kv-cache-C4mv2ZFQ.js → kv-cache-D7TiyMxv.js} +6 -6
  44. package/dist/{kv-cache-D2Bz6t_c.cjs → kv-cache-D9nRbG2Q.cjs} +6 -6
  45. package/dist/{ld-u4JOD4iO.mjs → ld-Dwc176V4.mjs} +3 -3
  46. package/dist/{metrics-DCLoLvLz.mjs → metrics-CDaHfBXY.mjs} +1 -1
  47. package/dist/{middleware-B7bmtONC.mjs → middleware-CmB1FD8A.mjs} +1 -1
  48. package/dist/{middleware-DWlAGlyP.js → middleware-CylX-pvI.js} +52 -28
  49. package/dist/{middleware-CgOqRkK-.mjs → middleware-DN3MdTDg.mjs} +64 -39
  50. package/dist/{mod-CTh7ijx5.d.ts → mod-Bz9tFXI2.d.ts} +14 -3
  51. package/dist/{mod-Cwdt_CWU.d.ts → mod-CZetqT5h.d.ts} +1 -1
  52. package/dist/{mod-Cr79cQlc.d.cts → mod-D4g9ffEn.d.cts} +1 -1
  53. package/dist/{mod-DnElMslb.d.cts → mod-DGOIxEfb.d.cts} +1 -1
  54. package/dist/{mod-DMdGFMEl.d.cts → mod-S--JxYjo.d.cts} +14 -3
  55. package/dist/{mod-COWgSo2v.d.ts → mod-owwElvpw.d.ts} +1 -1
  56. package/dist/mod.cjs +8 -8
  57. package/dist/mod.d.cts +5 -5
  58. package/dist/mod.d.ts +5 -5
  59. package/dist/mod.js +5 -5
  60. package/dist/nodeinfo/handler.test.mjs +2 -2
  61. package/dist/{owner-CPFM-sqo.mjs → owner-BNHhwjNu.mjs} +37 -4
  62. package/dist/{sig-C1xOA4ml.js → portable-collection-CRpTUHCY.mjs} +5 -4
  63. package/dist/portable-collection-CgglAI9B.js +656 -0
  64. package/dist/{sig-Cb9VSeiF.cjs → portable-collection-PTGD0q70.cjs} +253 -2
  65. package/dist/portable-key-DqX2emHQ.mjs +395 -0
  66. package/dist/{portable-key-id-T7lecIbt.mjs → portable-key-id-Bg259c4Z.mjs} +69 -2
  67. package/dist/{proof-BCOg7Njv.mjs → proof-B4_0E-KU.mjs} +5 -5
  68. package/dist/{send-C8eOPat3.mjs → send-Dpu5qbP5.mjs} +3 -3
  69. package/dist/sig/compound-proof-discovery.test.mjs +1 -1
  70. package/dist/sig/compound-proof-producer.test.mjs +1 -1
  71. package/dist/sig/compound-proof-verification.test.mjs +2 -2
  72. package/dist/sig/compound-proof.test.mjs +2 -2
  73. package/dist/sig/http.test.mjs +2 -2
  74. package/dist/sig/key.test.mjs +1 -1
  75. package/dist/sig/ld.test.mjs +2 -2
  76. package/dist/sig/mod.cjs +5 -6
  77. package/dist/sig/mod.d.cts +2 -2
  78. package/dist/sig/mod.d.ts +2 -2
  79. package/dist/sig/mod.js +2 -3
  80. package/dist/sig/owner.test.mjs +1 -1
  81. package/dist/sig/portable-collection.test.mjs +20 -431
  82. package/dist/sig/portable-key.test.mjs +479 -5
  83. package/dist/sig/proof.test.mjs +3 -3
  84. package/dist/{temporal-vjHOhBpy.mjs → temporal-78JVR3GK.mjs} +1 -1
  85. package/dist/testing/kv.test.mjs +1 -1
  86. package/dist/testing/mod.d.mts +30 -4
  87. package/dist/testing/mod.mjs +2 -2
  88. package/dist/{testing-BWq1dCzx.mjs → testing-otQhu4aD.mjs} +1 -1
  89. package/dist/utils/docloader.test.mjs +81 -2
  90. package/dist/utils/kv-cache.test.mjs +1 -1
  91. package/dist/utils/mod.cjs +1 -1
  92. package/dist/utils/mod.d.cts +1 -1
  93. package/dist/utils/mod.d.ts +1 -1
  94. package/dist/utils/mod.js +1 -1
  95. package/package.json +7 -7
  96. package/dist/owner-DkA0Lz7v.cjs +0 -225
  97. package/dist/owner-wNinn7RK.js +0 -215
  98. package/dist/portable-key-DyylakVV.mjs +0 -214
@@ -2,12 +2,12 @@ import { Temporal } from "temporal-polyfill";
2
2
  import "urlpattern-polyfill";
3
3
  globalThis.addEventListener = () => {};
4
4
  import { t as MemoryKvStore } from "../kv-DCHkaJ9-.mjs";
5
- import { i as fetchKey } from "../key-PyYvun0F.mjs";
6
- import { h as verifyJsonLd, p as signJsonLd } from "../ld-u4JOD4iO.mjs";
7
- import { n as createProof, u as verifyProof } from "../proof-BCOg7Njv.mjs";
8
- import { d as verifyRequest, f as verifyRequestDetailed, l as signRequest } from "../http-B9BQDNIy.mjs";
5
+ import { i as fetchKey } from "../key-DolJjJab.mjs";
6
+ import { h as verifyJsonLd, p as signJsonLd } from "../ld-Dwc176V4.mjs";
7
+ import { n as createProof, u as verifyProof } from "../proof-B4_0E-KU.mjs";
8
+ import { d as verifyRequest, f as verifyRequestDetailed, l as signRequest } from "../http-BpOiZE09.mjs";
9
9
  import { a as rsaPrivateKey3, c as rsaPublicKey3, i as rsaPrivateKey2, n as ed25519PrivateKey, r as ed25519PublicKey, s as rsaPublicKey2 } from "../keys-DIJNHW-Y.mjs";
10
- import { n as getKeyOwner, t as doesActorOwnKey } from "../owner-CPFM-sqo.mjs";
10
+ import { n as getKeyOwner, t as doesActorOwnKey } from "../owner-BNHhwjNu.mjs";
11
11
  import { t as KvKeyCache } from "../keycache-BInaECGj.mjs";
12
12
  import { Create, CryptographicKey, Multikey, Note, Person } from "@fedify/vocab";
13
13
  import { test } from "@fedify/fixture";
@@ -715,5 +715,479 @@ test("verifyRequest() binds gateway keys to the actor whose document is signed",
715
715
  contextLoader: shiftingContextLoader
716
716
  }), null);
717
717
  });
718
+ test("verifyRequest() matches gateway keys listed under ap: URIs", async () => {
719
+ const portableKeyId = parseIri(`${actorId}#main-key`);
720
+ const cases = {
721
+ "both properties": await actorJson({ key: portableKeyId }),
722
+ "assertionMethod only": await actorJson({
723
+ key: portableKeyId,
724
+ publicKey: null
725
+ }),
726
+ "publicKey only": await actorJson({
727
+ key: portableKeyId,
728
+ assertionMethod: false
729
+ })
730
+ };
731
+ for (const [name, document] of Object.entries(cases)) {
732
+ const documentLoader = createLoader({ [compatibleId(gw1)]: await sign(document) });
733
+ for (const spec of specs) {
734
+ const key = await verifyRequest(await signedRequest(spec), {
735
+ documentLoader,
736
+ contextLoader,
737
+ spec
738
+ });
739
+ strictEqual(key?.id?.href, keyId.href, `${name}, ${spec}`);
740
+ strictEqual(key?.ownerId?.href, parseIri(actorId).href, name);
741
+ }
742
+ }
743
+ });
744
+ test("verifyRequest() rejects ambiguous or foreign aliases of gateway keys", async () => {
745
+ const portableKeyId = `${actorId}#main-key`;
746
+ const base = await actorJson();
747
+ const assertionMethod = base.assertionMethod;
748
+ const publicKey = base.publicKey;
749
+ const cases = {
750
+ "an exact entry and an ap: alias in assertionMethod": {
751
+ ...base,
752
+ assertionMethod: [assertionMethod, {
753
+ ...assertionMethod,
754
+ id: portableKeyId
755
+ }]
756
+ },
757
+ "a reference and an embedded ap: alias in assertionMethod": {
758
+ ...base,
759
+ assertionMethod: [keyId.href, {
760
+ ...assertionMethod,
761
+ id: portableKeyId
762
+ }]
763
+ },
764
+ "an exact entry and an ap: alias in publicKey": {
765
+ ...base,
766
+ publicKey: [publicKey, {
767
+ ...publicKey,
768
+ id: portableKeyId
769
+ }]
770
+ },
771
+ "a key of another gateway": await actorJson({ key: new URL(`${compatibleId(gw2)}#main-key`) }),
772
+ "an ap: key ID with dot segments": {
773
+ ...base,
774
+ assertionMethod: {
775
+ ...assertionMethod,
776
+ id: portableKeyId.replace("/actor#", "/x/../actor#")
777
+ },
778
+ publicKey: {
779
+ ...publicKey,
780
+ id: portableKeyId.replace("/actor#", "/x/../actor#")
781
+ }
782
+ },
783
+ "a compatible key ID with dot segments": {
784
+ ...base,
785
+ assertionMethod: {
786
+ ...assertionMethod,
787
+ id: keyId.href.replace("/actor#", "/x/../actor#")
788
+ },
789
+ publicKey: {
790
+ ...publicKey,
791
+ id: keyId.href.replace("/actor#", "/x/../actor#")
792
+ }
793
+ }
794
+ };
795
+ for (const [name, document] of Object.entries(cases)) {
796
+ const documentLoader = createLoader({ [compatibleId(gw1)]: await sign(document) });
797
+ strictEqual(await verifyRequest(await signedRequest(), {
798
+ documentLoader,
799
+ contextLoader
800
+ }), null, name);
801
+ }
802
+ });
803
+ test("verifyRequest() refuses key IDs whose identity URL parsing changes", async () => {
804
+ const documentLoader = createLoader({ [compatibleId(gw1)]: await sign(await actorJson()) });
805
+ const request = await signedRequest("draft-cavage-http-signatures-12");
806
+ const dotted = keyId.href.replace("/actor#", "/x/../actor#");
807
+ const headers = new Headers(request.headers);
808
+ headers.set("Signature", headers.get("Signature").replace(keyId.href, dotted));
809
+ strictEqual(await verifyRequest(new Request(request, { headers }), {
810
+ documentLoader,
811
+ contextLoader
812
+ }), null);
813
+ strictEqual(documentLoader.fetched.length, 0);
814
+ const dottedPortable = `${actorId.replace("/actor", "/x/../actor")}#main-key`;
815
+ for (const id of [
816
+ dotted,
817
+ dottedPortable,
818
+ ` ${dotted}`,
819
+ ` ${dottedPortable}`,
820
+ ` ${parseIri(dottedPortable).href}`,
821
+ dottedPortable.replace("ap+ef61:", "ap+ ef61:"),
822
+ dotted.replace("did:key:", "did:ke y:"),
823
+ `${gw1}/.well-known/apgateway/${did}/../../../users/alice#main-key`,
824
+ ` ap://not-a-did/actor#main-key`
825
+ ]) {
826
+ let error;
827
+ try {
828
+ await fetchKey(id, CryptographicKey, {
829
+ documentLoader,
830
+ contextLoader
831
+ });
832
+ } catch (e) {
833
+ error = e;
834
+ }
835
+ ok(error instanceof TypeError, id);
836
+ }
837
+ });
838
+ const apKeyId = parseIri(`${actorId}#main-key`);
839
+ function hinted(...gateways) {
840
+ const query = gateways.map((g) => `@gateway=${encodeURIComponent(g)}`).join("&");
841
+ return parseIri(`${actorId}${query === "" ? "" : `?${query}`}#main-key`);
842
+ }
843
+ async function apActorJson(options = {}) {
844
+ return await actorJson({
845
+ key: apKeyId,
846
+ ...options
847
+ });
848
+ }
849
+ test("verifyRequest() accepts keys of portable actors at ap: key IDs", async () => {
850
+ const documentLoader = createLoader({ [compatibleId(gw1)]: await sign(await apActorJson()) });
851
+ const hintedKeyId = hinted(gw1);
852
+ for (const spec of specs) {
853
+ const key = await verifyRequest(await signedRequest(spec, hintedKeyId), {
854
+ documentLoader,
855
+ contextLoader,
856
+ spec
857
+ });
858
+ ok(key != null, spec);
859
+ strictEqual(key.id?.href, hintedKeyId.href, spec);
860
+ strictEqual(key.ownerId?.href, parseIri(actorId).href, spec);
861
+ }
862
+ const raw = `ap://${did}/actor?@gateway=${encodeURIComponent(gw1)}#main-key`;
863
+ for (const written of [raw, raw.replace(/^ap:/, "ap+ef61:")]) {
864
+ const request = await signedRequest("draft-cavage-http-signatures-12", hintedKeyId);
865
+ const headers = new Headers(request.headers);
866
+ headers.set("Signature", headers.get("Signature").replace(hintedKeyId.href, written));
867
+ strictEqual((await verifyRequest(new Request(request, { headers }), {
868
+ documentLoader,
869
+ contextLoader
870
+ }))?.ownerId?.href, parseIri(actorId).href, written);
871
+ }
872
+ deepStrictEqual([...new Set(documentLoader.fetched)], [compatibleId(gw1)]);
873
+ });
874
+ test("verifyRequest() asks the document loader for ap: key IDs without hints", async () => {
875
+ const documentLoader = createLoader({ [actorId]: await sign(await apActorJson()) });
876
+ strictEqual((await verifyRequest(await signedRequest(void 0, apKeyId), {
877
+ documentLoader,
878
+ contextLoader
879
+ }))?.ownerId?.href, parseIri(actorId).href);
880
+ deepStrictEqual(documentLoader.fetched, [actorId]);
881
+ const result = await verifyRequestDetailed(await signedRequest(void 0, apKeyId), {
882
+ documentLoader: createLoader({}),
883
+ contextLoader
884
+ });
885
+ ok(!result.verified);
886
+ strictEqual(result.reason.type, "keyFetchError");
887
+ });
888
+ test("verifyRequest() rejects keys at ap: key IDs that portable actors do not vouch for", async () => {
889
+ const unsigned = await apActorJson();
890
+ const ordinaryActorId = "https://gw1.example/users/alice";
891
+ const cases = {
892
+ "an unsigned actor document": unsigned,
893
+ "an actor document signed by another DID": await sign(unsigned, otherKeyPair.privateKey, `${otherDid}#${otherDid.slice(8)}`),
894
+ "a key missing from both assertionMethod and publicKey": await sign(await apActorJson({
895
+ assertionMethod: false,
896
+ publicKey: null
897
+ })),
898
+ "a key referred to by URL": await sign({
899
+ ...await apActorJson({
900
+ publicKey: null,
901
+ assertionMethod: false
902
+ }),
903
+ assertionMethod: [apKeyId.href]
904
+ }),
905
+ "a key only in publicKey without an owner": await sign(withoutPublicKeyOwner(await apActorJson({ assertionMethod: false }))),
906
+ "a publicKey entry with other key material": await sign(await apActorJson({ publicKey: rsaPublicKey3.publicKey })),
907
+ "a key only at a compatible identifier": await sign(await actorJson()),
908
+ "an actor without gateways": await sign(await apActorJson({ gateways: [] })),
909
+ "the document of another actor": await sign(await apActorJson({ id: `${actorId}/other` })),
910
+ "an ordinary actor": await actorJson({
911
+ id: ordinaryActorId,
912
+ key: apKeyId
913
+ }),
914
+ "a standalone key": {
915
+ "@context": "https://w3id.org/security/v1",
916
+ id: apKeyId.href,
917
+ type: "Key",
918
+ owner: actorId,
919
+ publicKeyPem: await exportSpki(gatewayPublicKey)
920
+ }
921
+ };
922
+ for (const [name, document] of Object.entries(cases)) {
923
+ const documentLoader = createLoader({ [compatibleId(gw1)]: document });
924
+ strictEqual(await verifyRequest(await signedRequest(void 0, hinted(gw1)), {
925
+ documentLoader,
926
+ contextLoader
927
+ }), null, name);
928
+ }
929
+ const documentLoader = createLoader({ [compatibleId(gw1)]: await sign(await apActorJson()) });
930
+ strictEqual(await verifyRequest(await signedRequest(void 0, parseIri(`${actorId}?@gateway=${encodeURIComponent(gw1)}`)), {
931
+ documentLoader,
932
+ contextLoader
933
+ }), null);
934
+ strictEqual(documentLoader.fetched.length, 0);
935
+ });
936
+ test("verifyRequest() tries the gateways of ap: key IDs in order", async () => {
937
+ const signed = await sign(await apActorJson());
938
+ const cases = {
939
+ "an invalid document at the first gateway": {
940
+ [compatibleId(gw1)]: await apActorJson(),
941
+ [compatibleId(gw2)]: signed
942
+ },
943
+ "an unreachable first gateway": { [compatibleId(gw2)]: signed }
944
+ };
945
+ for (const [name, responses] of Object.entries(cases)) {
946
+ const documentLoader = createLoader(responses);
947
+ strictEqual((await verifyRequest(await signedRequest(void 0, hinted(gw1, gw2)), {
948
+ documentLoader,
949
+ contextLoader
950
+ }))?.ownerId?.href, parseIri(actorId).href, name);
951
+ deepStrictEqual(documentLoader.fetched, [compatibleId(gw1), compatibleId(gw2)], name);
952
+ }
953
+ const documentLoader = createLoader({
954
+ [compatibleId(gw1)]: signed,
955
+ [compatibleId(gw2)]: signed
956
+ });
957
+ ok(await verifyRequest(await signedRequest(void 0, hinted(gw1, gw2)), {
958
+ documentLoader,
959
+ contextLoader
960
+ }) != null);
961
+ deepStrictEqual(documentLoader.fetched, [compatibleId(gw1)]);
962
+ const gateways = Array.from({ length: 7 }, (_, i) => `https://gw${i + 10}.example`);
963
+ const many = createLoader({});
964
+ ok(!(await verifyRequestDetailed(await signedRequest(void 0, hinted(...gateways)), {
965
+ documentLoader: many,
966
+ contextLoader
967
+ })).verified);
968
+ deepStrictEqual(many.fetched, gateways.slice(0, 5).map((g) => compatibleId(g)));
969
+ });
970
+ function failingLoader(responses) {
971
+ const fetched = [];
972
+ const loader = (url) => {
973
+ fetched.push(url);
974
+ const response = responses[url.replace(/#.*$/, "")] ?? 404;
975
+ if (typeof response === "number") return Promise.reject(new FetchError(url, `HTTP ${response}`, new Response(null, { status: response })));
976
+ return Promise.resolve({
977
+ contextUrl: null,
978
+ documentUrl: url,
979
+ document: structuredClone(response)
980
+ });
981
+ };
982
+ return Object.assign(loader, { fetched });
983
+ }
984
+ test("verifyRequest() reports why keys at ap: key IDs could not be looked up", async () => {
985
+ const hintedKeyId = hinted(gw1, gw2);
986
+ const invalid = await apActorJson();
987
+ const cases = [
988
+ {
989
+ name: "every gateway serves an invalid document",
990
+ responses: {
991
+ [compatibleId(gw1)]: invalid,
992
+ [compatibleId(gw2)]: invalid
993
+ },
994
+ reason: "invalidSignature",
995
+ cached: true
996
+ },
997
+ {
998
+ name: "every gateway responds with 410 Gone",
999
+ responses: {
1000
+ [compatibleId(gw1)]: 410,
1001
+ [compatibleId(gw2)]: 410
1002
+ },
1003
+ reason: "keyFetchError",
1004
+ status: 410,
1005
+ cached: true
1006
+ },
1007
+ {
1008
+ name: "the gateways respond with different statuses",
1009
+ responses: {
1010
+ [compatibleId(gw1)]: 404,
1011
+ [compatibleId(gw2)]: 503
1012
+ },
1013
+ reason: "keyFetchError",
1014
+ cached: true
1015
+ },
1016
+ {
1017
+ name: "one gateway serves an invalid document, the other is gone",
1018
+ responses: {
1019
+ [compatibleId(gw1)]: invalid,
1020
+ [compatibleId(gw2)]: 410
1021
+ },
1022
+ reason: "keyFetchError",
1023
+ cached: false
1024
+ }
1025
+ ];
1026
+ for (const { name, responses, reason, status, cached } of cases) for (const spec of specs) {
1027
+ const documentLoader = failingLoader(responses);
1028
+ const keyCache = new KvKeyCache(new MemoryKvStore(), ["pk"]);
1029
+ const options = {
1030
+ documentLoader,
1031
+ contextLoader,
1032
+ keyCache,
1033
+ spec
1034
+ };
1035
+ for (let i = 0; i < 2; i++) {
1036
+ const result = await verifyRequestDetailed(await signedRequest(spec, hintedKeyId), options);
1037
+ ok(!result.verified, `${name}, ${spec}`);
1038
+ strictEqual(result.reason.type, reason, `${name}, ${spec}`);
1039
+ if (result.reason.type === "keyFetchError") strictEqual("status" in result.reason.result ? result.reason.result.status : void 0, status, `${name}, ${spec}`);
1040
+ }
1041
+ strictEqual(documentLoader.fetched.length, cached ? 2 : 4, `${name}, ${spec}`);
1042
+ }
1043
+ });
1044
+ test("verifyRequest() accepts keys at ap: key IDs only for HTTP Signatures", async () => {
1045
+ const documentLoader = createLoader({ [compatibleId(gw1)]: await sign(await apActorJson()) });
1046
+ const hintedKeyId = hinted(gw1);
1047
+ const keyCache = new KvKeyCache(new MemoryKvStore(), ["pk"]);
1048
+ const options = {
1049
+ documentLoader,
1050
+ contextLoader,
1051
+ keyCache
1052
+ };
1053
+ for (const cls of [CryptographicKey, Multikey]) strictEqual((cls === CryptographicKey ? await fetchKey(hintedKeyId, CryptographicKey, options) : await fetchKey(hintedKeyId, Multikey, options)).key, null);
1054
+ strictEqual(documentLoader.fetched.length, 0);
1055
+ for (let i = 0; i < 3; i++) {
1056
+ const key = await verifyRequest(await signedRequest(void 0, hintedKeyId), options);
1057
+ strictEqual(key?.id?.href, hintedKeyId.href);
1058
+ strictEqual(key?.ownerId?.href, parseIri(actorId).href);
1059
+ }
1060
+ strictEqual(documentLoader.fetched.length, 1);
1061
+ for (const cls of [CryptographicKey, Multikey]) strictEqual((cls === CryptographicKey ? await fetchKey(hintedKeyId, CryptographicKey, options) : await fetchKey(hintedKeyId, Multikey, options)).key, null);
1062
+ const document = {
1063
+ "@context": "https://www.w3.org/ns/activitystreams",
1064
+ type: "Create",
1065
+ id: "https://gw1.example/activities/1",
1066
+ actor: compatibleId(gw1)
1067
+ };
1068
+ const { signature } = await signJsonLd(document, rsaPrivateKey2, hintedKeyId, { contextLoader });
1069
+ ok(!await verifyJsonLd({
1070
+ ...document,
1071
+ signature
1072
+ }, options));
1073
+ strictEqual(documentLoader.fetched.length, 1);
1074
+ const legacyCache = {
1075
+ get: () => Promise.resolve(new CryptographicKey({
1076
+ id: hintedKeyId,
1077
+ owner: parseIri(actorId),
1078
+ publicKey: gatewayPublicKey
1079
+ })),
1080
+ set: () => Promise.resolve()
1081
+ };
1082
+ const empty = createLoader({});
1083
+ strictEqual(await verifyRequest(await signedRequest(void 0, hintedKeyId), {
1084
+ documentLoader: empty,
1085
+ contextLoader,
1086
+ keyCache: legacyCache
1087
+ }), null);
1088
+ strictEqual(empty.fetched.length, 1);
1089
+ });
1090
+ test("verifyRequest() refreshes a cached key at an ap: key ID that fails to verify", async () => {
1091
+ for (const spec of specs) {
1092
+ const responses = { [compatibleId(gw1)]: await sign(await apActorJson()) };
1093
+ const documentLoader = failingLoader(responses);
1094
+ const hintedKeyId = hinted(gw1, gw2);
1095
+ const keyCache = new KvKeyCache(new MemoryKvStore(), ["pk"]);
1096
+ const options = {
1097
+ documentLoader,
1098
+ contextLoader,
1099
+ keyCache,
1100
+ spec
1101
+ };
1102
+ ok(await verifyRequest(await signedRequest(spec, hintedKeyId), options) != null);
1103
+ strictEqual(documentLoader.fetched.length, 1, spec);
1104
+ responses[compatibleId(gw1)] = await apActorJson();
1105
+ responses[compatibleId(gw2)] = 503;
1106
+ ok(!(await verifyRequestDetailed(await signRequest(new Request("https://recipient.example/inbox", {
1107
+ method: "POST",
1108
+ body: "{}",
1109
+ headers: { "Content-Type": "application/activity+json" }
1110
+ }), rsaPrivateKey3, hintedKeyId, { spec }), options)).verified, spec);
1111
+ strictEqual(documentLoader.fetched.length, 3, spec);
1112
+ const next = await verifyRequestDetailed(await signedRequest(spec, hintedKeyId), options);
1113
+ ok(!next.verified, spec);
1114
+ strictEqual(next.reason.type, "keyFetchError", spec);
1115
+ strictEqual(documentLoader.fetched.length, 5, spec);
1116
+ }
1117
+ });
1118
+ test("doesActorOwnKey() and getKeyOwner() resolve keys at ap: key IDs to portable actors", async () => {
1119
+ const options = {
1120
+ documentLoader: createLoader({ [compatibleId(gw1)]: await sign(await apActorJson()) }),
1121
+ contextLoader
1122
+ };
1123
+ const hintedKeyId = hinted(gw1);
1124
+ const key = new CryptographicKey({
1125
+ id: hintedKeyId,
1126
+ owner: parseIri(actorId),
1127
+ publicKey: gatewayPublicKey
1128
+ });
1129
+ const activity = new Create({
1130
+ id: parseIri(`${actorId.replace("/actor", "")}/activities/1`),
1131
+ actor: parseIri(`ap://${did}/actor`)
1132
+ });
1133
+ ok(await doesActorOwnKey(activity, key, options));
1134
+ strictEqual((await getKeyOwner(hintedKeyId, options))?.id?.href, parseIri(actorId).href);
1135
+ strictEqual((await getKeyOwner(key, options))?.id?.href, parseIri(actorId).href);
1136
+ ok(!await doesActorOwnKey(new Create({
1137
+ id: parseIri(`ap+ef61://${otherDid}/activities/1`),
1138
+ actor: parseIri(`ap+ef61://${otherDid}/actor`)
1139
+ }), key, options));
1140
+ const other = key.clone({ publicKey: rsaPublicKey3.publicKey });
1141
+ ok(!await doesActorOwnKey(activity, other, options));
1142
+ strictEqual(await getKeyOwner(other, options), null);
1143
+ const unsigned = {
1144
+ documentLoader: createLoader({ [compatibleId(gw1)]: await apActorJson() }),
1145
+ contextLoader
1146
+ };
1147
+ ok(!await doesActorOwnKey(activity, key, unsigned));
1148
+ strictEqual(await getKeyOwner(hintedKeyId, unsigned), null);
1149
+ });
1150
+ test("verifyRequest() does not trust a cached key at an ap: key ID for longer than its proof", async () => {
1151
+ let now = Temporal.Now.instant();
1152
+ const keyCache = new KvKeyCache(new MemoryKvStore(), ["pk"], { now: () => now });
1153
+ const expires = now.add({ minutes: 30 });
1154
+ const documentLoader = createLoader({ [compatibleId(gw1)]: await sign(await apActorJson(), ed25519PrivateKey, didKeyId, { expires: expires.toString() }) });
1155
+ const options = {
1156
+ documentLoader,
1157
+ contextLoader,
1158
+ keyCache
1159
+ };
1160
+ const hintedKeyId = hinted(gw1);
1161
+ ok(await verifyRequest(await signedRequest(void 0, hintedKeyId), options) != null);
1162
+ now = expires.subtract({ seconds: 1 });
1163
+ ok(await verifyRequest(await signedRequest(void 0, hintedKeyId), options) != null);
1164
+ strictEqual(documentLoader.fetched.length, 1);
1165
+ now = expires;
1166
+ ok(await verifyRequest(await signedRequest(void 0, hintedKeyId), options) != null);
1167
+ strictEqual(documentLoader.fetched.length, 2);
1168
+ });
1169
+ test("verifyRequest() does not cache keys at ap: key IDs it could not process", async () => {
1170
+ let failures = 1;
1171
+ const flakyContextLoader = (url, options) => {
1172
+ if (failures > 0) {
1173
+ failures--;
1174
+ return Promise.reject(/* @__PURE__ */ new Error(`Temporarily failed to load ${url}`));
1175
+ }
1176
+ return contextLoader(url, options);
1177
+ };
1178
+ const documentLoader = createLoader({ [compatibleId(gw1)]: await sign(await apActorJson()) });
1179
+ const options = {
1180
+ documentLoader,
1181
+ contextLoader: flakyContextLoader,
1182
+ keyCache: new KvKeyCache(new MemoryKvStore(), ["pk"])
1183
+ };
1184
+ const hintedKeyId = hinted(gw1);
1185
+ const result = await verifyRequestDetailed(await signedRequest(void 0, hintedKeyId), options);
1186
+ ok(!result.verified);
1187
+ strictEqual(result.reason.type, "keyFetchError");
1188
+ if (result.reason.type === "keyFetchError") ok(!("status" in result.reason.result));
1189
+ strictEqual((await verifyRequest(await signedRequest(void 0, hintedKeyId), options))?.ownerId?.href, parseIri(actorId).href);
1190
+ strictEqual(documentLoader.fetched.length, 2);
1191
+ });
718
1192
  //#endregion
719
1193
  export {};
@@ -10,10 +10,10 @@ import "../std__assert-DvMDRPes.mjs";
10
10
  import { t as assertRejects } from "../assert_rejects-CTptdYck.mjs";
11
11
  import { t as assert } from "../assert-D1Eafmd_.mjs";
12
12
  import { r as normalizeOutgoingActivityJsonLd } from "../outgoing-jsonld-4IJqPYos.mjs";
13
- import { i as signObject, n as createProof, o as verifyObject, r as hasProofLike, s as verifyPortableObjectProof, u as verifyProof } from "../proof-BCOg7Njv.mjs";
14
- import { d as verifyRequest } from "../http-B9BQDNIy.mjs";
13
+ import { i as signObject, n as createProof, o as verifyObject, r as hasProofLike, s as verifyPortableObjectProof, u as verifyProof } from "../proof-B4_0E-KU.mjs";
14
+ import { d as verifyRequest } from "../http-BpOiZE09.mjs";
15
15
  import { i as rsaPrivateKey2, n as ed25519PrivateKey, r as ed25519PublicKey, s as rsaPublicKey2, t as ed25519Multikey } from "../keys-DIJNHW-Y.mjs";
16
- import { t as getAuthenticatedDocumentLoader } from "../docloader-3BHaS20Z.mjs";
16
+ import { t as getAuthenticatedDocumentLoader } from "../docloader-BbS0rO-Y.mjs";
17
17
  import { Create, DataIntegrityProof, Document, Multikey, Note, PUBLIC_COLLECTION, Place, Translation } from "@fedify/vocab";
18
18
  import { createTestMeterProvider, mockDocumentLoader, test } from "@fedify/fixture";
19
19
  import { decodeMultibase, encodeMultibase, exportDidKey, exportMultibaseKey, formatIri, importMultibaseKey, parseIri } from "@fedify/vocab-runtime";
@@ -1,7 +1,7 @@
1
1
  import { Temporal } from "temporal-polyfill";
2
2
  import "urlpattern-polyfill";
3
3
  globalThis.addEventListener = () => {};
4
- import { c as getNormalizationContextLoader } from "./ld-u4JOD4iO.mjs";
4
+ import { c as getNormalizationContextLoader } from "./ld-Dwc176V4.mjs";
5
5
  import jsonld from "@fedify/vocab-runtime/jsonld";
6
6
  //#region src/federation/temporal.ts
7
7
  function isPlainObject(value) {
@@ -6,7 +6,7 @@ import { t as MemoryKvStore } from "../kv-DCHkaJ9-.mjs";
6
6
  import { t as ManualClockKvStore } from "../kv-DBRWVx3Q.mjs";
7
7
  import { n as baseOptions, o as stringSchema, t as MockQueue } from "../tasks-dbCShlCb.mjs";
8
8
  import { t as assertEquals } from "../assert_equals-Z6IcEbWU.mjs";
9
- import { o as createFederation } from "../middleware-CgOqRkK-.mjs";
9
+ import { o as createFederation } from "../middleware-DN3MdTDg.mjs";
10
10
  //#region src/testing/kv.test.ts
11
11
  test("ManualClockKvStore keeps an entry at its exact expiration instant", async () => {
12
12
  const kv = new ManualClockKvStore();
@@ -5,7 +5,7 @@ import { Router } from "@fedify/uri-template";
5
5
  import { Activity, Actor, Collection, CryptographicKey, Hashtag, Link, LookupObjectOptions, Multikey, Note, Object as Object$1, Recipient, Tombstone, TraverseCollectionOptions } from "@fedify/vocab";
6
6
  import { Link as Link$1, LookupWebFingerOptions, ResourceDescriptor } from "@fedify/webfinger";
7
7
  import { MeterProvider, TracerProvider } from "@opentelemetry/api";
8
- import { AuthenticatedDocumentLoaderFactory, DocumentLoader, DocumentLoaderFactory, GetUserAgentOptions } from "@fedify/vocab-runtime";
8
+ import { AuthenticatedDocumentLoaderFactory, DocumentLoader, DocumentLoaderFactory, GetUserAgentOptions, PortableObjectVerifier } from "@fedify/vocab-runtime";
9
9
  import { MeterProvider as MeterProvider$1, MetricReader } from "@opentelemetry/sdk-metrics";
10
10
  import { StandardSchemaV1 } from "@standard-schema/spec";
11
11
 
@@ -2881,6 +2881,27 @@ interface Context<TContextData> {
2881
2881
  * The context loader for loading remote JSON-LD contexts.
2882
2882
  */
2883
2883
  readonly contextLoader: DocumentLoader;
2884
+ /**
2885
+ * The [FEP-ef61] portable object policy that this context applies to
2886
+ * portable objects it dereferences. It is `verifyPortableObject()` with
2887
+ * this context's document loader, context loader, and tracer provider as
2888
+ * defaults, which the options passed to it override.
2889
+ *
2890
+ * Since its name is the same as the `verifyPortableObject` option of
2891
+ * property accessors, `lookupObject()`, and `traverseCollection()`, passing
2892
+ * a context as their options applies the policy, e.g.,
2893
+ * `await create.getObject(ctx)`. Objects parsed with a context as options,
2894
+ * such as activities that inboxes receive, also use it by default for
2895
+ * their property accessors. It never verifies an object by itself being
2896
+ * there: it only applies to objects that are dereferenced.
2897
+ *
2898
+ * It is optional only so that custom implementations of this interface
2899
+ * keep working; contexts that Fedify creates always have it.
2900
+ *
2901
+ * [FEP-ef61]: https://w3id.org/fep/ef61
2902
+ * @since 2.4.0
2903
+ */
2904
+ readonly verifyPortableObject?: PortableObjectVerifier;
2884
2905
  /**
2885
2906
  * The federation object that this context belongs to.
2886
2907
  * @since 1.6.0
@@ -3158,10 +3179,13 @@ interface Context<TContextData> {
3158
3179
  *
3159
3180
  * It's almost the same as the {@link lookupObject} function, but it uses
3160
3181
  * the context's document loader and context loader by default. It also
3161
- * uses `verifyPortableObjectProof()` as the `verifyPortableObject`
3182
+ * uses {@link Context.verifyPortableObject} as the `verifyPortableObject`
3162
3183
  * option by default (since 2.4.0), so [FEP-ef61] portable objects,
3163
3184
  * including portable actors found through WebFinger, are looked up through
3164
- * their gateways and returned only if they have valid proofs.
3185
+ * their gateways and returned only if they have valid proofs, or, for
3186
+ * collections without proofs, if they are served by gateways that their
3187
+ * owners list. The returned object uses the same policy by default for
3188
+ * its property accessors.
3165
3189
  *
3166
3190
  * [FEP-ef61]: https://w3id.org/fep/ef61
3167
3191
  *
@@ -3187,7 +3211,9 @@ interface Context<TContextData> {
3187
3211
  * ```
3188
3212
  *
3189
3213
  * It's almost the same as the {@link traverseCollection} function, but it
3190
- * uses the context's document loader and context loader by default.
3214
+ * uses the context's document loader and context loader by default. It
3215
+ * also uses {@link Context.verifyPortableObject} as the
3216
+ * `verifyPortableObject` option by default (since 2.4.0).
3191
3217
  * @param collection The collection to traverse.
3192
3218
  * @param options Options for traversing the collection.
3193
3219
  * @returns An async iterable of each item in the collection.
@@ -2,8 +2,8 @@ import "temporal-polyfill";
2
2
  import "urlpattern-polyfill";
3
3
  globalThis.addEventListener = () => {};
4
4
  import { r as testDefinitions } from "../dist-BQvArojP.mjs";
5
- import { n as createOutboxContext, r as createRequestContext, t as createInboxContext } from "../context-CoNRxGYp.mjs";
5
+ import { n as createOutboxContext, r as createRequestContext, t as createInboxContext } from "../context-DfSVPcJ8.mjs";
6
6
  import { t as ManualClockKvStore } from "../kv-DBRWVx3Q.mjs";
7
7
  import { a as numberSchema, i as makeSchema, n as baseOptions, o as stringSchema, r as envelopeSchema, t as MockQueue } from "../tasks-dbCShlCb.mjs";
8
- import "../testing-BWq1dCzx.mjs";
8
+ import "../testing-otQhu4aD.mjs";
9
9
  export { ManualClockKvStore, MockQueue, baseOptions, createInboxContext, createOutboxContext, createRequestContext, envelopeSchema, makeSchema, numberSchema, stringSchema, testDefinitions };
@@ -2,7 +2,7 @@ import "temporal-polyfill";
2
2
  import "urlpattern-polyfill";
3
3
  globalThis.addEventListener = () => {};
4
4
  import "./dist-BQvArojP.mjs";
5
- import "./context-CoNRxGYp.mjs";
5
+ import "./context-DfSVPcJ8.mjs";
6
6
  import "./kv-DBRWVx3Q.mjs";
7
7
  import "./tasks-dbCShlCb.mjs";
8
8
  export {};