@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,16 +1,16 @@
1
1
  import { Temporal } from "@js-temporal/polyfill";
2
2
  import "urlpattern-polyfill";
3
- import { _ as version, d as validateCryptoKey, g as name, s as fetchKey } from "./http-C8puyZ4Z.js";
3
+ import { _ as version, d as validateCryptoKey, g as name, s as fetchKey } from "./http-pZce7PcA.js";
4
4
  import { getLogger } from "@logtape/logtape";
5
- import { Activity, CryptographicKey, DataIntegrityProof, Multikey, Object as Object$1, getTypeId, isActor } from "@fedify/vocab";
5
+ import { Activity, CryptographicKey, DataIntegrityProof, Multikey, Object as Object$1, PUBLIC_COLLECTION, getTypeId, isActor } from "@fedify/vocab";
6
6
  import { SpanKind, SpanStatusCode, trace } from "@opentelemetry/api";
7
7
  import { encodeHex } from "byte-encodings/hex";
8
- import { getDocumentLoader } from "@fedify/vocab-runtime";
8
+ import { getDocumentLoader, preloadedContexts } from "@fedify/vocab-runtime";
9
9
  import { decodeBase64, encodeBase64 } from "byte-encodings/base64";
10
10
  import jsonld from "@fedify/vocab-runtime/jsonld";
11
11
  import serialize from "json-canon";
12
12
  //#region src/sig/ld.ts
