@fedify/fedify 2.4.0-dev.1964 → 2.4.0-dev.1995

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 (94) hide show
  1. package/dist/{builder-B4zpKIFU.mjs → builder-D0DxgOV5.mjs} +2 -2
  2. package/dist/{client-CtxvmazV.mjs → client-DpnpgjyX.mjs} +78 -4
  3. package/dist/compat/mod.d.cts +1 -1
  4. package/dist/compat/mod.d.ts +1 -1
  5. package/dist/compat/transformers.test.mjs +1 -1
  6. package/dist/compound-proof-CJnWc6c-.mjs +495 -0
  7. package/dist/{context-COLwHogJ.d.cts → context-BeDnDuF3.d.cts} +4 -3
  8. package/dist/{context-DiiCt4S7.d.ts → context-uyRUWoZQ.d.ts} +4 -3
  9. package/dist/{deno-CtG93ohj.mjs → deno-CABhzCb_.mjs} +1 -1
  10. package/dist/{docloader-dk_k3S8d.mjs → docloader-BbGSL0Xq.mjs} +2 -2
  11. package/dist/federation/builder.test.mjs +1 -1
  12. package/dist/federation/handler-compound-proof.test.d.mts +2 -0
  13. package/dist/federation/handler-compound-proof.test.mjs +152 -0
  14. package/dist/federation/handler.test.mjs +122 -4
  15. package/dist/federation/idempotency.test.mjs +2 -2
  16. package/dist/federation/keycache.test.mjs +52 -9
  17. package/dist/federation/metrics.test.mjs +1 -1
  18. package/dist/federation/middleware.test.mjs +111 -15
  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/send.test.mjs +139 -4
  24. package/dist/federation/tasks/enqueue.test.mjs +1 -1
  25. package/dist/federation/tasks/tasks.test.mjs +2 -2
  26. package/dist/federation/temporal.test.mjs +1 -1
  27. package/dist/federation/webfinger.test.mjs +1 -1
  28. package/dist/{federation-Drjlrrax.cjs → federation-DCEHQk4G.cjs} +632 -57
  29. package/dist/{http-6TI0tI03.d.cts → http-65PYi9jY.d.ts} +34 -2
  30. package/dist/{http-Dj4s1UQ0.js → http-9WyuldYh.js} +170 -8
  31. package/dist/{http-CdrSn2ip.d.ts → http-CP-7OdSq.d.cts} +34 -2
  32. package/dist/{http-B3skRvGo.cjs → http-CRFns-UE.cjs} +181 -7
  33. package/dist/{http-D7LX-BL9.mjs → http-D9urSO2D.mjs} +3 -3
  34. package/dist/{key-BVjgbs_9.mjs → key-Ccs8cnd7.mjs} +169 -7
  35. package/dist/{keycache-ZLqIfrkL.mjs → keycache-BUDHB5rY.mjs} +12 -4
  36. package/dist/{kv-cache-DtsjH_bY.cjs → kv-cache-DXT9vmIg.cjs} +2 -2
  37. package/dist/{kv-cache-tGNbvDCF.js → kv-cache-DtmXhPXG.js} +2 -2
  38. package/dist/{kv-cache-xRcC_xxP.mjs → kv-cache-lKyFn_Dn.mjs} +1 -1
  39. package/dist/{ld-OuCeQFny.mjs → ld-qup__MAW.mjs} +3 -3
  40. package/dist/map-local-context-conflict-DOYEEJ3u.mjs +204 -0
  41. package/dist/map-local-create-note-B0SeXJms.mjs +214 -0
  42. package/dist/{metrics-B9N27cCB.mjs → metrics-CISXCvB1.mjs} +1 -1
  43. package/dist/{middleware-TeLjzoxn.mjs → middleware-CzvJvPHK.mjs} +58 -15
  44. package/dist/{middleware-Cd_vPqDH.mjs → middleware-DkrU2WhY.mjs} +1 -1
  45. package/dist/{middleware-CPTDFMmJ.js → middleware-DsfWajaD.js} +593 -18
  46. package/dist/{mod-DypxPS9H.d.ts → mod-C0BYc0OU.d.ts} +1 -1
  47. package/dist/{mod-CnGLNoVw.d.cts → mod-CbKDOnCt.d.cts} +1 -1
  48. package/dist/{mod-CLHkCwLm.d.cts → mod-CyiSjRkk.d.cts} +1 -1
  49. package/dist/{mod-CSH1Ee-I.d.ts → mod-D882_Xsg.d.ts} +1 -1
  50. package/dist/{mod-DzRXACM5.d.ts → mod-UDwMjuHM.d.ts} +1 -1
  51. package/dist/{mod-BZldCEU0.d.cts → mod-x9W0d0Pk.d.cts} +1 -1
  52. package/dist/mod.cjs +20 -20
  53. package/dist/mod.d.cts +6 -6
  54. package/dist/mod.d.ts +6 -6
  55. package/dist/mod.js +5 -5
  56. package/dist/nodeinfo/client.test.mjs +28 -1
  57. package/dist/nodeinfo/handler.test.mjs +1 -1
  58. package/dist/nodeinfo/mod.cjs +1 -1
  59. package/dist/nodeinfo/mod.js +1 -1
  60. package/dist/{owner-DGSfntI-.d.ts → owner-BF0L9cMt.d.cts} +12 -1
  61. package/dist/{proof-BO_esQii.js → owner-BIPEZGiU.js} +1255 -1098
  62. package/dist/{owner-r_waTiQj.d.cts → owner-BSzr5-I8.d.ts} +12 -1
  63. package/dist/owner-CRoq8Spb.mjs +175 -0
  64. package/dist/{proof-Dua0d906.cjs → owner-CVA7L9MA.cjs} +1266 -1097
  65. package/dist/{proof-D5Ajk4DJ.mjs → proof-BDxNVZ3E.mjs} +186 -83
  66. package/dist/{send-DVXyUqhO.mjs → send-BzM5GLyn.mjs} +68 -11
  67. package/dist/sig/compound-proof-discovery.test.d.mts +2 -0
  68. package/dist/sig/compound-proof-discovery.test.mjs +331 -0
  69. package/dist/sig/compound-proof-verification.test.d.mts +2 -0
  70. package/dist/sig/compound-proof-verification.test.mjs +465 -0
  71. package/dist/sig/compound-proof.test.d.mts +2 -0
  72. package/dist/sig/compound-proof.test.mjs +324 -0
  73. package/dist/sig/http.test.mjs +2 -2
  74. package/dist/sig/key.test.mjs +169 -2
  75. package/dist/sig/ld.test.mjs +55 -2
  76. package/dist/sig/mod.cjs +17 -17
  77. package/dist/sig/mod.d.cts +3 -3
  78. package/dist/sig/mod.d.ts +3 -3
  79. package/dist/sig/mod.js +2 -2
  80. package/dist/sig/owner.test.mjs +103 -4
  81. package/dist/sig/proof.test.mjs +120 -1
  82. package/dist/{temporal-BoDw51A-.mjs → temporal-Dv6Qv-8k.mjs} +1 -1
  83. package/dist/testing/kv.test.mjs +1 -1
  84. package/dist/testing/mod.d.mts +2 -1
  85. package/dist/{types-RXm5YxpC.cjs → types-Be2zbyeG.cjs} +94 -2
  86. package/dist/{types-Dtyf8lVj.js → types-C_cNlNVK.js} +78 -4
  87. package/dist/utils/docloader.test.mjs +27 -3
  88. package/dist/utils/kv-cache.test.mjs +1 -1
  89. package/dist/utils/mod.cjs +1 -1
  90. package/dist/utils/mod.d.cts +1 -1
  91. package/dist/utils/mod.d.ts +1 -1
  92. package/dist/utils/mod.js +1 -1
  93. package/package.json +6 -6
  94. package/dist/owner-DTEqTzSr.mjs +0 -120
