@fedify/fedify 2.4.0-dev.2127 → 2.4.0-dev.2141

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 (92) hide show
  1. package/dist/{builder-B2bgGYbP.mjs → builder-BI7H61zO.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-SVlHSzV7.mjs → compound-proof-D0H4LLvQ.mjs} +15 -20
  6. package/dist/{context-Ci5vuUr_.d.ts → context-B2C8nuN3.d.ts} +4 -3
  7. package/dist/{context-Bm6ldGvx.d.cts → context-CrDDVrbF.d.cts} +4 -3
  8. package/dist/{deno-Be9xoxps.mjs → deno-BXI_BLNy.mjs} +1 -1
  9. package/dist/{docloader-Dtuyb0ps.mjs → docloader-B5BIo7e9.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 +64 -8
  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 +6 -6
  23. package/dist/federation/portable-actor.test.mjs +3 -3
  24. package/dist/federation/portable-chain.test.mjs +17 -5
  25. package/dist/federation/portable-gateway-keys.test.mjs +76 -5
  26. package/dist/federation/portable-inbox.test.mjs +9 -4
  27. package/dist/federation/portable.test.mjs +3 -3
  28. package/dist/federation/send.test.mjs +3 -3
  29. package/dist/federation/tasks/enqueue.test.mjs +1 -1
  30. package/dist/federation/tasks/tasks.test.mjs +2 -2
  31. package/dist/federation/temporal.test.mjs +1 -1
  32. package/dist/federation/webfinger.test.mjs +1 -1
  33. package/dist/{federation-B6IHuS6n.cjs → federation-DqDOPjWo.cjs} +23 -28
  34. package/dist/{http-D76Py0M4.d.cts → http-C-x_OLSp.d.ts} +57 -14
  35. package/dist/{http-BFeK9sBg.mjs → http-CEx2SZjf.mjs} +18 -12
  36. package/dist/{http-BrQgkJay.d.ts → http-CjezPjFX.d.cts} +57 -14
  37. package/dist/{http-Rmbiu-5S.js → http-D4KW2GTi.js} +376 -36
  38. package/dist/{http-o8l4QeUY.cjs → http-DSYQQn23.cjs} +392 -34
  39. package/dist/{key-Cc1b6pW9.mjs → key-C0giKuki.mjs} +103 -16
  40. package/dist/{kv-cache-tLKBfxHc.mjs → kv-cache-AWTrxTq3.mjs} +1 -1
  41. package/dist/{kv-cache-BUkgZAcX.cjs → kv-cache-BcsvaCCL.cjs} +1 -1
  42. package/dist/{kv-cache-BweDce2I.js → kv-cache-D_E-2K3i.js} +1 -1
  43. package/dist/{ld-DQNTIrAe.mjs → ld-CWDReGMZ.mjs} +3 -3
  44. package/dist/{metrics-DDE9S63Q.mjs → metrics-DxlVXwn5.mjs} +1 -1
  45. package/dist/{middleware-K9aLnf57.js → middleware-1mv9QR16.js} +23 -28
  46. package/dist/{middleware-CsQorLC_.mjs → middleware-B7stx5gy.mjs} +1 -1
  47. package/dist/{middleware-BdhvduR9.mjs → middleware-CAWRGSwH.mjs} +24 -24
  48. package/dist/{mod-AVir5nVI.d.cts → mod-B7SfEqIP.d.cts} +1 -1
  49. package/dist/{mod-Co53r-VP.d.ts → mod-Bz9tFXI2.d.ts} +1 -1
  50. package/dist/{mod-eVOUPbQE.d.ts → mod-C06GoL5Z.d.ts} +1 -1
  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-Byt5tvAS.d.cts → mod-S--JxYjo.d.cts} +1 -1
  54. package/dist/mod.cjs +4 -4
  55. package/dist/mod.d.cts +5 -5
  56. package/dist/mod.d.ts +5 -5
  57. package/dist/mod.js +4 -4
  58. package/dist/nodeinfo/handler.test.mjs +1 -1
  59. package/dist/{owner-CCfdL83M.mjs → owner-CgoCOTUQ.mjs} +37 -4
  60. package/dist/{portable-collection-WXaHkNqC.mjs → portable-collection-0_5DyXMt.mjs} +2 -2
  61. package/dist/{portable-collection-DXogVs__.cjs → portable-collection-BD8fE3j3.cjs} +34 -1
  62. package/dist/{portable-collection-DdXsKBg4.js → portable-collection-CG9P_i0E.js} +34 -1
  63. package/dist/portable-key-BXkgMpw1.mjs +395 -0
  64. package/dist/{portable-key-id-T7lecIbt.mjs → portable-key-id-Bg259c4Z.mjs} +69 -2
  65. package/dist/{proof-CW8k0R1G.mjs → proof-B5s-W6Sm.mjs} +7 -6
  66. package/dist/{send--FORaIc4.mjs → send-BAJvkdNL.mjs} +3 -3
  67. package/dist/sig/compound-proof-discovery.test.mjs +1 -1
  68. package/dist/sig/compound-proof-producer.test.mjs +1 -1
  69. package/dist/sig/compound-proof-verification.test.mjs +196 -2
  70. package/dist/sig/compound-proof.test.mjs +2 -2
  71. package/dist/sig/http.test.mjs +2 -2
  72. package/dist/sig/key.test.mjs +1 -1
  73. package/dist/sig/ld.test.mjs +2 -2
  74. package/dist/sig/mod.cjs +2 -2
  75. package/dist/sig/mod.d.cts +2 -2
  76. package/dist/sig/mod.d.ts +2 -2
  77. package/dist/sig/mod.js +2 -2
  78. package/dist/sig/owner.test.mjs +1 -1
  79. package/dist/sig/portable-collection.test.mjs +1 -1
  80. package/dist/sig/portable-key.test.mjs +479 -5
  81. package/dist/sig/proof.test.mjs +45 -3
  82. package/dist/{temporal-CffY_XOJ.mjs → temporal-BkCPJ_uC.mjs} +1 -1
  83. package/dist/testing/kv.test.mjs +1 -1
  84. package/dist/testing/mod.d.mts +3 -2
  85. package/dist/utils/docloader.test.mjs +2 -2
  86. package/dist/utils/kv-cache.test.mjs +1 -1
  87. package/dist/utils/mod.cjs +1 -1
  88. package/dist/utils/mod.d.cts +1 -1
  89. package/dist/utils/mod.d.ts +1 -1
  90. package/dist/utils/mod.js +1 -1
  91. package/package.json +6 -6
  92. package/dist/portable-key-C-fPq_Yf.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-Cc1b6pW9.mjs";
