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

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 (96) hide show
  1. package/dist/{assert-CebeUf0C.mjs → assert-D1Eafmd_.mjs} +1 -1
  2. package/dist/{assert_equals-CBlRfA-_.mjs → assert_equals-Z6IcEbWU.mjs} +2 -28
  3. package/dist/{assert_exists-DtzkXC47.mjs → assert_exists-cffSpJ7X.mjs} +1 -1
  4. package/dist/{assert_false-3pLLLsO4.mjs → assert_false-CPdRnc3E.mjs} +1 -1
  5. package/dist/{assert_greater_or_equal-BApOTh1h.mjs → assert_greater_or_equal-BlafOF4-.mjs} +2 -1
  6. package/dist/{assert_instance_of-g3WcyYNR.mjs → assert_instance_of-mrXWWNY-.mjs} +1 -1
  7. package/dist/{assert_not_equals-BN7yDMWe.mjs → assert_not_equals-CpPK2t89.mjs} +2 -1
  8. package/dist/{assert_rejects-wFbSPn1O.mjs → assert_rejects-CTptdYck.mjs} +2 -2
  9. package/dist/{assert_strict_equals-Bpjl4Psw.mjs → assert_strict_equals-CYvBmkpC.mjs} +2 -1
  10. package/dist/{assert_throws-BJ5wuvR2.mjs → assert_throws-BYITOvPO.mjs} +2 -1
  11. package/dist/assertion_error-B-cx0fFA.mjs +31 -0
  12. package/dist/{builder-D0DxgOV5.mjs → builder-BM255b90.mjs} +2 -2
  13. package/dist/compat/outgoing-jsonld.test.mjs +71 -3
  14. package/dist/compat/public-audience.test.mjs +3 -3
  15. package/dist/compat/secured-document.test.d.mts +2 -0
  16. package/dist/compat/secured-document.test.mjs +58 -0
  17. package/dist/compat/transformers.test.mjs +4 -4
  18. package/dist/{compound-proof-CJnWc6c-.mjs → compound-proof-CPvNisT2.mjs} +1 -1
  19. package/dist/{deno-CABhzCb_.mjs → deno-BcZNXlJn.mjs} +1 -1
  20. package/dist/{docloader-BbGSL0Xq.mjs → docloader-iHi0-9mK.mjs} +2 -2
  21. package/dist/federation/builder.test.mjs +5 -5
  22. package/dist/federation/circuit-breaker.test.mjs +3 -3
  23. package/dist/federation/collection.test.mjs +2 -2
  24. package/dist/federation/compound-fanout.test.d.mts +2 -0
  25. package/dist/federation/compound-fanout.test.mjs +228 -0
  26. package/dist/federation/handler-compound-proof.test.mjs +5 -5
  27. package/dist/federation/handler.test.mjs +11 -11
  28. package/dist/federation/idempotency.test.mjs +4 -4
  29. package/dist/federation/inbox.test.mjs +2 -2
  30. package/dist/federation/keycache.test.mjs +3 -3
  31. package/dist/federation/kv.test.mjs +2 -2
  32. package/dist/federation/metrics.test.mjs +4 -4
  33. package/dist/federation/middleware.test.mjs +17 -17
  34. package/dist/federation/mod.cjs +1 -1
  35. package/dist/federation/mod.js +1 -1
  36. package/dist/federation/mq.test.mjs +7 -6
  37. package/dist/federation/negotiation.test.mjs +3 -3
  38. package/dist/federation/retry.test.mjs +3 -2
  39. package/dist/federation/router.test.mjs +5 -5
  40. package/dist/federation/send.test.mjs +11 -11
  41. package/dist/federation/tasks/enqueue.test.mjs +1 -1
  42. package/dist/federation/tasks/tasks.test.mjs +2 -2
  43. package/dist/federation/temporal.test.mjs +4 -4
  44. package/dist/federation/webfinger.test.mjs +4 -4
  45. package/dist/{federation-DCEHQk4G.cjs → federation-C0ijTwhx.cjs} +21 -5
  46. package/dist/{http-CRFns-UE.cjs → http-CWXKwHPv.cjs} +1 -1
  47. package/dist/{http-9WyuldYh.js → http-KZWtR1H8.js} +1 -1
  48. package/dist/{http-D9urSO2D.mjs → http-fIK5D83n.mjs} +3 -3
  49. package/dist/{key-Ccs8cnd7.mjs → key-Xb1hVVZO.mjs} +2 -2
  50. package/dist/{kv-cache-lKyFn_Dn.mjs → kv-cache-CU1rmQFB.mjs} +1 -1
  51. package/dist/{kv-cache-DXT9vmIg.cjs → kv-cache-Dos4tOWb.cjs} +1 -1
  52. package/dist/{kv-cache-DtmXhPXG.js → kv-cache-HOO6BsuW.js} +1 -1
  53. package/dist/{ld-qup__MAW.mjs → ld-DFHvxLtx.mjs} +3 -3
  54. package/dist/{metrics-CISXCvB1.mjs → metrics-D7RydU_p.mjs} +1 -1
  55. package/dist/{middleware-CzvJvPHK.mjs → middleware-BsYD_ufs.mjs} +32 -16
  56. package/dist/{middleware-DkrU2WhY.mjs → middleware-DgU5bJnn.mjs} +1 -1
  57. package/dist/{middleware-DsfWajaD.js → middleware-iffqYoTV.js} +21 -5
  58. package/dist/{mod-UDwMjuHM.d.ts → mod-CNRAxXEb.d.ts} +1 -5
  59. package/dist/{mod-CbKDOnCt.d.cts → mod-CVQFWefn.d.cts} +1 -5
  60. package/dist/mod.cjs +4 -4
  61. package/dist/mod.d.cts +1 -1
  62. package/dist/mod.d.ts +1 -1
  63. package/dist/mod.js +4 -4
  64. package/dist/nodeinfo/client.test.mjs +2 -2
  65. package/dist/nodeinfo/handler.test.mjs +3 -3
  66. package/dist/nodeinfo/types.test.mjs +3 -3
  67. package/dist/otel/exporter.test.mjs +2 -2
  68. package/dist/{outgoing-jsonld-B5Ne7JoL.mjs → outgoing-jsonld-4IJqPYos.mjs} +11 -9
  69. package/dist/{owner-BIPEZGiU.js → owner-B7lgRMdU.js} +127 -35
  70. package/dist/{owner-CRoq8Spb.mjs → owner-D53bUI-n.mjs} +2 -2
  71. package/dist/{owner-CVA7L9MA.cjs → owner-DffLVeYC.cjs} +126 -34
  72. package/dist/{proof-BDxNVZ3E.mjs → proof-4ADU3qy0.mjs} +80 -23
  73. package/dist/{public-audience-Cc2YJ3ip.mjs → public-audience--klBIOk1.mjs} +13 -9
  74. package/dist/secured-document-CxzZZ9nL.mjs +35 -0
  75. package/dist/{send-BzM5GLyn.mjs → send-n-W6bG7y.mjs} +3 -3
  76. package/dist/sig/compound-proof-discovery.test.mjs +3 -3
  77. package/dist/sig/compound-proof-verification.test.mjs +5 -5
  78. package/dist/sig/compound-proof.test.mjs +145 -58
  79. package/dist/sig/http.test.mjs +11 -10
  80. package/dist/sig/key.test.mjs +6 -6
  81. package/dist/sig/ld.test.mjs +9 -9
  82. package/dist/sig/mod.cjs +2 -2
  83. package/dist/sig/mod.d.cts +1 -1
  84. package/dist/sig/mod.d.ts +1 -1
  85. package/dist/sig/mod.js +2 -2
  86. package/dist/sig/owner.test.mjs +5 -5
  87. package/dist/sig/proof.test.mjs +9 -9
  88. package/dist/std__assert-DvMDRPes.mjs +8 -0
  89. package/dist/{temporal-Dv6Qv-8k.mjs → temporal-BFUyXrYg.mjs} +1 -1
  90. package/dist/testing/kv.test.mjs +2 -2
  91. package/dist/utils/docloader.test.mjs +5 -5
  92. package/dist/utils/kv-cache.test.mjs +1 -1
  93. package/dist/utils/mod.cjs +1 -1
  94. package/dist/utils/mod.js +1 -1
  95. package/package.json +6 -6
  96. package/dist/std__assert-CxFT0MNt.mjs +0 -8
