@fedify/vocab-runtime 2.2.7 → 2.2.9

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.
package/deno.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fedify/vocab-runtime",
3
- "version": "2.2.7",
3
+ "version": "2.2.9",
4
4
  "license": "MIT",
5
5
  "exports": {
6
6
  ".": "./src/mod.ts",
package/dist/mod.cjs CHANGED
@@ -782,6 +782,25 @@ var joinmastodon_default = { "@context": {
782
782
  }
783
783
  } };
784
784
  //#endregion
785
+ //#region src/contexts/miscellany.json
786
+ var miscellany_default = { "@context": {
787
+ "as": "https://www.w3.org/ns/activitystreams#",
788
+ "xsd": "http://www.w3.org/2001/XMLSchema#",
789
+ "Hashtag": "as:Hashtag",
790
+ "manuallyApprovesFollowers": {
791
+ "@id": "as:manuallyApprovesFollowers",
792
+ "@type": "xsd:boolean"
793
+ },
794
+ "movedTo": {
795
+ "@id": "as:movedTo",
796
+ "@type": "@id"
797
+ },
798
+ "sensitive": {
799
+ "@id": "as:sensitive",
800
+ "@type": "xsd:boolean"
801
+ }
802
+ } };
803
+ //#endregion
785
804
  //#region src/contexts/schemaorg.json
