@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,6 +1,6 @@
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-DiNUVHGB.js";
3
+ import { _ as version, d as validateCryptoKey, g as name, s as fetchKey } from "./http-DMkdP3lE.js";
4
4
  import { getLogger } from "@logtape/logtape";
5
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";
@@ -10,7 +10,7 @@ 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$2 = 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$2.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$2.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$2.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$2.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$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}", {
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$2.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$2.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,8 +386,27 @@ 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
389
408
  //#region src/compat/public-audience.ts
390
- const logger$1 = getLogger([
409
+ const logger$2 = getLogger([
391
410
  "fedify",
392
411
  "compat",
393
412
  "public-audience"
@@ -399,20 +418,12 @@ const PUBLIC_ADDRESSING_FIELDS = new Set([
399
418
  "bcc",
400
419
  "audience"
401
420
  ]);
402
- const preloadedOnlyDocumentLoader = (url) => {
403
- if (Object.hasOwn(preloadedContexts, url)) return Promise.resolve({
404
- contextUrl: null,
405
- documentUrl: url,
406
- document: preloadedContexts[url]
407
- });
408
- return Promise.reject(/* @__PURE__ */ new Error("Refusing to fetch a non-preloaded JSON-LD context: " + url));
409
- };
410
- const AS_CONTEXT_URL = "https://www.w3.org/ns/activitystreams";
411
- const MAX_TRAVERSAL_DEPTH = 64;
412
- const KNOWN_SAFE_CONTEXT_URLS = new Set(Object.keys(preloadedContexts));
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));
413
424
  function hasPublicCurieInAddressing(value, parentKey, depth = 0) {
414
425
  if (typeof value === "string") return parentKey != null && PUBLIC_ADDRESSING_FIELDS.has(parentKey) && (value === "as:Public" || value === "Public");
415
- if (depth >= MAX_TRAVERSAL_DEPTH) return false;
426
+ if (depth >= MAX_TRAVERSAL_DEPTH$1) return false;
416
427
  if (Array.isArray(value)) return value.some((item) => hasPublicCurieInAddressing(item, parentKey, depth + 1));
417
428
  if (typeof value !== "object" || value == null) return false;
418
429
  const record = value;
@@ -424,7 +435,7 @@ function hasPublicCurieInAddressing(value, parentKey, depth = 0) {
424
435
  }
425
436
  function rewritePublicAudience(value, parentKey, depth = 0) {
426
437
  if (typeof value === "string" && parentKey != null && PUBLIC_ADDRESSING_FIELDS.has(parentKey) && (value === "as:Public" || value === "Public")) return PUBLIC_COLLECTION.href;
427
- if (depth >= MAX_TRAVERSAL_DEPTH) return value;
438
+ if (depth >= MAX_TRAVERSAL_DEPTH$1) return value;
428
439
  if (Array.isArray(value)) {
429
440
  let changed = false;
430
441
  const mapped = value.map((item) => {
@@ -452,14 +463,14 @@ function rewritePublicAudience(value, parentKey, depth = 0) {
452
463
  * even when the top-level `@context` is safe, so the fast path must defer
453
464
  * to the URDNA2015 equivalence check whenever one is present.
454
465
  */
455
- function hasNestedContext(value, depth = 0) {
456
- if (depth >= MAX_TRAVERSAL_DEPTH) return true;
457
- if (Array.isArray(value)) return value.some((item) => hasNestedContext(item, depth + 1));
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));
458
469
  if (typeof value !== "object" || value == null) return false;
459
470
  const record = value;
460
471
  for (const key of Object.keys(record)) {
461
472
  if (key === "@context") return true;
462
- if (hasNestedContext(record[key], depth + 1)) return true;
473
+ if (hasNestedContext$1(record[key], depth + 1)) return true;
463
474
  }
464
475
  return false;
465
476
  }
@@ -475,7 +486,7 @@ function hasNestedContext(value, depth = 0) {
475
486
  * objects at the top level, nested `@context` blocks) is treated as
476
487
  * potentially unsafe.
477
488
  */
478
- function hasKnownSafeContext(jsonLd) {
489
+ function hasKnownSafeContext$1(jsonLd) {
479
490
  if (typeof jsonLd !== "object" || jsonLd == null) return false;
480
491
  const record = jsonLd;
481
492
  if (!Object.hasOwn(record, "@context")) return false;
@@ -485,13 +496,13 @@ function hasKnownSafeContext(jsonLd) {
485
496
  let hasAs = false;
486
497
  for (const entry of entries) {
487
498
  if (typeof entry !== "string") return false;
488
- if (!KNOWN_SAFE_CONTEXT_URLS.has(entry)) return false;
489
- if (entry === AS_CONTEXT_URL) hasAs = true;
499
+ if (!KNOWN_SAFE_CONTEXT_URLS$1.has(entry)) return false;
500
+ if (entry === AS_CONTEXT_URL$1) hasAs = true;
490
501
  }
491
502
  if (!hasAs) return false;
492
503
  for (const key of Object.keys(record)) {
493
504
  if (key === "@context") continue;
494
- if (hasNestedContext(record[key])) return false;
505
+ if (hasNestedContext$1(record[key])) return false;
495
506
  }
496
507
  return true;
497
508
  }
@@ -541,6 +552,192 @@ function hasKnownSafeContext(jsonLd) {
541
552
  async function normalizePublicAudience(jsonLd, contextLoader) {
542
553
  if (!hasPublicCurieInAddressing(jsonLd)) return jsonLd;
543
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
+ }
544
741
  if (hasKnownSafeContext(jsonLd)) return normalized;
545
742
  const loader = contextLoader ?? preloadedOnlyDocumentLoader;
546
743
  try {
@@ -552,12 +749,21 @@ async function normalizePublicAudience(jsonLd, contextLoader) {
552
749
  documentLoader: loader
553
750
  })]);
554
751
  if (before === after) return normalized;
555
- 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.");
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));
556
753
  } catch (error) {
557
- logger$1.debug("Failed to verify public audience normalization equivalence via JSON-LD canonicalization; sending the activity as is.\n{error}", { 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 });
558
755
  }
559
756
  return jsonLd;
560
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
+ }
561
767
  //#endregion
562
768
  //#region src/sig/proof.ts
563
769
  const logger = getLogger([
@@ -612,7 +818,7 @@ async function createProof(object, privateKey, keyId, { contextLoader, context,
612
818
  contextLoader,
613
819
  context
614
820
  });
615
- compactMsg = await normalizePublicAudience(compactMsg, contextLoader);
821
+ compactMsg = await normalizeOutgoingActivityJsonLd(compactMsg, contextLoader);
616
822
  const msgCanon = serialize(compactMsg);
617
823
  const encoder = new TextEncoder();
618
824
  const msgBytes = encoder.encode(msgCanon);
@@ -723,9 +929,6 @@ async function verifyProofInternal(jsonLd, proof, options) {
723
929
  const msg = { ...jsonLd };
724
930
  if ("proof" in msg) delete msg.proof;
725
931
  if ("https://w3id.org/security#proof" in msg) delete msg["https://w3id.org/security#proof"];
726
- const candidates = [msg];
727
- const normalized = await normalizePublicAudience(msg, options.contextLoader);
728
- if (normalized !== msg) candidates.push(normalized);
729
932
  let fetchedKey;
730
933
  try {
731
934
  fetchedKey = await publicKeyPromise;
@@ -767,12 +970,16 @@ async function verifyProofInternal(jsonLd, proof, options) {
767
970
  }
768
971
  const digest = new Uint8Array(proofDigest.byteLength + 32);
769
972
  digest.set(new Uint8Array(proofDigest), 0);
770
- for (const candidate of candidates) {
973
+ const proofValue = proof.proofValue;
974
+ const verifyCandidate = async (candidate) => {
771
975
  const msgBytes = encoder.encode(serialize(candidate));
772
976
  const msgDigest = await crypto.subtle.digest("SHA-256", msgBytes);
773
977
  digest.set(new Uint8Array(msgDigest), proofDigest.byteLength);
774
- if (await crypto.subtle.verify("Ed25519", publicKey.publicKey, proof.proofValue.slice(), digest)) return publicKey;
775
- }
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;
776
983
  if (fetchedKey.cached) {
777
984
  logger.debug("Failed to verify the proof with the cached key {keyId}; retrying with the freshly fetched key...", {
778
985
  keyId: proof.verificationMethodId.href,
@@ -831,4 +1038,4 @@ async function verifyObject(cls, jsonLd, options = {}) {
831
1038
  return object;
832
1039
  }
833
1040
  //#endregion
834
- 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, normalizePublicAudience as o, signJsonLd as p, signObject as r, doesActorOwnKey as s, createProof as t, createSignature 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 };
@@ -5,6 +5,25 @@ import { PUBLIC_COLLECTION } from "@fedify/vocab";
5
5
  import { preloadedContexts } from "@fedify/vocab-runtime";
6
6
  import { getLogger } from "@logtape/logtape";
7
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
8
27
  //#region src/compat/public-audience.ts
9
28
  const logger = getLogger([
10
29
  "fedify",
@@ -18,14 +37,6 @@ const PUBLIC_ADDRESSING_FIELDS = new Set([
18
37
  "bcc",
19
38
  "audience"
20
39
  ]);
21
- const preloadedOnlyDocumentLoader = (url) => {
22
- if (Object.hasOwn(preloadedContexts, url)) return Promise.resolve({
23
- contextUrl: null,
24
- documentUrl: url,
25
- document: preloadedContexts[url]
26
- });
27
- return Promise.reject(/* @__PURE__ */ new Error("Refusing to fetch a non-preloaded JSON-LD context: " + url));
28
- };
29
40
  const AS_CONTEXT_URL = "https://www.w3.org/ns/activitystreams";
30
41
  const MAX_TRAVERSAL_DEPTH = 64;
31
42
  const KNOWN_SAFE_CONTEXT_URLS = new Set(Object.keys(preloadedContexts));
@@ -178,4 +189,4 @@ async function normalizePublicAudience(jsonLd, contextLoader) {
178
189
  return jsonLd;
179
190
  }
180
191
  //#endregion
181
- export { normalizePublicAudience as t };
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-BukNyK1t.mjs";
5
- import { n as doubleKnock } from "./http-DSghOjS0.mjs";
4
+ import { n as version, t as name } from "./deno-sVjM503s.mjs";
5
+ import { n as doubleKnock } from "./http-pO-cqL07.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-DAfSmMg7.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-DSghOjS0.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-Ch1SiRyF.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-pO-cqL07.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-DAfSmMg7.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-Ch1SiRyF.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-DAfSmMg7.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-DYpo7uUC.mjs";
7
+ import { t as assert } from "../assert-DikXweDx.mjs";
8
+ import { i as generateCryptoKeyPair } from "../key-Ch1SiRyF.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-DkpX94b7.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-1uLerNXX.cjs");
5
- const require_proof = require("../proof-DgRfG4AE.cjs");
4
+ const require_http = require("../http-De4te5mA.cjs");
5
+ const require_proof = require("../proof-Bku-2gS1.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-DiNUVHGB.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-DdnQ5edt.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-DMkdP3lE.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-DONzhIHm.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-B0_w8O-Y.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-uOWCZ4oR.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