package/dist/sig/mod.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  /// <reference lib="esnext.temporal" />
2
- import { C as exportJwk, D as importJwk, E as generateCryptoKeyPair, S as KeyCache, T as fetchKeyDetailed, _ as validateAcceptSignature, a as VerifyRequestDetailedResult, b as FetchKeyOptions, c as signRequest, d as AcceptSignatureMember, f as AcceptSignatureParameters, g as parseAcceptSignature, h as fulfillAcceptSignature, i as SignRequestOptions, l as verifyRequest, m as formatAcceptSignature, n as HttpMessageSignaturesSpecDeterminer, o as VerifyRequestFailureReason, p as FulfillAcceptSignatureResult, r as Rfc9421SignRequestOptions, s as VerifyRequestOptions, t as HttpMessageSignaturesSpec, u as verifyRequestDetailed, v as FetchKeyDetailedResult, w as fetchKey, x as FetchKeyResult, y as FetchKeyErrorResult } from "../http-CdrSn2ip.js";
3
- import { i as getKeyOwner, n as GetKeyOwnerOptions, r as doesActorOwnKey, t as DoesActorOwnKeyOptions } from "../owner-DGSfntI-.js";
4
- import { C as verifyJsonLd, S as signJsonLd, _ as VerifySignatureOptions, a as VerifyPortableObjectProofOptions, b as detachSignature, c as createProof, d as verifyObject, f as verifyPortableObjectProof, g as VerifyJsonLdOptions, h as SignJsonLdOptions, i as VerifyPortableObjectProofFailureReason, l as hasProofLike, m as CreateSignatureOptions, n as SignObjectOptions, o as VerifyPortableObjectProofResult, p as verifyProof, r as VerifyObjectOptions, s as VerifyProofOptions, t as CreateProofOptions, u as signObject, v as attachSignature, w as verifySignature, x as hasSignatureLike, y as createSignature } from "../mod-DzRXACM5.js";
2
+ import { C as exportJwk, D as importJwk, E as generateCryptoKeyPair, S as KeyCache, T as fetchKeyDetailed, _ as validateAcceptSignature, a as VerifyRequestDetailedResult, b as FetchKeyOptions, c as signRequest, d as AcceptSignatureMember, f as AcceptSignatureParameters, g as parseAcceptSignature, h as fulfillAcceptSignature, i as SignRequestOptions, l as verifyRequest, m as formatAcceptSignature, n as HttpMessageSignaturesSpecDeterminer, o as VerifyRequestFailureReason, p as FulfillAcceptSignatureResult, r as Rfc9421SignRequestOptions, s as VerifyRequestOptions, t as HttpMessageSignaturesSpec, u as verifyRequestDetailed, v as FetchKeyDetailedResult, w as fetchKey, x as FetchKeyResult, y as FetchKeyErrorResult } from "../http-65PYi9jY.js";
3
+ import { i as getKeyOwner, n as GetKeyOwnerOptions, r as doesActorOwnKey, t as DoesActorOwnKeyOptions } from "../owner-BSzr5-I8.js";
4
+ import { C as verifyJsonLd, S as signJsonLd, _ as VerifySignatureOptions, a as VerifyPortableObjectProofOptions, b as detachSignature, c as createProof, d as verifyObject, f as verifyPortableObjectProof, g as VerifyJsonLdOptions, h as SignJsonLdOptions, i as VerifyPortableObjectProofFailureReason, l as hasProofLike, m as CreateSignatureOptions, n as SignObjectOptions, o as VerifyPortableObjectProofResult, p as verifyProof, r as VerifyObjectOptions, s as VerifyProofOptions, t as CreateProofOptions, u as signObject, v as attachSignature, w as verifySignature, x as hasSignatureLike, y as createSignature } from "../mod-UDwMjuHM.js";
5
5
  export { AcceptSignatureMember, AcceptSignatureParameters, CreateProofOptions, CreateSignatureOptions, DoesActorOwnKeyOptions, FetchKeyDetailedResult, FetchKeyErrorResult, FetchKeyOptions, FetchKeyResult, FulfillAcceptSignatureResult, GetKeyOwnerOptions, HttpMessageSignaturesSpec, HttpMessageSignaturesSpecDeterminer, KeyCache, Rfc9421SignRequestOptions, SignJsonLdOptions, SignObjectOptions, SignRequestOptions, VerifyJsonLdOptions, VerifyObjectOptions, VerifyPortableObjectProofFailureReason, VerifyPortableObjectProofOptions, VerifyPortableObjectProofResult, VerifyProofOptions, VerifyRequestDetailedResult, VerifyRequestFailureReason, VerifyRequestOptions, VerifySignatureOptions, attachSignature, createProof, createSignature, detachSignature, doesActorOwnKey, exportJwk, fetchKey, fetchKeyDetailed, formatAcceptSignature, fulfillAcceptSignature, generateCryptoKeyPair, getKeyOwner, hasProofLike, hasSignatureLike, importJwk, parseAcceptSignature, signJsonLd, signObject, signRequest, validateAcceptSignature, verifyJsonLd, verifyObject, verifyPortableObjectProof, verifyProof, verifyRequest, verifyRequestDetailed, verifySignature };
