@fedify/vocab-runtime 2.4.0-dev.1668 → 2.4.0-dev.1740

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 (62) hide show
  1. package/deno.json +1 -1
  2. package/dist/internal/jsonld-cache.cjs +1 -1
  3. package/dist/internal/jsonld-cache.d.cts +1 -1
  4. package/dist/internal/jsonld-cache.d.ts +1 -1
  5. package/dist/internal/jsonld-cache.js +1 -1
  6. package/dist/jsonld.cjs +2 -2
  7. package/dist/mod.cjs +26 -8
  8. package/dist/mod.d.cts +4 -2
  9. package/dist/mod.d.ts +4 -3
  10. package/dist/mod.js +23 -5
  11. package/dist/tests/decimal.test.cjs +6 -7
  12. package/dist/tests/decimal.test.mjs +6 -6
  13. package/dist/tests/{digest-DLwUyHCK.cjs → digest-3FeH2Y-Q.cjs} +6 -5
  14. package/dist/tests/{digest-BJlumPNJ.mjs → digest-COC7xDiQ.mjs} +6 -5
  15. package/dist/tests/digest.test.cjs +11 -12
  16. package/dist/tests/digest.test.mjs +11 -11
  17. package/dist/tests/{docloader-u_7i2eIZ.mjs → docloader-BWBddOkN.mjs} +20 -3
  18. package/dist/tests/{docloader-BLOFskDa.cjs → docloader-CS1O52ch.cjs} +20 -4
  19. package/dist/tests/docloader.test.cjs +19 -12
  20. package/dist/tests/docloader.test.mjs +16 -9
  21. package/dist/tests/internal/multicodec.test.cjs +1 -2
  22. package/dist/tests/internal/multicodec.test.mjs +1 -1
  23. package/dist/tests/jsonld-cache.test.cjs +17 -17
  24. package/dist/tests/jsonld-cache.test.mjs +15 -15
  25. package/dist/tests/{key-BcOWDLMm.cjs → key-8Qg2SbZi.cjs} +2 -3
  26. package/dist/tests/{key-Drbw_2tw.mjs → key-C7MNhlkO.mjs} +2 -2
  27. package/dist/tests/key.test.cjs +3 -4
  28. package/dist/tests/key.test.mjs +3 -3
  29. package/dist/tests/langstr.test.cjs +3 -3
  30. package/dist/tests/langstr.test.mjs +1 -1
  31. package/dist/tests/link.test.cjs +1 -2
  32. package/dist/tests/link.test.mjs +1 -1
  33. package/dist/tests/multibase/multibase.test.cjs +1 -2
  34. package/dist/tests/multibase/multibase.test.mjs +1 -1
  35. package/dist/tests/{multibase-DcDQDNnI.cjs → multibase-B5Mea7Ip.cjs} +2 -2
  36. package/dist/tests/{request-BCN6x611.cjs → request-D1rLeiIi.cjs} +3 -3
  37. package/dist/tests/{request-C7z-t67V.mjs → request-DpcxnLEa.mjs} +1 -1
  38. package/dist/tests/request.test.cjs +3 -3
  39. package/dist/tests/request.test.mjs +1 -1
  40. package/dist/tests/temporal.test.cjs +0 -1
  41. package/dist/tests/{url-Du7RQQgP.mjs → url-Basm1L_c.mjs} +33 -6
  42. package/dist/tests/{url-CsOV_B_P.cjs → url-CPfMatUL.cjs} +38 -6
  43. package/dist/tests/url.test.cjs +30 -2
  44. package/dist/tests/url.test.mjs +30 -1
  45. package/dist/{url-DGVbSVVi.js → url-BHAShw9J.js} +32 -5
  46. package/dist/{url-DD4F0ULf.cjs → url-i33c0US6.cjs} +32 -6
  47. package/package.json +1 -1
  48. package/src/contexts/miscellany.json +19 -0
  49. package/src/contexts.ts +13 -0
  50. package/src/url.test.ts +27 -0
  51. package/src/url.ts +35 -5
  52. /package/dist/{docloader-C_dir7Xb.d.ts → docloader-BK627tQQ.d.cts} +0 -0
  53. /package/dist/{docloader-D2DTRiyA.d.cts → docloader-BK627tQQ.d.ts} +0 -0
  54. /package/dist/{chunk-M78iaK0I.cjs → rolldown-runtime-B7lfambq.cjs} +0 -0
  55. /package/dist/tests/{langstr-BOQHfJDr.cjs → langstr-C4Fl80ae.cjs} +0 -0
  56. /package/dist/tests/{langstr-DDLc4833.mjs → langstr-CQ26J_L7.mjs} +0 -0
  57. /package/dist/tests/{link-NUUWCdnK.mjs → link-Cevmc87v.mjs} +0 -0
  58. /package/dist/tests/{link-FguCydMA.cjs → link-DlKm8bEr.cjs} +0 -0
  59. /package/dist/tests/{multibase-Cluwl8OM.mjs → multibase-BPnF_L4e.mjs} +0 -0
  60. /package/dist/tests/{multicodec-CxGVGa91.cjs → multicodec-CLRPeW4N.cjs} +0 -0
  61. /package/dist/tests/{multicodec-CyFp54fI.mjs → multicodec-CRIj_05H.mjs} +0 -0
  62. /package/dist/tests/{chunk-C2EiDwsr.cjs → rolldown-runtime-emK7D4bc.cjs} +0 -0
