@fedify/vocab-runtime 2.4.0-dev.2035 → 2.4.0-dev.2054

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 (41) hide show
  1. package/deno.json +2 -1
  2. package/dist/internal/portable-dereference.cjs +315 -0
  3. package/dist/internal/portable-dereference.d.cts +87 -0
  4. package/dist/internal/portable-dereference.d.ts +87 -0
  5. package/dist/internal/portable-dereference.js +311 -0
  6. package/dist/mod.cjs +8 -2
  7. package/dist/mod.d.cts +2 -1
  8. package/dist/mod.d.ts +2 -1
  9. package/dist/mod.js +8 -2
  10. package/dist/portable-CduElToy.d.ts +52 -0
  11. package/dist/portable-DxF2Ut-Z.d.cts +52 -0
  12. package/dist/tests/{body-DRpkDnEg.cjs → body-B-qUpCl3.cjs} +1 -1
  13. package/dist/tests/{body-BEIDQFV6.mjs → body-hdMfBU1p.mjs} +1 -1
  14. package/dist/tests/body.test.cjs +1 -1
  15. package/dist/tests/body.test.mjs +1 -1
  16. package/dist/tests/decimal.test.cjs +1 -1
  17. package/dist/tests/decimal.test.mjs +1 -1
  18. package/dist/tests/{docloader-M8t8o-GD.cjs → docloader-BLHl6D4V.cjs} +2 -2
  19. package/dist/tests/{docloader-DHux1ztB.mjs → docloader-C_fQRm09.mjs} +2 -2
  20. package/dist/tests/docloader.test.cjs +2 -2
  21. package/dist/tests/docloader.test.mjs +2 -2
  22. package/dist/tests/internal/portable-dereference.test.cjs +123 -0
  23. package/dist/tests/internal/portable-dereference.test.d.cts +1 -0
  24. package/dist/tests/internal/portable-dereference.test.d.mts +1 -0
  25. package/dist/tests/internal/portable-dereference.test.mjs +124 -0
  26. package/dist/tests/jsonld-CmDhHNTx.cjs +12 -0
  27. package/dist/tests/jsonld-DweIJ07b.mjs +5 -0
  28. package/dist/tests/jsonld-cache.test.cjs +17 -22
  29. package/dist/tests/jsonld-cache.test.mjs +1 -4
  30. package/dist/tests/{request-eP_YLVjb.mjs → request-Bbg1-1C3.mjs} +8 -2
  31. package/dist/tests/{request-BbczASfn.cjs → request-CcifKcqU.cjs} +8 -2
  32. package/dist/tests/request.test.cjs +4 -1
  33. package/dist/tests/request.test.mjs +4 -1
  34. package/package.json +11 -1
  35. package/src/internal/portable-dereference.test.ts +74 -0
  36. package/src/internal/portable-dereference.ts +426 -0
  37. package/src/mod.ts +4 -0
  38. package/src/portable.ts +52 -0
  39. package/src/request.test.ts +10 -1
  40. package/src/request.ts +9 -1
  41. package/tsdown.config.ts +1 -0
