@fedify/vocab-runtime 2.4.0-dev.1599 → 2.4.0-dev.1634

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 (38) hide show
  1. package/deno.json +1 -1
  2. package/dist/{docloader-DnUMWHaJ.d.cts → docloader-C_dir7Xb.d.ts} +2 -0
  3. package/dist/{docloader-xRGn1azD.d.ts → docloader-D2DTRiyA.d.cts} +2 -0
  4. package/dist/internal/jsonld-cache.cjs +2 -2
  5. package/dist/internal/jsonld-cache.d.cts +1 -1
  6. package/dist/internal/jsonld-cache.d.ts +1 -1
  7. package/dist/internal/jsonld-cache.js +2 -2
  8. package/dist/mod.cjs +16 -6
  9. package/dist/mod.d.cts +28 -2
  10. package/dist/mod.d.ts +28 -2
  11. package/dist/mod.js +13 -7
  12. package/dist/tests/decimal.test.cjs +2 -2
  13. package/dist/tests/decimal.test.mjs +2 -2
  14. package/dist/tests/{docloader-CDde_fhH.mjs → docloader-CNzoXrLM.mjs} +12 -6
  15. package/dist/tests/{docloader-BiSxywul.cjs → docloader-CTd4XwqK.cjs} +12 -6
  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 +2 -2
  19. package/dist/tests/jsonld-cache.test.mjs +2 -2
  20. package/dist/tests/{request-BBw3lNNz.mjs → request-BWDludWb.mjs} +1 -1
  21. package/dist/tests/{request-C9VKqYdG.cjs → request-Zpjq9m-W.cjs} +1 -1
  22. package/dist/tests/request.test.cjs +1 -1
  23. package/dist/tests/request.test.mjs +1 -1
  24. package/dist/tests/{url-CtKcX6ma.cjs → url-CsOV_B_P.cjs} +90 -3
  25. package/dist/tests/{url-D7ay_5pk.mjs → url-Du7RQQgP.mjs} +67 -4
  26. package/dist/tests/url.test.cjs +62 -3
  27. package/dist/tests/url.test.mjs +62 -3
  28. package/dist/{url-BMDN9GnE.cjs → url-DD4F0ULf.cjs} +90 -3
  29. package/dist/{url-BlFta-Eh.js → url-DGVbSVVi.js} +67 -4
  30. package/package.json +1 -1
  31. package/src/contexts/fep-ef61.json +10 -0
  32. package/src/contexts/security-data-integrity-v1.json +0 -4
  33. package/src/contexts.ts +2 -0
  34. package/src/docloader.ts +2 -0
  35. package/src/internal/jsonld-cache.ts +3 -2
  36. package/src/mod.ts +4 -0
  37. package/src/url.test.ts +152 -2
  38. package/src/url.ts +88 -3
@@ -63,7 +63,7 @@ function canonicalizePortableUri(input) {
63
63
  const parsed = parsePortableIri(input);
64
64
  if (parsed == null) throw new TypeError("Invalid portable ActivityPub IRI.");
65
65
  const match = input.match(PORTABLE_IRI_PATTERN);
66
- return `ap+ef61://${normalizePortableAuthority(decodePortableAuthority(parsed.host).replace(DID_SCHEME_PATTERN, "did:"))}${normalizePortableComponent(match[3])}${match[5] == null ? "" : normalizePortableComponent(match[5])}`;
66
+ return `ap+ef61://${normalizePortableAuthority(getDidUrlOrigin(decodePortableAuthority(parsed.host)))}${normalizePortableComponent(match[3])}${match[5] == null ? "" : normalizePortableComponent(match[5])}`;
67
67
  }
68
68
  /**
69
69
  * Checks whether two FEP-ef61 portable ActivityPub URIs identify the same
@@ -88,6 +88,45 @@ function arePortableUrisEqual(left, right) {
88
88
  }
89
89
  }
90
90
  /**
91
+ * Computes an IRI's FEP-fe34 origin.
92
+ *
93
+ * HTTP(S) IRIs use their web origin. FEP-ef61 portable ActivityPub IRIs and
94
+ * DID URLs use their DID as a cryptographic origin.
95
+ *
96
+ * @throws {TypeError} If the IRI does not have a supported FEP-fe34 origin.
97
+ * @since 2.4.0
98
+ */
99
+ function getFe34Origin(input) {
100
+ if (input instanceof URL) {
101
+ const portable = normalizePortableUrl(input);
102
+ if (portable != null) return getPortableCryptographicOrigin(portable);
103
+ if (input.protocol === "did:") return getDidUrlOrigin(input.href);
104
+ if (input.protocol === "http:" || input.protocol === "https:") return input.origin;
105
+ throw new TypeError("Unsupported FEP-fe34 origin IRI.");
106
+ }
107
+ const portable = parsePortableIri(input);
108
+ if (portable != null) return getPortableCryptographicOrigin(portable);
109
+ if (DID_SCHEME_PATTERN.test(input)) return getDidUrlOrigin(input);
110
+ const parsed = new URL(input);
111
+ if (parsed.protocol === "http:" || parsed.protocol === "https:") return parsed.origin;
112
+ throw new TypeError("Unsupported FEP-fe34 origin IRI.");
113
+ }
114
+ /**
115
+ * Checks whether two IRIs have the same FEP-fe34 origin.
116
+ *
117
+ * Malformed or unsupported IRIs are treated as non-matching.
118
+ *
119
+ * @since 2.4.0
120
+ */
121
+ function haveSameFe34Origin(left, right) {
122
+ try {
123
+ return getFe34Origin(left) === getFe34Origin(right);
124
+ } catch (error) {
125
+ if (error instanceof TypeError) return false;
126
+ throw error;
127
+ }
128
+ }
129
+ /**
91
130
  * Checks whether two IRIs have the same origin.
92
131
  */
