@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
@@ -0,0 +1,249 @@
1
+ import { t as preloadedContexts } from "./contexts-CIKsin4e.mjs";
2
+ import { h as parseGatewayOrigin, o as formatIri, s as fromCompatibleEf61Id, t as GATEWAY_HINT_PARAMETER, u as haveSameFe34Origin } from "./url-DMxmp7ZG.mjs";
3
+ import { c as unwrapReleasedDocumentLoader, s as registerDocumentLoaderWrapper } from "./jsonld-cache-BPQmOZWD.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
+ const provenances = /* @__PURE__ */ new WeakMap();
25
+ function getObjectId(object) {
26
+ if (object == null || typeof object !== "object" || !("id" in object)) return null;
27
+ const id = object.id;
28
+ return id instanceof URL ? id : null;
29
+ }
30
+ /**
31
+ * Checks whether a vocabulary object is part of a chain of portable objects,
32
+ * i.e., whether it has a portable ID, has an FEP-ef61 compatible identifier
33
+ * as its ID (even a malformed one, so that it fails closed), or was obtained
34
+ * from a portable object. Accessors of such objects dereference references
35
+ * as portable objects.
36
+ *
37
+ * Being part of a chain does not mean that the object itself is verified:
38
+ * an object parsed from arbitrary JSON-LD is in a chain just because of its
39
+ * ID. Only {@link dereferencePortableIri} records that an object was
40
+ * accepted by a verifier.
41
+ *
42
+ * @internal Technically exported for generated vocabulary classes, but not
43
+ * part of the public API contract. This is not considered public API for
44
+ * Semantic Versioning decisions.
45
+ */
46
+ function isInPortableChain(object) {
47
+ if (provenances.has(object)) return true;
48
+ const id = getObjectId(object);
49
+ return id != null && (isPortableIri(id) || isCompatibleEf61Iri(id));
50
+ }
51
+ /**
52
+ * Checks whether a URL is an FEP-ef61 compatible identifier, i.e., an HTTP(S)
53
+ * URL under a gateway's `/.well-known/apgateway/` path that stands for
54
+ * a portable object. Malformed compatible identifiers count as compatible
55
+ * identifiers too.
56
+ *
57
+ * @internal Technically exported for generated vocabulary classes, but not
58
+ * part of the public API contract. This is not considered public API for
59
+ * Semantic Versioning decisions.
60
+ */
61
+ function isCompatibleEf61Iri(url) {
62
+ try {
63
+ return fromCompatibleEf61Id(url) != null;
64
+ } catch (error) {
65
+ if (error instanceof TypeError) return true;
66
+ throw error;
67
+ }
68
+ }
69
+ /**
70
+ * Records that a vocabulary object returned by a property accessor was
71
+ * obtained through the given property of a portable object, so that later
72
+ * dereferences from it can tell where it came from. Nothing is recorded if
73
+ * the parent is not in a portable chain, or if the child already has its
74
+ * provenance.
75
+ *
76
+ * @internal Technically exported for generated vocabulary classes, but not
77
+ * part of the public API contract. This is not considered public API for
78
+ * Semantic Versioning decisions.
79
+ */
80
+ function recordPortableReferrer(parent, child, property) {
81
+ if (child == null || typeof child !== "object") return;
82
+ if (provenances.has(child) || !isInPortableChain(parent)) return;
83
+ provenances.set(child, { referrer: {
84
+ object: parent,
85
+ property
86
+ } });
87
+ }
88
+ /**
89
+ * Checks whether a URL is an FEP-ef61 portable ActivityPub IRI.
90
+ *
91
+ * @internal Technically exported for generated vocabulary classes, but not
92
+ * part of the public API contract. This is not considered public API for
93
+ * Semantic Versioning decisions.
94
+ */
95
+ function isPortableIri(url) {
96
+ return url.protocol === "ap+ef61:" || url.protocol === "ap:";
97
+ }
98
+ /**
99
+ * Picks the ordered list of FEP-ef61 gateways to fetch a portable IRI from.
100
+ *
101
+ * If `gateways` is given, it is used as is (even when empty), and `@gateway`
102
+ * location hints in the IRI are ignored. Otherwise, up to
103
+ * {@link MAX_GATEWAY_HINTS} valid `@gateway` hints are used. Duplicate
104
+ * gateways are dropped in both cases.
105
+ *
106
+ * @throws {TypeError} If an explicit gateway is not an HTTP(S) origin.
107
+ * @internal
108
+ */
109
+ function getPortableGatewayCandidates(url, gateways) {
110
+ const candidates = [];
111
+ const seen = /* @__PURE__ */ new Set();
112
+ const add = (gateway) => {
113
+ if (seen.has(gateway.href)) return;
114
+ seen.add(gateway.href);
115
+ candidates.push(gateway);
116
+ };
117
+ if (gateways != null) {
118
+ for (const gateway of gateways) {
119
+ const parsed = parseGatewayOrigin(gateway);
120
+ if (parsed == null) throw new TypeError("FEP-ef61 gateways must be HTTP(S) origins with no credentials, path, query, or fragment: " + String(gateway));
121
+ add(parsed);
122
+ }
123
+ return candidates;
124
+ }
125
+ for (const hint of new URLSearchParams(url.search).getAll(GATEWAY_HINT_PARAMETER)) {
126
+ if (candidates.length >= MAX_GATEWAY_HINTS) break;
127
+ const parsed = parseGatewayOrigin(hint);
128
+ if (parsed == null) {
129
+ logger.debug("Ignoring an invalid FEP-ef61 gateway hint {hint} in {url}.", {
130
+ hint,
131
+ url: formatIri(url)
132
+ });
133
+ continue;
134
+ }
135
+ add(parsed);
136
+ }
137
+ return candidates;
138
+ }
139
+ /**
140
+ * Gets the gateways through which a portable reference without location hints
141
+ * can be dereferenced, from the portable actor that the reference belongs to.
142
+ *
143
+ * The references in a portable actor's own document, such as its `outbox`,
144
+ * usually have no `@gateway` hints, as they are not needed there, since
145
+ * the actor's `gateways` already tells where to retrieve them. So this walks
146
+ * from the object whose property is being dereferenced up through the objects
147
+ * it was obtained from, e.g., from a collection page to the collection and
148
+ * then to the actor, and returns the `gateways` of the first object that has
149
+ * any, but only if that object has the same DID as the reference. The
150
+ * gateways only tell where to look; whatever they serve is still verified.
151
+ *
152
+ * @param object The object whose property is being dereferenced.
153
+ * @param url The portable IRI to dereference.
154
+ * @returns Up to {@link MAX_GATEWAY_HINTS} valid gateways, or `undefined` if
155
+ * the IRI has valid `@gateway` hints or no such object is found.
156
+ * @internal Technically exported for generated vocabulary classes, but not
157
+ * part of the public API contract. This is not considered public API for
158
+ * Semantic Versioning decisions.
159
+ */
160
+ function getReferrerGateways(object, url) {
161
+ if (getPortableGatewayCandidates(url).length > 0) return void 0;
162
+ const visited = /* @__PURE__ */ new Set();
163
+ let current = object;
164
+ while (current != null && !visited.has(current)) {
165
+ visited.add(current);
166
+ const gateways = "gateways" in current ? current.gateways : void 0;
167
+ if (Array.isArray(gateways) && gateways.length > 0) {
168
+ const id = getObjectId(current);
169
+ const portableId = id == null ? null : isPortableIri(id) ? id : getCompatibleEf61Target(id);
170
+ if (portableId == null || !haveSameFe34Origin(portableId, url)) return;
171
+ const candidates = [];
172
+ for (const gateway of gateways) {
173
+ if (candidates.length >= MAX_GATEWAY_HINTS) break;
174
+ if (typeof gateway !== "string" && !(gateway instanceof URL)) continue;
175
+ const parsed = parseGatewayOrigin(gateway);
176
+ if (parsed == null) continue;
177
+ if (candidates.some((c) => c.href === parsed.href)) continue;
178
+ candidates.push(parsed);
179
+ }
180
+ return candidates.length > 0 ? candidates : void 0;
181
+ }
182
+ current = provenances.get(current)?.referrer?.object;
183
+ }
184
+ }
185
+ function getCompatibleEf61Target(id) {
186
+ try {
187
+ return fromCompatibleEf61Id(id);
188
+ } catch {
189
+ return null;
190
+ }
191
+ }
192
+ /**
193
+ * Creates a context loader that returns the same context documents for the
194
+ * whole dereference operation, so that the identity check, the proof
195
+ * verifier, and the parser interpret the fetched document identically even
196
+ * if the underlying loader is nondeterministic. Failed loads are not
197
+ * remembered, so a transient failure does not affect the next gateway.
198
+ *
199
+ * The parsed object keeps the loader for its own later dereferences, so
200
+ * `release()` turns it into a plain pass-through to the underlying loader
201
+ * once the operation is over.
202
+ *
203
+ * @internal Technically exported for generated vocabulary classes, but not
204
+ * part of the public API contract. This is not considered public API for
205
+ * Semantic Versioning decisions.
206
+ */
207
+ function createSnapshotContextLoader(contextLoader, suppressError) {
208
+ contextLoader = unwrapReleasedDocumentLoader(contextLoader);
209
+ const cache = /* @__PURE__ */ new Map();
210
+ let released = false;
211
+ const release = () => {
212
+ released = true;
213
+ state.released = true;
214
+ cache.clear();
215
+ };
216
+ const loader = async (url, options) => {
217
+ if (released) return await contextLoader(url, options);
218
+ const key = URL.canParse(url) ? new URL(url).href : url;
219
+ if (BASELINE_CONTEXT_URLS.has(key)) return {
220
+ contextUrl: null,
221
+ document: structuredClone(preloadedContexts[key]),
222
+ documentUrl: key
223
+ };
224
+ let promise = cache.get(key);
225
+ if (promise == null) {
226
+ const loading = contextLoader(url, suppressError ? {
227
+ ...options,
228
+ suppressError: true
229
+ } : options).then((document) => structuredClone(document));
230
+ promise = loading;
231
+ cache.set(key, loading);
232
+ loading.catch(() => {
233
+ if (cache.get(key) === loading) cache.delete(key);
234
+ });
235
+ }
236
+ return structuredClone(await promise);
237
+ };
238
+ const state = {
239
+ base: contextLoader,
240
+ released: false
241
+ };
242
+ registerDocumentLoaderWrapper(loader, state);
243
+ return {
244
+ loader,
245
+ release
246
+ };
247
+ }
248
+ //#endregion
249
+ export { recordPortableReferrer as a, isPortableIri as i, getPortableGatewayCandidates as n, getReferrerGateways as r, createSnapshotContextLoader as t };
@@ -1,6 +1,6 @@
1
- const require_request = require("./request-BE3wHjK4.cjs");
2
- const require_body = require("./body-ILOzDJe-.cjs");
3
- const require_url = require("./url-Ddeuv0sE.cjs");
1
+ const require_request = require("./request-kRkxuOkZ.cjs");
2
+ const require_body = require("./body-edyRdC9n.cjs");
3
+ const require_url = require("./url-BNakuZ8k.cjs");
4
4
  const require_digest = require("./digest-3FeH2Y-Q.cjs");
