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

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 (94) hide show
  1. package/dist/{builder-B4zpKIFU.mjs → builder-D0DxgOV5.mjs} +2 -2
  2. package/dist/{client-CtxvmazV.mjs → client-DpnpgjyX.mjs} +78 -4
  3. package/dist/compat/mod.d.cts +1 -1
  4. package/dist/compat/mod.d.ts +1 -1
  5. package/dist/compat/transformers.test.mjs +1 -1
  6. package/dist/compound-proof-CJnWc6c-.mjs +495 -0
  7. package/dist/{context-COLwHogJ.d.cts → context-BeDnDuF3.d.cts} +4 -3
  8. package/dist/{context-DiiCt4S7.d.ts → context-uyRUWoZQ.d.ts} +4 -3
  9. package/dist/{deno-CtG93ohj.mjs → deno-CABhzCb_.mjs} +1 -1
  10. package/dist/{docloader-dk_k3S8d.mjs → docloader-BbGSL0Xq.mjs} +2 -2
  11. package/dist/federation/builder.test.mjs +1 -1
  12. package/dist/federation/handler-compound-proof.test.d.mts +2 -0
  13. package/dist/federation/handler-compound-proof.test.mjs +152 -0
  14. package/dist/federation/handler.test.mjs +122 -4
  15. package/dist/federation/idempotency.test.mjs +2 -2
  16. package/dist/federation/keycache.test.mjs +52 -9
  17. package/dist/federation/metrics.test.mjs +1 -1
  18. package/dist/federation/middleware.test.mjs +111 -15
  19. package/dist/federation/mod.cjs +1 -1
  20. package/dist/federation/mod.d.cts +2 -2
  21. package/dist/federation/mod.d.ts +2 -2
  22. package/dist/federation/mod.js +1 -1
  23. package/dist/federation/send.test.mjs +139 -4
  24. package/dist/federation/tasks/enqueue.test.mjs +1 -1
  25. package/dist/federation/tasks/tasks.test.mjs +2 -2
  26. package/dist/federation/temporal.test.mjs +1 -1
  27. package/dist/federation/webfinger.test.mjs +1 -1
  28. package/dist/{federation-Drjlrrax.cjs → federation-DCEHQk4G.cjs} +632 -57
  29. package/dist/{http-6TI0tI03.d.cts → http-65PYi9jY.d.ts} +34 -2
  30. package/dist/{http-Dj4s1UQ0.js → http-9WyuldYh.js} +170 -8
  31. package/dist/{http-CdrSn2ip.d.ts → http-CP-7OdSq.d.cts} +34 -2
  32. package/dist/{http-B3skRvGo.cjs → http-CRFns-UE.cjs} +181 -7
  33. package/dist/{http-D7LX-BL9.mjs → http-D9urSO2D.mjs} +3 -3
  34. package/dist/{key-BVjgbs_9.mjs → key-Ccs8cnd7.mjs} +169 -7
  35. package/dist/{keycache-ZLqIfrkL.mjs → keycache-BUDHB5rY.mjs} +12 -4
  36. package/dist/{kv-cache-DtsjH_bY.cjs → kv-cache-DXT9vmIg.cjs} +2 -2
  37. package/dist/{kv-cache-tGNbvDCF.js → kv-cache-DtmXhPXG.js} +2 -2
  38. package/dist/{kv-cache-xRcC_xxP.mjs → kv-cache-lKyFn_Dn.mjs} +1 -1
  39. package/dist/{ld-OuCeQFny.mjs → ld-qup__MAW.mjs} +3 -3
  40. package/dist/map-local-context-conflict-DOYEEJ3u.mjs +204 -0
  41. package/dist/map-local-create-note-B0SeXJms.mjs +214 -0
  42. package/dist/{metrics-B9N27cCB.mjs → metrics-CISXCvB1.mjs} +1 -1
  43. package/dist/{middleware-TeLjzoxn.mjs → middleware-CzvJvPHK.mjs} +58 -15
  44. package/dist/{middleware-Cd_vPqDH.mjs → middleware-DkrU2WhY.mjs} +1 -1
  45. package/dist/{middleware-CPTDFMmJ.js → middleware-DsfWajaD.js} +593 -18
  46. package/dist/{mod-DypxPS9H.d.ts → mod-C0BYc0OU.d.ts} +1 -1
  47. package/dist/{mod-CnGLNoVw.d.cts → mod-CbKDOnCt.d.cts} +1 -1
  48. package/dist/{mod-CLHkCwLm.d.cts → mod-CyiSjRkk.d.cts} +1 -1
  49. package/dist/{mod-CSH1Ee-I.d.ts → mod-D882_Xsg.d.ts} +1 -1
  50. package/dist/{mod-DzRXACM5.d.ts → mod-UDwMjuHM.d.ts} +1 -1
  51. package/dist/{mod-BZldCEU0.d.cts → mod-x9W0d0Pk.d.cts} +1 -1
  52. package/dist/mod.cjs +20 -20
  53. package/dist/mod.d.cts +6 -6
  54. package/dist/mod.d.ts +6 -6
  55. package/dist/mod.js +5 -5
  56. package/dist/nodeinfo/client.test.mjs +28 -1
  57. package/dist/nodeinfo/handler.test.mjs +1 -1
  58. package/dist/nodeinfo/mod.cjs +1 -1
  59. package/dist/nodeinfo/mod.js +1 -1
  60. package/dist/{owner-DGSfntI-.d.ts → owner-BF0L9cMt.d.cts} +12 -1
  61. package/dist/{proof-BO_esQii.js → owner-BIPEZGiU.js} +1255 -1098
  62. package/dist/{owner-r_waTiQj.d.cts → owner-BSzr5-I8.d.ts} +12 -1
  63. package/dist/owner-CRoq8Spb.mjs +175 -0
  64. package/dist/{proof-Dua0d906.cjs → owner-CVA7L9MA.cjs} +1266 -1097
  65. package/dist/{proof-D5Ajk4DJ.mjs → proof-BDxNVZ3E.mjs} +186 -83
  66. package/dist/{send-DVXyUqhO.mjs → send-BzM5GLyn.mjs} +68 -11
  67. package/dist/sig/compound-proof-discovery.test.d.mts +2 -0
  68. package/dist/sig/compound-proof-discovery.test.mjs +331 -0
  69. package/dist/sig/compound-proof-verification.test.d.mts +2 -0
  70. package/dist/sig/compound-proof-verification.test.mjs +465 -0
  71. package/dist/sig/compound-proof.test.d.mts +2 -0
  72. package/dist/sig/compound-proof.test.mjs +324 -0
  73. package/dist/sig/http.test.mjs +2 -2
  74. package/dist/sig/key.test.mjs +169 -2
  75. package/dist/sig/ld.test.mjs +55 -2
  76. package/dist/sig/mod.cjs +17 -17
  77. package/dist/sig/mod.d.cts +3 -3
  78. package/dist/sig/mod.d.ts +3 -3
  79. package/dist/sig/mod.js +2 -2
  80. package/dist/sig/owner.test.mjs +103 -4
  81. package/dist/sig/proof.test.mjs +120 -1
  82. package/dist/{temporal-BoDw51A-.mjs → temporal-Dv6Qv-8k.mjs} +1 -1
  83. package/dist/testing/kv.test.mjs +1 -1
  84. package/dist/testing/mod.d.mts +2 -1
  85. package/dist/{types-RXm5YxpC.cjs → types-Be2zbyeG.cjs} +94 -2
  86. package/dist/{types-Dtyf8lVj.js → types-C_cNlNVK.js} +78 -4
  87. package/dist/utils/docloader.test.mjs +27 -3
  88. package/dist/utils/kv-cache.test.mjs +1 -1
  89. package/dist/utils/mod.cjs +1 -1
  90. package/dist/utils/mod.d.cts +1 -1
  91. package/dist/utils/mod.d.ts +1 -1
  92. package/dist/utils/mod.js +1 -1
  93. package/package.json +6 -6
  94. package/dist/owner-DTEqTzSr.mjs +0 -120