6
- import { h as verifyJsonLd, p as signJsonLd } from "../ld-DQNTIrAe.mjs";
7
- import { n as createProof, u as verifyProof } from "../proof-CW8k0R1G.mjs";
8
- import { d as verifyRequest, f as verifyRequestDetailed, l as signRequest } from "../http-BFeK9sBg.mjs";
5
+ import { i as fetchKey } from "../key-C0giKuki.mjs";
6
+ import { h as verifyJsonLd, p as signJsonLd } from "../ld-CWDReGMZ.mjs";
7
+ import { n as createProof, u as verifyProof } from "../proof-B5s-W6Sm.mjs";
8
+ import { d as verifyRequest, f as verifyRequestDetailed, l as signRequest } from "../http-CEx2SZjf.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-CCfdL83M.mjs";
10
+ import { n as getKeyOwner, t as doesActorOwnKey } from "../owner-CgoCOTUQ.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-CW8k0R1G.mjs";
14
- import { d as verifyRequest } from "../http-BFeK9sBg.mjs";
13
+ import { i as signObject, n as createProof, o as verifyObject, r as hasProofLike, s as verifyPortableObjectProof, u as verifyProof } from "../proof-B5s-W6Sm.mjs";
14
+ import { d as verifyRequest } from "../http-CEx2SZjf.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-Dtuyb0ps.mjs";
16
+ import { t as getAuthenticatedDocumentLoader } from "../docloader-B5BIo7e9.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";
@@ -1655,6 +1655,48 @@ test("verifyObject() hydrates pending proof references", async () => {
1655
1655
  assertEquals(proofFetches, 1);
1656
1656
  assertInstanceOf(verified, Note);
1657
1657
  });
