@fedify/fedify 2.2.0-pr.715.28 → 2.2.0-pr.731.34

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 (87) hide show
  1. package/dist/assert_strict_equals-Dmjbg-bA.mjs +41 -0
  2. package/dist/{builder-BfmqkrIE.mjs → builder-OFkoAQ85.mjs} +3 -3
  3. package/dist/compat/mod.d.cts +1 -1
  4. package/dist/compat/mod.d.ts +1 -1
  5. package/dist/compat/outgoing-jsonld.test.d.mts +2 -0
  6. package/dist/compat/outgoing-jsonld.test.mjs +189 -0
  7. package/dist/compat/public-audience.test.d.mts +2 -0
  8. package/dist/compat/public-audience.test.mjs +178 -0
  9. package/dist/compat/transformers.test.mjs +3 -3
  10. package/dist/{context-BGrYMSTk.d.ts → context-BzH2-ajs.d.ts} +22 -0
  11. package/dist/{context-CMUd4wy0.d.cts → context-DJGagtNd.d.cts} +22 -0
  12. package/dist/{deno-CjZGHpS5.mjs → deno-BYRjHaeb.mjs} +1 -1
  13. package/dist/{docloader-Du9_2PyA.mjs → docloader-CuVh02a1.mjs} +2 -2
  14. package/dist/federation/builder.test.mjs +3 -3
  15. package/dist/federation/collection.test.mjs +2 -2
  16. package/dist/federation/handler.test.mjs +8 -8
  17. package/dist/federation/idempotency.test.mjs +5 -5
  18. package/dist/federation/inbox.test.mjs +1 -1
  19. package/dist/federation/keycache.test.mjs +3 -3
  20. package/dist/federation/kv.test.mjs +2 -2
  21. package/dist/federation/middleware.test.mjs +160 -10
  22. package/dist/federation/mod.cjs +1 -1
  23. package/dist/federation/mod.d.cts +2 -2
  24. package/dist/federation/mod.d.ts +2 -2
  25. package/dist/federation/mod.js +1 -1
  26. package/dist/federation/mq.test.mjs +17 -10
  27. package/dist/federation/negotiation.test.mjs +3 -3
  28. package/dist/federation/retry.test.mjs +1 -1
  29. package/dist/federation/router.test.mjs +2 -2
  30. package/dist/federation/send.test.mjs +6 -6
  31. package/dist/federation/webfinger.test.mjs +3 -3
  32. package/dist/{http-9zjtsC0n.mjs → http-BrVfkREl.mjs} +3 -3
  33. package/dist/{http-Wm7tvhRa.cjs → http-H-4FzBb3.cjs} +1 -1
  34. package/dist/{http-C8puyZ4Z.js → http-pZce7PcA.js} +1 -1
  35. package/dist/{key-CMhIdO1-.mjs → key-cK-YTNKa.mjs} +1 -1
  36. package/dist/{kv-cache-BTq6qqgJ.js → kv-cache-CjweM0Am.js} +1 -1
  37. package/dist/{kv-cache-DyRjARFV.cjs → kv-cache-NgzTkTjv.cjs} +1 -1
  38. package/dist/{ld-BH-6muxq.mjs → ld-SXDkzuUo.mjs} +2 -2
  39. package/dist/{middleware-CO5RpqOP.mjs → middleware-AqjYcDmu.mjs} +1 -1
  40. package/dist/{middleware-DBgitte6.mjs → middleware-BMePykoo.mjs} +34 -22
  41. package/dist/{middleware-D3S_Ctwx.cjs → middleware-CXhCkBG4.cjs} +1 -1
  42. package/dist/{middleware-C_CRggCg.cjs → middleware-TZHT1gKk.cjs} +20 -9
  43. package/dist/{middleware-Da-j6H9U.js → middleware-YcQIUUHs.js} +19 -8
  44. package/dist/{mod-CJXfyw7v.d.ts → mod-2d12ffz3.d.ts} +1 -1
  45. package/dist/{mod-BcJHeuv1.d.cts → mod-D35TRn09.d.cts} +1 -1
  46. package/dist/mod.cjs +4 -4
  47. package/dist/mod.d.cts +2 -2
  48. package/dist/mod.d.ts +2 -2
  49. package/dist/mod.js +4 -4
  50. package/dist/nodeinfo/client.test.mjs +2 -2
  51. package/dist/nodeinfo/handler.test.mjs +3 -3
  52. package/dist/nodeinfo/types.test.mjs +2 -2
  53. package/dist/otel/exporter.test.mjs +2 -2
  54. package/dist/outgoing-jsonld-CNmZLixq.mjs +203 -0
  55. package/dist/{owner-CE5FwvNR.mjs → owner-Cudh-ej0.mjs} +2 -2
  56. package/dist/{proof-jHDv7IKD.js → proof-BJrEACyu.js} +425 -41
  57. package/dist/{proof-D-HuDCQe.cjs → proof-CFERN43j.cjs} +428 -38
  58. package/dist/{proof-Ds3T1-sE.mjs → proof-DC69vtxY.mjs} +38 -31
  59. package/dist/public-audience-DYFHzm_c.mjs +192 -0
  60. package/dist/{send-TicMA6nJ.mjs → send-BBRG1CKC.mjs} +2 -2
  61. package/dist/sig/accept.test.mjs +1 -1
  62. package/dist/sig/http.test.mjs +5 -5
  63. package/dist/sig/key.test.mjs +3 -3
  64. package/dist/sig/ld.test.mjs +4 -4
  65. package/dist/sig/mod.cjs +2 -2
  66. package/dist/sig/mod.js +2 -2
  67. package/dist/sig/owner.test.mjs +4 -4
  68. package/dist/sig/proof.test.mjs +78 -5
  69. package/dist/{std__assert-Duiq_YC9.mjs → std__assert-CRDpx_HF.mjs} +3 -38
  70. package/dist/testing/mod.d.mts +22 -0
  71. package/dist/utils/docloader.test.mjs +4 -4
  72. package/dist/utils/kv-cache.test.mjs +1 -1
  73. package/dist/utils/mod.cjs +1 -1
  74. package/dist/utils/mod.js +1 -1
  75. package/package.json +5 -5
  76. /package/dist/{accept-Dd__NiUL.mjs → accept-CPkZzmGN.mjs} +0 -0
  77. /package/dist/{activity-listener-Ck3JZ_hR.mjs → activity-listener-ell7W1s9.mjs} +0 -0
  78. /package/dist/{assert-ddO5KLpe.mjs → assert-DikXweDx.mjs} +0 -0
  79. /package/dist/{client-DEpOVgY1.mjs → client-D_1QpnWt.mjs} +0 -0
  80. /package/dist/{collection-BD6-SZ6O.mjs → collection-D-HqUuA2.mjs} +0 -0
  81. /package/dist/{keycache-CCSwkQcY.mjs → keycache-EGATflN-.mjs} +0 -0
  82. /package/dist/{keys-BAK-tUlf.mjs → keys-DGu1NFwu.mjs} +0 -0
  83. /package/dist/{kv-cache-B01V7s3h.mjs → kv-cache-U__xU4qR.mjs} +0 -0
  84. /package/dist/{kv-tL2TOE9X.mjs → kv-rV3vodCc.mjs} +0 -0
  85. /package/dist/{negotiation-DnsfFF8I.mjs → negotiation-SQvQgUqe.mjs} +0 -0
  86. /package/dist/{retry-B_E3V_Dx.mjs → retry-bMXBL97A.mjs} +0 -0
  87. /package/dist/{types-DCP0WLdt.mjs → types-J53Kw7so.mjs} +0 -0
