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

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 (33) hide show
  1. package/deno.json +1 -1
  2. package/dist/internal/jsonld-cache.cjs +1 -1
  3. package/dist/internal/jsonld-cache.js +1 -1
  4. package/dist/mod.cjs +4 -2
  5. package/dist/mod.d.cts +74 -1
  6. package/dist/mod.d.ts +74 -1
  7. package/dist/mod.js +3 -3
  8. package/dist/tests/{body-C8W6PG-f.mjs → body-BEIDQFV6.mjs} +1 -1
  9. package/dist/tests/{body-CjqMU6be.cjs → body-DRpkDnEg.cjs} +1 -1
  10. package/dist/tests/body.test.cjs +1 -1
  11. package/dist/tests/body.test.mjs +1 -1
  12. package/dist/tests/decimal.test.cjs +2 -2
  13. package/dist/tests/decimal.test.mjs +2 -2
  14. package/dist/tests/{docloader-BcthkJvx.mjs → docloader-DHux1ztB.mjs} +3 -3
  15. package/dist/tests/{docloader-DDwe-rFE.cjs → docloader-M8t8o-GD.cjs} +3 -3
  16. package/dist/tests/docloader.test.cjs +3 -3
  17. package/dist/tests/docloader.test.mjs +3 -3
  18. package/dist/tests/jsonld-cache.test.cjs +1 -1
  19. package/dist/tests/jsonld-cache.test.mjs +1 -1
  20. package/dist/tests/{request-DV2_yz3V.cjs → request-BbczASfn.cjs} +1 -1
  21. package/dist/tests/{request-CeEwiwiU.mjs → request-eP_YLVjb.mjs} +1 -1
  22. package/dist/tests/request.test.cjs +1 -1
  23. package/dist/tests/request.test.mjs +1 -1
  24. package/dist/{url-i33c0US6.cjs → tests/url-BqlldzT8.cjs} +164 -1
  25. package/dist/tests/{url-Basm1L_c.mjs → url-DgJM9WJn.mjs} +147 -1
  26. package/dist/tests/url.test.cjs +220 -1
  27. package/dist/tests/url.test.mjs +220 -1
  28. package/dist/{url-BHAShw9J.js → url-CEhi4hb4.js} +147 -1
  29. package/dist/{tests/url-CPfMatUL.cjs → url-Cke4irYD.cjs} +155 -2
  30. package/package.json +2 -2
  31. package/src/mod.ts +2 -0
  32. package/src/url.test.ts +361 -0
  33. package/src/url.ts +235 -0
@@ -223,6 +223,152 @@ function parseGatewayUrl(url) {
223
223
  if (!isGatewayUrl(parsed)) throw new TypeError("FEP-ef61 gateways must be HTTP(S) base URIs with no credentials, path, query, or fragment.");
224
224
  return parsed;
225
225
  }