5
5
  //#region src/portable-media.ts
6
6
  function resolveTimeout(value, fallback) {
@@ -1,6 +1,6 @@
1
- import { r as getUserAgent } from "./request-DkiHcgVt.mjs";
2
- import { r as readBoundedBytes } from "./body-NALc-7u_.mjs";
3
- import { g as parseGatewayUrl, x as validatePublicUrl } from "./url-ClVBNQup.mjs";
1
+ import { r as getUserAgent } from "./request-D-SOvXf_.mjs";
2
+ import { r as readBoundedBytes } from "./body-BbkIKvTI.mjs";
3
+ import { g as parseGatewayUrl, x as validatePublicUrl } from "./url-DMxmp7ZG.mjs";
4
4
  import { a as verifyDigestMultibase, i as parseHashlink, r as parseDigestMultibase } from "./digest-COC7xDiQ.mjs";
5
5
  //#region src/portable-media.ts
6
6
  function resolveTimeout(value, fallback) {
@@ -1,5 +1,5 @@
1
1
  const require_digest = require("./digest-3FeH2Y-Q.cjs");
2
- const require_portable_media = require("./portable-media-HeXeBuyd.cjs");
2
+ const require_portable_media = require("./portable-media-BC-QRzPd.cjs");
3
3
  let node_assert_strict = require("node:assert/strict");
4
4
  let node_test = require("node:test");
5
5
  let node_http = require("node:http");
@@ -1,5 +1,5 @@
1
1
  import { n as createHashlink, t as computeDigestMultibase } from "./digest-COC7xDiQ.mjs";
2
- import { t as fetchPortableMedia } from "./portable-media-SWj7DacP.mjs";
2
+ import { t as fetchPortableMedia } from "./portable-media-CP6hh3ja.mjs";
3
3
  import { deepStrictEqual, equal, rejects } from "node:assert/strict";
4
4
  import { test } from "node:test";
5
5
  import { createServer } from "node:http";
@@ -1,8 +1,8 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
- const require_url = require("./url-Ddeuv0sE.cjs");
2
+ const require_url = require("./url-BNakuZ8k.cjs");
3
3
  const require_key = require("./key-C-AYkdJJ.cjs");
4
4
  const require_digest = require("./digest-3FeH2Y-Q.cjs");
5
- const require_portable_dereference = require("./portable-dereference-BCfSQoXP.cjs");
5
+ const require_portable_dereference = require("./portable-dereference-BNXtgg5U.cjs");
6
6
  let node_assert_strict = require("node:assert/strict");
7
7
  let _fedify_fixture = require("@fedify/fixture");
8
8
  //#region src/portable-workers.test.ts
@@ -1,7 +1,7 @@
1
- import { _ as parseIri, i as canonicalizePortableUri, o as formatIri, s as fromCompatibleEf61Id, y as toCompatibleEf61Id } from "./url-ClVBNQup.mjs";
1
+ import { _ as parseIri, i as canonicalizePortableUri, o as formatIri, s as fromCompatibleEf61Id, y as toCompatibleEf61Id } from "./url-DMxmp7ZG.mjs";
2
2
  import { i as importDidKey, t as exportDidKey } from "./key-C2Db_TAJ.mjs";
3
3
  import { n as createHashlink, o as verifyHashlink, t as computeDigestMultibase } from "./digest-COC7xDiQ.mjs";
4
- import { n as getPortableGatewayCandidates } from "./portable-dereference-EAbEhAJ4.mjs";
4
+ import { n as getPortableGatewayCandidates } from "./portable-dereference-DRE5bz-l.mjs";
5
5
  import { deepStrictEqual, equal } from "node:assert/strict";
6
6
  import { test, testDefinitions } from "@fedify/fixture";
7
7
  //#region src/portable-workers.test.ts
@@ -1,7 +1,7 @@
1
1
  import process from "node:process";
2
2
  //#region deno.json
3
3
  var name = "@fedify/vocab-runtime";
4
- var version = "2.4.0-dev.2256+683c50ba";
4
+ var version = "2.4.0-dev.2260+42356e7d";
5
5
  //#endregion
6
6
  //#region src/request.ts
7
7
  /**
@@ -3,7 +3,7 @@ let node_process = require("node:process");
3
3
  node_process = require_rolldown_runtime.__toESM(node_process, 1);
4
4
  //#region deno.json
5
5
  var name = "@fedify/vocab-runtime";
6
- var version = "2.4.0-dev.2256+683c50ba";
6
+ var version = "2.4.0-dev.2260+42356e7d";
7
7
  //#endregion
8
8
  //#region src/request.ts
9
9
  /**
@@ -1,5 +1,5 @@
1
1
  const require_rolldown_runtime = require("./rolldown-runtime-emK7D4bc.cjs");
2
- const require_request = require("./request-BE3wHjK4.cjs");
2
+ const require_request = require("./request-kRkxuOkZ.cjs");
3
3
  let node_test = require("node:test");
4
4
  let node_process = require("node:process");
5
5
  node_process = require_rolldown_runtime.__toESM(node_process, 1);
@@ -1,4 +1,4 @@
1
- import { n as createActivityPubRequest, o as version, r as getUserAgent } from "./request-DkiHcgVt.mjs";
1
+ import { n as createActivityPubRequest, o as version, r as getUserAgent } from "./request-D-SOvXf_.mjs";
2
2
  import { test } from "node:test";
3
3
  import process from "node:process";
4
4
  import { deepStrictEqual } from "node:assert";
@@ -42,7 +42,14 @@ function assertCompatiblePathCanBeParsed(raw, parsed) {
42
42
  if ((compatiblePath || rawCompatiblePath) && DOT_SEGMENT_PATTERN.test(rawPath)) throw new TypeError("FEP-ef61 compatible identifier paths with dot segments cannot be represented as URLs.");
43
43
  }
44
44
  /**
45
- * Parses a JSON-LD `@id` value as an IRI.
45
+ * Parses a JSON-LD `@id` value as an IRI, including FEP-ef61 portable
46
+ * ActivityPub IRIs. See {@link parseIri} for how IRIs are parsed.
47
+ * @param id The `@id` value.
48
+ * @param base The base IRI to resolve a relative `@id` against.
49
+ * @returns The parsed IRI, or `undefined` if `id` is missing or a blank node
50
+ * identifier.
51
+ * @throws {TypeError} If `id` is not a valid IRI.
52
+ * @since 2.4.0
46
53
  */
47
54
  function parseJsonLdId(id, base) {
48
55
  if (id == null || id.startsWith("_:")) return void 0;
@@ -60,6 +67,16 @@ function parseJsonLdId(id, base) {
60
67
  * This also applies to compatible identifier strings used as relative bases.
61
68
  * A `URL` argument may already have lost such segments before this function
62
69
  * receives it.
70
+ *
71
+ * Portable IRIs, e.g., `ap://did:key:z6Mk.../actor`, cannot be represented by
72
+ * JavaScript `URL` as they are, so the returned `URL` keeps the DID authority
73
+ * percent-encoded, e.g., `ap+ef61://did%3Akey%3Az6Mk.../actor`. Use
74
+ * {@link formatIri} to get the canonical string back.
75
+ * @param iri The IRI to parse.
76
+ * @param base The base IRI to resolve a relative IRI against.
77
+ * @returns The parsed IRI.
78
+ * @throws {TypeError} If the IRI is malformed.
79
+ * @since 2.4.0
63
80
  */
64
81
  function parseIri(iri, base) {
65
82
  if (iri instanceof URL) return normalizePortableUrl(iri) ?? new URL(iri.href);
@@ -84,6 +101,12 @@ function parseIri(iri, base) {
84
101
  * the scheme this function produces. Do not compare its results as strings to
85
102
  * tell whether two portable IRIs identify the same object; use
86
103
  * `arePortableUrisEqual()` instead.
104
+ * @param iri The IRI to format.
105
+ * @returns The formatted IRI. A string that cannot be parsed as a URL at all
106
+ * is returned unchanged.
107
+ * @throws {TypeError} If the IRI is a malformed portable IRI or FEP-ef61
108
+ * compatible identifier, e.g., one with dot segments.
109
+ * @since 2.4.0
87
110
  */
88
111
  function formatIri(iri) {
89
112
  if (typeof iri === "string") assertPortablePathCanBeParsed(iri);
@@ -186,7 +209,20 @@ function haveSameFe34Origin(left, right) {
186
209
  }
187
210
  }
188
211
  /**
189
- * Checks whether two IRIs have the same origin.
212
+ * Checks whether two IRIs have the same origin. Unlike comparing
213
+ * `URL.origin`, which is `"null"` for URLs with non-special schemes, this
214
+ * compares FEP-ef61 portable ActivityPub IRIs by their schemes and DIDs, so
215
+ * that `ap:` and `ap+ef61:` IRIs of the same DID with decoded or
216
+ * percent-encoded authorities have the same origin. Other IRIs with
217
+ * a non-special scheme and a host are compared by their schemes and hosts.
218
+ *
219
+ * This is not an FEP-fe34 origin check: an `ap+ef61:` IRI and the `did:key`
220
+ * verification method of the same DID have different origins here. Use
221
+ * {@link haveSameFe34Origin} for that.
222
+ * @param left The first IRI.
223
+ * @param right The second IRI.
224
+ * @returns `true` if the IRIs have the same origin.
225
+ * @since 2.4.0
190
226
  */
191
227
  function haveSameIriOrigin(left, right) {
192
228
  return getComparableIriOrigin(left) === getComparableIriOrigin(right);
@@ -274,14 +310,27 @@ function parseAtUri(uri) {
274
310
  return new URL("at://" + encodeURIComponent(authority) + path);
275
311
  }
276
312
  /**
277
- * Checks whether the URL is an FEP-ef61 gateway base URI.
313
+ * Checks whether the URL is an FEP-ef61 gateway base URI, i.e., an HTTP(S)
314
+ * URI with no credentials, path, query, or fragment, such as
315
+ * `https://example.com/`. FEP-ef61 requires every item of a portable actor's
316
+ * `gateways` to be such a URI. A URI with an empty query or fragment
317
+ * delimiter, such as `https://example.com/?`, is not a gateway base URI.
318
+ *
319
+ * Note that the `URL` class normalizes `https://example.com` to
320
+ * `https://example.com/`, so both are gateway base URIs.
321
+ * @param url The URL to check.
322
+ * @returns `true` if the URL is an FEP-ef61 gateway base URI.
278
323
  * @since 2.4.0
279
324
  */
280
325
  function isGatewayUrl(url) {
281
326
  return (url.protocol === "http:" || url.protocol === "https:") && url.href === `${url.origin}/`;
282
327
  }
283
328
  /**
284
- * Parses and validates an FEP-ef61 gateway base URI.
329
+ * Parses and validates an FEP-ef61 gateway base URI, i.e., an HTTP(S) URI with
330
+ * no credentials, path, query, or fragment, such as `https://example.com`.
331
+ * See {@link isGatewayUrl} for the rules.
332
+ * @param url The gateway base URI to parse.
333
+ * @returns The parsed gateway base URI, whose path is `/`.
285
334
  * @throws {TypeError} If the URI is malformed, or is not an HTTP(S) base URI
286
335
  * with no credentials, path, query, or fragment. In the
287
336
  * latter case, the message starts with
@@ -40,7 +40,14 @@ function assertCompatiblePathCanBeParsed(raw, parsed) {
40
40
  if ((compatiblePath || rawCompatiblePath) && DOT_SEGMENT_PATTERN.test(rawPath)) throw new TypeError("FEP-ef61 compatible identifier paths with dot segments cannot be represented as URLs.");
41
41
  }
42
42
  /**
43
- * Parses a JSON-LD `@id` value as an IRI.
43
+ * Parses a JSON-LD `@id` value as an IRI, including FEP-ef61 portable
44
+ * ActivityPub IRIs. See {@link parseIri} for how IRIs are parsed.
45
+ * @param id The `@id` value.
46
+ * @param base The base IRI to resolve a relative `@id` against.
47
+ * @returns The parsed IRI, or `undefined` if `id` is missing or a blank node
48
+ * identifier.
49
+ * @throws {TypeError} If `id` is not a valid IRI.
50
+ * @since 2.4.0
44
51
  */
45
52
  function parseJsonLdId(id, base) {
46
53
  if (id == null || id.startsWith("_:")) return void 0;
@@ -58,6 +65,16 @@ function parseJsonLdId(id, base) {
58
65
  * This also applies to compatible identifier strings used as relative bases.
59
66
  * A `URL` argument may already have lost such segments before this function
60
67
  * receives it.
68
+ *
69
+ * Portable IRIs, e.g., `ap://did:key:z6Mk.../actor`, cannot be represented by
70
+ * JavaScript `URL` as they are, so the returned `URL` keeps the DID authority
71
+ * percent-encoded, e.g., `ap+ef61://did%3Akey%3Az6Mk.../actor`. Use
72
+ * {@link formatIri} to get the canonical string back.
73
+ * @param iri The IRI to parse.
74
+ * @param base The base IRI to resolve a relative IRI against.
75
+ * @returns The parsed IRI.
76
+ * @throws {TypeError} If the IRI is malformed.
77
+ * @since 2.4.0
61
78
  */
62
79
  function parseIri(iri, base) {
63
80
  if (iri instanceof URL) return normalizePortableUrl(iri) ?? new URL(iri.href);
@@ -82,6 +99,12 @@ function parseIri(iri, base) {
82
99
  * the scheme this function produces. Do not compare its results as strings to
83
100
  * tell whether two portable IRIs identify the same object; use
84
101
  * `arePortableUrisEqual()` instead.
102
+ * @param iri The IRI to format.
103
+ * @returns The formatted IRI. A string that cannot be parsed as a URL at all
104
+ * is returned unchanged.
105
+ * @throws {TypeError} If the IRI is a malformed portable IRI or FEP-ef61
106
+ * compatible identifier, e.g., one with dot segments.
107
+ * @since 2.4.0
85
108
  */
86
109
  function formatIri(iri) {
87
110
  if (typeof iri === "string") assertPortablePathCanBeParsed(iri);
@@ -184,7 +207,20 @@ function haveSameFe34Origin(left, right) {
184
207
  }
185
208
  }
186
209
  /**
187
- * Checks whether two IRIs have the same origin.
210
+ * Checks whether two IRIs have the same origin. Unlike comparing
211
+ * `URL.origin`, which is `"null"` for URLs with non-special schemes, this
212
+ * compares FEP-ef61 portable ActivityPub IRIs by their schemes and DIDs, so
213
+ * that `ap:` and `ap+ef61:` IRIs of the same DID with decoded or
214
+ * percent-encoded authorities have the same origin. Other IRIs with
215
+ * a non-special scheme and a host are compared by their schemes and hosts.
216
+ *
217
+ * This is not an FEP-fe34 origin check: an `ap+ef61:` IRI and the `did:key`
218
+ * verification method of the same DID have different origins here. Use
219
+ * {@link haveSameFe34Origin} for that.
220
+ * @param left The first IRI.
221
+ * @param right The second IRI.
222
+ * @returns `true` if the IRIs have the same origin.
223
+ * @since 2.4.0
188
224
  */
189
225
  function haveSameIriOrigin(left, right) {
190
226
  return getComparableIriOrigin(left) === getComparableIriOrigin(right);
@@ -272,14 +308,27 @@ function parseAtUri(uri) {
272
308
  return new URL("at://" + encodeURIComponent(authority) + path);
273
309
  }
274
310
  /**
275
- * Checks whether the URL is an FEP-ef61 gateway base URI.
311
+ * Checks whether the URL is an FEP-ef61 gateway base URI, i.e., an HTTP(S)
312
+ * URI with no credentials, path, query, or fragment, such as
313
+ * `https://example.com/`. FEP-ef61 requires every item of a portable actor's
314
+ * `gateways` to be such a URI. A URI with an empty query or fragment
315
+ * delimiter, such as `https://example.com/?`, is not a gateway base URI.
316
+ *
317
+ * Note that the `URL` class normalizes `https://example.com` to
318
+ * `https://example.com/`, so both are gateway base URIs.
319
+ * @param url The URL to check.
320
+ * @returns `true` if the URL is an FEP-ef61 gateway base URI.
276
321
  * @since 2.4.0
277
322
  */
278
323
  function isGatewayUrl(url) {
279
324
  return (url.protocol === "http:" || url.protocol === "https:") && url.href === `${url.origin}/`;
280
325
  }
281
326
  /**
282
- * Parses and validates an FEP-ef61 gateway base URI.
327
+ * Parses and validates an FEP-ef61 gateway base URI, i.e., an HTTP(S) URI with
328
+ * no credentials, path, query, or fragment, such as `https://example.com`.
329
+ * See {@link isGatewayUrl} for the rules.
330
+ * @param url The gateway base URI to parse.
331
+ * @returns The parsed gateway base URI, whose path is `/`.
283
332
  * @throws {TypeError} If the URI is malformed, or is not an HTTP(S) base URI
284
333
  * with no credentials, path, query, or fragment. In the
285
334
  * latter case, the message starts with
@@ -1,4 +1,4 @@
1
- const require_url = require("./url-Ddeuv0sE.cjs");
1
+ const require_url = require("./url-BNakuZ8k.cjs");
2
2
  let node_test = require("node:test");
3
3
  let node_assert = require("node:assert");
4
4
  //#region src/url.test.ts
@@ -1,4 +1,4 @@
1
- import { C as withoutGatewayHints, S as withGatewayHints, _ as parseIri, a as expandIPv6Address, b as validateLookupAddresses, c as getFe34Origin, d as haveSameIriOrigin, f as isGatewayUrl, g as parseGatewayUrl, i as canonicalizePortableUri, l as getGatewayHints, m as isValidPublicIPv6Address, n as UrlError, o as formatIri, p as isValidPublicIPv4Address, r as arePortableUrisEqual, s as fromCompatibleEf61Id, u as haveSameFe34Origin, v as parseJsonLdId, x as validatePublicUrl, y as toCompatibleEf61Id } from "./url-ClVBNQup.mjs";
1
+ import { C as withoutGatewayHints, S as withGatewayHints, _ as parseIri, a as expandIPv6Address, b as validateLookupAddresses, c as getFe34Origin, d as haveSameIriOrigin, f as isGatewayUrl, g as parseGatewayUrl, i as canonicalizePortableUri, l as getGatewayHints, m as isValidPublicIPv6Address, n as UrlError, o as formatIri, p as isValidPublicIPv4Address, r as arePortableUrisEqual, s as fromCompatibleEf61Id, u as haveSameFe34Origin, v as parseJsonLdId, x as validatePublicUrl, y as toCompatibleEf61Id } from "./url-DMxmp7ZG.mjs";
2
2
  import { test } from "node:test";
3
3
  import { deepStrictEqual, ok, rejects, strictEqual, throws } from "node:assert";
4
4
  //#region src/url.test.ts
@@ -43,7 +43,14 @@ function assertCompatiblePathCanBeParsed(raw, parsed) {
43
43
  if ((compatiblePath || rawCompatiblePath) && DOT_SEGMENT_PATTERN.test(rawPath)) throw new TypeError("FEP-ef61 compatible identifier paths with dot segments cannot be represented as URLs.");
44
44
  }
45
45
  /**
46
- * Parses a JSON-LD `@id` value as an IRI.
46
+ * Parses a JSON-LD `@id` value as an IRI, including FEP-ef61 portable
47
+ * ActivityPub IRIs. See {@link parseIri} for how IRIs are parsed.
48
+ * @param id The `@id` value.
49
+ * @param base The base IRI to resolve a relative `@id` against.
50
+ * @returns The parsed IRI, or `undefined` if `id` is missing or a blank node
51
+ * identifier.
52
+ * @throws {TypeError} If `id` is not a valid IRI.
53
+ * @since 2.4.0
47
54
  */
48
55
  function parseJsonLdId(id, base) {
49
56
  if (id == null || id.startsWith("_:")) return void 0;
@@ -61,6 +68,16 @@ function parseJsonLdId(id, base) {
61
68
  * This also applies to compatible identifier strings used as relative bases.
62
69
  * A `URL` argument may already have lost such segments before this function
63
70
  * receives it.
71
+ *
72
+ * Portable IRIs, e.g., `ap://did:key:z6Mk.../actor`, cannot be represented by
73
+ * JavaScript `URL` as they are, so the returned `URL` keeps the DID authority
74
+ * percent-encoded, e.g., `ap+ef61://did%3Akey%3Az6Mk.../actor`. Use
75
+ * {@link formatIri} to get the canonical string back.
76
+ * @param iri The IRI to parse.
77
+ * @param base The base IRI to resolve a relative IRI against.
78
+ * @returns The parsed IRI.
79
+ * @throws {TypeError} If the IRI is malformed.
80
+ * @since 2.4.0
64
81
  */
65
82
  function parseIri(iri, base) {
66
83
  if (iri instanceof URL) return normalizePortableUrl(iri) ?? new URL(iri.href);
@@ -85,6 +102,12 @@ function parseIri(iri, base) {
85
102
  * the scheme this function produces. Do not compare its results as strings to
86
103
  * tell whether two portable IRIs identify the same object; use
87
104
  * `arePortableUrisEqual()` instead.
105
+ * @param iri The IRI to format.
106
+ * @returns The formatted IRI. A string that cannot be parsed as a URL at all
107
+ * is returned unchanged.
108
+ * @throws {TypeError} If the IRI is a malformed portable IRI or FEP-ef61
109
+ * compatible identifier, e.g., one with dot segments.
110
+ * @since 2.4.0
88
111
  */
89
112
  function formatIri(iri) {
90
113
  if (typeof iri === "string") assertPortablePathCanBeParsed(iri);
@@ -187,7 +210,20 @@ function haveSameFe34Origin(left, right) {
187
210
  }
188
211
  }
189
212
  /**
190
- * Checks whether two IRIs have the same origin.
213
+ * Checks whether two IRIs have the same origin. Unlike comparing
214
+ * `URL.origin`, which is `"null"` for URLs with non-special schemes, this
215
+ * compares FEP-ef61 portable ActivityPub IRIs by their schemes and DIDs, so
216
+ * that `ap:` and `ap+ef61:` IRIs of the same DID with decoded or
217
+ * percent-encoded authorities have the same origin. Other IRIs with
218
+ * a non-special scheme and a host are compared by their schemes and hosts.
219
+ *
220
+ * This is not an FEP-fe34 origin check: an `ap+ef61:` IRI and the `did:key`
221
+ * verification method of the same DID have different origins here. Use
222
+ * {@link haveSameFe34Origin} for that.
223
+ * @param left The first IRI.
224
+ * @param right The second IRI.
225
+ * @returns `true` if the IRIs have the same origin.
226
+ * @since 2.4.0
191
227
  */
192
228
  function haveSameIriOrigin(left, right) {
193
229
  return getComparableIriOrigin(left) === getComparableIriOrigin(right);
@@ -275,14 +311,27 @@ function parseAtUri(uri) {
275
311
  return new URL("at://" + encodeURIComponent(authority) + path);
276
312
  }
277
313
  /**
278
- * Checks whether the URL is an FEP-ef61 gateway base URI.
314
+ * Checks whether the URL is an FEP-ef61 gateway base URI, i.e., an HTTP(S)
315
+ * URI with no credentials, path, query, or fragment, such as
316
+ * `https://example.com/`. FEP-ef61 requires every item of a portable actor's
317
+ * `gateways` to be such a URI. A URI with an empty query or fragment
318
+ * delimiter, such as `https://example.com/?`, is not a gateway base URI.
319
+ *
320
+ * Note that the `URL` class normalizes `https://example.com` to
321
+ * `https://example.com/`, so both are gateway base URIs.
322
+ * @param url The URL to check.
323
+ * @returns `true` if the URL is an FEP-ef61 gateway base URI.
279
324
  * @since 2.4.0
280
325
  */
281
326
  function isGatewayUrl(url) {
282
327
  return (url.protocol === "http:" || url.protocol === "https:") && url.href === `${url.origin}/`;
283
328
  }
284
329
  /**
285
- * Parses and validates an FEP-ef61 gateway base URI.
330
+ * Parses and validates an FEP-ef61 gateway base URI, i.e., an HTTP(S) URI with
331
+ * no credentials, path, query, or fragment, such as `https://example.com`.
332
+ * See {@link isGatewayUrl} for the rules.
333
+ * @param url The gateway base URI to parse.
334
+ * @returns The parsed gateway base URI, whose path is `/`.
286
335
  * @throws {TypeError} If the URI is malformed, or is not an HTTP(S) base URI
287
336
  * with no credentials, path, query, or fragment. In the
288
337
  * latter case, the message starts with