@@ -2,19 +2,19 @@ import { Temporal } from "temporal-polyfill";
2
2
  import { URLPattern } from "urlpattern-polyfill";
3
3
  import { t as __exportAll } from "./rolldown-runtime-BC-Eo5Mj.js";
4
4
  import { r as getDefaultActivityTransformers } from "./transformers-DAtXl_Kc.js";
5
- import { A as formatAcceptSignature, D as recordOutboxEnqueue, E as recordOutboxActivity, F as version, O as recordWebFingerHandle, P as name, S as recordCollectionTotalItems, T as recordInboxActivity, a as verifyRequestDetailed, b as recordCollectionPageItems, d as validateCryptoKey, f as getDurationMs, g as isAbortError, h as instrumentDocumentLoader, i as verifyRequest, k as registerQueueDepthGauge, m as getRemoteHost, n as parseRfc9421SignatureInput, o as exportJwk, p as getFederationMetrics, t as doubleKnock, u as importJwk, v as recordCircuitBreakerStateChange, w as recordFanoutRecipients, x as recordCollectionRequest, y as recordCollectionDispatchDuration } from "./http-Dj4s1UQ0.js";
6
- import { S as verifyCompactJsonLd, T as wrapContextLoaderForJsonLd, _ as hasSignature, b as isInvalidUrlTypeError, c as doesActorOwnKey, d as assertSafeJsonLd, g as getNormalizationContextLoader, h as detachSignature, i as verifyObject, l as getKeyOwner, n as hasProofLike, p as compactJsonLd, r as signObject, s as normalizeOutgoingActivityJsonLd, u as InvalidContextReferenceError, v as hasSignatureLike, x as signJsonLd, y as isClearlyMalformedContextReference } from "./proof-BO_esQii.js";
7
- import { n as getNodeInfo, t as nodeInfoToJson } from "./types-Dtyf8lVj.js";
8
- import { n as getAuthenticatedDocumentLoader, t as kvCache } from "./kv-cache-tGNbvDCF.js";
5
+ import { A as recordWebFingerHandle, C as recordCollectionRequest, D as recordInboxActivity, E as recordFanoutRecipients, I as name, L as version, M as formatAcceptSignature, O as recordOutboxActivity, S as recordCollectionPageItems, _ as instrumentDocumentLoader, a as verifyRequestDetailed, b as recordCircuitBreakerStateChange, d as importJwk, f as validateCryptoKey, g as getRemoteHost, h as getFederationMetrics, i as verifyRequest, j as registerQueueDepthGauge, k as recordOutboxEnqueue, m as getDurationMs, n as parseRfc9421SignatureInput, o as exportJwk, t as doubleKnock, v as isAbortError, w as recordCollectionTotalItems, x as recordCollectionDispatchDuration } from "./http-9WyuldYh.js";
6
+ import { a as MAX_BODY_SIZE, i as BodyTooLargeError, n as getNodeInfo, o as readBoundedText, t as nodeInfoToJson } from "./types-C_cNlNVK.js";
7
+ import { C as verifyCompactJsonLd, D as normalizeOutgoingActivityJsonLd, E as wrapContextLoaderForJsonLd, S as signJsonLd, _ as getNormalizationContextLoader, a as signObject, b as isClearlyMalformedContextReference, d as InvalidContextReferenceError, f as assertSafeJsonLd, g as detachSignature, i as hasProofLike, l as verifyPortableObjectProofPolicy, m as compactJsonLd, n as getKeyOwner, o as verifyMapLocalProof, s as verifyObject, t as doesActorOwnKey, v as hasSignature, x as isInvalidUrlTypeError, y as hasSignatureLike } from "./owner-BIPEZGiU.js";
8
+ import { n as getAuthenticatedDocumentLoader, t as kvCache } from "./kv-cache-DtmXhPXG.js";
9
9
  import { getLogger, withContext } from "@logtape/logtape";
10
10
  import { Router, RouterError, assertPath } from "@fedify/uri-template";