226
+ const COMPATIBLE_ID_PATH_PREFIX = "/.well-known/apgateway/";
227
+ const COMPATIBLE_ID_DID_PATTERN = /^did(?::|%3A)/i;
228
+ const LOCATION_HINT_PARAMETERS = /* @__PURE__ */ new Set(["@gateway", "gateways"]);
229
+ /**
230
+ * Converts an [FEP-ef61] compatible identifier into a portable ActivityPub
231
+ * URI.
232
+ *
233
+ * A compatible identifier is an HTTP(S) URL under a gateway's fixed
234
+ * `/.well-known/apgateway/` path, such as
235
+ * `https://server.example/.well-known/apgateway/did:key:z6Mk.../objects/1`.
236
+ * This function removes the gateway part and returns the corresponding
237
+ * portable URI, e.g., `ap+ef61://did:key:z6Mk.../objects/1`, in the same
238
+ * internal `URL` form that {@link parseIri} produces. The path, query, and
239
+ * fragment are preserved, so the result is a portable URI, not a comparison
240
+ * form; pass its `href` to {@link canonicalizePortableUri} or
241
+ * {@link arePortableUrisEqual} to compare it with other portable URIs.
242
+ *
243
+ * The conversion only reveals the *claimed* portable identifier. Anyone can
244
+ * publish a compatible identifier for any DID on their own server, so the
245
+ * gateway that served it is neither the object's origin nor authorized to act
246
+ * for the DID. Callers must still verify the retrieved document's Object
247
+ * Integrity Proof against the DID, as FEP-ef61 requires, and should keep the
248
+ * original URL if they need the gateway as a retrieval hint.
249
+ *
250
+ * Arbitrary gateway paths are not supported.
251
+ *
252
+ * [FEP-ef61]: https://w3id.org/fep/ef61
253
+ *
254
+ * @param input The URL to convert.
255
+ * @returns The portable ActivityPub URI, or `null` if the input is not an
256
+ * HTTP(S) URL whose path starts with `/.well-known/apgateway/did:`.
257
+ * Other gateway routes, such as the gateway discovery endpoint and
258
+ * hashlink media URLs, also yield `null`.
259
+ * @throws {TypeError} If the input looks like a compatible identifier but is
260
+ * malformed, e.g., it has an invalid DID, no object path,
261
+ * invalid percent-encoding, credentials, or location
262
+ * hints (`@gateway` query parameters, or the legacy
263
+ * `gateways` parameter), which FEP-ef61 forbids in
264
+ * compatible identifiers.
265
+ * @since 2.4.0
266
+ */
267
+ function fromCompatibleEf61Id(input) {
268
+ return convertCompatibleEf61Id(input)?.url ?? null;
269
+ }
270
+ function convertCompatibleEf61Id(input) {
271
+ let url;
272
+ if (input instanceof URL) url = input;
273
+ else if (typeof input === "string" && URL.canParse(input)) url = new URL(input);
274
+ else return null;
275
+ if (url.protocol !== "http:" && url.protocol !== "https:") return null;
276
+ if (!url.pathname.startsWith(COMPATIBLE_ID_PATH_PREFIX)) return null;
277
+ const tail = url.pathname.slice(23);
278
+ if (!COMPATIBLE_ID_DID_PATTERN.test(tail)) return null;
279
+ if (url.username !== "" || url.password !== "") throw new TypeError("Invalid FEP-ef61 compatible identifier: credentials are not allowed.");
280
+ const iri = "ap+ef61://" + url.href.slice(url.origin.length + 23);
281
+ try {
282
+ const parsed = parsePortableIri(iri);
283
+ if (parsed == null) throw new TypeError("Not a portable IRI.");
284
+ canonicalizePortableUri(iri);
285
+ if ((url.search === "" ? [] : normalizePortableComponent(url.search.slice(1)).split("&")).some(isLocationHint)) throw new TypeError("Location hints are not allowed.");
286
+ return {
287
+ url: parsed,
288
+ iri
289
+ };
290
+ } catch (error) {
291
+ if (error instanceof TypeError) throw new TypeError("Invalid FEP-ef61 compatible identifier.", { cause: error });
292
+ throw error;
293
+ }
294
+ }
295
+ /**
296
+ * Converts a portable ActivityPub URI into an [FEP-ef61] compatible
297
+ * identifier, which is an HTTP(S) URL under the gateway's fixed
298
+ * `/.well-known/apgateway/` path.
299
+ *
300
+ * For example, `ap+ef61://did:key:z6Mk.../objects/1` and
301
+ * `https://server.example` yield
302
+ * `https://server.example/.well-known/apgateway/did:key:z6Mk.../objects/1`.
303
+ * Both `ap:` and `ap+ef61:` URIs with decoded or percent-encoded DID
304
+ * authorities are accepted. Publishers should use the first gateway in the
305
+ * actor's `gateways` list, as FEP-ef61 requires.
306
+ *
307
+ * The path and fragment are preserved, with characters that are not allowed
308
+ * in HTTP(S) URLs percent-encoded the same way as
309
+ * {@link canonicalizePortableUri} does. FEP-ef61 location hints (`@gateway`
310
+ * query parameters, and the legacy `gateways` parameter) are removed, since
311
+ * compatible identifiers must not have them; other query parameters are kept
312
+ * in order.
313
+ *
314
+ * Arbitrary gateway paths are not supported.
315
+ *
316
+ * [FEP-ef61]: https://w3id.org/fep/ef61
317
+ *
318
+ * @param portableId The `ap:` or `ap+ef61:` URI to convert.
319
+ * @param gateway The gateway's HTTP(S) origin, e.g., `https://server.example`.
320
+ * @returns The compatible identifier.
321
+ * @throws {TypeError} If the portable ID is not a valid `ap:` or `ap+ef61:`
322
+ * URI, if its path has `.` or `..` segments (which
323
+ * HTTP(S) URLs cannot represent), or if the gateway is
324
+ * not an HTTP(S) origin with no credentials, path, query,
325
+ * or fragment.
326
+ * @since 2.4.0
327
+ */
328
+ function toCompatibleEf61Id(portableId, gateway) {
329
+ const gatewayUrl = parseCompatibleEf61Gateway(gateway);
330
+ const raw = getRawPortableIri(portableId);
331
+ const match = raw.match(PORTABLE_IRI_PATTERN);
332
+ const parsed = parsePortableIri(raw);
333
+ if (match == null || parsed == null) throw new TypeError("Invalid portable ActivityPub IRI.");
334
+ let did = decodePortableAuthority(parsed.host);
335
+ if (/%25/i.test(did)) did = did.replace(/%/g, "%25");
336
+ const path = normalizePortableComponent(match[3]);
337
+ if (path.split("/").some((segment) => segment === "." || segment === "..")) throw new TypeError("FEP-ef61 compatible identifiers cannot represent portable IRI paths with dot segments.");
338
+ const query = match[4] == null ? "" : stripLocationHints(normalizePortableComponent(match[4].slice(1)));
339
+ const fragment = match[5] == null ? "" : normalizePortableComponent(match[5]);
340
+ const result = new URL(gatewayUrl.origin + COMPATIBLE_ID_PATH_PREFIX + did + path + query + fragment);
341
+ const converted = convertCompatibleEf61Id(result);
342
+ if (converted == null || canonicalizePortableUri(converted.iri) !== canonicalizePortableUri(raw)) throw new TypeError("The portable ActivityPub IRI cannot be represented as an FEP-ef61 compatible identifier.");
343
+ return result;
344
+ }
345
+ function parseCompatibleEf61Gateway(gateway) {
346
+ const url = gateway instanceof URL ? gateway : typeof gateway === "string" && URL.canParse(gateway) ? new URL(gateway) : null;
347
+ if (url == null || url.protocol !== "http:" && url.protocol !== "https:" || url.href !== `${url.origin}/`) throw new TypeError("FEP-ef61 gateways for compatible identifiers must be HTTP(S) origins with no credentials, path, query, or fragment.");
348
+ return url;
349
+ }
350
+ function getRawPortableIri(portableId) {
351
+ if (portableId instanceof URL) {
352
+ if (portableId.protocol !== "ap:" && portableId.protocol !== "ap+ef61:") throw new TypeError("Invalid portable ActivityPub IRI.");
353
+ if (portableId.username !== "" || portableId.password !== "" || portableId.port !== "") throw new TypeError("Invalid portable ActivityPub IRI authority.");
354
+ return portableId.href;
355
+ }
356
+ if (typeof portableId !== "string") throw new TypeError("Invalid portable ActivityPub IRI.");
357
+ return portableId;
358
+ }
359
+ function stripLocationHints(query) {
360
+ const pairs = query.split("&").filter((pair) => !isLocationHint(pair));
361
+ return pairs.length < 1 ? "" : `?${pairs.join("&")}`;
362
+ }
363
+ function isLocationHint(pair) {
364
+ const name = pair.split("=", 1)[0].replace(/\+/g, " ");
365
+ try {
366
+ return LOCATION_HINT_PARAMETERS.has(decodeURIComponent(name));
367
+ } catch (error) {
368
+ if (error instanceof URIError) return false;
369
+ throw error;
370
+ }
371
+ }
226
372
  /**
227
373
  * Validates a URL to prevent SSRF attacks.
228
374
  */