@@ -1,11 +1,12 @@
1
1
  import { Temporal } from "temporal-polyfill";
2
2
  import { URLPattern } from "urlpattern-polyfill";
3
- import { I as name, L as version, c as fetchKey, f as validateCryptoKey, h as getFederationMetrics, m as getDurationMs, o as exportJwk, p as verifyKeyOwnership, s as fetchActorDocument, y as measureSignatureKeyFetch } from "./http-9WyuldYh.js";
3
+ import { I as name, L as version, c as fetchKey, f as validateCryptoKey, h as getFederationMetrics, m as getDurationMs, o as exportJwk, p as verifyKeyOwnership, s as fetchActorDocument, y as measureSignatureKeyFetch } from "./http-KZWtR1H8.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";
7
7
  import { encodeHex } from "byte-encodings/hex";
8
- import { formatIri, getDocumentLoader, getFe34Origin, haveSameFe34Origin, parseIri, preloadedContexts } from "@fedify/vocab-runtime";
8
+ import { encodeMultibase, formatIri, getDocumentLoader, getFe34Origin, haveSameFe34Origin, parseIri, preloadedContexts } from "@fedify/vocab-runtime";
9
+ import { isPlainJsonTree, retainSignedRepresentation } from "@fedify/vocab-runtime/internal/signed-representation";
9
10
  import jsonld from "@fedify/vocab-runtime/jsonld";
10
11
  import serialize from "json-canon";
11
12
  import { decodeBase64, encodeBase64 } from "byte-encodings/base64";
@@ -28,6 +29,37 @@ const preloadedOnlyDocumentLoader = (url) => {
28
29
  return Promise.reject(/* @__PURE__ */ new Error("Refusing to fetch a non-preloaded JSON-LD context: " + url));
29
30
  };
30
31
  //#endregion
32
+ //#region src/compat/secured-document.ts
33
+ function isJsonMap$1(value) {
34
+ return typeof value === "object" && value != null && !Array.isArray(value);
35
+ }
36
+ function isNonEmptyString(value) {
37
+ return typeof value === "string" && value.length > 0;
38
+ }
39
+ function hasDataIntegrityProofType(value) {
40
+ return (Array.isArray(value) ? value : [value]).some((type) => type === "DataIntegrityProof" || type === "https://w3id.org/security#DataIntegrityProof");
41
+ }
42
+ /**
43
+ * Checks whether a JSON value is a complete, self-contained secured document:
44
+ * a map with its own `@context` and exactly one direct, map-valued `proof`
45
+ * that carries every member `eddsa-jcs-2022` proof generation produces.
46
+ *
47
+ * This is deliberately stricter than {@link hasProofLike}, which exists to
48
+ * decide whether a document is worth verifying. Here a false positive would
49
+ * silently suppress a wire-compatibility fix on an ordinary document, so a
50
+ * partial or malformed proof does not qualify.
51
+ *
52
+ * @internal
53
+ */
54
+ function isSelfContainedSecuredDocument(value) {
55
+ if (!isJsonMap$1(value)) return false;
56
+ if (!Object.hasOwn(value, "@context") || value["@context"] == null) return false;
57
+ if (!Object.hasOwn(value, "proof")) return false;
58
+ const proof = value.proof;
59
+ if (!isJsonMap$1(proof)) return false;
60
+ return hasDataIntegrityProofType(proof.type) && isNonEmptyString(proof.cryptosuite) && isNonEmptyString(proof.created) && isNonEmptyString(proof.verificationMethod) && isNonEmptyString(proof.proofPurpose) && isNonEmptyString(proof.proofValue);
61
+ }
62
+ //#endregion
31
63
  //#region src/compat/public-audience.ts