11
11
  import { Activity, Collection, CollectionPage, CryptographicKey, Link, Multikey, Object as Object$1, OrderedCollection, OrderedCollectionPage, Tombstone, getTypeId, lookupObject, traverseCollection } from "@fedify/vocab";
12
12
  import { SpanKind, SpanStatusCode, context, metrics, propagation, trace } from "@opentelemetry/api";
13
13
  import { encodeHex } from "byte-encodings/hex";
14
+ import { FetchError, UrlError, getDocumentLoader, validatePublicUrl } from "@fedify/vocab-runtime";
14
15
  import { uniq } from "es-toolkit";
15
- import { FetchError, getDocumentLoader } from "@fedify/vocab-runtime";
16
- import { ATTR_HTTP_REQUEST_HEADER, ATTR_HTTP_REQUEST_METHOD, ATTR_HTTP_RESPONSE_HEADER, ATTR_HTTP_RESPONSE_STATUS_CODE, ATTR_URL_FULL } from "@opentelemetry/semantic-conventions";
17
16
  import jsonld from "@fedify/vocab-runtime/jsonld";
17
+ import { ATTR_HTTP_REQUEST_HEADER, ATTR_HTTP_REQUEST_METHOD, ATTR_HTTP_RESPONSE_HEADER, ATTR_HTTP_RESPONSE_STATUS_CODE, ATTR_URL_FULL } from "@opentelemetry/semantic-conventions";
18
18
  import { lookupWebFinger } from "@fedify/webfinger";
19
19
  import { DataPointType, MeterProvider as MeterProvider$1, MetricReader } from "@opentelemetry/sdk-metrics";
20
20
  import { parse, stringifyAsync } from "devalue";
@@ -1296,6 +1296,474 @@ async function buildCollectionSynchronizationHeader(collectionId, actorIds) {
1296
1296
  return `collectionId="${collectionId}", url="${url}", digest="${encodeHex(await digest(actorIds))}"`;
1297
1297
  }
1298
1298
  //#endregion