@@ -0,0 +1,426 @@
1
+ import { getLogger } from "@logtape/logtape";
2
+ import {
3
+ type Span,
4
+ SpanStatusCode,
5
+ type TracerProvider,
6
+ } from "@opentelemetry/api";
7
+ import preloadedContexts from "../contexts.ts";
8
+ import type { DocumentLoader, RemoteDocument } from "../docloader.ts";
9
+ import jsonld from "../jsonld.ts";
10
+ import type { PortableObjectVerifier } from "../portable.ts";
11
+ import {
12
+ canonicalizePortableUri,
13
+ formatIri,
14
+ parseIri,
15
+ toCompatibleEf61Id,
16
+ } from "../url.ts";
17
+
18
+ const logger = getLogger(["fedify", "vocab", "gateway"]);
19
+
20
+ /**
21
+ * The maximum number of `@gateway` location hints to try for one reference.
22
+ * Hints come from possibly untrusted documents, so they are bounded to keep
23
+ * a single accessor call from fanning out to many servers.
24
+ */
25
+ const MAX_GATEWAY_HINTS = 5;
26
+
27
+ const LOCATION_HINT_PARAMETER = "@gateway";
28
+
29
+ // The same baseline contexts that Fedify's proof verifier always resolves
30
+ // from its built-in copies (see getNormalizationContextLoader() in
31
+ // @fedify/fedify). Serving them identically here keeps the identity check,
32
+ // the verifier, and the parser on the same context documents. Other
33
+ // contexts go through the caller's context loader.
34
+ const BASELINE_CONTEXT_URLS: ReadonlySet<string> = new Set([
35
+ "https://w3id.org/identity/v1",
36
+ "https://www.w3.org/ns/activitystreams",
37
+ "https://w3id.org/security/v1",
38
+ "https://w3id.org/security/data-integrity/v1",
39
+ ]);
40
+
41
+ /**
42
+ * Checks whether a URL is an FEP-ef61 portable ActivityPub IRI.
43
+ *
44
+ * @internal Technically exported for generated vocabulary classes, but not
45
+ * part of the public API contract. This is not considered public API for
46
+ * Semantic Versioning decisions.
47
+ */
48
+ export function isPortableIri(url: URL): boolean {
49
+ return url.protocol === "ap+ef61:" || url.protocol === "ap:";
50
+ }
51
+
52
+ /**
53
+ * Picks the ordered list of FEP-ef61 gateways to fetch a portable IRI from.
54
+ *
55
+ * If `gateways` is given, it is used as is (even when empty), and `@gateway`
56
+ * location hints in the IRI are ignored. Otherwise, up to
57
+ * {@link MAX_GATEWAY_HINTS} valid `@gateway` hints are used. Duplicate
58
+ * gateways are dropped in both cases.
59
+ *
60
+ * @throws {TypeError} If an explicit gateway is not an HTTP(S) origin.
61
+ * @internal
62
+ */
63
+ export function getPortableGatewayCandidates(
64
+ url: URL,
65
+ gateways?: readonly (string | URL)[],
66
+ ): URL[] {
67
+ const candidates: URL[] = [];
68
+ const seen = new Set<string>();
69
+ const add = (gateway: URL) => {
70
+ if (seen.has(gateway.href)) return;
71
+ seen.add(gateway.href);
72
+ candidates.push(gateway);
73
+ };
74
+ if (gateways != null) {
75
+ for (const gateway of gateways) {
76
+ const parsed = parseGatewayOrigin(gateway);
77
+ if (parsed == null) {
78
+ throw new TypeError(
79
+ "FEP-ef61 gateways must be HTTP(S) origins with no credentials, " +
80
+ "path, query, or fragment: " + String(gateway),
81
+ );
82
+ }
83
+ add(parsed);
84
+ }
85
+ return candidates;
86
+ }
87
+ for (
88
+ const hint of new URLSearchParams(url.search).getAll(
89
+ LOCATION_HINT_PARAMETER,
90
+ )
91
+ ) {
92
+ if (candidates.length >= MAX_GATEWAY_HINTS) break;
93
+ const parsed = parseGatewayOrigin(hint);
94
+ if (parsed == null) {
95
+ logger.debug(
96
+ "Ignoring an invalid FEP-ef61 gateway hint {hint} in {url}.",
97
+ { hint, url: formatIri(url) },
98
+ );
99
+ continue;
100
+ }
101
+ add(parsed);
102
+ }
103
+ return candidates;
104
+ }
105
+
106
+ function parseGatewayOrigin(gateway: string | URL): URL | null {
107
+ let url: URL;
108
+ if (gateway instanceof URL) url = new URL(gateway.href);
109
+ else if (typeof gateway === "string" && URL.canParse(gateway)) {
110
+ url = new URL(gateway);
111
+ } else return null;
112
+ // Comparing href with the origin also rejects credentials, a path, and
113
+ // query and fragment components, including empty ? and # delimiters.
114
+ if (
115
+ (url.protocol !== "http:" && url.protocol !== "https:") ||
116
+ url.href !== `${url.origin}/`
117
+ ) {
118
+ return null;
119
+ }
120
+ return url;
121
+ }
122
+
123
+ /**
124
+ * Creates a context loader that returns the same context documents for the
125
+ * whole dereference operation, so that the identity check, the proof
126
+ * verifier, and the parser interpret the fetched document identically even
127
+ * if the underlying loader is nondeterministic. Failed loads are not
128
+ * remembered, so a transient failure does not affect the next gateway.
129
+ *
130
+ * The parsed object keeps the loader for its own later dereferences, so
131
+ * `release()` turns it into a plain pass-through to the underlying loader
132
+ * once the operation is over.
133
+ */
134
+ function createSnapshotContextLoader(
135
+ contextLoader: DocumentLoader,
136
+ ): { loader: DocumentLoader; release: () => void } {
137
+ const cache = new Map<string, Promise<RemoteDocument>>();
138
+ let released = false;
139
+ const release = () => {
140
+ released = true;
141
+ cache.clear();
142
+ };
143
+ const loader: DocumentLoader = async (url, options) => {
144
+ if (released) return await contextLoader(url, options);
145
+ const key = URL.canParse(url) ? new URL(url).href : url;
146
+ if (BASELINE_CONTEXT_URLS.has(key)) {
147
+ return {
148
+ contextUrl: null,
149
+ document: structuredClone(preloadedContexts[key]),
150
+ documentUrl: key,
151
+ };
152
+ }
153
+ let promise = cache.get(key);
154
+ if (promise == null) {
155
+ const loading = contextLoader(url, options).then((document) =>
156
+ structuredClone(document)
157
+ );
158
+ promise = loading;
159
+ cache.set(key, loading);
160
+ loading.catch(() => {
161
+ if (cache.get(key) === loading) cache.delete(key);
162
+ });
163
+ }
164
+ return structuredClone(await promise);
165
+ };
166
+ return { loader, release };
167
+ }
168
+
169
+ /**
170
+ * Options for {@link dereferencePortableIri}.
171
+ *
172
+ * @internal Technically exported for generated vocabulary classes, but not
173
+ * part of the public API contract. This is not considered public API for
174
+ * Semantic Versioning decisions.
175
+ */
176
+ export interface DereferencePortableIriOptions<T> {
177
+ documentLoader: DocumentLoader;
178
+ contextLoader: DocumentLoader;
179
+ tracerProvider: TracerProvider;
180
+ gateways?: readonly (string | URL)[];
181
+ verifyPortableObject?: PortableObjectVerifier;
182
+ suppressError?: boolean;
183
+ crossOrigin?: "ignore" | "throw" | "trust";
184
+ /**
185
+ * The signal for cancelling the dereference. It is passed to the document
186
+ * loader for requests to gateways, and checked between attempts. Requests
187
+ * for JSON-LD contexts and verification methods may not be cancelled.
188
+ */
189
+ signal?: AbortSignal;
190
+ parse: (
191
+ document: unknown,
192
+ options: { contextLoader: DocumentLoader; baseUrl: URL },
193
+ ) => Promise<T>;
194
+ span?: Span;
195
+ }
196
+
197
+ /**
198
+ * The error thrown by {@link dereferencePortableIri} under
199
+ * `crossOrigin: "throw"` when gateways returned objects but none of them
200
+ * satisfied the identity and proof checks.
201
+ *
202
+ * @internal Not part of the public API contract.
203
+ */
204
+ export class PortableObjectRejectedError extends Error {
205
+ constructor(message: string) {
206
+ super(message);
207
+ this.name = "PortableObjectRejectedError";
208
+ }
209
+ }
210
+
211
+ type Attempt =
212
+ | { readonly type: "rejected"; readonly source: string }
213
+ | { readonly type: "error"; readonly error: unknown };
214
+
215
+ class PortableObjectRejection {
216
+ constructor(
217
+ readonly message: string,
218
+ readonly values: Record<string, unknown> = {},
219
+ ) {}
220
+ }
221
+
222
+ /**
223
+ * Dereferences an FEP-ef61 portable IRI through its gateways.
224
+ *
225
+ * Gateways are tried one by one until one of them returns a single-node
226
+ * document whose `@id`, if any, canonically matches the requested IRI and
227
+ * which passes `verifyPortableObject`. When there is no gateway to try, the document
228
+ * loader is asked for the portable IRI itself, and its result is validated
229
+ * the same way.
230
+ *
231
+ * @returns The parsed object, or `null` if no valid object was retrieved and
232
+ * `suppressError` is set or the failure was a rejected object
233
+ * (unless `crossOrigin` is `"throw"`).
234
+ * @throws {TypeError} If `gateways` has an invalid entry (always), if no
235
+ * verifier is given, or if the IRI cannot be turned into
236
+ * a gateway URL (unless `suppressError` is set).
237
+ * @internal Technically exported for generated vocabulary classes, but not
238
+ * part of the public API contract. This is not considered public API for
239
+ * Semantic Versioning decisions.
240
+ */
241
+ export async function dereferencePortableIri<T extends { id: URL | null }>(
242
+ url: URL,
243
+ options: DereferencePortableIriOptions<T>,
244
+ ): Promise<T | null> {
245
+ url = parseIri(url);
246
+ const lookupUrl = formatIri(url);
247
+ const { span } = options;
248
+ // Invalid gateways are a programming error, so they are never suppressed:
249
+ const gateways = getPortableGatewayCandidates(url, options.gateways);
250
+ const fail = (error: unknown): null => {
251
+ span?.setStatus({ code: SpanStatusCode.ERROR, message: String(error) });
252
+ if (options.suppressError) {
253
+ logger.error("Failed to dereference {url}: {error}", {
254
+ url: lookupUrl,
255
+ error,
256
+ });
257
+ return null;
258
+ }
259
+ throw error;
260
+ };
261
+ const verify = options.verifyPortableObject;
262
+ if (verify == null) {
263
+ return fail(
264
+ new TypeError(
265
+ "Dereferencing the portable object " + lookupUrl + " requires the " +
266
+ "verifyPortableObject option, e.g., verifyPortableObjectProof() " +
267
+ "from @fedify/fedify.",
268
+ ),
269
+ );
270
+ }
271
+ const requestUrls: { url: string; gateway: URL | null }[] = [];
272
+ let expectedId: string;
273
+ try {
274
+ expectedId = canonicalizePortableUri(lookupUrl);
275
+ for (const gateway of gateways) {
276
+ requestUrls.push({ url: toCompatibleEf61Id(url, gateway).href, gateway });
277
+ }
278
+ } catch (error) {
279
+ return fail(error);
280
+ }
281
+ if (requestUrls.length < 1) {
282
+ // No gateway to ask; custom document loaders may still know how to
283
+ // retrieve the portable IRI itself:
284
+ requestUrls.push({ url: lookupUrl, gateway: null });
285
+ }
286
+ const snapshot = createSnapshotContextLoader(options.contextLoader);
287
+ const contextLoader = snapshot.loader;
288
+ const attempts: Attempt[] = [];
289
+ const { signal } = options;
290
+ try {
291
+ for (const { url: requestUrl, gateway } of requestUrls) {
292
+ signal?.throwIfAborted();
293
+ let remoteDocument: RemoteDocument;
294
+ try {
295
+ remoteDocument = await options.documentLoader(
296
+ requestUrl,
297
+ signal == null ? undefined : { signal },
298
+ );
299
+ } catch (error) {
300
+ signal?.throwIfAborted();
301
+ logger.debug("Failed to fetch {url} from {requestUrl}: {error}", {
302
+ url: lookupUrl,
303
+ requestUrl,
304
+ error,
305
+ });
306
+ attempts.push({ type: "error", error });
307
+ continue;
308
+ }
309
+ const { document } = remoteDocument;
310
+ try {
311
+ await checkPortableObjectId(document, expectedId, contextLoader);
312
+ let result: { readonly verified: boolean };
313
+ try {
314
+ result = await verify(document, {
315
+ documentLoader: options.documentLoader,
316
+ contextLoader,
317
+ tracerProvider: options.tracerProvider,
318
+ });
319
+ } catch (error) {
320
+ throw new PortableObjectRejection(
321
+ "the proof verifier failed: {error}",
322
+ { error },
323
+ );
324
+ }
325
+ if (!result.verified) {
326
+ throw new PortableObjectRejection(
327
+ "it does not satisfy the FEP-ef61 proof policy: {result}",
328
+ { result },
329
+ );
330
+ }
331
+ const object = await options.parse(document, {
332
+ contextLoader,
333
+ baseUrl: url,
334
+ });
335
+ signal?.throwIfAborted();
336
+ if (gateway != null) {
337
+ span?.setAttribute("activitypub.gateway", gateway.href);
338
+ }
339
+ return object;
340
+ } catch (error) {
341
+ signal?.throwIfAborted();
342
+ if (error instanceof PortableObjectRejection) {
343
+ logger.warn(
344
+ "Rejected the portable object {url} served from {requestUrl}, " +
345
+ "because " + error.message,
346
+ { ...error.values, url: lookupUrl, requestUrl },
347
+ );
348
+ attempts.push({ type: "rejected", source: requestUrl });
349
+ continue;
350
+ }
351
+ logger.debug("Failed to parse {url} from {requestUrl}: {error}", {
352
+ url: lookupUrl,
353
+ requestUrl,
354
+ error,
355
+ });
356
+ attempts.push({ type: "error", error });
357
+ }
358
+ }
359
+ } finally {
360
+ snapshot.release();
361
+ }
362
+ const rejected = attempts.filter((a) => a.type === "rejected");
363
+ if (rejected.length > 0) {
364
+ const message = "No gateway returned a valid portable object for " +
365
+ lookupUrl + "; refusing to return the object. Objects retrieved " +
366
+ "from: " + rejected.map((a) => a.source).join(", ") + ".";
367
+ if (options.suppressError || options.crossOrigin !== "throw") {
368
+ span?.setStatus({ code: SpanStatusCode.ERROR, message });
369
+ logger.warn(message);
370
+ return null;
371
+ }
372
+ throw new PortableObjectRejectedError(message);
373
+ }
374
+ const errors = attempts.flatMap((a) => a.type === "error" ? [a.error] : []);
375
+ return fail(
376
+ errors.length === 1 ? errors[0] : new AggregateError(
377
+ errors,
378
+ "Failed to dereference " + lookupUrl + " through any of its " +
379
+ "gateways: " + requestUrls.map((r) => r.url).join(", "),
380
+ ),
381
+ );
382
+ }
383
+
384
+ async function checkPortableObjectId(
385
+ document: unknown,
386
+ expectedId: string,
387
+ contextLoader: DocumentLoader,
388
+ ): Promise<void> {
389
+ if (
390
+ document == null || typeof document !== "object" || Array.isArray(document)
391
+ ) {
392
+ throw new PortableObjectRejection("it is not a single JSON object");
393
+ }
394
+ const expanded = await jsonld.expand(document, {
395
+ documentLoader: contextLoader,
396
+ keepFreeFloatingNodes: true,
397
+ });
398
+ if (expanded.length !== 1) {
399
+ throw new PortableObjectRejection(
400
+ "it does not have exactly one top-level node",
401
+ );
402
+ }
403
+ const id = expanded[0]["@id"];
404
+ // A document without @id, such as a proof document, is left to the
405
+ // verifier; verifyPortableObjectProof() rejects it, because there is no
406
+ // portable ID to check its proof against:
407
+ if (id == null) return;
408
+ if (typeof id !== "string") {
409
+ throw new PortableObjectRejection("it has an invalid @id");
410
+ }
411
+ // Compare the raw @id rather than a parsed URL, because URL parsing
412
+ // normalizes opaque path segments (e.g., dot segments):
413
+ let actualId: string | null;
414
+ try {
415
+ actualId = canonicalizePortableUri(id);
416
+ } catch (error) {
417
+ if (!(error instanceof TypeError)) throw error;
418
+ actualId = null;
419
+ }
420
+ if (actualId !== expectedId) {
421
+ throw new PortableObjectRejection(
422
+ "its @id ({objectId}) does not match the requested portable ID",
423
+ { objectId: id },
424
+ );
425
+ }
426
+ }
package/src/mod.ts CHANGED
@@ -45,6 +45,10 @@ export {
45
45
  verifyHashlink,
46
46
  } from "./digest.ts";