@@ -416,4 +562,4 @@ function matchesIPv6Prefix(address, prefixWords, prefixLength) {
416
562
  return true;
417
563
  }
418
564
  //#endregion
419
- export { formatIri as a, haveSameIriOrigin as c, isValidPublicIPv6Address as d, parseGatewayUrl as f, validatePublicUrl as g, validateLookupAddresses as h, expandIPv6Address as i, isGatewayUrl as l, parseJsonLdId as m, arePortableUrisEqual as n, getFe34Origin as o, parseIri as p, canonicalizePortableUri as r, haveSameFe34Origin as s, UrlError as t, isValidPublicIPv4Address as u };
565
+ export { validateLookupAddresses as _, formatIri as a, haveSameFe34Origin as c, isValidPublicIPv4Address as d, isValidPublicIPv6Address as f, toCompatibleEf61Id as g, parseJsonLdId as h, expandIPv6Address as i, haveSameIriOrigin as l, parseIri as m, arePortableUrisEqual as n, fromCompatibleEf61Id as o, parseGatewayUrl as p, canonicalizePortableUri as r, getFe34Origin as s, UrlError as t, isGatewayUrl as u, validatePublicUrl as v };
@@ -1,4 +1,4 @@
1
- const require_url = require("./url-CPfMatUL.cjs");
1
+ const require_url = require("./url-BqlldzT8.cjs");
2
2
  let node_test = require("node:test");
3
3
  let node_assert = require("node:assert");
4
4
  //#region src/url.test.ts
@@ -244,6 +244,225 @@ let node_assert = require("node:assert");
244
244
  (0, node_assert.ok)(!require_url.isGatewayUrl(new URL(url)));
245
245
  }
246
246
  });
