@fedify/fedify 2.4.0-dev.2141 → 2.4.0-dev.2155

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 (85) hide show
  1. package/dist/{builder-BI7H61zO.mjs → builder-CxHALSwA.mjs} +56 -3
  2. package/dist/compat/mod.d.cts +1 -1
  3. package/dist/compat/mod.d.ts +1 -1
  4. package/dist/compat/transformers.test.mjs +1 -1
  5. package/dist/{compound-proof-D0H4LLvQ.mjs → compound-proof-nhrXTRQO.mjs} +216 -4
  6. package/dist/{context-CrDDVrbF.d.cts → context-BMggDR-l.d.cts} +394 -5
  7. package/dist/{context-DfSVPcJ8.mjs → context-OrnjnuMq.mjs} +8 -1
  8. package/dist/{context-B2C8nuN3.d.ts → context-exYu7gDU.d.ts} +394 -5
  9. package/dist/{deno-BXI_BLNy.mjs → deno-YLLbRq06.mjs} +1 -1
  10. package/dist/{docloader-B5BIo7e9.mjs → docloader-vc-pSu9-.mjs} +2 -2
  11. package/dist/federation/builder.test.mjs +1 -1
  12. package/dist/federation/compound-fanout.test.mjs +3 -3
  13. package/dist/federation/handler-compound-proof.test.mjs +4 -4
  14. package/dist/federation/handler.test.mjs +6 -6
  15. package/dist/federation/hashlink-media.test.mjs +2 -2
  16. package/dist/federation/idempotency.test.mjs +2 -2
  17. package/dist/federation/metrics.test.mjs +1 -1
  18. package/dist/federation/middleware.test.mjs +8 -8
  19. package/dist/federation/mod.cjs +1 -1
  20. package/dist/federation/mod.d.cts +3 -3
  21. package/dist/federation/mod.d.ts +3 -3
  22. package/dist/federation/mod.js +1 -1
  23. package/dist/federation/outgoing-proof.test.mjs +5 -5
  24. package/dist/federation/portable-actor.test.mjs +3 -3
  25. package/dist/federation/portable-chain.test.mjs +3 -3
  26. package/dist/federation/portable-collection.test.d.mts +2 -0
  27. package/dist/federation/portable-collection.test.mjs +768 -0
  28. package/dist/federation/portable-gateway-keys.test.mjs +4 -4
  29. package/dist/federation/portable-inbox.test.mjs +4 -4
  30. package/dist/federation/portable.test.mjs +4 -4
  31. package/dist/federation/send.test.mjs +3 -3
  32. package/dist/federation/tasks/enqueue.test.mjs +2 -2
  33. package/dist/federation/tasks/tasks.test.mjs +3 -3
  34. package/dist/federation/temporal.test.mjs +1 -1
  35. package/dist/federation/webfinger.test.mjs +2 -2
  36. package/dist/{federation-DqDOPjWo.cjs → federation-D9TufmIO.cjs} +684 -68
  37. package/dist/{http-CEx2SZjf.mjs → http-CXnNbRe_.mjs} +4 -4
  38. package/dist/{http-D4KW2GTi.js → http-Dk4O3WM0.js} +7 -2
  39. package/dist/{http-DSYQQn23.cjs → http-eqYXHRkz.cjs} +12 -1
  40. package/dist/{key-C0giKuki.mjs → key-BZ-Z4uez.mjs} +2 -2
  41. package/dist/{kv-cache-D_E-2K3i.js → kv-cache-3uKc0G76.js} +1 -1
  42. package/dist/{kv-cache-AWTrxTq3.mjs → kv-cache-DZr8HTel.mjs} +1 -1
  43. package/dist/{kv-cache-BcsvaCCL.cjs → kv-cache-DzClEalR.cjs} +1 -1
  44. package/dist/{ld-CWDReGMZ.mjs → ld-Djxsq6JR.mjs} +3 -3
  45. package/dist/{metrics-DxlVXwn5.mjs → metrics-CBHhbHgG.mjs} +1 -1
  46. package/dist/{middleware-1mv9QR16.js → middleware-BDUR99k3.js} +686 -70
  47. package/dist/{middleware-B7stx5gy.mjs → middleware-BahNnLfD.mjs} +1 -1
  48. package/dist/{middleware-CAWRGSwH.mjs → middleware-DDa8pCUa.mjs} +431 -80
  49. package/dist/{mod-C06GoL5Z.d.ts → mod-CnGtADvO.d.ts} +1 -1
  50. package/dist/{mod-B7SfEqIP.d.cts → mod-Decmur1U.d.cts} +1 -1
  51. package/dist/mod.cjs +4 -4
  52. package/dist/mod.d.cts +4 -4
  53. package/dist/mod.d.ts +4 -4
  54. package/dist/mod.js +4 -4
  55. package/dist/nodeinfo/handler.test.mjs +2 -2
  56. package/dist/{owner-CgoCOTUQ.mjs → owner-OdWSE8am.mjs} +3 -3
  57. package/dist/{portable-collection-CG9P_i0E.js → portable-collection-BCy06GiS.js} +1 -1
  58. package/dist/{portable-collection-0_5DyXMt.mjs → portable-collection-DAV7Ho3m.mjs} +2 -2
  59. package/dist/{portable-collection-BD8fE3j3.cjs → portable-collection-wBWbuHOH.cjs} +1 -1
  60. package/dist/{portable-key-BXkgMpw1.mjs → portable-key-BL6K88p2.mjs} +1 -1
  61. package/dist/{proof-B5s-W6Sm.mjs → proof-vZ04IAjq.mjs} +10 -5
  62. package/dist/{send-BAJvkdNL.mjs → send-ylV8KCLx.mjs} +3 -3
  63. package/dist/sig/compound-proof-discovery.test.mjs +1 -1
  64. package/dist/sig/compound-proof-producer.test.mjs +1 -1
  65. package/dist/sig/compound-proof-verification.test.mjs +2 -2
  66. package/dist/sig/compound-proof.test.mjs +2 -2
  67. package/dist/sig/http.test.mjs +2 -2
  68. package/dist/sig/key.test.mjs +1 -1
  69. package/dist/sig/ld.test.mjs +2 -2
  70. package/dist/sig/mod.cjs +2 -2
  71. package/dist/sig/mod.js +2 -2
  72. package/dist/sig/owner.test.mjs +1 -1
  73. package/dist/sig/portable-collection.test.mjs +1 -1
  74. package/dist/sig/portable-key.test.mjs +5 -5
  75. package/dist/sig/proof.test.mjs +3 -3
  76. package/dist/{temporal-BkCPJ_uC.mjs → temporal-BTD4f_mZ.mjs} +1 -1
  77. package/dist/testing/kv.test.mjs +1 -1
  78. package/dist/testing/mod.d.mts +393 -4
  79. package/dist/testing/mod.mjs +2 -2
  80. package/dist/{testing-otQhu4aD.mjs → testing-CKaJhcA0.mjs} +1 -1
  81. package/dist/utils/docloader.test.mjs +2 -2
  82. package/dist/utils/kv-cache.test.mjs +1 -1
  83. package/dist/utils/mod.cjs +1 -1
  84. package/dist/utils/mod.js +1 -1
  85. package/package.json +6 -6