1658
+ test("verifyObject() does not pass its proof verifier on to cached proofs", async () => {
1659
+ const did = await exportDidKey(ed25519PublicKey.publicKey);
1660
+ const method = did.substring(8);
1661
+ const keyId = new URL(`${did}#${method}`);
1662
+ const proofUrl = `ap://did:key:${method}/proofs/1`;
1663
+ const signed = await signPortableJsonLd({
1664
+ "@context": portableContext,
1665
+ id: `ap://did:key:${method}/objects/1`,
1666
+ type: "Note",
1667
+ attributedTo: `ap://did:key:${method}/actor`,
1668
+ content: "Portable note with a referenced proof"
1669
+ }, {
1670
+ verificationMethod: keyId,
1671
+ proofOptions: { id: proofUrl }
1672
+ });
1673
+ const rawProof = signed.proof;
1674
+ const referencedJsonLd = { ...signed };
1675
+ delete referencedJsonLd.proof;
1676
+ referencedJsonLd["https://w3id.org/security#proof"] = [{ "@graph": [rawProof] }, { "@graph": [{ "@id": proofUrl }] }];
1677
+ const rootVerifier = async () => ({ verified: false });
1678
+ for (const verifyPortableObject of [void 0, rootVerifier]) {
1679
+ let proofFetches = 0;
1680
+ const verified = await verifyObject(Note, referencedJsonLd, {
1681
+ documentLoader(url) {
1682
+ proofFetches++;
1683
+ return Promise.resolve({
1684
+ contextUrl: null,
1685
+ document: structuredClone(rawProof),
1686
+ documentUrl: url
1687
+ });
1688
+ },
1689
+ contextLoader: mockDocumentLoader,
1690
+ ...verifyPortableObject == null ? {} : { verifyPortableObject }
1691
+ });
1692
+ assertInstanceOf(verified, Note);
1693
+ assertEquals(proofFetches, 1);
1694
+ const proofs = await Array.fromAsync(verified.getProofs());
1695
+ assertEquals(proofs.length, 2);
1696
+ assertEquals(proofFetches, 1);
1697
+ for (const proof of proofs) assert(proof._verifyPortableObject === verifyPortableObject);
1698
+ }
1699
+ });
1658
1700
  test("verifyObject() hydrates portable proof references with gateway hints", async () => {
1659
1701
  const did = await exportDidKey(ed25519PublicKey.publicKey);
1660
1702
  const method = did.substring(8);
@@ -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-DQNTIrAe.mjs";
4
+ import { c as getNormalizationContextLoader } from "./ld-CWDReGMZ.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-BdhvduR9.mjs";
9
+ import { o as createFederation } from "../middleware-CAWRGSwH.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();
@@ -2891,8 +2891,9 @@ interface Context<TContextData> {
2891
2891
  * property accessors, `lookupObject()`, and `traverseCollection()`, passing
2892
2892
  * a context as their options applies the policy, e.g.,
2893
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
2894
+ * such as activities that inboxes receive, and objects that accessors
2895
+ * fetch with a context as options also use it by default for their
2896
+ * property accessors. It never verifies an object by itself being
2896
2897
  * there: it only applies to objects that are dereferenced.
2897
2898
  *
2898
2899
  * It is optional only so that custom implementations of this interface
@@ -5,9 +5,9 @@ import { t as esm_default } from "../esm-BHXXx4Wx.mjs";
5
5
  import { t as assertEquals } from "../assert_equals-Z6IcEbWU.mjs";
6
6
  import "../std__assert-DvMDRPes.mjs";
7
7
  import { t as assertRejects } from "../assert_rejects-CTptdYck.mjs";
8
- import { d as verifyRequest } from "../http-BFeK9sBg.mjs";
8
+ import { d as verifyRequest } from "../http-CEx2SZjf.mjs";
9
9
  import { i as rsaPrivateKey2 } from "../keys-DIJNHW-Y.mjs";
10
- import { t as getAuthenticatedDocumentLoader } from "../docloader-Dtuyb0ps.mjs";
10
+ import { t as getAuthenticatedDocumentLoader } from "../docloader-B5BIo7e9.mjs";
11
11
  import { configure, reset } from "@logtape/logtape";
12
12
  import { mockDocumentLoader, test } from "@fedify/fixture";
13
13
  import { FetchError, UrlError } from "@fedify/vocab-runtime";
@@ -1,7 +1,7 @@
1
1
  import { Temporal } from "temporal-polyfill";
2
2
  import { URLPattern } from "urlpattern-polyfill";
3
3
  globalThis.addEventListener = () => {};
4
- import { n as kvCache, t as MockKvStore } from "../kv-cache-tLKBfxHc.mjs";
4
+ import { n as kvCache, t as MockKvStore } from "../kv-cache-AWTrxTq3.mjs";
5
5
  import { deepStrictEqual, throws } from "node:assert";
6
6
  import { createTestMeterProvider, mockDocumentLoader, test } from "@fedify/fixture";
7
7
  import { preloadedContexts } from "@fedify/vocab-runtime";
@@ -1,5 +1,5 @@
1
1
  const { URLPattern } = require("urlpattern-polyfill");
2
2
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
3
- const require_kv_cache = require("../kv-cache-BUkgZAcX.cjs");
3
+ const require_kv_cache = require("../kv-cache-BcsvaCCL.cjs");
4
4
  exports.getAuthenticatedDocumentLoader = require_kv_cache.getAuthenticatedDocumentLoader;
5
5
  exports.kvCache = require_kv_cache.kvCache;
@@ -1,3 +1,3 @@
1
1
  /// <reference lib="esnext.temporal" />
2
- import { n as getAuthenticatedDocumentLoader, t as kvCache } from "../mod-Cr79cQlc.cjs";
2
+ import { n as getAuthenticatedDocumentLoader, t as kvCache } from "../mod-D4g9ffEn.cjs";
3
3
  export { getAuthenticatedDocumentLoader, kvCache };
@@ -1,3 +1,3 @@
1
1
  /// <reference lib="esnext.temporal" />
2
- import { n as getAuthenticatedDocumentLoader, t as kvCache } from "../mod-Cwdt_CWU.js";
2
+ import { n as getAuthenticatedDocumentLoader, t as kvCache } from "../mod-CZetqT5h.js";
3
3
  export { getAuthenticatedDocumentLoader, kvCache };
package/dist/utils/mod.js CHANGED
@@ -1,4 +1,4 @@
1
1
  import { Temporal } from "temporal-polyfill";
2
2
  import { URLPattern } from "urlpattern-polyfill";
3
- import { n as getAuthenticatedDocumentLoader, t as kvCache } from "../kv-cache-BweDce2I.js";
3
+ import { n as getAuthenticatedDocumentLoader, t as kvCache } from "../kv-cache-D_E-2K3i.js";
4
4
  export { getAuthenticatedDocumentLoader, kvCache };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fedify/fedify",
3
- "version": "2.4.0-dev.2127+c41ce2eb",
3
+ "version": "2.4.0-dev.2141+881cc858",
4
4
  "description": "An ActivityPub server framework",
5
5
  "keywords": [
6
6
  "ActivityPub",
@@ -151,10 +151,10 @@
151
151
  "structured-field-values": "^2.0.4",
152
152
  "temporal-polyfill": "^1.0.1",
153
153
  "urlpattern-polyfill": "^10.1.0",
154
- "@fedify/vocab-runtime": "2.4.0-dev.2127+c41ce2eb",
155
- "@fedify/vocab": "2.4.0-dev.2127+c41ce2eb",
156
- "@fedify/uri-template": "2.4.0-dev.2127+c41ce2eb",
157
- "@fedify/webfinger": "2.4.0-dev.2127+c41ce2eb"
154
+ "@fedify/uri-template": "2.4.0-dev.2141+881cc858",
155
+ "@fedify/vocab": "2.4.0-dev.2141+881cc858",
156
+ "@fedify/vocab-runtime": "2.4.0-dev.2141+881cc858",
157
+ "@fedify/webfinger": "2.4.0-dev.2141+881cc858"
158
158
  },
159
159
  "devDependencies": {
160
160
  "@std/assert": "npm:@jsr/std__assert@^0.226.0",
@@ -167,7 +167,7 @@
167
167
  "typescript": "^6.0.0",
168
168
  "wrangler": "^4.17.0",
169
169
  "@fedify/fixture": "2.0.0",
170
- "@fedify/vocab-tools": "^2.4.0-dev.2127+c41ce2eb"
170
+ "@fedify/vocab-tools": "^2.4.0-dev.2141+881cc858"
171
171
  },
172
172
  "scripts": {
173
173
  "build:self": "tsdown",