47
47
  export { LanguageString } from "./langstr.ts";
48
+ export type {
49
+ PortableObjectVerifier,
50
+ PortableObjectVerifierOptions,
51
+ } from "./portable.ts";
48
52
  export {
49
53
  decodeMultibase,
50
54
  encodeMultibase,
@@ -0,0 +1,52 @@
1
+ import type { TracerProvider } from "@opentelemetry/api";
2
+ import type { DocumentLoader } from "./docloader.ts";
3
+
4
+ /**
5
+ * Options passed to a {@link PortableObjectVerifier}.
6
+ * @since 2.4.0
7
+ */
8
+ export interface PortableObjectVerifierOptions {
9
+ /**
10
+ * The document loader for fetching remote documents, such as verification
11
+ * methods that are not `did:key` URLs.
12
+ */
13
+ readonly documentLoader?: DocumentLoader;
14
+
15
+ /**
16
+ * The document loader for fetching remote JSON-LD contexts. During
17
+ * gateway dereferencing, this loader returns the same context documents
18
+ * that the vocabulary parser sees.
19
+ */
20
+ readonly contextLoader?: DocumentLoader;
21
+
22
+ /**
23
+ * The OpenTelemetry tracer provider.
24
+ */
25
+ readonly tracerProvider?: TracerProvider;
26
+ }
27
+
28
+ /**
29
+ * A function that applies the [FEP-ef61] proof policy to a portable object
30
+ * fetched through a gateway.
31
+ *
32
+ * It receives the fetched JSON-LD document as is, and resolves to an object
33
+ * whose `verified` property tells whether the document carries a valid
34
+ * [FEP-8b32] Object Integrity Proof made by the DID in its portable ID.
35
+ * `verifyPortableObjectProof()` from `@fedify/fedify` satisfies this type, so
36
+ * it can be passed as the `verifyPortableObject` option of generated
37
+ * vocabulary accessors such as `Activity.getObject()`.
38
+ *
39
+ * [FEP-ef61]: https://w3id.org/fep/ef61
40
+ * [FEP-8b32]: https://w3id.org/fep/8b32
41
+ *
42
+ * @param document The fetched JSON-LD document.
43
+ * @param options Loaders and tracing options for verification.
44
+ * @returns An object whose `verified` property is `true` if and only if the
45
+ * document satisfies the proof policy. Other properties, such as
46
+ * a failure reason, are logged when verification fails.
47
+ * @since 2.4.0
48
+ */
49
+ export type PortableObjectVerifier = (
50
+ document: unknown,
51
+ options: PortableObjectVerifierOptions,
52
+ ) => Promise<{ readonly verified: boolean }>;
@@ -2,7 +2,7 @@ import { deepStrictEqual } from "node:assert";
2
2
  import process from "node:process";
3
3
  import { test } from "node:test";
4
4
  import metadata from "../deno.json" with { type: "json" };
5
- import { getUserAgent } from "./request.ts";
5
+ import { createActivityPubRequest, getUserAgent } from "./request.ts";
6
6
 
7
7
  test("getUserAgent()", () => {
8
8
  if ("Deno" in globalThis) {
@@ -91,3 +91,12 @@ test("getUserAgent()", () => {
91
91
  );
92
92
  }
93
93
  });
94
+
95
+ test("createActivityPubRequest() asks for the ActivityStreams profile", () => {
96
+ const request = createActivityPubRequest("https://example.com/object");
97
+ deepStrictEqual(
98
+ request.headers.get("Accept"),
99
+ "application/activity+json, " +
100
+ 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"',
101
+ );
102
+ });
package/src/request.ts CHANGED
@@ -31,6 +31,14 @@ export class FetchError extends Error {
31
31
  }
32
32
  }
33
33
 
34
+ /**
35
+ * The `Accept` header value for fetching ActivityPub objects. ActivityPub
36
+ * and FEP-ef61 gateways require the ActivityStreams profile on the JSON-LD
37
+ * media type.
38
+ */
39
+ const ACTIVITYPUB_ACCEPT =
40
+ 'application/activity+json, application/ld+json; profile="https://www.w3.org/ns/activitystreams"';
41
+
34
42
  /**
35
43
  * Options for creating a request.
36
44
  * @internal
@@ -52,7 +60,7 @@ export function createActivityPubRequest(
52
60
  ): Request {
53
61
  return new Request(url, {
54
62
  headers: {
55
- Accept: "application/activity+json, application/ld+json",
63
+ Accept: ACTIVITYPUB_ACCEPT,
56
64
  "User-Agent": typeof options.userAgent === "string"
57
65
  ? options.userAgent
58
66
  : getUserAgent(options.userAgent),
package/tsdown.config.ts CHANGED
@@ -7,6 +7,7 @@ export default [
7
7
  entry: [
8
8
  "src/mod.ts",
9
9
  "src/internal/jsonld-cache.ts",
10
+ "src/internal/portable-dereference.ts",
10
11
  "src/internal/signed-representation.ts",
11
12
  "src/jsonld.ts",
12
13
  "src/temporal.ts",