@@ -1,34 +1,34 @@
1
1
  import { Temporal } from "temporal-polyfill";
2
2
  import "urlpattern-polyfill";
3
3
  globalThis.addEventListener = () => {};
4
- import { n as version, t as name } from "./deno-BXI_BLNy.mjs";
5
- import { a as instrumentDocumentLoader, b as recordWebFingerHandle, c as recordCircuitBreakerStateChange, d as recordCollectionRequest, f as recordCollectionTotalItems, g as recordInboxActivity, h as recordFanoutRecipients, i as getRemoteHost, l as recordCollectionDispatchDuration, n as getDurationMs, o as isAbortError, r as getFederationMetrics, u as recordCollectionPageItems, v as recordOutboxActivity, x as registerQueueDepthGauge, y as recordOutboxEnqueue } from "./metrics-DxlVXwn5.mjs";
4
+ import { n as version, t as name } from "./deno-YLLbRq06.mjs";
5
+ import { a as instrumentDocumentLoader, b as recordWebFingerHandle, c as recordCircuitBreakerStateChange, d as recordCollectionRequest, f as recordCollectionTotalItems, g as recordInboxActivity, h as recordFanoutRecipients, i as getRemoteHost, l as recordCollectionDispatchDuration, n as getDurationMs, o as isAbortError, r as getFederationMetrics, u as recordCollectionPageItems, v as recordOutboxActivity, x as registerQueueDepthGauge, y as recordOutboxEnqueue } from "./metrics-CBHhbHgG.mjs";
6
6
  import { t as formatAcceptSignature } from "./accept-CPVqki78.mjs";
7
7
  import { a as hasPortableActor, c as isPortableId, l as isPortableKeyId, n as getGatewayKeyBase, o as isCompatibleKeyId, r as getPortableDid, t as getCanonicalPortableId, u as isPortableUri } from "./portable-key-id-Bg259c4Z.mjs";
8
- import { c as validateCryptoKey, n as exportJwk, s as importJwk } from "./key-C0giKuki.mjs";
8
+ import { c as validateCryptoKey, n as exportJwk, s as importJwk } from "./key-BZ-Z4uez.mjs";
9
9
  import { r as normalizeOutgoingActivityJsonLd } from "./outgoing-jsonld-4IJqPYos.mjs";
10
- import { _ as wrapContextLoaderForJsonLd, a as compactJsonLd, c as getNormalizationContextLoader, d as isClearlyMalformedContextReference, f as isInvalidUrlTypeError, l as hasSignature, m as verifyCompactJsonLd, p as signJsonLd, r as assertSafeJsonLd, s as detachSignature, t as InvalidContextReferenceError, u as hasSignatureLike } from "./ld-CWDReGMZ.mjs";
11
- import { i as signObject, l as verifyPortableObjectProofWithRoot, o as verifyObject, r as hasProofLike, s as verifyPortableObjectProof, t as InvalidPortableObjectIdError } from "./proof-B5s-W6Sm.mjs";
12
- import { a as listRequestSignatures, c as selectRequestSignature, d as verifyRequest, f as verifyRequestDetailed, s as parseRfc9421SignatureInput } from "./http-CEx2SZjf.mjs";
13
- import { t as getAuthenticatedDocumentLoader } from "./docloader-B5BIo7e9.mjs";
14
- import { n as kvCache } from "./kv-cache-AWTrxTq3.mjs";
10
+ import { _ as wrapContextLoaderForJsonLd, a as compactJsonLd, c as getNormalizationContextLoader, d as isClearlyMalformedContextReference, f as isInvalidUrlTypeError, l as hasSignature, m as verifyCompactJsonLd, p as signJsonLd, r as assertSafeJsonLd, s as detachSignature, t as InvalidContextReferenceError, u as hasSignatureLike } from "./ld-Djxsq6JR.mjs";
11
+ import { a as signObject, c as verifyPortableObjectProof, i as hasProofLike, s as verifyObject, t as InvalidPortableObjectIdError, u as verifyPortableObjectProofWithRoot } from "./proof-vZ04IAjq.mjs";
12
+ import { a as listRequestSignatures, c as selectRequestSignature, d as verifyRequest, f as verifyRequestDetailed, s as parseRfc9421SignatureInput } from "./http-CXnNbRe_.mjs";
13
+ import { t as getAuthenticatedDocumentLoader } from "./docloader-vc-pSu9-.mjs";
14
+ import { n as kvCache } from "./kv-cache-DZr8HTel.mjs";
15
15
  import { d as readBoundedText, l as BodyTooLargeError, t as getNodeInfo, u as MAX_BODY_SIZE } from "./client-DpnpgjyX.mjs";
16
16
  import { t as nodeInfoToJson } from "./types-DE_LV1JB.mjs";
17
- import { n as getKeyOwner, t as doesActorOwnKey } from "./owner-CgoCOTUQ.mjs";
18
- import { t as verifyPortableObject } from "./portable-collection-0_5DyXMt.mjs";
19
- import { n as extractInboxes, r as sendActivity, t as SendActivityError } from "./send-BAJvkdNL.mjs";
20
- import { n as FederationBuilderImpl, t as ACTOR_ALIAS_PREFIX } from "./builder-BI7H61zO.mjs";
17
+ import { n as getKeyOwner, t as doesActorOwnKey } from "./owner-OdWSE8am.mjs";
18
+ import { t as verifyPortableObject } from "./portable-collection-DAV7Ho3m.mjs";
19
+ import { n as extractInboxes, r as sendActivity, t as SendActivityError } from "./send-ylV8KCLx.mjs";
20
+ import { n as FederationBuilderImpl, t as ACTOR_ALIAS_PREFIX } from "./builder-CxHALSwA.mjs";
21
21
  import { t as CircuitBreaker } from "./circuit-breaker-C5NqoLBJ.mjs";
22
22
  import { t as buildCollectionSynchronizationHeader } from "./collection-CnnP1UAv.mjs";
23
- import { a as verifyCompoundPortableObjectProofs, i as inspectCompoundPortableObjectApplicability, r as findUnsupportedCompoundProofShape, t as containsCompoundPortableObject } from "./compound-proof-D0H4LLvQ.mjs";
23
+ import { a as verifyCompoundPortableObjectProofs, i as inspectCompoundPortableObjectApplicability, r as findUnsupportedCompoundProofShape, s as verifyServedPortableObjects, t as containsCompoundPortableObject } from "./compound-proof-nhrXTRQO.mjs";
24
24
  import { t as KvKeyCache } from "./keycache-BInaECGj.mjs";
25
25
  import { t as acceptsJsonLd } from "./negotiation-CCocWT_N.mjs";
26
- import { t as hasMalformedKnownTemporalLiteral } from "./temporal-BkCPJ_uC.mjs";
26
+ import { t as hasMalformedKnownTemporalLiteral } from "./temporal-BTD4f_mZ.mjs";
27
27
  import { t as createExponentialBackoffPolicy } from "./retry-zX5xw5e0.mjs";
28
28
  import { t as TaskCodec } from "./codec-C6rTrpIp.mjs";
29
29
  import { getLogger, withContext } from "@logtape/logtape";
30
30
  import { RouterError } from "@fedify/uri-template";