247
+ (0, node_test.test)("fromCompatibleEf61Id() converts compatible identifiers", () => {
248
+ for (const [input, expected] of [
249
+ ["https://server.example/.well-known/apgateway/did:key:z6MkAlice/objects/1", "ap+ef61://did%3Akey%3Az6MkAlice/objects/1"],
250
+ ["http://server.example:8080/.well-known/apgateway/did:key:z6MkAlice/actor", "ap+ef61://did%3Akey%3Az6MkAlice/actor"],
251
+ ["https://server.example/.well-known/apgateway/did%3Akey%3Az6MkAlice/actor", "ap+ef61://did%3Akey%3Az6MkAlice/actor"],
252
+ ["https://server.example/.well-known/apgateway/DID:key:z6MkAlice/actor", "ap+ef61://did%3Akey%3Az6MkAlice/actor"],
253
+ ["https://server.example/.well-known/apgateway/did:key:z6MkAlice/a/b/c", "ap+ef61://did%3Akey%3Az6MkAlice/a/b/c"],
254
+ ["https://server.example/.well-known/apgateway/did:key:z6MkAlice/actor#main-key", "ap+ef61://did%3Akey%3Az6MkAlice/actor#main-key"],
255
+ ["https://server.example/.well-known/apgateway/did:key:z6MkAlice/actor#", "ap+ef61://did%3Akey%3Az6MkAlice/actor#"],
256
+ ["https://server.example/.well-known/apgateway/did:key:z6MkAlice/outbox?page=2", "ap+ef61://did%3Akey%3Az6MkAlice/outbox?page=2"],
257
+ ["https://server.example/.well-known/apgateway/did:key:z6MkAlice/a%2Fb", "ap+ef61://did%3Akey%3Az6MkAlice/a%2Fb"],
258
+ ["https://server.example/.well-known/apgateway/did:web:example.com%3A8080/actor", "ap+ef61://did%3Aweb%3Aexample.com%253A8080/actor"],
259
+ ["https://server.example/.well-known/apgateway/did:example:a%2525/x", "ap+ef61://did%3Aexample%3Aa%2525/x"]
260
+ ]) {
261
+ (0, node_assert.deepStrictEqual)(require_url.fromCompatibleEf61Id(input)?.href, expected, input);
262
+ (0, node_assert.deepStrictEqual)(require_url.fromCompatibleEf61Id(new URL(input))?.href, expected, input);
263
+ }
264
+ const converted = require_url.fromCompatibleEf61Id("https://server.example/.well-known/apgateway/did:key:z6MkAlice/objects/1");
265
+ (0, node_assert.deepStrictEqual)(require_url.formatIri(converted), "ap+ef61://did:key:z6MkAlice/objects/1");
266
+ (0, node_assert.deepStrictEqual)(require_url.canonicalizePortableUri(converted.href), "ap+ef61://did:key:z6MkAlice/objects/1");
267
+ (0, node_assert.ok)(require_url.arePortableUrisEqual(require_url.fromCompatibleEf61Id("https://a.example/.well-known/apgateway/did:key:z6MkAlice/actor").href, require_url.fromCompatibleEf61Id("https://b.example/.well-known/apgateway/did:key:z6MkAlice/actor").href));
268
+ });
269
+ (0, node_test.test)("fromCompatibleEf61Id() returns null for other URLs", () => {
270
+ for (const input of [
271
+ "https://server.example/users/alice",
272
+ "https://server.example/.well-known/webfinger?resource=acct:a@b",
273
+ "https://server.example/.well-known/apgateway",
274
+ "https://server.example/.well-known/apgateway/",
275
+ "https://server.example/.well-known/apgateway/hl:zQmdfTbBqBPQ7VNxZEYEj14VmRuZBkqFbiwReogJgS1zR1n",
276
+ "https://server.example/.well-known/apgateway/objects/did:key:z6MkAlice/x",
277
+ "https://server.example/.well-known/apgatewayx/did:key:z6MkAlice/x",
278
+ "https://server.example/ap/did:key:z6MkAlice/actor",
279
+ "https://server.example/?id=/.well-known/apgateway/did:key:z6MkAlice/x",
280
+ "ap://did:key:z6MkAlice/actor",
281
+ "ap+ef61://did:key:z6MkAlice/actor",
282
+ "ftp://server.example/.well-known/apgateway/did:key:z6MkAlice/actor",
283
+ "not a URL",
284
+ "/.well-known/apgateway/did:key:z6MkAlice/actor"
285
+ ]) (0, node_assert.deepStrictEqual)(require_url.fromCompatibleEf61Id(input), null, input);
286
+ (0, node_assert.deepStrictEqual)(require_url.fromCompatibleEf61Id(new URL("ap+ef61://did%3Akey%3Az6MkAlice/actor")), null);
287
+ (0, node_assert.deepStrictEqual)(require_url.fromCompatibleEf61Id(123), null);
288
+ });
289
+ (0, node_test.test)("fromCompatibleEf61Id() rejects malformed compatible identifiers", () => {
290
+ for (const input of [
291
+ "https://server.example/.well-known/apgateway/did:key:z6MkAlice",
292
+ "https://server.example/.well-known/apgateway/did:",
293
+ "https://server.example/.well-known/apgateway/did:key/actor",
294
+ "https://server.example/.well-known/apgateway/did:ke%y:z6MkAlice/actor",
295
+ "https://server.example/.well-known/apgateway/did:key:z6MkAlice/a%zz",
296
+ "https://server.example/.well-known/apgateway/did:key:z6MkAlice/a#%zz",
297
+ "https://server.example/.well-known/apgateway/did:key:z6MkAlice/a?x=%zz",
298
+ "https://server.example/.well-known/apgateway/did:key:z6MkAlice/a?@gateway=https%3A%2F%2Fevil.example",
299
+ "https://server.example/.well-known/apgateway/did:key:z6MkAlice/a?page=2&%40gateway=x",
300
+ "https://server.example/.well-known/apgateway/did:key:z6MkAlice/a?@%67ateway=x",
301
+ "https://server.example/.well-known/apgateway/did:key:z6MkAlice/a?gateways=x",
302
+ "https://server.example/.well-known/apgateway/did:example:a%25/x",
303
+ "https://user:pass@server.example/.well-known/apgateway/did:key:z6MkAlice/a",
304
+ "https://user@server.example/.well-known/apgateway/did:key:z6MkAlice/a"
305
+ ]) (0, node_assert.throws)(() => require_url.fromCompatibleEf61Id(input), TypeError, input);
306
+ });
307
+ (0, node_test.test)("toCompatibleEf61Id() converts portable URIs", () => {
308
+ const cases = [
309
+ [
310
+ "ap+ef61://did:key:z6MkAlice/objects/1",
311
+ "https://server.example",
312
+ "https://server.example/.well-known/apgateway/did:key:z6MkAlice/objects/1"
313
+ ],
314
+ [
315
+ "ap://did:key:z6MkAlice/objects/1",
316
+ "https://server.example/",
317
+ "https://server.example/.well-known/apgateway/did:key:z6MkAlice/objects/1"
318
+ ],
319
+ [
320
+ "ap://did%3Akey%3Az6MkAlice/actor",
321
+ new URL("http://server.example:8080"),
322
+ "http://server.example:8080/.well-known/apgateway/did:key:z6MkAlice/actor"
323
+ ],
324
+ [
325
+ new URL("ap+ef61://did%3Akey%3Az6MkAlice/actor"),
326
+ "https://server.example",
327
+ "https://server.example/.well-known/apgateway/did:key:z6MkAlice/actor"
328
+ ],
329
+ [
330
+ new URL("ap://did%3Akey%3Az6MkAlice/actor"),
331
+ "https://SERVER.example",
332
+ "https://server.example/.well-known/apgateway/did:key:z6MkAlice/actor"
333
+ ],
334
+ [
335
+ "ap://did:key:z6MkAlice/a/b/c",
336
+ "https://server.example",
337
+ "https://server.example/.well-known/apgateway/did:key:z6MkAlice/a/b/c"
338
+ ],
339
+ [
340
+ "ap://did:key:z6MkAlice/actor#main-key",
341
+ "https://server.example",
342
+ "https://server.example/.well-known/apgateway/did:key:z6MkAlice/actor#main-key"
343
+ ],
344
+ [
345
+ "ap://did:key:z6MkAlice/actor#",
346
+ "https://server.example",
347
+ "https://server.example/.well-known/apgateway/did:key:z6MkAlice/actor#"
348
+ ],
349
+ [
350
+ "ap://DID:key:z6MkAlice/actor",
351
+ "https://server.example",
352
+ "https://server.example/.well-known/apgateway/did:key:z6MkAlice/actor"
353
+ ],
354
+ [
355
+ "ap://did:example:a%2525/x",
356
+ "https://server.example",
357
+ "https://server.example/.well-known/apgateway/did:example:a%2525/x"
358
+ ],
359
+ [
360
+ "ap://did:web:example.com%3A8080/actor",
361
+ "https://server.example",
362
+ "https://server.example/.well-known/apgateway/did:web:example.com%3A8080/actor"
363
+ ],
364
+ [
365
+ "ap://did:key:z6MkAlice/a%2Fb/%252e",
366
+ "https://server.example",
367
+ "https://server.example/.well-known/apgateway/did:key:z6MkAlice/a%2Fb/%252e"
368
+ ],
369
+ [
370
+ "ap://did:key:z6MkAlice/a\\b c é/%7euser",
371
+ "https://server.example",
372
+ "https://server.example/.well-known/apgateway/did:key:z6MkAlice/a%5Cb%20c%09%C3%A9/~user"
373
+ ]
374
+ ];
375
+ for (const [portableId, gateway, expected] of cases) (0, node_assert.deepStrictEqual)(require_url.toCompatibleEf61Id(portableId, gateway).href, expected, String(portableId));
376
+ });
377
+ (0, node_test.test)("toCompatibleEf61Id() removes location hints from queries", () => {
378
+ const base = "https://server.example/.well-known/apgateway/did:key:z6MkAlice";
379
+ const cases = [
380
+ ["ap://did:key:z6MkAlice/actor?@gateway=https%3A%2F%2Fa.example&@gateway=https%3A%2F%2Fb.example", `${base}/actor`],
381
+ ["ap://did:key:z6MkAlice/actor?gateways=https%3A%2F%2Fa.example", `${base}/actor`],
382
+ ["ap://did:key:z6MkAlice/actor?%40gateway=https%3A%2F%2Fa.example", `${base}/actor`],
383
+ ["ap://did:key:z6MkAlice/actor?@%67ateway=https%3A%2F%2Fa.example", `${base}/actor`],
384
+ ["ap://did:key:z6MkAlice/actor?@gateway", `${base}/actor`],
385
+ ["ap://did:key:z6MkAlice/c?page=3&@gateway=https%3A%2F%2Fa.example&maxItems=20&page=4", `${base}/c?page=3&maxItems=20&page=4`],
386
+ ["ap://did:key:z6MkAlice/c?a=1&&b=2#frag", `${base}/c?a=1&&b=2#frag`],
387
+ ["ap://did:key:z6MkAlice/c?", `${base}/c?`],
388
+ ["ap://did:key:z6MkAlice/c?x='y'", `${base}/c?x=%27y%27`],
389
+ ["ap://did:key:z6MkAlice/c?@gate way=https%3A%2F%2Fa.example", `${base}/c?@gate%09way=https%3A%2F%2Fa.example`],
390
+ ["ap://did:key:z6MkAlice/c?@gate\nway=1&x=\r2", `${base}/c?@gate%0Away=1&x=%0D2`],
391
+ ["ap://did:key:z6MkAlice/c?%ff=1", `${base}/c?%FF=1`]
392
+ ];
393
+ for (const [portableId, expected] of cases) {
394
+ const result = require_url.toCompatibleEf61Id(portableId, "https://server.example");
395
+ (0, node_assert.deepStrictEqual)(result.href, expected, portableId);
396
+ (0, node_assert.ok)(!/[?&](%40|@)gateway(=|&|$)/i.test(result.search), portableId);
397
+ }
398
+ });
399
+ (0, node_test.test)("toCompatibleEf61Id() rejects invalid portable IDs", () => {
400
+ const cases = [
401
+ "https://server.example/actor",
402
+ "https://server.example/.well-known/apgateway/did:key:z6MkAlice/actor",
403
+ "at://did:plc:example/record",
404
+ "ap://not-a-did/actor",
405
+ "ap://did:key:z6MkAlice",
406
+ "ap://did:key:z6MkAlice/a%zz",
407
+ "ap://did:key:z6MkAlice/a#%zz",
408
+ "ap://did:key:z6MkAlice/a?x=%zz",
409
+ "ap://did:key:z6MkAlice/.",
410
+ "ap://did:key:z6MkAlice/..",
411
+ "ap://did:key:z6MkAlice/a/./b",
412
+ "ap://did:key:z6MkAlice/a/../b",
413
+ "ap://did:key:z6MkAlice/a/%2e/b",
414
+ "ap://did:key:z6MkAlice/a/%2E%2e/b",
415
+ "ap://did:key:z6MkAlice/a/.%2e/b",
416
+ "ap://did:key:z6MkAlice/a/%2e./b",
417
+ new URL("https://server.example/actor"),
418
+ new URL("ap+ef61://user:pass@did%3Akey%3Az6MkAlice/actor"),
419
+ new URL("ap+ef61://did%3Akey%3Az6MkAlice:8080/actor"),
420
+ 123
421
+ ];
422
+ for (const portableId of cases) (0, node_assert.throws)(() => require_url.toCompatibleEf61Id(portableId, "https://server.example"), TypeError, String(portableId));
423
+ });
424
+ (0, node_test.test)("toCompatibleEf61Id() rejects gateways that are not HTTP(S) origins", () => {
425
+ const cases = [
426
+ "ftp://server.example",
427
+ "ap+ef61://did:key:z6MkAlice/actor",
428
+ "https://user:pass@server.example",
429
+ "https://user@server.example",
430
+ "https://server.example/ap",
431
+ "https://server.example/ap/",
432
+ "https://server.example/.well-known/apgateway",
433
+ "https://server.example/?x=1",
434
+ "https://server.example/?",
435
+ "https://server.example/#fragment",
436
+ "https://server.example/#",
437
+ "server.example",
438
+ "",
439
+ new URL("https://server.example/?"),
440
+ new URL("https://server.example/#"),
441
+ new URL("https://server.example/ap"),
442
+ new URL("ftp://server.example/"),
443
+ 123
444
+ ];
445
+ for (const gateway of cases) (0, node_assert.throws)(() => require_url.toCompatibleEf61Id("ap://did:key:z6MkAlice/actor", gateway), TypeError, String(gateway));
446
+ });
447
+ (0, node_test.test)("compatible identifier conversion round-trips", () => {
448
+ for (const portableId of [
449
+ "ap+ef61://did:key:z6MkAlice/objects/1",
450
+ "ap://did:key:z6MkAlice/actor",
451
+ "ap://did%3Akey%3Az6MkAlice/actor/inbox",
452
+ "ap://did:key:z6MkAlice/actor#",
453
+ "ap://did:key:z6MkAlice/actor#main-key",
454
+ "ap://did:key:z6MkAlice/a%2Fb/%252e",
455
+ "ap://did:key:z6MkAlice/a\\b c é",
456
+ "ap://did:example:a%2525/x",
457
+ "ap://did:web:example.com%3A8080/actor",
458
+ "ap://did:key:z6MkAlice/c?page=3&@gateway=https%3A%2F%2Fa.example"
459
+ ]) {
460
+ const converted = require_url.fromCompatibleEf61Id(require_url.toCompatibleEf61Id(portableId, "https://server.example"));
461
+ (0, node_assert.ok)(converted != null, portableId);
462
+ (0, node_assert.deepStrictEqual)(require_url.canonicalizePortableUri(converted.href), require_url.canonicalizePortableUri(portableId), portableId);
463
+ }
464
+ (0, node_assert.ok)(!require_url.arePortableUrisEqual(require_url.fromCompatibleEf61Id(require_url.toCompatibleEf61Id("ap://did:key:z6MkAlice/actor", "https://server.example")).href, require_url.fromCompatibleEf61Id(require_url.toCompatibleEf61Id("ap://did:key:z6MkAlice/actor#", "https://server.example")).href));
465
+ });
247
466
  (0, node_test.test)("validatePublicUrl()", async () => {
248
467
  await (0, node_assert.rejects)(() => require_url.validatePublicUrl("ftp://localhost"), require_url.UrlError);
249
468
  await (0, node_assert.rejects)(() => require_url.validatePublicUrl("data:text/plain;base64,SGVsbG8sIFdvcmxkIQ=="), require_url.UrlError);
@@ -1,4 +1,4 @@
1
- import { a as formatIri, c as haveSameIriOrigin, d as isValidPublicIPv6Address, f as parseGatewayUrl, g as validatePublicUrl, h as validateLookupAddresses, i as expandIPv6Address, l as isGatewayUrl, m as parseJsonLdId, n as arePortableUrisEqual, o as getFe34Origin, p as parseIri, r as canonicalizePortableUri, s as haveSameFe34Origin, t as UrlError, u as isValidPublicIPv4Address } from "./url-Basm1L_c.mjs";
1
+ import { _ as validateLookupAddresses, a as formatIri, c as haveSameFe34Origin, d as isValidPublicIPv4Address, f as isValidPublicIPv6Address, g as toCompatibleEf61Id, h as parseJsonLdId, i as expandIPv6Address, l as haveSameIriOrigin, m as parseIri, n as arePortableUrisEqual, o as fromCompatibleEf61Id, p as parseGatewayUrl, r as canonicalizePortableUri, s as getFe34Origin, t as UrlError, u as isGatewayUrl, v as validatePublicUrl } from "./url-DgJM9WJn.mjs";
2
2
  import { test } from "node:test";
3
3
  import { deepStrictEqual, ok, rejects, throws } from "node:assert";
4
4
  //#region src/url.test.ts
@@ -244,6 +244,225 @@ test("parseGatewayUrl() accepts only HTTP(S) base URIs", () => {
244
244
  ok(!isGatewayUrl(new URL(url)));
245
245
  }
246
246
  });
247
+ test("fromCompatibleEf61Id() converts compatible identifiers", () => {
248
+ for (const [input, expected] of [
249
+ ["https://server.example/.well-known/apgateway/did:key:z6MkAlice/objects/1", "ap+ef61://did%3Akey%3Az6MkAlice/objects/1"],
250
+ ["http://server.example:8080/.well-known/apgateway/did:key:z6MkAlice/actor", "ap+ef61://did%3Akey%3Az6MkAlice/actor"],
251
+ ["https://server.example/.well-known/apgateway/did%3Akey%3Az6MkAlice/actor", "ap+ef61://did%3Akey%3Az6MkAlice/actor"],
252
+ ["https://server.example/.well-known/apgateway/DID:key:z6MkAlice/actor", "ap+ef61://did%3Akey%3Az6MkAlice/actor"],
253
+ ["https://server.example/.well-known/apgateway/did:key:z6MkAlice/a/b/c", "ap+ef61://did%3Akey%3Az6MkAlice/a/b/c"],
254
+ ["https://server.example/.well-known/apgateway/did:key:z6MkAlice/actor#main-key", "ap+ef61://did%3Akey%3Az6MkAlice/actor#main-key"],
255
+ ["https://server.example/.well-known/apgateway/did:key:z6MkAlice/actor#", "ap+ef61://did%3Akey%3Az6MkAlice/actor#"],
256
+ ["https://server.example/.well-known/apgateway/did:key:z6MkAlice/outbox?page=2", "ap+ef61://did%3Akey%3Az6MkAlice/outbox?page=2"],
257
+ ["https://server.example/.well-known/apgateway/did:key:z6MkAlice/a%2Fb", "ap+ef61://did%3Akey%3Az6MkAlice/a%2Fb"],
258
+ ["https://server.example/.well-known/apgateway/did:web:example.com%3A8080/actor", "ap+ef61://did%3Aweb%3Aexample.com%253A8080/actor"],
259
+ ["https://server.example/.well-known/apgateway/did:example:a%2525/x", "ap+ef61://did%3Aexample%3Aa%2525/x"]
260
+ ]) {
261
+ deepStrictEqual(fromCompatibleEf61Id(input)?.href, expected, input);
262
+ deepStrictEqual(fromCompatibleEf61Id(new URL(input))?.href, expected, input);
263
+ }
264
+ const converted = fromCompatibleEf61Id("https://server.example/.well-known/apgateway/did:key:z6MkAlice/objects/1");
265
+ deepStrictEqual(formatIri(converted), "ap+ef61://did:key:z6MkAlice/objects/1");
266
+ deepStrictEqual(canonicalizePortableUri(converted.href), "ap+ef61://did:key:z6MkAlice/objects/1");
267
+ ok(arePortableUrisEqual(fromCompatibleEf61Id("https://a.example/.well-known/apgateway/did:key:z6MkAlice/actor").href, fromCompatibleEf61Id("https://b.example/.well-known/apgateway/did:key:z6MkAlice/actor").href));
268
+ });
269
+ test("fromCompatibleEf61Id() returns null for other URLs", () => {
270
+ for (const input of [
271
+ "https://server.example/users/alice",
272
+ "https://server.example/.well-known/webfinger?resource=acct:a@b",
273
+ "https://server.example/.well-known/apgateway",
274
+ "https://server.example/.well-known/apgateway/",
275
+ "https://server.example/.well-known/apgateway/hl:zQmdfTbBqBPQ7VNxZEYEj14VmRuZBkqFbiwReogJgS1zR1n",
276
+ "https://server.example/.well-known/apgateway/objects/did:key:z6MkAlice/x",
277
+ "https://server.example/.well-known/apgatewayx/did:key:z6MkAlice/x",
278
+ "https://server.example/ap/did:key:z6MkAlice/actor",
279
+ "https://server.example/?id=/.well-known/apgateway/did:key:z6MkAlice/x",
280
+ "ap://did:key:z6MkAlice/actor",
281
+ "ap+ef61://did:key:z6MkAlice/actor",
282
+ "ftp://server.example/.well-known/apgateway/did:key:z6MkAlice/actor",
283
+ "not a URL",
284
+ "/.well-known/apgateway/did:key:z6MkAlice/actor"
285
+ ]) deepStrictEqual(fromCompatibleEf61Id(input), null, input);
286
+ deepStrictEqual(fromCompatibleEf61Id(new URL("ap+ef61://did%3Akey%3Az6MkAlice/actor")), null);
287
+ deepStrictEqual(fromCompatibleEf61Id(123), null);
288
+ });
289
+ test("fromCompatibleEf61Id() rejects malformed compatible identifiers", () => {
290
+ for (const input of [
291
+ "https://server.example/.well-known/apgateway/did:key:z6MkAlice",
292
+ "https://server.example/.well-known/apgateway/did:",
293
+ "https://server.example/.well-known/apgateway/did:key/actor",
294
+ "https://server.example/.well-known/apgateway/did:ke%y:z6MkAlice/actor",
295
+ "https://server.example/.well-known/apgateway/did:key:z6MkAlice/a%zz",
296
+ "https://server.example/.well-known/apgateway/did:key:z6MkAlice/a#%zz",
297
+ "https://server.example/.well-known/apgateway/did:key:z6MkAlice/a?x=%zz",
298
+ "https://server.example/.well-known/apgateway/did:key:z6MkAlice/a?@gateway=https%3A%2F%2Fevil.example",
299
+ "https://server.example/.well-known/apgateway/did:key:z6MkAlice/a?page=2&%40gateway=x",
300
+ "https://server.example/.well-known/apgateway/did:key:z6MkAlice/a?@%67ateway=x",
301
+ "https://server.example/.well-known/apgateway/did:key:z6MkAlice/a?gateways=x",
302
+ "https://server.example/.well-known/apgateway/did:example:a%25/x",
303
+ "https://user:pass@server.example/.well-known/apgateway/did:key:z6MkAlice/a",
304
+ "https://user@server.example/.well-known/apgateway/did:key:z6MkAlice/a"
305
+ ]) throws(() => fromCompatibleEf61Id(input), TypeError, input);
306
+ });
307
+ test("toCompatibleEf61Id() converts portable URIs", () => {
308
+ const cases = [
309
+ [
310
+ "ap+ef61://did:key:z6MkAlice/objects/1",
311
+ "https://server.example",
312
+ "https://server.example/.well-known/apgateway/did:key:z6MkAlice/objects/1"
313
+ ],
314
+ [
315
+ "ap://did:key:z6MkAlice/objects/1",
316
+ "https://server.example/",
317
+ "https://server.example/.well-known/apgateway/did:key:z6MkAlice/objects/1"
318
+ ],
319
+ [
320
+ "ap://did%3Akey%3Az6MkAlice/actor",
321
+ new URL("http://server.example:8080"),
322
+ "http://server.example:8080/.well-known/apgateway/did:key:z6MkAlice/actor"
323
+ ],
324
+ [
325
+ new URL("ap+ef61://did%3Akey%3Az6MkAlice/actor"),
326
+ "https://server.example",
327
+ "https://server.example/.well-known/apgateway/did:key:z6MkAlice/actor"
328
+ ],
329
+ [
330
+ new URL("ap://did%3Akey%3Az6MkAlice/actor"),
331
+ "https://SERVER.example",
332
+ "https://server.example/.well-known/apgateway/did:key:z6MkAlice/actor"
333
+ ],
334
+ [
335
+ "ap://did:key:z6MkAlice/a/b/c",
336
+ "https://server.example",
337
+ "https://server.example/.well-known/apgateway/did:key:z6MkAlice/a/b/c"
338
+ ],
339
+ [
340
+ "ap://did:key:z6MkAlice/actor#main-key",
341
+ "https://server.example",
342
+ "https://server.example/.well-known/apgateway/did:key:z6MkAlice/actor#main-key"
343
+ ],
344
+ [
345
+ "ap://did:key:z6MkAlice/actor#",
346
+ "https://server.example",
347
+ "https://server.example/.well-known/apgateway/did:key:z6MkAlice/actor#"
348
+ ],
349
+ [
350
+ "ap://DID:key:z6MkAlice/actor",
351
+ "https://server.example",
352
+ "https://server.example/.well-known/apgateway/did:key:z6MkAlice/actor"
353
+ ],
354
+ [
355
+ "ap://did:example:a%2525/x",
356
+ "https://server.example",
357
+ "https://server.example/.well-known/apgateway/did:example:a%2525/x"
358
+ ],
359
+ [
360
+ "ap://did:web:example.com%3A8080/actor",
361
+ "https://server.example",
362
+ "https://server.example/.well-known/apgateway/did:web:example.com%3A8080/actor"
363
+ ],
364
+ [
365
+ "ap://did:key:z6MkAlice/a%2Fb/%252e",
366
+ "https://server.example",
367
+ "https://server.example/.well-known/apgateway/did:key:z6MkAlice/a%2Fb/%252e"
368
+ ],
369
+ [
370
+ "ap://did:key:z6MkAlice/a\\b c é/%7euser",
371
+ "https://server.example",
372
+ "https://server.example/.well-known/apgateway/did:key:z6MkAlice/a%5Cb%20c%09%C3%A9/~user"
373
+ ]
374
+ ];
375
+ for (const [portableId, gateway, expected] of cases) deepStrictEqual(toCompatibleEf61Id(portableId, gateway).href, expected, String(portableId));
376
+ });
377
+ test("toCompatibleEf61Id() removes location hints from queries", () => {
378
+ const base = "https://server.example/.well-known/apgateway/did:key:z6MkAlice";
379
+ const cases = [
380
+ ["ap://did:key:z6MkAlice/actor?@gateway=https%3A%2F%2Fa.example&@gateway=https%3A%2F%2Fb.example", `${base}/actor`],
381
+ ["ap://did:key:z6MkAlice/actor?gateways=https%3A%2F%2Fa.example", `${base}/actor`],
382
+ ["ap://did:key:z6MkAlice/actor?%40gateway=https%3A%2F%2Fa.example", `${base}/actor`],
383
+ ["ap://did:key:z6MkAlice/actor?@%67ateway=https%3A%2F%2Fa.example", `${base}/actor`],
384
+ ["ap://did:key:z6MkAlice/actor?@gateway", `${base}/actor`],
385
+ ["ap://did:key:z6MkAlice/c?page=3&@gateway=https%3A%2F%2Fa.example&maxItems=20&page=4", `${base}/c?page=3&maxItems=20&page=4`],
386
+ ["ap://did:key:z6MkAlice/c?a=1&&b=2#frag", `${base}/c?a=1&&b=2#frag`],
387
+ ["ap://did:key:z6MkAlice/c?", `${base}/c?`],
388
+ ["ap://did:key:z6MkAlice/c?x='y'", `${base}/c?x=%27y%27`],
389
+ ["ap://did:key:z6MkAlice/c?@gate way=https%3A%2F%2Fa.example", `${base}/c?@gate%09way=https%3A%2F%2Fa.example`],
390
+ ["ap://did:key:z6MkAlice/c?@gate\nway=1&x=\r2", `${base}/c?@gate%0Away=1&x=%0D2`],
391
+ ["ap://did:key:z6MkAlice/c?%ff=1", `${base}/c?%FF=1`]
392
+ ];
393
+ for (const [portableId, expected] of cases) {
394
+ const result = toCompatibleEf61Id(portableId, "https://server.example");
395
+ deepStrictEqual(result.href, expected, portableId);
396
+ ok(!/[?&](%40|@)gateway(=|&|$)/i.test(result.search), portableId);
397
+ }
398
+ });
399
+ test("toCompatibleEf61Id() rejects invalid portable IDs", () => {
400
+ const cases = [
401
+ "https://server.example/actor",
402
+ "https://server.example/.well-known/apgateway/did:key:z6MkAlice/actor",
403
+ "at://did:plc:example/record",
404
+ "ap://not-a-did/actor",
405
+ "ap://did:key:z6MkAlice",
406
+ "ap://did:key:z6MkAlice/a%zz",
407
+ "ap://did:key:z6MkAlice/a#%zz",
408
+ "ap://did:key:z6MkAlice/a?x=%zz",
409
+ "ap://did:key:z6MkAlice/.",
410
+ "ap://did:key:z6MkAlice/..",
411
+ "ap://did:key:z6MkAlice/a/./b",
412
+ "ap://did:key:z6MkAlice/a/../b",
413
+ "ap://did:key:z6MkAlice/a/%2e/b",
414
+ "ap://did:key:z6MkAlice/a/%2E%2e/b",
415
+ "ap://did:key:z6MkAlice/a/.%2e/b",
416
+ "ap://did:key:z6MkAlice/a/%2e./b",
417
+ new URL("https://server.example/actor"),
418
+ new URL("ap+ef61://user:pass@did%3Akey%3Az6MkAlice/actor"),
419
+ new URL("ap+ef61://did%3Akey%3Az6MkAlice:8080/actor"),
420
+ 123
421
+ ];
422
+ for (const portableId of cases) throws(() => toCompatibleEf61Id(portableId, "https://server.example"), TypeError, String(portableId));
423
+ });
424
+ test("toCompatibleEf61Id() rejects gateways that are not HTTP(S) origins", () => {
425
+ const cases = [
426
+ "ftp://server.example",
427
+ "ap+ef61://did:key:z6MkAlice/actor",
428
+ "https://user:pass@server.example",
429
+ "https://user@server.example",
430
+ "https://server.example/ap",
431
+ "https://server.example/ap/",
432
+ "https://server.example/.well-known/apgateway",
433
+ "https://server.example/?x=1",
434
+ "https://server.example/?",
435
+ "https://server.example/#fragment",
436
+ "https://server.example/#",
437
+ "server.example",
438
+ "",
439
+ new URL("https://server.example/?"),
440
+ new URL("https://server.example/#"),
441
+ new URL("https://server.example/ap"),
442
+ new URL("ftp://server.example/"),
443
+ 123
444
+ ];
445
+ for (const gateway of cases) throws(() => toCompatibleEf61Id("ap://did:key:z6MkAlice/actor", gateway), TypeError, String(gateway));
446
+ });
447
+ test("compatible identifier conversion round-trips", () => {
448
+ for (const portableId of [
449
+ "ap+ef61://did:key:z6MkAlice/objects/1",
450
+ "ap://did:key:z6MkAlice/actor",
451
+ "ap://did%3Akey%3Az6MkAlice/actor/inbox",
452
+ "ap://did:key:z6MkAlice/actor#",
453
+ "ap://did:key:z6MkAlice/actor#main-key",
454
+ "ap://did:key:z6MkAlice/a%2Fb/%252e",
455
+ "ap://did:key:z6MkAlice/a\\b c é",
456
+ "ap://did:example:a%2525/x",
457
+ "ap://did:web:example.com%3A8080/actor",
458
+ "ap://did:key:z6MkAlice/c?page=3&@gateway=https%3A%2F%2Fa.example"
459
+ ]) {
460
+ const converted = fromCompatibleEf61Id(toCompatibleEf61Id(portableId, "https://server.example"));
461
+ ok(converted != null, portableId);
462
+ deepStrictEqual(canonicalizePortableUri(converted.href), canonicalizePortableUri(portableId), portableId);
463
+ }
464
+ ok(!arePortableUrisEqual(fromCompatibleEf61Id(toCompatibleEf61Id("ap://did:key:z6MkAlice/actor", "https://server.example")).href, fromCompatibleEf61Id(toCompatibleEf61Id("ap://did:key:z6MkAlice/actor#", "https://server.example")).href));
465
+ });
247
466
  test("validatePublicUrl()", async () => {
248
467
  await rejects(() => validatePublicUrl("ftp://localhost"), UrlError);
249
468
  await rejects(() => validatePublicUrl("data:text/plain;base64,SGVsbG8sIFdvcmxkIQ=="), UrlError);