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