@fedify/vocab-runtime 2.4.0-dev.2155 → 2.4.0-dev.2157
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/{docloader-oRpq1nQ8.d.cts → docloader-ClQraSWr.d.cts} +3 -0
- package/dist/{docloader-oRpq1nQ8.d.ts → docloader-ClQraSWr.d.ts} +3 -0
- package/dist/internal/jsonld-cache.cjs +1 -1
- package/dist/internal/jsonld-cache.d.cts +1 -1
- package/dist/internal/jsonld-cache.d.ts +1 -1
- package/dist/internal/jsonld-cache.js +1 -1
- package/dist/internal/portable-dereference.cjs +2 -2
- package/dist/internal/portable-dereference.d.cts +2 -2
- package/dist/internal/portable-dereference.d.ts +2 -2
- package/dist/internal/portable-dereference.js +2 -2
- package/dist/internal/signed-representation.cjs +1 -1
- package/dist/internal/signed-representation.js +1 -1
- package/dist/mod.cjs +18 -6
- package/dist/mod.d.cts +9 -2
- package/dist/mod.d.ts +9 -2
- package/dist/mod.js +18 -6
- package/dist/{portable-rwhiyMvY.d.ts → portable-DRtvOxTb.d.ts} +1 -1
- package/dist/{portable-Drfl02vZ.d.cts → portable-ilkFPcXa.d.cts} +1 -1
- package/dist/tests/{body-CLlfvGkr.cjs → body-CUsW69cS.cjs} +1 -1
- package/dist/tests/{body-BJRBGr6n.mjs → body-Dl8UzKxO.mjs} +1 -1
- package/dist/tests/body.test.cjs +1 -1
- package/dist/tests/body.test.mjs +1 -1
- package/dist/tests/decimal.test.cjs +2 -2
- package/dist/tests/decimal.test.mjs +2 -2
- package/dist/tests/{docloader-DsoAI6Tk.mjs → docloader-DqEFkmoW.mjs} +19 -7
- package/dist/tests/{docloader-s_sA2gs3.cjs → docloader-DqS3g5mL.cjs} +19 -7
- package/dist/tests/docloader.test.cjs +241 -4
- package/dist/tests/docloader.test.mjs +240 -4
- package/dist/tests/internal/portable-dereference.test.cjs +3 -3
- package/dist/tests/internal/portable-dereference.test.mjs +3 -3
- package/dist/tests/{jsonld-cache-DnZS0z1G.mjs → jsonld-cache-B7PKo3Lu.mjs} +1 -1
- package/dist/tests/{jsonld-cache-9jap4yM0.cjs → jsonld-cache-BO-QXxIc.cjs} +1 -1
- package/dist/tests/jsonld-cache.test.cjs +2 -2
- package/dist/tests/jsonld-cache.test.mjs +2 -2
- package/dist/tests/{request-BzZ8Wn6I.mjs → request-BGCgNeTu.mjs} +1 -1
- package/dist/tests/{request-Dt_khD3j.cjs → request-CbPKU-j6.cjs} +1 -1
- package/dist/tests/request.test.cjs +1 -1
- package/dist/tests/request.test.mjs +1 -1
- package/dist/tests/signed-representation.test.cjs +1 -1
- package/dist/tests/signed-representation.test.mjs +1 -1
- package/dist/tests/{url-DgJM9WJn.mjs → url-D7qRF-dF.mjs} +14 -4
- package/dist/tests/{url-BqlldzT8.cjs → url-gLKqAO1H.cjs} +15 -3
- package/dist/tests/url.test.cjs +36 -3
- package/dist/tests/url.test.mjs +37 -4
- package/dist/{url-CEhi4hb4.js → url-BHZYe8Vu.js} +14 -4
- package/dist/{url-Cke4irYD.cjs → url-P6l9s9dN.cjs} +15 -3
- package/package.json +1 -1
- package/src/contexts.ts +7 -1
- package/src/docloader.test.ts +307 -1
- package/src/docloader.ts +32 -7
- package/src/url.test.ts +29 -3
- package/src/url.ts +21 -5
- package/dist/{contexts-2hEbs3Qk.cjs → contexts-BK1CqBR5.cjs} +22 -22
- package/dist/{contexts-DoZUIW7y.js → contexts-DPuJ4UYL.js} +22 -22
- package/dist/tests/{contexts-BBX_qhBL.mjs → contexts-CIKsin4e.mjs} +9 -9
- package/dist/tests/{contexts-Ch3v2Cgu.cjs → contexts-DizzBjz4.cjs} +9 -9
|
@@ -1,10 +1,17 @@
|
|
|
1
|
-
import
|
|
1
|
+
import dns 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
|
+
/**
|
|
6
|
+
* The failure category. Defaults to `"disallowed"`.
|
|
7
|
+
* `"dns"` includes lookup errors and results with no usable IP addresses.
|
|
8
|
+
* @since 2.0.29
|
|
9
|
+
*/
|
|
10
|
+
reason;
|
|
5
11
|
constructor(message, options) {
|
|
6
12
|
super(message, options);
|
|
7
13
|
this.name = "UrlError";
|
|
14
|
+
this.reason = options?.reason ?? "disallowed";
|
|
8
15
|
}
|
|
9
16
|
};
|
|
10
17
|
const PORTABLE_IRI_PATTERN = /^(ap|ap\+ef61):\/\/([^/?#]*)([^?#]*)(\?[^#]*)?(#.*)?$/i;
|
|
@@ -392,9 +399,12 @@ async function validatePublicUrl(url) {
|
|
|
392
399
|
}
|
|
393
400
|
let addresses;
|
|
394
401
|
try {
|
|
395
|
-
addresses = await lookup(hostname, { all: true });
|
|
402
|
+
addresses = await dns.lookup(hostname, { all: true });
|
|
396
403
|
} catch (error) {
|
|
397
|
-
throw new UrlError("DNS lookup failed", {
|
|
404
|
+
throw new UrlError("DNS lookup failed", {
|
|
405
|
+
cause: error,
|
|
406
|
+
reason: "dns"
|
|
407
|
+
});
|
|
398
408
|
}
|
|
399
409
|
validateLookupAddresses(addresses);
|
|
400
410
|
}
|
|
@@ -423,7 +433,7 @@ function validateLookupAddresses(addresses) {
|
|
|
423
433
|
ipAddressCount++;
|
|
424
434
|
validatePublicIpAddress(address, family);
|
|
425
435
|
}
|
|
426
|
-
if (ipAddressCount === 0) throw new UrlError("DNS lookup did not return any IP address");
|
|
436
|
+
if (ipAddressCount === 0) throw new UrlError("DNS lookup did not return any IP address", { reason: "dns" });
|
|
427
437
|
}
|
|
428
438
|
function validatePublicIpAddress(address, family) {
|
|
429
439
|
if (family === 4 && isValidPublicIPv4Address(address) || family === 6 && isValidPublicIPv6Address(address)) return;
|
|
@@ -1,10 +1,19 @@
|
|
|
1
|
+
const require_rolldown_runtime = require("./rolldown-runtime-emK7D4bc.cjs");
|
|
1
2
|
let node_dns_promises = require("node:dns/promises");
|
|
3
|
+
node_dns_promises = require_rolldown_runtime.__toESM(node_dns_promises, 1);
|
|
2
4
|
let node_net = require("node:net");
|
|
3
5
|
//#region src/url.ts
|
|
4
6
|
var UrlError = class extends Error {
|
|
7
|
+
/**
|
|
8
|
+
* The failure category. Defaults to `"disallowed"`.
|
|
9
|
+
* `"dns"` includes lookup errors and results with no usable IP addresses.
|
|
10
|
+
* @since 2.0.29
|
|
11
|
+
*/
|
|
12
|
+
reason;
|
|
5
13
|
constructor(message, options) {
|
|
6
14
|
super(message, options);
|
|
7
15
|
this.name = "UrlError";
|
|
16
|
+
this.reason = options?.reason ?? "disallowed";
|
|
8
17
|
}
|
|
9
18
|
};
|
|
10
19
|
const PORTABLE_IRI_PATTERN = /^(ap|ap\+ef61):\/\/([^/?#]*)([^?#]*)(\?[^#]*)?(#.*)?$/i;
|
|
@@ -392,9 +401,12 @@ async function validatePublicUrl(url) {
|
|
|
392
401
|
}
|
|
393
402
|
let addresses;
|
|
394
403
|
try {
|
|
395
|
-
addresses = await
|
|
404
|
+
addresses = await node_dns_promises.default.lookup(hostname, { all: true });
|
|
396
405
|
} catch (error) {
|
|
397
|
-
throw new UrlError("DNS lookup failed", {
|
|
406
|
+
throw new UrlError("DNS lookup failed", {
|
|
407
|
+
cause: error,
|
|
408
|
+
reason: "dns"
|
|
409
|
+
});
|
|
398
410
|
}
|
|
399
411
|
validateLookupAddresses(addresses);
|
|
400
412
|
}
|
|
@@ -423,7 +435,7 @@ function validateLookupAddresses(addresses) {
|
|
|
423
435
|
ipAddressCount++;
|
|
424
436
|
validatePublicIpAddress(address, family);
|
|
425
437
|
}
|
|
426
|
-
if (ipAddressCount === 0) throw new UrlError("DNS lookup did not return any IP address");
|
|
438
|
+
if (ipAddressCount === 0) throw new UrlError("DNS lookup did not return any IP address", { reason: "dns" });
|
|
427
439
|
}
|
|
428
440
|
function validatePublicIpAddress(address, family) {
|
|
429
441
|
if (family === 4 && isValidPublicIPv4Address(address) || family === 6 && isValidPublicIPv6Address(address)) return;
|
package/dist/tests/url.test.cjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
const require_url = require("./url-
|
|
1
|
+
const require_url = require("./url-gLKqAO1H.cjs");
|
|
2
2
|
let node_test = require("node:test");
|
|
3
3
|
let node_assert = require("node:assert");
|
|
4
4
|
//#region src/url.test.ts
|
|
@@ -463,6 +463,29 @@ let node_assert = require("node:assert");
|
|
|
463
463
|
}
|
|
464
464
|
(0, node_assert.ok)(!require_url.arePortableUrisEqual(require_url.fromCompatibleEf61Id(require_url.toCompatibleEf61Id("ap://did:key:z6MkAlice/actor", "https://server.example")).href, require_url.fromCompatibleEf61Id(require_url.toCompatibleEf61Id("ap://did:key:z6MkAlice/actor#", "https://server.example")).href));
|
|
465
465
|
});
|
|
466
|
+
(0, node_test.test)("UrlError reason is independent of its cause", () => {
|
|
467
|
+
const cause = /* @__PURE__ */ new Error("Underlying failure");
|
|
468
|
+
(0, node_assert.strictEqual)(new require_url.UrlError("Rejected URL").reason, "disallowed");
|
|
469
|
+
const disallowedError = new require_url.UrlError("Rejected URL", { cause });
|
|
470
|
+
(0, node_assert.strictEqual)(disallowedError.reason, "disallowed");
|
|
471
|
+
(0, node_assert.strictEqual)(disallowedError.cause, cause);
|
|
472
|
+
const dnsError = new require_url.UrlError("Lookup failed", {
|
|
473
|
+
reason: "dns",
|
|
474
|
+
cause
|
|
475
|
+
});
|
|
476
|
+
(0, node_assert.strictEqual)(dnsError.reason, "dns");
|
|
477
|
+
(0, node_assert.strictEqual)(dnsError.cause, cause);
|
|
478
|
+
});
|
|
479
|
+
(0, node_test.test)("validatePublicUrl() classifies disallowed URLs", async () => {
|
|
480
|
+
for (const url of [
|
|
481
|
+
"ftp://example.com",
|
|
482
|
+
"https://localhost",
|
|
483
|
+
"https://127.0.0.1"
|
|
484
|
+
]) await (0, node_assert.rejects)(() => require_url.validatePublicUrl(url), {
|
|
485
|
+
name: "UrlError",
|
|
486
|
+
reason: "disallowed"
|
|
487
|
+
});
|
|
488
|
+
});
|
|
466
489
|
(0, node_test.test)("validatePublicUrl()", async () => {
|
|
467
490
|
await (0, node_assert.rejects)(() => require_url.validatePublicUrl("ftp://localhost"), require_url.UrlError);
|
|
468
491
|
await (0, node_assert.rejects)(() => require_url.validatePublicUrl("data:text/plain;base64,SGVsbG8sIFdvcmxkIQ=="), require_url.UrlError);
|
|
@@ -512,11 +535,21 @@ let node_assert = require("node:assert");
|
|
|
512
535
|
}, {
|
|
513
536
|
address: "127.0.0.1",
|
|
514
537
|
family: 4
|
|
515
|
-
}]),
|
|
538
|
+
}]), {
|
|
539
|
+
name: "UrlError",
|
|
540
|
+
reason: "disallowed"
|
|
541
|
+
});
|
|
516
542
|
(0, node_assert.throws)(() => require_url.validateLookupAddresses([{
|
|
517
543
|
address: "app-host.example.net.",
|
|
518
544
|
family: 4
|
|
519
|
-
}]),
|
|
545
|
+
}]), {
|
|
546
|
+
name: "UrlError",
|
|
547
|
+
reason: "dns"
|
|
548
|
+
});
|
|
549
|
+
(0, node_assert.throws)(() => require_url.validateLookupAddresses([]), {
|
|
550
|
+
name: "UrlError",
|
|
551
|
+
reason: "dns"
|
|
552
|
+
});
|
|
520
553
|
});
|
|
521
554
|
(0, node_test.test)("isValidPublicIPv4Address()", () => {
|
|
522
555
|
(0, node_assert.ok)(require_url.isValidPublicIPv4Address("8.8.8.8"));
|
package/dist/tests/url.test.mjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { _ as validateLookupAddresses, a as formatIri, c as haveSameFe34Origin, d as isValidPublicIPv4Address, f as isValidPublicIPv6Address, g as toCompatibleEf61Id, h as parseJsonLdId, i as expandIPv6Address, l as haveSameIriOrigin, m as parseIri, n as arePortableUrisEqual, o as fromCompatibleEf61Id, p as parseGatewayUrl, r as canonicalizePortableUri, s as getFe34Origin, t as UrlError, u as isGatewayUrl, v as validatePublicUrl } from "./url-
|
|
1
|
+
import { _ as validateLookupAddresses, a as formatIri, c as haveSameFe34Origin, d as isValidPublicIPv4Address, f as isValidPublicIPv6Address, g as toCompatibleEf61Id, h as parseJsonLdId, i as expandIPv6Address, l as haveSameIriOrigin, m as parseIri, n as arePortableUrisEqual, o as fromCompatibleEf61Id, p as parseGatewayUrl, r as canonicalizePortableUri, s as getFe34Origin, t as UrlError, u as isGatewayUrl, v as validatePublicUrl } from "./url-D7qRF-dF.mjs";
|
|
2
2
|
import { test } from "node:test";
|
|
3
|
-
import { deepStrictEqual, ok, rejects, throws } from "node:assert";
|
|
3
|
+
import { deepStrictEqual, ok, rejects, strictEqual, throws } from "node:assert";
|
|
4
4
|
//#region src/url.test.ts
|
|
5
5
|
test("parseIri() accepts portable ActivityPub URI schemes", () => {
|
|
6
6
|
for (const iri of [
|
|
@@ -463,6 +463,29 @@ test("compatible identifier conversion round-trips", () => {
|
|
|
463
463
|
}
|
|
464
464
|
ok(!arePortableUrisEqual(fromCompatibleEf61Id(toCompatibleEf61Id("ap://did:key:z6MkAlice/actor", "https://server.example")).href, fromCompatibleEf61Id(toCompatibleEf61Id("ap://did:key:z6MkAlice/actor#", "https://server.example")).href));
|
|
465
465
|
});
|
|
466
|
+
test("UrlError reason is independent of its cause", () => {
|
|
467
|
+
const cause = /* @__PURE__ */ new Error("Underlying failure");
|
|
468
|
+
strictEqual(new UrlError("Rejected URL").reason, "disallowed");
|
|
469
|
+
const disallowedError = new UrlError("Rejected URL", { cause });
|
|
470
|
+
strictEqual(disallowedError.reason, "disallowed");
|
|
471
|
+
strictEqual(disallowedError.cause, cause);
|
|
472
|
+
const dnsError = new UrlError("Lookup failed", {
|
|
473
|
+
reason: "dns",
|
|
474
|
+
cause
|
|
475
|
+
});
|
|
476
|
+
strictEqual(dnsError.reason, "dns");
|
|
477
|
+
strictEqual(dnsError.cause, cause);
|
|
478
|
+
});
|
|
479
|
+
test("validatePublicUrl() classifies disallowed URLs", async () => {
|
|
480
|
+
for (const url of [
|
|
481
|
+
"ftp://example.com",
|
|
482
|
+
"https://localhost",
|
|
483
|
+
"https://127.0.0.1"
|
|
484
|
+
]) await rejects(() => validatePublicUrl(url), {
|
|
485
|
+
name: "UrlError",
|
|
486
|
+
reason: "disallowed"
|
|
487
|
+
});
|
|
488
|
+
});
|
|
466
489
|
test("validatePublicUrl()", async () => {
|
|
467
490
|
await rejects(() => validatePublicUrl("ftp://localhost"), UrlError);
|
|
468
491
|
await rejects(() => validatePublicUrl("data:text/plain;base64,SGVsbG8sIFdvcmxkIQ=="), UrlError);
|
|
@@ -512,11 +535,21 @@ test("validateLookupAddresses() rejects unsafe or CNAME-only Cloudflare Workers
|
|
|
512
535
|
}, {
|
|
513
536
|
address: "127.0.0.1",
|
|
514
537
|
family: 4
|
|
515
|
-
}]),
|
|
538
|
+
}]), {
|
|
539
|
+
name: "UrlError",
|
|
540
|
+
reason: "disallowed"
|
|
541
|
+
});
|
|
516
542
|
throws(() => validateLookupAddresses([{
|
|
517
543
|
address: "app-host.example.net.",
|
|
518
544
|
family: 4
|
|
519
|
-
}]),
|
|
545
|
+
}]), {
|
|
546
|
+
name: "UrlError",
|
|
547
|
+
reason: "dns"
|
|
548
|
+
});
|
|
549
|
+
throws(() => validateLookupAddresses([]), {
|
|
550
|
+
name: "UrlError",
|
|
551
|
+
reason: "dns"
|
|
552
|
+
});
|
|
520
553
|
});
|
|
521
554
|
test("isValidPublicIPv4Address()", () => {
|
|
522
555
|
ok(isValidPublicIPv4Address("8.8.8.8"));
|
|
@@ -1,11 +1,18 @@
|
|
|
1
1
|
|
|
2
|
-
import
|
|
2
|
+
import dns 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
|
+
/**
|
|
7
|
+
* The failure category. Defaults to `"disallowed"`.
|
|
8
|
+
* `"dns"` includes lookup errors and results with no usable IP addresses.
|
|
9
|
+
* @since 2.0.29
|
|
10
|
+
*/
|
|
11
|
+
reason;
|
|
6
12
|
constructor(message, options) {
|
|
7
13
|
super(message, options);
|
|
8
14
|
this.name = "UrlError";
|
|
15
|
+
this.reason = options?.reason ?? "disallowed";
|
|
9
16
|
}
|
|
10
17
|
};
|
|
11
18
|
const PORTABLE_IRI_PATTERN = /^(ap|ap\+ef61):\/\/([^/?#]*)([^?#]*)(\?[^#]*)?(#.*)?$/i;
|
|
@@ -393,9 +400,12 @@ async function validatePublicUrl(url) {
|
|
|
393
400
|
}
|
|
394
401
|
let addresses;
|
|
395
402
|
try {
|
|
396
|
-
addresses = await lookup(hostname, { all: true });
|
|
403
|
+
addresses = await dns.lookup(hostname, { all: true });
|
|
397
404
|
} catch (error) {
|
|
398
|
-
throw new UrlError("DNS lookup failed", {
|
|
405
|
+
throw new UrlError("DNS lookup failed", {
|
|
406
|
+
cause: error,
|
|
407
|
+
reason: "dns"
|
|
408
|
+
});
|
|
399
409
|
}
|
|
400
410
|
validateLookupAddresses(addresses);
|
|
401
411
|
}
|
|
@@ -424,7 +434,7 @@ function validateLookupAddresses(addresses) {
|
|
|
424
434
|
ipAddressCount++;
|
|
425
435
|
validatePublicIpAddress(address, family);
|
|
426
436
|
}
|
|
427
|
-
if (ipAddressCount === 0) throw new UrlError("DNS lookup did not return any IP address");
|
|
437
|
+
if (ipAddressCount === 0) throw new UrlError("DNS lookup did not return any IP address", { reason: "dns" });
|
|
428
438
|
}
|
|
429
439
|
function validatePublicIpAddress(address, family) {
|
|
430
440
|
if (family === 4 && isValidPublicIPv4Address(address) || family === 6 && isValidPublicIPv6Address(address)) return;
|
|
@@ -1,11 +1,20 @@
|
|
|
1
1
|
|
|
2
|
+
const require_rolldown_runtime = require("./rolldown-runtime-B7lfambq.cjs");
|
|
2
3
|
let node_dns_promises = require("node:dns/promises");
|
|
4
|
+
node_dns_promises = require_rolldown_runtime.__toESM(node_dns_promises, 1);
|
|
3
5
|
let node_net = require("node:net");
|
|
4
6
|
//#region src/url.ts
|
|
5
7
|
var UrlError = class extends Error {
|
|
8
|
+
/**
|
|
9
|
+
* The failure category. Defaults to `"disallowed"`.
|
|
10
|
+
* `"dns"` includes lookup errors and results with no usable IP addresses.
|
|
11
|
+
* @since 2.0.29
|
|
12
|
+
*/
|
|
13
|
+
reason;
|
|
6
14
|
constructor(message, options) {
|
|
7
15
|
super(message, options);
|
|
8
16
|
this.name = "UrlError";
|
|
17
|
+
this.reason = options?.reason ?? "disallowed";
|
|
9
18
|
}
|
|
10
19
|
};
|
|
11
20
|
const PORTABLE_IRI_PATTERN = /^(ap|ap\+ef61):\/\/([^/?#]*)([^?#]*)(\?[^#]*)?(#.*)?$/i;
|
|
@@ -393,9 +402,12 @@ async function validatePublicUrl(url) {
|
|
|
393
402
|
}
|
|
394
403
|
let addresses;
|
|
395
404
|
try {
|
|
396
|
-
addresses = await
|
|
405
|
+
addresses = await node_dns_promises.default.lookup(hostname, { all: true });
|
|
397
406
|
} catch (error) {
|
|
398
|
-
throw new UrlError("DNS lookup failed", {
|
|
407
|
+
throw new UrlError("DNS lookup failed", {
|
|
408
|
+
cause: error,
|
|
409
|
+
reason: "dns"
|
|
410
|
+
});
|
|
399
411
|
}
|
|
400
412
|
validateLookupAddresses(addresses);
|
|
401
413
|
}
|
|
@@ -424,7 +436,7 @@ function validateLookupAddresses(addresses) {
|
|
|
424
436
|
ipAddressCount++;
|
|
425
437
|
validatePublicIpAddress(address, family);
|
|
426
438
|
}
|
|
427
|
-
if (ipAddressCount === 0) throw new UrlError("DNS lookup did not return any IP address");
|
|
439
|
+
if (ipAddressCount === 0) throw new UrlError("DNS lookup did not return any IP address", { reason: "dns" });
|
|
428
440
|
}
|
|
429
441
|
function validatePublicIpAddress(address, family) {
|
|
430
442
|
if (family === 4 && isValidPublicIPv4Address(address) || family === 6 && isValidPublicIPv6Address(address)) return;
|
package/package.json
CHANGED
package/src/contexts.ts
CHANGED
|
@@ -41,7 +41,6 @@ const preloadedContexts: Record<string, unknown> = {
|
|
|
41
41
|
"https://gotosocial.org/ns": gotosocial,
|
|
42
42
|
"https://w3id.org/fep/22cd": fep22cd,
|
|
43
43
|
"https://w3id.org/fep/5711": fep5711,
|
|
44
|
-
"https://w3id.org/fep/7aa9": fep7aa9,
|
|
45
44
|
|
|
46
45
|
// Controlled Identifiers v1.0 requires JSON-LD processors to treat this
|
|
47
46
|
// context URL as already resolved. We ship the official W3C context so that
|
|
@@ -60,6 +59,13 @@ const preloadedContexts: Record<string, unknown> = {
|
|
|
60
59
|
// https://github.com/fedify-dev/fedify/issues/982
|
|
61
60
|
"https://w3id.org/fep/ef61": fepEf61,
|
|
62
61
|
|
|
62
|
+
// The FEP-7aa9 context is hosted on Codeberg Pages, whose outages can
|
|
63
|
+
// prevent actor documents from being parsed. Resolve it locally so that
|
|
64
|
+
// parsing does not depend on the remote context server's availability.
|
|
65
|
+
// See: https://w3id.org/fep/7aa9
|
|
66
|
+
// https://github.com/fedify-dev/fedify/issues/1078
|
|
67
|
+
"https://w3id.org/fep/7aa9": fep7aa9,
|
|
68
|
+
|
|
63
69
|
// Lemmy's context document is served as application/json without the JSON-LD
|
|
64
70
|
// context Link header. The default document loader treats that as a regular
|
|
65
71
|
// JSON response instead of a JSON-LD context, so every Lemmy activity that
|
package/src/docloader.test.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import fetchMock from "fetch-mock";
|
|
2
1
|
import { configure, type LogRecord, reset } from "@logtape/logtape";
|
|
2
|
+
import fetchMock from "fetch-mock";
|
|
3
3
|
import { deepStrictEqual, ok, rejects } from "node:assert";
|
|
4
|
+
import dns from "node:dns/promises";
|
|
4
5
|
import { test } from "node:test";
|
|
5
6
|
import { createServer } from "node:http";
|
|
6
7
|
import { gzipSync } from "node:zlib";
|
|
@@ -507,6 +508,43 @@ test("getDocumentLoader()", async (t) => {
|
|
|
507
508
|
});
|
|
508
509
|
});
|
|
509
510
|
|
|
511
|
+
// A Codeberg Pages outage must not prevent loading the FEP-7aa9 context.
|
|
512
|
+
// See: https://github.com/fedify-dev/fedify/issues/1078
|
|
513
|
+
fetchMock.get("https://w3id.org/fep/7aa9", { status: 502 });
|
|
514
|
+
await t.test("preloaded FEP-7aa9 context", async () => {
|
|
515
|
+
const url = "https://w3id.org/fep/7aa9";
|
|
516
|
+
deepStrictEqual(await fetchDocumentLoader(url), {
|
|
517
|
+
contextUrl: null,
|
|
518
|
+
documentUrl: url,
|
|
519
|
+
document: {
|
|
520
|
+
"@context": {
|
|
521
|
+
"FeaturedCollection": "https://w3id.org/fep/7aa9#FeaturedCollection",
|
|
522
|
+
"FeaturedItem": "https://w3id.org/fep/7aa9#FeaturedItem",
|
|
523
|
+
"FeatureRequest": "https://w3id.org/fep/7aa9#FeatureRequest",
|
|
524
|
+
"FeatureAuthorization":
|
|
525
|
+
"https://w3id.org/fep/7aa9#FeatureAuthorization",
|
|
526
|
+
"topic": {
|
|
527
|
+
"@id": "https://w3id.org/fep/7aa9#topic",
|
|
528
|
+
"@type": "@id",
|
|
529
|
+
},
|
|
530
|
+
"featuredObject": {
|
|
531
|
+
"@id": "https://w3id.org/fep/7aa9#featuredObject",
|
|
532
|
+
"@type": "@id",
|
|
533
|
+
},
|
|
534
|
+
"canFeature": {
|
|
535
|
+
"@id": "https://w3id.org/fep/7aa9#canFeature",
|
|
536
|
+
"@type": "@id",
|
|
537
|
+
},
|
|
538
|
+
"featureAuthorization": {
|
|
539
|
+
"@id": "https://w3id.org/fep/7aa9#featureAuthorization",
|
|
540
|
+
"@type": "@id",
|
|
541
|
+
},
|
|
542
|
+
},
|
|
543
|
+
},
|
|
544
|
+
});
|
|
545
|
+
deepStrictEqual(fetchMock.callHistory.calls(url).length, 0);
|
|
546
|
+
});
|
|
547
|
+
|
|
510
548
|
await t.test("deny non-HTTP/HTTPS", async () => {
|
|
511
549
|
await rejects(
|
|
512
550
|
() => fetchDocumentLoader("ftp://localhost"),
|
|
@@ -812,3 +850,271 @@ test("getRemoteDocument() bounds JSON by default", async () => {
|
|
|
812
850
|
deepStrictEqual(canceled, true);
|
|
813
851
|
deepStrictEqual(pulls, 257);
|
|
814
852
|
});
|
|
853
|
+
|
|
854
|
+
test("getDocumentLoader() logs DNS failures as such", async (t) => {
|
|
855
|
+
// The validator skips DNS when Deno has no network permission. Checked
|
|
856
|
+
// here rather than with top-level await, which the CommonJS build rejects.
|
|
857
|
+
if (
|
|
858
|
+
"Deno" in globalThis &&
|
|
859
|
+
(await Deno.permissions.query({ name: "net" })).state !== "granted"
|
|
860
|
+
) {
|
|
861
|
+
t.skip("requires the net permission");
|
|
862
|
+
return;
|
|
863
|
+
}
|
|
864
|
+
const loader = getDocumentLoader();
|
|
865
|
+
for (const result of ["throws", "empty", "private"] as const) {
|
|
866
|
+
await t.test(result, async () => {
|
|
867
|
+
// Stubbing works only because url.ts uses the default node:dns/promises
|
|
868
|
+
// import; see the FIXME there.
|
|
869
|
+
const originalLookup = dns.lookup;
|
|
870
|
+
dns.lookup = (() =>
|
|
871
|
+
result === "throws"
|
|
872
|
+
? Promise.reject(new Error("Resolver unavailable"))
|
|
873
|
+
: Promise.resolve(
|
|
874
|
+
result === "empty" ? [] : [{ address: "127.0.0.1", family: 4 }],
|
|
875
|
+
)) as typeof dns.lookup;
|
|
876
|
+
const records: LogRecord[] = [];
|
|
877
|
+
await configure({
|
|
878
|
+
sinks: {
|
|
879
|
+
buffer: (record) =>
|
|
880
|
+
records.push(record),
|
|
881
|
+
},
|
|
882
|
+
loggers: [
|
|
883
|
+
{ category: "fedify", sinks: ["buffer"], lowestLevel: "debug" },
|
|
884
|
+
{ category: ["logtape", "meta"], sinks: [] },
|
|
885
|
+
],
|
|
886
|
+
reset: true,
|
|
887
|
+
});
|
|
888
|
+
try {
|
|
889
|
+
const url = "https://dns-failure.invalid/object";
|
|
890
|
+
let error: unknown;
|
|
891
|
+
await rejects(() => loader(url), (e) => {
|
|
892
|
+
error = e;
|
|
893
|
+
return true;
|
|
894
|
+
});
|
|
895
|
+
ok(error instanceof UrlError);
|
|
896
|
+
deepStrictEqual(
|
|
897
|
+
error.reason,
|
|
898
|
+
result === "private" ? "disallowed" : "dns",
|
|
899
|
+
);
|
|
900
|
+
deepStrictEqual(
|
|
901
|
+
records.map((r) => [r.level, r.rawMessage, r.properties.url]),
|
|
902
|
+
[
|
|
903
|
+
result === "private"
|
|
904
|
+
? ["error", "Disallowed private URL: {url}", url]
|
|
905
|
+
: ["debug", "DNS lookup failed for {url}", url],
|
|
906
|
+
],
|
|
907
|
+
);
|
|
908
|
+
ok(records[0].properties.error === error);
|
|
909
|
+
} finally {
|
|
910
|
+
await reset();
|
|
911
|
+
dns.lookup = originalLookup;
|
|
912
|
+
}
|
|
913
|
+
});
|
|
914
|
+
}
|
|
915
|
+
});
|
|
916
|
+
|
|
917
|
+
test("getDocumentLoader() bounds alternate document chains", async (t) => {
|
|
918
|
+
const base = "https://example.com/alternate-chain/";
|
|
919
|
+
const loader = getDocumentLoader({ allowPrivateAddress: true });
|
|
920
|
+
for (const mode of ["header", "html", "mixed"] as const) {
|
|
921
|
+
for (const hops of [20, 21]) {
|
|
922
|
+
await t.test(`${mode}: ${hops} hops`, async () => {
|
|
923
|
+
fetchMock.mockGlobal();
|
|
924
|
+
let requests = 0;
|
|
925
|
+
fetchMock.get(`begin:${base}`, ({ url }) => {
|
|
926
|
+
requests++;
|
|
927
|
+
const index = Number(new URL(url).pathname.split("/").at(-1));
|
|
928
|
+
// Keep the unpatched regression bounded too.
|
|
929
|
+
if (index === hops || requests > 30) {
|
|
930
|
+
return Response.json({ done: true });
|
|
931
|
+
}
|
|
932
|
+
const next = `${base}${index + 1}`;
|
|
933
|
+
if (mode === "mixed" && index % 2 === 1) {
|
|
934
|
+
return Response.redirect(next, 302);
|
|
935
|
+
}
|
|
936
|
+
return alternate(next, mode === "html");
|
|
937
|
+
});
|
|
938
|
+
try {
|
|
939
|
+
if (hops === 20) {
|
|
940
|
+
deepStrictEqual((await loader(`${base}0`)).document, {
|
|
941
|
+
done: true,
|
|
942
|
+
});
|
|
943
|
+
} else {
|
|
944
|
+
await rejects(loader(`${base}0`), (error: unknown) => {
|
|
945
|
+
ok(error instanceof FetchError);
|
|
946
|
+
ok(error.message.includes("Too many redirections (21)"));
|
|
947
|
+
return true;
|
|
948
|
+
});
|
|
949
|
+
}
|
|
950
|
+
deepStrictEqual(requests, 21);
|
|
951
|
+
} finally {
|
|
952
|
+
fetchMock.hardReset();
|
|
953
|
+
}
|
|
954
|
+
});
|
|
955
|
+
}
|
|
956
|
+
}
|
|
957
|
+
|
|
958
|
+
for (
|
|
959
|
+
const mode of [
|
|
960
|
+
"header",
|
|
961
|
+
"html",
|
|
962
|
+
"alternate-redirect",
|
|
963
|
+
"redirect-alternate",
|
|
964
|
+
"redirect-intermediate",
|
|
965
|
+
]
|
|
966
|
+
) {
|
|
967
|
+
await t.test(`${mode}: cycle`, async () => {
|
|
968
|
+
fetchMock.mockGlobal();
|
|
969
|
+
let requests = 0;
|
|
970
|
+
fetchMock.get(`begin:${base}`, ({ url }) => {
|
|
971
|
+
requests++;
|
|
972
|
+
if (requests > 30) return Response.json({ stopped: true });
|
|
973
|
+
const index = Number(new URL(url).pathname.split("/").at(-1));
|
|
974
|
+
const next = `${base}${
|
|
975
|
+
mode === "redirect-intermediate"
|
|
976
|
+
? (index === 0 ? 1 : index === 1 ? 2 : 1)
|
|
977
|
+
: 1 - index
|
|
978
|
+
}`;
|
|
979
|
+
if (
|
|
980
|
+
(mode === "alternate-redirect" && index === 1) ||
|
|
981
|
+
((mode === "redirect-alternate" ||
|
|
982
|
+
mode === "redirect-intermediate") && index === 0)
|
|
983
|
+
) {
|
|
984
|
+
return Response.redirect(next, 302);
|
|
985
|
+
}
|
|
986
|
+
return alternate(next, mode === "html");
|
|
987
|
+
});
|
|
988
|
+
try {
|
|
989
|
+
await rejects(loader(`${base}0`), (error: unknown) => {
|
|
990
|
+
ok(error instanceof FetchError);
|
|
991
|
+
ok(error.message.includes("Redirect loop detected:"));
|
|
992
|
+
return true;
|
|
993
|
+
});
|
|
994
|
+
deepStrictEqual(requests, mode === "redirect-intermediate" ? 3 : 2);
|
|
995
|
+
} finally {
|
|
996
|
+
fetchMock.hardReset();
|
|
997
|
+
}
|
|
998
|
+
});
|
|
999
|
+
}
|
|
1000
|
+
|
|
1001
|
+
await t.test("alternate followed by 20 redirects shares the limit", async () => {
|
|
1002
|
+
fetchMock.mockGlobal();
|
|
1003
|
+
let requests = 0;
|
|
1004
|
+
fetchMock.get(`begin:${base}`, ({ url }) => {
|
|
1005
|
+
requests++;
|
|
1006
|
+
const index = Number(new URL(url).pathname.split("/").at(-1));
|
|
1007
|
+
if (index === 21) return Response.json({ done: true });
|
|
1008
|
+
return index === 0
|
|
1009
|
+
? alternate(`${base}1`, false)
|
|
1010
|
+
: Response.redirect(`${base}${index + 1}`, 302);
|
|
1011
|
+
});
|
|
1012
|
+
try {
|
|
1013
|
+
await rejects(loader(`${base}0`), (error: unknown) => {
|
|
1014
|
+
ok(error instanceof FetchError);
|
|
1015
|
+
ok(error.message.includes("Too many redirections (21)"));
|
|
1016
|
+
return true;
|
|
1017
|
+
});
|
|
1018
|
+
deepStrictEqual(requests, 21);
|
|
1019
|
+
} finally {
|
|
1020
|
+
fetchMock.hardReset();
|
|
1021
|
+
}
|
|
1022
|
+
});
|
|
1023
|
+
|
|
1024
|
+
await t.test("relative alternate after redirect and isolated calls", async () => {
|
|
1025
|
+
fetchMock.mockGlobal();
|
|
1026
|
+
fetchMock.get(`${base}start`, Response.redirect(`${base}html`, 302));
|
|
1027
|
+
fetchMock.get(`${base}html`, alternate("./document", true));
|
|
1028
|
+
fetchMock.get(`${base}document`, Response.json({ done: true }));
|
|
1029
|
+
try {
|
|
1030
|
+
for (let i = 0; i < 2; i++) {
|
|
1031
|
+
const results = await Promise.all([
|
|
1032
|
+
loader(`${base}start`),
|
|
1033
|
+
loader(`${base}start`),
|
|
1034
|
+
]);
|
|
1035
|
+
for (const result of results) {
|
|
1036
|
+
deepStrictEqual(result.document, { done: true });
|
|
1037
|
+
deepStrictEqual(result.documentUrl, `${base}document`);
|
|
1038
|
+
}
|
|
1039
|
+
}
|
|
1040
|
+
} finally {
|
|
1041
|
+
fetchMock.hardReset();
|
|
1042
|
+
}
|
|
1043
|
+
});
|
|
1044
|
+
|
|
1045
|
+
function alternate(next: string, html: boolean): Response {
|
|
1046
|
+
return html
|
|
1047
|
+
? new Response(
|
|
1048
|
+
`<link rel="alternate" type="application/activity+json" href="${next}">`,
|
|
1049
|
+
{
|
|
1050
|
+
headers: { "Content-Type": "text/html" },
|
|
1051
|
+
},
|
|
1052
|
+
)
|
|
1053
|
+
: new Response("not JSON", {
|
|
1054
|
+
headers: {
|
|
1055
|
+
"Content-Type": "text/plain",
|
|
1056
|
+
Link: `<${next}>; rel="alternate"; type="application/activity+json"`,
|
|
1057
|
+
},
|
|
1058
|
+
});
|
|
1059
|
+
}
|
|
1060
|
+
});
|
|
1061
|
+
|
|
1062
|
+
test("getDocumentLoader() preserves cancellation across alternates", async (t) => {
|
|
1063
|
+
const base = "https://example.com/alternate-abort/";
|
|
1064
|
+
const loader = getDocumentLoader({ allowPrivateAddress: true });
|
|
1065
|
+
for (const html of [false, true]) {
|
|
1066
|
+
await t.test(html ? "HTML" : "Link header", async () => {
|
|
1067
|
+
fetchMock.mockGlobal();
|
|
1068
|
+
const controller = new AbortController();
|
|
1069
|
+
let requests = 0;
|
|
1070
|
+
fetchMock.get(`begin:${base}`, ({ url }) => {
|
|
1071
|
+
requests++;
|
|
1072
|
+
const index = Number(new URL(url).pathname.split("/").at(-1));
|
|
1073
|
+
if (index === 2) return Response.json({ done: true });
|
|
1074
|
+
if (index === 1) controller.abort();
|
|
1075
|
+
const next = `${base}${index + 1}`;
|
|
1076
|
+
return new Response(
|
|
1077
|
+
html
|
|
1078
|
+
? `<link rel="alternate" type="application/activity+json" href="${next}">`
|
|
1079
|
+
: "not JSON",
|
|
1080
|
+
{
|
|
1081
|
+
headers: html ? { "Content-Type": "text/html" } : {
|
|
1082
|
+
"Content-Type": "text/plain",
|
|
1083
|
+
Link:
|
|
1084
|
+
`<${next}>; rel="alternate"; type="application/activity+json"`,
|
|
1085
|
+
},
|
|
1086
|
+
},
|
|
1087
|
+
);
|
|
1088
|
+
});
|
|
1089
|
+
try {
|
|
1090
|
+
await rejects(loader(`${base}0`, { signal: controller.signal }), {
|
|
1091
|
+
name: "AbortError",
|
|
1092
|
+
});
|
|
1093
|
+
deepStrictEqual(requests, 2);
|
|
1094
|
+
} finally {
|
|
1095
|
+
fetchMock.hardReset();
|
|
1096
|
+
}
|
|
1097
|
+
});
|
|
1098
|
+
}
|
|
1099
|
+
});
|
|
1100
|
+
|
|
1101
|
+
test("getDocumentLoader() rejects cancellation before fetching", async () => {
|
|
1102
|
+
fetchMock.mockGlobal();
|
|
1103
|
+
let requests = 0;
|
|
1104
|
+
const url = "https://example.com/pre-aborted-alternate";
|
|
1105
|
+
fetchMock.get(url, () => {
|
|
1106
|
+
requests++;
|
|
1107
|
+
return Response.json({ done: true });
|
|
1108
|
+
});
|
|
1109
|
+
try {
|
|
1110
|
+
const loader = getDocumentLoader({ allowPrivateAddress: true });
|
|
1111
|
+
const controller = new AbortController();
|
|
1112
|
+
controller.abort();
|
|
1113
|
+
await rejects(loader(url, { signal: controller.signal }), {
|
|
1114
|
+
name: "AbortError",
|
|
1115
|
+
});
|
|
1116
|
+
deepStrictEqual(requests, 0);
|
|
1117
|
+
} finally {
|
|
1118
|
+
fetchMock.hardReset();
|
|
1119
|
+
}
|
|
1120
|
+
});
|