@fedify/fedify 2.4.0-dev.2035 → 2.4.0-dev.2047

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 (75) hide show
  1. package/dist/{builder-CxB0RNUS.mjs → builder-Cue1kVHU.mjs} +12 -11
  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-DAEfpSL-.mjs → compound-proof-YT4tajyj.mjs} +1 -1
  6. package/dist/{context-uyRUWoZQ.d.ts → context-BJPHhBmD.d.ts} +100 -2
  7. package/dist/{context-BeDnDuF3.d.cts → context-Bh_mJfbD.d.cts} +100 -2
  8. package/dist/{context-CbS35fLr.mjs → context-DG5QeQ8W.mjs} +3 -1
  9. package/dist/{deno-ByRFTNQR.mjs → deno-Dcj6O4LJ.mjs} +1 -1
  10. package/dist/{docloader-CQ8Edzd4.mjs → docloader-BRlNjHlQ.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/idempotency.test.mjs +2 -2
  16. package/dist/federation/metrics.test.mjs +1 -1
  17. package/dist/federation/middleware.test.mjs +8 -8
  18. package/dist/federation/mod.cjs +1 -1
  19. package/dist/federation/mod.d.cts +3 -3
  20. package/dist/federation/mod.d.ts +3 -3
  21. package/dist/federation/mod.js +1 -1
  22. package/dist/federation/outgoing-proof.test.mjs +5 -5
  23. package/dist/federation/portable.test.d.mts +2 -0
  24. package/dist/federation/portable.test.mjs +430 -0
  25. package/dist/federation/send.test.mjs +3 -3
  26. package/dist/federation/tasks/enqueue.test.mjs +2 -2
  27. package/dist/federation/tasks/tasks.test.mjs +3 -3
  28. package/dist/federation/temporal.test.mjs +1 -1
  29. package/dist/federation/webfinger.test.mjs +115 -2
  30. package/dist/{federation-Bxy5QoWo.cjs → federation-Bdu06LEl.cjs} +324 -26
  31. package/dist/{http-BMAvWfcv.js → http-BmdqHyms.js} +1 -1
  32. package/dist/{http--RCp5CRI.cjs → http-CGjvl34X.cjs} +1 -1
  33. package/dist/{http-BZl8ShOJ.mjs → http-CK6kjLnO.mjs} +3 -3
  34. package/dist/{key-BeFBBxpa.mjs → key-CzCAq6D9.mjs} +2 -2
  35. package/dist/{kv-cache-yChGryXN.cjs → kv-cache-CvEicbrf.cjs} +1 -1
  36. package/dist/{kv-cache-D_2ZatKb.mjs → kv-cache-YATuzCy-.mjs} +1 -1
  37. package/dist/{kv-cache-CnAYugy5.js → kv-cache-fi_-XZco.js} +1 -1
  38. package/dist/{ld-BepC--ab.mjs → ld-CNWm8mJc.mjs} +3 -3
  39. package/dist/{metrics-eZ0bEvxw.mjs → metrics-CDoBPWjf.mjs} +1 -1
  40. package/dist/{middleware-DDFdZjkU.mjs → middleware-CF6uz8wa.mjs} +1 -1
  41. package/dist/{middleware-8c3mI8D0.mjs → middleware-CtFNjIUD.mjs} +326 -28
  42. package/dist/{middleware-CeK467Oy.js → middleware-Drzx-OID.js} +325 -27
  43. package/dist/{mod-D882_Xsg.d.ts → mod-BofpDIcU.d.ts} +1 -1
  44. package/dist/{mod-x9W0d0Pk.d.cts → mod-DU8aVzQJ.d.cts} +1 -1
  45. package/dist/mod.cjs +4 -4
  46. package/dist/mod.d.cts +4 -4
  47. package/dist/mod.d.ts +4 -4
  48. package/dist/mod.js +4 -4
  49. package/dist/nodeinfo/handler.test.mjs +2 -2
  50. package/dist/{owner-DZOj3ivi.mjs → owner-B3PgGiJ1.mjs} +2 -2
  51. package/dist/{owner-DS-Bz_zW.js → owner-CMnmv7S9.js} +16 -3
  52. package/dist/{owner-DwF4aZoy.cjs → owner-Hurn_ocn.cjs} +16 -3
  53. package/dist/{proof-DyPuryze.mjs → proof-fKycdssC.mjs} +18 -5
  54. package/dist/{send-jgopmjWM.mjs → send-BqBILxYE.mjs} +3 -3
  55. package/dist/sig/compound-proof-discovery.test.mjs +1 -1
  56. package/dist/sig/compound-proof-producer.test.mjs +1 -1
  57. package/dist/sig/compound-proof-verification.test.mjs +2 -2
  58. package/dist/sig/compound-proof.test.mjs +2 -2
  59. package/dist/sig/http.test.mjs +2 -2
  60. package/dist/sig/key.test.mjs +1 -1
  61. package/dist/sig/ld.test.mjs +2 -2
  62. package/dist/sig/mod.cjs +2 -2
  63. package/dist/sig/mod.js +2 -2
  64. package/dist/sig/owner.test.mjs +1 -1
  65. package/dist/sig/proof.test.mjs +208 -1
  66. package/dist/{temporal-DjFCwpqJ.mjs → temporal-SUiLJ0Co.mjs} +1 -1
  67. package/dist/testing/kv.test.mjs +1 -1
  68. package/dist/testing/mod.d.mts +99 -1
  69. package/dist/testing/mod.mjs +2 -2
  70. package/dist/{testing-hbky2gMN.mjs → testing-By1YYsIK.mjs} +1 -1
  71. package/dist/utils/docloader.test.mjs +2 -2
  72. package/dist/utils/kv-cache.test.mjs +1 -1
  73. package/dist/utils/mod.cjs +1 -1
  74. package/dist/utils/mod.js +1 -1
  75. package/package.json +6 -6
@@ -2,18 +2,18 @@ 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 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-BMAvWfcv.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-BmdqHyms.js";
6
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-DS-Bz_zW.js";
8
- import { n as getAuthenticatedDocumentLoader, t as kvCache } from "./kv-cache-CnAYugy5.js";
7
+ import { C as verifyCompactJsonLd, D as normalizeOutgoingActivityJsonLd, E as wrapContextLoaderForJsonLd, S as signJsonLd, _ as getNormalizationContextLoader, a as signObject, b as isClearlyMalformedContextReference, c as verifyPortableObjectProof, 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-CMnmv7S9.js";
8
+ import { n as getAuthenticatedDocumentLoader, t as kvCache } from "./kv-cache-fi_-XZco.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, getFe34Origin, haveSameFe34Origin, validatePublicUrl } from "@fedify/vocab-runtime";
15
- import { uniq } from "es-toolkit";
14
+ import { FetchError, UrlError, canonicalizePortableUri, formatIri, fromCompatibleEf61Id, getDocumentLoader, getFe34Origin, haveSameFe34Origin, isGatewayUrl, parseIri, toCompatibleEf61Id, validatePublicUrl } from "@fedify/vocab-runtime";
16
15
  import jsonld from "@fedify/vocab-runtime/jsonld";
16
+ import { uniq } from "es-toolkit";
17
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";
@@ -203,38 +203,39 @@ var FederationBuilderImpl = class {
203
203
  "federation",
204
204
  "actor"
205
205
  ]);
206
+ const portable = actor.id?.protocol === "ap:" || actor.id?.protocol === "ap+ef61:";
206
207
  if (actor.id == null) logger.warn("Actor dispatcher returned an actor without an id property. Set the property with Context.getActorUri(identifier).");
207
- else if (actor.id.href != context.getActorUri(identifier).href) logger.warn("Actor dispatcher returned an actor with an id property that does not match the actor URI. Set the property with Context.getActorUri(identifier).");
208
+ else if (!portable && actor.id.href != context.getActorUri(identifier).href) logger.warn("Actor dispatcher returned an actor with an id property that does not match the actor URI. Set the property with Context.getActorUri(identifier).");
208
209
  if (actor instanceof Tombstone) return actor;
209
210
  if (this.followingCallbacks != null && this.followingCallbacks.dispatcher != null) {
210
211
  if (actor.followingId == null) logger.warn("You configured a following collection dispatcher, but the actor does not have a following property. Set the property with Context.getFollowingUri(identifier).");
211
- else if (actor.followingId.href != context.getFollowingUri(identifier).href) logger.warn("You configured a following collection dispatcher, but the actor's following property does not match the following collection URI. Set the property with Context.getFollowingUri(identifier).");
212
+ else if (!portable && actor.followingId.href != context.getFollowingUri(identifier).href) logger.warn("You configured a following collection dispatcher, but the actor's following property does not match the following collection URI. Set the property with Context.getFollowingUri(identifier).");
212
213
  }
213
214
  if (this.followersCallbacks != null && this.followersCallbacks.dispatcher != null) {
214
215
  if (actor.followersId == null) logger.warn("You configured a followers collection dispatcher, but the actor does not have a followers property. Set the property with Context.getFollowersUri(identifier).");
215
- else if (actor.followersId.href != context.getFollowersUri(identifier).href) logger.warn("You configured a followers collection dispatcher, but the actor's followers property does not match the followers collection URI. Set the property with Context.getFollowersUri(identifier).");
216
+ else if (!portable && actor.followersId.href != context.getFollowersUri(identifier).href) logger.warn("You configured a followers collection dispatcher, but the actor's followers property does not match the followers collection URI. Set the property with Context.getFollowersUri(identifier).");
216
217
  }
217
218
  if (this.outboxCallbacks != null && this.outboxCallbacks.dispatcher != null) {
218
219
  if (actor?.outboxId == null) logger.warn("You configured an outbox collection dispatcher, but the actor does not have an outbox property. Set the property with Context.getOutboxUri(identifier).");
219
- else if (actor.outboxId.href != context.getOutboxUri(identifier).href) logger.warn("You configured an outbox collection dispatcher, but the actor's outbox property does not match the outbox collection URI. Set the property with Context.getOutboxUri(identifier).");
220
+ else if (!portable && actor.outboxId.href != context.getOutboxUri(identifier).href) logger.warn("You configured an outbox collection dispatcher, but the actor's outbox property does not match the outbox collection URI. Set the property with Context.getOutboxUri(identifier).");
220
221
  }
221
222
  if (this.likedCallbacks != null && this.likedCallbacks.dispatcher != null) {
222
223
  if (actor?.likedId == null) logger.warn("You configured a liked collection dispatcher, but the actor does not have a liked property. Set the property with Context.getLikedUri(identifier).");
223
- else if (actor.likedId.href != context.getLikedUri(identifier).href) logger.warn("You configured a liked collection dispatcher, but the actor's liked property does not match the liked collection URI. Set the property with Context.getLikedUri(identifier).");
224
+ else if (!portable && actor.likedId.href != context.getLikedUri(identifier).href) logger.warn("You configured a liked collection dispatcher, but the actor's liked property does not match the liked collection URI. Set the property with Context.getLikedUri(identifier).");
224
225
  }
225
226
  if (this.featuredCallbacks != null && this.featuredCallbacks.dispatcher != null) {
226
227
  if (actor?.featuredId == null) logger.warn("You configured a featured collection dispatcher, but the actor does not have a featured property. Set the property with Context.getFeaturedUri(identifier).");
227
- else if (actor.featuredId.href != context.getFeaturedUri(identifier).href) logger.warn("You configured a featured collection dispatcher, but the actor's featured property does not match the featured collection URI. Set the property with Context.getFeaturedUri(identifier).");
228
+ else if (!portable && actor.featuredId.href != context.getFeaturedUri(identifier).href) logger.warn("You configured a featured collection dispatcher, but the actor's featured property does not match the featured collection URI. Set the property with Context.getFeaturedUri(identifier).");
228
229
  }
229
230
  if (this.featuredTagsCallbacks != null && this.featuredTagsCallbacks.dispatcher != null) {
230
231
  if (actor?.featuredTagsId == null) logger.warn("You configured a featured tags collection dispatcher, but the actor does not have a featuredTags property. Set the property with Context.getFeaturedTagsUri(identifier).");
231
- else if (actor.featuredTagsId.href != context.getFeaturedTagsUri(identifier).href) logger.warn("You configured a featured tags collection dispatcher, but the actor's featuredTags property does not match the featured tags collection URI. Set the property with Context.getFeaturedTagsUri(identifier).");
232
+ else if (!portable && actor.featuredTagsId.href != context.getFeaturedTagsUri(identifier).href) logger.warn("You configured a featured tags collection dispatcher, but the actor's featuredTags property does not match the featured tags collection URI. Set the property with Context.getFeaturedTagsUri(identifier).");
232
233
  }
233
234
  if (this.router.has("inbox")) {
234
235
  if (actor.inboxId == null) logger.warn("You configured inbox listeners, but the actor does not have an inbox property. Set the property with Context.getInboxUri(identifier).");
235
- else if (actor.inboxId.href != context.getInboxUri(identifier).href) logger.warn("You configured inbox listeners, but the actor's inbox property does not match the inbox URI. Set the property with Context.getInboxUri(identifier).");
236
+ else if (!portable && actor.inboxId.href != context.getInboxUri(identifier).href) logger.warn("You configured inbox listeners, but the actor's inbox property does not match the inbox URI. Set the property with Context.getInboxUri(identifier).");
236
237
  if (actor.endpoints == null || actor.endpoints.sharedInbox == null) logger.warn("You configured inbox listeners, but the actor does not have a endpoints.sharedInbox property. Set the property with Context.getInboxUri().");
237
- else if (actor.endpoints.sharedInbox.href != context.getInboxUri().href) logger.warn("You configured inbox listeners, but the actor's endpoints.sharedInbox property does not match the shared inbox URI. Set the property with Context.getInboxUri().");
238
+ else if (!portable && actor.endpoints.sharedInbox.href != context.getInboxUri().href) logger.warn("You configured inbox listeners, but the actor's endpoints.sharedInbox property does not match the shared inbox URI. Set the property with Context.getInboxUri().");
238
239
  }
239
240
  if (callbacks.keyPairsDispatcher != null) {
240
241
  if (actor.publicKeyId == null) logger.warn("You configured a key pairs dispatcher, but the actor does not have a publicKey property. Set the property with Context.getActorKeyPairs(identifier).");
@@ -2188,6 +2189,68 @@ function acceptsJsonLd(request) {
2188
2189
  return types.includes("application/activity+json") || types.includes("application/ld+json") || types.includes("application/json");
2189
2190
  }
2190
2191
  //#endregion
2192
+ //#region src/federation/portable.ts
2193
+ /**
2194
+ * The media type that FEP-ef61 gateways must use for portable objects.
2195
+ */
2196
+ const PORTABLE_OBJECT_CONTENT_TYPE = "application/ld+json; profile=\"https://www.w3.org/ns/activitystreams\"";
2197
+ const GATEWAY_OBJECT_PATH_PATTERN = /^\/\.well-known\/apgateway\/did(?::|%3A)/i;
2198
+ const BARE_DID_PATTERN = /^did:[a-z0-9]+:[^/?#]+$/i;
2199
+ /**
2200
+ * Recognizes an FEP-ef61 gateway request for a portable object, e.g.,
2201
+ * `GET /.well-known/apgateway/did:key:z6Mk.../notes/123`.
2202
+ *
2203
+ * The query is not part of the portable ID, so it is ignored here. It is not
2204
+ * removed from the request itself, which HTTP Signatures may cover.
2205
+ *
2206
+ * @param url The request URL.
2207
+ * @returns The parsed request, or `null` if the URL is not a gateway request
2208
+ * for a portable object, e.g., the gateway discovery endpoint or
2209
+ * a hashlink media URL.
2210
+ */
2211
+ function parsePortableGatewayRequest(url) {
2212
+ if (url.protocol !== "http:" && url.protocol !== "https:") return null;
2213
+ if (!GATEWAY_OBJECT_PATH_PATTERN.test(url.pathname)) return null;
2214
+ let id;
2215
+ let canonicalId;
2216
+ try {
2217
+ id = fromCompatibleEf61Id(url.origin + url.pathname);
2218
+ if (id == null) return null;
2219
+ canonicalId = canonicalizePortableUri(formatIri(id));
2220
+ } catch (error) {
2221
+ if (error instanceof TypeError) return {
2222
+ type: "malformed",
2223
+ error
2224
+ };
2225
+ throw error;
2226
+ }
2227
+ const authority = getFe34Origin(id);
2228
+ const href = id.href;
2229
+ return {
2230
+ type: "object",
2231
+ portableRequest: Object.freeze({
2232
+ authority,
2233
+ get id() {
2234
+ return new URL(href);
2235
+ }
2236
+ }),
2237
+ canonicalId,
2238
+ path: id.pathname
2239
+ };
2240
+ }
2241
+ /**
2242
+ * Builds a portable ID from a DID authority and an object path.
2243
+ * @param authority The bare DID, e.g., `did:key:z6Mk...`.
2244
+ * @param path The object path, e.g., `/notes/123`.
2245
+ * @returns The portable ID.
2246
+ * @throws {TypeError} If the authority is not a bare DID.
2247
+ */
2248
+ function buildPortableUri(authority, path) {
2249
+ if (typeof authority !== "string" || !BARE_DID_PATTERN.test(authority)) throw new TypeError("The authority of a portable ID must be a DID without a path, query, or fragment.");
2250
+ getFe34Origin(authority);
2251
+ return parseIri(`ap+ef61://${authority}${path}`);
2252
+ }
2253
+ //#endregion
2191
2254
  //#region src/federation/temporal.ts
2192
2255
  function isPlainObject$1(value) {
2193
2256
  return typeof value === "object" && value != null && !Array.isArray(value);
@@ -2372,6 +2435,102 @@ async function handleObject(request, { values, context, objectDispatcher, author
2372
2435
  Vary: "Accept"
2373
2436
  } });
2374
2437
  }
2438
+ /**
2439
+ * Handles an FEP-ef61 gateway request for a portable object through an object
2440
+ * dispatcher. The object is served only if its ID canonically matches the
2441
+ * requested portable ID and it satisfies the FEP-ef61 proof policy.
2442
+ * @template TContextData The context data to pass to the context.
2443
+ * @param request The HTTP request.
2444
+ * @param parameters The parameters for handling the portable object.
2445
+ * @returns A promise that resolves to an HTTP response.
2446
+ */
2447
+ async function handlePortableObject(request, { values, context, objectDispatcher, authorizePredicate, canonicalId, onNotFound, onUnauthorized }) {
2448
+ const logger = getLogger([
2449
+ "fedify",
2450
+ "federation",
2451
+ "object"
2452
+ ]);
2453
+ if (objectDispatcher == null) return await onNotFound(request);
2454
+ const object = await objectDispatcher(context, values);
2455
+ if (object == null) return await onNotFound(request);
2456
+ if (!isRequestedPortableObject(object, canonicalId)) {
2457
+ logger.debug("The object {objectId} does not match the requested portable object {portableId}.", {
2458
+ objectId: object.id?.href,
2459
+ portableId: canonicalId
2460
+ });
2461
+ return await onNotFound(request);
2462
+ }
2463
+ if (authorizePredicate != null) {
2464
+ if (!await authorizePredicate(context, values)) return await onUnauthorized(request);
2465
+ }
2466
+ let jsonLd;
2467
+ let servedId;
2468
+ let result;
2469
+ try {
2470
+ jsonLd = await object.toJsonLd(context);
2471
+ servedId = await getJsonLdRootId(jsonLd, context.contextLoader);
2472
+ result = await verifyPortableObjectProof(jsonLd, {
2473
+ contextLoader: context.contextLoader,
2474
+ documentLoader: context.documentLoader,
2475
+ tracerProvider: context.tracerProvider,
2476
+ meterProvider: context.meterProvider
2477
+ });
2478
+ } catch (error) {
2479
+ logger.error("Failed to verify the Object Integrity Proofs of the portable object {portableId}:\n{error}", {
2480
+ portableId: canonicalId,
2481
+ error
2482
+ });
2483
+ return portableObjectInternalServerError(request);
2484
+ }
2485
+ if (servedId == null || !isRequestedPortableId(servedId, canonicalId)) {
2486
+ logger.debug("The serialized object does not match the requested portable object {portableId}.", { portableId: canonicalId });
2487
+ return await onNotFound(request);
2488
+ }
2489
+ if (!result.verified && result.reason.type !== "unsecuredCollection" && result.reason.type !== "unsupportedObjectType") {
2490
+ logger.error("Refusing to serve the portable object {portableId}, as it does not satisfy the FEP-ef61 proof policy: {reason}. Portable actors, activities, and objects need an Object Integrity Proof made with a key of the DID in their ID.", {
2491
+ portableId: canonicalId,
2492
+ reason: result.reason.type
2493
+ });
2494
+ return portableObjectInternalServerError(request);
2495
+ }
2496
+ return new Response(request.method === "HEAD" ? null : JSON.stringify(jsonLd), { headers: {
2497
+ "Content-Type": PORTABLE_OBJECT_CONTENT_TYPE,
2498
+ Vary: "Accept"
2499
+ } });
2500
+ }
2501
+ function isRequestedPortableObject(object, canonicalId) {
2502
+ if (object.id == null) return false;
2503
+ if (object.id.protocol !== "ap:" && object.id.protocol !== "ap+ef61:") return false;
2504
+ try {
2505
+ return canonicalizePortableUri(formatIri(object.id)) === canonicalId;
2506
+ } catch (error) {
2507
+ if (error instanceof TypeError) return false;
2508
+ throw error;
2509
+ }
2510
+ }
2511
+ async function getJsonLdRootId(jsonLd, contextLoader) {
2512
+ const expanded = await jsonld.expand(jsonLd, {
2513
+ documentLoader: getNormalizationContextLoader(contextLoader),
2514
+ keepFreeFloatingNodes: true
2515
+ });
2516
+ if (expanded.length !== 1) return null;
2517
+ const id = expanded[0]["@id"];
2518
+ return typeof id === "string" ? id : null;
2519
+ }
2520
+ function isRequestedPortableId(id, canonicalId) {
2521
+ try {
2522
+ return canonicalizePortableUri(id) === canonicalId;
2523
+ } catch (error) {
2524
+ if (error instanceof TypeError) return false;
2525
+ throw error;
2526
+ }
2527
+ }
2528
+ function portableObjectInternalServerError(request) {
2529
+ return new Response(request.method === "HEAD" ? null : "Internal server error.", {
2530
+ status: 500,
2531
+ headers: { "Content-Type": "text/plain; charset=utf-8" }
2532
+ });
2533
+ }
2375
2534
  const BUILT_IN_COLLECTION_METRIC_KINDS = /* @__PURE__ */ new Set([
2376
2535
  "inbox",
2377
2536
  "outbox",
@@ -2409,13 +2568,13 @@ function recordCollectionMetrics(meterProvider, base, result, options = {}) {
2409
2568
  * @param parameters The parameters for handling the collection.
2410
2569
  * @returns A promise that resolves to an HTTP response.
2411
2570
  */
2412
- async function handleCollection(request, { name: name$1, identifier, uriGetter, filter, filterPredicate, context, collectionCallbacks, tracerProvider, meterProvider, onUnauthorized, onNotFound }) {
2413
- const spanName = name$1.trim().replace(/\s+/g, "_");
2571
+ async function handleCollection(request, { name: name$2, identifier, uriGetter, filter, filterPredicate, context, collectionCallbacks, tracerProvider, meterProvider, onUnauthorized, onNotFound }) {
2572
+ const spanName = name$2.trim().replace(/\s+/g, "_");
2414
2573
  tracerProvider = tracerProvider ?? trace.getTracerProvider();
2415
2574
  const tracer = tracerProvider.getTracer(name, version);
2416
2575
  const cursor = new URL(request.url).searchParams.get("cursor");
2417
2576
  const metricBase = {
2418
- kind: getCollectionMetricKind(name$1),
2577
+ kind: getCollectionMetricKind(name$2),
2419
2578
  page: cursor != null,
2420
2579
  dispatcher: "built_in"
2421
2580
  };
@@ -2456,7 +2615,7 @@ async function handleCollection(request, { name: name$1, identifier, uriGetter,
2456
2615
  span.setStatus({ code: SpanStatusCode.ERROR });
2457
2616
  return await onNotFound(request);
2458
2617
  }
2459
- const items = filterCollectionItems(page.items, name$1, filterPredicate);
2618
+ const items = filterCollectionItems(page.items, name$2, filterPredicate);
2460
2619
  itemCount = items.length;
2461
2620
  span.setAttribute("fedify.collection.items", itemCount);
2462
2621
  return items;
@@ -2496,7 +2655,7 @@ async function handleCollection(request, { name: name$1, identifier, uriGetter,
2496
2655
  } else {
2497
2656
  const uri = new URL(baseUri);
2498
2657
  uri.searchParams.set("cursor", cursor);
2499
- const pageOrResponse = await tracer.startActiveSpan(`activitypub.dispatch_collection_page ${name$1}`, {
2658
+ const pageOrResponse = await tracer.startActiveSpan(`activitypub.dispatch_collection_page ${name$2}`, {
2500
2659
  kind: SpanKind.SERVER,
2501
2660
  attributes: {
2502
2661
  "activitypub.collection.id": uri.href,
@@ -2512,7 +2671,7 @@ async function handleCollection(request, { name: name$1, identifier, uriGetter,
2512
2671
  span.setStatus({ code: SpanStatusCode.ERROR });
2513
2672
  return await onNotFound(request);
2514
2673
  }
2515
- const items = filterCollectionItems(page.items, name$1, filterPredicate);
2674
+ const items = filterCollectionItems(page.items, name$2, filterPredicate);
2516
2675
  itemCount = items.length;
2517
2676
  span.setAttribute("fedify.collection.items", itemCount);
2518
2677
  return {
@@ -3605,8 +3764,8 @@ var CustomCollectionHandler = class {
3605
3764
  * @param CollectionPage The CollectionPage constructor.
3606
3765
  * @param filterPredicate Optional filter predicate for items.
3607
3766
  */
3608
- constructor(name$2, values, context, callbacks, tracerProvider = trace.getTracerProvider(), meterProvider, Collection, CollectionPage, filterPredicate) {
3609
- this.name = name$2;
3767
+ constructor(name$1, values, context, callbacks, tracerProvider = trace.getTracerProvider(), meterProvider, Collection, CollectionPage, filterPredicate) {
3768
+ this.name = name$1;
3610
3769
  this.values = values;
3611
3770
  this.context = context;
3612
3771
  this.callbacks = callbacks;
@@ -5092,6 +5251,34 @@ function getWebFingerSubjectAndAliases({ resourceUrl, actorUri, preferredUsernam
5092
5251
  };
5093
5252
  }
5094
5253
  /**
5254
+ * Computes the subject and aliases for an FEP-ef61 portable actor, whose
5255
+ * WebFinger address takes its domain from the first gateway of the actor
5256
+ * rather than from the actor ID. The canonical `ap:` ID is not emitted,
5257
+ * since it is not a valid URI for software that does not support FEP-ef61;
5258
+ * the compatible identifier stands for it.
5259
+ */
5260
+ function getPortableWebFingerSubjectAndAliases({ resourceUrl, compatibleId, preferredUsername, acctUsername, gatewayHost }) {
5261
+ const aliases = [];
5262
+ const add = (alias) => {
5263
+ if (alias !== subject && !aliases.includes(alias)) aliases.push(alias);
5264
+ };
5265
+ let subject;
5266
+ if (resourceUrl.protocol === "acct:") {
5267
+ const username = preferredUsername ?? acctUsername;
5268
+ subject = username == null ? resourceUrl.href : `acct:${username}@${gatewayHost}`;
5269
+ add(compatibleId.href);
5270
+ add(resourceUrl.href);
5271
+ } else {
5272
+ subject = preferredUsername == null ? compatibleId.href : `acct:${preferredUsername}@${gatewayHost}`;
5273
+ add(compatibleId.href);
5274
+ if (resourceUrl.protocol === "http:" || resourceUrl.protocol === "https:") add(resourceUrl.href);
5275
+ }
5276
+ return {
5277
+ subject,
5278
+ aliases
5279
+ };
5280
+ }
5281
+ /**
5095
5282
  * Handles a WebFinger request. You would not typically call this function
5096
5283
  * directly, but instead use {@link Federation.fetch} method.
5097
5284
  * @param request The WebFinger request to handle.
@@ -5176,7 +5363,7 @@ async function handleWebFingerInternal(request, { context, host, actorDispatcher
5176
5363
  span?.setAttribute("webfinger.resource", resource);
5177
5364
  let resourceUrl;
5178
5365
  try {
5179
- resourceUrl = new URL(resource);
5366
+ resourceUrl = parseIri(resource);
5180
5367
  } catch (e) {
5181
5368
  if (e instanceof TypeError) return new Response("Invalid resource URL.", { status: 400 });
5182
5369
  throw e;
@@ -5225,7 +5412,29 @@ async function handleWebFingerInternal(request, { context, host, actorDispatcher
5225
5412
  status: 410,
5226
5413
  headers: { "Access-Control-Allow-Origin": "*" }
5227
5414
  });
5228
- const actorUri = context.getActorUri(identifier);
5415
+ let actorUri = context.getActorUri(identifier);
5416
+ let portable;
5417
+ if (actor.id != null && (actor.id.protocol === "ap:" || actor.id.protocol === "ap+ef61:")) {
5418
+ const gateway = actor.gateway;
5419
+ let compatibleId;
5420
+ if (gateway != null && isGatewayUrl(gateway)) try {
5421
+ compatibleId = toCompatibleEf61Id(actor.id, gateway);
5422
+ } catch (error) {
5423
+ if (!(error instanceof TypeError)) throw error;
5424
+ }
5425
+ if (gateway == null || compatibleId == null) {
5426
+ logger.error("The portable actor {actorId} (identifier: {identifier}) needs an HTTP(S) origin as the first item of its gateways property to be discovered through WebFinger.", {
5427
+ actorId: formatIri(actor.id),
5428
+ identifier
5429
+ });
5430
+ return await onNotFound(request);
5431
+ }
5432
+ actorUri = compatibleId;
5433
+ portable = {
5434
+ compatibleId,
5435
+ gatewayHost: gateway.host
5436
+ };
5437
+ }
5229
5438
  const links = [{
5230
5439
  rel: "self",
5231
5440
  href: actorUri.href,
@@ -5252,13 +5461,19 @@ async function handleWebFingerInternal(request, { context, host, actorDispatcher
5252
5461
  const customLinks = await webFingerLinksDispatcher(context, resourceUrl);
5253
5462
  if (customLinks != null) for (const link of customLinks) links.push(link);
5254
5463
  }
5255
- const { subject, aliases } = getWebFingerSubjectAndAliases({
5464
+ const { subject, aliases } = portable == null ? getWebFingerSubjectAndAliases({
5256
5465
  resourceUrl,
5257
5466
  actorUri,
5258
5467
  preferredUsername: actor.preferredUsername,
5259
5468
  acctUsername,
5260
5469
  host,
5261
5470
  contextHost: context.url.host
5471
+ }) : getPortableWebFingerSubjectAndAliases({
5472
+ resourceUrl,
5473
+ compatibleId: portable.compatibleId,
5474
+ preferredUsername: actor.preferredUsername,
5475
+ acctUsername,
5476
+ gatewayHost: portable.gatewayHost
5262
5477
  });
5263
5478
  return new Response(JSON.stringify({
5264
5479
  subject,
@@ -6485,7 +6700,8 @@ var FederationImpl = class extends FederationBuilderImpl {
6485
6700
  ...ctxOptions,
6486
6701
  request,
6487
6702
  invokedFromActorDispatcher: opts.invokedFromActorDispatcher,
6488
- invokedFromObjectDispatcher: opts.invokedFromObjectDispatcher
6703
+ invokedFromObjectDispatcher: opts.invokedFromObjectDispatcher,
6704
+ portableRequest: opts.portableRequest
6489
6705
  });
6490
6706
  }
6491
6707
  #getLoaderOptions(origin) {
@@ -6736,6 +6952,24 @@ var FederationImpl = class extends FederationBuilderImpl {
6736
6952
  const url = new URL(request.url);
6737
6953
  const route = this.router.route(url.pathname);
6738
6954
  if (route == null) {
6955
+ const portable = request.method === "GET" || request.method === "HEAD" ? parsePortableGatewayRequest(url) : null;
6956
+ if (portable != null) {
6957
+ const response = await this.#fetchPortableObject(request, portable, {
6958
+ onNotFound,
6959
+ onNotAcceptable,
6960
+ onUnauthorized,
6961
+ contextData,
6962
+ span,
6963
+ metricState
6964
+ });
6965
+ if (request.method !== "HEAD" || response.body == null) return response;
6966
+ await response.body.cancel();
6967
+ return new Response(null, {
6968
+ status: response.status,
6969
+ statusText: response.statusText,
6970
+ headers: response.headers
6971
+ });
6972
+ }
6739
6973
  metricState.endpoint = "not_found";
6740
6974
  return await onNotFound(request);
6741
6975
  }
@@ -7011,7 +7245,56 @@ var FederationImpl = class extends FederationBuilderImpl {
7011
7245
  }
7012
7246
  }
7013
7247
  }
7248
+ async #fetchPortableObject(request, portable, { onNotFound, onNotAcceptable, onUnauthorized, contextData, span, metricState }) {
7249
+ if (portable.type === "malformed") {
7250
+ metricState.endpoint = "not_found";
7251
+ getLogger([
7252
+ "fedify",
7253
+ "federation",
7254
+ "object"
7255
+ ]).debug("Malformed FEP-ef61 gateway request {url}: {error}", {
7256
+ url: request.url,
7257
+ error: portable.error
7258
+ });
7259
+ return new Response(request.method === "HEAD" ? null : "Malformed portable object ID.", {
7260
+ status: 400,
7261
+ headers: { "Content-Type": "text/plain; charset=utf-8" }
7262
+ });
7263
+ }
7264
+ const route = this.router.route(portable.path);
7265
+ if (route == null || !route.name.startsWith("object:")) {
7266
+ metricState.endpoint = "not_found";
7267
+ return await onNotFound(request);
7268
+ }
7269
+ metricState.routeTemplate = PORTABLE_GATEWAY_ROUTE_PREFIX + route.template;
7270
+ metricState.endpoint = "object";
7271
+ span.updateName(`${request.method} ${metricState.routeTemplate}`);
7272
+ if (!acceptsJsonLd(request)) {
7273
+ metricState.endpoint = "not_acceptable";
7274
+ return await onNotAcceptable(request);
7275
+ }
7276
+ const typeId = route.name.replace(/^object:/, "");
7277
+ const callbacks = this.objectCallbacks[typeId];
7278
+ const cls = this.objectTypeIds[typeId];
7279
+ const context = this.#createContext(request, contextData, {
7280
+ invokedFromObjectDispatcher: {
7281
+ cls,
7282
+ values: route.values
7283
+ },
7284
+ portableRequest: portable.portableRequest
7285
+ });
7286
+ return await handlePortableObject(request, {
7287
+ values: route.values,
7288
+ context,
7289
+ objectDispatcher: callbacks?.dispatcher,
7290
+ authorizePredicate: callbacks?.authorizePredicate,
7291
+ canonicalId: portable.canonicalId,
7292
+ onUnauthorized,
7293
+ onNotFound
7294
+ });
7295
+ }
7014
7296
  };
7297
+ const PORTABLE_GATEWAY_ROUTE_PREFIX = "/.well-known/apgateway/{did}";
7015
7298
  function getEndpointCategory(routeName) {
7016
7299
  if (routeName.startsWith("object:")) return "object";
7017
7300
  if (routeName.startsWith("collection:") || routeName.startsWith("orderedCollection:")) return "collection";
@@ -7146,12 +7429,20 @@ var ContextImpl = class ContextImpl {
7146
7429
  return new URL(path, this.canonicalOrigin);
7147
7430
  }
7148
7431
  getObjectUri(cls, values) {
7432
+ return new URL(this.#getObjectPath(cls, values), this.canonicalOrigin);
7433
+ }
7434
+ getPortableObjectUri(cls, values, authority) {
7435
+ const path = this.#getObjectPath(cls, values);
7436
+ if (authority == null) throw new TypeError("The authority of a portable ID is required outside an FEP-ef61 gateway request.");
7437
+ return buildPortableUri(authority, path);
7438
+ }
7439
+ #getObjectPath(cls, values) {
7149
7440
  const callbacks = this.federation.objectCallbacks[cls.typeId.href];
7150
7441
  if (callbacks == null) throw new RouterError("No object dispatcher registered.");
7151
7442
  for (const param of callbacks.parameters) if (!(param in values)) throw new TypeError(`Missing parameter: ${param}`);
7152
7443
  const path = this.federation.router.build(`object:${cls.typeId.href}`, values);
7153
7444
  if (path == null) throw new RouterError("No object dispatcher registered.");
7154
- return new URL(path, this.canonicalOrigin);
7445
+ return path;
7155
7446
  }
7156
7447
  getOutboxUri(identifier) {
7157
7448
  const path = this.federation.router.build("outbox", { identifier });
@@ -7380,6 +7671,7 @@ var ContextImpl = class ContextImpl {
7380
7671
  userAgent: options.userAgent ?? this.federation.userAgent,
7381
7672
  tracerProvider: options.tracerProvider ?? this.tracerProvider,
7382
7673
  meterProvider: options.meterProvider ?? this.meterProvider,
7674
+ verifyPortableObject: options.verifyPortableObject ?? verifyPortableObjectProof,
7383
7675
  allowPrivateAddress: this.federation.allowPrivateAddress
7384
7676
  });
7385
7677
  }
@@ -7726,12 +8018,14 @@ var RequestContextImpl = class RequestContextImpl extends ContextImpl {
7726
8018
  #invokedFromObjectDispatcher;
7727
8019
  request;
7728
8020
  url = void 0;
8021
+ portableRequest;
7729
8022
  constructor(options) {
7730
8023
  super(options);
7731
8024
  this.#invokedFromActorDispatcher = options.invokedFromActorDispatcher;
7732
8025
  this.#invokedFromObjectDispatcher = options.invokedFromObjectDispatcher;
7733
8026
  this.request = options.request;
7734
8027
  this.url = options.url;
8028
+ this.portableRequest = options.portableRequest;
7735
8029
  }
7736
8030
  clone(data) {
7737
8031
  return new RequestContextImpl({
@@ -7743,9 +8037,13 @@ var RequestContextImpl = class RequestContextImpl extends ContextImpl {
7743
8037
  invokedFromActorKeyPairsDispatcher: this.invokedFromActorKeyPairsDispatcher,
7744
8038
  invokedFromActorDispatcher: this.#invokedFromActorDispatcher,
7745
8039
  invokedFromObjectDispatcher: this.#invokedFromObjectDispatcher,
8040
+ portableRequest: this.portableRequest,
7746
8041
  request: this.request
7747
8042
  });
7748
8043
  }
8044
+ getPortableObjectUri(cls, values, authority) {
8045
+ return super.getPortableObjectUri(cls, values, authority ?? this.portableRequest?.authority);
8046
+ }
7749
8047
  async getActor(identifier, options) {
7750
8048
  if (this.federation.actorCallbacks == null || this.federation.actorCallbacks.dispatcher == null) throw new Error("No actor dispatcher registered.");
7751
8049
  if (this.#invokedFromActorDispatcher != null) getLogger([
@@ -1,5 +1,5 @@
1
1
  /// <reference lib="esnext.temporal" />
2
- import { _t as ActorHandleMapper, gt as ActorDispatcher, ht as ActorAliasMapper, l as RequestContext, zt as WebFingerLinksDispatcher } from "./context-uyRUWoZQ.js";
2
+ import { Bt as WebFingerLinksDispatcher, _t as ActorDispatcher, gt as ActorAliasMapper, u as RequestContext, vt as ActorHandleMapper } from "./context-BJPHhBmD.js";
3
3
  import { RouterError, RouterOptions, RouterRouteResult } from "@fedify/uri-template";
4
4
  import { MeterProvider, Span, Tracer } from "@opentelemetry/api";
5
5
 
@@ -1,5 +1,5 @@
1
1
  /// <reference lib="esnext.temporal" />
2
- import { _t as ActorHandleMapper, gt as ActorDispatcher, ht as ActorAliasMapper, l as RequestContext, zt as WebFingerLinksDispatcher } from "./context-BeDnDuF3.cjs";
2
+ import { Bt as WebFingerLinksDispatcher, _t as ActorDispatcher, gt as ActorAliasMapper, u as RequestContext, vt as ActorHandleMapper } from "./context-Bh_mJfbD.cjs";
3
3
  import { MeterProvider, Span, Tracer } from "@opentelemetry/api";
4
4
  import { RouterError, RouterOptions, RouterRouteResult } from "@fedify/uri-template";
5
5
 
package/dist/mod.cjs CHANGED
@@ -3,11 +3,11 @@ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
3
3
  require("./rolldown-runtime-Dmm3t92W.cjs");
4
4
  const require_transformers = require("./transformers-BalCrSsr.cjs");
5
5
  require("./compat/mod.cjs");
6
- const require_http = require("./http--RCp5CRI.cjs");
7
- const require_federation = require("./federation-Bxy5QoWo.cjs");
6
+ const require_http = require("./http-CGjvl34X.cjs");
7
+ const require_federation = require("./federation-Bdu06LEl.cjs");
8
8
  const require_types = require("./types-Be2zbyeG.cjs");
9
- const require_owner = require("./owner-DwF4aZoy.cjs");
10
- const require_kv_cache = require("./kv-cache-yChGryXN.cjs");
9
+ const require_owner = require("./owner-Hurn_ocn.cjs");
10
+ const require_kv_cache = require("./kv-cache-CvEicbrf.cjs");
11
11
  require("./nodeinfo/mod.cjs");
12
12
  require("./runtime/mod.cjs");
13
13
  require("./sig/mod.cjs");