1299
+ //#region src/sig/compound-proof.ts
1300
+ const textEncoder = new TextEncoder();
1301
+ const PORTABLE_OBJECT_ID_PATTERN = /^ap(?:\+ef61)?:\/\//i;
1302
+ function isJsonMap(value) {
1303
+ return typeof value === "object" && value != null && !Array.isArray(value);
1304
+ }
1305
+ function isCompoundProofJsonObject(value) {
1306
+ return typeof value === "object" && value != null && !Array.isArray(value);
1307
+ }
1308
+ function hasOnlyDataProperties(value) {
1309
+ try {
1310
+ return Reflect.ownKeys(value).every((key) => {
1311
+ if (typeof key !== "string") return false;
1312
+ const descriptor = Object.getOwnPropertyDescriptor(value, key);
1313
+ return descriptor != null && "value" in descriptor && descriptor.enumerable;
1314
+ });
1315
+ } catch {
1316
+ return false;
1317
+ }
1318
+ }
1319
+ function hasOnlyJsonArrayProperties(value) {
1320
+ try {
1321
+ const keys = Reflect.ownKeys(value);
1322
+ if (keys.length !== value.length + 1) return false;
1323
+ for (const key of keys) {
1324
+ if (key === "length") continue;
1325
+ if (typeof key !== "string") return false;
1326
+ const index = Number(key);
1327
+ if (!Number.isInteger(index) || index < 0 || index >= value.length || String(index) !== key) return false;
1328
+ const descriptor = Object.getOwnPropertyDescriptor(value, key);
1329
+ if (descriptor == null || !descriptor.enumerable || !("value" in descriptor)) return false;
1330
+ }
1331
+ return true;
1332
+ } catch {
1333
+ return false;
1334
+ }
1335
+ }
1336
+ function escapeJsonPointerSegment(segment) {
1337
+ return segment.replaceAll("~", "~0").replaceAll("/", "~1");
1338
+ }
1339
+ function childPath(path, segment) {
1340
+ return `${path}/${escapeJsonPointerSegment(segment)}`;
1341
+ }
1342
+ function comparePaths(left, right) {
1343
+ return left < right ? -1 : left > right ? 1 : 0;
1344
+ }
1345
+ function encodedLength(value) {
1346
+ return textEncoder.encode(value).byteLength;
1347
+ }
1348
+ function validateLimits(limits) {
1349
+ for (const [name, value] of Object.entries(limits)) if (!Number.isSafeInteger(value) || value < 0) throw new RangeError(`${name} must be a non-negative safe integer.`);
1350
+ }
1351
+ function limitExceeded(limit, maximum, actual, path) {
1352
+ return {
1353
+ status: "unsupported",
1354
+ reason: {
1355
+ type: "limitExceeded",
1356
+ limit,
1357
+ maximum,
1358
+ actual,
1359
+ path
1360
+ }
1361
+ };
1362
+ }
1363
+ function addBytes(statistics, bytes, limits, path) {
1364
+ statistics.byteLength += bytes;
1365
+ if (statistics.byteLength > limits.maxBytes) return limitExceeded("bytes", limits.maxBytes, statistics.byteLength, path);
1366
+ }
1367
+ function inspectJsonTree(root, limits) {
1368
+ if (!isJsonMap(root)) return {
1369
+ status: "unsupported",
1370
+ reason: {
1371
+ type: "invalidJsonTree",
1372
+ path: ""
1373
+ }
1374
+ };
1375
+ const statistics = {
1376
+ byteLength: 0,
1377
+ mapCount: 0,
1378
+ proofCount: 0,
1379
+ maxDepth: 0
1380
+ };
1381
+ const seen = /* @__PURE__ */ new Set();
1382
+ const pending = [{
1383
+ value: root,
1384
+ path: "",
1385
+ depth: 0,
1386
+ insideProof: false,
1387
+ insideContext: false
1388
+ }];
1389
+ while (pending.length > 0) {
1390
+ const current = pending.pop();
1391
+ statistics.maxDepth = Math.max(statistics.maxDepth, current.depth);
1392
+ if (current.depth > limits.maxDepth) return limitExceeded("depth", limits.maxDepth, current.depth, current.path);
1393
+ const value = current.value;
1394
+ if (value === null) {
1395
+ const failure = addBytes(statistics, 4, limits, current.path);
1396
+ if (failure != null) return failure;
1397
+ } else if (typeof value === "boolean") {
1398
+ const failure = addBytes(statistics, value ? 4 : 5, limits, current.path);
1399
+ if (failure != null) return failure;
1400
+ } else if (typeof value === "string") {
1401
+ const failure = addBytes(statistics, encodedLength(JSON.stringify(value)), limits, current.path);
1402
+ if (failure != null) return failure;
1403
+ } else if (typeof value === "number") {
1404
+ if (!Number.isFinite(value)) return {
1405
+ status: "unsupported",
1406
+ reason: {
1407
+ type: "invalidJsonTree",
1408
+ path: current.path
1409
+ }
1410
+ };
1411
+ const failure = addBytes(statistics, encodedLength(JSON.stringify(value)), limits, current.path);
1412
+ if (failure != null) return failure;
1413
+ } else if (Array.isArray(value)) {
1414
+ if (seen.has(value) || !hasOnlyJsonArrayProperties(value)) return {
1415
+ status: "unsupported",
1416
+ reason: {
1417
+ type: "invalidJsonTree",
1418
+ path: current.path
1419
+ }
1420
+ };
1421
+ seen.add(value);
1422
+ const failure = addBytes(statistics, 2 + Math.max(0, value.length - 1), limits, current.path);
1423
+ if (failure != null) return failure;
1424
+ for (let index = value.length - 1; index >= 0; index--) pending.push({
1425
+ value: value[index],
1426
+ path: childPath(current.path, String(index)),
1427
+ depth: current.depth + 1,
1428
+ insideProof: current.insideProof,
1429
+ insideContext: current.insideContext
1430
+ });
1431
+ } else if (isJsonMap(value)) {
1432
+ const prototype = Object.getPrototypeOf(value);
1433
+ if (seen.has(value) || !hasOnlyDataProperties(value) || prototype !== Object.prototype && prototype !== null) return {
1434
+ status: "unsupported",
1435
+ reason: {
1436
+ type: "invalidJsonTree",
1437
+ path: current.path
1438
+ }
1439
+ };
1440
+ seen.add(value);
1441
+ statistics.mapCount++;
1442
+ if (statistics.mapCount > limits.maxMaps) return limitExceeded("maps", limits.maxMaps, statistics.mapCount, current.path);
1443
+ const entries = Object.entries(value).sort(([left], [right]) => comparePaths(left, right));
1444
+ let objectBytes = 2 + Math.max(0, entries.length - 1);
1445
+ for (const [key] of entries) objectBytes += encodedLength(JSON.stringify(key)) + 1;
1446
+ const failure = addBytes(statistics, objectBytes, limits, current.path);
1447
+ if (failure != null) return failure;
1448
+ const hasDirectProof = !current.insideProof && !current.insideContext && Object.hasOwn(value, "proof");
1449
+ if (hasDirectProof) {
1450
+ statistics.proofCount++;
1451
+ if (statistics.proofCount > limits.maxProofs) return limitExceeded("proofs", limits.maxProofs, statistics.proofCount, childPath(current.path, "proof"));
1452
+ if (!isJsonMap(value.proof)) return {
1453
+ status: "unsupported",
1454
+ reason: {
1455
+ type: "unsupportedProofShape",
1456
+ path: childPath(current.path, "proof")
1457
+ }
1458
+ };
1459
+ }
1460
+ for (let index = entries.length - 1; index >= 0; index--) {
1461
+ const [key, child] = entries[index];
1462
+ pending.push({
1463
+ value: child,
1464
+ path: childPath(current.path, key),
1465
+ depth: current.depth + 1,
1466
+ insideProof: current.insideProof || hasDirectProof && key === "proof",
1467
+ insideContext: current.insideContext || key === "@context"
1468
+ });
1469
+ }
1470
+ } else return {
1471
+ status: "unsupported",
1472
+ reason: {
1473
+ type: "invalidJsonTree",
1474
+ path: current.path
1475
+ }
1476
+ };
1477
+ }
1478
+ return statistics;
1479
+ }
1480
+ function freezeJsonTree(root) {
1481
+ const pending = [root];
1482
+ while (pending.length > 0) {
1483
+ const value = pending.pop();
1484
+ if (typeof value !== "object" || value == null) continue;
1485
+ if (Array.isArray(value)) for (const child of value) pending.push(child);
1486
+ else for (const child of Object.values(value)) pending.push(child);
1487
+ Object.freeze(value);
1488
+ }
1489
+ }
1490
+ function collectDocuments(snapshot) {
1491
+ const documents = [];
1492
+ const pending = [{
1493
+ value: snapshot,
1494
+ path: "",
1495
+ depth: 0
1496
+ }];
1497
+ while (pending.length > 0) {
1498
+ const current = pending.pop();
1499
+ if (Array.isArray(current.value)) {
1500
+ for (let index = current.value.length - 1; index >= 0; index--) pending.push({
1501
+ value: current.value[index],
1502
+ path: childPath(current.path, String(index)),
1503
+ depth: current.depth + 1
1504
+ });
1505
+ continue;
1506
+ }
1507
+ if (!isCompoundProofJsonObject(current.value)) continue;
1508
+ const document = current.value;
1509
+ if (Object.hasOwn(document, "proof")) {
1510
+ const proof = document.proof;
1511
+ const id = typeof document.id === "string" ? document.id : typeof document["@id"] === "string" ? document["@id"] : void 0;
1512
+ documents.push(Object.freeze({
1513
+ path: current.path,
1514
+ id,
1515
+ depth: current.depth,
1516
+ securedDocument: document,
1517
+ proof
1518
+ }));
1519
+ }
1520
+ const entries = Object.entries(document).sort(([left], [right]) => comparePaths(left, right));
1521
+ for (let index = entries.length - 1; index >= 0; index--) {
1522
+ const [key, value] = entries[index];
1523
+ if (key === "proof" || key === "@context") continue;
1524
+ pending.push({
1525
+ value,
1526
+ path: childPath(current.path, key),
1527
+ depth: current.depth + 1
1528
+ });
1529
+ }
1530
+ }
1531
+ documents.sort((left, right) => right.depth - left.depth || comparePaths(left.path, right.path));
1532
+ return documents;
1533
+ }
1534
+ function collectPortableObjects(snapshot) {
1535
+ const objects = [];
1536
+ const pending = [{
1537
+ value: snapshot,
1538
+ path: "",
1539
+ depth: 0
1540
+ }];
1541
+ while (pending.length > 0) {
1542
+ const current = pending.pop();
1543
+ if (Array.isArray(current.value)) {
1544
+ for (let index = current.value.length - 1; index >= 0; index--) pending.push({
1545
+ value: current.value[index],
1546
+ path: childPath(current.path, String(index)),
1547
+ depth: current.depth + 1
1548
+ });
1549
+ continue;
1550
+ }
1551
+ if (!isCompoundProofJsonObject(current.value)) continue;
1552
+ const document = current.value;
1553
+ const id = [document.id, document["@id"]].find((value) => typeof value === "string" && PORTABLE_OBJECT_ID_PATTERN.test(value));
1554
+ if (typeof id === "string") objects.push(Object.freeze({
1555
+ path: current.path,
1556
+ id,
1557
+ depth: current.depth,
1558
+ document
1559
+ }));
1560
+ const entries = Object.entries(document).sort(([left], [right]) => comparePaths(left, right));
1561
+ for (let index = entries.length - 1; index >= 0; index--) {
1562
+ const [key, value] = entries[index];
1563
+ if (key === "proof" || key === "@context") continue;
1564
+ pending.push({
1565
+ value,
1566
+ path: childPath(current.path, key),
1567
+ depth: current.depth + 1
1568
+ });
1569
+ }
1570
+ }
1571
+ objects.sort((left, right) => right.depth - left.depth || comparePaths(left.path, right.path));
1572
+ return objects;
1573
+ }
1574
+ /**
1575
+ * Determines whether a received JSON tree contains a portable object ID
1576
+ * outside a proof or context definition, without exceeding discovery limits.
1577
+ *
1578
+ * This is only an applicability check. It does not authenticate the input;
1579
+ * callers must still use {@link verifyCompoundPortableObjectProofs} to obtain
1580
+ * an atomic result from an immutable snapshot. `indeterminate` means the
1581
+ * applicability scan itself reached a limit, so a caller must not treat the
1582
+ * document as outside the compound profile.
1583
+ *
1584
+ * @internal
1585
+ */
1586
+ function inspectCompoundPortableObjectApplicability(json, limits) {
1587
+ validateLimits(limits);
1588
+ let byteLength = 0;
1589
+ let mapCount = 0;
1590
+ const pending = [{
1591
+ iterator: [json].values(),
1592
+ depth: 0
1593
+ }];
1594
+ const addByteLength = (length) => {
1595
+ byteLength += length;
1596
+ return byteLength <= limits.maxBytes;
1597
+ };
1598
+ while (pending.length > 0) {
1599
+ const frame = pending[pending.length - 1];
1600
+ const next = frame.iterator.next();
1601
+ if (next.done) {
1602
+ pending.pop();
1603
+ if (byteLength > limits.maxBytes) return "indeterminate";
1604
+ continue;
1605
+ }
1606
+ const value = next.value;
1607
+ if (frame.depth > limits.maxDepth) return "indeterminate";
1608
+ if (value === null) {
1609
+ if (!addByteLength(4)) return "indeterminate";
1610
+ } else if (typeof value === "boolean") {
1611
+ if (!addByteLength(value ? 4 : 5)) return "indeterminate";
1612
+ } else if (typeof value === "string" || typeof value === "number") {
1613
+ if (!addByteLength(encodedLength(JSON.stringify(value)))) return "indeterminate";
1614
+ } else if (Array.isArray(value)) {
1615
+ if (!addByteLength(2 + Math.max(0, value.length - 1))) return "indeterminate";
1616
+ pending.push({
1617
+ iterator: value.values(),
1618
+ depth: frame.depth + 1
1619
+ });
1620
+ } else if (isJsonMap(value)) {
1621
+ mapCount++;
1622
+ if (mapCount > limits.maxMaps) return "indeterminate";
1623
+ if ([value.id, value["@id"]].some((id) => typeof id === "string" && PORTABLE_OBJECT_ID_PATTERN.test(id))) return "present";
1624
+ if (!addByteLength(2)) return "indeterminate";
1625
+ const children = function* () {
1626
+ let first = true;
1627
+ for (const key in value) {
1628
+ if (!Object.hasOwn(value, key)) continue;
1629
+ if (key === "proof" || key === "@context") continue;
1630
+ if (!first && !addByteLength(1)) return;
1631
+ first = false;
1632
+ if (!addByteLength(encodedLength(JSON.stringify(key)) + 1)) return;
1633
+ yield value[key];
1634
+ }
1635
+ }();
1636
+ pending.push({
1637
+ iterator: children,
1638
+ depth: frame.depth + 1
1639
+ });
1640
+ }
1641
+ if (byteLength > limits.maxBytes) return "indeterminate";
1642
+ }
1643
+ return "absent";
1644
+ }
1645
+ /**
1646
+ * Discovers direct literal proof-bearing maps in an immutable JSON snapshot.
1647
+ *
1648
+ * Discovery is bounded and atomic. Unsupported proof shapes, non-JSON input,
1649
+ * or a resource-limit failure return no partial candidate list. Proof
1650
+ * configuration subtrees are validated and counted as JSON, but are not
1651
+ * themselves searched for secured documents.
1652
+ */
1653
+ function discoverCompoundProofDocuments(json, limits) {
1654
+ validateLimits(limits);
1655
+ let inspected;
1656
+ try {
1657
+ inspected = inspectJsonTree(json, limits);
1658
+ } catch {
1659
+ return {
1660
+ status: "unsupported",
1661
+ reason: {
1662
+ type: "invalidJsonTree",
1663
+ path: ""
1664
+ }
1665
+ };
1666
+ }
1667
+ if ("status" in inspected) return inspected;
1668
+ let snapshot;
1669
+ try {
1670
+ snapshot = structuredClone(json);
1671
+ } catch {
1672
+ return {
1673
+ status: "unsupported",
1674
+ reason: {
1675
+ type: "invalidJsonTree",
1676
+ path: ""
1677
+ }
1678
+ };
1679
+ }
1680
+ freezeJsonTree(snapshot);
1681
+ return {
1682
+ status: "ok",
1683
+ snapshot,
1684
+ documents: Object.freeze(collectDocuments(snapshot)),
1685
+ statistics: Object.freeze({ ...inspected })
1686
+ };
1687
+ }
1688
+ async function verifyDiscoveredProofDocuments(discovered, options) {
1689
+ const documents = await Promise.all(discovered.documents.map(async (document) => {
1690
+ const metadata = {
1691
+ path: document.path,
1692
+ ...document.id == null ? {} : { id: document.id },
1693
+ depth: document.depth
1694
+ };
1695
+ if (document.depth > 0 && !Object.hasOwn(document.securedDocument, "@context")) return Object.freeze({
1696
+ ...metadata,
1697
+ verified: false,
1698
+ reason: { type: "missingContext" }
1699
+ });
1700
+ const key = await verifyMapLocalProof(document.securedDocument, options);
1701
+ return key == null ? Object.freeze({
1702
+ ...metadata,
1703
+ verified: false,
1704
+ reason: { type: "invalidProof" }
1705
+ }) : Object.freeze({
1706
+ ...metadata,
1707
+ verified: true,
1708
+ key
1709
+ });
1710
+ }));
1711
+ return Object.freeze(documents);
1712
+ }
1713
+ /**
1714
+ * Applies FEP-ef61 portable-object policy to every portable map in a compound
1715
+ * snapshot without changing the map-local cryptographic inputs.
1716
+ *
1717
+ * @internal
1718
+ */
1719
+ async function verifyCompoundPortableObjectProofs(json, limits, options = {}) {
1720
+ const discovered = discoverCompoundProofDocuments(json, limits);
1721
+ if (discovered.status !== "ok") return discovered;
1722
+ const proofs = await verifyDiscoveredProofDocuments(discovered, options);
1723
+ const proofByPath = new Map(proofs.map((proof) => [proof.path, proof]));
1724
+ const portableObjects = await Promise.all(collectPortableObjects(discovered.snapshot).map(async (object) => {
1725
+ const metadata = {
1726
+ path: object.path,
1727
+ id: object.id,
1728
+ depth: object.depth
1729
+ };
1730
+ if (object.depth > 0 && !Object.hasOwn(object.document, "@context")) return Object.freeze({
1731
+ ...metadata,
1732
+ verified: false,
1733
+ reason: { type: "missingContext" }
1734
+ });
1735
+ const proof = proofByPath.get(object.path);
1736
+ const key = proof?.verified === true ? proof.key : null;
1737
+ try {
1738
+ const policy = await verifyPortableObjectProofPolicy(object.document, key, options);
1739
+ return policy.verified ? Object.freeze({
1740
+ ...metadata,
1741
+ id: policy.objectId,
1742
+ verified: true,
1743
+ keys: policy.keys
1744
+ }) : Object.freeze({
1745
+ ...metadata,
1746
+ verified: false,
1747
+ reason: policy.reason
1748
+ });
1749
+ } catch {
1750
+ return Object.freeze({
1751
+ ...metadata,
1752
+ verified: false,
1753
+ reason: { type: "invalidPortableObject" }
1754
+ });
1755
+ }
1756
+ }));
1757
+ return {
1758
+ status: "ok",
1759
+ verified: portableObjects.length > 0 && proofs.every((proof) => proof.verified) && portableObjects.every((object) => object.verified),
1760
+ snapshot: discovered.snapshot,
1761
+ proofs,
1762
+ portableObjects: Object.freeze(portableObjects),
1763
+ statistics: discovered.statistics
1764
+ };
1765
+ }
1766
+ //#endregion
1299
1767
  //#region src/federation/inbox.ts