package/dist/sig/mod.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { Temporal } from "temporal-polyfill";
2
2
  import { URLPattern } from "urlpattern-polyfill";
3
- import { A as formatAcceptSignature, M as parseAcceptSignature, N as validateAcceptSignature, a as verifyRequestDetailed, c as fetchKeyDetailed, i as verifyRequest, j as fulfillAcceptSignature, l as generateCryptoKeyPair, o as exportJwk, r as signRequest, s as fetchKey, u as importJwk } from "../http-Dj4s1UQ0.js";
4
- import { C as verifyJsonLd, a as verifyPortableObjectProof, c as doesActorOwnKey, f as attachSignature, h as detachSignature, i as verifyObject, l as getKeyOwner, m as createSignature, n as hasProofLike, o as verifyProof, r as signObject, t as createProof, v as hasSignatureLike, w as verifySignature, x as signJsonLd } from "../proof-BO_esQii.js";
3
+ import { F as validateAcceptSignature, M as formatAcceptSignature, N as fulfillAcceptSignature, P as parseAcceptSignature, a as verifyRequestDetailed, c as fetchKey, d as importJwk, i as verifyRequest, l as fetchKeyDetailed, o as exportJwk, r as signRequest, u as generateCryptoKeyPair } from "../http-9WyuldYh.js";
4
+ import { S as signJsonLd, T as verifySignature, a as signObject, c as verifyPortableObjectProof, g as detachSignature, h as createSignature, i as hasProofLike, n as getKeyOwner, p as attachSignature, r as createProof, s as verifyObject, t as doesActorOwnKey, u as verifyProof, w as verifyJsonLd, y as hasSignatureLike } from "../owner-BIPEZGiU.js";
5
5
  export { attachSignature, createProof, createSignature, detachSignature, doesActorOwnKey, exportJwk, fetchKey, fetchKeyDetailed, formatAcceptSignature, fulfillAcceptSignature, generateCryptoKeyPair, getKeyOwner, hasProofLike, hasSignatureLike, importJwk, parseAcceptSignature, signJsonLd, signObject, signRequest, validateAcceptSignature, verifyJsonLd, verifyObject, verifyPortableObjectProof, verifyProof, verifyRequest, verifyRequestDetailed, verifySignature };
@@ -6,7 +6,7 @@ import { t as assertFalse } from "../assert_false-3pLLLsO4.mjs";
6
6
  import "../std__assert-CxFT0MNt.mjs";
7
7
  import { t as assert } from "../assert-CebeUf0C.mjs";
8
8
  import { o as rsaPublicKey1, s as rsaPublicKey2 } from "../keys-DIJNHW-Y.mjs";
9
- import { n as getKeyOwner, t as doesActorOwnKey } from "../owner-DTEqTzSr.mjs";
9
+ import { n as getKeyOwner, t as doesActorOwnKey } from "../owner-CRoq8Spb.mjs";
10
10
  import { Create, CryptographicKey, lookupObject } from "@fedify/vocab";
11
11
  import { createTestTracerProvider, mockDocumentLoader, test } from "@fedify/fixture";
12
12
  //#region src/sig/owner.test.ts
