@fedify/vocab-runtime 2.4.0-pr.936.41 → 2.4.1

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 (152) hide show
  1. package/deno.json +4 -2
  2. package/dist/{tests/docloader-Ck8bcBir.mjs → contexts-B09jb9Pi.cjs} +173 -274
  3. package/dist/{tests/docloader-S_gQZ2tt.cjs → contexts-DWrj4AGR.js} +169 -293
  4. package/dist/{docloader-C_dir7Xb.d.ts → docloader-BMHljyYb.d.cts} +68 -2
  5. package/dist/{docloader-D2DTRiyA.d.cts → docloader-BMHljyYb.d.ts} +68 -2
  6. package/dist/internal/jsonld-cache.cjs +70 -1
  7. package/dist/internal/jsonld-cache.d.cts +35 -2
  8. package/dist/internal/jsonld-cache.d.ts +35 -2
  9. package/dist/internal/jsonld-cache.js +67 -2
  10. package/dist/internal/portable-dereference.cjs +721 -0
  11. package/dist/internal/portable-dereference.d.cts +349 -0
  12. package/dist/internal/portable-dereference.d.ts +349 -0
  13. package/dist/internal/portable-dereference.js +702 -0
  14. package/dist/internal/signed-representation.cjs +378 -0
  15. package/dist/internal/signed-representation.d.cts +144 -0
  16. package/dist/internal/signed-representation.d.ts +144 -0
  17. package/dist/internal/signed-representation.js +371 -0
  18. package/dist/jsonld.cjs +2 -2
  19. package/dist/mod.cjs +762 -4529
  20. package/dist/mod.d.cts +427 -9
  21. package/dist/mod.d.ts +427 -10
  22. package/dist/mod.js +742 -4524
  23. package/dist/portable-6Aj-7dZJ.d.cts +160 -0
  24. package/dist/portable-nkTiqH0w.d.ts +160 -0
  25. package/dist/tests/body-DpC9p-iG.mjs +130 -0
  26. package/dist/tests/body-cKLBprYm.cjs +153 -0
  27. package/dist/tests/body.test.cjs +89 -0
  28. package/dist/tests/body.test.d.cts +1 -0
  29. package/dist/tests/body.test.d.mts +1 -0
  30. package/dist/tests/body.test.mjs +90 -0
  31. package/dist/tests/contexts-C9Lj11SC.cjs +4533 -0
  32. package/dist/tests/contexts-duRA_dDC.mjs +4522 -0
  33. package/dist/tests/decimal.test.cjs +9 -7
  34. package/dist/tests/decimal.test.mjs +9 -6
  35. package/dist/tests/digest-3FeH2Y-Q.cjs +176 -0
  36. package/dist/tests/digest-COC7xDiQ.mjs +141 -0
  37. package/dist/tests/digest.test.cjs +102 -0
  38. package/dist/tests/digest.test.d.cts +1 -0
  39. package/dist/tests/digest.test.d.mts +1 -0
  40. package/dist/tests/digest.test.mjs +103 -0
  41. package/dist/tests/docloader-DyZ1JGIl.cjs +398 -0
  42. package/dist/tests/docloader-tUmMeXjU.mjs +375 -0
  43. package/dist/tests/docloader.test.cjs +816 -39
  44. package/dist/tests/docloader.test.mjs +810 -34
  45. package/dist/tests/internal/multicodec.test.cjs +2 -3
  46. package/dist/tests/internal/multicodec.test.mjs +2 -2
  47. package/dist/tests/internal/portable-dereference.test.cjs +172 -0
  48. package/dist/tests/internal/portable-dereference.test.d.cts +1 -0
  49. package/dist/tests/internal/portable-dereference.test.d.mts +1 -0
  50. package/dist/tests/internal/portable-dereference.test.mjs +173 -0
  51. package/dist/tests/jsonld-CmDhHNTx.cjs +12 -0
  52. package/dist/tests/jsonld-DweIJ07b.mjs +5 -0
  53. package/dist/tests/jsonld-cache-BEXiiM-V.cjs +386 -0
  54. package/dist/tests/jsonld-cache-DFnWsLuZ.mjs +339 -0
  55. package/dist/tests/jsonld-cache.test.cjs +102 -298
  56. package/dist/tests/jsonld-cache.test.mjs +95 -289
  57. package/dist/tests/{key-_wXwomh_.cjs → key-C-AYkdJJ.cjs} +11 -4
  58. package/dist/tests/{key-CDGDH_vC.mjs → key-C2Db_TAJ.mjs} +11 -3
  59. package/dist/tests/key.test.cjs +6 -5
  60. package/dist/tests/key.test.mjs +6 -4
  61. package/dist/tests/{langstr-Di5AvKpB.mjs → langstr-DYRmJqFn.mjs} +5 -2
  62. package/dist/tests/{langstr-CbAxaeEZ.cjs → langstr-_PqsL1ae.cjs} +5 -2
  63. package/dist/tests/langstr.test.cjs +11 -4
  64. package/dist/tests/langstr.test.mjs +9 -2
  65. package/dist/tests/langtag-CUppHs_s.mjs +45 -0
  66. package/dist/tests/langtag-Czjvxfzx.cjs +50 -0
  67. package/dist/tests/langtag.test.cjs +45 -0
  68. package/dist/tests/langtag.test.d.cts +1 -0
  69. package/dist/tests/langtag.test.d.mts +1 -0
  70. package/dist/tests/langtag.test.mjs +46 -0
  71. package/dist/tests/link.test.cjs +2 -3
  72. package/dist/tests/link.test.mjs +2 -2
  73. package/dist/tests/multibase/multibase.test.cjs +8 -9
  74. package/dist/tests/multibase/multibase.test.mjs +6 -6
  75. package/dist/tests/{multibase-Bz_UUDtL.cjs → multibase-B5Mea7Ip.cjs} +19 -2
  76. package/dist/tests/{multibase-B4bvakyA.mjs → multibase-BPnF_L4e.mjs} +12 -1
  77. package/dist/tests/portable-dereference-DmTYDy2p.mjs +250 -0
  78. package/dist/tests/portable-dereference-qTLMqe2_.cjs +279 -0
  79. package/dist/tests/portable-media-5p8azal0.cjs +153 -0
  80. package/dist/tests/portable-media-B_lt9MMA.mjs +148 -0
  81. package/dist/tests/portable-media.test.cjs +222 -0
  82. package/dist/tests/portable-media.test.d.cts +1 -0
  83. package/dist/tests/portable-media.test.d.mts +1 -0
  84. package/dist/tests/portable-media.test.mjs +223 -0
  85. package/dist/tests/portable-workers.test.cjs +36 -0
  86. package/dist/tests/portable-workers.test.d.cts +2 -0
  87. package/dist/tests/portable-workers.test.d.mts +2 -0
  88. package/dist/tests/portable-workers.test.mjs +35 -0
  89. package/dist/tests/{request-uk51rkhO.cjs → request-BgjA9wyl.cjs} +10 -4
  90. package/dist/tests/{request-C8CaGwtt.mjs → request-DodZNukP.mjs} +8 -2
  91. package/dist/tests/request.test.cjs +7 -4
  92. package/dist/tests/request.test.mjs +5 -2
  93. package/dist/tests/signed-representation.test.cjs +588 -0
  94. package/dist/tests/signed-representation.test.d.cts +1 -0
  95. package/dist/tests/signed-representation.test.d.mts +1 -0
  96. package/dist/tests/signed-representation.test.mjs +589 -0
  97. package/dist/tests/temporal.test.cjs +1 -2
  98. package/dist/tests/temporal.test.mjs +1 -1
  99. package/dist/tests/url-BNakuZ8k.cjs +998 -0
  100. package/dist/tests/url-DMxmp7ZG.mjs +859 -0
  101. package/dist/tests/url.test.cjs +489 -6
  102. package/dist/tests/url.test.mjs +489 -5
  103. package/dist/url-DrGTR8yv.cjs +993 -0
  104. package/dist/url-Dzyp-NsC.js +860 -0
  105. package/package.json +27 -4
  106. package/scripts/test-bun.mjs +17 -0
  107. package/src/body.test.ts +125 -0
  108. package/src/body.ts +152 -0
  109. package/src/contexts/cid-v1.json +114 -0
  110. package/src/contexts/fep-22cd.json +21 -0
  111. package/src/contexts/fep-6757.json +12 -0
  112. package/src/contexts/joinmastodon.json +1 -1
  113. package/src/contexts/miscellany.json +17 -0
  114. package/src/contexts.ts +47 -1
  115. package/src/digest.test.ts +220 -0
  116. package/src/digest.ts +229 -0
  117. package/src/docloader.test.ts +1066 -27
  118. package/src/docloader.ts +322 -105
  119. package/src/internal/jsonld-cache.ts +97 -1
  120. package/src/internal/portable-dereference.test.ts +254 -0
  121. package/src/internal/portable-dereference.ts +1000 -0
  122. package/src/internal/signed-representation.ts +565 -0
  123. package/src/jsonld-cache.test.ts +89 -0
  124. package/src/key.test.ts +9 -0
  125. package/src/key.ts +11 -1
  126. package/src/langstr.test.ts +18 -1
  127. package/src/langstr.ts +6 -2
  128. package/src/langtag.test.ts +53 -0
  129. package/src/langtag.ts +76 -0
  130. package/src/mod.ts +29 -0
  131. package/src/multibase/multibase.test.ts +5 -5
  132. package/src/portable-media.test.ts +293 -0
  133. package/src/portable-media.ts +264 -0
  134. package/src/portable-workers.test.ts +81 -0
  135. package/src/portable.ts +180 -0
  136. package/src/preprocessor.ts +7 -0
  137. package/src/request.test.ts +10 -1
  138. package/src/request.ts +9 -1
  139. package/src/signed-representation.test.ts +338 -0
  140. package/src/url.test.ts +823 -3
  141. package/src/url.ts +700 -23
  142. package/tsdown.config.ts +2 -0
  143. package/dist/tests/url-CsOV_B_P.cjs +0 -482
  144. package/dist/tests/url-Du7RQQgP.mjs +0 -392
  145. package/dist/url-DD4F0ULf.cjs +0 -483
  146. package/dist/url-DGVbSVVi.js +0 -393
  147. /package/dist/{chunk-M78iaK0I.cjs → rolldown-runtime-B7lfambq.cjs} +0 -0
  148. /package/dist/tests/{link-NUUWCdnK.mjs → link-Cevmc87v.mjs} +0 -0
  149. /package/dist/tests/{link-FguCydMA.cjs → link-DlKm8bEr.cjs} +0 -0
  150. /package/dist/tests/{multicodec-CxGVGa91.cjs → multicodec-CLRPeW4N.cjs} +0 -0
  151. /package/dist/tests/{multicodec-CyFp54fI.mjs → multicodec-CRIj_05H.mjs} +0 -0
  152. /package/dist/tests/{chunk-C2EiDwsr.cjs → rolldown-runtime-emK7D4bc.cjs} +0 -0
