@fedify/vocab-runtime 2.4.0-dev.2256 → 2.4.0-dev.2260

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 (58) hide show
  1. package/deno.json +1 -1
  2. package/dist/{docloader-C63enuyr.d.cts → docloader-CYwqh5Df.d.cts} +1 -0
  3. package/dist/{docloader-C63enuyr.d.ts → docloader-CYwqh5Df.d.ts} +1 -0
  4. package/dist/internal/jsonld-cache.cjs +1 -1
  5. package/dist/internal/jsonld-cache.d.cts +1 -1
  6. package/dist/internal/jsonld-cache.d.ts +1 -1
  7. package/dist/internal/jsonld-cache.js +1 -1
  8. package/dist/internal/portable-dereference.cjs +55 -1
  9. package/dist/internal/portable-dereference.d.cts +25 -3
  10. package/dist/internal/portable-dereference.d.ts +25 -3
  11. package/dist/internal/portable-dereference.js +55 -2
  12. package/dist/mod.cjs +2 -2
  13. package/dist/mod.d.cts +55 -6
  14. package/dist/mod.d.ts +55 -6
  15. package/dist/mod.js +2 -2
  16. package/dist/{portable-Cux3IA20.d.cts → portable-DqtfLy_1.d.cts} +1 -1
  17. package/dist/{portable-DBA1FS_L.d.ts → portable-DtWsu2yU.d.ts} +1 -1
  18. package/dist/tests/{body-NALc-7u_.mjs → body-BbkIKvTI.mjs} +1 -1
  19. package/dist/tests/{body-ILOzDJe-.cjs → body-edyRdC9n.cjs} +1 -1
  20. package/dist/tests/body.test.cjs +1 -1
  21. package/dist/tests/body.test.mjs +1 -1
  22. package/dist/tests/decimal.test.cjs +3 -3
  23. package/dist/tests/decimal.test.mjs +3 -3
  24. package/dist/tests/{docloader-PCF1uReU.mjs → docloader-DS598-E-.mjs} +3 -3
  25. package/dist/tests/{docloader-CZ-W5dIM.cjs → docloader-DVikRN3s.cjs} +3 -3
  26. package/dist/tests/docloader.test.cjs +3 -3
  27. package/dist/tests/docloader.test.mjs +3 -3
  28. package/dist/tests/internal/portable-dereference.test.cjs +31 -3
  29. package/dist/tests/internal/portable-dereference.test.mjs +31 -3
  30. package/dist/tests/{jsonld-cache-DCh2kTs6.mjs → jsonld-cache-BPQmOZWD.mjs} +1 -1
  31. package/dist/tests/{jsonld-cache-CMzevnnN.cjs → jsonld-cache-C07AyNOY.cjs} +1 -1
  32. package/dist/tests/jsonld-cache.test.cjs +2 -2
  33. package/dist/tests/jsonld-cache.test.mjs +2 -2
  34. package/dist/tests/portable-dereference-BNXtgg5U.cjs +278 -0
  35. package/dist/tests/portable-dereference-DRE5bz-l.mjs +249 -0
  36. package/dist/tests/{portable-media-HeXeBuyd.cjs → portable-media-BC-QRzPd.cjs} +3 -3
  37. package/dist/tests/{portable-media-SWj7DacP.mjs → portable-media-CP6hh3ja.mjs} +3 -3
  38. package/dist/tests/portable-media.test.cjs +1 -1
  39. package/dist/tests/portable-media.test.mjs +1 -1
  40. package/dist/tests/portable-workers.test.cjs +2 -2
  41. package/dist/tests/portable-workers.test.mjs +2 -2
  42. package/dist/tests/{request-DkiHcgVt.mjs → request-D-SOvXf_.mjs} +1 -1
  43. package/dist/tests/{request-BE3wHjK4.cjs → request-kRkxuOkZ.cjs} +1 -1
  44. package/dist/tests/request.test.cjs +1 -1
  45. package/dist/tests/request.test.mjs +1 -1
  46. package/dist/tests/{url-Ddeuv0sE.cjs → url-BNakuZ8k.cjs} +53 -4
  47. package/dist/tests/{url-ClVBNQup.mjs → url-DMxmp7ZG.mjs} +53 -4
  48. package/dist/tests/url.test.cjs +1 -1
  49. package/dist/tests/url.test.mjs +1 -1
  50. package/dist/{url-DKA6dTWV.cjs → url-DrGTR8yv.cjs} +53 -4
  51. package/dist/{url-Cr9lJ6cD.js → url-Dzyp-NsC.js} +53 -4
  52. package/package.json +1 -1
  53. package/src/docloader.ts +1 -0
  54. package/src/internal/portable-dereference.test.ts +62 -0
  55. package/src/internal/portable-dereference.ts +67 -0
  56. package/src/url.ts +53 -4
  57. package/dist/tests/portable-dereference-BCfSQoXP.cjs +0 -149
  58. package/dist/tests/portable-dereference-EAbEhAJ4.mjs +0 -132