@@ -40,7 +40,7 @@ test("doesActorOwnKey() records OpenTelemetry span", async () => {
40
40
  id: new URL("https://example.com/activity"),
41
41
  actor: new URL("https://example.com/person")
42
42
  }), new CryptographicKey({
43
- id: new URL("https://example.com/person#key"),
43
+ id: new URL("https://example.com/key"),
44
44
  owner: new URL("https://example.com/person")
45
45
  }), {
46
46
  documentLoader: mockDocumentLoader,
@@ -50,9 +50,108 @@ test("doesActorOwnKey() records OpenTelemetry span", async () => {
50
50
  assertEquals(spans.length, 1);
51
51
  const span = spans[0];
52
52
  assertEquals(span.attributes["activitypub.actor.id"], "https://example.com/person");
53
- assertEquals(span.attributes["activitypub.key.id"], "https://example.com/person#key");
53
+ assertEquals(span.attributes["activitypub.key.id"], "https://example.com/key");
54
54
  assertEquals(span.attributes["activitypub.key_ownership.verified"], true);
55
- assertEquals(span.attributes["activitypub.key_ownership.method"], "owner_id");
55
+ assertEquals(span.attributes["activitypub.key_ownership.method"], "key_owner");
56
+ });
57
+ test("doesActorOwnKey() rejects a key that only claims to be owned", async () => {
58
+ const options = {
59
+ documentLoader: mockDocumentLoader,
60
+ contextLoader: mockDocumentLoader
61
+ };
62
+ assertFalse(await doesActorOwnKey(new Create({ actor: new URL("https://example.com/person") }), new CryptographicKey({
63
+ id: new URL("https://attacker.example/key"),
64
+ owner: new URL("https://example.com/person")
65
+ }), options));
66
+ const unresolvable = new URL("https://impersonated.invalid/users/victim");
67
+ assertFalse(await doesActorOwnKey(new Create({ actor: unresolvable }), new CryptographicKey({
68
+ id: new URL("https://attacker.example/key"),
69
+ owner: unresolvable
70
+ }), options));
71
+ });
72
+ test("doesActorOwnKey() ignores an actor embedded in the activity", async () => {
73
+ const { publicKeyPem } = await rsaPublicKey1.toJsonLd({ contextLoader: mockDocumentLoader });
74
+ const activity = await Create.fromJsonLd({
75
+ "@context": ["https://www.w3.org/ns/activitystreams", "https://w3id.org/security/v1"],
76
+ id: "https://example.com/activity",
77
+ type: "Create",
78
+ actor: {
79
+ id: "https://example.com/person",
80
+ type: "Person",
81
+ publicKey: {
82
+ id: "https://attacker.example/key",
83
+ type: "CryptographicKey",
84
+ owner: "https://example.com/person",
85
+ publicKeyPem
86
+ }
87
+ }
88
+ }, {
89
+ documentLoader: mockDocumentLoader,
90
+ contextLoader: mockDocumentLoader
91
+ });
92
+ assertEquals(activity.actorId, new URL("https://example.com/person"));
93
+ assertFalse(await doesActorOwnKey(activity, new CryptographicKey({ id: new URL("https://attacker.example/key") }), {
94
+ documentLoader: mockDocumentLoader,
95
+ contextLoader: mockDocumentLoader
96
+ }));
97
+ });
98
+ test("getKeyOwner() rejects an actor document from another origin", async () => {
99
+ const keyId = "https://attacker.example/key";
100
+ const { publicKeyPem } = await rsaPublicKey1.toJsonLd({ contextLoader: mockDocumentLoader });
101
+ const options = {
102
+ documentLoader(resource) {
103
+ if (resource === keyId) return Promise.resolve({
104
+ contextUrl: null,
105
+ documentUrl: resource,
106
+ document: {
107
+ "@context": ["https://www.w3.org/ns/activitystreams", "https://w3id.org/security/v1"],
108
+ id: "https://example.com/person",
109
+ type: "Person",
110
+ publicKey: [{
111
+ id: keyId,
112
+ type: "CryptographicKey",
113
+ publicKeyPem
114
+ }]
115
+ }
116
+ });
117
+ return mockDocumentLoader(resource);
118
+ },
119
+ contextLoader: mockDocumentLoader
120
+ };
121
+ assertEquals(await getKeyOwner(new URL(keyId), options), null);
122
+ });
123
+ test("doesActorOwnKey() matches a key that leaves its id implicit", async () => {
124
+ const actorId = "https://example.com/implicit-key-actor";
125
+ const { publicKeyPem } = await rsaPublicKey1.toJsonLd({ contextLoader: mockDocumentLoader });
126
+ const options = {
127
+ documentLoader(resource) {
128
+ if (resource === actorId) return Promise.resolve({
129
+ contextUrl: null,
130
+ documentUrl: resource,
131
+ document: {
132
+ "@context": ["https://www.w3.org/ns/activitystreams", "https://w3id.org/security/v1"],
133
+ id: actorId,
134
+ type: "Person",
135
+ publicKey: [{
136
+ type: "CryptographicKey",
137
+ owner: actorId,
138
+ publicKeyPem
139
+ }]
140
+ }
141
+ });
142
+ return mockDocumentLoader(resource);
143
+ },
144
+ contextLoader: mockDocumentLoader
145
+ };
146
+ const activity = new Create({ actor: new URL(actorId) });
147
+ assert(await doesActorOwnKey(activity, new CryptographicKey({
148
+ owner: new URL(actorId),
149
+ publicKey: rsaPublicKey1.publicKey
150
+ }), options));
151
+ assertFalse(await doesActorOwnKey(activity, new CryptographicKey({
152
+ owner: new URL(actorId),
153
+ publicKey: rsaPublicKey2.publicKey
154
+ }), options));
56
155
  });
57
156
  //#endregion
58
157
  export {};
@@ -10,7 +10,7 @@ import { t as assertRejects } from "../assert_rejects-wFbSPn1O.mjs";
10
10
  import { t as assert } from "../assert-CebeUf0C.mjs";
11
11
  import { i as rsaPrivateKey2, n as ed25519PrivateKey, r as ed25519PublicKey, s as rsaPublicKey2, t as ed25519Multikey } from "../keys-DIJNHW-Y.mjs";
12
12
  import { r as normalizeOutgoingActivityJsonLd } from "../outgoing-jsonld-B5Ne7JoL.mjs";
13
- import { a as verifyPortableObjectProof, i as verifyObject, n as hasProofLike, o as verifyProof, r as signObject, t as createProof } from "../proof-D5Ajk4DJ.mjs";
13
+ import { a as verifyObject, c as verifyProof, n as hasProofLike, o as verifyPortableObjectProof, r as signObject, t as createProof } from "../proof-BDxNVZ3E.mjs";
14
14
  import { Create, DataIntegrityProof, Document, Multikey, Note, PUBLIC_COLLECTION, Place, Translation } from "@fedify/vocab";
15
15
  import { createTestMeterProvider, mockDocumentLoader, test } from "@fedify/fixture";
16
16
  import { decodeMultibase, encodeMultibase, exportDidKey, exportMultibaseKey, formatIri, importMultibaseKey, parseIri } from "@fedify/vocab-runtime";
@@ -1005,6 +1005,56 @@ test("verifyPortableObjectProof()", async (t) => {
1005
1005
  })).verified);
