Unverified Commit c8474d94 authored by Hong Minhee's avatar Hong Minhee
Browse files

`documentLoaderFactory` & `contextLoaderFactory`

parent 203d67a0
Loading
Loading
Loading
Loading
+14 −0
Original line number Diff line number Diff line
@@ -8,6 +8,20 @@ Version 1.4.0

To be released.

 -  Document loader and context loader are now configurable with a factory
    function for more flexibility.

     -  Deprecated `CreateFederationOptions.documentLoader` option.
        Use `CreateFederationOptions.documentLoaderFactory` option instead.
     -  Deprecated `CreateFederationOptions.contextLoader` option.
        Use `CreateFederationOptions.contextLoaderFactory` option instead.
     -  Added `DocumentLoaderFactory` type.
     -  Added `DocumentLoaderFactoryOptions` interface.
     -  Added the second parameter with `DocumentLoaderFactoryOptions` type
        to `AuthenticatedDocumentLoaderFactory` type.
     -  `GetAuthenticatedDocumentLoaderOptions` interface became to extend
        `DocumentLoaderFactoryOptions` interface.

 -  The `suppressError` option of Activity Vocabulary APIs,
    `traverseCollection()` function, and `Context.traverseCollection()` method
    now suppresses errors occurred JSON-LD processing.