32
64
  const logger$4 = getLogger([
33
65
  "fedify",
@@ -44,37 +76,39 @@ const PUBLIC_ADDRESSING_FIELDS = /* @__PURE__ */ new Set([
44
76
  const AS_CONTEXT_URL$1 = "https://www.w3.org/ns/activitystreams";
45
77
  const MAX_TRAVERSAL_DEPTH$1 = 64;
46
78
  const KNOWN_SAFE_CONTEXT_URLS$1 = new Set(Object.keys(preloadedContexts));
47
- function hasPublicCurieInAddressing(value, parentKey, depth = 0) {
79
+ function hasPublicCurieInAddressing(value, preserveSecured, parentKey, depth = 0) {
48
80
  if (typeof value === "string") return parentKey != null && PUBLIC_ADDRESSING_FIELDS.has(parentKey) && (value === "as:Public" || value === "Public");
49
81
  if (depth >= MAX_TRAVERSAL_DEPTH$1) return false;
50
- if (Array.isArray(value)) return value.some((item) => hasPublicCurieInAddressing(item, parentKey, depth + 1));
82
+ if (Array.isArray(value)) return value.some((item) => hasPublicCurieInAddressing(item, preserveSecured, parentKey, depth + 1));
51
83
  if (typeof value !== "object" || value == null) return false;
84
+ if (preserveSecured && depth > 0 && isSelfContainedSecuredDocument(value)) return false;
52
85
  const record = value;
53
86
  for (const key of Object.keys(record)) {
54
87
  if (record.type === "Follow" && key === "object" && (record[key] === "as:Public" || record[key] === "Public")) return true;
55
88
  if (key === "@context") continue;
56
- if (hasPublicCurieInAddressing(record[key], key, depth + 1)) return true;
89
+ if (hasPublicCurieInAddressing(record[key], preserveSecured, key, depth + 1)) return true;
57
90
  }
58
91
  return false;
59
92
  }
60
- function rewritePublicAudience(value, parentKey, depth = 0) {
93
+ function rewritePublicAudience(value, preserveSecured, parentKey, depth = 0) {
61
94
  if (typeof value === "string" && parentKey != null && PUBLIC_ADDRESSING_FIELDS.has(parentKey) && (value === "as:Public" || value === "Public")) return PUBLIC_COLLECTION.href;
62
95
  if (depth >= MAX_TRAVERSAL_DEPTH$1) return value;
63
96
  if (Array.isArray(value)) {
64
97
  let changed = false;
65
98
  const mapped = value.map((item) => {
66
- const rewritten = rewritePublicAudience(item, parentKey, depth + 1);
99
+ const rewritten = rewritePublicAudience(item, preserveSecured, parentKey, depth + 1);
67
100
  if (rewritten !== item) changed = true;
68
101
  return rewritten;
69
102
  });
70
103
  return changed ? mapped : value;
71
104
  }
72
105
  if (typeof value !== "object" || value == null) return value;
106
+ if (preserveSecured && depth > 0 && isSelfContainedSecuredDocument(value)) return value;
73
107
  const record = value;
74
108
  let changed = false;
75
109
  const normalized = Object.create(null);
76
110
  for (const key of Object.keys(record)) {
77
- const rewritten = key === "@context" ? record[key] : record.type === "Follow" && key === "object" && (record[key] === "as:Public" || record[key] === "Public") ? PUBLIC_COLLECTION.href : rewritePublicAudience(record[key], key, depth + 1);
111
+ const rewritten = key === "@context" ? record[key] : record.type === "Follow" && key === "object" && (record[key] === "as:Public" || record[key] === "Public") ? PUBLIC_COLLECTION.href : rewritePublicAudience(record[key], preserveSecured, key, depth + 1);
78
112
  if (rewritten !== record[key]) changed = true;
79
113
  normalized[key] = rewritten;
80
114
  }
@@ -173,9 +207,10 @@ function hasKnownSafeContext$1(jsonLd) {
173
207
  * using the `eddsa-jcs-2022` cryptosuite), so the signed payload
174
208
  * matches what is sent on the wire.
175
209
  */
176
- async function normalizePublicAudience(jsonLd, contextLoader) {
177
- if (!hasPublicCurieInAddressing(jsonLd)) return jsonLd;
178
- const normalized = rewritePublicAudience(jsonLd);
210
+ async function normalizePublicAudience(jsonLd, contextLoader, options = {}) {
211
+ const preserveSecured = options.preserveNestedSecuredDocuments ?? false;
212
+ if (!hasPublicCurieInAddressing(jsonLd, preserveSecured)) return jsonLd;
213
+ const normalized = rewritePublicAudience(jsonLd, preserveSecured);
179
214
  if (hasKnownSafeContext$1(jsonLd)) return normalized;
180
215
  const loader = contextLoader ?? preloadedOnlyDocumentLoader;
181
216
  try {
@@ -253,13 +288,14 @@ function getKnownSafeContextUrls() {
253
288
  /**
254
289
  * Wraps scalar ActivityStreams attachment properties in arrays.
255
290
  */
256
- function wrapScalarAttachments(jsonLd, depth = 0) {
291
+ function wrapScalarAttachments(jsonLd, preserveSecured, depth = 0) {
257
292
  if (depth >= MAX_TRAVERSAL_DEPTH) return jsonLd;
293
+ if (preserveSecured && depth > 0 && isSelfContainedSecuredDocument(jsonLd)) return jsonLd;
258
294
  if (Array.isArray(jsonLd)) {
259
295
  let normalized = null;
260
296
  for (let i = 0; i < jsonLd.length; i++) {
261
297
  const item = jsonLd[i];
262
- const next = wrapScalarAttachments(item, depth + 1);
298
+ const next = wrapScalarAttachments(item, preserveSecured, depth + 1);
263
299
  if (normalized == null && next !== item) normalized = jsonLd.slice(0, i);
264
300
  if (normalized != null) normalized[i] = next;
265
301
  }
@@ -272,7 +308,7 @@ function wrapScalarAttachments(jsonLd, depth = 0) {
272
308
  for (let i = 0; i < keys.length; i++) {
273
309
  const key = keys[i];
274
310
  const value = record[key];
275
- const next = key === "@context" || key === "@value" && isJsonLdValueObject(jsonLd) ? value : wrapScalarAttachments(value, depth + 1);
311
+ const next = key === "@context" || key === "@value" && isJsonLdValueObject(jsonLd) ? value : wrapScalarAttachments(value, preserveSecured, depth + 1);
276
312
  const output = ATTACHMENT_FIELDS.has(key) && next != null && !Array.isArray(next) && !isJsonLdListObject(next) ? [next] : next;
277
313
  if (normalized == null && output !== value) {
278
314
  const cloned = Object.create(null);
@@ -355,8 +391,8 @@ function getLogSafeJsonLdMetadata(jsonLd) {
355
391
  * the semantic-preservation check to succeed; otherwise canonicalization
356
392
  * failures leave the original document unchanged.
357
393
  */
358
- async function normalizeAttachmentArrays(jsonLd, contextLoader) {
359
- const normalized = wrapScalarAttachments(jsonLd);
394
+ async function normalizeAttachmentArrays(jsonLd, contextLoader, options = {}) {
395
+ const normalized = wrapScalarAttachments(jsonLd, options.preserveNestedSecuredDocuments ?? false);
360
396
  if (normalized === jsonLd) return jsonLd;
361
397
  if (exceedsTraversalDepth(jsonLd)) {
362
398
  logger$3.debug("Skipping attachment array normalization because the JSON-LD document exceeds the safe traversal depth; leaving it unchanged.");
@@ -384,9 +420,9 @@ async function normalizeAttachmentArrays(jsonLd, contextLoader) {
384
420
  * to locally generated outgoing activities before they are signed, enqueued,
385
421
  * or sent.
386
422
  */
387
- async function normalizeOutgoingActivityJsonLd(jsonLd, contextLoader) {
388
- jsonLd = await normalizePublicAudience(jsonLd, contextLoader);
389
- return await normalizeAttachmentArrays(jsonLd, contextLoader);
423
+ async function normalizeOutgoingActivityJsonLd(jsonLd, contextLoader, options = {}) {
424
+ jsonLd = await normalizePublicAudience(jsonLd, contextLoader, options);
425
+ return await normalizeAttachmentArrays(jsonLd, contextLoader, options);
390
426
  }
391
427
  //#endregion
392
428
  //#region src/sig/ld.ts
@@ -1055,17 +1091,49 @@ function hasProofLike(jsonLd) {
1055
1091
  };
1056
1092
  return Array.isArray(proof) ? proof.some(isProofLike) : isProofLike(proof);
1057
1093
  }
1094
+ function isJsonMap(value) {
1095
+ return typeof value === "object" && value != null && !Array.isArray(value);
1096
+ }
1058
1097
  /**
1059
- * Creates a proof for the given object.
1060
- * @param object The object to create a proof for.
1061
- * @param privateKey The private key to sign the proof with.
1062
- * @param keyId The key ID to use in the proof. It will be used by the verifier.
1063
- * @param options Additional options. See also {@link CreateProofOptions}.
1064
- * @returns The created proof.
1065
- * @throws {TypeError} If the private key is invalid or unsupported.
1066
- * @since 0.10.0
1098
+ * Assembles the secured JSON document from the bytes that were just signed.
1099
+ *
1100
+ * The document is not re-derived from the vocabulary object: it is
1101
+ * `compactMsg`, the value whose JCS form was hashed, with the serialized
1102
+ * proof added. Every part of it is then checked against the digests and the
1103
+ * signature that {@link createProofInternal} produced, so a document that is
1104
+ * returned verifies under the map-local compound-proof profile by
1105
+ * construction. Anything that does not check out yields `null`, and the
1106
+ * caller simply does not retain a representation.
1067
1107
  */
1068
- async function createProof(object, privateKey, keyId, { contextLoader, context, created } = {}) {
1108
+ async function captureSecuredDocument(proof, compactMsg, msgCanon, proofCanon, proofValue, contextLoader) {
1109
+ if (!isJsonMap(compactMsg)) return null;
1110
+ const documentContext = compactMsg["@context"];
1111
+ if (documentContext == null) return null;
1112
+ if (globalThis.Object.hasOwn(compactMsg, "proof")) return null;
1113
+ try {
1114
+ const proofJson = await proof.toJsonLd({
1115
+ format: "compact",
1116
+ contextLoader,
1117
+ context: documentContext
1118
+ });
1119
+ if (!isJsonMap(proofJson)) return null;
1120
+ const { proofValue: serializedProofValue, ...proofConfiguration } = proofJson;
1121
+ if (serializedProofValue !== proofValue) return null;
1122
+ if (serialize(proofConfiguration) !== proofCanon) return null;
1123
+ const securedDocument = {
1124
+ ...structuredClone(compactMsg),
1125
+ proof: structuredClone(proofJson)
1126
+ };
1127
+ const { proof: _embeddedProof, ...unsecuredDocument } = securedDocument;
1128
+ if (serialize(unsecuredDocument) !== msgCanon) return null;
1129
+ if (!isPlainJsonTree(securedDocument)) return null;
1130
+ return securedDocument;
1131
+ } catch (error) {
1132
+ logger$1.debug("Failed to capture the secured JSON document for a created proof; the signed object will not preserve its representation when it is embedded in another object.\n{error}", { error });
1133
+ return null;
1134
+ }
1135
+ }
1136
+ async function createProofInternal(object, privateKey, keyId, { contextLoader, context, created } = {}, capture = false) {
1069
1137
  validateCryptoKey(privateKey, "private");
1070
1138
  if (privateKey.algorithm.name !== "Ed25519") throw new TypeError("Unsupported algorithm: " + privateKey.algorithm.name);
1071
1139
  let compactMsg = await object.clone({ proofs: [] }).toJsonLd({
@@ -1073,7 +1141,7 @@ async function createProof(object, privateKey, keyId, { contextLoader, context,
1073
1141
  contextLoader,
1074
1142
  context
1075
1143
  });
1076
- compactMsg = await normalizeOutgoingActivityJsonLd(compactMsg, contextLoader);
1144
+ compactMsg = await normalizeOutgoingActivityJsonLd(compactMsg, contextLoader, { preserveNestedSecuredDocuments: true });
1077
1145
  const msgCanon = serialize(compactMsg);
1078
1146
  const encoder = new TextEncoder();
1079
1147
  const msgBytes = encoder.encode(msgCanon);
@@ -1092,14 +1160,32 @@ async function createProof(object, privateKey, keyId, { contextLoader, context,
1092
1160
  const digest = new Uint8Array(proofDigest.byteLength + msgDigest.byteLength);
1093
1161
  digest.set(new Uint8Array(proofDigest), 0);
1094
1162
  digest.set(new Uint8Array(msgDigest), proofDigest.byteLength);
1095
- const sig = await crypto.subtle.sign("Ed25519", privateKey, digest);
1096
- return new DataIntegrityProof({
1163
+ const sig = new Uint8Array(await crypto.subtle.sign("Ed25519", privateKey, digest));
1164
+ const proof = new DataIntegrityProof({
1097
1165
  cryptosuite: "eddsa-jcs-2022",
1098
1166
  verificationMethod: keyId,
1099
1167
  proofPurpose: "assertionMethod",
1100
- created: created ?? Temporal.Now.instant(),
1101
- proofValue: new Uint8Array(sig)
1168
+ created,
1169
+ proofValue: sig
1102
1170
  });
1171
+ return {
1172
+ proof,
1173
+ securedDocument: capture ? await captureSecuredDocument(proof, compactMsg, msgCanon, proofCanon, new TextDecoder().decode(encodeMultibase("base58btc", sig)), contextLoader) : null
1174
+ };
1175
+ }
1176
+ /**
1177
+ * Creates a proof for the given object.
1178
+ * @param object The object to create a proof for.
1179
+ * @param privateKey The private key to sign the proof with.
1180
+ * @param keyId The key ID to use in the proof. It will be used by the verifier.
1181
+ * @param options Additional options. See also {@link CreateProofOptions}.
1182
+ * @returns The created proof.
1183
+ * @throws {TypeError} If the private key is invalid or unsupported.
1184
+ * @since 0.10.0
1185
+ */
1186
+ async function createProof(object, privateKey, keyId, options = {}) {
1187
+ const { proof } = await createProofInternal(object, privateKey, keyId, options);
1188
+ return proof;
1103
1189
  }
1104
1190
  /**
1105
1191
  * Signs the given object with the private key and returns the signed object.
@@ -1117,13 +1203,19 @@ async function signObject(object, privateKey, keyId, options = {}) {
1117
1203
  if (object.id != null) span.setAttribute("activitypub.object.id", object.id.href);
1118
1204
  const existingProofs = [];
1119
1205
  for await (const proof of object.getProofs(options)) existingProofs.push(proof);
1120
- const proof = await createProof(object, privateKey, keyId, options);
1206
+ const { proof, securedDocument } = await createProofInternal(object, privateKey, keyId, options, existingProofs.length < 1);
1121
1207
  if (span.isRecording()) {
1122
1208
  if (proof.cryptosuite != null) span.setAttribute("object_integrity_proofs.cryptosuite", proof.cryptosuite);
1123
1209
  if (proof.verificationMethodId != null) span.setAttribute("object_integrity_proofs.key_id", proof.verificationMethodId.href);
1124
1210
  if (proof.proofValue != null) span.setAttribute("object_integrity_proofs.signature", encodeHex(proof.proofValue));
1125
1211
  }
1126
- return object.clone({ proofs: [...existingProofs, proof] });
1212
+ const signed = object.clone({ proofs: [...existingProofs, proof] });
1213
+ if (securedDocument != null) retainSignedRepresentation(signed, securedDocument);
1214
+ else if (existingProofs.length > 0) logger$1.debug("The object {objectId} already had {proofCount} proof(s), so its signed representation is not retained for embedding; the map-local compound-proof profile accepts exactly one direct proof per map.", {
1215
+ objectId: object.id?.href,
1216
+ proofCount: existingProofs.length
1217
+ });
1218
+ return signed;
1127
1219
  } catch (error) {
1128
1220
  span.setStatus({
1129
1221
  code: SpanStatusCode.ERROR,
@@ -1,8 +1,8 @@
1
1
  import "temporal-polyfill";
2
2
  import "urlpattern-polyfill";
3
3
  globalThis.addEventListener = () => {};
4
- import { n as version, t as name } from "./deno-CABhzCb_.mjs";
5
- import { c as verifyKeyOwnership, n as fetchActorDocument, t as exportJwk } from "./key-Ccs8cnd7.mjs";
4
+ import { n as version, t as name } from "./deno-BcZNXlJn.mjs";
5
+ import { c as verifyKeyOwnership, n as fetchActorDocument, t as exportJwk } from "./key-Xb1hVVZO.mjs";
6
6
  import { getLogger } from "@logtape/logtape";
7
7
  import { CryptographicKey, Object as Object$1, isActor } from "@fedify/vocab";
8
8
  import { SpanKind, SpanStatusCode, trace } from "@opentelemetry/api";
@@ -1,11 +1,12 @@
1
1
  const { URLPattern } = require("urlpattern-polyfill");
2
2
  const require_rolldown_runtime = require("./rolldown-runtime-Dmm3t92W.cjs");
3
- const require_http = require("./http-CRFns-UE.cjs");
3
+ const require_http = require("./http-CWXKwHPv.cjs");
4
4
  let _logtape_logtape = require("@logtape/logtape");
5
5
  let _fedify_vocab = require("@fedify/vocab");
6
6
  let _opentelemetry_api = require("@opentelemetry/api");
7
7
  let byte_encodings_hex = require("byte-encodings/hex");
8
8
  let _fedify_vocab_runtime = require("@fedify/vocab-runtime");
9
+ let _fedify_vocab_runtime_internal_signed_representation = require("@fedify/vocab-runtime/internal/signed-representation");
9
10
  let _fedify_vocab_runtime_jsonld = require("@fedify/vocab-runtime/jsonld");
10
11
  _fedify_vocab_runtime_jsonld = require_rolldown_runtime.__toESM(_fedify_vocab_runtime_jsonld, 1);
11
12
  let json_canon = require("json-canon");
@@ -30,6 +31,37 @@ const preloadedOnlyDocumentLoader = (url) => {
30
31
  return Promise.reject(/* @__PURE__ */ new Error("Refusing to fetch a non-preloaded JSON-LD context: " + url));
31
32
  };
32
33
  //#endregion
34
+ //#region src/compat/secured-document.ts
35
+ function isJsonMap$1(value) {
36
+ return typeof value === "object" && value != null && !Array.isArray(value);
37
+ }
38
+ function isNonEmptyString(value) {
39
+ return typeof value === "string" && value.length > 0;
40
+ }
41
+ function hasDataIntegrityProofType(value) {
42
+ return (Array.isArray(value) ? value : [value]).some((type) => type === "DataIntegrityProof" || type === "https://w3id.org/security#DataIntegrityProof");
43
+ }
44
+ /**
45
+ * Checks whether a JSON value is a complete, self-contained secured document:
46
+ * a map with its own `@context` and exactly one direct, map-valued `proof`
47
+ * that carries every member `eddsa-jcs-2022` proof generation produces.
48
+ *
49
+ * This is deliberately stricter than {@link hasProofLike}, which exists to
50
+ * decide whether a document is worth verifying. Here a false positive would
51
+ * silently suppress a wire-compatibility fix on an ordinary document, so a
52
+ * partial or malformed proof does not qualify.
53
+ *
54
+ * @internal
55
+ */
56
+ function isSelfContainedSecuredDocument(value) {
57
+ if (!isJsonMap$1(value)) return false;
58
+ if (!Object.hasOwn(value, "@context") || value["@context"] == null) return false;
59
+ if (!Object.hasOwn(value, "proof")) return false;
60
+ const proof = value.proof;
61
+ if (!isJsonMap$1(proof)) return false;
62
+ return hasDataIntegrityProofType(proof.type) && isNonEmptyString(proof.cryptosuite) && isNonEmptyString(proof.created) && isNonEmptyString(proof.verificationMethod) && isNonEmptyString(proof.proofPurpose) && isNonEmptyString(proof.proofValue);
63
+ }
64
+ //#endregion
33
65
  //#region src/compat/public-audience.ts
34
66
  const logger$4 = (0, _logtape_logtape.getLogger)([
35
67
  "fedify",
@@ -46,37 +78,39 @@ const PUBLIC_ADDRESSING_FIELDS = /* @__PURE__ */ new Set([
46
78
  const AS_CONTEXT_URL$1 = "https://www.w3.org/ns/activitystreams";
47
79
  const MAX_TRAVERSAL_DEPTH$1 = 64;
48
80
  const KNOWN_SAFE_CONTEXT_URLS$1 = new Set(Object.keys(_fedify_vocab_runtime.preloadedContexts));
49
- function hasPublicCurieInAddressing(value, parentKey, depth = 0) {
81
+ function hasPublicCurieInAddressing(value, preserveSecured, parentKey, depth = 0) {
50
82
  if (typeof value === "string") return parentKey != null && PUBLIC_ADDRESSING_FIELDS.has(parentKey) && (value === "as:Public" || value === "Public");
51
83
  if (depth >= MAX_TRAVERSAL_DEPTH$1) return false;
52
- if (Array.isArray(value)) return value.some((item) => hasPublicCurieInAddressing(item, parentKey, depth + 1));
84
+ if (Array.isArray(value)) return value.some((item) => hasPublicCurieInAddressing(item, preserveSecured, parentKey, depth + 1));
53
85
  if (typeof value !== "object" || value == null) return false;
86
+ if (preserveSecured && depth > 0 && isSelfContainedSecuredDocument(value)) return false;
54
87
  const record = value;
55
88
  for (const key of Object.keys(record)) {
56
89
  if (record.type === "Follow" && key === "object" && (record[key] === "as:Public" || record[key] === "Public")) return true;
57
90
  if (key === "@context") continue;
58
- if (hasPublicCurieInAddressing(record[key], key, depth + 1)) return true;
91
+ if (hasPublicCurieInAddressing(record[key], preserveSecured, key, depth + 1)) return true;
59
92
  }
60
93
  return false;
61
94
  }
62
- function rewritePublicAudience(value, parentKey, depth = 0) {
95
+ function rewritePublicAudience(value, preserveSecured, parentKey, depth = 0) {
63
96
  if (typeof value === "string" && parentKey != null && PUBLIC_ADDRESSING_FIELDS.has(parentKey) && (value === "as:Public" || value === "Public")) return _fedify_vocab.PUBLIC_COLLECTION.href;
64
97
  if (depth >= MAX_TRAVERSAL_DEPTH$1) return value;
65
98
  if (Array.isArray(value)) {
66
99
  let changed = false;
67
100
  const mapped = value.map((item) => {
68
- const rewritten = rewritePublicAudience(item, parentKey, depth + 1);
101
+ const rewritten = rewritePublicAudience(item, preserveSecured, parentKey, depth + 1);
69
102
  if (rewritten !== item) changed = true;
70
103
  return rewritten;
71
104
  });
72
105
  return changed ? mapped : value;
73
106
  }
74
107
  if (typeof value !== "object" || value == null) return value;
108
+ if (preserveSecured && depth > 0 && isSelfContainedSecuredDocument(value)) return value;
75
109
  const record = value;
76
110
  let changed = false;
77
111
  const normalized = Object.create(null);
78
112
  for (const key of Object.keys(record)) {
79
- const rewritten = key === "@context" ? record[key] : record.type === "Follow" && key === "object" && (record[key] === "as:Public" || record[key] === "Public") ? _fedify_vocab.PUBLIC_COLLECTION.href : rewritePublicAudience(record[key], key, depth + 1);
113
+ const rewritten = key === "@context" ? record[key] : record.type === "Follow" && key === "object" && (record[key] === "as:Public" || record[key] === "Public") ? _fedify_vocab.PUBLIC_COLLECTION.href : rewritePublicAudience(record[key], preserveSecured, key, depth + 1);
80
114
  if (rewritten !== record[key]) changed = true;
81
115
  normalized[key] = rewritten;
82
116
  }
@@ -175,9 +209,10 @@ function hasKnownSafeContext$1(jsonLd) {
175
209
  * using the `eddsa-jcs-2022` cryptosuite), so the signed payload
176
210
  * matches what is sent on the wire.
177
211
  */
178
- async function normalizePublicAudience(jsonLd, contextLoader) {
179
- if (!hasPublicCurieInAddressing(jsonLd)) return jsonLd;
180
- const normalized = rewritePublicAudience(jsonLd);
212
+ async function normalizePublicAudience(jsonLd, contextLoader, options = {}) {
213
+ const preserveSecured = options.preserveNestedSecuredDocuments ?? false;
214
+ if (!hasPublicCurieInAddressing(jsonLd, preserveSecured)) return jsonLd;
215
+ const normalized = rewritePublicAudience(jsonLd, preserveSecured);
181
216
  if (hasKnownSafeContext$1(jsonLd)) return normalized;
182
217
  const loader = contextLoader ?? preloadedOnlyDocumentLoader;
183
218
  try {
@@ -255,13 +290,14 @@ function getKnownSafeContextUrls() {
255
290
  /**
256
291
  * Wraps scalar ActivityStreams attachment properties in arrays.
257
292
  */
258
- function wrapScalarAttachments(jsonLd, depth = 0) {
293
+ function wrapScalarAttachments(jsonLd, preserveSecured, depth = 0) {
259
294
  if (depth >= MAX_TRAVERSAL_DEPTH) return jsonLd;
295
+ if (preserveSecured && depth > 0 && isSelfContainedSecuredDocument(jsonLd)) return jsonLd;
260
296
  if (Array.isArray(jsonLd)) {
261
297
  let normalized = null;
262
298
  for (let i = 0; i < jsonLd.length; i++) {
263
299
  const item = jsonLd[i];
264
- const next = wrapScalarAttachments(item, depth + 1);
300
+ const next = wrapScalarAttachments(item, preserveSecured, depth + 1);
265
301
  if (normalized == null && next !== item) normalized = jsonLd.slice(0, i);
266
302
  if (normalized != null) normalized[i] = next;
267
303
  }
@@ -274,7 +310,7 @@ function wrapScalarAttachments(jsonLd, depth = 0) {
274
310
  for (let i = 0; i < keys.length; i++) {
275
311
  const key = keys[i];
276
312
  const value = record[key];
277
- const next = key === "@context" || key === "@value" && isJsonLdValueObject(jsonLd) ? value : wrapScalarAttachments(value, depth + 1);
313
+ const next = key === "@context" || key === "@value" && isJsonLdValueObject(jsonLd) ? value : wrapScalarAttachments(value, preserveSecured, depth + 1);
278
314
  const output = ATTACHMENT_FIELDS.has(key) && next != null && !Array.isArray(next) && !isJsonLdListObject(next) ? [next] : next;
279
315
  if (normalized == null && output !== value) {
280
316
  const cloned = Object.create(null);
@@ -357,8 +393,8 @@ function getLogSafeJsonLdMetadata(jsonLd) {
357
393
  * the semantic-preservation check to succeed; otherwise canonicalization
358
394
  * failures leave the original document unchanged.
359
395
  */
360
- async function normalizeAttachmentArrays(jsonLd, contextLoader) {
361
- const normalized = wrapScalarAttachments(jsonLd);
396
+ async function normalizeAttachmentArrays(jsonLd, contextLoader, options = {}) {
397
+ const normalized = wrapScalarAttachments(jsonLd, options.preserveNestedSecuredDocuments ?? false);
362
398
  if (normalized === jsonLd) return jsonLd;
363
399
  if (exceedsTraversalDepth(jsonLd)) {
364
400
  logger$3.debug("Skipping attachment array normalization because the JSON-LD document exceeds the safe traversal depth; leaving it unchanged.");
@@ -386,9 +422,9 @@ async function normalizeAttachmentArrays(jsonLd, contextLoader) {
386
422
  * to locally generated outgoing activities before they are signed, enqueued,
387
423
  * or sent.
388
424
  */
389
- async function normalizeOutgoingActivityJsonLd(jsonLd, contextLoader) {
390
- jsonLd = await normalizePublicAudience(jsonLd, contextLoader);
391
- return await normalizeAttachmentArrays(jsonLd, contextLoader);
425
+ async function normalizeOutgoingActivityJsonLd(jsonLd, contextLoader, options = {}) {
426
+ jsonLd = await normalizePublicAudience(jsonLd, contextLoader, options);
427
+ return await normalizeAttachmentArrays(jsonLd, contextLoader, options);
392
428
  }
393
429
  //#endregion
394
430
  //#region src/sig/ld.ts
@@ -1058,17 +1094,49 @@ function hasProofLike(jsonLd) {
1058
1094
  };
1059
1095
  return Array.isArray(proof) ? proof.some(isProofLike) : isProofLike(proof);
1060
1096
  }
1097
+ function isJsonMap(value) {
1098
+ return typeof value === "object" && value != null && !Array.isArray(value);
1099
+ }
1061
1100
  /**
1062
- * Creates a proof for the given object.
1063
- * @param object The object to create a proof for.
1064
- * @param privateKey The private key to sign the proof with.
1065
- * @param keyId The key ID to use in the proof. It will be used by the verifier.
1066
- * @param options Additional options. See also {@link CreateProofOptions}.
1067
- * @returns The created proof.
1068
- * @throws {TypeError} If the private key is invalid or unsupported.
1069
- * @since 0.10.0
1101
+ * Assembles the secured JSON document from the bytes that were just signed.
1102
+ *
1103
+ * The document is not re-derived from the vocabulary object: it is
1104
+ * `compactMsg`, the value whose JCS form was hashed, with the serialized
1105
+ * proof added. Every part of it is then checked against the digests and the
1106
+ * signature that {@link createProofInternal} produced, so a document that is
1107
+ * returned verifies under the map-local compound-proof profile by
1108
+ * construction. Anything that does not check out yields `null`, and the
1109
+ * caller simply does not retain a representation.
1070
1110
  */
1071
- async function createProof(object, privateKey, keyId, { contextLoader, context, created } = {}) {
1111
+ async function captureSecuredDocument(proof, compactMsg, msgCanon, proofCanon, proofValue, contextLoader) {
1112
+ if (!isJsonMap(compactMsg)) return null;
1113
+ const documentContext = compactMsg["@context"];
1114
+ if (documentContext == null) return null;
1115
+ if (globalThis.Object.hasOwn(compactMsg, "proof")) return null;
1116
+ try {
1117
+ const proofJson = await proof.toJsonLd({
1118
+ format: "compact",
1119
+ contextLoader,
1120
+ context: documentContext
1121
+ });
1122
+ if (!isJsonMap(proofJson)) return null;
1123
+ const { proofValue: serializedProofValue, ...proofConfiguration } = proofJson;
1124
+ if (serializedProofValue !== proofValue) return null;
1125
+ if ((0, json_canon.default)(proofConfiguration) !== proofCanon) return null;
1126
+ const securedDocument = {
1127
+ ...structuredClone(compactMsg),
1128
+ proof: structuredClone(proofJson)
1129
+ };
1130
+ const { proof: _embeddedProof, ...unsecuredDocument } = securedDocument;
1131
+ if ((0, json_canon.default)(unsecuredDocument) !== msgCanon) return null;
1132
+ if (!(0, _fedify_vocab_runtime_internal_signed_representation.isPlainJsonTree)(securedDocument)) return null;
1133
+ return securedDocument;
1134
+ } catch (error) {
1135
+ logger$1.debug("Failed to capture the secured JSON document for a created proof; the signed object will not preserve its representation when it is embedded in another object.\n{error}", { error });
1136
+ return null;
1137
+ }
1138
+ }
1139
+ async function createProofInternal(object, privateKey, keyId, { contextLoader, context, created } = {}, capture = false) {
1072
1140
  require_http.validateCryptoKey(privateKey, "private");
1073
1141
  if (privateKey.algorithm.name !== "Ed25519") throw new TypeError("Unsupported algorithm: " + privateKey.algorithm.name);
1074
1142
  let compactMsg = await object.clone({ proofs: [] }).toJsonLd({
@@ -1076,7 +1144,7 @@ async function createProof(object, privateKey, keyId, { contextLoader, context,
1076
1144
  contextLoader,
1077
1145
  context
1078
1146
  });
1079
- compactMsg = await normalizeOutgoingActivityJsonLd(compactMsg, contextLoader);
1147
+ compactMsg = await normalizeOutgoingActivityJsonLd(compactMsg, contextLoader, { preserveNestedSecuredDocuments: true });
1080
1148
  const msgCanon = (0, json_canon.default)(compactMsg);
1081
1149
  const encoder = new TextEncoder();
1082
1150
  const msgBytes = encoder.encode(msgCanon);
@@ -1095,14 +1163,32 @@ async function createProof(object, privateKey, keyId, { contextLoader, context,
1095
1163
  const digest = new Uint8Array(proofDigest.byteLength + msgDigest.byteLength);
1096
1164
  digest.set(new Uint8Array(proofDigest), 0);
1097
1165
  digest.set(new Uint8Array(msgDigest), proofDigest.byteLength);
1098
- const sig = await crypto.subtle.sign("Ed25519", privateKey, digest);
1099
- return new _fedify_vocab.DataIntegrityProof({
1166
+ const sig = new Uint8Array(await crypto.subtle.sign("Ed25519", privateKey, digest));
1167
+ const proof = new _fedify_vocab.DataIntegrityProof({
1100
1168
  cryptosuite: "eddsa-jcs-2022",
1101
1169
  verificationMethod: keyId,
1102
1170
  proofPurpose: "assertionMethod",
1103
- created: created ?? Temporal.Now.instant(),
1104
- proofValue: new Uint8Array(sig)
1171
+ created,
1172
+ proofValue: sig
1105
1173
  });
1174
+ return {
1175
+ proof,
1176
+ securedDocument: capture ? await captureSecuredDocument(proof, compactMsg, msgCanon, proofCanon, new TextDecoder().decode((0, _fedify_vocab_runtime.encodeMultibase)("base58btc", sig)), contextLoader) : null
1177
+ };
1178
+ }
1179
+ /**
1180
+ * Creates a proof for the given object.
1181
+ * @param object The object to create a proof for.
1182
+ * @param privateKey The private key to sign the proof with.
1183
+ * @param keyId The key ID to use in the proof. It will be used by the verifier.
1184
+ * @param options Additional options. See also {@link CreateProofOptions}.
1185
+ * @returns The created proof.
1186
+ * @throws {TypeError} If the private key is invalid or unsupported.
1187
+ * @since 0.10.0
1188
+ */
1189
+ async function createProof(object, privateKey, keyId, options = {}) {
1190
+ const { proof } = await createProofInternal(object, privateKey, keyId, options);
1191
+ return proof;
1106
1192
  }
1107
1193
  /**
1108
1194
  * Signs the given object with the private key and returns the signed object.
@@ -1120,13 +1206,19 @@ async function signObject(object, privateKey, keyId, options = {}) {
1120
1206
  if (object.id != null) span.setAttribute("activitypub.object.id", object.id.href);
1121
1207
  const existingProofs = [];
1122
1208
  for await (const proof of object.getProofs(options)) existingProofs.push(proof);
1123
- const proof = await createProof(object, privateKey, keyId, options);
1209
+ const { proof, securedDocument } = await createProofInternal(object, privateKey, keyId, options, existingProofs.length < 1);
1124
1210
  if (span.isRecording()) {
1125
1211
  if (proof.cryptosuite != null) span.setAttribute("object_integrity_proofs.cryptosuite", proof.cryptosuite);
1126
1212
  if (proof.verificationMethodId != null) span.setAttribute("object_integrity_proofs.key_id", proof.verificationMethodId.href);
1127
1213
  if (proof.proofValue != null) span.setAttribute("object_integrity_proofs.signature", (0, byte_encodings_hex.encodeHex)(proof.proofValue));
1128
1214
  }
1129
- return object.clone({ proofs: [...existingProofs, proof] });
1215
+ const signed = object.clone({ proofs: [...existingProofs, proof] });
1216
+ if (securedDocument != null) (0, _fedify_vocab_runtime_internal_signed_representation.retainSignedRepresentation)(signed, securedDocument);
1217
+ else if (existingProofs.length > 0) logger$1.debug("The object {objectId} already had {proofCount} proof(s), so its signed representation is not retained for embedding; the map-local compound-proof profile accepts exactly one direct proof per map.", {
1218
+ objectId: object.id?.href,
1219
+ proofCount: existingProofs.length
1220
+ });
1221
+ return signed;
1130
1222
  } catch (error) {
1131
1223
  span.setStatus({
1132
1224
  code: _opentelemetry_api.SpanStatusCode.ERROR,