1006
1006
  assertEquals(fetches, 1);
1007
1007
  });
1008
+ await t.step("caches keys owned by their own DID", async () => {
1009
+ const verificationMethod = new URL("did:web:example.com#key");
1010
+ const document = {
1011
+ ...unsignedObject,
1012
+ id: "ap://did:web:example.com/objects/1"
1013
+ };
1014
+ const cached = [];
1015
+ assert((await verifyPortableObjectProof(await signPortableJsonLd(document, { verificationMethod }), {
1016
+ contextLoader: mockDocumentLoader,
1017
+ keyCache: {
1018
+ get: () => Promise.resolve(void 0),
1019
+ set: (keyId) => {
1020
+ cached.push(keyId.href);
1021
+ return Promise.resolve();
1022
+ }
1023
+ },
1024
+ documentLoader: async (url) => ({
1025
+ contextUrl: null,
1026
+ documentUrl: url,
1027
+ document: {
1028
+ "@context": "https://w3id.org/security/multikey/v1",
1029
+ id: verificationMethod.href,
1030
+ type: "Multikey",
1031
+ controller: "did:web:example.com",
1032
+ publicKeyMultibase: await exportMultibaseKey(ed25519PublicKey.publicKey)
1033
+ }
1034
+ })
1035
+ })).verified);
1036
+ assertEquals(cached, [verificationMethod.href]);
1037
+ });
1038
+ await t.step("rejects a portable key that names another controller", async () => {
1039
+ const verificationMethod = new URL("did:web:example.com#key");
1040
+ assertEquals((await verifyPortableObjectProof(await signPortableJsonLd({
1041
+ ...unsignedObject,
1042
+ id: "ap://did:web:example.com/objects/1"
1043
+ }, { verificationMethod }), {
1044
+ contextLoader: mockDocumentLoader,
1045
+ documentLoader: async (url) => ({
1046
+ contextUrl: null,
1047
+ documentUrl: url,
1048
+ document: {
1049
+ "@context": "https://w3id.org/security/multikey/v1",
1050
+ id: verificationMethod.href,
1051
+ type: "Multikey",
1052
+ controller: "did:web:attacker.example",
1053
+ publicKeyMultibase: await exportMultibaseKey(ed25519PublicKey.publicKey)
1054
+ }
1055
+ })
1056
+ })).verified, false);
1057
+ });
1008
1058
  await t.step("reports a missing proof on portable objects", async () => {
1009
1059
  assertEquals(await verifyPortableObjectProof(unsignedObject, options), {
1010
1060
  verified: false,
@@ -1699,5 +1749,74 @@ test("signObject() preserves FEP-22cd contexts through proof verification", asyn
1699
1749
  assertInstanceOf(verified, Note);
1700
1750
  assertEquals(verified.translations[0].language?.baseName, "ko");
1701
1751
  });
1752
+ test("verifyObject() rejects a key that claims a forged controller", async () => {
1753
+ const keyId = new URL("https://attacker.example/multikey");
1754
+ const impersonated = "https://example.com/person2";
1755
+ const attackerActor = "https://attacker.example/actor";
1756
+ const multikeyDocument = await ed25519Multikey.toJsonLd({ contextLoader: mockDocumentLoader });
1757
+ const options = {
1758
+ format: "compact",
1759
+ contextLoader: mockDocumentLoader,
1760
+ documentLoader: mockDocumentLoader,
1761
+ context: ["https://www.w3.org/ns/activitystreams", "https://w3id.org/security/data-integrity/v1"]
1762
+ };
1763
+ const signed = await signObject(new Create({
1764
+ id: new URL("https://attacker.example/activities/1"),
1765
+ actor: new URL(impersonated),
1766
+ object: new Note({
1767
+ id: new URL("https://attacker.example/notes/1"),
1768
+ attribution: new URL(impersonated),
1769
+ content: "Hello world"
1770
+ })
1771
+ }), ed25519PrivateKey, keyId, options);
1772
+ const jsonLd = await signed.toJsonLd(options);
1773
+ const serveKeyControlledBy = (controller) => (resource) => {
1774
+ if (resource === keyId.href) return Promise.resolve({
1775
+ contextUrl: null,
1776
+ documentUrl: resource,
1777
+ document: {
1778
+ ...multikeyDocument,
1779
+ id: keyId.href,
1780
+ controller
1781
+ }
1782
+ });
1783
+ if (resource === attackerActor) return Promise.resolve({
1784
+ contextUrl: null,
1785
+ documentUrl: resource,
1786
+ document: {
1787
+ "@context": [
1788
+ "https://www.w3.org/ns/activitystreams",
1789
+ "https://w3id.org/security/v1",
1790
+ "https://w3id.org/security/multikey/v1",
1791
+ "https://w3id.org/security/data-integrity/v1",
1792
+ "https://www.w3.org/ns/did/v1"
1793
+ ],
1794
+ id: resource,
1795
+ type: "Person",
1796
+ assertionMethod: [keyId.href]
1797
+ }
1798
+ });
1799
+ return mockDocumentLoader(resource);
1800
+ };
1801
+ assertEquals(await verifyObject(Create, jsonLd, {
1802
+ documentLoader: serveKeyControlledBy(impersonated),
1803
+ contextLoader: mockDocumentLoader
1804
+ }), null);
1805
+ const honestLoader = serveKeyControlledBy(attackerActor);
1806
+ let proof = null;
1807
+ for await (const p of signed.getProofs(options)) {
1808
+ proof = p;
1809
+ break;
1810
+ }
1811
+ assertInstanceOf(proof, DataIntegrityProof);
1812
+ assertInstanceOf(await verifyProof(jsonLd, proof, {
1813
+ documentLoader: honestLoader,
1814
+ contextLoader: mockDocumentLoader
1815
+ }), Multikey);
1816
+ assertEquals(await verifyObject(Create, jsonLd, {
1817
+ documentLoader: honestLoader,
1818
+ contextLoader: mockDocumentLoader
1819
+ }), null);
1820
+ });
1702
1821
  //#endregion
1703
1822
  export {};
@@ -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-OuCeQFny.mjs";
4
+ import { c as getNormalizationContextLoader } from "./ld-qup__MAW.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-CBlRfA-_.mjs";
9
- import { o as createFederation } from "../middleware-TeLjzoxn.mjs";
9
+ import { o as createFederation } from "../middleware-CzvJvPHK.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();
@@ -2021,7 +2021,8 @@ interface FederationOptions<TContextData> {
2021
2021
  */
2022
2022
  authenticatedDocumentLoaderFactory?: AuthenticatedDocumentLoaderFactory;
2023
2023
  /**
2024
- * Whether to allow fetching private network addresses in the document loader.
2024
+ * Whether to allow private network addresses in the document loader and
2025
+ * outbound activity delivery, including redirects.
2025
2026
  *
2026
2027
  * If turned on, {@link FederationOptions.documentLoader},
2027
2028
  * {@link FederationOptions.contextLoader}, and
@@ -1,6 +1,80 @@
1
1
  const { URLPattern } = require("urlpattern-polyfill");
2
2
  let _logtape_logtape = require("@logtape/logtape");
3
3
  let _fedify_vocab_runtime = require("@fedify/vocab-runtime");
4
+ //#region src/utils/body.ts
5
+ /** Decoded JSON body limit: 16 MiB. @internal */
6
+ const MAX_BODY_SIZE = 16 * 1024 * 1024;
7
+ /** A body exceeded the byte limit. @internal */
8
+ var BodyTooLargeError = class extends _fedify_vocab_runtime.FetchError {
9
+ /** Creates an error for a body exceeding the given limit. */
10
+ constructor(url, maxBytes) {
11
+ super(url, `Body exceeds the limit of ${maxBytes} bytes`);
12
+ this.name = "BodyTooLargeError";
13
+ }
14
+ };
15
+ /** Validates a finite, positive byte limit. @internal */
16
+ function validateBodySizeLimit(maxBytes) {
17
+ if (!Number.isSafeInteger(maxBytes) || maxBytes <= 0) throw new RangeError("The body size limit must be a positive safe integer.");
18
+ }
19
+ /**
20
+ * Reads UTF-8 text while limiting bytes received from the body stream.
21
+ * Fetch responses are already decompressed, so the stream count is authoritative.
22
+ * @param message The response or request to read.
23
+ * @param maxBytes The maximum number of decoded bytes to read.
24
+ * @param url The URL to include in errors and logs.
25
+ * @returns The decoded body text.
26
+ * @throws {BodyTooLargeError} If the body exceeds the limit.
27
+ * @internal
28
+ */
29
+ async function readBoundedText(message, maxBytes, url) {
30
+ validateBodySizeLimit(maxBytes);
31
+ const reader = message.body?.getReader();
32
+ const tooLarge = () => {
33
+ (0, _logtape_logtape.getLogger)([
34
+ "fedify",
35
+ "runtime",
36
+ "body"
37
+ ]).warn("Body from {url} exceeds the limit of {maxBytes} bytes.", {
38
+ url: url.toString(),
39
+ maxBytes
40
+ });
41
+ throw new BodyTooLargeError(url, maxBytes);
42
+ };
43
+ try {
44
+ const length = message.headers.get("Content-Length");
45
+ const encoding = message.headers.get("Content-Encoding");
46
+ if ((encoding == null || encoding.toLowerCase() === "identity") && length != null && /^\d+$/.test(length) && Number(length) > maxBytes) tooLarge();
47
+ if (reader == null) return "";
48
+ let size = 0;
49
+ const textReader = new ReadableStream({ async pull(controller) {
50
+ const { done, value } = await reader.read();
51
+ if (done) {
52
+ controller.close();
53
+ return;
54
+ }
55
+ size += value.byteLength;
56
+ if (size > maxBytes) tooLarge();
57
+ controller.enqueue(value);
58
+ } }, { highWaterMark: 0 }).pipeThrough(new TextDecoderStream()).getReader();
59
+ try {
60
+ const chunks = [];
61
+ while (true) {
62
+ const { done, value } = await textReader.read();
63
+ if (done) break;
64
+ chunks.push(value);
65
+ }
66
+ return chunks.join("");
67
+ } finally {
68
+ textReader.releaseLock();
69
+ }
70
+ } catch (error) {
71
+ if (reader != null) reader.cancel(error).catch(() => {});
72
+ throw error;
73
+ } finally {
74
+ reader?.releaseLock();
75
+ }
76
+ }
77
+ //#endregion
4
78
  //#region src/nodeinfo/client.ts
5
79
  const logger = (0, _logtape_logtape.getLogger)([
6
80
  "fedify",
@@ -81,7 +155,7 @@ async function getNodeInfo(url, options = {}) {
81
155
  });
82
156
  return;
83
157
  }
84
- const wellKnownRd = await wellKnownResponse.json();
158
+ const wellKnownRd = JSON.parse(await readBoundedText(wellKnownResponse, MAX_BODY_SIZE, wellKnownUrl));
85
159
  const link = wellKnownRd?.links?.find((link) => link != null && "rel" in link && (link.rel === "http://nodeinfo.diaspora.software/ns/schema/2.0" || link.rel === "http://nodeinfo.diaspora.software/ns/schema/2.1") && "href" in link && link.href != null);
86
160
  if (link == null || link.href == null) {
87
161
  logger.error("Failed to find a NodeInfo document link from {url}: {resourceDescriptor}", {
@@ -105,7 +179,7 @@ async function getNodeInfo(url, options = {}) {
105
179
  });
106
180
  return;
107
181
  }
108
- const data = await response.json();
182
+ const data = JSON.parse(await readBoundedText(response, MAX_BODY_SIZE, nodeInfoUrl));
109
183
  if (options.parse === "none") return data;
110
184
  return parseNodeInfo(data, { tryBestEffort: options.parse === "best-effort" }) ?? void 0;
111
185
  } catch (error) {
@@ -351,6 +425,18 @@ function nodeInfoToJson(nodeInfo) {
351
425
  };
352
426
  }
353
427
  //#endregion
428
+ Object.defineProperty(exports, "BodyTooLargeError", {
429
+ enumerable: true,
430
+ get: function() {
431
+ return BodyTooLargeError;
432
+ }
433
+ });
434
+ Object.defineProperty(exports, "MAX_BODY_SIZE", {
435
+ enumerable: true,
436
+ get: function() {
437
+ return MAX_BODY_SIZE;
438
+ }
439
+ });
354
440
  Object.defineProperty(exports, "getNodeInfo", {
355
441
  enumerable: true,
356
442
  get: function() {
@@ -369,3 +455,9 @@ Object.defineProperty(exports, "parseNodeInfo", {
369
455
  return parseNodeInfo;
370
456
  }
371
457
  });
458
+ Object.defineProperty(exports, "readBoundedText", {
459
+ enumerable: true,
460
+ get: function() {
461
+ return readBoundedText;
462
+ }
463
+ });
@@ -1,7 +1,81 @@
1
1
  import { Temporal } from "temporal-polyfill";
2
2
  import { URLPattern } from "urlpattern-polyfill";
3
3
  import { getLogger } from "@logtape/logtape";
4
- import { UrlError, getUserAgent, validatePublicUrl } from "@fedify/vocab-runtime";
4
+ import { FetchError, UrlError, getUserAgent, validatePublicUrl } from "@fedify/vocab-runtime";
5
+ //#region src/utils/body.ts
6
+ /** Decoded JSON body limit: 16 MiB. @internal */
7
+ const MAX_BODY_SIZE = 16 * 1024 * 1024;
8
+ /** A body exceeded the byte limit. @internal */
9
+ var BodyTooLargeError = class extends FetchError {
10
+ /** Creates an error for a body exceeding the given limit. */
11
+ constructor(url, maxBytes) {
12
+ super(url, `Body exceeds the limit of ${maxBytes} bytes`);
13
+ this.name = "BodyTooLargeError";
14
+ }
15
+ };
16
+ /** Validates a finite, positive byte limit. @internal */
17
+ function validateBodySizeLimit(maxBytes) {
18
+ if (!Number.isSafeInteger(maxBytes) || maxBytes <= 0) throw new RangeError("The body size limit must be a positive safe integer.");
19
+ }
20
+ /**
21
+ * Reads UTF-8 text while limiting bytes received from the body stream.
22
+ * Fetch responses are already decompressed, so the stream count is authoritative.
23
+ * @param message The response or request to read.
24
+ * @param maxBytes The maximum number of decoded bytes to read.
25
+ * @param url The URL to include in errors and logs.
26
+ * @returns The decoded body text.
27
+ * @throws {BodyTooLargeError} If the body exceeds the limit.
28
+ * @internal
29
+ */
30
+ async function readBoundedText(message, maxBytes, url) {
31
+ validateBodySizeLimit(maxBytes);
32
+ const reader = message.body?.getReader();
33
+ const tooLarge = () => {
34
+ getLogger([
35
+ "fedify",
36
+ "runtime",
37
+ "body"
38
+ ]).warn("Body from {url} exceeds the limit of {maxBytes} bytes.", {
39
+ url: url.toString(),
40
+ maxBytes
41
+ });
42
+ throw new BodyTooLargeError(url, maxBytes);
43
+ };
44
+ try {
45
+ const length = message.headers.get("Content-Length");
46
+ const encoding = message.headers.get("Content-Encoding");
47
+ if ((encoding == null || encoding.toLowerCase() === "identity") && length != null && /^\d+$/.test(length) && Number(length) > maxBytes) tooLarge();
48
+ if (reader == null) return "";
49
+ let size = 0;
50
+ const textReader = new ReadableStream({ async pull(controller) {
51
+ const { done, value } = await reader.read();
52
+ if (done) {
53
+ controller.close();
54
+ return;
55
+ }
56
+ size += value.byteLength;
57
+ if (size > maxBytes) tooLarge();
58
+ controller.enqueue(value);
59
+ } }, { highWaterMark: 0 }).pipeThrough(new TextDecoderStream()).getReader();
60
+ try {
61
+ const chunks = [];
62
+ while (true) {
63
+ const { done, value } = await textReader.read();
64
+ if (done) break;
65
+ chunks.push(value);
66
+ }
67
+ return chunks.join("");
68
+ } finally {
69
+ textReader.releaseLock();
70
+ }
71
+ } catch (error) {
72
+ if (reader != null) reader.cancel(error).catch(() => {});
73
+ throw error;
74
+ } finally {
75
+ reader?.releaseLock();
76
+ }
77
+ }
78
+ //#endregion
5
79
  //#region src/nodeinfo/client.ts
6
80
  const logger = getLogger([
7
81
  "fedify",
@@ -82,7 +156,7 @@ async function getNodeInfo(url, options = {}) {
82
156
  });
83
157
  return;
84
158
  }
85
- const wellKnownRd = await wellKnownResponse.json();
159
+ const wellKnownRd = JSON.parse(await readBoundedText(wellKnownResponse, MAX_BODY_SIZE, wellKnownUrl));
86
160
  const link = wellKnownRd?.links?.find((link) => link != null && "rel" in link && (link.rel === "http://nodeinfo.diaspora.software/ns/schema/2.0" || link.rel === "http://nodeinfo.diaspora.software/ns/schema/2.1") && "href" in link && link.href != null);
87
161
  if (link == null || link.href == null) {
88
162
  logger.error("Failed to find a NodeInfo document link from {url}: {resourceDescriptor}", {
@@ -106,7 +180,7 @@ async function getNodeInfo(url, options = {}) {
106
180
  });
107
181
  return;
108
182
  }
109
- const data = await response.json();
183
+ const data = JSON.parse(await readBoundedText(response, MAX_BODY_SIZE, nodeInfoUrl));
110
184
  if (options.parse === "none") return data;
111
185
  return parseNodeInfo(data, { tryBestEffort: options.parse === "best-effort" }) ?? void 0;
112
186
  } catch (error) {
@@ -352,4 +426,4 @@ function nodeInfoToJson(nodeInfo) {
352
426
  };
353
427
  }
354
428
  //#endregion
355
- export { getNodeInfo as n, parseNodeInfo as r, nodeInfoToJson as t };
429
+ export { MAX_BODY_SIZE as a, BodyTooLargeError as i, getNodeInfo as n, readBoundedText as o, parseNodeInfo as r, nodeInfoToJson as t };
@@ -5,11 +5,11 @@ import { t as esm_default } from "../esm-BHXXx4Wx.mjs";
5
5
  import { t as assertEquals } from "../assert_equals-CBlRfA-_.mjs";
6
6
  import "../std__assert-CxFT0MNt.mjs";
7
7
  import { t as assertRejects } from "../assert_rejects-wFbSPn1O.mjs";
8
- import { l as verifyRequest } from "../http-D7LX-BL9.mjs";
8
+ import { l as verifyRequest } from "../http-D9urSO2D.mjs";
9
9
  import { i as rsaPrivateKey2 } from "../keys-DIJNHW-Y.mjs";
10
- import { t as getAuthenticatedDocumentLoader } from "../docloader-dk_k3S8d.mjs";
10
+ import { t as getAuthenticatedDocumentLoader } from "../docloader-BbGSL0Xq.mjs";
11
11
  import { mockDocumentLoader, test } from "@fedify/fixture";
12
- import { UrlError } from "@fedify/vocab-runtime";
12
+ import { FetchError, UrlError } from "@fedify/vocab-runtime";
13
13
  //#region src/utils/docloader.test.ts
14
14
  test("getAuthenticatedDocumentLoader()", async (t) => {
15
15
  esm_default.spyGlobal();
@@ -136,5 +136,29 @@ test("getAuthenticatedDocumentLoader() cancellation", {
136
136
  });
137
137
  esm_default.hardReset();
138
138
  });
139
+ test("getAuthenticatedDocumentLoader() bounds JSON after redirects", async () => {
140
+ esm_default.mockGlobal();
141
+ let oversized = true;
142
+ try {
143
+ const url = "https://example.com/bounded";
144
+ esm_default.get(url, {
145
+ status: 302,
146
+ headers: { Location: `${url}/document` }
147
+ });
148
+ esm_default.get(`${url}/document`, () => new Response("{\"name\":\"hello\"}", { headers: {
149
+ "Content-Type": "application/activity+json",
150
+ ...oversized ? { "Content-Length": String(16777217) } : {}
151
+ } }));
152
+ const loader = getAuthenticatedDocumentLoader({
153
+ privateKey: rsaPrivateKey2,
154
+ keyId: new URL("https://example.com/key2")
155
+ }, { allowPrivateAddress: true });
156
+ await assertRejects(() => loader(url), FetchError);
157
+ oversized = false;
158
+ assertEquals((await loader(url)).document, { name: "hello" });
159
+ } finally {
160
+ esm_default.hardReset();
161
+ }
162
+ });
139
163
  //#endregion
140
164
  export {};
@@ -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-xRcC_xxP.mjs";
4
+ import { n as kvCache, t as MockKvStore } from "../kv-cache-lKyFn_Dn.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-DtsjH_bY.cjs");
3
+ const require_kv_cache = require("../kv-cache-DXT9vmIg.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-CLHkCwLm.cjs";
2
+ import { n as getAuthenticatedDocumentLoader, t as kvCache } from "../mod-CyiSjRkk.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-DypxPS9H.js";
2
+ import { n as getAuthenticatedDocumentLoader, t as kvCache } from "../mod-C0BYc0OU.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-tGNbvDCF.js";
3
+ import { n as getAuthenticatedDocumentLoader, t as kvCache } from "../kv-cache-DtmXhPXG.js";
4
4
  export { getAuthenticatedDocumentLoader, kvCache };