@@ -0,0 +1,998 @@
1
+ const require_rolldown_runtime = require("./rolldown-runtime-emK7D4bc.cjs");
2
+ let node_dns_promises = require("node:dns/promises");
3
+ node_dns_promises = require_rolldown_runtime.__toESM(node_dns_promises, 1);
4
+ let node_net = require("node:net");
5
+ //#region src/url.ts
6
+ var UrlError = class extends Error {
7
+ /**
8
+ * The failure category. Defaults to `"disallowed"`.
9
+ * `"dns"` includes lookup errors and results with no usable IP addresses.
10
+ * @since 2.0.29
11
+ */
12
+ reason;
13
+ constructor(message, options) {
14
+ super(message, options);
15
+ this.name = "UrlError";
16
+ this.reason = options?.reason ?? "disallowed";
17
+ }
18
+ };
19
+ const PORTABLE_IRI_PATTERN = /^(ap|ap\+ef61):\/\/([^/?#]*)([^?#]*)(\?[^#]*)?(#.*)?$/i;
20
+ const INVALID_PERCENT_ENCODING_PATTERN = /%(?![0-9A-Fa-f]{2})/;
21
+ const PERCENT_ENCODING_PATTERN = /%[0-9A-Fa-f]{2}/g;
22
+ const DID_SCHEME_PATTERN = /^did:/i;
23
+ const DID_PATTERN = /^did:[a-z0-9]+:[-A-Za-z0-9._%]+(?::[-A-Za-z0-9._%]+)*$/i;
24
+ const DOT_SEGMENT_PATTERN = /(?:^|\/)(?:\.|%2e){1,2}(?=\/|$)/i;
25
+ function prepareUrlInput(iri) {
26
+ let prepared = iri.replace(/[\t\n\r]/g, "");
27
+ while (prepared.length > 0 && prepared.charCodeAt(0) <= 32) prepared = prepared.slice(1);
28
+ while (prepared.length > 0 && prepared.charCodeAt(prepared.length - 1) <= 32) prepared = prepared.slice(0, -1);
29
+ return prepared;
30
+ }
31
+ function assertPortablePathCanBeParsed(iri) {
32
+ const match = prepareUrlInput(iri).match(PORTABLE_IRI_PATTERN);
33
+ if (match != null && DOT_SEGMENT_PATTERN.test(match[3])) throw new TypeError("Portable ActivityPub IRI paths with dot segments cannot be represented as URLs.");
34
+ }
35
+ function assertCompatiblePathCanBeParsed(raw, parsed) {
36
+ if (parsed.protocol !== "http:" && parsed.protocol !== "https:") return;
37
+ const match = prepareUrlInput(raw).match(/^https?:[\/\\]*[^/?#\\]*([^?#]*)/i);
38
+ if (match == null) return;
39
+ const rawPath = match[1].replace(/\\/g, "/");
40
+ const compatiblePath = parsed.pathname.startsWith(COMPATIBLE_ID_PATH_PREFIX) && COMPATIBLE_ID_DID_PATTERN.test(parsed.pathname.slice(COMPATIBLE_ID_PATH_PREFIX.length));
41
+ const rawCompatiblePath = rawPath.startsWith(COMPATIBLE_ID_PATH_PREFIX) && COMPATIBLE_ID_DID_PATTERN.test(rawPath.slice(COMPATIBLE_ID_PATH_PREFIX.length));
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
+ }
44
+ /**
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
53
+ */
54
+ function parseJsonLdId(id, base) {
55
+ if (id == null || id.startsWith("_:")) return void 0;
56
+ try {
57
+ return parseIri(id, base);
58
+ } catch {
59
+ throw new TypeError("Invalid @id: " + id);
60
+ }
61
+ }
62
+ /**
63
+ * Parses an IRI as a URL, including FEP-ef61 portable ActivityPub IRIs.
64
+ * Portable URI and FEP-ef61 compatible identifier strings whose path contains
65
+ * a `.` or `..` segment, including percent-encoded spellings, throw a
66
+ * `TypeError`: JavaScript `URL` would otherwise identify a different object.
67
+ * This also applies to compatible identifier strings used as relative bases.
68
+ * A `URL` argument may already have lost such segments before this function
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
80
+ */
81
+ function parseIri(iri, base) {
82
+ if (iri instanceof URL) return normalizePortableUrl(iri) ?? new URL(iri.href);
83
+ assertPortablePathCanBeParsed(iri);
84
+ const portable = parsePortableIri(iri);
85
+ if (portable != null) return portable;
86
+ base = normalizeBaseIri(base);
87
+ if (!URL.canParse(iri, base) && iri.startsWith("at://")) return parseAtUri(iri);
88
+ const parsed = new URL(iri, base);
89
+ assertCompatiblePathCanBeParsed(iri, parsed);
90
+ return normalizePortableUrl(parsed) ?? parsed;
91
+ }
92
+ /**
93
+ * Formats a URL as an IRI, including FEP-ef61 portable ActivityPub IRIs.
94
+ * Portable URI and FEP-ef61 compatible identifier strings with dot segments
95
+ * throw a `TypeError` because their paths cannot be represented by JavaScript
96
+ * `URL` without normalization.
97
+ *
98
+ * Portable IRIs are formatted with the `ap+ef61:` scheme and a decoded DID
99
+ * authority, even if they were parsed from `ap:` IRIs. FEP-ef61 currently
100
+ * recommends the `ap:` scheme, so a future major version of Fedify may change
101
+ * the scheme this function produces. Do not compare its results as strings to
102
+ * tell whether two portable IRIs identify the same object; use
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
110
+ */
111
+ function formatIri(iri) {
112
+ if (typeof iri === "string") assertPortablePathCanBeParsed(iri);
113
+ const parsed = parsePortableIri(iri instanceof URL ? iri.href : iri);
114
+ if (parsed == null) {
115
+ if (iri instanceof URL) return iri.href;
116
+ if (!URL.canParse(iri)) return iri;
117
+ const url = new URL(iri);
118
+ assertCompatiblePathCanBeParsed(iri, url);
119
+ return url.href;
120
+ }
121
+ return `ap+ef61://${decodePortableAuthority(parsed.host)}${parsed.pathname}${parsed.search}${parsed.hash}`;
122
+ }
123
+ /**
124
+ * Canonicalizes a FEP-ef61 portable ActivityPub URI for comparison.
125
+ *
126
+ * This accepts both `ap:` and `ap+ef61:` URI strings with decoded or
127
+ * percent-encoded DID authorities. The returned value uses the `ap+ef61:`
128
+ * scheme, a decoded DID authority, and no query component. Pass the raw URI
129
+ * string, not a `URL` object, because JavaScript `URL` normalizes opaque path
130
+ * segments before Fedify can compare them.
131
+ *
132
+ * The `ap+ef61:` scheme of the result is a choice of Fedify's, whereas
133
+ * FEP-ef61 currently canonicalizes portable URIs with the `ap:` scheme.
134
+ * A future major version of Fedify may change the scheme of the result, so
135
+ * do not persist the result as the only copy of an identifier; keep the
136
+ * original URI, and canonicalize it again when comparing.
137
+ *
138
+ * @param input The raw portable ActivityPub URI string to canonicalize.
139
+ * @returns The canonical portable ActivityPub URI string.
140
+ * @throws {TypeError} If the input is not a valid portable ActivityPub IRI.
141
+ * @since 2.4.0
142
+ */
143
+ function canonicalizePortableUri(input) {
144
+ if (typeof input !== "string") throw new TypeError("Invalid portable ActivityPub IRI.");
145
+ const parsed = parsePortableIri(input);
146
+ if (parsed == null) throw new TypeError("Invalid portable ActivityPub IRI.");
147
+ const match = input.match(PORTABLE_IRI_PATTERN);
148
+ return `ap+ef61://${normalizePortableAuthority(getDidUrlOrigin(decodePortableAuthority(parsed.host)))}${normalizePortableComponent(match[3])}${match[5] == null ? "" : normalizePortableComponent(match[5])}`;
149
+ }
150
+ /**
151
+ * Checks whether two FEP-ef61 portable ActivityPub URIs identify the same
152
+ * portable object.
153
+ *
154
+ * Non-string inputs return `false`. Non-portable URI strings use strict string
155
+ * equality. Portable URI strings are compared through
156
+ * {@link canonicalizePortableUri}; malformed portable URI strings return
157
+ * `false` unless they are exactly equal.
158
+ *
159
+ * @since 2.4.0
160
+ */
161
+ function arePortableUrisEqual(left, right) {
162
+ if (typeof left !== "string" || typeof right !== "string") return false;
163
+ if (left === right) return true;
164
+ if (!PORTABLE_IRI_PATTERN.test(left) || !PORTABLE_IRI_PATTERN.test(right)) return false;
165
+ try {
166
+ return canonicalizePortableUri(left) === canonicalizePortableUri(right);
167
+ } catch (error) {
168
+ if (error instanceof TypeError) return false;
169
+ throw error;
170
+ }
171
+ }
172
+ /**
173
+ * Computes an IRI's FEP-fe34 origin.
174
+ *
175
+ * HTTP(S) IRIs use their web origin. FEP-ef61 portable ActivityPub IRIs and
176
+ * DID URLs use their DID as a cryptographic origin.
177
+ *
178
+ * @throws {TypeError} If the IRI does not have a supported FEP-fe34 origin.
179
+ * @since 2.4.0
180
+ */
181
+ function getFe34Origin(input) {
182
+ if (input instanceof URL) {
183
+ const portable = normalizePortableUrl(input);
184
+ if (portable != null) return getPortableCryptographicOrigin(portable);
185
+ if (input.protocol === "did:") return getDidUrlOrigin(input.href);
186
+ if (input.protocol === "http:" || input.protocol === "https:") return input.origin;
187
+ throw new TypeError("Unsupported FEP-fe34 origin IRI.");
188
+ }
189
+ const portable = parsePortableIri(input);
190
+ if (portable != null) return getPortableCryptographicOrigin(portable);
191
+ if (DID_SCHEME_PATTERN.test(input)) return getDidUrlOrigin(input);
192
+ const parsed = new URL(input);
193
+ if (parsed.protocol === "http:" || parsed.protocol === "https:") return parsed.origin;
194
+ throw new TypeError("Unsupported FEP-fe34 origin IRI.");
195
+ }
196
+ /**
197
+ * Checks whether two IRIs have the same FEP-fe34 origin.
198
+ *
199
+ * Malformed or unsupported IRIs are treated as non-matching.
200
+ *
201
+ * @since 2.4.0
202
+ */
203
+ function haveSameFe34Origin(left, right) {
204
+ try {
205
+ return getFe34Origin(left) === getFe34Origin(right);
206
+ } catch (error) {
207
+ if (error instanceof TypeError) return false;
208
+ throw error;
209
+ }
210
+ }
211
+ /**
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
226
+ */
227
+ function haveSameIriOrigin(left, right) {
228
+ return getComparableIriOrigin(left) === getComparableIriOrigin(right);
229
+ }
230
+ function getComparableIriOrigin(iri) {
231
+ iri = normalizePortableUrl(iri) ?? iri;
232
+ if (iri.origin !== "null") return iri.origin;
233
+ if (iri.host !== "") {
234
+ const host = iri.protocol === "ap+ef61:" ? encodeURIComponent(getDidUrlOrigin(decodePortableAuthority(iri.host))) : iri.host;
235
+ return `${iri.protocol}//${host}`;
236
+ }
237
+ return iri.href;
238
+ }
239
+ function getPortableCryptographicOrigin(iri) {
240
+ return getDidUrlOrigin(decodePortableAuthority(iri.host));
241
+ }
242
+ function getDidUrlOrigin(iri) {
243
+ const did = iri.split(/[/?#]/, 1)[0].replace(DID_SCHEME_PATTERN, "did:");
244
+ if (!DID_PATTERN.test(did)) throw new TypeError("Invalid DID URL.");
245
+ const parts = did.split(":");
246
+ parts[1] = parts[1].toLowerCase();
247
+ return normalizePortableAuthority(parts.join(":"));
248
+ }
249
+ function parsePortableIri(iri) {
250
+ const match = iri.match(PORTABLE_IRI_PATTERN);
251
+ if (match == null) return null;
252
+ const authority = getDidUrlOrigin(decodePortableAuthority(match[2]));
253
+ if (!DID_PATTERN.test(authority)) throw new TypeError("Invalid portable ActivityPub IRI authority.");
254
+ if (match[3] === "") throw new TypeError("Invalid portable ActivityPub IRI path.");
255
+ return new URL(`ap+ef61://${encodeURIComponent(authority)}${match[3]}${match[4] ?? ""}${match[5] ?? ""}`);
256
+ }
257
+ function normalizePortableUrl(iri) {
258
+ if (iri.protocol !== "ap:" && iri.protocol !== "ap+ef61:") return null;
259
+ const raw = `ap+ef61://${iri.host}${iri.pathname}${iri.search}${iri.hash}`;
260
+ assertPortablePathCanBeParsed(raw);
261
+ return parsePortableIri(raw);
262
+ }
263
+ function normalizeBaseIri(base) {
264
+ if (base == null) return void 0;
265
+ if (base instanceof URL) return normalizePortableUrl(base) ?? base;
266
+ assertPortablePathCanBeParsed(base);
267
+ if (URL.canParse(base)) assertCompatiblePathCanBeParsed(base, new URL(base));
268
+ return parsePortableIri(base) ?? (base.startsWith("at://") && !URL.canParse(".", base) ? parseAtUri(base) : base);
269
+ }
270
+ function decodePortableAuthority(authority) {
271
+ if (INVALID_PERCENT_ENCODING_PATTERN.test(authority)) throw new TypeError("Invalid portable ActivityPub IRI authority.");
272
+ if (DID_SCHEME_PATTERN.test(authority)) {
273
+ const decoded = authority.replace(/%25/gi, "%");
274
+ if (INVALID_PERCENT_ENCODING_PATTERN.test(decoded)) throw new TypeError("Invalid portable ActivityPub IRI authority.");
275
+ return decoded;
276
+ }
277
+ const decoded = authority.replace(/%(25|3A)/gi, (match) => match.toLowerCase() === "%3a" ? ":" : "%");
278
+ if (INVALID_PERCENT_ENCODING_PATTERN.test(decoded)) throw new TypeError("Invalid portable ActivityPub IRI authority.");
279
+ return decoded;
280
+ }
281
+ function normalizePercentEncoding(value) {
282
+ return value.replace(PERCENT_ENCODING_PATTERN, (match) => match.toUpperCase());
283
+ }
284
+ function normalizePortableAuthority(authority) {
285
+ return normalizePercentEncoding(authority).replace(PERCENT_ENCODING_PATTERN, (match) => {
286
+ const decoded = String.fromCharCode(Number.parseInt(match.slice(1), 16));
287
+ return /[A-Za-z0-9._~-]/.test(decoded) ? decoded : match;
288
+ });
289
+ }
290
+ function normalizePortableComponent(value) {
291
+ if (INVALID_PERCENT_ENCODING_PATTERN.test(value)) throw new TypeError("Invalid portable ActivityPub IRI component.");
292
+ return value.replace(/%[0-9A-Fa-f]{2}|[^%]+/g, (match) => {
293
+ if (match.startsWith("%")) {
294
+ const upper = match.toUpperCase();
295
+ const decoded = String.fromCharCode(Number.parseInt(upper.slice(1), 16));
296
+ return /[A-Za-z0-9._~-]/.test(decoded) ? decoded : upper;
297
+ }
298
+ try {
299
+ return encodeURI(match);
300
+ } catch (error) {
301
+ if (error instanceof URIError) throw new TypeError("Invalid portable ActivityPub IRI component.");
302
+ throw error;
303
+ }
304
+ });
305
+ }
306
+ function parseAtUri(uri) {
307
+ const index = uri.indexOf("/", 5);
308
+ const authority = index >= 0 ? uri.slice(5, index) : uri.slice(5);
309
+ const path = index >= 0 ? uri.slice(index) : "";
310
+ return new URL("at://" + encodeURIComponent(authority) + path);
311
+ }
312
+ /**
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.
323
+ * @since 2.4.0
324
+ */
325
+ function isGatewayUrl(url) {
326
+ return (url.protocol === "http:" || url.protocol === "https:") && url.href === `${url.origin}/`;
327
+ }
328
+ /**
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 `/`.
334
+ * @throws {TypeError} If the URI is malformed, or is not an HTTP(S) base URI
335
+ * with no credentials, path, query, or fragment. In the
336
+ * latter case, the message starts with
337
+ * `Invalid FEP-ef61 gateway:`.
338
+ * @since 2.4.0
339
+ */
340
+ function parseGatewayUrl(url) {
341
+ const parsed = parseIri(url);
342
+ if (!isGatewayUrl(parsed)) throw new TypeError(`Invalid FEP-ef61 gateway: ${url}. FEP-ef61 gateways must be HTTP(S) base URIs with no credentials, path, query, or fragment.`);
343
+ return parsed;
344
+ }
345
+ const COMPATIBLE_ID_PATH_PREFIX = "/.well-known/apgateway/";
346
+ const COMPATIBLE_ID_DID_PATTERN = /^did(?::|%3A)/i;
347
+ const RAW_COMPATIBLE_ID_PREFIX_PATTERN = /^(?:[hH][tT][tT][pP][sS]?):\/\/[^/?#]*\/\.well-known\/apgateway\/(?=[dD][iI][dD](?::|%3[aA]))/;
348
+ const LOCATION_HINT_PARAMETERS = /* @__PURE__ */ new Set(["@gateway", "gateways"]);
349
+ /**
350
+ * Converts an [FEP-ef61] compatible identifier into a portable ActivityPub
351
+ * URI.
352
+ *
353
+ * A compatible identifier is an HTTP(S) URL under a gateway's fixed
354
+ * `/.well-known/apgateway/` path, such as
355
+ * `https://server.example/.well-known/apgateway/did:key:z6Mk.../objects/1`.
356
+ * This function removes the gateway part and returns the corresponding
357
+ * portable URI, e.g., `ap+ef61://did:key:z6Mk.../objects/1`, in the same
358
+ * internal `URL` form that {@link parseIri} produces. The path, query, and
359
+ * fragment are preserved, so the result is a portable URI, not a comparison
360
+ * form; pass its `href` to {@link canonicalizePortableUri} or
361
+ * {@link arePortableUrisEqual} to compare it with other portable URIs.
362
+ *
363
+ * The conversion only reveals the *claimed* portable identifier. Anyone can
364
+ * publish a compatible identifier for any DID on their own server, so the
365
+ * gateway that served it is neither the object's origin nor authorized to act
366
+ * for the DID. Callers must still verify the retrieved document's Object
367
+ * Integrity Proof against the DID, as FEP-ef61 requires, and should keep the
368
+ * original URL if they need the gateway as a retrieval hint.
369
+ *
370
+ * Arbitrary gateway paths are not supported.
371
+ *
372
+ * [FEP-ef61]: https://w3id.org/fep/ef61
373
+ *
374
+ * @param input The URL to convert.
375
+ * @returns The portable ActivityPub URI, or `null` if the input is not an
376
+ * HTTP(S) URL whose path starts with `/.well-known/apgateway/did:`.
377
+ * Other gateway routes, such as the gateway discovery endpoint and
378
+ * hashlink media URLs, also yield `null`.
379
+ * @throws {TypeError} If the input looks like a compatible identifier but is
380
+ * malformed, e.g., it has an invalid DID, no object path,
381
+ * invalid percent-encoding, credentials, or location
382
+ * hints (`@gateway` query parameters, or the legacy
383
+ * `gateways` parameter), or its raw string path would
384
+ * change during URL parsing. Already-parsed `URL`
385
+ * arguments cannot reveal segments lost by their parser.
386
+ * @since 2.4.0
387
+ */
388
+ function fromCompatibleEf61Id(input) {
389
+ return convertCompatibleEf61Id(input)?.url ?? null;
390
+ }
391
+ function convertCompatibleEf61Id(input) {
392
+ let url;
393
+ const rawPrefix = typeof input === "string" ? input.match(RAW_COMPATIBLE_ID_PREFIX_PATTERN) : null;
394
+ if (input instanceof URL) url = input;
395
+ else if (typeof input === "string" && URL.canParse(input)) url = new URL(input);
396
+ else return null;
397
+ if (url.protocol !== "http:" && url.protocol !== "https:") return null;
398
+ if (typeof input === "string" && rawPrefix == null) {
399
+ if (url.pathname.startsWith(COMPATIBLE_ID_PATH_PREFIX) && COMPATIBLE_ID_DID_PATTERN.test(url.pathname.slice(23))) throw new TypeError("Invalid FEP-ef61 compatible identifier.");
400
+ }
401
+ if (!url.pathname.startsWith(COMPATIBLE_ID_PATH_PREFIX)) {
402
+ if (rawPrefix != null) throw new TypeError("Invalid FEP-ef61 compatible identifier.");
403
+ return null;
404
+ }
405
+ const tail = url.pathname.slice(23);
406
+ if (!COMPATIBLE_ID_DID_PATTERN.test(tail)) {
407
+ if (rawPrefix != null) throw new TypeError("Invalid FEP-ef61 compatible identifier.");
408
+ return null;
409
+ }
410
+ if (url.username !== "" || url.password !== "") throw new TypeError("Invalid FEP-ef61 compatible identifier: credentials are not allowed.");
411
+ const iri = "ap+ef61://" + url.href.slice(url.origin.length + 23);
412
+ try {
413
+ const parsed = parsePortableIri(iri);
414
+ if (parsed == null) throw new TypeError("Not a portable IRI.");
415
+ if (typeof input === "string" && rawPrefix != null && canonicalizePortableUri("ap://" + input.slice(rawPrefix[0].length)) !== canonicalizePortableUri(iri)) throw new TypeError("URL parsing changed the portable identifier.");
416
+ canonicalizePortableUri(iri);
417
+ if ((url.search === "" ? [] : normalizePortableComponent(url.search.slice(1)).split("&")).some(isLocationHint)) throw new TypeError("Location hints are not allowed.");
418
+ return {
419
+ url: parsed,
420
+ iri
421
+ };
422
+ } catch (error) {
423
+ if (error instanceof TypeError) throw new TypeError("Invalid FEP-ef61 compatible identifier.", { cause: error });
424
+ throw error;
425
+ }
426
+ }
427
+ /**
428
+ * Converts a portable ActivityPub URI into an [FEP-ef61] compatible
429
+ * identifier, which is an HTTP(S) URL under the gateway's fixed
430
+ * `/.well-known/apgateway/` path.
431
+ *
432
+ * For example, `ap+ef61://did:key:z6Mk.../objects/1` and
433
+ * `https://server.example` yield
434
+ * `https://server.example/.well-known/apgateway/did:key:z6Mk.../objects/1`.
435
+ * Both `ap:` and `ap+ef61:` URIs with decoded or percent-encoded DID
436
+ * authorities are accepted. Publishers should use the first gateway in the
437
+ * actor's `gateways` list, as FEP-ef61 requires.
438
+ *
439
+ * The path and fragment are preserved, with characters that are not allowed
440
+ * in HTTP(S) URLs percent-encoded the same way as
441
+ * {@link canonicalizePortableUri} does. FEP-ef61 location hints (`@gateway`
442
+ * query parameters, and the legacy `gateways` parameter) are removed, since
443
+ * compatible identifiers must not have them; other query parameters are kept
444
+ * in order.
445
+ *
446
+ * Arbitrary gateway paths are not supported.
447
+ *
448
+ * [FEP-ef61]: https://w3id.org/fep/ef61
449
+ *
450
+ * @param portableId The `ap:` or `ap+ef61:` URI to convert.
451
+ * @param gateway The gateway's HTTP(S) origin, e.g., `https://server.example`.
452
+ * @returns The compatible identifier.
453
+ * @throws {TypeError} If the portable ID is not a valid `ap:` or `ap+ef61:`
454
+ * URI, if its path has `.` or `..` segments (which
455
+ * HTTP(S) URLs cannot represent without changing the
456
+ * identified object), or if the gateway is
457
+ * not an HTTP(S) origin with no credentials, path, query,
458
+ * or fragment.
459
+ * @since 2.4.0
460
+ */
461
+ function toCompatibleEf61Id(portableId, gateway) {
462
+ const gatewayUrl = parseCompatibleEf61Gateway(gateway);
463
+ const raw = getRawPortableIri(portableId);
464
+ const match = raw.match(PORTABLE_IRI_PATTERN);
465
+ const parsed = parsePortableIri(raw);
466
+ if (match == null || parsed == null) throw new TypeError("Invalid portable ActivityPub IRI.");
467
+ let did = decodePortableAuthority(parsed.host);
468
+ if (/%25/i.test(did)) did = did.replace(/%/g, "%25");
469
+ const path = normalizePortableComponent(match[3]);
470
+ if (path.split("/").some((segment) => segment === "." || segment === "..")) throw new TypeError("FEP-ef61 compatible identifiers cannot represent portable IRI paths with dot segments.");
471
+ const query = match[4] == null ? "" : stripLocationHints(normalizePortableComponent(match[4].slice(1)));
472
+ const fragment = match[5] == null ? "" : normalizePortableComponent(match[5]);
473
+ const result = new URL(gatewayUrl.origin + COMPATIBLE_ID_PATH_PREFIX + did + path + query + fragment);
474
+ const converted = convertCompatibleEf61Id(result);
475
+ if (converted == null || canonicalizePortableUri(converted.iri) !== canonicalizePortableUri(raw)) throw new TypeError("The portable ActivityPub IRI cannot be represented as an FEP-ef61 compatible identifier.");
476
+ return result;
477
+ }
478
+ function parseCompatibleEf61Gateway(gateway) {
479
+ const url = gateway instanceof URL ? gateway : typeof gateway === "string" && URL.canParse(gateway) ? new URL(gateway) : null;
480
+ if (url == null || !isGatewayUrl(url)) throw new TypeError("FEP-ef61 gateways for compatible identifiers must be HTTP(S) origins with no credentials, path, query, or fragment.");
481
+ return url;
482
+ }
483
+ function getRawPortableIri(portableId) {
484
+ if (portableId instanceof URL) {
485
+ if (portableId.protocol !== "ap:" && portableId.protocol !== "ap+ef61:") throw new TypeError("Invalid portable ActivityPub IRI.");
486
+ if (portableId.username !== "" || portableId.password !== "" || portableId.port !== "") throw new TypeError("Invalid portable ActivityPub IRI authority.");
487
+ return portableId.href;
488
+ }
489
+ if (typeof portableId !== "string") throw new TypeError("Invalid portable ActivityPub IRI.");
490
+ return portableId;
491
+ }
492
+ function stripLocationHints(query) {
493
+ const pairs = query.split("&").filter((pair) => !isLocationHint(pair));
494
+ return pairs.length < 1 ? "" : `?${pairs.join("&")}`;
495
+ }
496
+ function isLocationHint(pair) {
497
+ const name = pair.split("=", 1)[0].replace(/\+/g, " ");
498
+ try {
499
+ return LOCATION_HINT_PARAMETERS.has(decodeURIComponent(name));
500
+ } catch (error) {
501
+ if (error instanceof URIError) return false;
502
+ throw error;
503
+ }
504
+ }
505
+ /**
506
+ * The name of the FEP-ef61 location hint query parameter.
507
+ * @internal
508
+ */
509
+ const GATEWAY_HINT_PARAMETER = "@gateway";
510
+ /**
511
+ * Parses an FEP-ef61 gateway, which has to be an HTTP(S) origin with no
512
+ * credentials, path, query, or fragment.
513
+ * @returns The gateway, or `null` if it is not a valid gateway.
514
+ * @internal
515
+ */
516
+ function parseGatewayOrigin(gateway) {
517
+ let url;
518
+ if (gateway instanceof URL) url = new URL(gateway.href);
519
+ else if (typeof gateway === "string" && URL.canParse(gateway)) url = new URL(gateway);
520
+ else return null;
521
+ return isGatewayUrl(url) ? url : null;
522
+ }
523
+ /**
524
+ * Returns a copy of an [FEP-ef61] portable ActivityPub URI with `@gateway`
525
+ * location hints for the given gateways, which tell consumers where they can
526
+ * retrieve the object. Put hints on *references* to portable actors, e.g.,
527
+ * in `actor`, `attributedTo`, `to`, or `cc`, when constructing an object, as
528
+ * FEP-ef61 recommends:
529
+ *
530
+ * ~~~~ typescript
531
+ * withGatewayHints("ap://did:key:z6Mk.../actor", [
532
+ * "https://server1.example",
533
+ * "https://server2.example",
534
+ * ]);
535
+ * // ap+ef61://did:key:z6Mk.../actor?@gateway=https%3A%2F%2Fserver1.example&@gateway=https%3A%2F%2Fserver2.example
536
+ * ~~~~
537
+ *
538
+ * Do not put hints on an object's own `id`. Hints do not change the
539
+ * identity of a portable URI, since FEP-ef61 drops the query when comparing
540
+ * portable URIs, but implementations that do not canonicalize portable URIs
541
+ * would take a hinted ID for another object. Add hints before signing the
542
+ * object, since its Object Integrity Proof covers its references too.
543
+ *
544
+ * The hints that the URI already has, including the legacy `gateways`
545
+ * parameter, are replaced. Each gateway becomes a `@gateway` query
546
+ * parameter whose value is its URI-encoded origin, e.g.,
547
+ * `@gateway=https%3A%2F%2Fserver1.example`, in the given order after the
548
+ * other query parameters. Duplicate gateways are dropped, and an empty list
549
+ * removes the hints as {@link withoutGatewayHints} does. The other query
550
+ * parameters, their order, and the fragment are kept, but percent-encoding
551
+ * is normalized the same way as {@link canonicalizePortableUri} does it.
552
+ *
553
+ * Fedify follows at most five hints when dereferencing a portable URI
554
+ * (three for the key ID of an HTTP Signature), so list the preferred
555
+ * gateways first; there is no limit on the number of hints added here.
556
+ *
557
+ * [FEP-ef61]: https://w3id.org/fep/ef61
558
+ *
559
+ * @param portableId The `ap:` or `ap+ef61:` URI. Pass the raw string rather
560
+ * than a `URL` if its path may have `.` or `..` segments,
561
+ * because the `URL` class resolves them.
562
+ * @param gateways The gateways, e.g., the `gateways` of the actor that the
563
+ * URI refers to. Each has to be an HTTP(S) origin with no
564
+ * credentials, path, query, or fragment.
565
+ * @returns The portable URI with the hints, in the same internal `URL` form
566
+ * as {@link parseIri} returns. Use {@link formatIri} to get its
567
+ * canonical string.
568
+ * @throws {TypeError} If the portable ID is not a valid `ap:` or `ap+ef61:`
569
+ * URI, e.g., it is a compatible identifier, which must
570
+ * not have location hints; if its path has `.` or `..`
571
+ * segments, which the `URL` class cannot represent; or
572
+ * if a gateway is invalid.
573
+ * @since 2.4.0
574
+ */
575
+ function withGatewayHints(portableId, gateways) {
576
+ const parts = splitPortableIri(portableId);
577
+ if (typeof gateways === "string") throw new TypeError("The gateways must be an iterable of gateways, not a string.");
578
+ const hints = [];
579
+ const seen = /* @__PURE__ */ new Set();
580
+ for (const gateway of gateways) {
581
+ const url = parseGatewayOrigin(gateway);
582
+ if (url == null) throw new TypeError("FEP-ef61 gateways must be HTTP(S) origins with no credentials, path, query, or fragment: " + String(gateway));
583
+ if (seen.has(url.href)) continue;
584
+ seen.add(url.href);
585
+ hints.push(url);
586
+ }
587
+ return replaceGatewayHints(parts, hints);
588
+ }
589
+ /**
590
+ * Returns a copy of an [FEP-ef61] portable ActivityPub URI without its
591
+ * location hints, i.e., `@gateway` query parameters and the legacy
592
+ * `gateways` parameter. The other query parameters, their order, and the
593
+ * fragment are kept, but percent-encoding is normalized the same way as
594
+ * {@link canonicalizePortableUri} does it.
595
+ *
596
+ * [FEP-ef61]: https://w3id.org/fep/ef61
597
+ *
598
+ * @param portableId The `ap:` or `ap+ef61:` URI. Pass the raw string rather
599
+ * than a `URL` if its path may have `.` or `..` segments,
600
+ * because the `URL` class resolves them.
601
+ * @returns The portable URI without the hints, in the same internal `URL`
602
+ * form as {@link parseIri} returns.
603
+ * @throws {TypeError} If the portable ID is not a valid `ap:` or `ap+ef61:`
604
+ * URI, or if its path has `.` or `..` segments, which
605
+ * the `URL` class cannot represent.
606
+ * @since 2.4.0
607
+ */
608
+ function withoutGatewayHints(portableId) {
609
+ return replaceGatewayHints(splitPortableIri(portableId), []);
610
+ }
611
+ /**
612
+ * Gets the gateways in the `@gateway` location hints of an [FEP-ef61]
613
+ * portable ActivityPub URI, in order. Hints that are not valid gateways,
614
+ * i.e., HTTP(S) origins with no credentials, path, query, or fragment, are
615
+ * skipped, and so are duplicates. The legacy `gateways` parameter is not
616
+ * read.
617
+ *
618
+ * Unlike Fedify's dereferencing, which follows at most five hints, this
619
+ * returns all of them.
620
+ *
621
+ * [FEP-ef61]: https://w3id.org/fep/ef61
622
+ *
623
+ * @param portableId The `ap:` or `ap+ef61:` URI.
624
+ * @returns The gateways, e.g., `https://server1.example/`.
625
+ * @throws {TypeError} If the portable ID is not a valid `ap:` or `ap+ef61:`
626
+ * URI.
627
+ * @since 2.4.0
628
+ */
629
+ function getGatewayHints(portableId) {
630
+ const { query } = splitPortableIri(portableId);
631
+ return query == null ? [] : parseGatewayHints(query);
632
+ }
633
+ function splitPortableIri(portableId) {
634
+ const raw = getRawPortableIri(portableId);
635
+ const match = raw.match(PORTABLE_IRI_PATTERN);
636
+ const parsed = parsePortableIri(raw);
637
+ if (match == null || parsed == null) throw new TypeError("Invalid portable ActivityPub IRI.");
638
+ return {
639
+ raw,
640
+ parsed,
641
+ path: normalizePortableComponent(match[3]),
642
+ query: match[4] == null ? null : normalizePortableComponent(match[4].slice(1)),
643
+ fragment: match[5] == null ? "" : normalizePortableComponent(match[5])
644
+ };
645
+ }
646
+ function parseGatewayHints(query) {
647
+ const hints = [];
648
+ const seen = /* @__PURE__ */ new Set();
649
+ for (const hint of new URLSearchParams(`?${query}`).getAll(GATEWAY_HINT_PARAMETER)) {
650
+ const url = parseGatewayOrigin(hint);
651
+ if (url == null || seen.has(url.href)) continue;
652
+ seen.add(url.href);
653
+ hints.push(url);
654
+ }
655
+ return hints;
656
+ }
657
+ function replaceGatewayHints(parts, hints) {
658
+ const pairs = parts.query == null ? [] : parts.query.split("&").filter((pair) => pair !== "" && !isLocationHint(pair));
659
+ for (const hint of hints) pairs.push(`${GATEWAY_HINT_PARAMETER}=${encodeURIComponent(hint.origin)}`);
660
+ const query = pairs.length < 1 ? "" : `?${pairs.join("&")}`;
661
+ const result = parsePortableIri(`ap+ef61://${parts.parsed.host}${parts.path}${query}${parts.fragment}`);
662
+ if (result == null || canonicalizePortableUri(result.href) !== canonicalizePortableUri(parts.raw) || parseGatewayHints(result.search.slice(1)).map((url) => url.href).join(" ") !== hints.map((url) => url.href).join(" ")) throw new TypeError("The portable ActivityPub IRI cannot be represented as a URL without changing the object it refers to.");
663
+ return result;
664
+ }
665
+ /**
666
+ * Validates a URL to prevent SSRF attacks.
667
+ */
668
+ async function validatePublicUrl(url) {
669
+ const parsed = new URL(url);
670
+ if (parsed.protocol !== "http:" && parsed.protocol !== "https:") throw new UrlError(`Unsupported protocol: ${parsed.protocol}`);
671
+ let hostname = parsed.hostname;
672
+ if (hostname.startsWith("[") && hostname.endsWith("]")) hostname = hostname.slice(1, -1);
673
+ if (hostname === "localhost") throw new UrlError("Localhost is not allowed");
674
+ const hostnameFamily = (0, node_net.isIP)(hostname);
675
+ if (hostnameFamily !== 0) {
676
+ validatePublicIpAddress(hostname, hostnameFamily);
677
+ return;
678
+ }
679
+ if ("Deno" in globalThis && !(0, node_net.isIP)(hostname)) {
680
+ if ((await Deno.permissions.query({ name: "net" })).state !== "granted") return;
681
+ }
682
+ if ("Bun" in globalThis) {
683
+ if (hostname === "example.com" || hostname.endsWith(".example.com")) return;
684
+ else if (hostname === "fedify-test.internal") throw new UrlError("Invalid or private address: fedify-test.internal");
685
+ }
686
+ let addresses;
687
+ try {
688
+ addresses = await node_dns_promises.default.lookup(hostname, { all: true });
689
+ } catch (error) {
690
+ throw new UrlError("DNS lookup failed", {
691
+ cause: error,
692
+ reason: "dns"
693
+ });
694
+ }
695
+ validateLookupAddresses(addresses);
696
+ }
697
+ /**
698
+ * Validates the IP addresses returned by `node:dns.lookup()`.
699
+ *
700
+ * Cloudflare Workers' `node:dns` implementation currently maps every record
701
+ * in a DNS-over-HTTPS `Answer` array—including CNAME records—to a
702
+ * `LookupAddress`, even though Node.js specifies that the `address` field must
703
+ * contain an IPv4 or IPv6 literal. See:
704
+ * https://github.com/cloudflare/workerd/issues/6886
705
+ *
706
+ * Work around that bug by ignoring non-IP entries only when the lookup also
707
+ * returns at least one actual IP address. This remains fail-closed: a result
708
+ * containing no IP addresses is rejected, and every returned IP address is
709
+ * still validated and must be public. This workaround can be revisited once
710
+ * the Workerd issue is fixed in supported Cloudflare Workers runtimes.
711
+ *
712
+ * @internal
713
+ */
714
+ function validateLookupAddresses(addresses) {
715
+ let ipAddressCount = 0;
716
+ for (const { address } of addresses) {
717
+ const family = (0, node_net.isIP)(address);
718
+ if (family === 0) continue;
719
+ ipAddressCount++;
720
+ validatePublicIpAddress(address, family);
721
+ }
722
+ if (ipAddressCount === 0) throw new UrlError("DNS lookup did not return any IP address", { reason: "dns" });
723
+ }
724
+ function validatePublicIpAddress(address, family) {
725
+ if (family === 4 && isValidPublicIPv4Address(address) || family === 6 && isValidPublicIPv6Address(address)) return;
726
+ throw new UrlError(`Invalid or private address: ${address}`);
727
+ }
728
+ function isValidPublicIPv4Address(address) {
729
+ const parts = parseIPv4Address(address);
730
+ if (parts == null) return false;
731
+ const value = ipv4PartsToNumber(parts);
732
+ return !nonPublicIPv4Prefixes.some(({ base, prefix }) => matchesIPv4Prefix(value, base, prefix));
733
+ }
734
+ function isValidPublicIPv6Address(address) {
735
+ const words = parseIPv6Address(address);
736
+ if (words == null) return false;
737
+ if (nonPublicIPv6Prefixes.some(({ words: prefixWords, prefix }) => matchesIPv6Prefix(words, prefixWords, prefix))) return false;
738
+ for (const { extractIPv4, prefix, words: prefixWords } of ipv6WithIPv4Prefixes) {
739
+ if (!matchesIPv6Prefix(words, prefixWords, prefix)) continue;
740
+ const ipv4Address = extractIPv4(words);
741
+ if (ipv4Address != null && !isValidPublicIPv4Address(ipv4Address)) return false;
742
+ }
743
+ return true;
744
+ }
745
+ function expandIPv6Address(address) {
746
+ address = address.toLowerCase();
747
+ const ipv4Delimiter = address.lastIndexOf(":");
748
+ if (address.includes(".") && ipv4Delimiter >= 0) {
749
+ const ipv4Parts = parseIPv4Address(address.substring(ipv4Delimiter + 1));
750
+ if (ipv4Parts == null) return address;
751
+ const high = (ipv4Parts[0] << 8) + ipv4Parts[1];
752
+ const low = (ipv4Parts[2] << 8) + ipv4Parts[3];
753
+ address = address.substring(0, ipv4Delimiter + 1) + high.toString(16) + ":" + low.toString(16);
754
+ }
755
+ if (address === "::") return "0000:0000:0000:0000:0000:0000:0000:0000";
756
+ if (address.startsWith("::")) address = "0000" + address;
757
+ if (address.endsWith("::")) address = address + "0000";
758
+ address = address.replace("::", ":0000".repeat(8 - (address.match(/:/g) || []).length) + ":");
759
+ return address.split(":").map((part) => part.padStart(4, "0")).join(":");
760
+ }
761
+ const nonPublicIPv4Prefixes = [
762
+ ipv4Prefix("0.0.0.0/8", "RFC 6890"),
763
+ ipv4Prefix("10.0.0.0/8", "RFC 1918"),
764
+ ipv4Prefix("100.64.0.0/10", "RFC 6598"),
765
+ ipv4Prefix("127.0.0.0/8", "RFC 1122"),
766
+ ipv4Prefix("169.254.0.0/16", "RFC 3927"),
767
+ ipv4Prefix("172.16.0.0/12", "RFC 1918"),
768
+ ipv4Prefix("192.0.0.0/24", "RFC 6890"),
769
+ ipv4Prefix("192.0.2.0/24", "RFC 5737"),
770
+ ipv4Prefix("192.88.99.0/24", "RFC 7526"),
771
+ ipv4Prefix("192.168.0.0/16", "RFC 1918"),
772
+ ipv4Prefix("198.18.0.0/15", "RFC 2544"),
773
+ ipv4Prefix("198.51.100.0/24", "RFC 5737"),
774
+ ipv4Prefix("203.0.113.0/24", "RFC 5737"),
775
+ ipv4Prefix("224.0.0.0/4", "RFC 5771"),
776
+ ipv4Prefix("240.0.0.0/4", "RFC 1112")
777
+ ];
778
+ const nonPublicIPv6Prefixes = [
779
+ ipv6Prefix("::/16", "RFC 4291"),
780
+ ipv6Prefix("2001::/32", "RFC 4380"),
781
+ ipv6Prefix("2002::/16", "RFC 3056"),
782
+ ipv6Prefix("64:ff9b:1::/48", "RFC 8215"),
783
+ ipv6Prefix("fc00::/7", "RFC 4193"),
784
+ ipv6Prefix("fe80::/10", "RFC 4291"),
785
+ ipv6Prefix("ff00::/8", "RFC 4291")
786
+ ];
787
+ const ipv6WithIPv4Prefixes = [{
788
+ ...ipv6Prefix("64:ff9b::/96", "RFC 6052"),
789
+ extractIPv4: (words) => ipv4FromWords(words[6], words[7])
790
+ }];
791
+ function ipv4Prefix(cidr, rfc) {
792
+ const [address, prefixText] = cidr.split("/");
793
+ const prefix = parseInt(prefixText, 10);
794
+ const parts = parseIPv4Address(address);
795
+ if (parts == null || !Number.isInteger(prefix) || prefix < 0 || prefix > 32) throw new Error(`Invalid IPv4 prefix: ${cidr}`);
796
+ return {
797
+ cidr,
798
+ base: ipv4PartsToNumber(parts),
799
+ prefix,
800
+ rfc
801
+ };
802
+ }
803
+ function ipv6Prefix(cidr, rfc) {
804
+ const [address, prefixText] = cidr.split("/");
805
+ const prefix = parseInt(prefixText, 10);
806
+ const words = parseIPv6Address(address);
807
+ if (words == null || !Number.isInteger(prefix) || prefix < 0 || prefix > 128) throw new Error(`Invalid IPv6 prefix: ${cidr}`);
808
+ return {
809
+ cidr,
810
+ words,
811
+ prefix,
812
+ rfc
813
+ };
814
+ }
815
+ function parseIPv4Address(address) {
816
+ const parts = address.split(".").map((part) => {
817
+ if (!/^\d+$/.test(part)) return NaN;
818
+ return parseInt(part, 10);
819
+ });
820
+ if (parts.length !== 4 || parts.some((part) => !Number.isInteger(part) || part < 0 || part > 255)) return null;
821
+ return parts;
822
+ }
823
+ function parseIPv6Address(address) {
824
+ const parts = expandIPv6Address(address).split(":");
825
+ if (parts.length !== 8) return null;
826
+ const words = parts.map((part) => {
827
+ if (!/^[0-9a-f]{1,4}$/i.test(part)) return NaN;
828
+ return parseInt(part, 16);
829
+ });
830
+ if (words.some((word) => !Number.isInteger(word) || word < 0 || word > 65535)) return null;
831
+ return words;
832
+ }
833
+ function ipv4PartsToNumber(parts) {
834
+ return parts[0] * 2 ** 24 + parts[1] * 2 ** 16 + parts[2] * 2 ** 8 + parts[3];
835
+ }
836
+ function ipv4FromWords(highWord, lowWord) {
837
+ return [
838
+ highWord >> 8,
839
+ highWord & 255,
840
+ lowWord >> 8,
841
+ lowWord & 255
842
+ ].join(".");
843
+ }
844
+ function matchesIPv4Prefix(address, prefixBase, prefixLength) {
845
+ const blockSize = 2 ** (32 - prefixLength);
846
+ return Math.floor(address / blockSize) === Math.floor(prefixBase / blockSize);
847
+ }
848
+ function matchesIPv6Prefix(address, prefixWords, prefixLength) {
849
+ let remaining = prefixLength;
850
+ for (let i = 0; i < 8 && remaining > 0; i++) if (remaining >= 16) {
851
+ if (address[i] !== prefixWords[i]) return false;
852
+ remaining -= 16;
853
+ } else {
854
+ const mask = 65535 << 16 - remaining & 65535;
855
+ if ((address[i] & mask) !== (prefixWords[i] & mask)) return false;
856
+ remaining = 0;
857
+ }
858
+ return true;
859
+ }
860
+ //#endregion
861
+ Object.defineProperty(exports, "GATEWAY_HINT_PARAMETER", {
862
+ enumerable: true,
863
+ get: function() {
864
+ return GATEWAY_HINT_PARAMETER;
865
+ }
866
+ });
867
+ Object.defineProperty(exports, "UrlError", {
868
+ enumerable: true,
869
+ get: function() {
870
+ return UrlError;
871
+ }
872
+ });
873
+ Object.defineProperty(exports, "arePortableUrisEqual", {
874
+ enumerable: true,
875
+ get: function() {
876
+ return arePortableUrisEqual;
877
+ }
878
+ });
879
+ Object.defineProperty(exports, "canonicalizePortableUri", {
880
+ enumerable: true,
881
+ get: function() {
882
+ return canonicalizePortableUri;
883
+ }
884
+ });
885
+ Object.defineProperty(exports, "expandIPv6Address", {
886
+ enumerable: true,
887
+ get: function() {
888
+ return expandIPv6Address;
889
+ }
890
+ });
891
+ Object.defineProperty(exports, "formatIri", {
892
+ enumerable: true,
893
+ get: function() {
894
+ return formatIri;
895
+ }
896
+ });
897
+ Object.defineProperty(exports, "fromCompatibleEf61Id", {
898
+ enumerable: true,
899
+ get: function() {
900
+ return fromCompatibleEf61Id;
901
+ }
902
+ });
903
+ Object.defineProperty(exports, "getFe34Origin", {
904
+ enumerable: true,
905
+ get: function() {
906
+ return getFe34Origin;
907
+ }
908
+ });
909
+ Object.defineProperty(exports, "getGatewayHints", {
910
+ enumerable: true,
911
+ get: function() {
912
+ return getGatewayHints;
913
+ }
914
+ });
915
+ Object.defineProperty(exports, "haveSameFe34Origin", {
916
+ enumerable: true,
917
+ get: function() {
918
+ return haveSameFe34Origin;
919
+ }
920
+ });
921
+ Object.defineProperty(exports, "haveSameIriOrigin", {
922
+ enumerable: true,
923
+ get: function() {
924
+ return haveSameIriOrigin;
925
+ }
926
+ });
927
+ Object.defineProperty(exports, "isGatewayUrl", {
928
+ enumerable: true,
929
+ get: function() {
930
+ return isGatewayUrl;
931
+ }
932
+ });
933
+ Object.defineProperty(exports, "isValidPublicIPv4Address", {
934
+ enumerable: true,
935
+ get: function() {
936
+ return isValidPublicIPv4Address;
937
+ }
938
+ });
939
+ Object.defineProperty(exports, "isValidPublicIPv6Address", {
940
+ enumerable: true,
941
+ get: function() {
942
+ return isValidPublicIPv6Address;
943
+ }
944
+ });
945
+ Object.defineProperty(exports, "parseGatewayOrigin", {
946
+ enumerable: true,
947
+ get: function() {
948
+ return parseGatewayOrigin;
949
+ }
950
+ });
951
+ Object.defineProperty(exports, "parseGatewayUrl", {
952
+ enumerable: true,
953
+ get: function() {
954
+ return parseGatewayUrl;
955
+ }
956
+ });
957
+ Object.defineProperty(exports, "parseIri", {
958
+ enumerable: true,
959
+ get: function() {
960
+ return parseIri;
961
+ }
962
+ });
963
+ Object.defineProperty(exports, "parseJsonLdId", {
964
+ enumerable: true,
965
+ get: function() {
966
+ return parseJsonLdId;
967
+ }
968
+ });
969
+ Object.defineProperty(exports, "toCompatibleEf61Id", {
970
+ enumerable: true,
971
+ get: function() {
972
+ return toCompatibleEf61Id;
973
+ }
974
+ });
975
+ Object.defineProperty(exports, "validateLookupAddresses", {
976
+ enumerable: true,
977
+ get: function() {
978
+ return validateLookupAddresses;
979
+ }
980
+ });
981
+ Object.defineProperty(exports, "validatePublicUrl", {
982
+ enumerable: true,
983
+ get: function() {
984
+ return validatePublicUrl;
985
+ }
986
+ });
987
+ Object.defineProperty(exports, "withGatewayHints", {
988
+ enumerable: true,
989
+ get: function() {
990
+ return withGatewayHints;
991
+ }
992
+ });
993
+ Object.defineProperty(exports, "withoutGatewayHints", {
994
+ enumerable: true,
995
+ get: function() {
996
+ return withoutGatewayHints;
997
+ }
998
+ });