@@ -1,5 +1,4 @@
1
- require("./chunk-C2EiDwsr.cjs");
2
- const require_url = require("./url-CsOV_B_P.cjs");
1
+ const require_url = require("./url-CPfMatUL.cjs");
3
2
  let node_assert = require("node:assert");
4
3
  let node_test = require("node:test");
5
4
  //#region src/url.test.ts
@@ -271,6 +270,35 @@ let node_test = require("node:test");
271
270
  await require_url.validatePublicUrl("https://[2001:db8::1]");
272
271
  await require_url.validatePublicUrl("https://[64:ff9b::8.8.8.8]");
273
272
  });
273
+ (0, node_test.test)("validateLookupAddresses() tolerates Cloudflare Workers CNAME entries", () => {
274
+ require_url.validateLookupAddresses([
275
+ {
276
+ address: "app-host.example.net.",
277
+ family: 4
278
+ },
279
+ {
280
+ address: "93.184.216.34",
281
+ family: 4
282
+ },
283
+ {
284
+ address: "2606:2800:220:1:248:1893:25c8:1946",
285
+ family: 6
286
+ }
287
+ ]);
288
+ });
289
+ (0, node_test.test)("validateLookupAddresses() rejects unsafe or CNAME-only Cloudflare Workers results", () => {
290
+ (0, node_assert.throws)(() => require_url.validateLookupAddresses([{
291
+ address: "private-host.example.net.",
292
+ family: 4
293
+ }, {
294
+ address: "127.0.0.1",
295
+ family: 4
296
+ }]), require_url.UrlError);
297
+ (0, node_assert.throws)(() => require_url.validateLookupAddresses([{
298
+ address: "app-host.example.net.",
299
+ family: 4
300
+ }]), require_url.UrlError);
301
+ });
274
302
  (0, node_test.test)("isValidPublicIPv4Address()", () => {
275
303
  (0, node_assert.ok)(require_url.isValidPublicIPv4Address("8.8.8.8"));
276
304
  (0, node_assert.ok)(!require_url.isValidPublicIPv4Address("192.168.1.1"));
@@ -1,4 +1,4 @@
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";
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";
2
2
  import { deepStrictEqual, ok, rejects, throws } from "node:assert";
3
3
  import { test } from "node:test";
4
4
  //#region src/url.test.ts
@@ -270,6 +270,35 @@ test("validatePublicUrl()", async () => {
270
270
  await validatePublicUrl("https://[2001:db8::1]");
271
271
  await validatePublicUrl("https://[64:ff9b::8.8.8.8]");
272
272
  });
273
+ test("validateLookupAddresses() tolerates Cloudflare Workers CNAME entries", () => {
274
+ validateLookupAddresses([
275
+ {
276
+ address: "app-host.example.net.",
277
+ family: 4
278
+ },
279
+ {
280
+ address: "93.184.216.34",
281
+ family: 4
282
+ },
283
+ {
284
+ address: "2606:2800:220:1:248:1893:25c8:1946",
285
+ family: 6
286
+ }
287
+ ]);
288
+ });
289
+ test("validateLookupAddresses() rejects unsafe or CNAME-only Cloudflare Workers results", () => {
290
+ throws(() => validateLookupAddresses([{
291
+ address: "private-host.example.net.",
292
+ family: 4
293
+ }, {
294
+ address: "127.0.0.1",
295
+ family: 4
296
+ }]), UrlError);
297
+ throws(() => validateLookupAddresses([{
298
+ address: "app-host.example.net.",
299
+ family: 4
300
+ }]), UrlError);
301
+ });
273
302
  test("isValidPublicIPv4Address()", () => {
274
303
  ok(isValidPublicIPv4Address("8.8.8.8"));
275
304
  ok(!isValidPublicIPv4Address("192.168.1.1"));
@@ -3,8 +3,8 @@ import { lookup } from "node:dns/promises";
3
3
  import { isIP } from "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
  };
@@ -248,10 +248,37 @@ async function validatePublicUrl(url) {
248
248
  let addresses;
249
249
  try {
250
250
  addresses = await lookup(hostname, { all: true });
251
- } catch {
252
- addresses = [];
251
+ } catch (error) {
252
+ throw new UrlError("DNS lookup failed", { cause: error });
253
+ }
254
+ validateLookupAddresses(addresses);
255
+ }
256
+ /**
257
+ * Validates the IP addresses returned by `node:dns.lookup()`.
258
+ *
259
+ * Cloudflare Workers' `node:dns` implementation currently maps every record
260
+ * in a DNS-over-HTTPS `Answer` array—including CNAME records—to a
261
+ * `LookupAddress`, even though Node.js specifies that the `address` field must
262
+ * contain an IPv4 or IPv6 literal. See:
263
+ * https://github.com/cloudflare/workerd/issues/6886
264
+ *
265
+ * Work around that bug by ignoring non-IP entries only when the lookup also
266
+ * returns at least one actual IP address. This remains fail-closed: a result
267
+ * containing no IP addresses is rejected, and every returned IP address is
268
+ * still validated and must be public. This workaround can be revisited once
269
+ * the Workerd issue is fixed in supported Cloudflare Workers runtimes.
270
+ *
271
+ * @internal
272
+ */
273
+ function validateLookupAddresses(addresses) {
274
+ let ipAddressCount = 0;
275
+ for (const { address } of addresses) {
276
+ const family = isIP(address);
277
+ if (family === 0) continue;
278
+ ipAddressCount++;
279
+ validatePublicIpAddress(address, family);
253
280
  }
254
- for (const { address, family } of addresses) validatePublicIpAddress(address, family);
281
+ if (ipAddressCount === 0) throw new UrlError("DNS lookup did not return any IP address");
255
282
  }
256
283
  function validatePublicIpAddress(address, family) {
257
284
  if (family === 4 && isValidPublicIPv4Address(address) || family === 6 && isValidPublicIPv6Address(address)) return;
@@ -1,11 +1,10 @@
1
1
 
2
- require("./chunk-M78iaK0I.cjs");
3
2
  let node_dns_promises = require("node:dns/promises");
4
3
  let node_net = require("node:net");
5
4
  //#region src/url.ts
6
5
  var UrlError = class extends Error {
7
- constructor(message) {
8
- super(message);
6
+ constructor(message, options) {
7
+ super(message, options);
9
8
  this.name = "UrlError";
10
9
  }
11
10
  };
@@ -249,10 +248,37 @@ async function validatePublicUrl(url) {
249
248
  let addresses;
250
249
  try {
251
250
  addresses = await (0, node_dns_promises.lookup)(hostname, { all: true });
252
- } catch {
253
- addresses = [];
251
+ } catch (error) {
252
+ throw new UrlError("DNS lookup failed", { cause: error });
253
+ }
254
+ validateLookupAddresses(addresses);
255
+ }
256
+ /**
257
+ * Validates the IP addresses returned by `node:dns.lookup()`.
258
+ *
259
+ * Cloudflare Workers' `node:dns` implementation currently maps every record
260
+ * in a DNS-over-HTTPS `Answer` array—including CNAME records—to a
261
+ * `LookupAddress`, even though Node.js specifies that the `address` field must
262
+ * contain an IPv4 or IPv6 literal. See:
263
+ * https://github.com/cloudflare/workerd/issues/6886
264
+ *
265
+ * Work around that bug by ignoring non-IP entries only when the lookup also
266
+ * returns at least one actual IP address. This remains fail-closed: a result
267
+ * containing no IP addresses is rejected, and every returned IP address is
268
+ * still validated and must be public. This workaround can be revisited once
269
+ * the Workerd issue is fixed in supported Cloudflare Workers runtimes.
270
+ *
271
+ * @internal
272
+ */
273
+ function validateLookupAddresses(addresses) {
274
+ let ipAddressCount = 0;
275
+ for (const { address } of addresses) {
276
+ const family = (0, node_net.isIP)(address);
277
+ if (family === 0) continue;
278
+ ipAddressCount++;
279
+ validatePublicIpAddress(address, family);
254
280
  }
255
- for (const { address, family } of addresses) validatePublicIpAddress(address, family);
281
+ if (ipAddressCount === 0) throw new UrlError("DNS lookup did not return any IP address");
256
282
  }
257
283
  function validatePublicIpAddress(address, family) {
258
284
  if (family === 4 && isValidPublicIPv4Address(address) || family === 6 && isValidPublicIPv6Address(address)) return;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fedify/vocab-runtime",
3
- "version": "2.4.0-dev.1668+90797b01",
3
+ "version": "2.4.0-dev.1740+32e9952c",
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
@@ -13,6 +13,7 @@ import gotosocial from "./contexts/gotosocial.json" with { type: "json" };
13
13
  import identityV1 from "./contexts/identity-v1.json" with { type: "json" };
14
14
  import joinLemmyContext from "./contexts/join-lemmy.json" with { type: "json" };
15
15
  import joinmastodon from "./contexts/joinmastodon.json" with { type: "json" };
16
+ import miscellany from "./contexts/miscellany.json" with { type: "json" };
16
17
  import schemaorg from "./contexts/schemaorg.json" with { type: "json" };
17
18
  import securityDataIntegrityV1 from "./contexts/security-data-integrity-v1.json" with {
18
19
  type: "json",
@@ -59,6 +60,18 @@ const preloadedContexts: Record<string, unknown> = {
59
60
  // See: https://github.com/mastodon/joinmastodon/issues/148
60
61
  // https://github.com/fedify-dev/fedify/issues/630
61
62
  "http://joinmastodon.org/ns": joinmastodon,
63
+
64
+ // The SWICG "ActivityPub Miscellaneous Terms" context. Bridgy Fed (via
65
+ // granary) references this URL in the @context of every activity it sends,
66
+ // and other implementations use it as the recommended home for widely
67
+ // deployed extension terms like as:Hashtag and as:sensitive. It is served
68
+ // through purl.archive.org (Internet Archive's PURL service), which suffers
69
+ // recurring outages; while it is unreachable, every activity referencing
70
+ // this URL fails JSON-LD expansion before application handlers run. The
71
+ // document is a deliberately stable, versioned SWICG deliverable, so we
72
+ // ship a built-in copy.
73
+ // See: https://swicg.github.io/miscellany/
74
+ "https://purl.archive.org/miscellany": miscellany,
62
75
  };
63
76
 
64
77
  export default preloadedContexts;
package/src/url.test.ts CHANGED
@@ -15,6 +15,7 @@ import {
15
15
  parseIri,
16
16
  parseJsonLdId,
17
17
  UrlError,
18
+ validateLookupAddresses,
18
19
  validatePublicUrl,
19
20
  } from "./url.ts";
20
21
 
@@ -718,6 +719,32 @@ test("validatePublicUrl()", async () => {
718
719
  await validatePublicUrl("https://[64:ff9b::8.8.8.8]");
719
720
  });
720
721
 
722
+ test("validateLookupAddresses() tolerates Cloudflare Workers CNAME entries", () => {
723
+ validateLookupAddresses([
724
+ { address: "app-host.example.net.", family: 4 },
725
+ { address: "93.184.216.34", family: 4 },
726
+ { address: "2606:2800:220:1:248:1893:25c8:1946", family: 6 },
727
+ ]);
728
+ });
729
+
730
+ test("validateLookupAddresses() rejects unsafe or CNAME-only Cloudflare Workers results", () => {
731
+ throws(
732
+ () =>
733
+ validateLookupAddresses([
734
+ { address: "private-host.example.net.", family: 4 },
735
+ { address: "127.0.0.1", family: 4 },
736
+ ]),
737
+ UrlError,
738
+ );
739
+ throws(
740
+ () =>
741
+ validateLookupAddresses([
742
+ { address: "app-host.example.net.", family: 4 },
743
+ ]),
744
+ UrlError,
745
+ );
746
+ });
747
+
721
748
  test("isValidPublicIPv4Address()", () => {
722
749
  ok(isValidPublicIPv4Address("8.8.8.8")); // Google DNS
723
750
  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
  }
@@ -388,12 +388,42 @@ export async function validatePublicUrl(url: string): Promise<void> {
388
388
  let addresses: LookupAddress[];
389
389
  try {
390
390
  addresses = await lookup(hostname, { all: true });
391
- } catch {
392
- addresses = [];
391
+ } catch (error) {
392
+ throw new UrlError("DNS lookup failed", { cause: error });
393
393
  }
394
- for (const { address, family } of addresses) {
394
+ validateLookupAddresses(addresses);
395
+ }
396
+
397
+ /**
398
+ * Validates the IP addresses returned by `node:dns.lookup()`.
399
+ *
400
+ * Cloudflare Workers' `node:dns` implementation currently maps every record
401
+ * in a DNS-over-HTTPS `Answer` array—including CNAME records—to a
402
+ * `LookupAddress`, even though Node.js specifies that the `address` field must
403
+ * contain an IPv4 or IPv6 literal. See:
404
+ * https://github.com/cloudflare/workerd/issues/6886
405
+ *
406
+ * Work around that bug by ignoring non-IP entries only when the lookup also
407
+ * returns at least one actual IP address. This remains fail-closed: a result
408
+ * containing no IP addresses is rejected, and every returned IP address is
409
+ * still validated and must be public. This workaround can be revisited once
410
+ * the Workerd issue is fixed in supported Cloudflare Workers runtimes.
411
+ *
412
+ * @internal
413
+ */
414
+ export function validateLookupAddresses(
415
+ addresses: readonly LookupAddress[],
416
+ ): void {
417
+ let ipAddressCount = 0;
418
+ for (const { address } of addresses) {
419
+ const family = isIP(address);
420
+ if (family === 0) continue;
421
+ ipAddressCount++;
395
422
  validatePublicIpAddress(address, family);
396
423
  }
424
+ if (ipAddressCount === 0) {
425
+ throw new UrlError("DNS lookup did not return any IP address");
426
+ }
397
427
  }
398
428
 
399
429
  function validatePublicIpAddress(address: string, family: number): void {