13
- const logger$1 = getLogger([
13
+ const logger$3 = getLogger([
14
14
  "fedify",
15
15
  "sig",
16
16
  "ld"
@@ -158,7 +158,7 @@ async function verifySignature(jsonLd, options = {}) {
158
158
  try {
159
159
  signature = decodeBase64(sig.signatureValue);
160
160
  } catch (error) {
161
- logger$1.debug("Failed to verify; invalid base64 signatureValue: {signatureValue}", {
161
+ logger$3.debug("Failed to verify; invalid base64 signatureValue: {signatureValue}", {
162
162
  ...sig,
163
163
  error
164
164
  });
@@ -177,7 +177,7 @@ async function verifySignature(jsonLd, options = {}) {
177
177
  try {
178
178
  sigOptsHash = await hashJsonLd(sigOpts, options.contextLoader);
179
179
  } catch (error) {
180
- logger$1.warn("Failed to verify; failed to hash the signature options: {signatureOptions}\n{error}", {
180
+ logger$3.warn("Failed to verify; failed to hash the signature options: {signatureOptions}\n{error}", {
181
181
  signatureOptions: sigOpts,
182
182
  error
183
183
  });
@@ -189,7 +189,7 @@ async function verifySignature(jsonLd, options = {}) {
189
189
  try {
190
190
  docHash = await hashJsonLd(document, options.contextLoader);
191
191
  } catch (error) {
192
- logger$1.warn("Failed to verify; failed to hash the document: {document}\n{error}", {
192
+ logger$3.warn("Failed to verify; failed to hash the document: {document}\n{error}", {
193
193
  document,
194
194
  error
195
195
  });
@@ -200,7 +200,7 @@ async function verifySignature(jsonLd, options = {}) {
200
200
  const messageBytes = encoder.encode(message);
201
201
  if (await crypto.subtle.verify("RSASSA-PKCS1-v1_5", key.publicKey, signature.slice(), messageBytes)) return key;
202
202
  if (cached) {
203
- logger$1.debug("Failed to verify with the cached key {keyId}; signature {signatureValue} is invalid. Retrying with the freshly fetched key...", {
203
+ logger$3.debug("Failed to verify with the cached key {keyId}; signature {signatureValue} is invalid. Retrying with the freshly fetched key...", {
204
204
  keyId: sig.creator,
205
205
  ...sig
206
206
  });
@@ -214,7 +214,7 @@ async function verifySignature(jsonLd, options = {}) {
214
214
  if (key == null) return null;
215
215
  return await crypto.subtle.verify("RSASSA-PKCS1-v1_5", key.publicKey, signature.slice(), messageBytes) ? key : null;
216
216
  }
217
- logger$1.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}", {
217
+ 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}", {
218
218
  keyId: sig.creator,
219
219
  ...sig,
220
220
  message
@@ -246,12 +246,12 @@ async function verifyJsonLd(jsonLd, options = {}) {
246
246
  const key = await verifySignature(jsonLd, options);
247
247
  if (key == null) return false;
248
248
  if (key.ownerId == null) {
249
- logger$1.debug("Key {keyId} has no owner.", { keyId: key.id?.href });
249
+ logger$3.debug("Key {keyId} has no owner.", { keyId: key.id?.href });
250
250
  return false;
251
251
  }
252
252
  attributions.delete(key.ownerId.href);
253
253
  if (attributions.size > 0) {
254
- logger$1.debug("Some attributions are not authenticated by the Linked Data Signatures: {attributions}.", { attributions: [...attributions] });
254
+ logger$3.debug("Some attributions are not authenticated by the Linked Data Signatures: {attributions}.", { attributions: [...attributions] });
255
255
  return false;
256
256
  }
257
257
  return true;
@@ -386,6 +386,385 @@ async function getKeyOwner(keyId, options) {
386
386
  return null;
387
387
  }
388
388
  //#endregion
389
+ //#region src/compat/preloaded-context-loader.ts
390
+ /**
391
+ * A restricted JSON-LD document loader that resolves only contexts bundled
392
+ * with Fedify.
393
+ *
394
+ * This is intentionally narrower than `getDocumentLoader()`: normalization
395
+ * helpers are also reached from verification paths that operate on inbound,
396
+ * attacker-controlled JSON-LD, so the default fallback must never fetch
397
+ * attacker-supplied context URLs.
398
+ */
399
+ const preloadedOnlyDocumentLoader = (url) => {
400
+ if (Object.hasOwn(preloadedContexts, url)) return Promise.resolve({
401
+ contextUrl: null,
402
+ documentUrl: url,
403
+ document: preloadedContexts[url]
404
+ });
405
+ return Promise.reject(/* @__PURE__ */ new Error("Refusing to fetch a non-preloaded JSON-LD context: " + url));
406
+ };
407
+ //#endregion
408
+ //#region src/compat/public-audience.ts
409
+ const logger$2 = getLogger([
410
+ "fedify",
411
+ "compat",
412
+ "public-audience"
413
+ ]);
414
+ const PUBLIC_ADDRESSING_FIELDS = new Set([
415
+ "to",
416
+ "cc",
417
+ "bto",
418
+ "bcc",
419
+ "audience"
420
+ ]);
421
+ const AS_CONTEXT_URL$1 = "https://www.w3.org/ns/activitystreams";
422
+ const MAX_TRAVERSAL_DEPTH$1 = 64;
423
+ const KNOWN_SAFE_CONTEXT_URLS$1 = new Set(Object.keys(preloadedContexts));
424
+ function hasPublicCurieInAddressing(value, parentKey, depth = 0) {
425
+ if (typeof value === "string") return parentKey != null && PUBLIC_ADDRESSING_FIELDS.has(parentKey) && (value === "as:Public" || value === "Public");
426
+ if (depth >= MAX_TRAVERSAL_DEPTH$1) return false;
427
+ if (Array.isArray(value)) return value.some((item) => hasPublicCurieInAddressing(item, parentKey, depth + 1));
428
+ if (typeof value !== "object" || value == null) return false;
429
+ const record = value;
430
+ for (const key of Object.keys(record)) {
431
+ if (key === "@context") continue;
432
+ if (hasPublicCurieInAddressing(record[key], key, depth + 1)) return true;
433
+ }
434
+ return false;
435
+ }
436
+ function rewritePublicAudience(value, parentKey, depth = 0) {
437
+ if (typeof value === "string" && parentKey != null && PUBLIC_ADDRESSING_FIELDS.has(parentKey) && (value === "as:Public" || value === "Public")) return PUBLIC_COLLECTION.href;
438
+ if (depth >= MAX_TRAVERSAL_DEPTH$1) return value;
439
+ if (Array.isArray(value)) {
440
+ let changed = false;
441
+ const mapped = value.map((item) => {
442
+ const rewritten = rewritePublicAudience(item, parentKey, depth + 1);
443
+ if (rewritten !== item) changed = true;
444
+ return rewritten;
445
+ });
446
+ return changed ? mapped : value;
447
+ }
448
+ if (typeof value !== "object" || value == null) return value;
449
+ const record = value;
450
+ let changed = false;
451
+ const normalized = Object.create(null);
452
+ for (const key of Object.keys(record)) {
453
+ const rewritten = key === "@context" ? record[key] : rewritePublicAudience(record[key], key, depth + 1);
454
+ if (rewritten !== record[key]) changed = true;
455
+ normalized[key] = rewritten;
456
+ }
457
+ return changed ? normalized : value;
458
+ }
459
+ /**
460
+ * Reports whether `value` carries an `@context` property anywhere inside
461
+ * its subtree (not counting the value itself). A nested `@context` can
462
+ * introduce a local term-definition scope that redefines `as:` or `Public`
463
+ * even when the top-level `@context` is safe, so the fast path must defer
464
+ * to the URDNA2015 equivalence check whenever one is present.
465
+ */
466
+ function hasNestedContext$1(value, depth = 0) {
467
+ if (depth >= MAX_TRAVERSAL_DEPTH$1) return true;
468
+ if (Array.isArray(value)) return value.some((item) => hasNestedContext$1(item, depth + 1));
469
+ if (typeof value !== "object" || value == null) return false;
470
+ const record = value;
471
+ for (const key of Object.keys(record)) {
472
+ if (key === "@context") return true;
473
+ if (hasNestedContext$1(record[key], depth + 1)) return true;
474
+ }
475
+ return false;
476
+ }
477
+ /**
478
+ * Checks whether the `@context` of a JSON-LD document is guaranteed not
479
+ * to redefine the `as:` prefix or the bare `Public` term. Only documents
480
+ * whose `@context` is a string, or an array of strings, drawn from Fedify's
481
+ * preloaded context set AND including the ActivityStreams URL qualify,
482
+ * AND no nested subtree carries its own `@context` that might redefine
483
+ * those terms within a local scope. When all of that holds the rewrite
484
+ * is provably semantics-preserving and the URDNA2015 equivalence check
485
+ * can be skipped. Any other shape (unknown external URLs, inline
486
+ * objects at the top level, nested `@context` blocks) is treated as
487
+ * potentially unsafe.
488
+ */
489
+ function hasKnownSafeContext$1(jsonLd) {
490
+ if (typeof jsonLd !== "object" || jsonLd == null) return false;
491
+ const record = jsonLd;
492
+ if (!Object.hasOwn(record, "@context")) return false;
493
+ const ctx = record["@context"];
494
+ const entries = typeof ctx === "string" ? [ctx] : Array.isArray(ctx) ? ctx : null;
495
+ if (entries == null || entries.length === 0) return false;
496
+ let hasAs = false;
497
+ for (const entry of entries) {
498
+ if (typeof entry !== "string") return false;
499
+ if (!KNOWN_SAFE_CONTEXT_URLS$1.has(entry)) return false;
500
+ if (entry === AS_CONTEXT_URL$1) hasAs = true;
501
+ }
502
+ if (!hasAs) return false;
503
+ for (const key of Object.keys(record)) {
504
+ if (key === "@context") continue;
505
+ if (hasNestedContext$1(record[key])) return false;
506
+ }
507
+ return true;
508
+ }
509
+ /**
510
+ * Rewrites the compact `as:Public` / `Public` CURIE appearing in activity
511
+ * addressing fields (`to`, `cc`, `bto`, `bcc`, `audience`) to the fully
512
+ * expanded `https://www.w3.org/ns/activitystreams#Public` URI.
513
+ *
514
+ * Several ActivityPub implementations, Lemmy among them, match these
515
+ * fields as plain URLs without running JSON-LD expansion, and silently
516
+ * drop activities whose public addressing appears in CURIE form. This
517
+ * helper works around that gap.
518
+ *
519
+ * For documents whose `@context` is drawn entirely from Fedify's
520
+ * preloaded context set and includes the ActivityStreams URL, the
521
+ * rewrite is applied directly: the content of every preloaded non-AS
522
+ * context is known not to redefine the `as:` prefix or the bare `Public`
523
+ * term, so the semantics are preserved by construction. Any other
524
+ * shape (an inline object, an unknown external URL, and so on) is
525
+ * treated as potentially unsafe and gated on a JSON-LD equivalence
526
+ * check; both forms are canonicalized with URDNA2015 and the resulting
527
+ * N-Quads are compared. When they differ, the original document is
528
+ * returned unchanged. Canonicalization failures also fall back to the
529
+ * original document.
530
+ *
531
+ * When no `contextLoader` is supplied the helper falls back to an
532
+ * internal loader that resolves only the URLs in Fedify's
533
+ * preloaded-contexts set and rejects every other URL without issuing a
534
+ * network request. That behaviour is deliberately narrower than
535
+ * `@fedify/vocab-runtime`'s `getDocumentLoader()`, which after its
536
+ * `validatePublicUrl` check will happily fetch non-preloaded URLs: the
537
+ * helper is reached from verification paths (`verifyProof()` /
538
+ * `verifyObject()`) that operate on inbound, potentially adversarial
539
+ * JSON-LD, and a default loader that fetches attacker-supplied
540
+ * `@context` URLs on the caller's behalf would be an SSRF vector.
541
+ * Canonicalization failures against the restricted loader fall back to
542
+ * the original document, same as any other canonicalization error.
543
+ * Callers that genuinely need the remote-fetch loader (for example
544
+ * applications that sign local JSON-LD against a custom vocabulary)
545
+ * should pass a `contextLoader` explicitly.
546
+ *
547
+ * Must be called before any signing step that canonicalizes the
548
+ * compact form byte-for-byte (for example, Object Integrity Proofs
549
+ * using the `eddsa-jcs-2022` cryptosuite), so the signed payload
550
+ * matches what is sent on the wire.
551
+ */
552
+ async function normalizePublicAudience(jsonLd, contextLoader) {
553
+ if (!hasPublicCurieInAddressing(jsonLd)) return jsonLd;
554
+ const normalized = rewritePublicAudience(jsonLd);
555
+ if (hasKnownSafeContext$1(jsonLd)) return normalized;
556
+ const loader = contextLoader ?? preloadedOnlyDocumentLoader;
557
+ try {
558
+ const [before, after] = await Promise.all([jsonld.canonize(jsonLd, {
559
+ format: "application/n-quads",
560
+ documentLoader: loader
561
+ }), jsonld.canonize(normalized, {
562
+ format: "application/n-quads",
563
+ documentLoader: loader
564
+ })]);
565
+ if (before === after) return normalized;
566
+ 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.");
567
+ } catch (error) {
568
+ logger$2.debug("Failed to verify public audience normalization equivalence via JSON-LD canonicalization; sending the activity as is.\n{error}", { error });
569
+ }
570
+ return jsonLd;
571
+ }
572
+ //#endregion
573
+ //#region src/compat/outgoing-jsonld.ts
574
+ const logger$1 = getLogger([
575
+ "fedify",
576
+ "compat",
577
+ "outgoing-jsonld"
578
+ ]);
579
+ const ATTACHMENT_FIELDS = new Set(["attachment", "https://www.w3.org/ns/activitystreams#attachment"]);
580
+ const AS_CONTEXT_URL = "https://www.w3.org/ns/activitystreams";
581
+ const KNOWN_SAFE_CONTEXT_URLS = getKnownSafeContextUrls();
582
+ const MAX_TRAVERSAL_DEPTH = 64;
583
+ function isJsonLdListObject(value) {
584
+ return typeof value === "object" && value != null && Object.hasOwn(value, "@list");
585
+ }
586
+ function isJsonLdValueObject(value) {
587
+ return typeof value === "object" && value != null && Object.hasOwn(value, "@value");
588
+ }
589
+ function* getContextObjects(value, seen = /* @__PURE__ */ new WeakSet()) {
590
+ if (Array.isArray(value)) {
591
+ if (seen.has(value)) return;
592
+ seen.add(value);
593
+ for (const item of value) yield* getContextObjects(item, seen);
594
+ return;
595
+ }
596
+ if (typeof value === "object" && value != null) {
597
+ if (seen.has(value)) return;
598
+ seen.add(value);
599
+ const record = value;
600
+ yield record;
601
+ for (const definition of Object.values(record)) {
602
+ if (typeof definition !== "object" || definition == null) continue;
603
+ const nestedContext = definition["@context"];
604
+ if (nestedContext == null) continue;
605
+ yield* getContextObjects(nestedContext, seen);
606
+ }
607
+ }
608
+ }
609
+ function isActivityStreamsAttachmentTerm(value) {
610
+ return typeof value === "object" && value != null && value["@id"] === "as:attachment" && value["@type"] === "@id";
611
+ }
612
+ /** @internal */
613
+ function isPreloadedContextAttachmentSafe(document) {
614
+ if (typeof document !== "object" || document == null) return true;
615
+ const context = document["@context"];
616
+ for (const contextObject of getContextObjects(context)) {
617
+ if (!Object.hasOwn(contextObject, "attachment")) continue;
618
+ if (isActivityStreamsAttachmentTerm(contextObject.attachment)) continue;
619
+ return false;
620
+ }
621
+ return true;
622
+ }
623
+ function getKnownSafeContextUrls() {
624
+ const urls = /* @__PURE__ */ new Set();
625
+ for (const [url, document] of Object.entries(preloadedContexts)) if (isPreloadedContextAttachmentSafe(document)) urls.add(url);
626
+ 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 });
627
+ return urls;
628
+ }
629
+ /**
630
+ * Wraps scalar ActivityStreams attachment properties in arrays.
631
+ */
632
+ function wrapScalarAttachments(jsonLd, depth = 0) {
633
+ if (depth >= MAX_TRAVERSAL_DEPTH) return jsonLd;
634
+ if (Array.isArray(jsonLd)) {
635
+ let normalized = null;
636
+ for (let i = 0; i < jsonLd.length; i++) {
637
+ const item = jsonLd[i];
638
+ const next = wrapScalarAttachments(item, depth + 1);
639
+ if (normalized == null && next !== item) normalized = jsonLd.slice(0, i);
640
+ if (normalized != null) normalized[i] = next;
641
+ }
642
+ return normalized ?? jsonLd;
643
+ }
644
+ if (typeof jsonLd !== "object" || jsonLd == null) return jsonLd;
645
+ const record = jsonLd;
646
+ const keys = Object.keys(record);
647
+ let normalized = null;
648
+ for (let i = 0; i < keys.length; i++) {
649
+ const key = keys[i];
650
+ const value = record[key];
651
+ const next = key === "@context" || key === "@value" && isJsonLdValueObject(jsonLd) ? value : wrapScalarAttachments(value, depth + 1);
652
+ const output = ATTACHMENT_FIELDS.has(key) && next != null && !Array.isArray(next) && !isJsonLdListObject(next) ? [next] : next;
653
+ if (normalized == null && output !== value) {
654
+ const cloned = Object.create(null);
655
+ for (let j = 0; j < i; j++) {
656
+ const previousKey = keys[j];
657
+ cloned[previousKey] = record[previousKey];
658
+ }
659
+ normalized = cloned;
660
+ }
661
+ if (normalized != null) normalized[key] = output;
662
+ }
663
+ return normalized ?? jsonLd;
664
+ }
665
+ function hasNestedContext(value, depth = 0) {
666
+ if (depth >= MAX_TRAVERSAL_DEPTH) return true;
667
+ if (Array.isArray(value)) return value.some((item) => hasNestedContext(item, depth + 1));
668
+ if (typeof value !== "object" || value == null) return false;
669
+ const record = value;
670
+ for (const key of Object.keys(record)) {
671
+ if (key === "@context") return true;
672
+ if (key === "@value" && isJsonLdValueObject(value)) continue;
673
+ if (hasNestedContext(record[key], depth + 1)) return true;
674
+ }
675
+ return false;
676
+ }
677
+ function exceedsTraversalDepth(value, depth = 0) {
678
+ if (depth >= MAX_TRAVERSAL_DEPTH) return true;
679
+ if (Array.isArray(value)) return value.some((item) => exceedsTraversalDepth(item, depth + 1));
680
+ if (typeof value !== "object" || value == null) return false;
681
+ const record = value;
682
+ for (const key of Object.keys(record)) {
683
+ if (key === "@context" || key === "@value" && isJsonLdValueObject(value)) continue;
684
+ if (exceedsTraversalDepth(record[key], depth + 1)) return true;
685
+ }
686
+ return false;
687
+ }
688
+ function hasKnownSafeContext(jsonLd) {
689
+ if (typeof jsonLd !== "object" || jsonLd == null) return false;
690
+ const record = jsonLd;
691
+ if (!Object.hasOwn(record, "@context")) return false;
692
+ const context = record["@context"];
693
+ const entries = typeof context === "string" ? [context] : Array.isArray(context) ? context : null;
694
+ if (entries == null || entries.length < 1) return false;
695
+ let hasActivityStreamsContext = false;
696
+ for (const entry of entries) {
697
+ if (typeof entry !== "string") return false;
698
+ if (!KNOWN_SAFE_CONTEXT_URLS.has(entry)) return false;
699
+ if (entry === AS_CONTEXT_URL) hasActivityStreamsContext = true;
700
+ }
701
+ if (!hasActivityStreamsContext) return false;
702
+ for (const key of Object.keys(record)) {
703
+ if (key === "@context") continue;
704
+ if (hasNestedContext(record[key])) return false;
705
+ }
706
+ return true;
707
+ }
708
+ function getLogSafeJsonLdMetadata(jsonLd) {
709
+ if (typeof jsonLd !== "object" || jsonLd == null) return {};
710
+ const record = jsonLd;
711
+ const context = record["@context"];
712
+ return {
713
+ id: typeof record.id === "string" ? record.id : typeof record["@id"] === "string" ? record["@id"] : void 0,
714
+ type: typeof record.type === "string" ? record.type : typeof record["@type"] === "string" ? record["@type"] : void 0,
715
+ context: typeof context === "string" ? context : Array.isArray(context) ? context.filter((entry) => typeof entry === "string").slice(0, 4) : context == null ? void 0 : "[inline context]"
716
+ };
717
+ }
718
+ /**
719
+ * Ensures ActivityStreams attachment properties are represented as arrays
720
+ * when doing so preserves the JSON-LD semantics.
721
+ *
722
+ * JSON-LD compaction collapses single-item arrays into scalar values by
723
+ * default. Some ActivityPub implementations, Pixelfed among them, parse
724
+ * `attachment` as a plain JSON array rather than a JSON-LD property and reject
725
+ * otherwise valid objects whose single attachment is emitted as a scalar.
726
+ *
727
+ * When no `contextLoader` is supplied, the helper falls back to a restricted
728
+ * loader that resolves only Fedify's preloaded JSON-LD contexts and rejects
729
+ * every other URL without network access. Documents with custom, inline, or
730
+ * otherwise uncached contexts should pass a real `contextLoader` if they need
731
+ * the semantic-preservation check to succeed; otherwise canonicalization
732
+ * failures leave the original document unchanged.
733
+ */
734
+ async function normalizeAttachmentArrays(jsonLd, contextLoader) {
735
+ const normalized = wrapScalarAttachments(jsonLd);
736
+ if (normalized === jsonLd) return jsonLd;
737
+ if (exceedsTraversalDepth(jsonLd)) {
738
+ logger$1.debug("Skipping attachment array normalization because the JSON-LD document exceeds the safe traversal depth; leaving it unchanged.");
739
+ return jsonLd;
740
+ }
741
+ if (hasKnownSafeContext(jsonLd)) return normalized;
742
+ const loader = contextLoader ?? preloadedOnlyDocumentLoader;
743
+ try {
744
+ const [before, after] = await Promise.all([jsonld.canonize(jsonLd, {
745
+ format: "application/n-quads",
746
+ documentLoader: loader
747
+ }), jsonld.canonize(normalized, {
748
+ format: "application/n-quads",
749
+ documentLoader: loader
750
+ })]);
751
+ if (before === after) return normalized;
752
+ 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));
753
+ } catch (error) {
754
+ logger$1.debug("Failed to verify attachment array normalization equivalence via JSON-LD canonicalization; leaving the JSON-LD document unchanged.\n{error}", { error });
755
+ }
756
+ return jsonLd;
757
+ }
758
+ /**
759
+ * Applies Fedify's internal JSON-LD wire-format interoperability workarounds
760
+ * to locally generated outgoing activities before they are signed, enqueued,
761
+ * or sent.
762
+ */
763
+ async function normalizeOutgoingActivityJsonLd(jsonLd, contextLoader) {
764
+ jsonLd = await normalizePublicAudience(jsonLd, contextLoader);
765
+ return await normalizeAttachmentArrays(jsonLd, contextLoader);
766
+ }
767
+ //#endregion
389
768
  //#region src/sig/proof.ts
390
769
  const logger = getLogger([
391
770
  "fedify",
@@ -434,11 +813,12 @@ function hasProofLike(jsonLd) {
434
813
  async function createProof(object, privateKey, keyId, { contextLoader, context, created } = {}) {
435
814
  validateCryptoKey(privateKey, "private");
436
815
  if (privateKey.algorithm.name !== "Ed25519") throw new TypeError("Unsupported algorithm: " + privateKey.algorithm.name);
437
- const compactMsg = await object.clone({ proofs: [] }).toJsonLd({
816
+ let compactMsg = await object.clone({ proofs: [] }).toJsonLd({
438
817
  format: "compact",
439
818
  contextLoader,
440
819
  context
441
820
  });
821
+ compactMsg = await normalizeOutgoingActivityJsonLd(compactMsg, contextLoader);
442
822
  const msgCanon = serialize(compactMsg);
443
823
  const encoder = new TextEncoder();
444
824
  const msgBytes = encoder.encode(msgCanon);
@@ -533,27 +913,22 @@ async function verifyProof(jsonLd, proof, options = {}) {
533
913
  });
534
914
  }
535
915
  async function verifyProofInternal(jsonLd, proof, options) {
536
- if (typeof jsonLd !== "object" || proof.cryptosuite !== "eddsa-jcs-2022" || proof.verificationMethodId == null || proof.proofPurpose !== "assertionMethod" || proof.proofValue == null || proof.created == null) return null;
916
+ 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;
537
917
  const publicKeyPromise = fetchKey(proof.verificationMethodId, Multikey, options);
538
- const proofCanon = serialize({
918
+ const proofConfig = {
539
919
  "@context": jsonLd["@context"],
540
920
  type: "DataIntegrityProof",
541
921
  cryptosuite: proof.cryptosuite,
542
922
  verificationMethod: proof.verificationMethodId.href,
543
923
  proofPurpose: proof.proofPurpose,
544
924
  created: proof.created.toString()
545
- });
925
+ };
546
926
  const encoder = new TextEncoder();
547
- const proofBytes = encoder.encode(proofCanon);
927
+ const proofBytes = encoder.encode(serialize(proofConfig));
548
928
  const proofDigest = await crypto.subtle.digest("SHA-256", proofBytes);
549
929
  const msg = { ...jsonLd };
550
930
  if ("proof" in msg) delete msg.proof;
551
- const msgCanon = serialize(msg);
552
- const msgBytes = encoder.encode(msgCanon);
553
- const msgDigest = await crypto.subtle.digest("SHA-256", msgBytes);
554
- const digest = new Uint8Array(proofDigest.byteLength + msgDigest.byteLength);
555
- digest.set(new Uint8Array(proofDigest), 0);
556
- digest.set(new Uint8Array(msgDigest), proofDigest.byteLength);
931
+ if ("https://w3id.org/security#proof" in msg) delete msg["https://w3id.org/security#proof"];
557
932
  let fetchedKey;
558
933
  try {
559
934
  fetchedKey = await publicKeyPromise;
@@ -582,7 +957,7 @@ async function verifyProofInternal(jsonLd, proof, options) {
582
957
  return await verifyProof(jsonLd, proof, {
583
958
  ...options,
584
959
  keyCache: {
585
- get: () => Promise.resolve(null),
960
+ get: () => Promise.resolve(void 0),
586
961
  set: async (keyId, key) => await options.keyCache?.set(keyId, key)
587
962
  }
588
963
  });
@@ -593,27 +968,36 @@ async function verifyProofInternal(jsonLd, proof, options) {
593
968
  });
594
969
  return null;
595
970
  }
596
- if (!await crypto.subtle.verify("Ed25519", publicKey.publicKey, proof.proofValue.slice(), digest)) {
597
- if (fetchedKey.cached) {
598
- logger.debug("Failed to verify the proof with the cached key {keyId}; retrying with the freshly fetched key...", {
599
- keyId: proof.verificationMethodId.href,
600
- proof
601
- });
602
- return await verifyProof(jsonLd, proof, {
603
- ...options,
604
- keyCache: {
605
- get: () => Promise.resolve(void 0),
606
- set: async (keyId, key) => await options.keyCache?.set(keyId, key)
607
- }
608
- });
609
- }
610
- logger.debug("Failed to verify the proof with the fetched key {keyId}:\n{proof}", {
971
+ const digest = new Uint8Array(proofDigest.byteLength + 32);
972
+ digest.set(new Uint8Array(proofDigest), 0);
973
+ const proofValue = proof.proofValue;
974
+ const verifyCandidate = async (candidate) => {
975
+ const msgBytes = encoder.encode(serialize(candidate));
976
+ const msgDigest = await crypto.subtle.digest("SHA-256", msgBytes);
977
+ digest.set(new Uint8Array(msgDigest), proofDigest.byteLength);
978
+ return await crypto.subtle.verify("Ed25519", publicKey.publicKey, proofValue.slice(), digest);
979
+ };
980
+ if (await verifyCandidate(msg)) return publicKey;
981
+ const normalized = await normalizeOutgoingActivityJsonLd(msg, preloadedOnlyDocumentLoader);
982
+ if (normalized !== msg && await verifyCandidate(normalized)) return publicKey;
983
+ if (fetchedKey.cached) {
984
+ logger.debug("Failed to verify the proof with the cached key {keyId}; retrying with the freshly fetched key...", {
611
985
  keyId: proof.verificationMethodId.href,
612
986
  proof
613
987
  });
614
- return null;
988
+ return await verifyProof(jsonLd, proof, {
989
+ ...options,
990
+ keyCache: {
991
+ get: () => Promise.resolve(void 0),
992
+ set: async (keyId, key) => await options.keyCache?.set(keyId, key)
993
+ }
994
+ });
615
995
  }
616
- return publicKey;
996
+ logger.debug("Failed to verify the proof with the fetched key {keyId}:\n{proof}", {
997
+ keyId: proof.verificationMethodId.href,
998
+ proof
999
+ });
1000
+ return null;
617
1001
  }
618
1002
  /**
619
1003
  * Verifies the given object. It will verify all the proofs in the object,
@@ -654,4 +1038,4 @@ async function verifyObject(cls, jsonLd, options = {}) {
654
1038
  return object;
655
1039
  }
656
1040
  //#endregion
657
- export { verifyProof as a, attachSignature as c, hasSignatureLike as d, signJsonLd as f, verifyObject as i, createSignature as l, verifySignature as m, hasProofLike as n, doesActorOwnKey as o, verifyJsonLd as p, signObject as r, getKeyOwner as s, createProof as t, detachSignature as u };
1041
+ export { verifyProof as a, getKeyOwner as c, detachSignature as d, hasSignatureLike as f, verifySignature as h, verifyObject as i, attachSignature as l, verifyJsonLd as m, hasProofLike as n, normalizeOutgoingActivityJsonLd as o, signJsonLd as p, signObject as r, doesActorOwnKey as s, createProof as t, createSignature as u };