+14 −16
Original line number Diff line number Diff line
@@ -200,14 +200,17 @@ By default, the queue starts automatically.
> and the worker process (e.g., a Redis-backed message queue) as
> the [`queue`](#queue) option.

### `documentLoader`
### `documentLoaderFactory`

A JSON-LD document loader function that the `Federation` object uses to
load remote JSON-LD documents.  The function takes a URL and returns a
promise that resolves to a `RemoteDocument`.
*This API is available since Fedify 1.4.0.*

A factory function that creates a JSON-LD `DocumentLoader` that the `Federation`
object uses to load remote JSON-LD documents.  The factory function takes
a `DocumentLoaderFactoryOptions` object and returns a `DocumentLoader`
function.

Usually, you don't need to set this property because the default document
loader function is sufficient for most cases.  The default document loader
loader is sufficient for most cases.  The default document loader
caches the loaded documents in the key-value store.

See the
@@ -230,14 +233,14 @@ store.
See the [*Getting an authenticated `DocumentLoader`*
section](./context.md#getting-an-authenticated-documentloader) for details.

### `contextLoader`
### `contextLoaderFactory`

*This API is available since Fedify 0.8.0.*
*This API is available since Fedify 1.4.0.*

A JSON-LD context loader function that the `Federation` object uses to
load remote JSON-LD contexts.  The type of the function is the same as the
`documentLoader` function, but their purposes are different (see also
[*Document loader vs. context loader*
A factory function to get a JSON-LD context loader that the `Federation`
object uses to load remote JSON-LD contexts.  The type of the factory is
the same as the `documentLoaderFactory`, but their purposes are different
(see also [*Document loader vs. context loader*
section](./context.md#document-loader-vs-context-loader)).

### `allowPrivateAddress`
@@ -250,11 +253,6 @@ section](./context.md#document-loader-vs-context-loader)).

Whether to allow fetching private network addresses in the document loader.

If turned on, [`documentLoader`](#documentloader),
[`contextLoader`](#contextloader),
and [`authenticatedDocumentLoaderFactory`](#authenticateddocumentloaderfactory)
cannot be configured.

Mostly useful for testing purposes.

Turned off by default.
+10 −4
Original line number Diff line number Diff line
@@ -993,7 +993,7 @@ test("FederationImpl.sendActivity()", async (t) => {
      [{ privateKey: rsaPrivateKey2, keyId: rsaPublicKey2.id! }],
      recipient,
      activity,
      { contextData: undefined },
      { contextData: undefined, origin: "https://example.com" },
    );
    assertEquals(verified, ["http"]);
    assertInstanceOf(request, Request);
@@ -1011,7 +1011,7 @@ test("FederationImpl.sendActivity()", async (t) => {
      activity.clone({
        actor: new URL("https://example.com/person2"),
      }),
      { contextData: undefined },
      { contextData: undefined, origin: "https://example.com" },
    );
    assertEquals(verified, ["ld", "http"]);
    assertInstanceOf(request, Request);
@@ -1031,7 +1031,7 @@ test("FederationImpl.sendActivity()", async (t) => {
      activity.clone({
        actor: new URL("https://example.com/person2"),
      }),
      { contextData: undefined },
      { contextData: undefined, origin: "https://example.com" },
    );
    assertEquals(verified, ["proof"]);
    assertInstanceOf(request, Request);
@@ -1052,7 +1052,7 @@ test("FederationImpl.sendActivity()", async (t) => {
      activity.clone({
        actor: new URL("https://example.com/person2"),
      }),
      { contextData: undefined },
      { contextData: undefined, origin: "https://example.com" },
    );
    assertEquals(verified, ["ld", "proof", "http"]);
    assertInstanceOf(request, Request);
@@ -1222,6 +1222,7 @@ test("ContextImpl.sendActivity()", async (t) => {
      federation,
      url: new URL("https://example.com/"),
      documentLoader: fetchDocumentLoader,
      contextLoader: fetchDocumentLoader,
    });
    await ctx.sendActivity(
      [{ privateKey: rsaPrivateKey2, keyId: rsaPublicKey2.id! }],
@@ -1337,6 +1338,7 @@ test("ContextImpl.routeActivity()", async () => {
    federation,
    data: undefined,
    documentLoader: mockDocumentLoader,
    contextLoader: fetchDocumentLoader,
  });

  // Unsigned & non-dereferenceable activity
@@ -1525,6 +1527,7 @@ test("InboxContextImpl.forwardActivity()", async (t) => {
        federation,
        url: new URL("https://example.com/"),
        documentLoader: fetchDocumentLoader,
        contextLoader: fetchDocumentLoader,
      },
    );
    await ctx.forwardActivity(
@@ -1555,6 +1558,7 @@ test("InboxContextImpl.forwardActivity()", async (t) => {
        federation,
        url: new URL("https://example.com/"),
        documentLoader: fetchDocumentLoader,
        contextLoader: fetchDocumentLoader,
      },
    );
    await assertRejects(() =>
@@ -1589,6 +1593,7 @@ test("InboxContextImpl.forwardActivity()", async (t) => {
        federation,
        url: new URL("https://example.com/"),
        documentLoader: fetchDocumentLoader,
        contextLoader: fetchDocumentLoader,
      },
    );
    await ctx.forwardActivity(
@@ -1624,6 +1629,7 @@ test("InboxContextImpl.forwardActivity()", async (t) => {
        federation,
        url: new URL("https://example.com/"),
        documentLoader: fetchDocumentLoader,
        contextLoader: fetchDocumentLoader,
      },
    );
    await ctx.forwardActivity(
+99 −19
Original line number Diff line number Diff line
@@ -24,6 +24,8 @@ import type { JsonValue, NodeInfo } from "../nodeinfo/types.ts";
import {
  type AuthenticatedDocumentLoaderFactory,
  type DocumentLoader,
  type DocumentLoaderFactory,
  type DocumentLoaderFactoryOptions,
  getAuthenticatedDocumentLoader,
  getDocumentLoader,
  type GetUserAgentOptions,
@@ -154,15 +156,30 @@ export interface CreateFederationOptions {
   */
  manuallyStartQueue?: boolean;

  /**
   * A custom JSON-LD document loader factory.  By default, this uses
   * the built-in cache-backed loader that fetches remote documents over
   * HTTP(S).
   */
  documentLoaderFactory?: DocumentLoaderFactory;

  /**
   * A custom JSON-LD context loader factory.  By default, this uses the same
   * loader as the document loader.
   */
  contextLoaderFactory?: DocumentLoaderFactory;

  /**
   * A custom JSON-LD document loader.  By default, this uses the built-in
   * cache-backed loader that fetches remote documents over HTTP(S).
   * @deprecated Use {@link documentLoaderFactory} instead.
   */
  documentLoader?: DocumentLoader;

  /**
   * A custom JSON-LD context loader.  By default, this uses the same loader
   * as the document loader.
   * @deprecated Use {@link contextLoaderFactory} instead.
   */
  contextLoader?: DocumentLoader;

@@ -374,8 +391,8 @@ export class FederationImpl<TContextData> implements Federation<TContextData> {
  inboxListeners?: InboxListenerSet<TContextData>;
  inboxErrorHandler?: InboxErrorHandler<TContextData>;
  sharedInboxKeyDispatcher?: SharedInboxKeyDispatcher<TContextData>;
  documentLoader: DocumentLoader;
  contextLoader: DocumentLoader;
  documentLoaderFactory: DocumentLoaderFactory;
  contextLoaderFactory: DocumentLoaderFactory;
  authenticatedDocumentLoaderFactory: AuthenticatedDocumentLoaderFactory;
  allowPrivateAddress: boolean;
  userAgent?: GetUserAgentOptions | string;
@@ -387,6 +404,7 @@ export class FederationImpl<TContextData> implements Federation<TContextData> {
  tracerProvider: TracerProvider;

  constructor(options: CreateFederationOptions) {
    const logger = getLogger(["fedify", "federation"]);
    this.kv = options.kv;
    this.kvPrefixes = {
      ...({
@@ -436,12 +454,48 @@ export class FederationImpl<TContextData> implements Federation<TContextData> {
    }
    const { allowPrivateAddress, userAgent } = options;
    this.allowPrivateAddress = allowPrivateAddress ?? false;
    this.documentLoader = options.documentLoader ?? kvCache({
      loader: getDocumentLoader({ allowPrivateAddress, userAgent }),
    if (options.documentLoader != null) {
      if (options.documentLoaderFactory != null) {
        throw new TypeError(
          "Cannot set both documentLoader and documentLoaderFactory options " +
            "at a time; use documentLoaderFactory only.",
        );
      }
      this.documentLoaderFactory = () => options.documentLoader!;
      logger.warn(
        "The documentLoader option is deprecated; use documentLoaderFactory " +
          "option instead.",
      );
    } else {
      this.documentLoaderFactory = options.documentLoaderFactory ??
        ((opts) => {
          return kvCache({
            loader: getDocumentLoader({
              allowPrivateAddress: opts?.allowPrivateAddress ??
                allowPrivateAddress,
              userAgent: opts?.userAgent ?? userAgent,
            }),
            kv: options.kv,
            prefix: this.kvPrefixes.remoteDocument,
          });
    this.contextLoader = options.contextLoader ?? this.documentLoader;
        });
    }
    if (options.contextLoader != null) {
      if (options.contextLoaderFactory != null) {
        throw new TypeError(
          "Cannot set both contextLoader and contextLoaderFactory options " +
            "at a time; use contextLoaderFactory only.",
        );
      }
      this.contextLoaderFactory = () => options.contextLoader!;
      logger.warn(
        "The contextLoader option is deprecated; use contextLoaderFactory " +
          "option instead.",
      );
    } else {
      this.contextLoaderFactory = options.contextLoaderFactory ??
        this.documentLoaderFactory;
    }
    this.authenticatedDocumentLoaderFactory =
      options.authenticatedDocumentLoaderFactory ??
        ((identity) =>
@@ -608,11 +662,12 @@ export class FederationImpl<TContextData> implements Federation<TContextData> {
      });
    } catch (error) {
      span.setStatus({ code: SpanStatusCode.ERROR, message: String(error) });
      const loaderOptions = this.#getLoaderOptions(message.baseUrl);
      const activity = await Activity.fromJsonLd(message.activity, {
        contextLoader: this.contextLoader,
        contextLoader: this.contextLoaderFactory(loaderOptions),
        documentLoader: rsaKeyPair == null
          ? this.documentLoader
          : this.authenticatedDocumentLoaderFactory(rsaKeyPair),
          ? this.documentLoaderFactory(loaderOptions)
          : this.authenticatedDocumentLoaderFactory(rsaKeyPair, loaderOptions),
        tracerProvider: this.tracerProvider,
      });
      try {
@@ -885,11 +940,14 @@ export class FederationImpl<TContextData> implements Federation<TContextData> {
      url.hash = "";
      url.search = "";
    }
    const loaderOptions = this.#getLoaderOptions(url.origin);
    const ctxOptions: ContextOptions<TContextData> = {
      url,
      federation: this,
      data: contextData,
      documentLoader: opts.documentLoader ?? this.documentLoader,
      documentLoader: opts.documentLoader ??
        this.documentLoaderFactory(loaderOptions),
      contextLoader: this.contextLoaderFactory(loaderOptions),
    };
    if (request == null) return new ContextImpl(ctxOptions);
    return new RequestContextImpl({
@@ -900,6 +958,19 @@ export class FederationImpl<TContextData> implements Federation<TContextData> {
    });
  }

  #getLoaderOptions(origin: URL | string): DocumentLoaderFactoryOptions {
    origin = typeof origin === "string"
      ? new URL(origin).origin
      : origin.origin;
    return {
      allowPrivateAddress: this.allowPrivateAddress,
      userAgent: typeof this.userAgent === "string" ? this.userAgent : {
        url: origin,
        ...this.userAgent,
      },
    };
  }

  setNodeInfoDispatcher(
    path: string,
    dispatcher: NodeInfoDispatcher<TContextData>,
@@ -1929,6 +2000,7 @@ export class FederationImpl<TContextData> implements Federation<TContextData> {
      excludeBaseUris,
      collectionSync,
      contextData,
      origin,
    } = options;
    if (keys.length < 1) {
      throw new TypeError("The sender's keys must not be empty.");
@@ -1976,6 +2048,9 @@ export class FederationImpl<TContextData> implements Federation<TContextData> {
    } else if (keys.length < 1) {
      throw new TypeError("The keys must not be empty.");
    }
    const contextLoader = this.contextLoaderFactory(
      this.#getLoaderOptions(origin),
    );
    const activityId = activity.id.href;
    let proofCreated = false;
    let rsaKey: { keyId: URL; privateKey: CryptoKey } | null = null;
@@ -1987,7 +2062,7 @@ export class FederationImpl<TContextData> implements Federation<TContextData> {
      }
      if (privateKey.algorithm.name === "Ed25519") {
        activity = await signObject(activity, privateKey, keyId, {
          contextLoader: this.contextLoader,
          contextLoader,
          tracerProvider: this.tracerProvider,
        });
        proofCreated = true;
@@ -1995,7 +2070,7 @@ export class FederationImpl<TContextData> implements Federation<TContextData> {
    }
    let jsonLd = await activity.toJsonLd({
      format: "compact",
      contextLoader: this.contextLoader,
      contextLoader,
    });
    if (rsaKey == null) {
      logger.warn(
@@ -2013,7 +2088,7 @@ export class FederationImpl<TContextData> implements Federation<TContextData> {
      );
    } else {
      jsonLd = await signJsonLd(jsonLd, rsaKey.privateKey, rsaKey.keyId, {
        contextLoader: this.contextLoader,
        contextLoader,
        tracerProvider: this.tracerProvider,
      });
    }
@@ -2085,6 +2160,7 @@ export class FederationImpl<TContextData> implements Federation<TContextData> {
      const message: OutboxMessage = {
        type: "outbox",
        id: crypto.randomUUID(),
        baseUrl: origin,
        keys: keyJwkPairs,
        activity: jsonLd,
        activityId: activity.id?.href,
@@ -2407,6 +2483,7 @@ interface ContextOptions<TContextData> {
  federation: FederationImpl<TContextData>;
  data: TContextData;
  documentLoader: DocumentLoader;
  contextLoader: DocumentLoader;
  invokedFromActorKeyPairsDispatcher?: { identifier: string };
}

@@ -2415,6 +2492,7 @@ export class ContextImpl<TContextData> implements Context<TContextData> {
  readonly federation: FederationImpl<TContextData>;
  readonly data: TContextData;
  readonly documentLoader: DocumentLoader;
  readonly contextLoader: DocumentLoader;
  readonly invokedFromActorKeyPairsDispatcher?: { identifier: string };

  constructor(
@@ -2423,6 +2501,7 @@ export class ContextImpl<TContextData> implements Context<TContextData> {
      federation,
      data,
      documentLoader,
      contextLoader,
      invokedFromActorKeyPairsDispatcher,
    }: ContextOptions<TContextData>,
  ) {
@@ -2430,6 +2509,7 @@ export class ContextImpl<TContextData> implements Context<TContextData> {
    this.federation = federation;
    this.data = data;
    this.documentLoader = documentLoader;
    this.contextLoader = contextLoader;
    this.invokedFromActorKeyPairsDispatcher =
      invokedFromActorKeyPairsDispatcher;
  }
@@ -2445,6 +2525,7 @@ export class ContextImpl<TContextData> implements Context<TContextData> {
      federation: this.federation,
      data: this.data,
      documentLoader: this.documentLoader,
      contextLoader: this.contextLoader,
      invokedFromActorKeyPairsDispatcher:
        this.invokedFromActorKeyPairsDispatcher,
    });
@@ -2462,10 +2543,6 @@ export class ContextImpl<TContextData> implements Context<TContextData> {
    return this.url.origin;
  }

  get contextLoader(): DocumentLoader {
    return this.federation.contextLoader;
  }

  get tracerProvider(): TracerProvider {
    return this.federation.tracerProvider;
  }
@@ -3058,6 +3135,7 @@ export class ContextImpl<TContextData> implements Context<TContextData> {
    }
    const opts: SendActivityInternalOptions<TContextData> = {
      contextData: this.data,
      origin: this.origin,
      ...options,
    };
    let expandedRecipients: Recipient[];
@@ -3645,6 +3723,7 @@ export class InboxContextImpl<TContextData> extends ContextImpl<TContextData>
      const message: OutboxMessage = {
        type: "outbox",
        id: crypto.randomUUID(),
        baseUrl: this.origin,
        keys: keyJwkPairs,
        activity: this.activity,
        activityId: this.activityId,
@@ -3696,6 +3775,7 @@ interface SendActivityInternalOptions<TContextData>
  extends SendActivityOptions {
  collectionSync?: string;
  contextData: TContextData;
  origin: string;
}

function notFound(_request: Request): Response {
+1 −0
Original line number Diff line number Diff line
@@ -8,6 +8,7 @@ export type Message = OutboxMessage | InboxMessage;
export interface OutboxMessage {
  type: "outbox";
  id: ReturnType<typeof crypto.randomUUID>;
  baseUrl: string;
  keys: SenderKeyJwkPair[];
  activity: unknown;
  activityId?: string;
Loading