93
132
  function haveSameIriOrigin(left, right) {
@@ -97,15 +136,25 @@ function getComparableIriOrigin(iri) {
97
136
  iri = normalizePortableUrl(iri) ?? iri;
98
137
  if (iri.origin !== "null") return iri.origin;
99
138
  if (iri.host !== "") {
100
- const host = iri.protocol === "ap+ef61:" ? encodeURIComponent(decodePortableAuthority(iri.host).replace(DID_SCHEME_PATTERN, "did:")) : iri.host;
139
+ const host = iri.protocol === "ap+ef61:" ? encodeURIComponent(getDidUrlOrigin(decodePortableAuthority(iri.host))) : iri.host;
101
140
  return `${iri.protocol}//${host}`;
102
141
  }
103
142
  return iri.href;
104
143
  }
144
+ function getPortableCryptographicOrigin(iri) {
145
+ return getDidUrlOrigin(decodePortableAuthority(iri.host));
146
+ }
147
+ function getDidUrlOrigin(iri) {
148
+ const did = iri.split(/[/?#]/, 1)[0].replace(DID_SCHEME_PATTERN, "did:");
149
+ if (!DID_PATTERN.test(did)) throw new TypeError("Invalid DID URL.");
150
+ const parts = did.split(":");
151
+ parts[1] = parts[1].toLowerCase();
152
+ return normalizePortableAuthority(parts.join(":"));
153
+ }
105
154
  function parsePortableIri(iri) {
106
155
  const match = iri.match(PORTABLE_IRI_PATTERN);
107
156
  if (match == null) return null;
108
- const authority = decodePortableAuthority(match[2]);
157
+ const authority = getDidUrlOrigin(decodePortableAuthority(match[2]));
109
158
  if (!DID_PATTERN.test(authority)) throw new TypeError("Invalid portable ActivityPub IRI authority.");
110
159
  if (match[3] === "") throw new TypeError("Invalid portable ActivityPub IRI path.");
111
160
  return new URL(`ap+ef61://${encodeURIComponent(authority)}${match[3]}${match[4] ?? ""}${match[5] ?? ""}`);
@@ -162,6 +211,20 @@ function parseAtUri(uri) {
162
211
  return new URL("at://" + encodeURIComponent(authority) + path);
163
212
  }
164
213
  /**
214
+ * Checks whether the URL is an FEP-ef61 gateway base URI.
215
+ */
216
+ function isGatewayUrl(url) {
217
+ return (url.protocol === "http:" || url.protocol === "https:") && url.username === "" && url.password === "" && url.pathname === "/" && url.search === "" && url.hash === "";
218
+ }
219
+ /**
220
+ * Parses and validates an FEP-ef61 gateway base URI.
221
+ */
222
+ function parseGatewayUrl(url) {
223
+ const parsed = parseIri(url);
224
+ if (!isGatewayUrl(parsed)) throw new TypeError("FEP-ef61 gateways must be HTTP(S) base URIs with no credentials, path, query, or fragment.");
225
+ return parsed;
226
+ }
227
+ /**
165
228
  * Validates a URL to prevent SSRF attacks.
166
229
  */
167
230
  async function validatePublicUrl(url) {
@@ -357,12 +420,30 @@ Object.defineProperty(exports, "formatIri", {
357
420
  return formatIri;
358
421
  }
359
422
  });
423
+ Object.defineProperty(exports, "getFe34Origin", {
424
+ enumerable: true,
425
+ get: function() {
426
+ return getFe34Origin;
427
+ }
428
+ });
429
+ Object.defineProperty(exports, "haveSameFe34Origin", {
430
+ enumerable: true,
431
+ get: function() {
432
+ return haveSameFe34Origin;
433
+ }
434
+ });
360
435
  Object.defineProperty(exports, "haveSameIriOrigin", {
361
436
  enumerable: true,
362
437
  get: function() {
363
438
  return haveSameIriOrigin;
364
439
  }
365
440
  });
441
+ Object.defineProperty(exports, "isGatewayUrl", {
442
+ enumerable: true,
443
+ get: function() {
444
+ return isGatewayUrl;
445
+ }
446
+ });
366
447
  Object.defineProperty(exports, "isValidPublicIPv4Address", {
367
448
  enumerable: true,
368
449
  get: function() {
@@ -375,6 +456,12 @@ Object.defineProperty(exports, "isValidPublicIPv6Address", {
375
456
  return isValidPublicIPv6Address;
376
457
  }
377
458
  });
459
+ Object.defineProperty(exports, "parseGatewayUrl", {
460
+ enumerable: true,
461
+ get: function() {
462
+ return parseGatewayUrl;
463
+ }
464
+ });
378
465
  Object.defineProperty(exports, "parseIri", {
379
466
  enumerable: true,
380
467
  get: function() {
@@ -62,7 +62,7 @@ function canonicalizePortableUri(input) {
62
62
  const parsed = parsePortableIri(input);
63
63
  if (parsed == null) throw new TypeError("Invalid portable ActivityPub IRI.");
64
64
  const match = input.match(PORTABLE_IRI_PATTERN);
65
- return `ap+ef61://${normalizePortableAuthority(decodePortableAuthority(parsed.host).replace(DID_SCHEME_PATTERN, "did:"))}${normalizePortableComponent(match[3])}${match[5] == null ? "" : normalizePortableComponent(match[5])}`;
65
+ return `ap+ef61://${normalizePortableAuthority(getDidUrlOrigin(decodePortableAuthority(parsed.host)))}${normalizePortableComponent(match[3])}${match[5] == null ? "" : normalizePortableComponent(match[5])}`;
66
66
  }
67
67
  /**
68
68
  * Checks whether two FEP-ef61 portable ActivityPub URIs identify the same
@@ -87,6 +87,45 @@ function arePortableUrisEqual(left, right) {
87
87
  }
88
88
  }
89
89
  /**
90
+ * Computes an IRI's FEP-fe34 origin.
91
+ *
92
+ * HTTP(S) IRIs use their web origin. FEP-ef61 portable ActivityPub IRIs and
93
+ * DID URLs use their DID as a cryptographic origin.
94
+ *
95
+ * @throws {TypeError} If the IRI does not have a supported FEP-fe34 origin.
96
+ * @since 2.4.0
97
+ */
98
+ function getFe34Origin(input) {
99
+ if (input instanceof URL) {
100
+ const portable = normalizePortableUrl(input);
101
+ if (portable != null) return getPortableCryptographicOrigin(portable);
102
+ if (input.protocol === "did:") return getDidUrlOrigin(input.href);
103
+ if (input.protocol === "http:" || input.protocol === "https:") return input.origin;
104
+ throw new TypeError("Unsupported FEP-fe34 origin IRI.");
105
+ }
106
+ const portable = parsePortableIri(input);
107
+ if (portable != null) return getPortableCryptographicOrigin(portable);
108
+ if (DID_SCHEME_PATTERN.test(input)) return getDidUrlOrigin(input);
109
+ const parsed = new URL(input);
110
+ if (parsed.protocol === "http:" || parsed.protocol === "https:") return parsed.origin;
111
+ throw new TypeError("Unsupported FEP-fe34 origin IRI.");
112
+ }
113
+ /**
114
+ * Checks whether two IRIs have the same FEP-fe34 origin.
115
+ *
116
+ * Malformed or unsupported IRIs are treated as non-matching.
117
+ *
118
+ * @since 2.4.0
119
+ */
120
+ function haveSameFe34Origin(left, right) {
121
+ try {
122
+ return getFe34Origin(left) === getFe34Origin(right);
123
+ } catch (error) {
124
+ if (error instanceof TypeError) return false;
125
+ throw error;
126
+ }
127
+ }
128
+ /**
90
129
  * Checks whether two IRIs have the same origin.
91
130
  */
92
131
  function haveSameIriOrigin(left, right) {
@@ -96,15 +135,25 @@ function getComparableIriOrigin(iri) {
96
135
  iri = normalizePortableUrl(iri) ?? iri;
97
136
  if (iri.origin !== "null") return iri.origin;
98
137
  if (iri.host !== "") {
99
- const host = iri.protocol === "ap+ef61:" ? encodeURIComponent(decodePortableAuthority(iri.host).replace(DID_SCHEME_PATTERN, "did:")) : iri.host;
138
+ const host = iri.protocol === "ap+ef61:" ? encodeURIComponent(getDidUrlOrigin(decodePortableAuthority(iri.host))) : iri.host;
100
139
  return `${iri.protocol}//${host}`;
101
140
  }
102
141
  return iri.href;
103
142
  }
143
+ function getPortableCryptographicOrigin(iri) {
144
+ return getDidUrlOrigin(decodePortableAuthority(iri.host));
145
+ }
146
+ function getDidUrlOrigin(iri) {
147
+ const did = iri.split(/[/?#]/, 1)[0].replace(DID_SCHEME_PATTERN, "did:");
148
+ if (!DID_PATTERN.test(did)) throw new TypeError("Invalid DID URL.");
149
+ const parts = did.split(":");
150
+ parts[1] = parts[1].toLowerCase();
151
+ return normalizePortableAuthority(parts.join(":"));
152
+ }
104
153
  function parsePortableIri(iri) {
105
154
  const match = iri.match(PORTABLE_IRI_PATTERN);
106
155
  if (match == null) return null;
107
- const authority = decodePortableAuthority(match[2]);
156
+ const authority = getDidUrlOrigin(decodePortableAuthority(match[2]));
108
157
  if (!DID_PATTERN.test(authority)) throw new TypeError("Invalid portable ActivityPub IRI authority.");
109
158
  if (match[3] === "") throw new TypeError("Invalid portable ActivityPub IRI path.");
110
159
  return new URL(`ap+ef61://${encodeURIComponent(authority)}${match[3]}${match[4] ?? ""}${match[5] ?? ""}`);
@@ -161,6 +210,20 @@ function parseAtUri(uri) {
161
210
  return new URL("at://" + encodeURIComponent(authority) + path);
162
211
  }
163
212
  /**
213
+ * Checks whether the URL is an FEP-ef61 gateway base URI.
214
+ */
215
+ function isGatewayUrl(url) {
216
+ return (url.protocol === "http:" || url.protocol === "https:") && url.username === "" && url.password === "" && url.pathname === "/" && url.search === "" && url.hash === "";
217
+ }
218
+ /**
219
+ * Parses and validates an FEP-ef61 gateway base URI.
220
+ */
221
+ function parseGatewayUrl(url) {
222
+ const parsed = parseIri(url);
223
+ if (!isGatewayUrl(parsed)) throw new TypeError("FEP-ef61 gateways must be HTTP(S) base URIs with no credentials, path, query, or fragment.");
224
+ return parsed;
225
+ }
226
+ /**
164
227
  * Validates a URL to prevent SSRF attacks.
165
228
  */
166
229
  async function validatePublicUrl(url) {
@@ -326,4 +389,4 @@ function matchesIPv6Prefix(address, prefixWords, prefixLength) {
326
389
  return true;
327
390
  }
328
391
  //#endregion
329
- export { formatIri as a, isValidPublicIPv6Address as c, validatePublicUrl as d, expandIPv6Address as i, parseIri as l, arePortableUrisEqual as n, haveSameIriOrigin as o, canonicalizePortableUri as r, isValidPublicIPv4Address as s, UrlError as t, parseJsonLdId as u };
392
+ export { formatIri as a, haveSameIriOrigin as c, isValidPublicIPv6Address as d, parseGatewayUrl as f, validatePublicUrl 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 };
@@ -1,5 +1,5 @@
1
1
  require("./chunk-C2EiDwsr.cjs");
2
- const require_url = require("./url-CtKcX6ma.cjs");
2
+ const require_url = require("./url-CsOV_B_P.cjs");
3
3
  let node_assert = require("node:assert");
4
4
  let node_test = require("node:test");
5
5
  //#region src/url.test.ts
@@ -12,8 +12,12 @@ let node_test = require("node:test");
12
12
  "AP+EF61://did:key:z6Mkabc/actor"
13
13
  ]) (0, node_assert.deepStrictEqual)(require_url.parseIri(iri), new URL("ap+ef61://did%3Akey%3Az6Mkabc/actor"));
14
14
  });
15
- (0, node_test.test)("parseIri() accepts DID schemes case-insensitively", () => {
16
- for (const iri of ["ap://DID:key:z6Mkabc/actor", "ap://DID%3Akey%3Az6Mkabc/actor"]) (0, node_assert.deepStrictEqual)(require_url.parseIri(iri), new URL("ap+ef61://DID%3Akey%3Az6Mkabc/actor"));
15
+ (0, node_test.test)("parseIri() normalizes DID scheme and method casing", () => {
16
+ for (const iri of [
17
+ "ap://DID:key:z6Mkabc/actor",
18
+ "ap://DID%3Akey%3Az6Mkabc/actor",
19
+ "ap://did:KEY:z6Mkabc/actor"
20
+ ]) (0, node_assert.deepStrictEqual)(require_url.parseIri(iri), new URL("ap+ef61://did%3Akey%3Az6Mkabc/actor"));
17
21
  });
18
22
  (0, node_test.test)("parseIri() accepts DID method names that start with digits", () => {
19
23
  (0, node_assert.deepStrictEqual)(require_url.parseIri("ap://did:3:abc/actor"), new URL("ap+ef61://did%3A3%3Aabc/actor"));
@@ -60,12 +64,50 @@ let node_test = require("node:test");
60
64
  ]) (0, node_assert.throws)(() => require_url.parseIri(iri), TypeError);
61
65
  });
62
66
  (0, node_test.test)("haveSameIriOrigin() compares portable IRI authorities", () => {
67
+ (0, node_assert.ok)(require_url.haveSameIriOrigin(new URL("ftp://example.com/pub/1"), new URL("ftp://example.com/pub/2")));
68
+ (0, node_assert.ok)(require_url.haveSameIriOrigin(new URL("mailto:alice@example.com"), new URL("mailto:alice@example.com")));
63
69
  (0, node_assert.ok)(require_url.haveSameIriOrigin(require_url.parseIri("ap://did:key:z6Mkabc/actor"), require_url.parseIri("ap://did:key:z6Mkabc/outbox")));
64
70
  (0, node_assert.ok)(require_url.haveSameIriOrigin(new URL("ap://did%3Akey%3Az6Mkabc/actor"), new URL("ap+ef61://did%3Akey%3Az6Mkabc/outbox")));
65
71
  (0, node_assert.ok)(require_url.haveSameIriOrigin(require_url.parseIri("ap://DID:key:z6Mkabc/actor"), require_url.parseIri("ap://did:key:z6Mkabc/outbox")));
66
72
  (0, node_assert.ok)(require_url.haveSameIriOrigin(new URL("ap+ef61://DID%3Akey%3Az6Mkabc/actor"), new URL("ap+ef61://did%3Akey%3Az6Mkabc/outbox")));
67
73
  (0, node_assert.ok)(!require_url.haveSameIriOrigin(require_url.parseIri("ap://did:key:z6Mkabc/actor"), require_url.parseIri("ap://did:key:z6Mkdef/actor")));
68
74
  });
75
+ (0, node_test.test)("getFe34Origin() computes web and cryptographic origins", () => {
76
+ (0, node_assert.deepStrictEqual)(require_url.getFe34Origin("https://Example.COM:443/users/alice"), "https://example.com");
77
+ (0, node_assert.deepStrictEqual)(require_url.getFe34Origin(new URL("http://example.com:8080/notes/1")), "http://example.com:8080");
78
+ (0, node_assert.deepStrictEqual)(require_url.getFe34Origin("ap://did:key:z6Mkabc/actor?gateways=https%3A%2F%2Fa.example"), "did:key:z6Mkabc");
79
+ (0, node_assert.deepStrictEqual)(require_url.getFe34Origin("ap+ef61://did%3Akey%3Az6Mkabc/objects/1#fragment"), "did:key:z6Mkabc");
80
+ (0, node_assert.deepStrictEqual)(require_url.getFe34Origin(new URL("ap+ef61://did%3Akey%3Az6Mkabc/actor")), "did:key:z6Mkabc");
81
+ (0, node_assert.deepStrictEqual)(require_url.getFe34Origin("did:key:z6Mkabc#z6Mkabc"), "did:key:z6Mkabc");
82
+ (0, node_assert.deepStrictEqual)(require_url.getFe34Origin(new URL("did:key:z6Mkabc?service=activitypub#key")), "did:key:z6Mkabc");
83
+ (0, node_assert.deepStrictEqual)(require_url.getFe34Origin("did:key:z6Mkabc/path/to/resource?service=activitypub#key"), "did:key:z6Mkabc");
84
+ (0, node_assert.deepStrictEqual)(require_url.getFe34Origin("did:KEY:z6Mkabc#z6Mkabc"), "did:key:z6Mkabc");
85
+ (0, node_assert.deepStrictEqual)(require_url.getFe34Origin("ap://did%3Aweb%3Afoo%2Dbar.example/actor"), "did:web:foo-bar.example");
86
+ (0, node_assert.deepStrictEqual)(require_url.getFe34Origin("did:web:foo%2dbar.example#key"), "did:web:foo-bar.example");
87
+ });
88
+ (0, node_test.test)("getFe34Origin() rejects unsupported or malformed identifiers", () => {
89
+ for (const iri of [
90
+ "mailto:alice@example.com",
91
+ "ap://not-a-did/actor",
92
+ "ap://did:key/actor",
93
+ "did:key",
94
+ "did:key:",
95
+ "did:"
96
+ ]) (0, node_assert.throws)(() => require_url.getFe34Origin(iri), TypeError);
97
+ });
98
+ (0, node_test.test)("haveSameFe34Origin() compares web and cryptographic origins", () => {
99
+ (0, node_assert.ok)(require_url.haveSameFe34Origin("https://example.com/users/alice", "https://example.com/notes/1"));
100
+ (0, node_assert.ok)(!require_url.haveSameFe34Origin("https://example.com/users/alice", "http://example.com/users/alice"));
101
+ (0, node_assert.ok)(!require_url.haveSameFe34Origin("https://example.com/users/alice", "https://example.com:8443/users/alice"));
102
+ (0, node_assert.ok)(require_url.haveSameFe34Origin("ap://did:key:z6Mkabc/actor", "ap+ef61://did%3Akey%3Az6Mkabc/objects/1"));
103
+ (0, node_assert.ok)(require_url.haveSameFe34Origin("ap+ef61://did:key:z6Mkabc/actor", "did:key:z6Mkabc#z6Mkabc"));
104
+ (0, node_assert.ok)(require_url.haveSameFe34Origin("ap://did:KEY:z6Mkabc/actor", "did:key:z6Mkabc#z6Mkabc"));
105
+ (0, node_assert.ok)(require_url.haveSameFe34Origin("ap://did%3Aweb%3Afoo%2Dbar.example/actor", "ap://did:web:foo-bar.example/note"));
106
+ (0, node_assert.ok)(require_url.haveSameFe34Origin("ap://did%3Aexample%3Aabc%252fdef/actor", "did:example:abc%2Fdef#key"));
107
+ (0, node_assert.ok)(!require_url.haveSameFe34Origin("ap+ef61://did:key:z6Mkabc/actor", "did:key:z6Mkdef#z6Mkdef"));
108
+ (0, node_assert.ok)(!require_url.haveSameFe34Origin("ap+ef61://did:key:z6Mkabc/actor", "https://example.com/actor"));
109
+ (0, node_assert.ok)(!require_url.haveSameFe34Origin("ap://not-a-did/actor", "ap://not-a-did/actor"));
110
+ });
69
111
  (0, node_test.test)("parseIri() normalizes portable URL instances", () => {
70
112
  (0, node_assert.deepStrictEqual)(require_url.parseIri(new URL("ap+ef61://did%3Aexample%3Aabc%2Fdef/actor")), new URL("ap+ef61://did%3Aexample%3Aabc%252Fdef/actor"));
71
113
  (0, node_assert.throws)(() => require_url.parseIri("ap+ef61://not-a-did/actor"), TypeError);
@@ -186,6 +228,23 @@ let node_test = require("node:test");
186
228
  (0, node_assert.deepStrictEqual)(parsed, new URL("ap+ef61://did%3Aweb%3Aexample.com%2500/u/1"));
187
229
  (0, node_assert.deepStrictEqual)(require_url.formatIri(parsed), "ap+ef61://did:web:example.com%00/u/1");
188
230
  });
231
+ (0, node_test.test)("parseGatewayUrl() accepts only HTTP(S) base URIs", () => {
232
+ for (const url of ["https://server.example/", "http://server.example/"]) {
233
+ (0, node_assert.deepStrictEqual)(require_url.parseGatewayUrl(url), new URL(url));
234
+ (0, node_assert.ok)(require_url.isGatewayUrl(new URL(url)));
235
+ }
236
+ for (const url of [
237
+ "ftp://server.example/",
238
+ "https://user:pass@server.example/",
239
+ "https://user@server.example/",
240
+ "https://server.example/path",
241
+ "https://server.example/?x=1",
242
+ "https://server.example/#fragment"
243
+ ]) {
244
+ (0, node_assert.throws)(() => require_url.parseGatewayUrl(url), TypeError);
245
+ (0, node_assert.ok)(!require_url.isGatewayUrl(new URL(url)));
246
+ }
247
+ });
189
248
  (0, node_test.test)("validatePublicUrl()", async () => {
190
249
  await (0, node_assert.rejects)(() => require_url.validatePublicUrl("ftp://localhost"), require_url.UrlError);
191
250
  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 isValidPublicIPv6Address, d as validatePublicUrl, i as expandIPv6Address, l as parseIri, n as arePortableUrisEqual, o as haveSameIriOrigin, r as canonicalizePortableUri, s as isValidPublicIPv4Address, t as UrlError, u as parseJsonLdId } from "./url-D7ay_5pk.mjs";
1
+ import { a as formatIri, c as haveSameIriOrigin, d as isValidPublicIPv6Address, f as parseGatewayUrl, h as validatePublicUrl, 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-Du7RQQgP.mjs";
2
2
  import { deepStrictEqual, ok, rejects, throws } from "node:assert";
3
3
  import { test } from "node:test";
4
4
  //#region src/url.test.ts
@@ -11,8 +11,12 @@ test("parseIri() accepts portable ActivityPub URI schemes", () => {
11
11
  "AP+EF61://did:key:z6Mkabc/actor"
12
12
  ]) deepStrictEqual(parseIri(iri), new URL("ap+ef61://did%3Akey%3Az6Mkabc/actor"));
13
13
  });
14
- test("parseIri() accepts DID schemes case-insensitively", () => {
15
- for (const iri of ["ap://DID:key:z6Mkabc/actor", "ap://DID%3Akey%3Az6Mkabc/actor"]) deepStrictEqual(parseIri(iri), new URL("ap+ef61://DID%3Akey%3Az6Mkabc/actor"));
14
+ test("parseIri() normalizes DID scheme and method casing", () => {
15
+ for (const iri of [
16
+ "ap://DID:key:z6Mkabc/actor",
17
+ "ap://DID%3Akey%3Az6Mkabc/actor",
18
+ "ap://did:KEY:z6Mkabc/actor"
19
+ ]) deepStrictEqual(parseIri(iri), new URL("ap+ef61://did%3Akey%3Az6Mkabc/actor"));
16
20
  });
17
21
  test("parseIri() accepts DID method names that start with digits", () => {
18
22
  deepStrictEqual(parseIri("ap://did:3:abc/actor"), new URL("ap+ef61://did%3A3%3Aabc/actor"));
@@ -59,12 +63,50 @@ test("parseIri() rejects malformed portable DID authorities", () => {
59
63
  ]) throws(() => parseIri(iri), TypeError);
60
64
  });
61
65
  test("haveSameIriOrigin() compares portable IRI authorities", () => {
66
+ ok(haveSameIriOrigin(new URL("ftp://example.com/pub/1"), new URL("ftp://example.com/pub/2")));
67
+ ok(haveSameIriOrigin(new URL("mailto:alice@example.com"), new URL("mailto:alice@example.com")));
62
68
  ok(haveSameIriOrigin(parseIri("ap://did:key:z6Mkabc/actor"), parseIri("ap://did:key:z6Mkabc/outbox")));
63
69
  ok(haveSameIriOrigin(new URL("ap://did%3Akey%3Az6Mkabc/actor"), new URL("ap+ef61://did%3Akey%3Az6Mkabc/outbox")));
64
70
  ok(haveSameIriOrigin(parseIri("ap://DID:key:z6Mkabc/actor"), parseIri("ap://did:key:z6Mkabc/outbox")));
65
71
  ok(haveSameIriOrigin(new URL("ap+ef61://DID%3Akey%3Az6Mkabc/actor"), new URL("ap+ef61://did%3Akey%3Az6Mkabc/outbox")));
66
72
  ok(!haveSameIriOrigin(parseIri("ap://did:key:z6Mkabc/actor"), parseIri("ap://did:key:z6Mkdef/actor")));
67
73
  });
74
+ test("getFe34Origin() computes web and cryptographic origins", () => {
75
+ deepStrictEqual(getFe34Origin("https://Example.COM:443/users/alice"), "https://example.com");
76
+ deepStrictEqual(getFe34Origin(new URL("http://example.com:8080/notes/1")), "http://example.com:8080");
77
+ deepStrictEqual(getFe34Origin("ap://did:key:z6Mkabc/actor?gateways=https%3A%2F%2Fa.example"), "did:key:z6Mkabc");
78
+ deepStrictEqual(getFe34Origin("ap+ef61://did%3Akey%3Az6Mkabc/objects/1#fragment"), "did:key:z6Mkabc");
79
+ deepStrictEqual(getFe34Origin(new URL("ap+ef61://did%3Akey%3Az6Mkabc/actor")), "did:key:z6Mkabc");
80
+ deepStrictEqual(getFe34Origin("did:key:z6Mkabc#z6Mkabc"), "did:key:z6Mkabc");
81
+ deepStrictEqual(getFe34Origin(new URL("did:key:z6Mkabc?service=activitypub#key")), "did:key:z6Mkabc");
82
+ deepStrictEqual(getFe34Origin("did:key:z6Mkabc/path/to/resource?service=activitypub#key"), "did:key:z6Mkabc");
83
+ deepStrictEqual(getFe34Origin("did:KEY:z6Mkabc#z6Mkabc"), "did:key:z6Mkabc");
84
+ deepStrictEqual(getFe34Origin("ap://did%3Aweb%3Afoo%2Dbar.example/actor"), "did:web:foo-bar.example");
85
+ deepStrictEqual(getFe34Origin("did:web:foo%2dbar.example#key"), "did:web:foo-bar.example");
86
+ });
87
+ test("getFe34Origin() rejects unsupported or malformed identifiers", () => {
88
+ for (const iri of [
89
+ "mailto:alice@example.com",
90
+ "ap://not-a-did/actor",
91
+ "ap://did:key/actor",
92
+ "did:key",
93
+ "did:key:",
94
+ "did:"
95
+ ]) throws(() => getFe34Origin(iri), TypeError);
96
+ });
97
+ test("haveSameFe34Origin() compares web and cryptographic origins", () => {
98
+ ok(haveSameFe34Origin("https://example.com/users/alice", "https://example.com/notes/1"));
99
+ ok(!haveSameFe34Origin("https://example.com/users/alice", "http://example.com/users/alice"));
100
+ ok(!haveSameFe34Origin("https://example.com/users/alice", "https://example.com:8443/users/alice"));
101
+ ok(haveSameFe34Origin("ap://did:key:z6Mkabc/actor", "ap+ef61://did%3Akey%3Az6Mkabc/objects/1"));
102
+ ok(haveSameFe34Origin("ap+ef61://did:key:z6Mkabc/actor", "did:key:z6Mkabc#z6Mkabc"));
103
+ ok(haveSameFe34Origin("ap://did:KEY:z6Mkabc/actor", "did:key:z6Mkabc#z6Mkabc"));
104
+ ok(haveSameFe34Origin("ap://did%3Aweb%3Afoo%2Dbar.example/actor", "ap://did:web:foo-bar.example/note"));
105
+ ok(haveSameFe34Origin("ap://did%3Aexample%3Aabc%252fdef/actor", "did:example:abc%2Fdef#key"));
106
+ ok(!haveSameFe34Origin("ap+ef61://did:key:z6Mkabc/actor", "did:key:z6Mkdef#z6Mkdef"));
107
+ ok(!haveSameFe34Origin("ap+ef61://did:key:z6Mkabc/actor", "https://example.com/actor"));
108
+ ok(!haveSameFe34Origin("ap://not-a-did/actor", "ap://not-a-did/actor"));
109
+ });
68
110
  test("parseIri() normalizes portable URL instances", () => {
69
111
  deepStrictEqual(parseIri(new URL("ap+ef61://did%3Aexample%3Aabc%2Fdef/actor")), new URL("ap+ef61://did%3Aexample%3Aabc%252Fdef/actor"));
70
112
  throws(() => parseIri("ap+ef61://not-a-did/actor"), TypeError);
@@ -185,6 +227,23 @@ test("parseIri() preserves encoded percent signs while decoding delimiters", ()
185
227
  deepStrictEqual(parsed, new URL("ap+ef61://did%3Aweb%3Aexample.com%2500/u/1"));
186
228
  deepStrictEqual(formatIri(parsed), "ap+ef61://did:web:example.com%00/u/1");
187
229
  });
230
+ test("parseGatewayUrl() accepts only HTTP(S) base URIs", () => {
231
+ for (const url of ["https://server.example/", "http://server.example/"]) {
232
+ deepStrictEqual(parseGatewayUrl(url), new URL(url));
233
+ ok(isGatewayUrl(new URL(url)));
234
+ }
235
+ for (const url of [
236
+ "ftp://server.example/",
237
+ "https://user:pass@server.example/",
238
+ "https://user@server.example/",
239
+ "https://server.example/path",
240
+ "https://server.example/?x=1",
241
+ "https://server.example/#fragment"
242
+ ]) {
243
+ throws(() => parseGatewayUrl(url), TypeError);
244
+ ok(!isGatewayUrl(new URL(url)));
245
+ }
246
+ });
188
247
  test("validatePublicUrl()", async () => {
189
248
  await rejects(() => validatePublicUrl("ftp://localhost"), UrlError);
190
249
  await rejects(() => validatePublicUrl("data:text/plain;base64,SGVsbG8sIFdvcmxkIQ=="), UrlError);
@@ -64,7 +64,7 @@ function canonicalizePortableUri(input) {
64
64
  const parsed = parsePortableIri(input);
65
65
  if (parsed == null) throw new TypeError("Invalid portable ActivityPub IRI.");
66
66
  const match = input.match(PORTABLE_IRI_PATTERN);
67
- return `ap+ef61://${normalizePortableAuthority(decodePortableAuthority(parsed.host).replace(DID_SCHEME_PATTERN, "did:"))}${normalizePortableComponent(match[3])}${match[5] == null ? "" : normalizePortableComponent(match[5])}`;
67
+ return `ap+ef61://${normalizePortableAuthority(getDidUrlOrigin(decodePortableAuthority(parsed.host)))}${normalizePortableComponent(match[3])}${match[5] == null ? "" : normalizePortableComponent(match[5])}`;
68
68
  }
69
69
  /**
70
70
  * Checks whether two FEP-ef61 portable ActivityPub URIs identify the same
@@ -89,6 +89,45 @@ function arePortableUrisEqual(left, right) {
89
89
  }
90
90
  }
91
91
  /**
92
+ * Computes an IRI's FEP-fe34 origin.
93
+ *
94
+ * HTTP(S) IRIs use their web origin. FEP-ef61 portable ActivityPub IRIs and
95
+ * DID URLs use their DID as a cryptographic origin.
96
+ *
97
+ * @throws {TypeError} If the IRI does not have a supported FEP-fe34 origin.
98
+ * @since 2.4.0
99
+ */
100
+ function getFe34Origin(input) {
101
+ if (input instanceof URL) {
102
+ const portable = normalizePortableUrl(input);
103
+ if (portable != null) return getPortableCryptographicOrigin(portable);
104
+ if (input.protocol === "did:") return getDidUrlOrigin(input.href);
105
+ if (input.protocol === "http:" || input.protocol === "https:") return input.origin;
106
+ throw new TypeError("Unsupported FEP-fe34 origin IRI.");
107
+ }
108
+ const portable = parsePortableIri(input);
109
+ if (portable != null) return getPortableCryptographicOrigin(portable);
110
+ if (DID_SCHEME_PATTERN.test(input)) return getDidUrlOrigin(input);
111
+ const parsed = new URL(input);
112
+ if (parsed.protocol === "http:" || parsed.protocol === "https:") return parsed.origin;
113
+ throw new TypeError("Unsupported FEP-fe34 origin IRI.");
114
+ }
115
+ /**
116
+ * Checks whether two IRIs have the same FEP-fe34 origin.
117
+ *
118
+ * Malformed or unsupported IRIs are treated as non-matching.
119
+ *
120
+ * @since 2.4.0
121
+ */
122
+ function haveSameFe34Origin(left, right) {
123
+ try {
124
+ return getFe34Origin(left) === getFe34Origin(right);
125
+ } catch (error) {
126
+ if (error instanceof TypeError) return false;
127
+ throw error;
128
+ }
129
+ }
130
+ /**
92
131
  * Checks whether two IRIs have the same origin.
93
132
  */
94
133
  function haveSameIriOrigin(left, right) {
@@ -98,15 +137,25 @@ function getComparableIriOrigin(iri) {
98
137
  iri = normalizePortableUrl(iri) ?? iri;
99
138
  if (iri.origin !== "null") return iri.origin;
100
139
  if (iri.host !== "") {
101
- const host = iri.protocol === "ap+ef61:" ? encodeURIComponent(decodePortableAuthority(iri.host).replace(DID_SCHEME_PATTERN, "did:")) : iri.host;
140
+ const host = iri.protocol === "ap+ef61:" ? encodeURIComponent(getDidUrlOrigin(decodePortableAuthority(iri.host))) : iri.host;
102
141
  return `${iri.protocol}//${host}`;
103
142
  }
104
143
  return iri.href;
105
144
  }
145
+ function getPortableCryptographicOrigin(iri) {
146
+ return getDidUrlOrigin(decodePortableAuthority(iri.host));
147
+ }
148
+ function getDidUrlOrigin(iri) {
149
+ const did = iri.split(/[/?#]/, 1)[0].replace(DID_SCHEME_PATTERN, "did:");
150
+ if (!DID_PATTERN.test(did)) throw new TypeError("Invalid DID URL.");
151
+ const parts = did.split(":");
152
+ parts[1] = parts[1].toLowerCase();
153
+ return normalizePortableAuthority(parts.join(":"));
154
+ }
106
155
  function parsePortableIri(iri) {
107
156
  const match = iri.match(PORTABLE_IRI_PATTERN);
108
157
  if (match == null) return null;
109
- const authority = decodePortableAuthority(match[2]);
158
+ const authority = getDidUrlOrigin(decodePortableAuthority(match[2]));
110
159
  if (!DID_PATTERN.test(authority)) throw new TypeError("Invalid portable ActivityPub IRI authority.");
111
160
  if (match[3] === "") throw new TypeError("Invalid portable ActivityPub IRI path.");
112
161
  return new URL(`ap+ef61://${encodeURIComponent(authority)}${match[3]}${match[4] ?? ""}${match[5] ?? ""}`);
@@ -163,6 +212,20 @@ function parseAtUri(uri) {
163
212
  return new URL("at://" + encodeURIComponent(authority) + path);
164
213
  }
165
214
  /**
215
+ * Checks whether the URL is an FEP-ef61 gateway base URI.
216
+ */
217
+ function isGatewayUrl(url) {
218
+ return (url.protocol === "http:" || url.protocol === "https:") && url.username === "" && url.password === "" && url.pathname === "/" && url.search === "" && url.hash === "";
219
+ }
220
+ /**
221
+ * Parses and validates an FEP-ef61 gateway base URI.
222
+ */
223
+ function parseGatewayUrl(url) {
224
+ const parsed = parseIri(url);
225
+ if (!isGatewayUrl(parsed)) throw new TypeError("FEP-ef61 gateways must be HTTP(S) base URIs with no credentials, path, query, or fragment.");
226
+ return parsed;
227
+ }
228
+ /**
166
229
  * Validates a URL to prevent SSRF attacks.
167
230
  */
168
231
  async function validatePublicUrl(url) {
@@ -358,12 +421,30 @@ Object.defineProperty(exports, "formatIri", {
358
421
  return formatIri;
359
422
  }
360
423
  });
424
+ Object.defineProperty(exports, "getFe34Origin", {
425
+ enumerable: true,
426
+ get: function() {
427
+ return getFe34Origin;
428
+ }
429
+ });
430
+ Object.defineProperty(exports, "haveSameFe34Origin", {
431
+ enumerable: true,
432
+ get: function() {
433
+ return haveSameFe34Origin;
434
+ }
435
+ });
361
436
  Object.defineProperty(exports, "haveSameIriOrigin", {
362
437
  enumerable: true,
363
438
  get: function() {
364
439
  return haveSameIriOrigin;
365
440
  }
366
441
  });
442
+ Object.defineProperty(exports, "isGatewayUrl", {
443
+ enumerable: true,
444
+ get: function() {
445
+ return isGatewayUrl;
446
+ }
447
+ });
367
448
  Object.defineProperty(exports, "isValidPublicIPv4Address", {
368
449
  enumerable: true,
369
450
  get: function() {
@@ -376,6 +457,12 @@ Object.defineProperty(exports, "isValidPublicIPv6Address", {
376
457
  return isValidPublicIPv6Address;
377
458
  }
378
459
  });
460
+ Object.defineProperty(exports, "parseGatewayUrl", {
461
+ enumerable: true,
462
+ get: function() {
463
+ return parseGatewayUrl;
464
+ }
465
+ });
379
466
  Object.defineProperty(exports, "parseIri", {
380
467
  enumerable: true,
381
468
  get: function() {