@@ -41,7 +41,14 @@ function assertCompatiblePathCanBeParsed(raw, parsed) {
41
41
  if ((compatiblePath || rawCompatiblePath) && DOT_SEGMENT_PATTERN.test(rawPath)) throw new TypeError("FEP-ef61 compatible identifier paths with dot segments cannot be represented as URLs.");
42
42
  }
43
43
  /**
44
- * Parses a JSON-LD `@id` value as an IRI.
44
+ * Parses a JSON-LD `@id` value as an IRI, including FEP-ef61 portable
45
+ * ActivityPub IRIs. See {@link parseIri} for how IRIs are parsed.
46
+ * @param id The `@id` value.
47
+ * @param base The base IRI to resolve a relative `@id` against.
48
+ * @returns The parsed IRI, or `undefined` if `id` is missing or a blank node
49
+ * identifier.
50
+ * @throws {TypeError} If `id` is not a valid IRI.
51
+ * @since 2.4.0
45
52
  */
46
53
  function parseJsonLdId(id, base) {
47
54
  if (id == null || id.startsWith("_:")) return void 0;
@@ -59,6 +66,16 @@ function parseJsonLdId(id, base) {
59
66
  * This also applies to compatible identifier strings used as relative bases.
60
67
  * A `URL` argument may already have lost such segments before this function
61
68
  * receives it.
69
+ *
70
+ * Portable IRIs, e.g., `ap://did:key:z6Mk.../actor`, cannot be represented by
71
+ * JavaScript `URL` as they are, so the returned `URL` keeps the DID authority
72
+ * percent-encoded, e.g., `ap+ef61://did%3Akey%3Az6Mk.../actor`. Use
73
+ * {@link formatIri} to get the canonical string back.
74
+ * @param iri The IRI to parse.
75
+ * @param base The base IRI to resolve a relative IRI against.
76
+ * @returns The parsed IRI.
77
+ * @throws {TypeError} If the IRI is malformed.
78
+ * @since 2.4.0
62
79
  */
63
80
  function parseIri(iri, base) {
64
81
  if (iri instanceof URL) return normalizePortableUrl(iri) ?? new URL(iri.href);
@@ -83,6 +100,12 @@ function parseIri(iri, base) {
83
100
  * the scheme this function produces. Do not compare its results as strings to
84
101
  * tell whether two portable IRIs identify the same object; use
85
102
  * `arePortableUrisEqual()` instead.
103
+ * @param iri The IRI to format.
104
+ * @returns The formatted IRI. A string that cannot be parsed as a URL at all
105
+ * is returned unchanged.
106
+ * @throws {TypeError} If the IRI is a malformed portable IRI or FEP-ef61
107
+ * compatible identifier, e.g., one with dot segments.
108
+ * @since 2.4.0
86
109
  */
87
110
  function formatIri(iri) {
88
111
  if (typeof iri === "string") assertPortablePathCanBeParsed(iri);
@@ -185,7 +208,20 @@ function haveSameFe34Origin(left, right) {
185
208
  }
186
209
  }
187
210
  /**
188
- * Checks whether two IRIs have the same origin.
211
+ * Checks whether two IRIs have the same origin. Unlike comparing
212
+ * `URL.origin`, which is `"null"` for URLs with non-special schemes, this
213
+ * compares FEP-ef61 portable ActivityPub IRIs by their schemes and DIDs, so
214
+ * that `ap:` and `ap+ef61:` IRIs of the same DID with decoded or
215
+ * percent-encoded authorities have the same origin. Other IRIs with
216
+ * a non-special scheme and a host are compared by their schemes and hosts.
217
+ *
218
+ * This is not an FEP-fe34 origin check: an `ap+ef61:` IRI and the `did:key`
219
+ * verification method of the same DID have different origins here. Use
220
+ * {@link haveSameFe34Origin} for that.
221
+ * @param left The first IRI.
222
+ * @param right The second IRI.
223
+ * @returns `true` if the IRIs have the same origin.
224
+ * @since 2.4.0
189
225
  */
190
226
  function haveSameIriOrigin(left, right) {
191
227
  return getComparableIriOrigin(left) === getComparableIriOrigin(right);
@@ -273,14 +309,27 @@ function parseAtUri(uri) {
273
309
  return new URL("at://" + encodeURIComponent(authority) + path);
274
310
  }
275
311
  /**
276
- * Checks whether the URL is an FEP-ef61 gateway base URI.
312
+ * Checks whether the URL is an FEP-ef61 gateway base URI, i.e., an HTTP(S)
313
+ * URI with no credentials, path, query, or fragment, such as
314
+ * `https://example.com/`. FEP-ef61 requires every item of a portable actor's
315
+ * `gateways` to be such a URI. A URI with an empty query or fragment
316
+ * delimiter, such as `https://example.com/?`, is not a gateway base URI.
317
+ *
318
+ * Note that the `URL` class normalizes `https://example.com` to
319
+ * `https://example.com/`, so both are gateway base URIs.
320
+ * @param url The URL to check.
321
+ * @returns `true` if the URL is an FEP-ef61 gateway base URI.
277
322
  * @since 2.4.0
278
323
  */
279
324
  function isGatewayUrl(url) {
280
325
  return (url.protocol === "http:" || url.protocol === "https:") && url.href === `${url.origin}/`;
281
326
  }
282
327
  /**
283
- * Parses and validates an FEP-ef61 gateway base URI.
328
+ * Parses and validates an FEP-ef61 gateway base URI, i.e., an HTTP(S) URI with
329
+ * no credentials, path, query, or fragment, such as `https://example.com`.
330
+ * See {@link isGatewayUrl} for the rules.
331
+ * @param url The gateway base URI to parse.
332
+ * @returns The parsed gateway base URI, whose path is `/`.
284
333
  * @throws {TypeError} If the URI is malformed, or is not an HTTP(S) base URI
285
334
  * with no credentials, path, query, or fragment. In the
286
335
  * latter case, the message starts with
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fedify/vocab-runtime",
3
- "version": "2.4.0-dev.2256+683c50ba",
3
+ "version": "2.4.0-dev.2260+42356e7d",
4
4
  "homepage": "https://fedify.dev/",
5
5
  "repository": {
6
6
  "type": "git",
package/src/docloader.ts CHANGED
@@ -62,6 +62,7 @@ export interface DocumentLoaderOptions {
62
62
  * Whether to lower error-level logs for recoverable document loading
63
63
  * failures to warning-level logs. The loader still throws the error.
64
64
  * @default `false`
65
+ * @since 2.4.0
65
66
  */
66
67
  suppressError?: boolean;
67
68
  }
@@ -6,7 +6,9 @@ import { createScopedContextLoader } from "./jsonld-cache.ts";
6
6
  import {
7
7
  createSnapshotContextLoader,
8
8
  getPortableGatewayCandidates,
9
+ getReferrerGateways,
9
10
  isPortableIri,
11
+ recordPortableReferrer,
10
12
  } from "./portable-dereference.ts";
11
13
 
12
14
  const hrefs = (urls: URL[]) => urls.map((url) => url.href);
@@ -91,6 +93,66 @@ test("getPortableGatewayCandidates() reads hints from withGatewayHints()", () =>
91
93
  );
92
94
  });
93
95
 
96
+ test("getReferrerGateways() takes gateways from the owner", () => {
97
+ const actor = {
98
+ id: parseIri("ap://did:key:z6Mkabc/actor"),
99
+ gateways: [
100
+ new URL("https://a.example"),
101
+ new URL("https://a.example/"),
102
+ new URL("https://b.example/path"),
103
+ new URL("https://c.example"),
104
+ ],
105
+ };
106
+ const outbox = { id: parseIri("ap://did:key:z6Mkabc/actor/outbox") };
107
+ recordPortableReferrer(actor, outbox, "outbox");
108
+ const page = parseIri("ap://did:key:z6Mkabc/actor/outbox?cursor=1");
109
+
110
+ // From the actor itself, and from an object obtained from it; duplicate and
111
+ // invalid gateways are dropped:
112
+ deepStrictEqual(
113
+ hrefs(getReferrerGateways(actor, outbox.id) ?? []),
114
+ ["https://a.example/", "https://c.example/"],
115
+ );
116
+ deepStrictEqual(
117
+ hrefs(getReferrerGateways(outbox, page) ?? []),
118
+ ["https://a.example/", "https://c.example/"],
119
+ );
120
+
121
+ // Location hints take precedence:
122
+ deepStrictEqual(
123
+ getReferrerGateways(
124
+ outbox,
125
+ withGatewayHints(page, [new URL("https://hint.example")]),
126
+ ),
127
+ undefined,
128
+ );
129
+
130
+ // A reference under another DID does not belong to the actor:
131
+ deepStrictEqual(
132
+ getReferrerGateways(outbox, parseIri("ap://did:key:z6Mkdef/notes/1")),
133
+ undefined,
134
+ );
135
+
136
+ // An actor with a compatible identifier works the same way:
137
+ const compatible = {
138
+ id: new URL(
139
+ "https://a.example/.well-known/apgateway/did:key:z6Mkabc/actor",
140
+ ),
141
+ gateways: [new URL("https://a.example")],
142
+ };
143
+ deepStrictEqual(
144
+ hrefs(getReferrerGateways(compatible, outbox.id) ?? []),
145
+ ["https://a.example/"],
146
+ );
147
+
148
+ // Objects that no portable actor leads to have no gateways to offer:
149
+ deepStrictEqual(getReferrerGateways({}, page), undefined);
150
+ deepStrictEqual(
151
+ getReferrerGateways({ id: actor.id, gateways: [] }, page),
152
+ undefined,
153
+ );
154
+ });
155
+
94
156
  test("createSnapshotContextLoader() does not nest released snapshots", async () => {
95
157
  const calls: string[] = [];
96
158
  const base: DocumentLoader = (url) => {
@@ -538,6 +538,73 @@ export function getPortableGatewayCandidates(
538
538
  return candidates;
539
539
  }
540
540
 
541
+ /**
542
+ * Gets the gateways through which a portable reference without location hints
543
+ * can be dereferenced, from the portable actor that the reference belongs to.
544
+ *
545
+ * The references in a portable actor's own document, such as its `outbox`,
546
+ * usually have no `@gateway` hints, as they are not needed there, since
547
+ * the actor's `gateways` already tells where to retrieve them. So this walks
548
+ * from the object whose property is being dereferenced up through the objects
549
+ * it was obtained from, e.g., from a collection page to the collection and
550
+ * then to the actor, and returns the `gateways` of the first object that has
551
+ * any, but only if that object has the same DID as the reference. The
552
+ * gateways only tell where to look; whatever they serve is still verified.
553
+ *
554
+ * @param object The object whose property is being dereferenced.
555
+ * @param url The portable IRI to dereference.
556
+ * @returns Up to {@link MAX_GATEWAY_HINTS} valid gateways, or `undefined` if
557
+ * the IRI has valid `@gateway` hints or no such object is found.
558
+ * @internal Technically exported for generated vocabulary classes, but not
559
+ * part of the public API contract. This is not considered public API for
560
+ * Semantic Versioning decisions.
561
+ */
562
+ export function getReferrerGateways(
563
+ object: object,
564
+ url: URL,
565
+ ): URL[] | undefined {
566
+ if (getPortableGatewayCandidates(url).length > 0) return undefined;
567
+ const visited = new Set<object>();
568
+ let current: object | undefined = object;
569
+ while (current != null && !visited.has(current)) {
570
+ visited.add(current);
571
+ const gateways: unknown = "gateways" in current
572
+ ? current.gateways
573
+ : undefined;
574
+ if (Array.isArray(gateways) && gateways.length > 0) {
575
+ const id = getObjectId(current);
576
+ const portableId = id == null
577
+ ? null
578
+ : isPortableIri(id)
579
+ ? id
580
+ : getCompatibleEf61Target(id);
581
+ if (portableId == null || !haveSameFe34Origin(portableId, url)) {
582
+ return undefined;
583
+ }
584
+ const candidates: URL[] = [];
585
+ for (const gateway of gateways) {
586
+ if (candidates.length >= MAX_GATEWAY_HINTS) break;
587
+ if (typeof gateway !== "string" && !(gateway instanceof URL)) continue;
588
+ const parsed = parseGatewayOrigin(gateway);
589
+ if (parsed == null) continue;
590
+ if (candidates.some((c) => c.href === parsed.href)) continue;
591
+ candidates.push(parsed);
592
+ }
593
+ return candidates.length > 0 ? candidates : undefined;
594
+ }
595
+ current = provenances.get(current)?.referrer?.object;
596
+ }
597
+ return undefined;
598
+ }
599
+
600
+ function getCompatibleEf61Target(id: URL): URL | null {
601
+ try {
602
+ return fromCompatibleEf61Id(id);
603
+ } catch {
604
+ return null;
605
+ }
606
+ }
607
+
541
608
  /**
542
609
  * Creates a context loader that returns the same context documents for the
543
610
  * whole dereference operation, so that the identity check, the proof
package/src/url.ts CHANGED
@@ -81,7 +81,14 @@ function assertCompatiblePathCanBeParsed(raw: string, parsed: URL): void {
81
81
  }
82
82
 
83
83
  /**
84
- * Parses a JSON-LD `@id` value as an IRI.
84
+ * Parses a JSON-LD `@id` value as an IRI, including FEP-ef61 portable
85
+ * ActivityPub IRIs. See {@link parseIri} for how IRIs are parsed.
86
+ * @param id The `@id` value.
87
+ * @param base The base IRI to resolve a relative `@id` against.
88
+ * @returns The parsed IRI, or `undefined` if `id` is missing or a blank node
89
+ * identifier.
90
+ * @throws {TypeError} If `id` is not a valid IRI.
91
+ * @since 2.4.0
85
92
  */
86
93
  export function parseJsonLdId(
87
94
  id: string | undefined,
@@ -103,6 +110,16 @@ export function parseJsonLdId(
103
110
  * This also applies to compatible identifier strings used as relative bases.
104
111
  * A `URL` argument may already have lost such segments before this function
105
112
  * receives it.
113
+ *
114
+ * Portable IRIs, e.g., `ap://did:key:z6Mk.../actor`, cannot be represented by
115
+ * JavaScript `URL` as they are, so the returned `URL` keeps the DID authority
116
+ * percent-encoded, e.g., `ap+ef61://did%3Akey%3Az6Mk.../actor`. Use
117
+ * {@link formatIri} to get the canonical string back.
118
+ * @param iri The IRI to parse.
119
+ * @param base The base IRI to resolve a relative IRI against.
120
+ * @returns The parsed IRI.
121
+ * @throws {TypeError} If the IRI is malformed.
122
+ * @since 2.4.0
106
123
  */
107
124
  export function parseIri(iri: string | URL, base?: string | URL): URL {
108
125
  if (iri instanceof URL) {
@@ -132,6 +149,12 @@ export function parseIri(iri: string | URL, base?: string | URL): URL {
132
149
  * the scheme this function produces. Do not compare its results as strings to
133
150
  * tell whether two portable IRIs identify the same object; use
134
151
  * `arePortableUrisEqual()` instead.
152
+ * @param iri The IRI to format.
153
+ * @returns The formatted IRI. A string that cannot be parsed as a URL at all
154
+ * is returned unchanged.
155
+ * @throws {TypeError} If the IRI is a malformed portable IRI or FEP-ef61
156
+ * compatible identifier, e.g., one with dot segments.
157
+ * @since 2.4.0
135
158
  */
136
159
  export function formatIri(iri: string | URL): string {
137
160
  if (typeof iri === "string") assertPortablePathCanBeParsed(iri);
@@ -271,7 +294,20 @@ export function haveSameFe34Origin(
271
294
  }
272
295
 
273
296
  /**
274
- * Checks whether two IRIs have the same origin.
297
+ * Checks whether two IRIs have the same origin. Unlike comparing
298
+ * `URL.origin`, which is `"null"` for URLs with non-special schemes, this
299
+ * compares FEP-ef61 portable ActivityPub IRIs by their schemes and DIDs, so
300
+ * that `ap:` and `ap+ef61:` IRIs of the same DID with decoded or
301
+ * percent-encoded authorities have the same origin. Other IRIs with
302
+ * a non-special scheme and a host are compared by their schemes and hosts.
303
+ *
304
+ * This is not an FEP-fe34 origin check: an `ap+ef61:` IRI and the `did:key`
305
+ * verification method of the same DID have different origins here. Use
306
+ * {@link haveSameFe34Origin} for that.
307
+ * @param left The first IRI.
308
+ * @param right The second IRI.
309
+ * @returns `true` if the IRIs have the same origin.
310
+ * @since 2.4.0
275
311
  */
276
312
  export function haveSameIriOrigin(left: URL, right: URL): boolean {
277
313
  return getComparableIriOrigin(left) === getComparableIriOrigin(right);
@@ -417,7 +453,16 @@ function parseAtUri(uri: string): URL {
417
453
  }
418
454
 
419
455
  /**
420
- * Checks whether the URL is an FEP-ef61 gateway base URI.
456
+ * Checks whether the URL is an FEP-ef61 gateway base URI, i.e., an HTTP(S)
457
+ * URI with no credentials, path, query, or fragment, such as
458
+ * `https://example.com/`. FEP-ef61 requires every item of a portable actor's
459
+ * `gateways` to be such a URI. A URI with an empty query or fragment
460
+ * delimiter, such as `https://example.com/?`, is not a gateway base URI.
461
+ *
462
+ * Note that the `URL` class normalizes `https://example.com` to
463
+ * `https://example.com/`, so both are gateway base URIs.
464
+ * @param url The URL to check.
465
+ * @returns `true` if the URL is an FEP-ef61 gateway base URI.
421
466
  * @since 2.4.0
422
467
  */
423
468
  export function isGatewayUrl(url: URL): boolean {
@@ -426,7 +471,11 @@ export function isGatewayUrl(url: URL): boolean {
426
471
  }
427
472
 
428
473
  /**
429
- * Parses and validates an FEP-ef61 gateway base URI.
474
+ * Parses and validates an FEP-ef61 gateway base URI, i.e., an HTTP(S) URI with
475
+ * no credentials, path, query, or fragment, such as `https://example.com`.
476
+ * See {@link isGatewayUrl} for the rules.
477
+ * @param url The gateway base URI to parse.
478
+ * @returns The parsed gateway base URI, whose path is `/`.
430
479
  * @throws {TypeError} If the URI is malformed, or is not an HTTP(S) base URI
431
480
  * with no credentials, path, query, or fragment. In the
432
481
  * latter case, the message starts with
@@ -1,149 +0,0 @@
1
- const require_contexts = require("./contexts-DizzBjz4.cjs");
2
- const require_url = require("./url-Ddeuv0sE.cjs");
3
- const require_jsonld_cache = require("./jsonld-cache-CMzevnnN.cjs");
4
- let _logtape_logtape = require("@logtape/logtape");
5
- require("@opentelemetry/api");
6
- //#region src/internal/portable-dereference.ts
7
- const logger = (0, _logtape_logtape.getLogger)([
8
- "fedify",
9
- "vocab",
10
- "gateway"
11
- ]);
12
- /**
13
- * The maximum number of `@gateway` location hints to try for one reference.
14
- * Hints come from possibly untrusted documents, so they are bounded to keep
15
- * a single accessor call from fanning out to many servers.
16
- */
17
- const MAX_GATEWAY_HINTS = 5;
18
- const BASELINE_CONTEXT_URLS = /* @__PURE__ */ new Set([
19
- "https://w3id.org/identity/v1",
20
- "https://www.w3.org/ns/activitystreams",
21
- "https://w3id.org/security/v1",
22
- "https://w3id.org/security/data-integrity/v1"
23
- ]);
24
- /**
25
- * Checks whether a URL is an FEP-ef61 portable ActivityPub IRI.
26
- *
27
- * @internal Technically exported for generated vocabulary classes, but not
28
- * part of the public API contract. This is not considered public API for
29
- * Semantic Versioning decisions.
30
- */
31
- function isPortableIri(url) {
32
- return url.protocol === "ap+ef61:" || url.protocol === "ap:";
33
- }
34
- /**
35
- * Picks the ordered list of FEP-ef61 gateways to fetch a portable IRI from.
36
- *
37
- * If `gateways` is given, it is used as is (even when empty), and `@gateway`
38
- * location hints in the IRI are ignored. Otherwise, up to
39
- * {@link MAX_GATEWAY_HINTS} valid `@gateway` hints are used. Duplicate
40
- * gateways are dropped in both cases.
41
- *
42
- * @throws {TypeError} If an explicit gateway is not an HTTP(S) origin.
43
- * @internal
44
- */
45
- function getPortableGatewayCandidates(url, gateways) {
46
- const candidates = [];
47
- const seen = /* @__PURE__ */ new Set();
48
- const add = (gateway) => {
49
- if (seen.has(gateway.href)) return;
50
- seen.add(gateway.href);
51
- candidates.push(gateway);
52
- };
53
- if (gateways != null) {
54
- for (const gateway of gateways) {
55
- const parsed = require_url.parseGatewayOrigin(gateway);
56
- if (parsed == null) throw new TypeError("FEP-ef61 gateways must be HTTP(S) origins with no credentials, path, query, or fragment: " + String(gateway));
57
- add(parsed);
58
- }
59
- return candidates;
60
- }
61
- for (const hint of new URLSearchParams(url.search).getAll(require_url.GATEWAY_HINT_PARAMETER)) {
62
- if (candidates.length >= MAX_GATEWAY_HINTS) break;
63
- const parsed = require_url.parseGatewayOrigin(hint);
64
- if (parsed == null) {
65
- logger.debug("Ignoring an invalid FEP-ef61 gateway hint {hint} in {url}.", {
66
- hint,
67
- url: require_url.formatIri(url)
68
- });
69
- continue;
70
- }
71
- add(parsed);
72
- }
73
- return candidates;
74
- }
75
- /**
76
- * Creates a context loader that returns the same context documents for the
77
- * whole dereference operation, so that the identity check, the proof
78
- * verifier, and the parser interpret the fetched document identically even
79
- * if the underlying loader is nondeterministic. Failed loads are not
80
- * remembered, so a transient failure does not affect the next gateway.
81
- *
82
- * The parsed object keeps the loader for its own later dereferences, so
83
- * `release()` turns it into a plain pass-through to the underlying loader
84
- * once the operation is over.
85
- *
86
- * @internal Technically exported for generated vocabulary classes, but not
87
- * part of the public API contract. This is not considered public API for
88
- * Semantic Versioning decisions.
89
- */
90
- function createSnapshotContextLoader(contextLoader, suppressError) {
91
- contextLoader = require_jsonld_cache.unwrapReleasedDocumentLoader(contextLoader);
92
- const cache = /* @__PURE__ */ new Map();
93
- let released = false;
94
- const release = () => {
95
- released = true;
96
- state.released = true;
97
- cache.clear();
98
- };
99
- const loader = async (url, options) => {
100
- if (released) return await contextLoader(url, options);
101
- const key = URL.canParse(url) ? new URL(url).href : url;
102
- if (BASELINE_CONTEXT_URLS.has(key)) return {
103
- contextUrl: null,
104
- document: structuredClone(require_contexts.preloadedContexts[key]),
105
- documentUrl: key
106
- };
107
- let promise = cache.get(key);
108
- if (promise == null) {
109
- const loading = contextLoader(url, suppressError ? {
110
- ...options,
111
- suppressError: true
112
- } : options).then((document) => structuredClone(document));
113
- promise = loading;
114
- cache.set(key, loading);
115
- loading.catch(() => {
116
- if (cache.get(key) === loading) cache.delete(key);
117
- });
118
- }
119
- return structuredClone(await promise);
120
- };
121
- const state = {
122
- base: contextLoader,
123
- released: false
124
- };
125
- require_jsonld_cache.registerDocumentLoaderWrapper(loader, state);
126
- return {
127
- loader,
128
- release
129
- };
130
- }
131
- //#endregion
132
- Object.defineProperty(exports, "createSnapshotContextLoader", {
133
- enumerable: true,
134
- get: function() {
135
- return createSnapshotContextLoader;
136
- }
137
- });
138
- Object.defineProperty(exports, "getPortableGatewayCandidates", {
139
- enumerable: true,
140
- get: function() {
141
- return getPortableGatewayCandidates;
142
- }
143
- });
144
- Object.defineProperty(exports, "isPortableIri", {
145
- enumerable: true,
146
- get: function() {
147
- return isPortableIri;
148
- }
149
- });
@@ -1,132 +0,0 @@
1
- import { t as preloadedContexts } from "./contexts-CIKsin4e.mjs";
2
- import { h as parseGatewayOrigin, o as formatIri, t as GATEWAY_HINT_PARAMETER } from "./url-ClVBNQup.mjs";
3
- import { c as unwrapReleasedDocumentLoader, s as registerDocumentLoaderWrapper } from "./jsonld-cache-DCh2kTs6.mjs";
4
- import { getLogger } from "@logtape/logtape";
5
- import "@opentelemetry/api";
6
- //#region src/internal/portable-dereference.ts
7
- const logger = getLogger([
8
- "fedify",
9
- "vocab",
10
- "gateway"
11
- ]);
12
- /**
13
- * The maximum number of `@gateway` location hints to try for one reference.
14
- * Hints come from possibly untrusted documents, so they are bounded to keep
15
- * a single accessor call from fanning out to many servers.
16
- */
17
- const MAX_GATEWAY_HINTS = 5;
18
- const BASELINE_CONTEXT_URLS = /* @__PURE__ */ new Set([
19
- "https://w3id.org/identity/v1",
20
- "https://www.w3.org/ns/activitystreams",
21
- "https://w3id.org/security/v1",
22
- "https://w3id.org/security/data-integrity/v1"
23
- ]);
24
- /**
25
- * Checks whether a URL is an FEP-ef61 portable ActivityPub IRI.
26
- *
27
- * @internal Technically exported for generated vocabulary classes, but not
28
- * part of the public API contract. This is not considered public API for
29
- * Semantic Versioning decisions.
30
- */
31
- function isPortableIri(url) {
32
- return url.protocol === "ap+ef61:" || url.protocol === "ap:";
33
- }
34
- /**
35
- * Picks the ordered list of FEP-ef61 gateways to fetch a portable IRI from.
36
- *
37
- * If `gateways` is given, it is used as is (even when empty), and `@gateway`
38
- * location hints in the IRI are ignored. Otherwise, up to
39
- * {@link MAX_GATEWAY_HINTS} valid `@gateway` hints are used. Duplicate
40
- * gateways are dropped in both cases.
41
- *
42
- * @throws {TypeError} If an explicit gateway is not an HTTP(S) origin.
43
- * @internal
44
- */
45
- function getPortableGatewayCandidates(url, gateways) {
46
- const candidates = [];
47
- const seen = /* @__PURE__ */ new Set();
48
- const add = (gateway) => {
49
- if (seen.has(gateway.href)) return;
50
- seen.add(gateway.href);
51
- candidates.push(gateway);
52
- };
53
- if (gateways != null) {
54
- for (const gateway of gateways) {
55
- const parsed = parseGatewayOrigin(gateway);
56
- if (parsed == null) throw new TypeError("FEP-ef61 gateways must be HTTP(S) origins with no credentials, path, query, or fragment: " + String(gateway));
57
- add(parsed);
58
- }
59
- return candidates;
60
- }
61
- for (const hint of new URLSearchParams(url.search).getAll(GATEWAY_HINT_PARAMETER)) {
62
- if (candidates.length >= MAX_GATEWAY_HINTS) break;
63
- const parsed = parseGatewayOrigin(hint);
64
- if (parsed == null) {
65
- logger.debug("Ignoring an invalid FEP-ef61 gateway hint {hint} in {url}.", {
66
- hint,
67
- url: formatIri(url)
68
- });
69
- continue;
70
- }
71
- add(parsed);
72
- }
73
- return candidates;
74
- }
75
- /**
76
- * Creates a context loader that returns the same context documents for the
77
- * whole dereference operation, so that the identity check, the proof
78
- * verifier, and the parser interpret the fetched document identically even
79
- * if the underlying loader is nondeterministic. Failed loads are not
80
- * remembered, so a transient failure does not affect the next gateway.
81
- *
82
- * The parsed object keeps the loader for its own later dereferences, so
83
- * `release()` turns it into a plain pass-through to the underlying loader
84
- * once the operation is over.
85
- *
86
- * @internal Technically exported for generated vocabulary classes, but not
87
- * part of the public API contract. This is not considered public API for
88
- * Semantic Versioning decisions.
89
- */
90
- function createSnapshotContextLoader(contextLoader, suppressError) {
91
- contextLoader = unwrapReleasedDocumentLoader(contextLoader);
92
- const cache = /* @__PURE__ */ new Map();
93
- let released = false;
94
- const release = () => {
95
- released = true;
96
- state.released = true;
97
- cache.clear();
98
- };
99
- const loader = async (url, options) => {
100
- if (released) return await contextLoader(url, options);
101
- const key = URL.canParse(url) ? new URL(url).href : url;
102
- if (BASELINE_CONTEXT_URLS.has(key)) return {
103
- contextUrl: null,
104
- document: structuredClone(preloadedContexts[key]),
105
- documentUrl: key
106
- };
107
- let promise = cache.get(key);
108
- if (promise == null) {
109
- const loading = contextLoader(url, suppressError ? {
110
- ...options,
111
- suppressError: true
112
- } : options).then((document) => structuredClone(document));
113
- promise = loading;
114
- cache.set(key, loading);
115
- loading.catch(() => {
116
- if (cache.get(key) === loading) cache.delete(key);
117
- });
118
- }
119
- return structuredClone(await promise);
120
- };
121
- const state = {
122
- base: contextLoader,
123
- released: false
124
- };
125
- registerDocumentLoaderWrapper(loader, state);
126
- return {
127
- loader,
128
- release
129
- };
130
- }
131
- //#endregion
132
- export { getPortableGatewayCandidates as n, isPortableIri as r, createSnapshotContextLoader as t };