@@ -1,8 +1,10 @@
1
1
  import { Temporal } from "@js-temporal/polyfill";
2
2
  import "urlpattern-polyfill";
3
3
  globalThis.addEventListener = () => {};
4
- import { n as version, t as name } from "./deno-CjZGHpS5.mjs";
5
- import { n as fetchKey, o as validateCryptoKey } from "./key-CMhIdO1-.mjs";
4
+ import { n as version, t as name } from "./deno-BYRjHaeb.mjs";
5
+ import { n as fetchKey, o as validateCryptoKey } from "./key-cK-YTNKa.mjs";
6
+ import { n as preloadedOnlyDocumentLoader } from "./public-audience-DYFHzm_c.mjs";
7
+ import { r as normalizeOutgoingActivityJsonLd } from "./outgoing-jsonld-CNmZLixq.mjs";
6
8
  import { Activity, DataIntegrityProof, Multikey, getTypeId } from "@fedify/vocab";
7
9
  import { SpanStatusCode, trace } from "@opentelemetry/api";
8
10
  import { getLogger } from "@logtape/logtape";
@@ -56,11 +58,12 @@ function hasProofLike(jsonLd) {
56
58
  async function createProof(object, privateKey, keyId, { contextLoader, context, created } = {}) {
57
59
  validateCryptoKey(privateKey, "private");
58
60
  if (privateKey.algorithm.name !== "Ed25519") throw new TypeError("Unsupported algorithm: " + privateKey.algorithm.name);
59
- const compactMsg = await object.clone({ proofs: [] }).toJsonLd({
61
+ let compactMsg = await object.clone({ proofs: [] }).toJsonLd({
60
62
  format: "compact",
61
63
  contextLoader,
62
64
  context
63
65
  });
66
+ compactMsg = await normalizeOutgoingActivityJsonLd(compactMsg, contextLoader);
64
67
  const msgCanon = serialize(compactMsg);
65
68
  const encoder = new TextEncoder();
66
69
  const msgBytes = encoder.encode(msgCanon);
@@ -155,27 +158,22 @@ async function verifyProof(jsonLd, proof, options = {}) {
155
158
  });
156
159
  }
157
160
  async function verifyProofInternal(jsonLd, proof, options) {
158
- if (typeof jsonLd !== "object" || proof.cryptosuite !== "eddsa-jcs-2022" || proof.verificationMethodId == null || proof.proofPurpose !== "assertionMethod" || proof.proofValue == null || proof.created == null) return null;
161
+ if (typeof jsonLd !== "object" || jsonLd == null || Array.isArray(jsonLd) || proof.cryptosuite !== "eddsa-jcs-2022" || proof.verificationMethodId == null || proof.proofPurpose !== "assertionMethod" || proof.proofValue == null || proof.created == null) return null;
159
162
  const publicKeyPromise = fetchKey(proof.verificationMethodId, Multikey, options);
160
- const proofCanon = serialize({
163
+ const proofConfig = {
161
164
  "@context": jsonLd["@context"],
162
165
  type: "DataIntegrityProof",
163
166
  cryptosuite: proof.cryptosuite,
164
167
  verificationMethod: proof.verificationMethodId.href,
165
168
  proofPurpose: proof.proofPurpose,
166
169
  created: proof.created.toString()
167
- });
170
+ };
168
171
  const encoder = new TextEncoder();
169
- const proofBytes = encoder.encode(proofCanon);
172
+ const proofBytes = encoder.encode(serialize(proofConfig));
170
173
  const proofDigest = await crypto.subtle.digest("SHA-256", proofBytes);
171
174
  const msg = { ...jsonLd };
172
175
  if ("proof" in msg) delete msg.proof;
173
- const msgCanon = serialize(msg);
174
- const msgBytes = encoder.encode(msgCanon);
175
- const msgDigest = await crypto.subtle.digest("SHA-256", msgBytes);
176
- const digest = new Uint8Array(proofDigest.byteLength + msgDigest.byteLength);
177
- digest.set(new Uint8Array(proofDigest), 0);
178
- digest.set(new Uint8Array(msgDigest), proofDigest.byteLength);
176
+ if ("https://w3id.org/security#proof" in msg) delete msg["https://w3id.org/security#proof"];
179
177
  let fetchedKey;
180
178
  try {
181
179
  fetchedKey = await publicKeyPromise;
@@ -204,7 +202,7 @@ async function verifyProofInternal(jsonLd, proof, options) {
204
202
  return await verifyProof(jsonLd, proof, {
205
203
  ...options,
206
204
  keyCache: {
207
- get: () => Promise.resolve(null),
205
+ get: () => Promise.resolve(void 0),
208
206
  set: async (keyId, key) => await options.keyCache?.set(keyId, key)
209
207
  }
210
208
  });
@@ -215,27 +213,36 @@ async function verifyProofInternal(jsonLd, proof, options) {
215
213
  });
216
214
  return null;
217
215
  }
218
- if (!await crypto.subtle.verify("Ed25519", publicKey.publicKey, proof.proofValue.slice(), digest)) {
219
- if (fetchedKey.cached) {
220
- logger.debug("Failed to verify the proof with the cached key {keyId}; retrying with the freshly fetched key...", {
221
- keyId: proof.verificationMethodId.href,
222
- proof
223
- });
224
- return await verifyProof(jsonLd, proof, {
225
- ...options,
226
- keyCache: {
227
- get: () => Promise.resolve(void 0),
228
- set: async (keyId, key) => await options.keyCache?.set(keyId, key)
229
- }
230
- });
231
- }
232
- logger.debug("Failed to verify the proof with the fetched key {keyId}:\n{proof}", {
216
+ const digest = new Uint8Array(proofDigest.byteLength + 32);
217
+ digest.set(new Uint8Array(proofDigest), 0);
218
+ const proofValue = proof.proofValue;
219
+ const verifyCandidate = async (candidate) => {
220
+ const msgBytes = encoder.encode(serialize(candidate));
221
+ const msgDigest = await crypto.subtle.digest("SHA-256", msgBytes);
222
+ digest.set(new Uint8Array(msgDigest), proofDigest.byteLength);
223
+ return await crypto.subtle.verify("Ed25519", publicKey.publicKey, proofValue.slice(), digest);
224
+ };
225
+ if (await verifyCandidate(msg)) return publicKey;
226
+ const normalized = await normalizeOutgoingActivityJsonLd(msg, preloadedOnlyDocumentLoader);
227
+ if (normalized !== msg && await verifyCandidate(normalized)) return publicKey;
228
+ if (fetchedKey.cached) {
229
+ logger.debug("Failed to verify the proof with the cached key {keyId}; retrying with the freshly fetched key...", {
233
230
  keyId: proof.verificationMethodId.href,
234
231
  proof
235
232
  });
236
- return null;
233
+ return await verifyProof(jsonLd, proof, {
234
+ ...options,
235
+ keyCache: {
236
+ get: () => Promise.resolve(void 0),
237
+ set: async (keyId, key) => await options.keyCache?.set(keyId, key)
238
+ }
239
+ });
237
240
  }
238
- return publicKey;
241
+ logger.debug("Failed to verify the proof with the fetched key {keyId}:\n{proof}", {
242
+ keyId: proof.verificationMethodId.href,
243
+ proof
244
+ });
245
+ return null;
239
246
  }
240
247
  /**
241
248
  * Verifies the given object. It will verify all the proofs in the object,
@@ -0,0 +1,192 @@
1
+ import "@js-temporal/polyfill";
2
+ import "urlpattern-polyfill";
3
+ globalThis.addEventListener = () => {};
4
+ import { PUBLIC_COLLECTION } from "@fedify/vocab";
5
+ import { preloadedContexts } from "@fedify/vocab-runtime";
6
+ import { getLogger } from "@logtape/logtape";
7
+ import jsonld from "@fedify/vocab-runtime/jsonld";
8
+ //#region src/compat/preloaded-context-loader.ts
9
+ /**
10
+ * A restricted JSON-LD document loader that resolves only contexts bundled
11
+ * with Fedify.
12
+ *
13
+ * This is intentionally narrower than `getDocumentLoader()`: normalization
14
+ * helpers are also reached from verification paths that operate on inbound,
15
+ * attacker-controlled JSON-LD, so the default fallback must never fetch
16
+ * attacker-supplied context URLs.
17
+ */
18
+ const preloadedOnlyDocumentLoader = (url) => {
19
+ if (Object.hasOwn(preloadedContexts, url)) return Promise.resolve({
20
+ contextUrl: null,
21
+ documentUrl: url,
22
+ document: preloadedContexts[url]
23
+ });
24
+ return Promise.reject(/* @__PURE__ */ new Error("Refusing to fetch a non-preloaded JSON-LD context: " + url));
25
+ };
26
+ //#endregion
27
+ //#region src/compat/public-audience.ts
28
+ const logger = getLogger([
29
+ "fedify",
30
+ "compat",
31
+ "public-audience"
32
+ ]);
33
+ const PUBLIC_ADDRESSING_FIELDS = new Set([
34
+ "to",
35
+ "cc",
36
+ "bto",
37
+ "bcc",
38
+ "audience"
39
+ ]);
40
+ const AS_CONTEXT_URL = "https://www.w3.org/ns/activitystreams";
41
+ const MAX_TRAVERSAL_DEPTH = 64;
42
+ const KNOWN_SAFE_CONTEXT_URLS = new Set(Object.keys(preloadedContexts));
43
+ function hasPublicCurieInAddressing(value, parentKey, depth = 0) {
44
+ if (typeof value === "string") return parentKey != null && PUBLIC_ADDRESSING_FIELDS.has(parentKey) && (value === "as:Public" || value === "Public");
45
+ if (depth >= MAX_TRAVERSAL_DEPTH) return false;
46
+ if (Array.isArray(value)) return value.some((item) => hasPublicCurieInAddressing(item, parentKey, depth + 1));
47
+ if (typeof value !== "object" || value == null) return false;
48
+ const record = value;
49
+ for (const key of Object.keys(record)) {
50
+ if (key === "@context") continue;
51
+ if (hasPublicCurieInAddressing(record[key], key, depth + 1)) return true;
52
+ }
53
+ return false;
54
+ }
55
+ function rewritePublicAudience(value, parentKey, depth = 0) {
56
+ if (typeof value === "string" && parentKey != null && PUBLIC_ADDRESSING_FIELDS.has(parentKey) && (value === "as:Public" || value === "Public")) return PUBLIC_COLLECTION.href;
57
+ if (depth >= MAX_TRAVERSAL_DEPTH) return value;
58
+ if (Array.isArray(value)) {
59
+ let changed = false;
60
+ const mapped = value.map((item) => {
61
+ const rewritten = rewritePublicAudience(item, parentKey, depth + 1);
62
+ if (rewritten !== item) changed = true;
63
+ return rewritten;
64
+ });
65
+ return changed ? mapped : value;
66
+ }
67
+ if (typeof value !== "object" || value == null) return value;
68
+ const record = value;
69
+ let changed = false;
70
+ const normalized = Object.create(null);
71
+ for (const key of Object.keys(record)) {
72
+ const rewritten = key === "@context" ? record[key] : rewritePublicAudience(record[key], key, depth + 1);
73
+ if (rewritten !== record[key]) changed = true;
74
+ normalized[key] = rewritten;
75
+ }
76
+ return changed ? normalized : value;
77
+ }
78
+ /**
79
+ * Reports whether `value` carries an `@context` property anywhere inside
80
+ * its subtree (not counting the value itself). A nested `@context` can
81
+ * introduce a local term-definition scope that redefines `as:` or `Public`
82
+ * even when the top-level `@context` is safe, so the fast path must defer
83
+ * to the URDNA2015 equivalence check whenever one is present.
84
+ */
85
+ function hasNestedContext(value, depth = 0) {
86
+ if (depth >= MAX_TRAVERSAL_DEPTH) return true;
87
+ if (Array.isArray(value)) return value.some((item) => hasNestedContext(item, depth + 1));
88
+ if (typeof value !== "object" || value == null) return false;
89
+ const record = value;
90
+ for (const key of Object.keys(record)) {
91
+ if (key === "@context") return true;
92
+ if (hasNestedContext(record[key], depth + 1)) return true;
93
+ }
94
+ return false;
95
+ }
96
+ /**
97
+ * Checks whether the `@context` of a JSON-LD document is guaranteed not
98
+ * to redefine the `as:` prefix or the bare `Public` term. Only documents
99
+ * whose `@context` is a string, or an array of strings, drawn from Fedify's
100
+ * preloaded context set AND including the ActivityStreams URL qualify,
101
+ * AND no nested subtree carries its own `@context` that might redefine
102
+ * those terms within a local scope. When all of that holds the rewrite
103
+ * is provably semantics-preserving and the URDNA2015 equivalence check
104
+ * can be skipped. Any other shape (unknown external URLs, inline
105
+ * objects at the top level, nested `@context` blocks) is treated as
106
+ * potentially unsafe.
107
+ */
108
+ function hasKnownSafeContext(jsonLd) {
109
+ if (typeof jsonLd !== "object" || jsonLd == null) return false;
110
+ const record = jsonLd;
111
+ if (!Object.hasOwn(record, "@context")) return false;
112
+ const ctx = record["@context"];
113
+ const entries = typeof ctx === "string" ? [ctx] : Array.isArray(ctx) ? ctx : null;
114
+ if (entries == null || entries.length === 0) return false;
115
+ let hasAs = false;
116
+ for (const entry of entries) {
117
+ if (typeof entry !== "string") return false;
118
+ if (!KNOWN_SAFE_CONTEXT_URLS.has(entry)) return false;
119
+ if (entry === AS_CONTEXT_URL) hasAs = true;
120
+ }
121
+ if (!hasAs) return false;
122
+ for (const key of Object.keys(record)) {
123
+ if (key === "@context") continue;
124
+ if (hasNestedContext(record[key])) return false;
125
+ }
126
+ return true;
127
+ }
128
+ /**
129
+ * Rewrites the compact `as:Public` / `Public` CURIE appearing in activity
130
+ * addressing fields (`to`, `cc`, `bto`, `bcc`, `audience`) to the fully
131
+ * expanded `https://www.w3.org/ns/activitystreams#Public` URI.
132
+ *
133
+ * Several ActivityPub implementations, Lemmy among them, match these
134
+ * fields as plain URLs without running JSON-LD expansion, and silently
135
+ * drop activities whose public addressing appears in CURIE form. This
136
+ * helper works around that gap.
137
+ *
138
+ * For documents whose `@context` is drawn entirely from Fedify's
139
+ * preloaded context set and includes the ActivityStreams URL, the
140
+ * rewrite is applied directly: the content of every preloaded non-AS
141
+ * context is known not to redefine the `as:` prefix or the bare `Public`
142
+ * term, so the semantics are preserved by construction. Any other
143
+ * shape (an inline object, an unknown external URL, and so on) is
144
+ * treated as potentially unsafe and gated on a JSON-LD equivalence
145
+ * check; both forms are canonicalized with URDNA2015 and the resulting
146
+ * N-Quads are compared. When they differ, the original document is
147
+ * returned unchanged. Canonicalization failures also fall back to the
148
+ * original document.
149
+ *
150
+ * When no `contextLoader` is supplied the helper falls back to an
151
+ * internal loader that resolves only the URLs in Fedify's
152
+ * preloaded-contexts set and rejects every other URL without issuing a
153
+ * network request. That behaviour is deliberately narrower than
154
+ * `@fedify/vocab-runtime`'s `getDocumentLoader()`, which after its
155
+ * `validatePublicUrl` check will happily fetch non-preloaded URLs: the
156
+ * helper is reached from verification paths (`verifyProof()` /
157
+ * `verifyObject()`) that operate on inbound, potentially adversarial
158
+ * JSON-LD, and a default loader that fetches attacker-supplied
159
+ * `@context` URLs on the caller's behalf would be an SSRF vector.
160
+ * Canonicalization failures against the restricted loader fall back to
161
+ * the original document, same as any other canonicalization error.
162
+ * Callers that genuinely need the remote-fetch loader (for example
163
+ * applications that sign local JSON-LD against a custom vocabulary)
164
+ * should pass a `contextLoader` explicitly.
165
+ *
166
+ * Must be called before any signing step that canonicalizes the
167
+ * compact form byte-for-byte (for example, Object Integrity Proofs
168
+ * using the `eddsa-jcs-2022` cryptosuite), so the signed payload
169
+ * matches what is sent on the wire.
170
+ */
171
+ async function normalizePublicAudience(jsonLd, contextLoader) {
172
+ if (!hasPublicCurieInAddressing(jsonLd)) return jsonLd;
173
+ const normalized = rewritePublicAudience(jsonLd);
174
+ if (hasKnownSafeContext(jsonLd)) return normalized;
175
+ const loader = contextLoader ?? preloadedOnlyDocumentLoader;
176
+ try {
177
+ const [before, after] = await Promise.all([jsonld.canonize(jsonLd, {
178
+ format: "application/n-quads",
179
+ documentLoader: loader
180
+ }), jsonld.canonize(normalized, {
181
+ format: "application/n-quads",
182
+ documentLoader: loader
183
+ })]);
184
+ if (before === after) return normalized;
185
+ logger.warn("Expanding the public audience CURIE to its full URI would change the canonical form of the activity; sending the activity as is. This usually means the active JSON-LD context redefines the `as:` prefix or the bare `Public` term.");
186
+ } catch (error) {
187
+ logger.debug("Failed to verify public audience normalization equivalence via JSON-LD canonicalization; sending the activity as is.\n{error}", { error });
188
+ }
189
+ return jsonLd;
190
+ }
191
+ //#endregion
192
+ export { preloadedOnlyDocumentLoader as n, normalizePublicAudience as t };
@@ -1,8 +1,8 @@
1
1
  import "@js-temporal/polyfill";
2
2
  import "urlpattern-polyfill";
3
3
  globalThis.addEventListener = () => {};
4
- import { n as version, t as name } from "./deno-CjZGHpS5.mjs";
5
- import { n as doubleKnock } from "./http-9zjtsC0n.mjs";
4
+ import { n as version, t as name } from "./deno-BYRjHaeb.mjs";
5
+ import { n as doubleKnock } from "./http-BrVfkREl.mjs";
6
6
  import { SpanKind, SpanStatusCode, trace } from "@opentelemetry/api";
7
7
  import { getLogger } from "@logtape/logtape";
8
8
  //#region src/federation/send.ts
@@ -1,7 +1,7 @@
1
1
  import "@js-temporal/polyfill";
2
2
  import "urlpattern-polyfill";
3
3
  globalThis.addEventListener = () => {};
4
- import { i as validateAcceptSignature, n as fulfillAcceptSignature, r as parseAcceptSignature, t as formatAcceptSignature } from "../accept-Dd__NiUL.mjs";
4
+ import { i as validateAcceptSignature, n as fulfillAcceptSignature, r as parseAcceptSignature, t as formatAcceptSignature } from "../accept-CPkZzmGN.mjs";
5
5
  import { test } from "@fedify/fixture";
6
6
  import { deepStrictEqual, strictEqual } from "node:assert/strict";
7
7
  //#region src/sig/accept.test.ts
@@ -3,13 +3,13 @@ import "urlpattern-polyfill";
3
3
  globalThis.addEventListener = () => {};
4
4
  import { t as esm_default } from "../esm-DVILvP5e.mjs";
5
5
  import { t as assertEquals } from "../assert_equals-Ew3jOFa3.mjs";
6
- import { a as assertExists, t as assertStringIncludes } from "../std__assert-Duiq_YC9.mjs";
6
+ import { i as assertExists, t as assertStringIncludes } from "../std__assert-CRDpx_HF.mjs";
7
7
  import { n as assertFalse, t as assertRejects } from "../assert_rejects-B-qJtC9Z.mjs";
8
8
  import { t as assertThrows } from "../assert_throws-4NwKEy2q.mjs";
9
- import { t as assert } from "../assert-ddO5KLpe.mjs";
10
- import { t as exportJwk } from "../key-CMhIdO1-.mjs";
11
- import { a as parseRfc9421Signature, c as timingSafeEqual, i as formatRfc9421SignatureParameters, l as verifyRequest, n as doubleKnock, o as parseRfc9421SignatureInput, r as formatRfc9421Signature, s as signRequest, t as createRfc9421SignatureBase, u as verifyRequestDetailed } from "../http-9zjtsC0n.mjs";
12
- import { i as rsaPrivateKey2, l as rsaPublicKey5, o as rsaPublicKey1, s as rsaPublicKey2 } from "../keys-BAK-tUlf.mjs";
9
+ import { t as assert } from "../assert-DikXweDx.mjs";
10
+ import { t as exportJwk } from "../key-cK-YTNKa.mjs";
11
+ import { a as parseRfc9421Signature, c as timingSafeEqual, i as formatRfc9421SignatureParameters, l as verifyRequest, n as doubleKnock, o as parseRfc9421SignatureInput, r as formatRfc9421Signature, s as signRequest, t as createRfc9421SignatureBase, u as verifyRequestDetailed } from "../http-BrVfkREl.mjs";
12
+ import { i as rsaPrivateKey2, l as rsaPublicKey5, o as rsaPublicKey1, s as rsaPublicKey2 } from "../keys-DGu1NFwu.mjs";
13
13
  import { createTestTracerProvider, mockDocumentLoader, test } from "@fedify/fixture";
14
14
  import { FetchError, exportSpki } from "@fedify/vocab-runtime";
15
15
  import { encodeBase64 } from "byte-encodings/base64";
@@ -2,11 +2,11 @@ import "@js-temporal/polyfill";
2
2
  import "urlpattern-polyfill";
3
3
  globalThis.addEventListener = () => {};
4
4
  import { t as assertEquals } from "../assert_equals-Ew3jOFa3.mjs";
5
- import "../std__assert-Duiq_YC9.mjs";
5
+ import "../std__assert-CRDpx_HF.mjs";
6
6
  import { t as assertRejects } from "../assert_rejects-B-qJtC9Z.mjs";
7
7
  import { t as assertThrows } from "../assert_throws-4NwKEy2q.mjs";
8
- import { a as importJwk, i as generateCryptoKeyPair, n as fetchKey, o as validateCryptoKey, r as fetchKeyDetailed, t as exportJwk } from "../key-CMhIdO1-.mjs";
9
- import { c as rsaPublicKey3, i as rsaPrivateKey2, o as rsaPublicKey1, s as rsaPublicKey2, t as ed25519Multikey } from "../keys-BAK-tUlf.mjs";
8
+ import { a as importJwk, i as generateCryptoKeyPair, n as fetchKey, o as validateCryptoKey, r as fetchKeyDetailed, t as exportJwk } from "../key-cK-YTNKa.mjs";
9
+ import { c as rsaPublicKey3, i as rsaPrivateKey2, o as rsaPublicKey1, s as rsaPublicKey2, t as ed25519Multikey } from "../keys-DGu1NFwu.mjs";
10
10
  import { createTestTracerProvider, mockDocumentLoader, test } from "@fedify/fixture";
11
11
  import { CryptographicKey, Multikey } from "@fedify/vocab";
12
12
  import { FetchError } from "@fedify/vocab-runtime";
@@ -4,10 +4,10 @@ globalThis.addEventListener = () => {};
4
4
  import { t as assertEquals } from "../assert_equals-Ew3jOFa3.mjs";
5
5
  import { n as assertFalse, t as assertRejects } from "../assert_rejects-B-qJtC9Z.mjs";
6
6
  import { t as assertThrows } from "../assert_throws-4NwKEy2q.mjs";
7
- import { t as assert } from "../assert-ddO5KLpe.mjs";
8
- import { i as generateCryptoKeyPair } from "../key-CMhIdO1-.mjs";
9
- import { a as rsaPrivateKey3, c as rsaPublicKey3, i as rsaPrivateKey2, n as ed25519PrivateKey, s as rsaPublicKey2, t as ed25519Multikey } from "../keys-BAK-tUlf.mjs";
10
- import { a as signJsonLd, i as hasSignatureLike, n as createSignature, o as verifyJsonLd, r as detachSignature, s as verifySignature, t as attachSignature } from "../ld-BH-6muxq.mjs";
7
+ import { t as assert } from "../assert-DikXweDx.mjs";
8
+ import { i as generateCryptoKeyPair } from "../key-cK-YTNKa.mjs";
9
+ import { a as rsaPrivateKey3, c as rsaPublicKey3, i as rsaPrivateKey2, n as ed25519PrivateKey, s as rsaPublicKey2, t as ed25519Multikey } from "../keys-DGu1NFwu.mjs";
10
+ import { a as signJsonLd, i as hasSignatureLike, n as createSignature, o as verifyJsonLd, r as detachSignature, s as verifySignature, t as attachSignature } from "../ld-SXDkzuUo.mjs";
11
11
  import { mockDocumentLoader, test } from "@fedify/fixture";
12
12
  import { CryptographicKey } from "@fedify/vocab";
13
13
  import { encodeBase64 } from "byte-encodings/base64";
package/dist/sig/mod.cjs CHANGED
@@ -1,8 +1,8 @@
1
1
  const { Temporal } = require("@js-temporal/polyfill");
2
2
  const { URLPattern } = require("urlpattern-polyfill");
3
3
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
4
- const require_http = require("../http-Wm7tvhRa.cjs");
5
- const require_proof = require("../proof-D-HuDCQe.cjs");
4
+ const require_http = require("../http-H-4FzBb3.cjs");
5
+ const require_proof = require("../proof-CFERN43j.cjs");
6
6
  exports.attachSignature = require_proof.attachSignature;
7
7
  exports.createProof = require_proof.createProof;
8
8
  exports.createSignature = require_proof.createSignature;
package/dist/sig/mod.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import "@js-temporal/polyfill";
2
2
  import "urlpattern-polyfill";
3
- import { a as verifyRequestDetailed, c as fetchKeyDetailed, f as formatAcceptSignature, h as validateAcceptSignature, i as verifyRequest, l as generateCryptoKeyPair, m as parseAcceptSignature, o as exportJwk, p as fulfillAcceptSignature, r as signRequest, s as fetchKey, u as importJwk } from "../http-C8puyZ4Z.js";
4
- import { a as verifyProof, c as attachSignature, d as hasSignatureLike, f as signJsonLd, i as verifyObject, l as createSignature, m as verifySignature, n as hasProofLike, o as doesActorOwnKey, p as verifyJsonLd, r as signObject, s as getKeyOwner, t as createProof, u as detachSignature } from "../proof-jHDv7IKD.js";
3
+ import { a as verifyRequestDetailed, c as fetchKeyDetailed, f as formatAcceptSignature, h as validateAcceptSignature, i as verifyRequest, l as generateCryptoKeyPair, m as parseAcceptSignature, o as exportJwk, p as fulfillAcceptSignature, r as signRequest, s as fetchKey, u as importJwk } from "../http-pZce7PcA.js";
4
+ import { a as verifyProof, c as getKeyOwner, d as detachSignature, f as hasSignatureLike, h as verifySignature, i as verifyObject, l as attachSignature, m as verifyJsonLd, n as hasProofLike, p as signJsonLd, r as signObject, s as doesActorOwnKey, t as createProof, u as createSignature } from "../proof-BJrEACyu.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, verifyProof, verifyRequest, verifyRequestDetailed, verifySignature };
@@ -2,11 +2,11 @@ import "@js-temporal/polyfill";
2
2
  import "urlpattern-polyfill";
3
3
  globalThis.addEventListener = () => {};
4
4
  import { t as assertEquals } from "../assert_equals-Ew3jOFa3.mjs";
5
- import "../std__assert-Duiq_YC9.mjs";
5
+ import "../std__assert-CRDpx_HF.mjs";
6
6
  import { n as assertFalse } from "../assert_rejects-B-qJtC9Z.mjs";
7
- import { t as assert } from "../assert-ddO5KLpe.mjs";
8
- import { o as rsaPublicKey1, s as rsaPublicKey2 } from "../keys-BAK-tUlf.mjs";
9
- import { n as getKeyOwner, t as doesActorOwnKey } from "../owner-CE5FwvNR.mjs";
7
+ import { t as assert } from "../assert-DikXweDx.mjs";
8
+ import { o as rsaPublicKey1, s as rsaPublicKey2 } from "../keys-DGu1NFwu.mjs";
9
+ import { n as getKeyOwner, t as doesActorOwnKey } from "../owner-Cudh-ej0.mjs";
10
10
  import { createTestTracerProvider, mockDocumentLoader, test } from "@fedify/fixture";
11
11
  import { Create, CryptographicKey, lookupObject } from "@fedify/vocab";
12
12
  //#region src/sig/owner.test.ts
@@ -2,14 +2,15 @@ import { Temporal } from "@js-temporal/polyfill";
2
2
  import "urlpattern-polyfill";
3
3
  globalThis.addEventListener = () => {};
4
4
  import { t as assertEquals } from "../assert_equals-Ew3jOFa3.mjs";
5
- import "../std__assert-Duiq_YC9.mjs";
5
+ import "../std__assert-CRDpx_HF.mjs";
6
6
  import { n as assertFalse, t as assertRejects } from "../assert_rejects-B-qJtC9Z.mjs";
7
7
  import { t as assertInstanceOf } from "../assert_instance_of-C4Ri6VuN.mjs";
8
- import { t as assert } from "../assert-ddO5KLpe.mjs";
9
- import { i as rsaPrivateKey2, n as ed25519PrivateKey, r as ed25519PublicKey, s as rsaPublicKey2, t as ed25519Multikey } from "../keys-BAK-tUlf.mjs";
10
- import { a as verifyProof, i as verifyObject, n as hasProofLike, r as signObject, t as createProof } from "../proof-Ds3T1-sE.mjs";
8
+ import { t as assert } from "../assert-DikXweDx.mjs";
9
+ import { i as rsaPrivateKey2, n as ed25519PrivateKey, r as ed25519PublicKey, s as rsaPublicKey2, t as ed25519Multikey } from "../keys-DGu1NFwu.mjs";
10
+ import { r as normalizeOutgoingActivityJsonLd } from "../outgoing-jsonld-CNmZLixq.mjs";
11
+ import { a as verifyProof, i as verifyObject, n as hasProofLike, r as signObject, t as createProof } from "../proof-DC69vtxY.mjs";
11
12
  import { mockDocumentLoader, test } from "@fedify/fixture";
12
- import { Create, DataIntegrityProof, Multikey, Note, Place } from "@fedify/vocab";
13
+ import { Create, DataIntegrityProof, Document, Multikey, Note, PUBLIC_COLLECTION, Place } from "@fedify/vocab";
13
14
  import { decodeMultibase, importMultibaseKey } from "@fedify/vocab-runtime";
14
15
  import { decodeHex } from "byte-encodings/hex";
15
16
  //#region src/sig/proof.test.ts
@@ -151,6 +152,47 @@ test("signObject()", async () => {
151
152
  created,
152
153
  contextLoader: mockDocumentLoader
153
154
  }), TypeError, "Unsupported algorithm");
155
+ const signed = await signObject(new Create({
156
+ id: new URL("https://server.example/activities/2"),
157
+ actor: new URL("https://server.example/users/alice"),
158
+ object: new Note({
159
+ id: new URL("https://server.example/objects/2"),
160
+ attribution: new URL("https://server.example/users/alice"),
161
+ content: "Hello public",
162
+ attachments: [new Document({
163
+ mediaType: "image/png",
164
+ url: new URL("https://server.example/objects/2/image.png")
165
+ })]
166
+ }),
167
+ tos: [PUBLIC_COLLECTION]
168
+ }), fep8b32TestVectorPrivateKey, fep8b32TestVectorKeyId, {
169
+ ...options,
170
+ created
171
+ });
172
+ const [proof] = await Array.fromAsync(signed.getProofs(options));
173
+ assertInstanceOf(proof, DataIntegrityProof);
174
+ const signedJson = await normalizeOutgoingActivityJsonLd(await signed.toJsonLd(options), mockDocumentLoader);
175
+ assertEquals(signedJson.to, PUBLIC_COLLECTION.href);
176
+ const signedJsonObject = signedJson.object;
177
+ assertEquals(Array.isArray(signedJsonObject.attachment), true);
178
+ const verifyCache = {};
179
+ const verifyOptions = {
180
+ contextLoader: mockDocumentLoader,
181
+ documentLoader: mockDocumentLoader,
182
+ keyCache: {
183
+ get: (keyId) => Promise.resolve(verifyCache[keyId.href]),
184
+ set: (keyId, key) => {
185
+ verifyCache[keyId.href] = key;
186
+ return Promise.resolve();
187
+ }
188
+ }
189
+ };
190
+ assertInstanceOf(await verifyProof(signedJson, proof, verifyOptions), Multikey);
191
+ const signedJsonWithCurie = await signed.toJsonLd(options);
192
+ assertEquals(signedJsonWithCurie.to, "as:Public");
193
+ const signedJsonWithCurieObject = signedJsonWithCurie.object;
194
+ assertEquals(Array.isArray(signedJsonWithCurieObject.attachment), false);
195
+ assertInstanceOf(await verifyProof(signedJsonWithCurie, proof, verifyOptions), Multikey);
154
196
  });
155
197
  test("hasProofLike()", () => {
156
198
  assert(hasProofLike({ proof: {
@@ -252,6 +294,37 @@ test("verifyProof()", async () => {
252
294
  }
253
295
  }, proof, options), null);
254
296
  assertEquals(await verifyProof(jsonLd, proof.clone({ created: Temporal.Now.instant() }), options), null);
297
+ assertEquals(await verifyProof({
298
+ ...jsonLd,
299
+ "https://w3id.org/security#proof": {
300
+ "@type": ["https://w3id.org/security#DataIntegrityProof"],
301
+ "https://w3id.org/security#proofValue": [{ "@value": "stale" }]
302
+ }
303
+ }, proof, options), expectedKey);
304
+ assertEquals(await verifyProof([jsonLd], proof, options), null);
305
+ const attackerInput = {
306
+ "@context": ["https://www.w3.org/ns/activitystreams", "https://attacker.example/ctx"],
307
+ id: "https://server.example/activities/attacker",
308
+ type: "Create",
309
+ actor: "https://server.example/users/alice",
310
+ object: {
311
+ id: "https://server.example/objects/attacker",
312
+ type: "Note",
313
+ attributedTo: "https://server.example/users/alice",
314
+ content: "n/a",
315
+ to: "as:Public"
316
+ }
317
+ };
318
+ const contextLoaderCalls = [];
319
+ assertEquals(await verifyProof(attackerInput, proof, {
320
+ contextLoader: async (url) => {
321
+ contextLoaderCalls.push(url);
322
+ return await mockDocumentLoader(url);
323
+ },
324
+ documentLoader: mockDocumentLoader,
325
+ keyCache: options.keyCache
326
+ }), null);
327
+ assertFalse(contextLoaderCalls.includes("https://attacker.example/ctx"));
255
328
  });
256
329
  test("verifyObject()", async () => {
257
330
  const options = {
@@ -1,9 +1,10 @@
1
1
  import "@js-temporal/polyfill";
2
2
  import "urlpattern-polyfill";
3
3
  globalThis.addEventListener = () => {};
4
- import { a as red, i as buildMessage, l as AssertionError, n as diffStr, r as diff, s as format } from "./assert_equals-Ew3jOFa3.mjs";
4
+ import { l as AssertionError, s as format } from "./assert_equals-Ew3jOFa3.mjs";
5
5
  import "./assert_rejects-B-qJtC9Z.mjs";
6
6
  import "./assert_throws-4NwKEy2q.mjs";
7
+ import "./assert_strict_equals-Dmjbg-bA.mjs";
7
8
  //#region ../../node_modules/.pnpm/@jsr+std__assert@0.226.0/node_modules/@jsr/std__assert/assert_exists.js
8
9
  /**
9
10
  * Make an assertion that actual is not null or undefined.
@@ -77,42 +78,6 @@ import "./assert_throws-4NwKEy2q.mjs";
77
78
  throw new AssertionError(msg ?? `Expect ${actualString} > ${expectedString}`);
78
79
  }
79
80
  //#endregion
80
- //#region ../../node_modules/.pnpm/@jsr+std__assert@0.226.0/node_modules/@jsr/std__assert/assert_strict_equals.js
81
- /**
82
- * Make an assertion that `actual` and `expected` are equal using
83
- * {@linkcode Object.is} for equality comparison. If not, then throw.
84
- *
85
- * @example Usage
86
- * ```ts no-eval
87
- * import { assertStrictEquals } from "@std/assert/assert-strict-equals";
88
- *
89
- * const a = {};
90
- * const b = a;
91
- * assertStrictEquals(a, b); // Doesn't throw
92
- *
93
- * const c = {};
94
- * const d = {};
95
- * assertStrictEquals(c, d); // Throws
96
- * ```
97
- *
98
- * @typeParam T The type of the expected value.
99
- * @param actual The actual value to compare.
100
- * @param expected The expected value to compare.
101
- * @param msg The optional message to display if the assertion fails.
102
- */ function assertStrictEquals(actual, expected, msg) {
103
- if (Object.is(actual, expected)) return;
104
- const msgSuffix = msg ? `: ${msg}` : ".";
105
- let message;
106
- const actualString = format(actual);
107
- const expectedString = format(expected);
108
- if (actualString === expectedString) message = `Values have the same structure but are not reference-equal${msgSuffix}\n\n${red(actualString.split("\n").map((l) => ` ${l}`).join("\n"))}\n`;
109
- else {
110
- const stringDiff = typeof actual === "string" && typeof expected === "string";
111
- message = `Values are not strictly equal${msgSuffix}\n${buildMessage(stringDiff ? diffStr(actual, expected) : diff(actualString.split("\n"), expectedString.split("\n")), { stringDiff }).join("\n")}`;
112
- }
113
- throw new AssertionError(message);
114
- }
115
- //#endregion
116
81
  //#region ../../node_modules/.pnpm/@jsr+std__assert@0.226.0/node_modules/@jsr/std__assert/assert_string_includes.js
117
82
  /**
118
83
  * Make an assertion that actual includes expected. If not
@@ -136,4 +101,4 @@ import "./assert_throws-4NwKEy2q.mjs";
136
101
  }
137
102
  }
138
103
  //#endregion
139
- export { assertExists as a, assertGreaterOrEqual as i, assertStrictEquals as n, assertGreater as r, assertStringIncludes as t };
104
+ export { assertExists as i, assertGreater as n, assertGreaterOrEqual as r, assertStringIncludes as t };
@@ -584,6 +584,15 @@ interface FanoutMessage {
584
584
  readonly activityType: string;
585
585
  readonly collectionSync?: string;
586
586
  readonly orderingKey?: string;
587
+ /**
588
+ * Whether to apply outgoing JSON-LD wire-format normalization to queued
589
+ * activities that already carry Object Integrity Proofs.
590
+ *
591
+ * `true` is used for proofs Fedify created before fanout, or when callers
592
+ * explicitly request normalization for locally pre-signed activities.
593
+ * `false`/`undefined` preserves existing proofs as-is.
594
+ */
595
+ readonly normalizeExistingProofs?: boolean;
587
596
  readonly traceContext: Readonly<Record<string, string>>;
588
597
  }
589
598
  interface OutboxMessage {
@@ -2215,6 +2224,19 @@ interface SendActivityOptions {
2215
2224
  * @since 1.5.0
2216
2225
  */
2217
2226
  readonly fanout?: "auto" | "skip" | "force";
2227
+ /**
2228
+ * Whether to apply Fedify's outgoing JSON-LD wire-format compatibility fixes
2229
+ * to activities that already carry Object Integrity Proofs.
2230
+ *
2231
+ * By default, Fedify preserves existing proofs byte-for-byte because it
2232
+ * cannot know whether they were created for the normalized outgoing wire
2233
+ * form. Set this to `true` when sending an activity that was pre-signed
2234
+ * locally with `signObject()` or `createProof()`, so the emitted
2235
+ * compact JSON-LD matches the bytes covered by the proof.
2236
+ *
2237
+ * @since 2.2.0
2238
+ */
2239
+ readonly normalizeExistingProofs?: boolean;
2218
2240
  /**
2219
2241
  * The base URIs to exclude from the recipients' inboxes. It is useful
2220
2242
  * for excluding the recipients having the same shared inbox with the sender.