@fedify/fedify 2.2.0-dev.924 → 2.2.0-dev.938

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 (86) hide show
  1. package/dist/assert_strict_equals-Dmjbg-bA.mjs +41 -0
  2. package/dist/{builder-FoLsluZw.mjs → builder-Dq6ijpsL.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.mjs +1 -1
  8. package/dist/compat/transformers.test.mjs +3 -3
  9. package/dist/{context-BGrYMSTk.d.ts → context-BzH2-ajs.d.ts} +22 -0
  10. package/dist/{context-CMUd4wy0.d.cts → context-DJGagtNd.d.cts} +22 -0
  11. package/dist/{deno-BukNyK1t.mjs → deno-sVjM503s.mjs} +1 -1
  12. package/dist/{docloader-BgBM76TI.mjs → docloader-C5hOIM67.mjs} +2 -2
  13. package/dist/federation/builder.test.mjs +3 -3
  14. package/dist/federation/collection.test.mjs +2 -2
  15. package/dist/federation/handler.test.mjs +8 -8
  16. package/dist/federation/idempotency.test.mjs +5 -5
  17. package/dist/federation/inbox.test.mjs +1 -1
  18. package/dist/federation/keycache.test.mjs +3 -3
  19. package/dist/federation/kv.test.mjs +2 -2
  20. package/dist/federation/middleware.test.mjs +150 -10
  21. package/dist/federation/mod.cjs +1 -1
  22. package/dist/federation/mod.d.cts +2 -2
  23. package/dist/federation/mod.d.ts +2 -2
  24. package/dist/federation/mod.js +1 -1
  25. package/dist/federation/mq.test.mjs +2 -2
  26. package/dist/federation/negotiation.test.mjs +3 -3
  27. package/dist/federation/retry.test.mjs +1 -1
  28. package/dist/federation/router.test.mjs +2 -2
  29. package/dist/federation/send.test.mjs +6 -6
  30. package/dist/federation/webfinger.test.mjs +3 -3
  31. package/dist/{http-DiNUVHGB.js → http-DMkdP3lE.js} +1 -1
  32. package/dist/{http-1uLerNXX.cjs → http-De4te5mA.cjs} +1 -1
  33. package/dist/{http-DSghOjS0.mjs → http-pO-cqL07.mjs} +3 -3
  34. package/dist/{key-DAfSmMg7.mjs → key-Ch1SiRyF.mjs} +1 -1
  35. package/dist/{kv-cache-ia7oECIG.cjs → kv-cache-BM50uOpt.cjs} +1 -1
  36. package/dist/{kv-cache-Dq9VS_Jn.js → kv-cache-D7IdkIte.js} +1 -1
  37. package/dist/{ld-DYpo7uUC.mjs → ld-DkpX94b7.mjs} +2 -2
  38. package/dist/{middleware-rZ0jYYM9.cjs → middleware-0gXHFt3T.cjs} +1 -1
  39. package/dist/{middleware-aawr753E.mjs → middleware-BOshhaxP.mjs} +34 -24
  40. package/dist/{middleware-CjJ_aBdD.mjs → middleware-BqT40f_u.mjs} +1 -1
  41. package/dist/{middleware-Dt0fC6dK.cjs → middleware-CSKiL4bq.cjs} +20 -10
  42. package/dist/{middleware-olp7n2S4.js → middleware-T1_RW8x2.js} +19 -9
  43. package/dist/{mod-CJXfyw7v.d.ts → mod-2d12ffz3.d.ts} +1 -1
  44. package/dist/{mod-BcJHeuv1.d.cts → mod-D35TRn09.d.cts} +1 -1
  45. package/dist/mod.cjs +4 -4
  46. package/dist/mod.d.cts +2 -2
  47. package/dist/mod.d.ts +2 -2
  48. package/dist/mod.js +4 -4
  49. package/dist/nodeinfo/client.test.mjs +2 -2
  50. package/dist/nodeinfo/handler.test.mjs +3 -3
  51. package/dist/nodeinfo/types.test.mjs +2 -2
  52. package/dist/otel/exporter.test.mjs +2 -2
  53. package/dist/outgoing-jsonld-CNmZLixq.mjs +203 -0
  54. package/dist/{owner-B0_w8O-Y.mjs → owner-uOWCZ4oR.mjs} +2 -2
  55. package/dist/{proof-DDZ2W7TX.mjs → proof-B9ynOKyy.mjs} +12 -10
  56. package/dist/{proof-DgRfG4AE.cjs → proof-Bku-2gS1.cjs} +249 -42
  57. package/dist/{proof-DdnQ5edt.js → proof-DONzhIHm.js} +248 -41
  58. package/dist/{public-audience-eovWqzOF.mjs → public-audience-DYFHzm_c.mjs} +20 -9
  59. package/dist/{send-DMLb0UwP.mjs → send-D-qKOq3i.mjs} +2 -2
  60. package/dist/sig/accept.test.mjs +1 -1
  61. package/dist/sig/http.test.mjs +5 -5
  62. package/dist/sig/key.test.mjs +3 -3
  63. package/dist/sig/ld.test.mjs +4 -4
  64. package/dist/sig/mod.cjs +2 -2
  65. package/dist/sig/mod.js +2 -2
  66. package/dist/sig/owner.test.mjs +4 -4
  67. package/dist/sig/proof.test.mjs +25 -10
  68. package/dist/{std__assert-Duiq_YC9.mjs → std__assert-CRDpx_HF.mjs} +3 -38
  69. package/dist/testing/mod.d.mts +22 -0
  70. package/dist/utils/docloader.test.mjs +4 -4
  71. package/dist/utils/kv-cache.test.mjs +1 -1
  72. package/dist/utils/mod.cjs +1 -1
  73. package/dist/utils/mod.js +1 -1
  74. package/package.json +5 -5
  75. /package/dist/{accept-Dd__NiUL.mjs → accept-CPkZzmGN.mjs} +0 -0
  76. /package/dist/{activity-listener-CFzUqoCS.mjs → activity-listener-ell7W1s9.mjs} +0 -0
  77. /package/dist/{assert-ddO5KLpe.mjs → assert-DikXweDx.mjs} +0 -0
  78. /package/dist/{client-DVu6Fmom.mjs → client-D_1QpnWt.mjs} +0 -0
  79. /package/dist/{collection-BQRKGS7L.mjs → collection-D-HqUuA2.mjs} +0 -0
  80. /package/dist/{keycache-C2t1kvP5.mjs → keycache-EGATflN-.mjs} +0 -0
  81. /package/dist/{keys-BAK-tUlf.mjs → keys-DGu1NFwu.mjs} +0 -0
  82. /package/dist/{kv-cache-B01V7s3h.mjs → kv-cache-U__xU4qR.mjs} +0 -0
  83. /package/dist/{kv-C-TG81Sv.mjs → kv-rV3vodCc.mjs} +0 -0
  84. /package/dist/{negotiation-xb0QR3u_.mjs → negotiation-SQvQgUqe.mjs} +0 -0
  85. /package/dist/{retry-CJL0poaU.mjs → retry-bMXBL97A.mjs} +0 -0
  86. /package/dist/{types-CGUnLkU3.mjs → types-J53Kw7so.mjs} +0 -0
@@ -1,9 +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-BukNyK1t.mjs";
5
- import { n as fetchKey, o as validateCryptoKey } from "./key-DAfSmMg7.mjs";
6
- import { t as normalizePublicAudience } from "./public-audience-eovWqzOF.mjs";
4
+ import { n as version, t as name } from "./deno-sVjM503s.mjs";
5
+ import { n as fetchKey, o as validateCryptoKey } from "./key-Ch1SiRyF.mjs";
6
+ import { n as preloadedOnlyDocumentLoader } from "./public-audience-DYFHzm_c.mjs";
7
+ import { r as normalizeOutgoingActivityJsonLd } from "./outgoing-jsonld-CNmZLixq.mjs";
7
8
  import { Activity, DataIntegrityProof, Multikey, getTypeId } from "@fedify/vocab";
8
9
  import { SpanStatusCode, trace } from "@opentelemetry/api";
9
10
  import { getLogger } from "@logtape/logtape";
@@ -62,7 +63,7 @@ async function createProof(object, privateKey, keyId, { contextLoader, context,
62
63
  contextLoader,
63
64
  context
64
65
  });
65
- compactMsg = await normalizePublicAudience(compactMsg, contextLoader);
66
+ compactMsg = await normalizeOutgoingActivityJsonLd(compactMsg, contextLoader);
66
67
  const msgCanon = serialize(compactMsg);
67
68
  const encoder = new TextEncoder();
68
69
  const msgBytes = encoder.encode(msgCanon);
@@ -173,9 +174,6 @@ async function verifyProofInternal(jsonLd, proof, options) {
173
174
  const msg = { ...jsonLd };
174
175
  if ("proof" in msg) delete msg.proof;
175
176
  if ("https://w3id.org/security#proof" in msg) delete msg["https://w3id.org/security#proof"];
176
- const candidates = [msg];
177
- const normalized = await normalizePublicAudience(msg, options.contextLoader);
178
- if (normalized !== msg) candidates.push(normalized);
179
177
  let fetchedKey;
180
178
  try {
181
179
  fetchedKey = await publicKeyPromise;
@@ -217,12 +215,16 @@ async function verifyProofInternal(jsonLd, proof, options) {
217
215
  }
218
216
  const digest = new Uint8Array(proofDigest.byteLength + 32);
219
217
  digest.set(new Uint8Array(proofDigest), 0);
220
- for (const candidate of candidates) {
218
+ const proofValue = proof.proofValue;
219
+ const verifyCandidate = async (candidate) => {
221
220
  const msgBytes = encoder.encode(serialize(candidate));
222
221
  const msgDigest = await crypto.subtle.digest("SHA-256", msgBytes);
223
222
  digest.set(new Uint8Array(msgDigest), proofDigest.byteLength);
224
- if (await crypto.subtle.verify("Ed25519", publicKey.publicKey, proof.proofValue.slice(), digest)) return publicKey;
225
- }
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;
226
228
  if (fetchedKey.cached) {
227
229
  logger.debug("Failed to verify the proof with the cached key {keyId}; retrying with the freshly fetched key...", {
228
230
  keyId: proof.verificationMethodId.href,
@@ -1,7 +1,7 @@
1
1
  const { Temporal } = require("@js-temporal/polyfill");
2
2
  const { URLPattern } = require("urlpattern-polyfill");
3
3
  const require_chunk = require("./chunk-DDcVe30Y.cjs");
4
- const require_http = require("./http-1uLerNXX.cjs");
4
+ const require_http = require("./http-De4te5mA.cjs");
5
5
  let _logtape_logtape = require("@logtape/logtape");
6
6
  let _fedify_vocab = require("@fedify/vocab");
7
7
  let _opentelemetry_api = require("@opentelemetry/api");
@@ -13,7 +13,7 @@ _fedify_vocab_runtime_jsonld = require_chunk.__toESM(_fedify_vocab_runtime_jsonl
13
13
  let json_canon = require("json-canon");
14
14
  json_canon = require_chunk.__toESM(json_canon);
15
15
  //#region src/sig/ld.ts
16
- const logger$2 = (0, _logtape_logtape.getLogger)([
16
+ const logger$3 = (0, _logtape_logtape.getLogger)([
17
17
  "fedify",
18
18
  "sig",
19
19
  "ld"
@@ -161,7 +161,7 @@ async function verifySignature(jsonLd, options = {}) {
161
161
  try {
162
162
  signature = (0, byte_encodings_base64.decodeBase64)(sig.signatureValue);
163
163
  } catch (error) {
164
- logger$2.debug("Failed to verify; invalid base64 signatureValue: {signatureValue}", {
164
+ logger$3.debug("Failed to verify; invalid base64 signatureValue: {signatureValue}", {
165
165
  ...sig,
166
166
  error
167
167
  });
@@ -180,7 +180,7 @@ async function verifySignature(jsonLd, options = {}) {
180
180
  try {
181
181
  sigOptsHash = await hashJsonLd(sigOpts, options.contextLoader);
182
182
  } catch (error) {
183
- logger$2.warn("Failed to verify; failed to hash the signature options: {signatureOptions}\n{error}", {
183
+ logger$3.warn("Failed to verify; failed to hash the signature options: {signatureOptions}\n{error}", {
184
184
  signatureOptions: sigOpts,
185
185
  error
186
186
  });
@@ -192,7 +192,7 @@ async function verifySignature(jsonLd, options = {}) {
192
192
  try {
193
193
  docHash = await hashJsonLd(document, options.contextLoader);
194
194
  } catch (error) {
195
- logger$2.warn("Failed to verify; failed to hash the document: {document}\n{error}", {
195
+ logger$3.warn("Failed to verify; failed to hash the document: {document}\n{error}", {
196
196
  document,
197
197
  error
198
198
  });
@@ -203,7 +203,7 @@ async function verifySignature(jsonLd, options = {}) {
203
203
  const messageBytes = encoder.encode(message);
204
204
  if (await crypto.subtle.verify("RSASSA-PKCS1-v1_5", key.publicKey, signature.slice(), messageBytes)) return key;
205
205
  if (cached) {
206
- logger$2.debug("Failed to verify with the cached key {keyId}; signature {signatureValue} is invalid. Retrying with the freshly fetched key...", {
206
+ logger$3.debug("Failed to verify with the cached key {keyId}; signature {signatureValue} is invalid. Retrying with the freshly fetched key...", {
207
207
  keyId: sig.creator,
208
208
  ...sig
209
209
  });
@@ -217,7 +217,7 @@ async function verifySignature(jsonLd, options = {}) {
217
217
  if (key == null) return null;
218
218
  return await crypto.subtle.verify("RSASSA-PKCS1-v1_5", key.publicKey, signature.slice(), messageBytes) ? key : null;
219
219
  }
220
- logger$2.debug("Failed to verify with the fetched key {keyId}; signature {signatureValue} is invalid. Check if the key is correct or if the signed message is correct. The message to sign is:\n{message}", {
220
+ logger$3.debug("Failed to verify with the fetched key {keyId}; signature {signatureValue} is invalid. Check if the key is correct or if the signed message is correct. The message to sign is:\n{message}", {
221
221
  keyId: sig.creator,
222
222
  ...sig,
223
223
  message
@@ -249,12 +249,12 @@ async function verifyJsonLd(jsonLd, options = {}) {
249
249
  const key = await verifySignature(jsonLd, options);
250
250
  if (key == null) return false;
251
251
  if (key.ownerId == null) {
252
- logger$2.debug("Key {keyId} has no owner.", { keyId: key.id?.href });
252
+ logger$3.debug("Key {keyId} has no owner.", { keyId: key.id?.href });
253
253
  return false;
254
254
  }
255
255
  attributions.delete(key.ownerId.href);
256
256
  if (attributions.size > 0) {
257
- logger$2.debug("Some attributions are not authenticated by the Linked Data Signatures: {attributions}.", { attributions: [...attributions] });
257
+ logger$3.debug("Some attributions are not authenticated by the Linked Data Signatures: {attributions}.", { attributions: [...attributions] });
258
258
  return false;
259
259
  }
260
260
  return true;
@@ -389,8 +389,27 @@ async function getKeyOwner(keyId, options) {
389
389
  return null;
390
390
  }
391
391
  //#endregion
392
+ //#region src/compat/preloaded-context-loader.ts
393
+ /**
394
+ * A restricted JSON-LD document loader that resolves only contexts bundled
395
+ * with Fedify.
396
+ *
397
+ * This is intentionally narrower than `getDocumentLoader()`: normalization
398
+ * helpers are also reached from verification paths that operate on inbound,
399
+ * attacker-controlled JSON-LD, so the default fallback must never fetch
400
+ * attacker-supplied context URLs.
401
+ */
402
+ const preloadedOnlyDocumentLoader = (url) => {
403
+ if (Object.hasOwn(_fedify_vocab_runtime.preloadedContexts, url)) return Promise.resolve({
404
+ contextUrl: null,
405
+ documentUrl: url,
406
+ document: _fedify_vocab_runtime.preloadedContexts[url]
407
+ });
408
+ return Promise.reject(/* @__PURE__ */ new Error("Refusing to fetch a non-preloaded JSON-LD context: " + url));
409
+ };
410
+ //#endregion
392
411
  //#region src/compat/public-audience.ts
393
- const logger$1 = (0, _logtape_logtape.getLogger)([
412
+ const logger$2 = (0, _logtape_logtape.getLogger)([
394
413
  "fedify",
395
414
  "compat",
396
415
  "public-audience"
@@ -402,20 +421,12 @@ const PUBLIC_ADDRESSING_FIELDS = new Set([
402
421
  "bcc",
403
422
  "audience"
404
423
  ]);
405
- const preloadedOnlyDocumentLoader = (url) => {
406
- if (Object.hasOwn(_fedify_vocab_runtime.preloadedContexts, url)) return Promise.resolve({
407
- contextUrl: null,
408
- documentUrl: url,
409
- document: _fedify_vocab_runtime.preloadedContexts[url]
410
- });
411
- return Promise.reject(/* @__PURE__ */ new Error("Refusing to fetch a non-preloaded JSON-LD context: " + url));
412
- };
413
- const AS_CONTEXT_URL = "https://www.w3.org/ns/activitystreams";
414
- const MAX_TRAVERSAL_DEPTH = 64;
415
- const KNOWN_SAFE_CONTEXT_URLS = new Set(Object.keys(_fedify_vocab_runtime.preloadedContexts));
424
+ const AS_CONTEXT_URL$1 = "https://www.w3.org/ns/activitystreams";
425
+ const MAX_TRAVERSAL_DEPTH$1 = 64;
426
+ const KNOWN_SAFE_CONTEXT_URLS$1 = new Set(Object.keys(_fedify_vocab_runtime.preloadedContexts));
416
427
  function hasPublicCurieInAddressing(value, parentKey, depth = 0) {
417
428
  if (typeof value === "string") return parentKey != null && PUBLIC_ADDRESSING_FIELDS.has(parentKey) && (value === "as:Public" || value === "Public");
418
- if (depth >= MAX_TRAVERSAL_DEPTH) return false;
429
+ if (depth >= MAX_TRAVERSAL_DEPTH$1) return false;
419
430
  if (Array.isArray(value)) return value.some((item) => hasPublicCurieInAddressing(item, parentKey, depth + 1));
420
431
  if (typeof value !== "object" || value == null) return false;
421
432
  const record = value;
@@ -427,7 +438,7 @@ function hasPublicCurieInAddressing(value, parentKey, depth = 0) {
427
438
  }
428
439
  function rewritePublicAudience(value, parentKey, depth = 0) {
429
440
  if (typeof value === "string" && parentKey != null && PUBLIC_ADDRESSING_FIELDS.has(parentKey) && (value === "as:Public" || value === "Public")) return _fedify_vocab.PUBLIC_COLLECTION.href;
430
- if (depth >= MAX_TRAVERSAL_DEPTH) return value;
441
+ if (depth >= MAX_TRAVERSAL_DEPTH$1) return value;
431
442
  if (Array.isArray(value)) {
432
443
  let changed = false;
433
444
  const mapped = value.map((item) => {
@@ -455,14 +466,14 @@ function rewritePublicAudience(value, parentKey, depth = 0) {
455
466
  * even when the top-level `@context` is safe, so the fast path must defer
456
467
  * to the URDNA2015 equivalence check whenever one is present.
457
468
  */
458
- function hasNestedContext(value, depth = 0) {
459
- if (depth >= MAX_TRAVERSAL_DEPTH) return true;
460
- if (Array.isArray(value)) return value.some((item) => hasNestedContext(item, depth + 1));
469
+ function hasNestedContext$1(value, depth = 0) {
470
+ if (depth >= MAX_TRAVERSAL_DEPTH$1) return true;
471
+ if (Array.isArray(value)) return value.some((item) => hasNestedContext$1(item, depth + 1));
461
472
  if (typeof value !== "object" || value == null) return false;
462
473
  const record = value;
463
474
  for (const key of Object.keys(record)) {
464
475
  if (key === "@context") return true;
465
- if (hasNestedContext(record[key], depth + 1)) return true;
476
+ if (hasNestedContext$1(record[key], depth + 1)) return true;
466
477
  }
467
478
  return false;
468
479
  }
@@ -478,7 +489,7 @@ function hasNestedContext(value, depth = 0) {
478
489
  * objects at the top level, nested `@context` blocks) is treated as
479
490
  * potentially unsafe.
480
491
  */
481
- function hasKnownSafeContext(jsonLd) {
492
+ function hasKnownSafeContext$1(jsonLd) {
482
493
  if (typeof jsonLd !== "object" || jsonLd == null) return false;
483
494
  const record = jsonLd;
484
495
  if (!Object.hasOwn(record, "@context")) return false;
@@ -488,13 +499,13 @@ function hasKnownSafeContext(jsonLd) {
488
499
  let hasAs = false;
489
500
  for (const entry of entries) {
490
501
  if (typeof entry !== "string") return false;
491
- if (!KNOWN_SAFE_CONTEXT_URLS.has(entry)) return false;
492
- if (entry === AS_CONTEXT_URL) hasAs = true;
502
+ if (!KNOWN_SAFE_CONTEXT_URLS$1.has(entry)) return false;
503
+ if (entry === AS_CONTEXT_URL$1) hasAs = true;
493
504
  }
494
505
  if (!hasAs) return false;
495
506
  for (const key of Object.keys(record)) {
496
507
  if (key === "@context") continue;
497
- if (hasNestedContext(record[key])) return false;
508
+ if (hasNestedContext$1(record[key])) return false;
498
509
  }
499
510
  return true;
500
511
  }
@@ -544,6 +555,192 @@ function hasKnownSafeContext(jsonLd) {
544
555
  async function normalizePublicAudience(jsonLd, contextLoader) {
545
556
  if (!hasPublicCurieInAddressing(jsonLd)) return jsonLd;
546
557
  const normalized = rewritePublicAudience(jsonLd);
558
+ if (hasKnownSafeContext$1(jsonLd)) return normalized;
559
+ const loader = contextLoader ?? preloadedOnlyDocumentLoader;
560
+ try {
561
+ const [before, after] = await Promise.all([_fedify_vocab_runtime_jsonld.default.canonize(jsonLd, {
562
+ format: "application/n-quads",
563
+ documentLoader: loader
564
+ }), _fedify_vocab_runtime_jsonld.default.canonize(normalized, {
565
+ format: "application/n-quads",
566
+ documentLoader: loader
567
+ })]);
568
+ if (before === after) return normalized;
569
+ logger$2.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.");
570
+ } catch (error) {
571
+ logger$2.debug("Failed to verify public audience normalization equivalence via JSON-LD canonicalization; sending the activity as is.\n{error}", { error });
572
+ }
573
+ return jsonLd;
574
+ }
575
+ //#endregion
576
+ //#region src/compat/outgoing-jsonld.ts
577
+ const logger$1 = (0, _logtape_logtape.getLogger)([
578
+ "fedify",
579
+ "compat",
580
+ "outgoing-jsonld"
581
+ ]);
582
+ const ATTACHMENT_FIELDS = new Set(["attachment", "https://www.w3.org/ns/activitystreams#attachment"]);
583
+ const AS_CONTEXT_URL = "https://www.w3.org/ns/activitystreams";
584
+ const KNOWN_SAFE_CONTEXT_URLS = getKnownSafeContextUrls();
585
+ const MAX_TRAVERSAL_DEPTH = 64;
586
+ function isJsonLdListObject(value) {
587
+ return typeof value === "object" && value != null && Object.hasOwn(value, "@list");
588
+ }
589
+ function isJsonLdValueObject(value) {
590
+ return typeof value === "object" && value != null && Object.hasOwn(value, "@value");
591
+ }
592
+ function* getContextObjects(value, seen = /* @__PURE__ */ new WeakSet()) {
593
+ if (Array.isArray(value)) {
594
+ if (seen.has(value)) return;
595
+ seen.add(value);
596
+ for (const item of value) yield* getContextObjects(item, seen);
597
+ return;
598
+ }
599
+ if (typeof value === "object" && value != null) {
600
+ if (seen.has(value)) return;
601
+ seen.add(value);
602
+ const record = value;
603
+ yield record;
604
+ for (const definition of Object.values(record)) {
605
+ if (typeof definition !== "object" || definition == null) continue;
606
+ const nestedContext = definition["@context"];
607
+ if (nestedContext == null) continue;
608
+ yield* getContextObjects(nestedContext, seen);
609
+ }
610
+ }
611
+ }
612
+ function isActivityStreamsAttachmentTerm(value) {
613
+ return typeof value === "object" && value != null && value["@id"] === "as:attachment" && value["@type"] === "@id";
614
+ }
615
+ /** @internal */
616
+ function isPreloadedContextAttachmentSafe(document) {
617
+ if (typeof document !== "object" || document == null) return true;
618
+ const context = document["@context"];
619
+ for (const contextObject of getContextObjects(context)) {
620
+ if (!Object.hasOwn(contextObject, "attachment")) continue;
621
+ if (isActivityStreamsAttachmentTerm(contextObject.attachment)) continue;
622
+ return false;
623
+ }
624
+ return true;
625
+ }
626
+ function getKnownSafeContextUrls() {
627
+ const urls = /* @__PURE__ */ new Set();
628
+ for (const [url, document] of Object.entries(_fedify_vocab_runtime.preloadedContexts)) if (isPreloadedContextAttachmentSafe(document)) urls.add(url);
629
+ else logger$1.warn("Preloaded JSON-LD context {contextUrl} redefines the `attachment` term incompatibly; attachment array normalization will require canonicalization for documents using it.", { contextUrl: url });
630
+ return urls;
631
+ }
632
+ /**
633
+ * Wraps scalar ActivityStreams attachment properties in arrays.
634
+ */
635
+ function wrapScalarAttachments(jsonLd, depth = 0) {
636
+ if (depth >= MAX_TRAVERSAL_DEPTH) return jsonLd;
637
+ if (Array.isArray(jsonLd)) {
638
+ let normalized = null;
639
+ for (let i = 0; i < jsonLd.length; i++) {
640
+ const item = jsonLd[i];
641
+ const next = wrapScalarAttachments(item, depth + 1);
642
+ if (normalized == null && next !== item) normalized = jsonLd.slice(0, i);
643
+ if (normalized != null) normalized[i] = next;
644
+ }
645
+ return normalized ?? jsonLd;
646
+ }
647
+ if (typeof jsonLd !== "object" || jsonLd == null) return jsonLd;
648
+ const record = jsonLd;
649
+ const keys = Object.keys(record);
650
+ let normalized = null;
651
+ for (let i = 0; i < keys.length; i++) {
652
+ const key = keys[i];
653
+ const value = record[key];
654
+ const next = key === "@context" || key === "@value" && isJsonLdValueObject(jsonLd) ? value : wrapScalarAttachments(value, depth + 1);
655
+ const output = ATTACHMENT_FIELDS.has(key) && next != null && !Array.isArray(next) && !isJsonLdListObject(next) ? [next] : next;
656
+ if (normalized == null && output !== value) {
657
+ const cloned = Object.create(null);
658
+ for (let j = 0; j < i; j++) {
659
+ const previousKey = keys[j];
660
+ cloned[previousKey] = record[previousKey];
661
+ }
662
+ normalized = cloned;
663
+ }
664
+ if (normalized != null) normalized[key] = output;
665
+ }
666
+ return normalized ?? jsonLd;
667
+ }
668
+ function hasNestedContext(value, depth = 0) {
669
+ if (depth >= MAX_TRAVERSAL_DEPTH) return true;
670
+ if (Array.isArray(value)) return value.some((item) => hasNestedContext(item, depth + 1));
671
+ if (typeof value !== "object" || value == null) return false;
672
+ const record = value;
673
+ for (const key of Object.keys(record)) {
674
+ if (key === "@context") return true;
675
+ if (key === "@value" && isJsonLdValueObject(value)) continue;
676
+ if (hasNestedContext(record[key], depth + 1)) return true;
677
+ }
678
+ return false;
679
+ }
680
+ function exceedsTraversalDepth(value, depth = 0) {
681
+ if (depth >= MAX_TRAVERSAL_DEPTH) return true;
682
+ if (Array.isArray(value)) return value.some((item) => exceedsTraversalDepth(item, depth + 1));
683
+ if (typeof value !== "object" || value == null) return false;
684
+ const record = value;
685
+ for (const key of Object.keys(record)) {
686
+ if (key === "@context" || key === "@value" && isJsonLdValueObject(value)) continue;
687
+ if (exceedsTraversalDepth(record[key], depth + 1)) return true;
688
+ }
689
+ return false;
690
+ }
691
+ function hasKnownSafeContext(jsonLd) {
692
+ if (typeof jsonLd !== "object" || jsonLd == null) return false;
693
+ const record = jsonLd;
694
+ if (!Object.hasOwn(record, "@context")) return false;
695
+ const context = record["@context"];
696
+ const entries = typeof context === "string" ? [context] : Array.isArray(context) ? context : null;
697
+ if (entries == null || entries.length < 1) return false;
698
+ let hasActivityStreamsContext = false;
699
+ for (const entry of entries) {
700
+ if (typeof entry !== "string") return false;
701
+ if (!KNOWN_SAFE_CONTEXT_URLS.has(entry)) return false;
702
+ if (entry === AS_CONTEXT_URL) hasActivityStreamsContext = true;
703
+ }
704
+ if (!hasActivityStreamsContext) return false;
705
+ for (const key of Object.keys(record)) {
706
+ if (key === "@context") continue;
707
+ if (hasNestedContext(record[key])) return false;
708
+ }
709
+ return true;
710
+ }
711
+ function getLogSafeJsonLdMetadata(jsonLd) {
712
+ if (typeof jsonLd !== "object" || jsonLd == null) return {};
713
+ const record = jsonLd;
714
+ const context = record["@context"];
715
+ return {
716
+ id: typeof record.id === "string" ? record.id : typeof record["@id"] === "string" ? record["@id"] : void 0,
717
+ type: typeof record.type === "string" ? record.type : typeof record["@type"] === "string" ? record["@type"] : void 0,
718
+ context: typeof context === "string" ? context : Array.isArray(context) ? context.filter((entry) => typeof entry === "string").slice(0, 4) : context == null ? void 0 : "[inline context]"
719
+ };
720
+ }
721
+ /**
722
+ * Ensures ActivityStreams attachment properties are represented as arrays
723
+ * when doing so preserves the JSON-LD semantics.
724
+ *
725
+ * JSON-LD compaction collapses single-item arrays into scalar values by
726
+ * default. Some ActivityPub implementations, Pixelfed among them, parse
727
+ * `attachment` as a plain JSON array rather than a JSON-LD property and reject
728
+ * otherwise valid objects whose single attachment is emitted as a scalar.
729
+ *
730
+ * When no `contextLoader` is supplied, the helper falls back to a restricted
731
+ * loader that resolves only Fedify's preloaded JSON-LD contexts and rejects
732
+ * every other URL without network access. Documents with custom, inline, or
733
+ * otherwise uncached contexts should pass a real `contextLoader` if they need
734
+ * the semantic-preservation check to succeed; otherwise canonicalization
735
+ * failures leave the original document unchanged.
736
+ */
737
+ async function normalizeAttachmentArrays(jsonLd, contextLoader) {
738
+ const normalized = wrapScalarAttachments(jsonLd);
739
+ if (normalized === jsonLd) return jsonLd;
740
+ if (exceedsTraversalDepth(jsonLd)) {
741
+ logger$1.debug("Skipping attachment array normalization because the JSON-LD document exceeds the safe traversal depth; leaving it unchanged.");
742
+ return jsonLd;
743
+ }
547
744
  if (hasKnownSafeContext(jsonLd)) return normalized;
548
745
  const loader = contextLoader ?? preloadedOnlyDocumentLoader;
549
746
  try {
@@ -555,12 +752,21 @@ async function normalizePublicAudience(jsonLd, contextLoader) {
555
752
  documentLoader: loader
556
753
  })]);
557
754
  if (before === after) return normalized;
558
- logger$1.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.");
755
+ logger$1.warn("Wrapping scalar attachment values in arrays would change the canonical form of the JSON-LD document; leaving it unchanged. This usually means the active JSON-LD context redefines the `attachment` term. Document: {id}; type: {type}; context: {context}.", getLogSafeJsonLdMetadata(jsonLd));
559
756
  } catch (error) {
560
- logger$1.debug("Failed to verify public audience normalization equivalence via JSON-LD canonicalization; sending the activity as is.\n{error}", { error });
757
+ logger$1.debug("Failed to verify attachment array normalization equivalence via JSON-LD canonicalization; leaving the JSON-LD document unchanged.\n{error}", { error });
561
758
  }
562
759
  return jsonLd;
563
760
  }
761
+ /**
762
+ * Applies Fedify's internal JSON-LD wire-format interoperability workarounds
763
+ * to locally generated outgoing activities before they are signed, enqueued,
764
+ * or sent.
765
+ */
766
+ async function normalizeOutgoingActivityJsonLd(jsonLd, contextLoader) {
767
+ jsonLd = await normalizePublicAudience(jsonLd, contextLoader);
768
+ return await normalizeAttachmentArrays(jsonLd, contextLoader);
769
+ }
564
770
  //#endregion
565
771
  //#region src/sig/proof.ts
566
772
  const logger = (0, _logtape_logtape.getLogger)([
@@ -615,7 +821,7 @@ async function createProof(object, privateKey, keyId, { contextLoader, context,
615
821
  contextLoader,
616
822
  context
617
823
  });
618
- compactMsg = await normalizePublicAudience(compactMsg, contextLoader);
824
+ compactMsg = await normalizeOutgoingActivityJsonLd(compactMsg, contextLoader);
619
825
  const msgCanon = (0, json_canon.default)(compactMsg);
620
826
  const encoder = new TextEncoder();
621
827
  const msgBytes = encoder.encode(msgCanon);
@@ -726,9 +932,6 @@ async function verifyProofInternal(jsonLd, proof, options) {
726
932
  const msg = { ...jsonLd };
727
933
  if ("proof" in msg) delete msg.proof;
728
934
  if ("https://w3id.org/security#proof" in msg) delete msg["https://w3id.org/security#proof"];
729
- const candidates = [msg];
730
- const normalized = await normalizePublicAudience(msg, options.contextLoader);
731
- if (normalized !== msg) candidates.push(normalized);
732
935
  let fetchedKey;
733
936
  try {
734
937
  fetchedKey = await publicKeyPromise;
@@ -770,12 +973,16 @@ async function verifyProofInternal(jsonLd, proof, options) {
770
973
  }
771
974
  const digest = new Uint8Array(proofDigest.byteLength + 32);
772
975
  digest.set(new Uint8Array(proofDigest), 0);
773
- for (const candidate of candidates) {
976
+ const proofValue = proof.proofValue;
977
+ const verifyCandidate = async (candidate) => {
774
978
  const msgBytes = encoder.encode((0, json_canon.default)(candidate));
775
979
  const msgDigest = await crypto.subtle.digest("SHA-256", msgBytes);
776
980
  digest.set(new Uint8Array(msgDigest), proofDigest.byteLength);
777
- if (await crypto.subtle.verify("Ed25519", publicKey.publicKey, proof.proofValue.slice(), digest)) return publicKey;
778
- }
981
+ return await crypto.subtle.verify("Ed25519", publicKey.publicKey, proofValue.slice(), digest);
982
+ };
983
+ if (await verifyCandidate(msg)) return publicKey;
984
+ const normalized = await normalizeOutgoingActivityJsonLd(msg, preloadedOnlyDocumentLoader);
985
+ if (normalized !== msg && await verifyCandidate(normalized)) return publicKey;
779
986
  if (fetchedKey.cached) {
780
987
  logger.debug("Failed to verify the proof with the cached key {keyId}; retrying with the freshly fetched key...", {
781
988
  keyId: proof.verificationMethodId.href,
@@ -882,10 +1089,10 @@ Object.defineProperty(exports, "hasSignatureLike", {
882
1089
  return hasSignatureLike;
883
1090
  }
884
1091
  });
885
- Object.defineProperty(exports, "normalizePublicAudience", {
1092
+ Object.defineProperty(exports, "normalizeOutgoingActivityJsonLd", {
886
1093
  enumerable: true,
887
1094
  get: function() {
888
- return normalizePublicAudience;
1095
+ return normalizeOutgoingActivityJsonLd;
889
1096
  }
890
1097
  });
891
1098
  Object.defineProperty(exports, "signJsonLd", {