1300
1768
  async function routeActivity({ context: ctx, json, originalJson, normalizedActivity, ldSignatureVerified, activity, recipient, inboxListeners, inboxContextFactory, listenerInboxContextFactory, inboxErrorHandler, kv, kvPrefixes, queue, span, meterProvider, tracerProvider, idempotencyStrategy }) {
1301
1769
  const logger = getLogger([
@@ -1455,6 +1923,7 @@ async function routeActivity({ context: ctx, json, originalJson, normalizedActiv
1455
1923
  }
1456
1924
  //#endregion
1457
1925
  //#region src/federation/keycache.ts
1926
+ const KEY_CACHE_GENERATION = "2";
1458
1927
  var KvKeyCache = class {
1459
1928
  kv;
1460
1929
  prefix;
@@ -1477,13 +1946,20 @@ var KvKeyCache = class {
1477
1946
  keyId.href
1478
1947
  ];
1479
1948
  }
1949
+ #entryKey(keyId) {
1950
+ return [
1951
+ ...this.prefix,
1952
+ KEY_CACHE_GENERATION,
1953
+ keyId.href
1954
+ ];
1955
+ }
1480
1956
  async get(keyId) {
1481
1957
  const negativeExpiration = this.nullKeys.get(keyId.href);
1482
1958
  if (negativeExpiration != null) {
1483
1959
  if (Temporal.Now.instant().until(negativeExpiration).sign >= 0) return null;
1484
1960
  this.nullKeys.delete(keyId.href);
1485
1961
  }
1486
- const serialized = await this.kv.get([...this.prefix, keyId.href]);
1962
+ const serialized = await this.kv.get(this.#entryKey(keyId));
1487
1963
  if (serialized === void 0) return void 0;
1488
1964
  if (serialized === null) {
1489
1965
  this.nullKeys.set(keyId.href, Temporal.Now.instant().add(this.unavailableKeyTtl));
@@ -1495,7 +1971,7 @@ var KvKeyCache = class {
1495
1971
  try {
1496
1972
  return await Multikey.fromJsonLd(serialized, this.options);
1497
1973
  } catch {
1498
- await this.kv.delete([...this.prefix, keyId.href]);
1974
+ await this.kv.delete(this.#entryKey(keyId));
1499
1975
  return;
1500
1976
  }
1501
1977
  }
@@ -1503,12 +1979,12 @@ var KvKeyCache = class {
1503
1979
  async set(keyId, key) {
1504
1980
  if (key == null) {
1505
1981
  this.nullKeys.set(keyId.href, Temporal.Now.instant().add(this.unavailableKeyTtl));
1506
- await this.kv.set([...this.prefix, keyId.href], null, { ttl: this.unavailableKeyTtl });
1982
+ await this.kv.set(this.#entryKey(keyId), null, { ttl: this.unavailableKeyTtl });
1507
1983
  return;
1508
1984
  }
1509
1985
  this.nullKeys.delete(keyId.href);
1510
1986
  const serialized = await key.toJsonLd(this.options);
1511
- await this.kv.set([...this.prefix, keyId.href], serialized, { ttl: this.keyTtl });
1987
+ await this.kv.set(this.#entryKey(keyId), serialized, { ttl: this.keyTtl });
1512
1988
  }
1513
1989
  async getFetchError(keyId) {
1514
1990
  const cached = await this.kv.get(this.#getFetchErrorKey(keyId));
@@ -1731,6 +2207,12 @@ async function hasMalformedKnownTemporalLiteral(value, contextLoader) {
1731
2207
  //#endregion
1732
2208
  //#region src/federation/handler.ts
1733
2209
  const rawInboxContextFactorySymbol = Symbol("fedify.rawInboxContextFactory");
2210
+ const INBOX_COMPOUND_PROOF_LIMITS = {
2211
+ maxDepth: 64,
2212
+ maxMaps: 1e4,
2213
+ maxProofs: 32,
2214
+ maxBytes: 10 * 1024 * 1024
2215
+ };
1734
2216
  function isRemoteContextLoadingFailure$1(error) {
1735
2217
  return error instanceof Error && typeof error.details === "object" && error.details != null && error.details.code === "loading remote context failed";
1736
2218
  }
@@ -2511,8 +2993,16 @@ async function handleInboxInternal(request, parameters, span) {
2511
2993
  }
2512
2994
  let json;
2513
2995
  try {
2514
- json = await request.clone().json();
2996
+ json = JSON.parse(await readBoundedText(request.clone(), MAX_BODY_SIZE, request.url));
2515
2997
  } catch (error) {
2998
+ if (error instanceof BodyTooLargeError) {
2999
+ request.body?.cancel(error).catch(() => {});
3000
+ span.setStatus({
3001
+ code: SpanStatusCode.ERROR,
3002
+ message: String(error)
3003
+ });
3004
+ return new Response("Inbox body too large.", { status: 413 });
3005
+ }
2516
3006
  logger.error("Failed to parse JSON:\n{error}", {
2517
3007
  recipient,
2518
3008
  error
@@ -2844,6 +3334,31 @@ async function handleInboxInternal(request, parameters, span) {
2844
3334
  headers: { "Content-Type": "text/plain; charset=utf-8" }
2845
3335
  });
2846
3336
  }
3337
+ if (!skipSignatureVerification) {
3338
+ if (inspectCompoundPortableObjectApplicability(json, INBOX_COMPOUND_PROOF_LIMITS) !== "absent") {
3339
+ const compoundProof = await verifyCompoundPortableObjectProofs(json, INBOX_COMPOUND_PROOF_LIMITS, {
3340
+ documentLoader: ctx.documentLoader,
3341
+ keyCache,
3342
+ meterProvider,
3343
+ tracerProvider
3344
+ });
3345
+ if (compoundProof.status !== "ok" || !compoundProof.verified) {
3346
+ logger.error("Failed to verify compound portable Object Integrity Proofs.", {
3347
+ recipient,
3348
+ activity: json,
3349
+ reason: compoundProof.status === "unsupported" ? compoundProof.reason.type : "invalidProof"
3350
+ });
3351
+ span.setStatus({
3352
+ code: SpanStatusCode.ERROR,
3353
+ message: "Failed to verify compound portable Object Integrity Proofs."
3354
+ });
3355
+ return new Response("Failed to verify compound portable Object Integrity Proofs.", {
3356
+ status: 401,
3357
+ headers: { "Content-Type": "text/plain; charset=utf-8" }
3358
+ });
3359
+ }
3360
+ }
3361
+ }
2847
3362
  if (pendingNonceLabel != null) {
2848
3363
  if (!await verifySignatureNonce(request, kv, kvPrefixes.acceptSignatureNonce, pendingNonceLabel)) {
2849
3364
  getFederationMetrics(parameters.meterProvider).recordSignatureVerificationFailure("invalidNonce", httpSigKey?.id == null ? void 0 : getRemoteHost(httpSigKey.id));
@@ -3644,7 +4159,7 @@ async function readLimitedResponseBody(response, maxBytes) {
3644
4159
  if (truncated) result += "… (truncated)";
3645
4160
  return result;
3646
4161
  }
3647
- async function sendActivityInternal({ activity, activityId, activityType, keys, inbox, headers, specDeterminer, meterProvider, tracerProvider }, span) {
4162
+ async function sendActivityInternal({ activity, activityId, activityType, keys, inbox, allowPrivateAddress, headers, specDeterminer, meterProvider, tracerProvider }, span) {
3648
4163
  const logger = getLogger([
3649
4164
  "fedify",
3650
4165
  "federation",
@@ -3653,12 +4168,25 @@ async function sendActivityInternal({ activity, activityId, activityType, keys,
3653
4168
  const federationMetrics = getFederationMetrics(meterProvider);
3654
4169
  const started = performance.now();
3655
4170
  let deliverySuccess = false;
4171
+ async function validateUrl(url) {
4172
+ if (!allowPrivateAddress) try {
4173
+ await validatePublicUrl(url);
4174
+ } catch (error) {
4175
+ if (error instanceof UrlError) logger.error("Disallowed private URL: {url}", {
4176
+ url,
4177
+ error
4178
+ });
4179
+ throw error;
4180
+ }
4181
+ }
4182
+ await validateUrl(inbox.href);
3656
4183
  headers = new Headers(headers);
3657
4184
  headers.set("Content-Type", "application/activity+json");
3658
4185
  const request = new Request(inbox, {
3659
4186
  method: "POST",
3660
4187
  headers,
3661
- body: JSON.stringify(activity)
4188
+ body: JSON.stringify(activity),
4189
+ redirect: "manual"
3662
4190
  });
3663
4191
  let rsaKey = null;
3664
4192
  for (const key of keys) if (key.privateKey.algorithm.name === "RSASSA-PKCS1-v1_5") {
@@ -3674,19 +4202,20 @@ async function sendActivityInternal({ activity, activityId, activityType, keys,
3674
4202
  });
3675
4203
  let response;
3676
4204
  try {
3677
- response = rsaKey == null ? await fetch(request) : await doubleKnock(request, rsaKey, {
4205
+ response = rsaKey == null ? await fetchWithValidatedRedirects(request, validateUrl) : await doubleKnock(request, rsaKey, {
3678
4206
  tracerProvider,
3679
- specDeterminer
4207
+ specDeterminer,
4208
+ validateRedirect: validateUrl
3680
4209
  });
3681
4210
  } catch (error) {
3682
- const transportError = error instanceof FetchError ? error : createFetchError(inbox.href, error);
4211
+ const failure = error instanceof FetchError || error instanceof UrlError ? error : createFetchError(inbox.href, error);
3683
4212
  logger.error("Failed to send activity {activityId} to {inbox}:\n{error}", {
3684
4213
  activityId,
3685
4214
  inbox: inbox.href,
3686
- error: transportError
4215
+ error: failure
3687
4216
  });
3688
4217
  federationMetrics.recordDelivery(inbox, getDurationMs(started), false, activityType);
3689
- throw transportError;
4218
+ throw failure;
3690
4219
  }
3691
4220
  try {
3692
4221
  if (!response.ok) {
@@ -3723,6 +4252,49 @@ function createFetchError(url, cause) {
3723
4252
  error.cause = cause;
3724
4253
  return error;
3725
4254
  }
4255
+ async function fetchWithValidatedRedirects(request, validateUrl) {
4256
+ let body = await request.clone().text();
4257
+ for (let redirects = 0;; redirects++) {
4258
+ const response = await fetch(request, { redirect: "manual" });
4259
+ const location = response.headers.get("Location");
4260
+ if (![
4261
+ 301,
4262
+ 302,
4263
+ 303,
4264
+ 307,
4265
+ 308
4266
+ ].includes(response.status) || location == null) return response;
4267
+ await response.body?.cancel();
4268
+ if (redirects >= 20) throw new TypeError("Too many redirects");
4269
+ const url = new URL(location, request.url);
4270
+ await validateUrl(url.href);
4271
+ const headers = new Headers(request.headers);
4272
+ if (url.origin !== new URL(request.url).origin) {
4273
+ headers.delete("Authorization");
4274
+ headers.delete("Proxy-Authorization");
4275
+ headers.delete("Cookie");
4276
+ headers.delete("Host");
4277
+ }
4278
+ let method = request.method;
4279
+ if ((response.status === 301 || response.status === 302) && method === "POST" || response.status === 303 && method !== "GET" && method !== "HEAD") {
4280
+ method = "GET";
4281
+ body = void 0;
4282
+ for (const name of [
4283
+ "Content-Encoding",
4284
+ "Content-Language",
4285
+ "Content-Length",
4286
+ "Content-Location",
4287
+ "Content-Type"
4288
+ ]) headers.delete(name);
4289
+ }
4290
+ request = new Request(url, {
4291
+ method,
4292
+ headers,
4293
+ body,
4294
+ redirect: "manual"
4295
+ });
4296
+ }
4297
+ }
3726
4298
  /**
3727
4299
  * An error that is thrown when an activity fails to send to a remote inbox.
3728
4300
  * It contains structured information about the failure, including the HTTP
@@ -5213,6 +5785,7 @@ var FederationImpl = class extends FederationBuilderImpl {
5213
5785
  if (decision.stateChange != null) recordCircuitBreakerSpanEvent(span, remoteHost, decision.stateChange);
5214
5786
  }
5215
5787
  await sendActivity({
5788
+ allowPrivateAddress: this.allowPrivateAddress,
5216
5789
  keys,
5217
5790
  activity: message.activity,
5218
5791
  activityId: message.activityId,
@@ -5834,6 +6407,7 @@ var FederationImpl = class extends FederationBuilderImpl {
5834
6407
  });
5835
6408
  const promises = [];
5836
6409
  for (const inbox in inboxes) promises.push(sendActivity({
6410
+ allowPrivateAddress: this.allowPrivateAddress,
5837
6411
  keys,
5838
6412
  activity: jsonLd,
5839
6413
  activityId: activity.id?.href,
@@ -7168,6 +7742,7 @@ async function forwardActivityInternal(ctx, loggerCategory, forwarder, recipient
7168
7742
  else logger.debug("Forwarding activity immediately without queue since queue is not set.");
7169
7743
  const promises = [];
7170
7744
  for (const inbox in inboxes) promises.push(sendActivity({
7745
+ allowPrivateAddress: ctx.federation.allowPrivateAddress,
7171
7746
  keys,
7172
7747
  activity: ctx.activity,
7173
7748
  activityId: ctx.activityId,