@fedify/vocab-runtime 2.3.2 → 2.3.4
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 +1 -1
- package/dist/mod.cjs +50 -6
- package/dist/mod.d.cts +3 -1
- package/dist/mod.d.ts +3 -2
- package/dist/mod.js +50 -6
- package/dist/tests/decimal.test.cjs +2 -2
- package/dist/tests/decimal.test.mjs +2 -2
- package/dist/tests/{docloader-DmWDiCxH.cjs → docloader-BFM7h1Xa.cjs} +19 -2
- package/dist/tests/{docloader-BOGED_Op.mjs → docloader-BGlBKr7D.mjs} +19 -2
- package/dist/tests/docloader.test.cjs +3 -3
- package/dist/tests/docloader.test.mjs +3 -3
- package/dist/tests/{request-ClAnB4aH.cjs → request-BOB5ItB9.cjs} +1 -1
- package/dist/tests/{request-DcqSc5NX.mjs → request-BaUm0-cs.mjs} +1 -1
- package/dist/tests/request.test.cjs +1 -1
- package/dist/tests/request.test.mjs +1 -1
- package/dist/tests/{url-C20FhC7p.cjs → url-BF_Nb_Cu.cjs} +38 -5
- package/dist/tests/{url-m9Qzxy-Y.mjs → url-o0e-aXQY.mjs} +33 -6
- package/dist/tests/url.test.cjs +30 -1
- package/dist/tests/url.test.mjs +31 -2
- package/package.json +1 -1
- package/src/contexts/miscellany.json +19 -0
- package/src/contexts.ts +13 -0
- package/src/url.test.ts +28 -1
- package/src/url.ts +35 -5
package/deno.json
CHANGED
package/dist/mod.cjs
CHANGED
|
@@ -4341,12 +4341,29 @@ const preloadedContexts = {
|
|
|
4341
4341
|
"@id": "toot:attributionDomains",
|
|
4342
4342
|
"@type": "@id"
|
|
4343
4343
|
}
|
|
4344
|
+
} },
|
|
4345
|
+
"https://purl.archive.org/miscellany": { "@context": {
|
|
4346
|
+
"as": "https://www.w3.org/ns/activitystreams#",
|
|
4347
|
+
"xsd": "http://www.w3.org/2001/XMLSchema#",
|
|
4348
|
+
"Hashtag": "as:Hashtag",
|
|
4349
|
+
"manuallyApprovesFollowers": {
|
|
4350
|
+
"@id": "as:manuallyApprovesFollowers",
|
|
4351
|
+
"@type": "xsd:boolean"
|
|
4352
|
+
},
|
|
4353
|
+
"movedTo": {
|
|
4354
|
+
"@id": "as:movedTo",
|
|
4355
|
+
"@type": "@id"
|
|
4356
|
+
},
|
|
4357
|
+
"sensitive": {
|
|
4358
|
+
"@id": "as:sensitive",
|
|
4359
|
+
"@type": "xsd:boolean"
|
|
4360
|
+
}
|
|
4344
4361
|
} }
|
|
4345
4362
|
};
|
|
4346
4363
|
//#endregion
|
|
4347
4364
|
//#region deno.json
|
|
4348
4365
|
var name = "@fedify/vocab-runtime";
|
|
4349
|
-
var version = "2.3.
|
|
4366
|
+
var version = "2.3.4";
|
|
4350
4367
|
//#endregion
|
|
4351
4368
|
//#region src/link.ts
|
|
4352
4369
|
const parametersNeedLowerCase = ["rel", "type"];
|
|
@@ -4601,8 +4618,8 @@ function logRequest(logger, request) {
|
|
|
4601
4618
|
//#endregion
|
|
4602
4619
|
//#region src/url.ts
|
|
4603
4620
|
var UrlError = class extends Error {
|
|
4604
|
-
constructor(message) {
|
|
4605
|
-
super(message);
|
|
4621
|
+
constructor(message, options) {
|
|
4622
|
+
super(message, options);
|
|
4606
4623
|
this.name = "UrlError";
|
|
4607
4624
|
}
|
|
4608
4625
|
};
|
|
@@ -4630,10 +4647,37 @@ async function validatePublicUrl(url) {
|
|
|
4630
4647
|
let addresses;
|
|
4631
4648
|
try {
|
|
4632
4649
|
addresses = await (0, node_dns_promises.lookup)(hostname, { all: true });
|
|
4633
|
-
} catch {
|
|
4634
|
-
|
|
4650
|
+
} catch (error) {
|
|
4651
|
+
throw new UrlError("DNS lookup failed", { cause: error });
|
|
4652
|
+
}
|
|
4653
|
+
validateLookupAddresses(addresses);
|
|
4654
|
+
}
|
|
4655
|
+
/**
|
|
4656
|
+
* Validates the IP addresses returned by `node:dns.lookup()`.
|
|
4657
|
+
*
|
|
4658
|
+
* Cloudflare Workers' `node:dns` implementation currently maps every record
|
|
4659
|
+
* in a DNS-over-HTTPS `Answer` array—including CNAME records—to a
|
|
4660
|
+
* `LookupAddress`, even though Node.js specifies that the `address` field must
|
|
4661
|
+
* contain an IPv4 or IPv6 literal. See:
|
|
4662
|
+
* https://github.com/cloudflare/workerd/issues/6886
|
|
4663
|
+
*
|
|
4664
|
+
* Work around that bug by ignoring non-IP entries only when the lookup also
|
|
4665
|
+
* returns at least one actual IP address. This remains fail-closed: a result
|
|
4666
|
+
* containing no IP addresses is rejected, and every returned IP address is
|
|
4667
|
+
* still validated and must be public. This workaround can be revisited once
|
|
4668
|
+
* the Workerd issue is fixed in supported Cloudflare Workers runtimes.
|
|
4669
|
+
*
|
|
4670
|
+
* @internal
|
|
4671
|
+
*/
|
|
4672
|
+
function validateLookupAddresses(addresses) {
|
|
4673
|
+
let ipAddressCount = 0;
|
|
4674
|
+
for (const { address } of addresses) {
|
|
4675
|
+
const family = (0, node_net.isIP)(address);
|
|
4676
|
+
if (family === 0) continue;
|
|
4677
|
+
ipAddressCount++;
|
|
4678
|
+
validatePublicIpAddress(address, family);
|
|
4635
4679
|
}
|
|
4636
|
-
|
|
4680
|
+
if (ipAddressCount === 0) throw new UrlError("DNS lookup did not return any IP address");
|
|
4637
4681
|
}
|
|
4638
4682
|
function validatePublicIpAddress(address, family) {
|
|
4639
4683
|
if (family === 4 && isValidPublicIPv4Address(address) || family === 6 && isValidPublicIPv6Address(address)) return;
|
package/dist/mod.d.cts
CHANGED
|
@@ -458,7 +458,9 @@ type PropertyPreprocessor<T = unknown> = (value: Json, context: PropertyPreproce
|
|
|
458
458
|
//#endregion
|
|
459
459
|
//#region src/url.d.ts
|
|
460
460
|
declare class UrlError extends Error {
|
|
461
|
-
constructor(message: string
|
|
461
|
+
constructor(message: string, options?: {
|
|
462
|
+
cause?: unknown;
|
|
463
|
+
});
|
|
462
464
|
}
|
|
463
465
|
/**
|
|
464
466
|
* Validates a URL to prevent SSRF attacks.
|
package/dist/mod.d.ts
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
/// <reference lib="esnext.temporal" />
|
|
2
2
|
import { Logger } from "@logtape/logtape";
|
|
3
3
|
import { TracerProvider } from "@opentelemetry/api";
|
|
4
|
-
|
|
5
4
|
//#region src/contexts.d.ts
|
|
6
5
|
declare const preloadedContexts: Record<string, unknown>;
|
|
7
6
|
//#endregion
|
|
@@ -458,7 +457,9 @@ type PropertyPreprocessor<T = unknown> = (value: Json, context: PropertyPreproce
|
|
|
458
457
|
//#endregion
|
|
459
458
|
//#region src/url.d.ts
|
|
460
459
|
declare class UrlError extends Error {
|
|
461
|
-
constructor(message: string
|
|
460
|
+
constructor(message: string, options?: {
|
|
461
|
+
cause?: unknown;
|
|
462
|
+
});
|
|
462
463
|
}
|
|
463
464
|
/**
|
|
464
465
|
* Validates a URL to prevent SSRF attacks.
|
package/dist/mod.js
CHANGED
|
@@ -4337,12 +4337,29 @@ const preloadedContexts = {
|
|
|
4337
4337
|
"@id": "toot:attributionDomains",
|
|
4338
4338
|
"@type": "@id"
|
|
4339
4339
|
}
|
|
4340
|
+
} },
|
|
4341
|
+
"https://purl.archive.org/miscellany": { "@context": {
|
|
4342
|
+
"as": "https://www.w3.org/ns/activitystreams#",
|
|
4343
|
+
"xsd": "http://www.w3.org/2001/XMLSchema#",
|
|
4344
|
+
"Hashtag": "as:Hashtag",
|
|
4345
|
+
"manuallyApprovesFollowers": {
|
|
4346
|
+
"@id": "as:manuallyApprovesFollowers",
|
|
4347
|
+
"@type": "xsd:boolean"
|
|
4348
|
+
},
|
|
4349
|
+
"movedTo": {
|
|
4350
|
+
"@id": "as:movedTo",
|
|
4351
|
+
"@type": "@id"
|
|
4352
|
+
},
|
|
4353
|
+
"sensitive": {
|
|
4354
|
+
"@id": "as:sensitive",
|
|
4355
|
+
"@type": "xsd:boolean"
|
|
4356
|
+
}
|
|
4340
4357
|
} }
|
|
4341
4358
|
};
|
|
4342
4359
|
//#endregion
|
|
4343
4360
|
//#region deno.json
|
|
4344
4361
|
var name = "@fedify/vocab-runtime";
|
|
4345
|
-
var version = "2.3.
|
|
4362
|
+
var version = "2.3.4";
|
|
4346
4363
|
//#endregion
|
|
4347
4364
|
//#region src/link.ts
|
|
4348
4365
|
const parametersNeedLowerCase = ["rel", "type"];
|
|
@@ -4597,8 +4614,8 @@ function logRequest(logger, request) {
|
|
|
4597
4614
|
//#endregion
|
|
4598
4615
|
//#region src/url.ts
|
|
4599
4616
|
var UrlError = class extends Error {
|
|
4600
|
-
constructor(message) {
|
|
4601
|
-
super(message);
|
|
4617
|
+
constructor(message, options) {
|
|
4618
|
+
super(message, options);
|
|
4602
4619
|
this.name = "UrlError";
|
|
4603
4620
|
}
|
|
4604
4621
|
};
|
|
@@ -4626,10 +4643,37 @@ async function validatePublicUrl(url) {
|
|
|
4626
4643
|
let addresses;
|
|
4627
4644
|
try {
|
|
4628
4645
|
addresses = await lookup(hostname, { all: true });
|
|
4629
|
-
} catch {
|
|
4630
|
-
|
|
4646
|
+
} catch (error) {
|
|
4647
|
+
throw new UrlError("DNS lookup failed", { cause: error });
|
|
4648
|
+
}
|
|
4649
|
+
validateLookupAddresses(addresses);
|
|
4650
|
+
}
|
|
4651
|
+
/**
|
|
4652
|
+
* Validates the IP addresses returned by `node:dns.lookup()`.
|
|
4653
|
+
*
|
|
4654
|
+
* Cloudflare Workers' `node:dns` implementation currently maps every record
|
|
4655
|
+
* in a DNS-over-HTTPS `Answer` array—including CNAME records—to a
|
|
4656
|
+
* `LookupAddress`, even though Node.js specifies that the `address` field must
|
|
4657
|
+
* contain an IPv4 or IPv6 literal. See:
|
|
4658
|
+
* https://github.com/cloudflare/workerd/issues/6886
|
|
4659
|
+
*
|
|
4660
|
+
* Work around that bug by ignoring non-IP entries only when the lookup also
|
|
4661
|
+
* returns at least one actual IP address. This remains fail-closed: a result
|
|
4662
|
+
* containing no IP addresses is rejected, and every returned IP address is
|
|
4663
|
+
* still validated and must be public. This workaround can be revisited once
|
|
4664
|
+
* the Workerd issue is fixed in supported Cloudflare Workers runtimes.
|
|
4665
|
+
*
|
|
4666
|
+
* @internal
|
|
4667
|
+
*/
|
|
4668
|
+
function validateLookupAddresses(addresses) {
|
|
4669
|
+
let ipAddressCount = 0;
|
|
4670
|
+
for (const { address } of addresses) {
|
|
4671
|
+
const family = isIP(address);
|
|
4672
|
+
if (family === 0) continue;
|
|
4673
|
+
ipAddressCount++;
|
|
4674
|
+
validatePublicIpAddress(address, family);
|
|
4631
4675
|
}
|
|
4632
|
-
|
|
4676
|
+
if (ipAddressCount === 0) throw new UrlError("DNS lookup did not return any IP address");
|
|
4633
4677
|
}
|
|
4634
4678
|
function validatePublicIpAddress(address, family) {
|
|
4635
4679
|
if (family === 4 && isValidPublicIPv4Address(address) || family === 6 && isValidPublicIPv6Address(address)) return;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
require("./chunk-C2EiDwsr.cjs");
|
|
2
|
-
require("./docloader-
|
|
3
|
-
require("./url-
|
|
2
|
+
require("./docloader-BFM7h1Xa.cjs");
|
|
3
|
+
require("./url-BF_Nb_Cu.cjs");
|
|
4
4
|
require("./key-pMmqUKuo.cjs");
|
|
5
5
|
require("./multibase-Bz_UUDtL.cjs");
|
|
6
6
|
require("./langstr-CbAxaeEZ.cjs");
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
require("./chunk-C2EiDwsr.cjs");
|
|
2
|
-
const require_request = require("./request-
|
|
2
|
+
const require_request = require("./request-BOB5ItB9.cjs");
|
|
3
3
|
const require_link = require("./link-FguCydMA.cjs");
|
|
4
|
-
const require_url = require("./url-
|
|
4
|
+
const require_url = require("./url-BF_Nb_Cu.cjs");
|
|
5
5
|
let _logtape_logtape = require("@logtape/logtape");
|
|
6
6
|
let _opentelemetry_api = require("@opentelemetry/api");
|
|
7
7
|
//#endregion
|
|
@@ -4331,6 +4331,23 @@ const preloadedContexts = {
|
|
|
4331
4331
|
"@id": "toot:attributionDomains",
|
|
4332
4332
|
"@type": "@id"
|
|
4333
4333
|
}
|
|
4334
|
+
} },
|
|
4335
|
+
"https://purl.archive.org/miscellany": { "@context": {
|
|
4336
|
+
"as": "https://www.w3.org/ns/activitystreams#",
|
|
4337
|
+
"xsd": "http://www.w3.org/2001/XMLSchema#",
|
|
4338
|
+
"Hashtag": "as:Hashtag",
|
|
4339
|
+
"manuallyApprovesFollowers": {
|
|
4340
|
+
"@id": "as:manuallyApprovesFollowers",
|
|
4341
|
+
"@type": "xsd:boolean"
|
|
4342
|
+
},
|
|
4343
|
+
"movedTo": {
|
|
4344
|
+
"@id": "as:movedTo",
|
|
4345
|
+
"@type": "@id"
|
|
4346
|
+
},
|
|
4347
|
+
"sensitive": {
|
|
4348
|
+
"@id": "as:sensitive",
|
|
4349
|
+
"@type": "xsd:boolean"
|
|
4350
|
+
}
|
|
4334
4351
|
} }
|
|
4335
4352
|
};
|
|
4336
4353
|
//#endregion
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { a as name, i as logRequest, n as createActivityPubRequest, o as version, t as FetchError } from "./request-
|
|
1
|
+
import { a as name, i as logRequest, n as createActivityPubRequest, o as version, t as FetchError } from "./request-BaUm0-cs.mjs";
|
|
2
2
|
import { t as HttpHeaderLink } from "./link-NUUWCdnK.mjs";
|
|
3
|
-
import {
|
|
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
|
//#endregion
|
|
@@ -4330,6 +4330,23 @@ const preloadedContexts = {
|
|
|
4330
4330
|
"@id": "toot:attributionDomains",
|
|
4331
4331
|
"@type": "@id"
|
|
4332
4332
|
}
|
|
4333
|
+
} },
|
|
4334
|
+
"https://purl.archive.org/miscellany": { "@context": {
|
|
4335
|
+
"as": "https://www.w3.org/ns/activitystreams#",
|
|
4336
|
+
"xsd": "http://www.w3.org/2001/XMLSchema#",
|
|
4337
|
+
"Hashtag": "as:Hashtag",
|
|
4338
|
+
"manuallyApprovesFollowers": {
|
|
4339
|
+
"@id": "as:manuallyApprovesFollowers",
|
|
4340
|
+
"@type": "xsd:boolean"
|
|
4341
|
+
},
|
|
4342
|
+
"movedTo": {
|
|
4343
|
+
"@id": "as:movedTo",
|
|
4344
|
+
"@type": "@id"
|
|
4345
|
+
},
|
|
4346
|
+
"sensitive": {
|
|
4347
|
+
"@id": "as:sensitive",
|
|
4348
|
+
"@type": "xsd:boolean"
|
|
4349
|
+
}
|
|
4333
4350
|
} }
|
|
4334
4351
|
};
|
|
4335
4352
|
//#endregion
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
const require_chunk = require("./chunk-C2EiDwsr.cjs");
|
|
2
|
-
const require_docloader = require("./docloader-
|
|
3
|
-
const require_request = require("./request-
|
|
4
|
-
const require_url = require("./url-
|
|
2
|
+
const require_docloader = require("./docloader-BFM7h1Xa.cjs");
|
|
3
|
+
const require_request = require("./request-BOB5ItB9.cjs");
|
|
4
|
+
const require_url = require("./url-BF_Nb_Cu.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-
|
|
2
|
-
import { t as FetchError } from "./request-
|
|
3
|
-
import { t as UrlError } from "./url-
|
|
1
|
+
import { n as preloadedContexts, t as getDocumentLoader } from "./docloader-BGlBKr7D.mjs";
|
|
2
|
+
import { t as FetchError } from "./request-BaUm0-cs.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,5 +1,5 @@
|
|
|
1
1
|
const require_chunk = require("./chunk-C2EiDwsr.cjs");
|
|
2
|
-
const require_request = require("./request-
|
|
2
|
+
const require_request = require("./request-BOB5ItB9.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-
|
|
1
|
+
import { o as version, r as getUserAgent } from "./request-BaUm0-cs.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
|
-
|
|
35
|
+
} catch (error) {
|
|
36
|
+
throw new UrlError("DNS lookup failed", { cause: error });
|
|
37
37
|
}
|
|
38
|
-
|
|
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
|
-
|
|
34
|
+
} catch (error) {
|
|
35
|
+
throw new UrlError("DNS lookup failed", { cause: error });
|
|
36
36
|
}
|
|
37
|
-
|
|
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 {
|
|
203
|
+
export { validateLookupAddresses as a, isValidPublicIPv6Address as i, expandIPv6Address as n, validatePublicUrl as o, isValidPublicIPv4Address as r, UrlError as t };
|
package/dist/tests/url.test.cjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
require("./chunk-C2EiDwsr.cjs");
|
|
2
|
-
const require_url = require("./url-
|
|
2
|
+
const require_url = require("./url-BF_Nb_Cu.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"));
|
package/dist/tests/url.test.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { a as
|
|
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
|
@@ -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
|
-
|
|
54
|
+
} catch (error) {
|
|
55
|
+
throw new UrlError("DNS lookup failed", { cause: error });
|
|
56
56
|
}
|
|
57
|
-
|
|
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 {
|