786
805
  var schemaorg_default = { "@context": {
787
806
  "type": "@type",
@@ -4379,12 +4398,13 @@ const preloadedContexts = {
4379
4398
  "https://gotosocial.org/ns": gotosocial_default,
4380
4399
  "https://w3id.org/fep/5711": fep_5711_default,
4381
4400
  "https://join-lemmy.org/context.json": join_lemmy_default,
4382
- "http://joinmastodon.org/ns": joinmastodon_default
4401
+ "http://joinmastodon.org/ns": joinmastodon_default,
4402
+ "https://purl.archive.org/miscellany": miscellany_default
4383
4403
  };
4384
4404
  //#endregion
4385
4405
  //#region deno.json
4386
4406
  var name = "@fedify/vocab-runtime";
4387
- var version = "2.2.7";
4407
+ var version = "2.2.9";
4388
4408
  //#endregion
4389
4409
  //#region src/link.ts
4390
4410
  const parametersNeedLowerCase = ["rel", "type"];
@@ -4639,8 +4659,8 @@ function logRequest(logger, request) {
4639
4659
  //#endregion
4640
4660
  //#region src/url.ts
4641
4661
  var UrlError = class extends Error {
4642
- constructor(message) {
4643
- super(message);
4662
+ constructor(message, options) {
4663
+ super(message, options);
4644
4664
  this.name = "UrlError";
4645
4665
  }
4646
4666
  };
@@ -4668,10 +4688,37 @@ async function validatePublicUrl(url) {
4668
4688
  let addresses;
4669
4689
  try {
4670
4690
  addresses = await (0, node_dns_promises.lookup)(hostname, { all: true });
4671
- } catch {
4672
- addresses = [];
4691
+ } catch (error) {
4692
+ throw new UrlError("DNS lookup failed", { cause: error });
4693
+ }
4694
+ validateLookupAddresses(addresses);
4695
+ }
4696
+ /**
4697
+ * Validates the IP addresses returned by `node:dns.lookup()`.
4698
+ *
4699
+ * Cloudflare Workers' `node:dns` implementation currently maps every record
4700
+ * in a DNS-over-HTTPS `Answer` array—including CNAME records—to a
4701
+ * `LookupAddress`, even though Node.js specifies that the `address` field must
4702
+ * contain an IPv4 or IPv6 literal. See:
4703
+ * https://github.com/cloudflare/workerd/issues/6886
4704
+ *
4705
+ * Work around that bug by ignoring non-IP entries only when the lookup also
4706
+ * returns at least one actual IP address. This remains fail-closed: a result
4707
+ * containing no IP addresses is rejected, and every returned IP address is
4708
+ * still validated and must be public. This workaround can be revisited once
4709
+ * the Workerd issue is fixed in supported Cloudflare Workers runtimes.
4710
+ *
4711
+ * @internal
4712
+ */
4713
+ function validateLookupAddresses(addresses) {
4714
+ let ipAddressCount = 0;
4715
+ for (const { address } of addresses) {
4716
+ const family = (0, node_net.isIP)(address);
4717
+ if (family === 0) continue;
4718
+ ipAddressCount++;
4719
+ validatePublicIpAddress(address, family);
4673
4720
  }
4674
- for (const { address, family } of addresses) validatePublicIpAddress(address, family);
4721
+ if (ipAddressCount === 0) throw new UrlError("DNS lookup did not return any IP address");
4675
4722
  }
4676
4723
  function validatePublicIpAddress(address, family) {
4677
4724
  if (family === 4 && isValidPublicIPv4Address(address) || family === 6 && isValidPublicIPv6Address(address)) return;
package/dist/mod.d.cts CHANGED
@@ -424,7 +424,9 @@ declare function encodingFromBaseData(data: string | Uint8Array): Base;
424
424
  //#endregion
425
425
  //#region src/url.d.ts
426
426
  declare class UrlError extends Error {
427
- constructor(message: string);
427
+ constructor(message: string, options?: {
428
+ cause?: unknown;
429
+ });
428
430
  }
429
431
  /**
430
432
  * Validates a URL to prevent SSRF attacks.
package/dist/mod.d.ts CHANGED
@@ -1,6 +1,5 @@
1
1
  /// <reference lib="esnext.temporal" />
2
2
  import { Logger } from "@logtape/logtape";
3
-
4
3
  //#region src/contexts.d.ts
5
4
  declare const preloadedContexts: Record<string, unknown>;
6
5
  //#endregion
@@ -424,7 +423,9 @@ declare function encodingFromBaseData(data: string | Uint8Array): Base;
424
423
  //#endregion
425
424
  //#region src/url.d.ts
426
425
  declare class UrlError extends Error {
427
- constructor(message: string);
426
+ constructor(message: string, options?: {
427
+ cause?: unknown;
428
+ });
428
429
  }
429
430
  /**
430
431
  * Validates a URL to prevent SSRF attacks.
package/dist/mod.js CHANGED
@@ -778,6 +778,25 @@ var joinmastodon_default = { "@context": {
778
778
  }
779
779
  } };
780
780
  //#endregion
781
+ //#region src/contexts/miscellany.json
782
+ var miscellany_default = { "@context": {
783
+ "as": "https://www.w3.org/ns/activitystreams#",
784
+ "xsd": "http://www.w3.org/2001/XMLSchema#",
785
+ "Hashtag": "as:Hashtag",
786
+ "manuallyApprovesFollowers": {
787
+ "@id": "as:manuallyApprovesFollowers",
788
+ "@type": "xsd:boolean"
789
+ },
790
+ "movedTo": {
791
+ "@id": "as:movedTo",
792
+ "@type": "@id"
793
+ },
794
+ "sensitive": {
795
+ "@id": "as:sensitive",
796
+ "@type": "xsd:boolean"
797
+ }
798
+ } };
799
+ //#endregion
781
800
  //#region src/contexts/schemaorg.json
782
801
  var schemaorg_default = { "@context": {
783
802
  "type": "@type",
@@ -4375,12 +4394,13 @@ const preloadedContexts = {
4375
4394
  "https://gotosocial.org/ns": gotosocial_default,
4376
4395
  "https://w3id.org/fep/5711": fep_5711_default,
4377
4396
  "https://join-lemmy.org/context.json": join_lemmy_default,
4378
- "http://joinmastodon.org/ns": joinmastodon_default
4397
+ "http://joinmastodon.org/ns": joinmastodon_default,
4398
+ "https://purl.archive.org/miscellany": miscellany_default
4379
4399
  };
4380
4400
  //#endregion
4381
4401
  //#region deno.json
4382
4402
  var name = "@fedify/vocab-runtime";
4383
- var version = "2.2.7";
4403
+ var version = "2.2.9";
4384
4404
  //#endregion
4385
4405
  //#region src/link.ts
4386
4406
  const parametersNeedLowerCase = ["rel", "type"];
@@ -4635,8 +4655,8 @@ function logRequest(logger, request) {
4635
4655
  //#endregion
4636
4656
  //#region src/url.ts
4637
4657
  var UrlError = class extends Error {
4638
- constructor(message) {
4639
- super(message);
4658
+ constructor(message, options) {
4659
+ super(message, options);
4640
4660
  this.name = "UrlError";
4641
4661
  }
4642
4662
  };
@@ -4664,10 +4684,37 @@ async function validatePublicUrl(url) {
4664
4684
  let addresses;
4665
4685
  try {
4666
4686
  addresses = await lookup(hostname, { all: true });
4667
- } catch {
4668
- addresses = [];
4687
+ } catch (error) {
4688
+ throw new UrlError("DNS lookup failed", { cause: error });
4689
+ }
4690
+ validateLookupAddresses(addresses);
4691
+ }
4692
+ /**
4693
+ * Validates the IP addresses returned by `node:dns.lookup()`.
4694
+ *
4695
+ * Cloudflare Workers' `node:dns` implementation currently maps every record
4696
+ * in a DNS-over-HTTPS `Answer` array—including CNAME records—to a
4697
+ * `LookupAddress`, even though Node.js specifies that the `address` field must
4698
+ * contain an IPv4 or IPv6 literal. See:
4699
+ * https://github.com/cloudflare/workerd/issues/6886
4700
+ *
4701
+ * Work around that bug by ignoring non-IP entries only when the lookup also
4702
+ * returns at least one actual IP address. This remains fail-closed: a result
4703
+ * containing no IP addresses is rejected, and every returned IP address is
4704
+ * still validated and must be public. This workaround can be revisited once
4705
+ * the Workerd issue is fixed in supported Cloudflare Workers runtimes.
4706
+ *
4707
+ * @internal
4708
+ */
4709
+ function validateLookupAddresses(addresses) {
4710
+ let ipAddressCount = 0;
4711
+ for (const { address } of addresses) {
4712
+ const family = isIP(address);
4713
+ if (family === 0) continue;
4714
+ ipAddressCount++;
4715
+ validatePublicIpAddress(address, family);
4669
4716
  }
4670
- for (const { address, family } of addresses) validatePublicIpAddress(address, family);
4717
+ if (ipAddressCount === 0) throw new UrlError("DNS lookup did not return any IP address");
4671
4718
  }
4672
4719
  function validatePublicIpAddress(address, family) {
4673
4720
  if (family === 4 && isValidPublicIPv4Address(address) || family === 6 && isValidPublicIPv6Address(address)) return;
@@ -1,6 +1,6 @@
1
1
  require("./chunk-Do9eywBl.cjs");
2
- require("./docloader-DwVt5hGF.cjs");
3
- require("./url-Ds8K8vxN.cjs");
2
+ require("./docloader-DF-6vC_M.cjs");
3
+ require("./url-CWU5HT3Y.cjs");
4
4
  require("./key-DTTIntwb.cjs");
5
5
  require("./multibase-F7LtMMsK.cjs");
6
6
  require("./langstr-CbAxaeEZ.cjs");
@@ -1,5 +1,5 @@
1
- import "./docloader-B4HK1gHZ.mjs";
2
- import "./url-m9Qzxy-Y.mjs";
1
+ import "./docloader-C0Vsye4I.mjs";
2
+ import "./url-o0e-aXQY.mjs";
3
3
  import "./key-BeTHFQJK.mjs";
4
4
  import "./multibase-BgU9XRf7.mjs";
5
5
  import "./langstr-Di5AvKpB.mjs";
@@ -1,6 +1,6 @@
1
- import { a as name, i as logRequest, n as createActivityPubRequest, o as version, t as FetchError } from "./request-JJ34PA3H.mjs";
1
+ import { a as name, i as logRequest, n as createActivityPubRequest, o as version, t as FetchError } from "./request-7S_qG7xc.mjs";
2
2
  import { t as HttpHeaderLink } from "./link-NUUWCdnK.mjs";
3
- import { a as validatePublicUrl, t as UrlError } from "./url-m9Qzxy-Y.mjs";
3
+ import { o as validatePublicUrl, t as UrlError } from "./url-o0e-aXQY.mjs";
4
4
  import { getLogger } from "@logtape/logtape";
5
5
  import { SpanKind, SpanStatusCode, trace } from "@opentelemetry/api";
6
6
  //#region src/contexts/activitystreams.json
@@ -771,6 +771,25 @@ var joinmastodon_default = { "@context": {
771
771
  }
772
772
  } };
773
773
  //#endregion
774
+ //#region src/contexts/miscellany.json
775
+ var miscellany_default = { "@context": {
776
+ "as": "https://www.w3.org/ns/activitystreams#",
777
+ "xsd": "http://www.w3.org/2001/XMLSchema#",
778
+ "Hashtag": "as:Hashtag",
779
+ "manuallyApprovesFollowers": {
780
+ "@id": "as:manuallyApprovesFollowers",
781
+ "@type": "xsd:boolean"
782
+ },
783
+ "movedTo": {
784
+ "@id": "as:movedTo",
785
+ "@type": "@id"
786
+ },
787
+ "sensitive": {
788
+ "@id": "as:sensitive",
789
+ "@type": "xsd:boolean"
790
+ }
791
+ } };
792
+ //#endregion
774
793
  //#region src/contexts/schemaorg.json
775
794
  var schemaorg_default = { "@context": {
776
795
  "type": "@type",
@@ -4368,7 +4387,8 @@ const preloadedContexts = {
4368
4387
  "https://gotosocial.org/ns": gotosocial_default,
4369
4388
  "https://w3id.org/fep/5711": fep_5711_default,
4370
4389
  "https://join-lemmy.org/context.json": join_lemmy_default,
4371
- "http://joinmastodon.org/ns": joinmastodon_default
4390
+ "http://joinmastodon.org/ns": joinmastodon_default,
4391
+ "https://purl.archive.org/miscellany": miscellany_default
4372
4392
  };
4373
4393
  //#endregion
4374
4394
  //#region src/docloader.ts
@@ -1,7 +1,7 @@
1
1
  require("./chunk-Do9eywBl.cjs");
2
- const require_request = require("./request-KYO06U6S.cjs");
2
+ const require_request = require("./request-MsnNlEal.cjs");
3
3
  const require_link = require("./link-FguCydMA.cjs");
4
- const require_url = require("./url-Ds8K8vxN.cjs");
4
+ const require_url = require("./url-CWU5HT3Y.cjs");
5
5
  let _logtape_logtape = require("@logtape/logtape");
6
6
  let _opentelemetry_api = require("@opentelemetry/api");
7
7
  //#region src/contexts/activitystreams.json
@@ -772,6 +772,25 @@ var joinmastodon_default = { "@context": {
772
772
  }
773
773
  } };
774
774
  //#endregion
775
+ //#region src/contexts/miscellany.json
776
+ var miscellany_default = { "@context": {
777
+ "as": "https://www.w3.org/ns/activitystreams#",
778
+ "xsd": "http://www.w3.org/2001/XMLSchema#",
779
+ "Hashtag": "as:Hashtag",
780
+ "manuallyApprovesFollowers": {
781
+ "@id": "as:manuallyApprovesFollowers",
782
+ "@type": "xsd:boolean"
783
+ },
784
+ "movedTo": {
785
+ "@id": "as:movedTo",
786
+ "@type": "@id"
787
+ },
788
+ "sensitive": {
789
+ "@id": "as:sensitive",
790
+ "@type": "xsd:boolean"
791
+ }
792
+ } };
793
+ //#endregion
775
794
  //#region src/contexts/schemaorg.json
776
795
  var schemaorg_default = { "@context": {
777
796
  "type": "@type",
@@ -4369,7 +4388,8 @@ const preloadedContexts = {
4369
4388
  "https://gotosocial.org/ns": gotosocial_default,
4370
4389
  "https://w3id.org/fep/5711": fep_5711_default,
4371
4390
  "https://join-lemmy.org/context.json": join_lemmy_default,
4372
- "http://joinmastodon.org/ns": joinmastodon_default
4391
+ "http://joinmastodon.org/ns": joinmastodon_default,
4392
+ "https://purl.archive.org/miscellany": miscellany_default
4373
4393
  };
4374
4394
  //#endregion
4375
4395
  //#region src/docloader.ts
@@ -1,7 +1,7 @@
1
1
  const require_chunk = require("./chunk-Do9eywBl.cjs");
2
- const require_docloader = require("./docloader-DwVt5hGF.cjs");
3
- const require_request = require("./request-KYO06U6S.cjs");
4
- const require_url = require("./url-Ds8K8vxN.cjs");
2
+ const require_docloader = require("./docloader-DF-6vC_M.cjs");
3
+ const require_request = require("./request-MsnNlEal.cjs");
4
+ const require_url = require("./url-CWU5HT3Y.cjs");
5
5
  let node_assert = require("node:assert");
6
6
  let node_test = require("node:test");
7
7
  //#region ../../node_modules/.pnpm/glob-to-regexp@0.4.1/node_modules/glob-to-regexp/index.js
@@ -1,6 +1,6 @@
1
- import { n as preloadedContexts, t as getDocumentLoader } from "./docloader-B4HK1gHZ.mjs";
2
- import { t as FetchError } from "./request-JJ34PA3H.mjs";
3
- import { t as UrlError } from "./url-m9Qzxy-Y.mjs";
1
+ import { n as preloadedContexts, t as getDocumentLoader } from "./docloader-C0Vsye4I.mjs";
2
+ import { t as FetchError } from "./request-7S_qG7xc.mjs";
3
+ import { t as UrlError } from "./url-o0e-aXQY.mjs";
4
4
  import { deepStrictEqual, ok, rejects } from "node:assert";
5
5
  import { test } from "node:test";
6
6
  //#region \0rolldown/runtime.js
@@ -1,7 +1,7 @@
1
1
  import process from "node:process";
2
2
  //#region deno.json
3
3
  var name = "@fedify/vocab-runtime";
4
- var version = "2.2.7";
4
+ var version = "2.2.9";
5
5
  //#endregion
6
6
  //#region src/request.ts
7
7
  /**
@@ -3,7 +3,7 @@ let node_process = require("node:process");
3
3
  node_process = require_chunk.__toESM(node_process);
4
4
  //#region deno.json
5
5
  var name = "@fedify/vocab-runtime";
6
- var version = "2.2.7";
6
+ var version = "2.2.9";
7
7
  //#endregion
8
8
  //#region src/request.ts
9
9
  /**
@@ -1,5 +1,5 @@
1
1
  const require_chunk = require("./chunk-Do9eywBl.cjs");
2
- const require_request = require("./request-KYO06U6S.cjs");
2
+ const require_request = require("./request-MsnNlEal.cjs");
3
3
  let node_assert = require("node:assert");
4
4
  let node_test = require("node:test");
5
5
  let node_process = require("node:process");
@@ -1,4 +1,4 @@
1
- import { o as version, r as getUserAgent } from "./request-JJ34PA3H.mjs";
1
+ import { o as version, r as getUserAgent } from "./request-7S_qG7xc.mjs";
2
2
  import { deepStrictEqual } from "node:assert";
3
3
  import { test } from "node:test";
4
4
  import process from "node:process";
@@ -3,8 +3,8 @@ let node_dns_promises = require("node:dns/promises");
3
3
  let node_net = require("node:net");
4
4
  //#region src/url.ts
5
5
  var UrlError = class extends Error {
6
- constructor(message) {
7
- super(message);
6
+ constructor(message, options) {
7
+ super(message, options);
8
8
  this.name = "UrlError";
9
9
  }
10
10
  };
@@ -32,10 +32,37 @@ async function validatePublicUrl(url) {
32
32
  let addresses;
33
33
  try {
34
34
  addresses = await (0, node_dns_promises.lookup)(hostname, { all: true });
35
- } catch {
36
- addresses = [];
35
+ } catch (error) {
36
+ throw new UrlError("DNS lookup failed", { cause: error });
37
37
  }
38
- for (const { address, family } of addresses) validatePublicIpAddress(address, family);
38
+ validateLookupAddresses(addresses);
39
+ }
40
+ /**
41
+ * Validates the IP addresses returned by `node:dns.lookup()`.
42
+ *
43
+ * Cloudflare Workers' `node:dns` implementation currently maps every record
44
+ * in a DNS-over-HTTPS `Answer` array—including CNAME records—to a
45
+ * `LookupAddress`, even though Node.js specifies that the `address` field must
46
+ * contain an IPv4 or IPv6 literal. See:
47
+ * https://github.com/cloudflare/workerd/issues/6886
48
+ *
49
+ * Work around that bug by ignoring non-IP entries only when the lookup also
50
+ * returns at least one actual IP address. This remains fail-closed: a result
51
+ * containing no IP addresses is rejected, and every returned IP address is
52
+ * still validated and must be public. This workaround can be revisited once
53
+ * the Workerd issue is fixed in supported Cloudflare Workers runtimes.
54
+ *
55
+ * @internal
56
+ */
57
+ function validateLookupAddresses(addresses) {
58
+ let ipAddressCount = 0;
59
+ for (const { address } of addresses) {
60
+ const family = (0, node_net.isIP)(address);
61
+ if (family === 0) continue;
62
+ ipAddressCount++;
63
+ validatePublicIpAddress(address, family);
64
+ }
65
+ if (ipAddressCount === 0) throw new UrlError("DNS lookup did not return any IP address");
39
66
  }
40
67
  function validatePublicIpAddress(address, family) {
41
68
  if (family === 4 && isValidPublicIPv4Address(address) || family === 6 && isValidPublicIPv6Address(address)) return;
@@ -198,6 +225,12 @@ Object.defineProperty(exports, "isValidPublicIPv6Address", {
198
225
  return isValidPublicIPv6Address;
199
226
  }
200
227
  });
228
+ Object.defineProperty(exports, "validateLookupAddresses", {
229
+ enumerable: true,
230
+ get: function() {
231
+ return validateLookupAddresses;
232
+ }
233
+ });
201
234
  Object.defineProperty(exports, "validatePublicUrl", {
202
235
  enumerable: true,
203
236
  get: function() {
@@ -2,8 +2,8 @@ import { lookup } from "node:dns/promises";
2
2
  import { isIP } from "node:net";
3
3
  //#region src/url.ts
4
4
  var UrlError = class extends Error {
5
- constructor(message) {
6
- super(message);
5
+ constructor(message, options) {
6
+ super(message, options);
7
7
  this.name = "UrlError";
8
8
  }
9
9
  };
@@ -31,10 +31,37 @@ async function validatePublicUrl(url) {
31
31
  let addresses;
32
32
  try {
33
33
  addresses = await lookup(hostname, { all: true });
34
- } catch {
35
- addresses = [];
34
+ } catch (error) {
35
+ throw new UrlError("DNS lookup failed", { cause: error });
36
36
  }
37
- for (const { address, family } of addresses) validatePublicIpAddress(address, family);
37
+ validateLookupAddresses(addresses);
38
+ }
39
+ /**
40
+ * Validates the IP addresses returned by `node:dns.lookup()`.
41
+ *
42
+ * Cloudflare Workers' `node:dns` implementation currently maps every record
43
+ * in a DNS-over-HTTPS `Answer` array—including CNAME records—to a
44
+ * `LookupAddress`, even though Node.js specifies that the `address` field must
45
+ * contain an IPv4 or IPv6 literal. See:
46
+ * https://github.com/cloudflare/workerd/issues/6886
47
+ *
48
+ * Work around that bug by ignoring non-IP entries only when the lookup also
49
+ * returns at least one actual IP address. This remains fail-closed: a result
50
+ * containing no IP addresses is rejected, and every returned IP address is
51
+ * still validated and must be public. This workaround can be revisited once
52
+ * the Workerd issue is fixed in supported Cloudflare Workers runtimes.
53
+ *
54
+ * @internal
55
+ */
56
+ function validateLookupAddresses(addresses) {
57
+ let ipAddressCount = 0;
58
+ for (const { address } of addresses) {
59
+ const family = isIP(address);
60
+ if (family === 0) continue;
61
+ ipAddressCount++;
62
+ validatePublicIpAddress(address, family);
63
+ }
64
+ if (ipAddressCount === 0) throw new UrlError("DNS lookup did not return any IP address");
38
65
  }
39
66
  function validatePublicIpAddress(address, family) {
40
67
  if (family === 4 && isValidPublicIPv4Address(address) || family === 6 && isValidPublicIPv6Address(address)) return;
@@ -173,4 +200,4 @@ function matchesIPv6Prefix(address, prefixWords, prefixLength) {
173
200
  return true;
174
201
  }
175
202
  //#endregion
176
- export { validatePublicUrl as a, isValidPublicIPv6Address as i, expandIPv6Address as n, isValidPublicIPv4Address as r, UrlError as t };
203
+ export { validateLookupAddresses as a, isValidPublicIPv6Address as i, expandIPv6Address as n, validatePublicUrl as o, isValidPublicIPv4Address as r, UrlError as t };
@@ -1,5 +1,5 @@
1
1
  require("./chunk-Do9eywBl.cjs");
2
- const require_url = require("./url-Ds8K8vxN.cjs");
2
+ const require_url = require("./url-CWU5HT3Y.cjs");
3
3
  let node_assert = require("node:assert");
4
4
  let node_test = require("node:test");
5
5
  //#region src/url.test.ts
@@ -29,6 +29,35 @@ let node_test = require("node:test");
29
29
  await require_url.validatePublicUrl("https://[2001:db8::1]");
30
30
  await require_url.validatePublicUrl("https://[64:ff9b::8.8.8.8]");
31
31
  });
32
+ (0, node_test.test)("validateLookupAddresses() tolerates Cloudflare Workers CNAME entries", () => {
33
+ require_url.validateLookupAddresses([
34
+ {
35
+ address: "app-host.example.net.",
36
+ family: 4
37
+ },
38
+ {
39
+ address: "93.184.216.34",
40
+ family: 4
41
+ },
42
+ {
43
+ address: "2606:2800:220:1:248:1893:25c8:1946",
44
+ family: 6
45
+ }
46
+ ]);
47
+ });
48
+ (0, node_test.test)("validateLookupAddresses() rejects unsafe or CNAME-only Cloudflare Workers results", () => {
49
+ (0, node_assert.throws)(() => require_url.validateLookupAddresses([{
50
+ address: "private-host.example.net.",
51
+ family: 4
52
+ }, {
53
+ address: "127.0.0.1",
54
+ family: 4
55
+ }]), require_url.UrlError);
56
+ (0, node_assert.throws)(() => require_url.validateLookupAddresses([{
57
+ address: "app-host.example.net.",
58
+ family: 4
59
+ }]), require_url.UrlError);
60
+ });
32
61
  (0, node_test.test)("isValidPublicIPv4Address()", () => {
33
62
  (0, node_assert.ok)(require_url.isValidPublicIPv4Address("8.8.8.8"));
34
63
  (0, node_assert.ok)(!require_url.isValidPublicIPv4Address("192.168.1.1"));
@@ -1,5 +1,5 @@
1
- import { a as validatePublicUrl, i as isValidPublicIPv6Address, n as expandIPv6Address, r as isValidPublicIPv4Address, t as UrlError } from "./url-m9Qzxy-Y.mjs";
2
- import { deepStrictEqual, ok, rejects } from "node:assert";
1
+ import { a as validateLookupAddresses, i as isValidPublicIPv6Address, n as expandIPv6Address, o as validatePublicUrl, r as isValidPublicIPv4Address, t as UrlError } from "./url-o0e-aXQY.mjs";
2
+ import { deepStrictEqual, ok, rejects, throws } from "node:assert";
3
3
  import { test } from "node:test";
4
4
  //#region src/url.test.ts
5
5
  test("validatePublicUrl()", async () => {
@@ -28,6 +28,35 @@ test("validatePublicUrl()", async () => {
28
28
  await validatePublicUrl("https://[2001:db8::1]");
29
29
  await validatePublicUrl("https://[64:ff9b::8.8.8.8]");
30
30
  });
31
+ test("validateLookupAddresses() tolerates Cloudflare Workers CNAME entries", () => {
32
+ validateLookupAddresses([
33
+ {
34
+ address: "app-host.example.net.",
35
+ family: 4
36
+ },
37
+ {
38
+ address: "93.184.216.34",
39
+ family: 4
40
+ },
41
+ {
42
+ address: "2606:2800:220:1:248:1893:25c8:1946",
43
+ family: 6
44
+ }
45
+ ]);
46
+ });
47
+ test("validateLookupAddresses() rejects unsafe or CNAME-only Cloudflare Workers results", () => {
48
+ throws(() => validateLookupAddresses([{
49
+ address: "private-host.example.net.",
50
+ family: 4
51
+ }, {
52
+ address: "127.0.0.1",
53
+ family: 4
54
+ }]), UrlError);
55
+ throws(() => validateLookupAddresses([{
56
+ address: "app-host.example.net.",
57
+ family: 4
58
+ }]), UrlError);
59
+ });
31
60
  test("isValidPublicIPv4Address()", () => {
32
61
  ok(isValidPublicIPv4Address("8.8.8.8"));
33
62
  ok(!isValidPublicIPv4Address("192.168.1.1"));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fedify/vocab-runtime",
3
- "version": "2.2.7",
3
+ "version": "2.2.9",
4
4
  "homepage": "https://fedify.dev/",
5
5
  "repository": {
6
6
  "type": "git",
@@ -0,0 +1,19 @@
1
+ {
2
+ "@context": {
3
+ "as": "https://www.w3.org/ns/activitystreams#",
4
+ "xsd": "http://www.w3.org/2001/XMLSchema#",
5
+ "Hashtag": "as:Hashtag",
6
+ "manuallyApprovesFollowers": {
7
+ "@id": "as:manuallyApprovesFollowers",
8
+ "@type": "xsd:boolean"
9
+ },
10
+ "movedTo": {
11
+ "@id": "as:movedTo",
12
+ "@type": "@id"
13
+ },
14
+ "sensitive": {
15
+ "@id": "as:sensitive",
16
+ "@type": "xsd:boolean"
17
+ }
18
+ }
19
+ }
package/src/contexts.ts CHANGED
@@ -11,6 +11,7 @@ import gotosocial from "./contexts/gotosocial.json" with { type: "json" };
11
11
  import identityV1 from "./contexts/identity-v1.json" with { type: "json" };
12
12
  import joinLemmyContext from "./contexts/join-lemmy.json" with { type: "json" };
13
13
  import joinmastodon from "./contexts/joinmastodon.json" with { type: "json" };
14
+ import miscellany from "./contexts/miscellany.json" with { type: "json" };
14
15
  import schemaorg from "./contexts/schemaorg.json" with { type: "json" };
15
16
  import securityDataIntegrityV1 from "./contexts/security-data-integrity-v1.json" with {
16
17
  type: "json",
@@ -55,6 +56,18 @@ const preloadedContexts: Record<string, unknown> = {
55
56
  // See: https://github.com/mastodon/joinmastodon/issues/148
56
57
  // https://github.com/fedify-dev/fedify/issues/630
57
58
  "http://joinmastodon.org/ns": joinmastodon,
59
+
60
+ // The SWICG "ActivityPub Miscellaneous Terms" context. Bridgy Fed (via
61
+ // granary) references this URL in the @context of every activity it sends,
62
+ // and other implementations use it as the recommended home for widely
63
+ // deployed extension terms like as:Hashtag and as:sensitive. It is served
64
+ // through purl.archive.org (Internet Archive's PURL service), which suffers
65
+ // recurring outages; while it is unreachable, every activity referencing
66
+ // this URL fails JSON-LD expansion before application handlers run. The
67
+ // document is a deliberately stable, versioned SWICG deliverable, so we
68
+ // ship a built-in copy.
69
+ // See: https://swicg.github.io/miscellany/
70
+ "https://purl.archive.org/miscellany": miscellany,
58
71
  };
59
72
 
60
73
  export default preloadedContexts;
package/src/url.test.ts CHANGED
@@ -1,10 +1,11 @@
1
- import { deepStrictEqual, ok, rejects } from "node:assert";
1
+ import { deepStrictEqual, ok, rejects, throws } from "node:assert";
2
2
  import { test } from "node:test";
3
3
  import {
4
4
  expandIPv6Address,
5
5
  isValidPublicIPv4Address,
6
6
  isValidPublicIPv6Address,
7
7
  UrlError,
8
+ validateLookupAddresses,
8
9
  validatePublicUrl,
9
10
  } from "./url.ts";
10
11
 
@@ -65,6 +66,32 @@ test("validatePublicUrl()", async () => {
65
66
  await validatePublicUrl("https://[64:ff9b::8.8.8.8]");
66
67
  });
67
68
 
69
+ test("validateLookupAddresses() tolerates Cloudflare Workers CNAME entries", () => {
70
+ validateLookupAddresses([
71
+ { address: "app-host.example.net.", family: 4 },
72
+ { address: "93.184.216.34", family: 4 },
73
+ { address: "2606:2800:220:1:248:1893:25c8:1946", family: 6 },
74
+ ]);
75
+ });
76
+
77
+ test("validateLookupAddresses() rejects unsafe or CNAME-only Cloudflare Workers results", () => {
78
+ throws(
79
+ () =>
80
+ validateLookupAddresses([
81
+ { address: "private-host.example.net.", family: 4 },
82
+ { address: "127.0.0.1", family: 4 },
83
+ ]),
84
+ UrlError,
85
+ );
86
+ throws(
87
+ () =>
88
+ validateLookupAddresses([
89
+ { address: "app-host.example.net.", family: 4 },
90
+ ]),
91
+ UrlError,
92
+ );
93
+ });
94
+
68
95
  test("isValidPublicIPv4Address()", () => {
69
96
  ok(isValidPublicIPv4Address("8.8.8.8")); // Google DNS
70
97
  ok(!isValidPublicIPv4Address("192.168.1.1")); // private
package/src/url.ts CHANGED
@@ -3,8 +3,8 @@ import { lookup } from "node:dns/promises";
3
3
  import { isIP } from "node:net";
4
4
 
5
5
  export class UrlError extends Error {
6
- constructor(message: string) {
7
- super(message);
6
+ constructor(message: string, options?: { cause?: unknown }) {
7
+ super(message, options);
8
8
  this.name = "UrlError";
9
9
  }
10
10
  }
@@ -51,12 +51,42 @@ export async function validatePublicUrl(url: string): Promise<void> {
51
51
  let addresses: LookupAddress[];
52
52
  try {
53
53
  addresses = await lookup(hostname, { all: true });
54
- } catch {
55
- addresses = [];
54
+ } catch (error) {
55
+ throw new UrlError("DNS lookup failed", { cause: error });
56
56
  }
57
- for (const { address, family } of addresses) {
57
+ validateLookupAddresses(addresses);
58
+ }
59
+
60
+ /**
61
+ * Validates the IP addresses returned by `node:dns.lookup()`.
62
+ *
63
+ * Cloudflare Workers' `node:dns` implementation currently maps every record
64
+ * in a DNS-over-HTTPS `Answer` array—including CNAME records—to a
65
+ * `LookupAddress`, even though Node.js specifies that the `address` field must
66
+ * contain an IPv4 or IPv6 literal. See:
67
+ * https://github.com/cloudflare/workerd/issues/6886
68
+ *
69
+ * Work around that bug by ignoring non-IP entries only when the lookup also
70
+ * returns at least one actual IP address. This remains fail-closed: a result
71
+ * containing no IP addresses is rejected, and every returned IP address is
72
+ * still validated and must be public. This workaround can be revisited once
73
+ * the Workerd issue is fixed in supported Cloudflare Workers runtimes.
74
+ *
75
+ * @internal
76
+ */
77
+ export function validateLookupAddresses(
78
+ addresses: readonly LookupAddress[],
79
+ ): void {
80
+ let ipAddressCount = 0;
81
+ for (const { address } of addresses) {
82
+ const family = isIP(address);
83
+ if (family === 0) continue;
84
+ ipAddressCount++;
58
85
  validatePublicIpAddress(address, family);
59
86
  }
87
+ if (ipAddressCount === 0) {
88
+ throw new UrlError("DNS lookup did not return any IP address");
89
+ }
60
90
  }
61
91
 
62
92
  function validatePublicIpAddress(address: string, family: number): void {