31
- import { Activity, Collection, CollectionPage, CryptographicKey, Link, Multikey, Object as Object$1, OrderedCollection, OrderedCollectionPage, Tombstone, getTypeId, lookupObject, traverseCollection } from "@fedify/vocab";
31
+ import { Activity, Collection, CollectionPage, CryptographicKey, Link, Multikey, Object as Object$1, OrderedCollection, OrderedCollectionPage, Tombstone, getTypeId, isActor, lookupObject, traverseCollection } from "@fedify/vocab";
32
32
  import { lookupWebFinger } from "@fedify/webfinger";
33
33
  import { SpanKind, SpanStatusCode, context, metrics, propagation, trace } from "@opentelemetry/api";
34
34
  import { uniq } from "es-toolkit";
@@ -1275,6 +1275,46 @@ function portableObjectInternalServerError(request) {
1275
1275
  headers: { "Content-Type": "text/plain; charset=utf-8" }
1276
1276
  });
1277
1277
  }
1278
+ /**
1279
+ * Checks the portable objects embedded in a portable collection or its page
1280
+ * that is about to be served against the FEP-ef61 proof policy. See
1281
+ * `verifyServedPortableObjects()`.
1282
+ * @returns `true` if the collection can be served.
1283
+ */
1284
+ async function verifyPortableCollection(jsonLd, context, portable) {
1285
+ const logger = getLogger([
1286
+ "fedify",
1287
+ "federation",
1288
+ "collection"
1289
+ ]);
1290
+ try {
1291
+ const result = await verifyServedPortableObjects(jsonLd, {
1292
+ contextLoader: context.contextLoader,
1293
+ documentLoader: context.documentLoader,
1294
+ tracerProvider: context.tracerProvider,
1295
+ meterProvider: context.meterProvider
1296
+ });
1297
+ if (result.verified) return true;
1298
+ logger.error("Refusing to serve the portable collection {collectionId}, as the portable object {objectId} it embeds at {path} does not satisfy the FEP-ef61 proof policy: {reason}. Embedded portable actors, activities, and objects need an Object Integrity Proof made with a key of the DID in their ID, and their own @context.", {
1299
+ collectionId: portable.id.href,
1300
+ objectId: result.id,
1301
+ path: result.path,
1302
+ reason: result.reason
1303
+ });
1304
+ } catch (error) {
1305
+ logger.error("Failed to verify the portable objects embedded in the portable collection {collectionId}:\n{error}", {
1306
+ collectionId: portable.id.href,
1307
+ error
1308
+ });
1309
+ }
1310
+ return false;
1311
+ }
1312
+ function respondWithPortableCollection(request, jsonLd) {
1313
+ return new Response(request.method === "HEAD" ? null : JSON.stringify(jsonLd), { headers: {
1314
+ "Content-Type": PORTABLE_OBJECT_CONTENT_TYPE,
1315
+ Vary: "Accept"
1316
+ } });
1317
+ }
1278
1318
  const BUILT_IN_COLLECTION_METRIC_KINDS = /* @__PURE__ */ new Set([
1279
1319
  "inbox",
1280
1320
  "outbox",
@@ -1312,13 +1352,13 @@ function recordCollectionMetrics(meterProvider, base, result, options = {}) {
1312
1352
  * @param parameters The parameters for handling the collection.
1313
1353
  * @returns A promise that resolves to an HTTP response.
1314
1354
  */
1315
- async function handleCollection(request, { name: name$2, identifier, uriGetter, filter, filterPredicate, context, collectionCallbacks, tracerProvider, meterProvider, onUnauthorized, onNotFound }) {
1316
- const spanName = name$2.trim().replace(/\s+/g, "_");
1355
+ async function handleCollection(request, { name: name$1, identifier, uriGetter, filter, filterPredicate, context, collectionCallbacks, portable, tracerProvider, meterProvider, onUnauthorized, onNotFound }) {
1356
+ const spanName = name$1.trim().replace(/\s+/g, "_");
1317
1357
  tracerProvider = tracerProvider ?? trace.getTracerProvider();
1318
1358
  const tracer = tracerProvider.getTracer(name, version);
1319
1359
  const cursor = new URL(request.url).searchParams.get("cursor");
1320
1360
  const metricBase = {
1321
- kind: getCollectionMetricKind(name$2),
1361
+ kind: getCollectionMetricKind(name$1),
1322
1362
  page: cursor != null,
1323
1363
  dispatcher: "built_in"
1324
1364
  };
@@ -1336,8 +1376,13 @@ async function handleCollection(request, { name: name$2, identifier, uriGetter,
1336
1376
  };
1337
1377
  try {
1338
1378
  if (collectionCallbacks == null) return finish(await onNotFound(request), "not_found");
1379
+ if (portable != null && collectionCallbacks.authorizePredicate != null) {
1380
+ if (!await collectionCallbacks.authorizePredicate(context, identifier)) return finish(await onUnauthorized(request), "unauthorized");
1381
+ }
1339
1382
  let collection;
1340
- const baseUri = uriGetter(identifier);
1383
+ const baseUri = portable?.id ?? uriGetter(identifier);
1384
+ const pageBase = portable?.view ?? context.url;
1385
+ const attribution = portable?.attribution ?? null;
1341
1386
  if (cursor == null) {
1342
1387
  const firstCursor = await collectionCallbacks.firstCursor?.(context, identifier);
1343
1388
  const totalItems = filter == null ? await collectionCallbacks.counter?.(context, identifier) : void 0;
@@ -1359,7 +1404,7 @@ async function handleCollection(request, { name: name$2, identifier, uriGetter,
1359
1404
  span.setStatus({ code: SpanStatusCode.ERROR });
1360
1405
  return await onNotFound(request);
1361
1406
  }
1362
- const items = filterCollectionItems(page.items, name$2, filterPredicate);
1407
+ const items = filterCollectionItems(page.items, name$1, filterPredicate);
1363
1408
  itemCount = items.length;
1364
1409
  span.setAttribute("fedify.collection.items", itemCount);
1365
1410
  return items;
@@ -1377,29 +1422,31 @@ async function handleCollection(request, { name: name$2, identifier, uriGetter,
1377
1422
  if (itemsOrResponse instanceof Response) return finish(itemsOrResponse, "not_found");
1378
1423
  collection = new OrderedCollection({
1379
1424
  id: baseUri,
1425
+ attribution,
1380
1426
  totalItems: totalItemCount ?? null,
1381
1427
  items: itemsOrResponse
1382
1428
  });
1383
1429
  } else {
1384
1430
  const lastCursor = await collectionCallbacks.lastCursor?.(context, identifier);
1385
- const first = new URL(context.url);
1431
+ const first = new URL(pageBase);
1386
1432
  first.searchParams.set("cursor", firstCursor);
1387
1433
  let last = null;
1388
1434
  if (lastCursor != null) {
1389
- last = new URL(context.url);
1435
+ last = new URL(pageBase);
1390
1436
  last.searchParams.set("cursor", lastCursor);
1391
1437
  }
1392
1438
  collection = new OrderedCollection({
1393
1439
  id: baseUri,
1440
+ attribution,
1394
1441
  totalItems: totalItemCount ?? null,
1395
1442
  first,
1396
1443
  last
1397
1444
  });
1398
1445
  }
1399
1446
  } else {
1400
- const uri = new URL(baseUri);
1447
+ const uri = new URL(portable == null ? baseUri : pageBase);
1401
1448
  uri.searchParams.set("cursor", cursor);
1402
- const pageOrResponse = await tracer.startActiveSpan(`activitypub.dispatch_collection_page ${name$2}`, {
1449
+ const pageOrResponse = await tracer.startActiveSpan(`activitypub.dispatch_collection_page ${name$1}`, {
1403
1450
  kind: SpanKind.SERVER,
1404
1451
  attributes: {
1405
1452
  "activitypub.collection.id": uri.href,
@@ -1415,7 +1462,7 @@ async function handleCollection(request, { name: name$2, identifier, uriGetter,
1415
1462
  span.setStatus({ code: SpanStatusCode.ERROR });
1416
1463
  return await onNotFound(request);
1417
1464
  }
1418
- const items = filterCollectionItems(page.items, name$2, filterPredicate);
1465
+ const items = filterCollectionItems(page.items, name$1, filterPredicate);
1419
1466
  itemCount = items.length;
1420
1467
  span.setAttribute("fedify.collection.items", itemCount);
1421
1468
  return {
@@ -1437,28 +1484,30 @@ async function handleCollection(request, { name: name$2, identifier, uriGetter,
1437
1484
  const { items, prevCursor, nextCursor } = pageOrResponse;
1438
1485
  let prev = null;
1439
1486
  if (prevCursor != null) {
1440
- prev = new URL(context.url);
1487
+ prev = new URL(pageBase);
1441
1488
  prev.searchParams.set("cursor", prevCursor);
1442
1489
  }
1443
1490
  let next = null;
1444
1491
  if (nextCursor != null) {
1445
- next = new URL(context.url);
1492
+ next = new URL(pageBase);
1446
1493
  next.searchParams.set("cursor", nextCursor);
1447
1494
  }
1448
- const partOf = new URL(context.url);
1495
+ const partOf = new URL(pageBase);
1449
1496
  partOf.searchParams.delete("cursor");
1450
1497
  collection = new OrderedCollectionPage({
1451
1498
  id: uri,
1499
+ attribution,
1452
1500
  prev,
1453
1501
  next,
1454
1502
  items,
1455
1503
  partOf
1456
1504
  });
1457
1505
  }
1458
- if (collectionCallbacks.authorizePredicate != null) {
1506
+ if (portable == null && collectionCallbacks.authorizePredicate != null) {
1459
1507
  if (!await collectionCallbacks.authorizePredicate(context, identifier)) return finish(await onUnauthorized(request), "unauthorized");
1460
1508
  }
1461
1509
  const jsonLd = await collection.toJsonLd(context);
1510
+ if (portable != null) return await verifyPortableCollection(jsonLd, context, portable) ? finish(respondWithPortableCollection(request, jsonLd), "served") : finish(portableObjectInternalServerError(request), "error");
1462
1511
  return finish(new Response(JSON.stringify(jsonLd), { headers: {
1463
1512
  "Content-Type": "application/activity+json",
1464
1513
  Vary: "Accept"
@@ -2506,19 +2555,11 @@ function recordDeferredPendingCollectionMetrics(error, result, response) {
2506
2555
  pendingCollectionMetricRecorders.delete(error);
2507
2556
  recorder?.(result, response);
2508
2557
  }
2509
- async function _handleCustomCollection(request, { name, values, context, tracerProvider, meterProvider, collectionCallbacks: callbacks, filterPredicate }) {
2558
+ async function _handleCustomCollection(request, { name, values, context, tracerProvider, meterProvider, collectionCallbacks: callbacks, filterPredicate, portable }) {
2510
2559
  verifyDefined(callbacks);
2511
2560
  await authIfNeeded(context, values, callbacks);
2512
2561
  const cursor = new URL(request.url).searchParams.get("cursor");
2513
- const handler = new CustomCollectionHandler(name, values, context, callbacks, tracerProvider, meterProvider, Collection, CollectionPage, filterPredicate).fetchCollection(cursor);
2514
- try {
2515
- const response = await handler.toJsonLd().then(respondAsActivity);
2516
- handler.recordPendingCollectionMetrics("served", response);
2517
- return response;
2518
- } catch (e) {
2519
- if (!deferPendingCollectionMetrics(e, (result, response) => handler.recordPendingCollectionMetrics(result, response))) handler.recordPendingCollectionMetrics("error");
2520
- throw e;
2521
- }
2562
+ return await respondWithCustomCollection(request, new CustomCollectionHandler(name, values, context, callbacks, tracerProvider, meterProvider, Collection, CollectionPage, filterPredicate, portable).fetchCollection(cursor), context, portable);
2522
2563
  }
2523
2564
  /**
2524
2565
  * Handles an ordered collection request.
@@ -2532,13 +2573,19 @@ async function _handleCustomCollection(request, { name, values, context, tracerP
2532
2573
  * @since 1.8.0
2533
2574
  */
2534
2575
  const handleOrderedCollection = exceptWrapper(_handleOrderedCollection);
2535
- async function _handleOrderedCollection(request, { name, values, context, tracerProvider, meterProvider, collectionCallbacks: callbacks, filterPredicate }) {
2576
+ async function _handleOrderedCollection(request, { name, values, context, tracerProvider, meterProvider, collectionCallbacks: callbacks, filterPredicate, portable }) {
2536
2577
  verifyDefined(callbacks);
2537
2578
  await authIfNeeded(context, values, callbacks);
2538
2579
  const cursor = new URL(request.url).searchParams.get("cursor");
2539
- const handler = new CustomCollectionHandler(name, values, context, callbacks, tracerProvider, meterProvider, OrderedCollection, OrderedCollectionPage, filterPredicate).fetchCollection(cursor);
2580
+ return await respondWithCustomCollection(request, new CustomCollectionHandler(name, values, context, callbacks, tracerProvider, meterProvider, OrderedCollection, OrderedCollectionPage, filterPredicate, portable).fetchCollection(cursor), context, portable);
2581
+ }
2582
+ async function respondWithCustomCollection(request, handler, context, portable) {
2540
2583
  try {
2541
- const response = await handler.toJsonLd().then(respondAsActivity);
2584
+ const jsonLd = await handler.toJsonLd();
2585
+ let response;
2586
+ if (portable == null) response = respondAsActivity(jsonLd);
2587
+ else if (await verifyPortableCollection(jsonLd, context, portable)) response = respondWithPortableCollection(request, jsonLd);
2588
+ else throw new PortableCollectionRefusedError();
2542
2589
  handler.recordPendingCollectionMetrics("served", response);
2543
2590
  return response;
2544
2591
  } catch (e) {
@@ -2568,6 +2615,7 @@ var CustomCollectionHandler = class {
2568
2615
  Collection;
2569
2616
  CollectionPage;
2570
2617
  filterPredicate;
2618
+ portable;
2571
2619
  /**
2572
2620
  * The tracer for telemetry.
2573
2621
  * @type {Tracer}
@@ -2604,8 +2652,8 @@ var CustomCollectionHandler = class {
2604
2652
  * @param CollectionPage The CollectionPage constructor.
2605
2653
  * @param filterPredicate Optional filter predicate for items.
2606
2654
  */
2607
- constructor(name$1, values, context, callbacks, tracerProvider = trace.getTracerProvider(), meterProvider, Collection, CollectionPage, filterPredicate) {
2608
- this.name = name$1;
2655
+ constructor(name$2, values, context, callbacks, tracerProvider = trace.getTracerProvider(), meterProvider, Collection, CollectionPage, filterPredicate, portable) {
2656
+ this.name = name$2;
2609
2657
  this.values = values;
2610
2658
  this.context = context;
2611
2659
  this.callbacks = callbacks;
@@ -2614,9 +2662,14 @@ var CustomCollectionHandler = class {
2614
2662
  this.Collection = Collection;
2615
2663
  this.CollectionPage = CollectionPage;
2616
2664
  this.filterPredicate = filterPredicate;
2665
+ this.portable = portable;
2617
2666
  this.name = this.name.trim().replace(/\s+/g, "_");
2618
2667
  this.#tracer = this.tracerProvider.getTracer(name, version);
2619
- this.#id = new URL(this.context.url);
2668
+ if (portable == null) this.#id = new URL(this.context.url);
2669
+ else {
2670
+ const cursor = this.context.url.searchParams.get("cursor");
2671
+ this.#id = cursor == null ? new URL(portable.view) : appendCursorIfExists(portable.view, cursor);
2672
+ }
2620
2673
  this.#dispatcher = callbacks.dispatcher.bind(callbacks);
2621
2674
  }
2622
2675
  /**
@@ -2667,6 +2720,7 @@ var CustomCollectionHandler = class {
2667
2720
  this.recordPendingCollectionItemCount(true, items.length);
2668
2721
  return {
2669
2722
  id,
2723
+ attribution: this.portable?.attribution ?? null,
2670
2724
  partOf,
2671
2725
  items,
2672
2726
  prev: this.appendToUrl(prevCursor),
@@ -2688,6 +2742,7 @@ var CustomCollectionHandler = class {
2688
2742
  });
2689
2743
  return {
2690
2744
  id: this.#id,
2745
+ attribution: this.portable?.attribution ?? null,
2691
2746
  first: this.appendToUrl(firstCursor),
2692
2747
  last: this.appendToUrl(lastCursor),
2693
2748
  totalItems
@@ -2704,6 +2759,7 @@ var CustomCollectionHandler = class {
2704
2759
  this.recordPendingCollectionItemCount(false, items.length);
2705
2760
  return {
2706
2761
  id: this.#id,
2762
+ attribution: this.portable?.attribution ?? null,
2707
2763
  totalItems,
2708
2764
  items
2709
2765
  };
@@ -2821,7 +2877,7 @@ var CustomCollectionHandler = class {
2821
2877
  * @returns The URL with cursor appended, or null if cursor is null/undefined.
2822
2878
  */
2823
2879
  appendToUrl(cursor) {
2824
- return appendCursorIfExists(this.context.url, cursor);
2880
+ return appendCursorIfExists(this.portable?.view ?? this.context.url, cursor);
2825
2881
  }
2826
2882
  /**
2827
2883
  * Gets the stored collection or collection page.
@@ -2898,6 +2954,12 @@ function exceptWrapper(handler) {
2898
2954
  recordCollectionRequest(meterProvider, collectionAttributes(metricBase, "not_found", response));
2899
2955
  return response;
2900
2956
  }
2957
+ case PortableCollectionRefusedError: {
2958
+ const response = portableObjectInternalServerError(request);
2959
+ recordDeferredPendingCollectionMetrics(error, "error", response);
2960
+ recordCollectionRequest(meterProvider, collectionAttributes(metricBase, "error", response));
2961
+ return response;
2962
+ }
2901
2963
  case UnauthorizedError: {
2902
2964
  const response = await onUnauthorized(request);
2903
2965
  recordDeferredPendingCollectionMetrics(error, "unauthorized", response);
@@ -2994,6 +3056,16 @@ var UnauthorizedError = class extends HandlerError {
2994
3056
  }
2995
3057
  };
2996
3058
  /**
3059
+ * Error thrown when a portable collection is not served because it embeds
3060
+ * portable objects that do not satisfy the FEP-ef61 proof policy.
3061
+ * @since 2.4.0
3062
+ */
3063
+ var PortableCollectionRefusedError = class extends HandlerError {
3064
+ constructor() {
3065
+ super("The portable collection embeds unverifiable portable objects.");
3066
+ }
3067
+ };
3068
+ /**
2997
3069
  * Responds with the given object in JSON-LD format.
2998
3070
  *
2999
3071
  * @param object The object to respond with.
@@ -3133,6 +3205,100 @@ async function verifyPortableActivityId(activityId, jsonLd, options) {
3133
3205
  return null;
3134
3206
  }
3135
3207
  //#endregion
3208
+ //#region src/federation/portable-collection.ts
3209
+ /**
3210
+ * The query parameters of a gateway request for a portable collection that
3211
+ * do not select a view of the collection: the page cursor, and FEP-ef61
3212
+ * location hints (`@gateway`, and the legacy `gateways`).
3213
+ */
3214
+ const NON_VIEW_PARAMETERS = /* @__PURE__ */ new Set([
3215
+ "cursor",
3216
+ "@gateway",
3217
+ "gateways"
3218
+ ]);
3219
+ /**
3220
+ * Checks that a collection requested through the FEP-ef61 gateway endpoint
3221
+ * belongs to a portable actor under the requested DID, and determines
3222
+ * the collection's ID from the actor.
3223
+ *
3224
+ * Collection dispatchers return items, not documents with IDs, so unlike
3225
+ * object dispatchers, there is nothing else to tell whether the requested
3226
+ * DID controls the collection.
3227
+ *
3228
+ * @param actor The actor that owns the collection according to
3229
+ * the application, dispatched without the portable request,
3230
+ * or `null` if there is none.
3231
+ * @param options The requested DID, and either the requested collection's
3232
+ * canonical ID and the actor's property that has to refer to
3233
+ * it (for the built-in collections of an actor), or
3234
+ * the requested collection's portable ID (for a custom
3235
+ * collection, whose ID has the same form as the actor's).
3236
+ * @returns The resolution.
3237
+ */
3238
+ function resolvePortableCollectionOwner(actor, options) {
3239
+ if (!isActor(actor)) return {
3240
+ status: "rejected",
3241
+ reason: "notFound"
3242
+ };
3243
+ const ownerId = actor.id;
3244
+ const did = ownerId == null || !isPortableId(ownerId) ? null : getPortableDid(ownerId);
3245
+ if (ownerId == null || did == null) return {
3246
+ status: "rejected",
3247
+ reason: "notPortable"
3248
+ };
3249
+ if (did !== options.authority) return {
3250
+ status: "rejected",
3251
+ reason: "authorityMismatch"
3252
+ };
3253
+ if ("portableId" in options) return {
3254
+ status: "accepted",
3255
+ ownerId,
3256
+ collectionId: getCustomCollectionId(ownerId, actor, options.portableId)
3257
+ };
3258
+ const property = options.property(actor);
3259
+ if (property == null || getCanonicalPortableId(property) !== options.canonicalId) return {
3260
+ status: "rejected",
3261
+ reason: "collectionMismatch"
3262
+ };
3263
+ const collectionId = new URL(property);
3264
+ collectionId.search = "";
3265
+ collectionId.hash = "";
3266
+ return {
3267
+ status: "accepted",
3268
+ ownerId,
3269
+ collectionId
3270
+ };
3271
+ }
3272
+ /**
3273
+ * Gets the ID of a custom portable collection in the same form as its owner's
3274
+ * ID: a portable ID if the owner has one, or otherwise a compatible identifier
3275
+ * on the owner's first gateway, as FEP-ef61 requires of publishers.
3276
+ */
3277
+ function getCustomCollectionId(ownerId, owner, portableId) {
3278
+ if (isPortableUri(ownerId)) return portableId;
3279
+ const gateway = owner.gateways[0];
3280
+ if (gateway == null || !isGatewayUrl(gateway)) return portableId;
3281
+ try {
3282
+ return toCompatibleEf61Id(portableId, gateway);
3283
+ } catch (error) {
3284
+ if (error instanceof TypeError) return portableId;
3285
+ throw error;
3286
+ }
3287
+ }
3288
+ /**
3289
+ * Builds the URI that the pages of a portable collection are relative to:
3290
+ * the collection's ID with the query parameters of the requested view, e.g.,
3291
+ * a filter, but without the page cursor and location hints.
3292
+ * @param collectionId The ID of the collection.
3293
+ * @param requestUrl The URL of the gateway request.
3294
+ * @returns The view URI.
3295
+ */
3296
+ function buildPortableCollectionView(collectionId, requestUrl) {
3297
+ const view = new URL(collectionId);
3298
+ for (const [name, value] of requestUrl.searchParams) if (!NON_VIEW_PARAMETERS.has(name)) view.searchParams.append(name, value);
3299
+ return view;
3300
+ }
3301
+ //#endregion
3136
3302
  //#region src/federation/outgoing-proof.ts
3137
3303
  function formatKeyIds(candidates) {
3138
3304
  return candidates.map((c) => c.verificationMethod.href).join(", ");
@@ -5415,12 +5581,7 @@ var FederationImpl = class extends FederationBuilderImpl {
5415
5581
  onNotFound
5416
5582
  });
5417
5583
  case "followers": {
5418
- let baseUrl = url.searchParams.get("base-url");
5419
- if (baseUrl != null) try {
5420
- baseUrl = `${new URL(baseUrl).origin}/`;
5421
- } catch {
5422
- baseUrl = null;
5423
- }
5584
+ const baseUrl = getFollowersBaseUrl(url);
5424
5585
  return await handleCollection(request, {
5425
5586
  name: "followers",
5426
5587
  identifier: route.values.identifier,
@@ -5431,7 +5592,7 @@ var FederationImpl = class extends FederationBuilderImpl {
5431
5592
  },
5432
5593
  context,
5433
5594
  filter: baseUrl != null ? new URL(baseUrl) : void 0,
5434
- filterPredicate: baseUrl != null ? ((i) => (i instanceof URL ? i.href : i.id?.href ?? "").startsWith(baseUrl)) : void 0,
5595
+ filterPredicate: baseUrl != null ? getFollowersFilterPredicate(baseUrl) : void 0,
5435
5596
  collectionCallbacks: this.followersCallbacks,
5436
5597
  tracerProvider: this.tracerProvider,
5437
5598
  meterProvider: this._meterProvider,
@@ -5525,17 +5686,30 @@ var FederationImpl = class extends FederationBuilderImpl {
5525
5686
  }
5526
5687
  const route = this.router.route(portable.path);
5527
5688
  const isActor = route != null && (route.name === "actor" || route.name.startsWith("actorAlias:"));
5528
- if (route == null || !isActor && !route.name.startsWith("object:")) {
5689
+ const collectionRoute = route == null ? void 0 : getCollectionMetricRoute(route.name.replace(/:.*$/, ""));
5690
+ if (route == null || !isActor && !route.name.startsWith("object:") && collectionRoute == null) {
5529
5691
  metricState.endpoint = "not_found";
5530
5692
  return await onNotFound(request);
5531
5693
  }
5532
5694
  metricState.routeTemplate = PORTABLE_GATEWAY_ROUTE_PREFIX + route.template;
5533
- metricState.endpoint = isActor ? "actor" : "object";
5695
+ metricState.endpoint = getEndpointCategory(route.name);
5534
5696
  span.updateName(`${request.method} ${metricState.routeTemplate}`);
5535
5697
  if (!acceptsJsonLd(request)) {
5536
5698
  metricState.endpoint = "not_acceptable";
5537
- return await onNotAcceptable(request);
5699
+ const response = await onNotAcceptable(request);
5700
+ if (collectionRoute != null) recordCollectionRequest(this._meterProvider, {
5701
+ ...collectionRoute,
5702
+ page: new URL(request.url).searchParams.get("cursor") != null,
5703
+ result: "not_acceptable",
5704
+ statusCode: response.status
5705
+ });
5706
+ return response;
5538
5707
  }
5708
+ if (collectionRoute != null) return await this.#fetchPortableCollection(request, portable, route, {
5709
+ onNotFound,
5710
+ onUnauthorized,
5711
+ contextData
5712
+ });
5539
5713
  if (isActor) {
5540
5714
  const identifier = route.name.startsWith("actorAlias:") ? route.name.substring(ACTOR_ALIAS_PREFIX.length) : route.values.identifier;
5541
5715
  const context = this.#createContext(request, contextData, {
@@ -5574,6 +5748,108 @@ var FederationImpl = class extends FederationBuilderImpl {
5574
5748
  onNotFound
5575
5749
  });
5576
5750
  }
5751
+ async #fetchPortableCollection(request, portable, route, { onNotFound, onUnauthorized, contextData }) {
5752
+ const logger = getLogger([
5753
+ "fedify",
5754
+ "federation",
5755
+ "collection"
5756
+ ]);
5757
+ const url = new URL(request.url);
5758
+ const { authority } = portable.portableRequest;
5759
+ const ownerContext = this.#createContext(request, contextData);
5760
+ const context = this.#createContext(request, contextData, { portableRequest: portable.portableRequest });
5761
+ const reject = async (reason, identifier) => {
5762
+ logger.debug("Not serving the portable collection {collection} of the actor {identifier}: {reason}.", {
5763
+ collection: portable.canonicalId,
5764
+ identifier,
5765
+ reason
5766
+ });
5767
+ return await onNotFound(request);
5768
+ };
5769
+ const routeName = route.name.replace(/:.*$/, "");
5770
+ if (routeName === "collection" || routeName === "orderedCollection") {
5771
+ const name = route.name.replace(/^(?:ordered)?[cC]ollection:/, "");
5772
+ const callbacks = this.collectionCallbacks[name];
5773
+ if (callbacks == null) return await onNotFound(request);
5774
+ const mapper = callbacks.portableOwnerMapper;
5775
+ if (mapper == null) return await reject("noPortableOwnerMapper");
5776
+ const identifier = await mapper(ownerContext, route.values);
5777
+ if (identifier == null) return await reject("notPortable");
5778
+ const resolution = resolvePortableCollectionOwner(await this.actorCallbacks?.dispatcher?.(ownerContext, identifier), {
5779
+ authority,
5780
+ portableId: portable.portableRequest.id
5781
+ });
5782
+ if (resolution.status === "rejected") return await reject(resolution.reason, identifier);
5783
+ const view = buildPortableCollectionView(resolution.collectionId, url);
5784
+ return await (routeName === "collection" ? handleCustomCollection : handleOrderedCollection)(request, {
5785
+ name,
5786
+ context,
5787
+ values: route.values,
5788
+ collectionCallbacks: callbacks,
5789
+ portable: {
5790
+ id: view,
5791
+ view,
5792
+ attribution: resolution.ownerId
5793
+ },
5794
+ tracerProvider: this.tracerProvider,
5795
+ meterProvider: this._meterProvider,
5796
+ onUnauthorized,
5797
+ onNotFound
5798
+ });
5799
+ }
5800
+ const identifier = route.values.identifier;
5801
+ const serve = async (name, callbacks, property, filter) => {
5802
+ if (callbacks == null) return await onNotFound(request);
5803
+ const resolution = resolvePortableCollectionOwner(await this.actorCallbacks?.dispatcher?.(ownerContext, identifier), {
5804
+ authority,
5805
+ canonicalId: portable.canonicalId,
5806
+ property
5807
+ });
5808
+ if (resolution.status === "rejected") return await reject(resolution.reason, identifier);
5809
+ const id = new URL(resolution.collectionId);
5810
+ if (filter != null) id.searchParams.set(...filter.parameter);
5811
+ const view = buildPortableCollectionView(resolution.collectionId, url);
5812
+ if (routeName === "followers") {
5813
+ view.searchParams.delete("base-url");
5814
+ if (filter != null) view.searchParams.set(...filter.parameter);
5815
+ }
5816
+ return await handleCollection(request, {
5817
+ name,
5818
+ identifier,
5819
+ uriGetter: () => id,
5820
+ context,
5821
+ filter: filter?.value,
5822
+ filterPredicate: filter?.predicate,
5823
+ collectionCallbacks: callbacks,
5824
+ portable: {
5825
+ id,
5826
+ view,
5827
+ attribution: resolution.ownerId
5828
+ },
5829
+ tracerProvider: this.tracerProvider,
5830
+ meterProvider: this._meterProvider,
5831
+ onUnauthorized,
5832
+ onNotFound
5833
+ });
5834
+ };
5835
+ switch (routeName) {
5836
+ case "outbox": return await serve("outbox", this.outboxCallbacks, (a) => a.outboxId);
5837
+ case "inbox": return await serve("inbox", this.inboxCallbacks, (a) => a.inboxId);
5838
+ case "following": return await serve("following", this.followingCallbacks, (a) => a.followingId);
5839
+ case "followers": {
5840
+ const baseUrl = getFollowersBaseUrl(url);
5841
+ return await serve("followers", this.followersCallbacks, (a) => a.followersId, baseUrl == null ? void 0 : {
5842
+ value: new URL(baseUrl),
5843
+ predicate: getFollowersFilterPredicate(baseUrl),
5844
+ parameter: ["base-url", baseUrl]
5845
+ });
5846
+ }
5847
+ case "liked": return await serve("liked", this.likedCallbacks, (a) => a.likedId);
5848
+ case "featured": return await serve("featured", this.featuredCallbacks, (a) => a.featuredId);
5849
+ case "featuredTags": return await serve("featured tags", this.featuredTagsCallbacks, (a) => a.featuredTagsId);
5850
+ }
5851
+ return await onNotFound(request);
5852
+ }
5577
5853
  async #handleInbox(request, { recipient, context, contextData, onNotFound, portableInbox }) {
5578
5854
  if (!this.manuallyStartQueue) this._startQueueInternal(contextData);
5579
5855
  const inboxContextFactory = context.toInboxContext.bind(context);
@@ -5760,6 +6036,41 @@ var FederationImpl = class extends FederationBuilderImpl {
5760
6036
  };
5761
6037
  const PORTABLE_GATEWAY_ROUTE_PREFIX = "/.well-known/apgateway/{did}";
5762
6038
  /**
6039
+ * Gets the origin that a followers collection request filters followers by,
6040
+ * i.e., its normalized `base-url` query parameter (FEP-8fcf).
6041
+ * @returns The origin with a trailing slash, or `null` if the parameter is
6042
+ * missing or invalid.
6043
+ */
6044
+ function getFollowersBaseUrl(url) {
6045
+ const baseUrl = url.searchParams.get("base-url");
6046
+ if (baseUrl == null) return null;
6047
+ try {
6048
+ return `${new URL(baseUrl).origin}/`;
6049
+ } catch {
6050
+ return null;
6051
+ }
6052
+ }
6053
+ function getFollowersFilterPredicate(baseUrl) {
6054
+ return (i) => (i instanceof URL ? i.href : i.id?.href ?? "").startsWith(baseUrl);
6055
+ }
6056
+ const ACTOR_COLLECTION_ROUTE_ERRORS = {
6057
+ outbox: "No outbox dispatcher registered.",
6058
+ following: "No following collection path registered.",
6059
+ followers: "No followers collection path registered.",
6060
+ liked: "No liked collection path registered.",
6061
+ featured: "No featured collection path registered.",
6062
+ featuredTags: "No featured tags collection path registered."
6063
+ };
6064
+ /**
6065
+ * Checks that the authority of a portable ID to build is given, either
6066
+ * explicitly or by the FEP-ef61 gateway request being handled.
6067
+ * @throws {TypeError} If the authority is missing.
6068
+ */
6069
+ function requirePortableAuthority(authority) {
6070
+ if (authority == null) throw new TypeError("The authority of a portable ID is required outside an FEP-ef61 gateway request.");
6071
+ return authority;
6072
+ }
6073
+ /**
5763
6074
  * Removes the body of a response to a `HEAD` request.
5764
6075
  * @param request The request.
5765
6076
  * @param response The response to the request.
@@ -5938,8 +6249,7 @@ var ContextImpl = class ContextImpl {
5938
6249
  }
5939
6250
  getPortableActorUri(identifier, authority) {
5940
6251
  const path = this.#getActorPath(identifier);
5941
- if (authority == null) throw new TypeError("The authority of a portable ID is required outside an FEP-ef61 gateway request.");
5942
- return buildPortableUri(authority, path);
6252
+ return buildPortableUri(requirePortableAuthority(authority), path);
5943
6253
  }
5944
6254
  #getActorPath(identifier) {
5945
6255
  const path = this.federation.router.build(`actorAlias:${identifier}`, {}) ?? this.federation.router.build("actor", { identifier });
@@ -5951,8 +6261,7 @@ var ContextImpl = class ContextImpl {
5951
6261
  }
5952
6262
  getPortableObjectUri(cls, values, authority) {
5953
6263
  const path = this.#getObjectPath(cls, values);
5954
- if (authority == null) throw new TypeError("The authority of a portable ID is required outside an FEP-ef61 gateway request.");
5955
- return buildPortableUri(authority, path);
6264
+ return buildPortableUri(requirePortableAuthority(authority), path);
5956
6265
  }
5957
6266
  #getObjectPath(cls, values) {
5958
6267
  const callbacks = this.federation.objectCallbacks[cls.typeId.href];
@@ -5962,10 +6271,17 @@ var ContextImpl = class ContextImpl {
5962
6271
  if (path == null) throw new RouterError("No object dispatcher registered.");
5963
6272
  return path;
5964
6273
  }
6274
+ #getActorCollectionPath(route, identifier) {
6275
+ const path = this.federation.router.build(route, { identifier });
6276
+ if (path == null) throw new RouterError(ACTOR_COLLECTION_ROUTE_ERRORS[route]);
6277
+ return path;
6278
+ }
5965
6279
  getOutboxUri(identifier) {
5966
- const path = this.federation.router.build("outbox", { identifier });
5967
- if (path == null) throw new RouterError("No outbox dispatcher registered.");
5968
- return new URL(path, this.canonicalOrigin);
6280
+ return new URL(this.#getActorCollectionPath("outbox", identifier), this.canonicalOrigin);
6281
+ }
6282
+ getPortableOutboxUri(identifier, authority) {
6283
+ const path = this.#getActorCollectionPath("outbox", identifier);
6284
+ return buildPortableUri(requirePortableAuthority(authority), path);
5969
6285
  }
5970
6286
  getMediaUploaderUri(identifier) {
5971
6287
  const path = this.federation.router.build("mediaUploader", { identifier });
@@ -5985,39 +6301,53 @@ var ContextImpl = class ContextImpl {
5985
6301
  getPortableInboxUri(identifier, authority) {
5986
6302
  const path = this.federation.router.build("inbox", { identifier });
5987
6303
  if (path == null) throw new RouterError("No inbox path registered.");
5988
- if (authority == null) throw new TypeError("The authority of a portable ID is required outside an FEP-ef61 gateway request.");
5989
- return buildPortableUri(authority, path);
6304
+ return buildPortableUri(requirePortableAuthority(authority), path);
5990
6305
  }
5991
6306
  getFollowingUri(identifier) {
5992
- const path = this.federation.router.build("following", { identifier });
5993
- if (path == null) throw new RouterError("No following collection path registered.");
5994
- return new URL(path, this.canonicalOrigin);
6307
+ return new URL(this.#getActorCollectionPath("following", identifier), this.canonicalOrigin);
6308
+ }
6309
+ getPortableFollowingUri(identifier, authority) {
6310
+ const path = this.#getActorCollectionPath("following", identifier);
6311
+ return buildPortableUri(requirePortableAuthority(authority), path);
5995
6312
  }
5996
6313
  getFollowersUri(identifier) {
5997
- const path = this.federation.router.build("followers", { identifier });
5998
- if (path == null) throw new RouterError("No followers collection path registered.");
5999
- return new URL(path, this.canonicalOrigin);
6314
+ return new URL(this.#getActorCollectionPath("followers", identifier), this.canonicalOrigin);
6315
+ }
6316
+ getPortableFollowersUri(identifier, authority) {
6317
+ const path = this.#getActorCollectionPath("followers", identifier);
6318
+ return buildPortableUri(requirePortableAuthority(authority), path);
6000
6319
  }
6001
6320
  getLikedUri(identifier) {
6002
- const path = this.federation.router.build("liked", { identifier });
6003
- if (path == null) throw new RouterError("No liked collection path registered.");
6004
- return new URL(path, this.canonicalOrigin);
6321
+ return new URL(this.#getActorCollectionPath("liked", identifier), this.canonicalOrigin);
6322
+ }
6323
+ getPortableLikedUri(identifier, authority) {
6324
+ const path = this.#getActorCollectionPath("liked", identifier);
6325
+ return buildPortableUri(requirePortableAuthority(authority), path);
6005
6326
  }
6006
6327
  getFeaturedUri(identifier) {
6007
- const path = this.federation.router.build("featured", { identifier });
6008
- if (path == null) throw new RouterError("No featured collection path registered.");
6009
- return new URL(path, this.canonicalOrigin);
6328
+ return new URL(this.#getActorCollectionPath("featured", identifier), this.canonicalOrigin);
6329
+ }
6330
+ getPortableFeaturedUri(identifier, authority) {
6331
+ const path = this.#getActorCollectionPath("featured", identifier);
6332
+ return buildPortableUri(requirePortableAuthority(authority), path);
6010
6333
  }
6011
6334
  getFeaturedTagsUri(identifier) {
6012
- const path = this.federation.router.build("featuredTags", { identifier });
6013
- if (path == null) throw new RouterError("No featured tags collection path registered.");
6014
- return new URL(path, this.canonicalOrigin);
6335
+ return new URL(this.#getActorCollectionPath("featuredTags", identifier), this.canonicalOrigin);
6336
+ }
6337
+ getPortableFeaturedTagsUri(identifier, authority) {
6338
+ const path = this.#getActorCollectionPath("featuredTags", identifier);
6339
+ return buildPortableUri(requirePortableAuthority(authority), path);
6015
6340
  }
6016
6341
  getCollectionUri(name, values) {
6017
6342
  const path = this.federation.getCollectionPath(name, values);
6018
6343
  if (path === null) throw new RouterError(`No collection dispatcher registered for "${String(name)}".`);
6019
6344
  return new URL(path, this.canonicalOrigin);
6020
6345
  }
6346
+ getPortableCollectionUri(name, values, authority) {
6347
+ const path = this.federation.getCollectionPath(name, values);
6348
+ if (path === null) throw new RouterError(`No collection dispatcher registered for "${String(name)}".`);
6349
+ return buildPortableUri(requirePortableAuthority(authority), path);
6350
+ }
6021
6351
  parseUri(uri) {
6022
6352
  if (uri == null) return null;
6023
6353
  if (uri.origin !== this.origin && uri.origin !== this.canonicalOrigin) return null;
@@ -6597,6 +6927,27 @@ var RequestContextImpl = class RequestContextImpl extends ContextImpl {
6597
6927
  getPortableInboxUri(identifier, authority) {
6598
6928
  return super.getPortableInboxUri(identifier, authority ?? this.portableRequest?.authority);
6599
6929
  }
6930
+ getPortableOutboxUri(identifier, authority) {
6931
+ return super.getPortableOutboxUri(identifier, authority ?? this.portableRequest?.authority);
6932
+ }
6933
+ getPortableFollowingUri(identifier, authority) {
6934
+ return super.getPortableFollowingUri(identifier, authority ?? this.portableRequest?.authority);
6935
+ }
6936
+ getPortableFollowersUri(identifier, authority) {
6937
+ return super.getPortableFollowersUri(identifier, authority ?? this.portableRequest?.authority);
6938
+ }
6939
+ getPortableLikedUri(identifier, authority) {
6940
+ return super.getPortableLikedUri(identifier, authority ?? this.portableRequest?.authority);
6941
+ }
6942
+ getPortableFeaturedUri(identifier, authority) {
6943
+ return super.getPortableFeaturedUri(identifier, authority ?? this.portableRequest?.authority);
6944
+ }
6945
+ getPortableFeaturedTagsUri(identifier, authority) {
6946
+ return super.getPortableFeaturedTagsUri(identifier, authority ?? this.portableRequest?.authority);
6947
+ }
6948
+ getPortableCollectionUri(name, values, authority) {
6949
+ return super.getPortableCollectionUri(name, values, authority ?? this.portableRequest?.authority);
6950
+ }
6600
6951
  async getActor(identifier, options) {
6601
6952
  if (this.federation.actorCallbacks == null || this.federation.actorCallbacks.dispatcher == null) throw new Error("No actor dispatcher registered.");
6602
6953
  if (this.#invokedFromActorDispatcher != null) getLogger([