@fedify/vocab-runtime 2.0.0-dev.12
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/LICENSE +20 -0
- package/README.md +26 -0
- package/deno.json +29 -0
- package/dist/mod.cjs +5229 -0
- package/dist/mod.d.cts +331 -0
- package/dist/mod.d.ts +331 -0
- package/dist/mod.js +5185 -0
- package/package.json +71 -0
- package/src/contexts.ts +4237 -0
- package/src/docloader.test.ts +393 -0
- package/src/docloader.ts +367 -0
- package/src/jwk.ts +70 -0
- package/src/key.test.ts +179 -0
- package/src/key.ts +187 -0
- package/src/langstr.test.ts +28 -0
- package/src/langstr.ts +38 -0
- package/src/link.test.ts +82 -0
- package/src/link.ts +345 -0
- package/src/mod.ts +47 -0
- package/src/multibase/base.ts +34 -0
- package/src/multibase/constants.ts +89 -0
- package/src/multibase/mod.ts +82 -0
- package/src/multibase/multibase.test.ts +117 -0
- package/src/multibase/rfc4648.ts +103 -0
- package/src/multibase/types.d.ts +61 -0
- package/src/multibase/util.ts +22 -0
- package/src/request.test.ts +93 -0
- package/src/request.ts +115 -0
- package/src/url.test.ts +59 -0
- package/src/url.ts +96 -0
- package/tsdown.config.ts +9 -0
package/src/request.ts
ADDED
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import type { Logger } from "@logtape/logtape";
|
|
2
|
+
import process from "node:process";
|
|
3
|
+
import metadata from "../deno.json" with { type: "json" };
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Error thrown when fetching a JSON-LD document failed.
|
|
7
|
+
*/
|
|
8
|
+
export class FetchError extends Error {
|
|
9
|
+
/**
|
|
10
|
+
* The URL that failed to fetch.
|
|
11
|
+
*/
|
|
12
|
+
url: URL;
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Constructs a new `FetchError`.
|
|
16
|
+
*
|
|
17
|
+
* @param url The URL that failed to fetch.
|
|
18
|
+
* @param message Error message.
|
|
19
|
+
*/
|
|
20
|
+
constructor(url: URL | string, message?: string) {
|
|
21
|
+
super(message == null ? url.toString() : `${url}: ${message}`);
|
|
22
|
+
this.name = "FetchError";
|
|
23
|
+
this.url = typeof url === "string" ? new URL(url) : url;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Options for creating a request.
|
|
29
|
+
* @internal
|
|
30
|
+
*/
|
|
31
|
+
export interface CreateRequestOptions {
|
|
32
|
+
userAgent?: GetUserAgentOptions | string;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Creates a request for the given URL.
|
|
37
|
+
* @param url The URL to create the request for.
|
|
38
|
+
* @param options The options for the request.
|
|
39
|
+
* @returns The created request.
|
|
40
|
+
* @internal
|
|
41
|
+
*/
|
|
42
|
+
export function createActivityPubRequest(
|
|
43
|
+
url: string,
|
|
44
|
+
options: CreateRequestOptions = {},
|
|
45
|
+
): Request {
|
|
46
|
+
return new Request(url, {
|
|
47
|
+
headers: {
|
|
48
|
+
Accept: "application/activity+json, application/ld+json",
|
|
49
|
+
"User-Agent": typeof options.userAgent === "string"
|
|
50
|
+
? options.userAgent
|
|
51
|
+
: getUserAgent(options.userAgent),
|
|
52
|
+
},
|
|
53
|
+
redirect: "manual",
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Options for making `User-Agent` string.
|
|
59
|
+
* @see {@link getUserAgent}
|
|
60
|
+
* @since 1.3.0
|
|
61
|
+
*/
|
|
62
|
+
export interface GetUserAgentOptions {
|
|
63
|
+
/**
|
|
64
|
+
* An optional software name and version, e.g., `"Hollo/1.0.0"`.
|
|
65
|
+
*/
|
|
66
|
+
software?: string | null;
|
|
67
|
+
/**
|
|
68
|
+
* An optional URL to append to the user agent string.
|
|
69
|
+
* Usually the URL of the ActivityPub instance.
|
|
70
|
+
*/
|
|
71
|
+
url?: string | URL | null;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Gets the user agent string for the given application and URL.
|
|
76
|
+
* @param options The options for making the user agent string.
|
|
77
|
+
* @returns The user agent string.
|
|
78
|
+
* @since 1.3.0
|
|
79
|
+
*/
|
|
80
|
+
export function getUserAgent(
|
|
81
|
+
{ software, url }: GetUserAgentOptions = {},
|
|
82
|
+
): string {
|
|
83
|
+
const fedify = `Fedify/${metadata.version}`;
|
|
84
|
+
const runtime = globalThis.Deno?.version?.deno != null
|
|
85
|
+
? `Deno/${Deno.version.deno}`
|
|
86
|
+
: globalThis.process?.versions?.bun != null
|
|
87
|
+
? `Bun/${process.versions.bun}`
|
|
88
|
+
: "navigator" in globalThis &&
|
|
89
|
+
navigator.userAgent === "Cloudflare-Workers"
|
|
90
|
+
? navigator.userAgent
|
|
91
|
+
: globalThis.process?.versions?.node != null
|
|
92
|
+
? `Node.js/${process.versions.node}`
|
|
93
|
+
: null;
|
|
94
|
+
const userAgent = software == null ? [fedify] : [software, fedify];
|
|
95
|
+
if (runtime != null) userAgent.push(runtime);
|
|
96
|
+
if (url != null) userAgent.push(`+${url.toString()}`);
|
|
97
|
+
const first = userAgent.shift();
|
|
98
|
+
return `${first} (${userAgent.join("; ")})`;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Logs the request.
|
|
103
|
+
* @param request The request to log.
|
|
104
|
+
* @internal
|
|
105
|
+
*/
|
|
106
|
+
export function logRequest(logger: Logger, request: Request) {
|
|
107
|
+
logger.debug(
|
|
108
|
+
"Fetching document: {method} {url} {headers}",
|
|
109
|
+
{
|
|
110
|
+
method: request.method,
|
|
111
|
+
url: request.url,
|
|
112
|
+
headers: Object.fromEntries(request.headers.entries()),
|
|
113
|
+
},
|
|
114
|
+
);
|
|
115
|
+
}
|
package/src/url.test.ts
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { deepStrictEqual, ok, rejects } from "node:assert";
|
|
2
|
+
import { test } from "node:test";
|
|
3
|
+
import {
|
|
4
|
+
expandIPv6Address,
|
|
5
|
+
isValidPublicIPv4Address,
|
|
6
|
+
isValidPublicIPv6Address,
|
|
7
|
+
UrlError,
|
|
8
|
+
validatePublicUrl,
|
|
9
|
+
} from "./url.ts";
|
|
10
|
+
|
|
11
|
+
test("validatePublicUrl()", async () => {
|
|
12
|
+
await rejects(() => validatePublicUrl("ftp://localhost"), UrlError);
|
|
13
|
+
await rejects(
|
|
14
|
+
// cSpell: disable
|
|
15
|
+
() => validatePublicUrl("data:text/plain;base64,SGVsbG8sIFdvcmxkIQ=="),
|
|
16
|
+
// cSpell: enable
|
|
17
|
+
UrlError,
|
|
18
|
+
);
|
|
19
|
+
await rejects(() => validatePublicUrl("https://localhost"), UrlError);
|
|
20
|
+
await rejects(() => validatePublicUrl("https://127.0.0.1"), UrlError);
|
|
21
|
+
await rejects(() => validatePublicUrl("https://[::1]"), UrlError);
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
test("isValidPublicIPv4Address()", () => {
|
|
25
|
+
ok(isValidPublicIPv4Address("8.8.8.8")); // Google DNS
|
|
26
|
+
ok(!isValidPublicIPv4Address("192.168.1.1")); // private
|
|
27
|
+
ok(!isValidPublicIPv4Address("127.0.0.1")); // localhost
|
|
28
|
+
ok(!isValidPublicIPv4Address("10.0.0.1")); // private
|
|
29
|
+
ok(!isValidPublicIPv4Address("127.16.0.1")); // private
|
|
30
|
+
ok(!isValidPublicIPv4Address("169.254.0.1")); // link-local
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
test("isValidPublicIPv6Address()", () => {
|
|
34
|
+
ok(isValidPublicIPv6Address("2001:db8::1"));
|
|
35
|
+
ok(!isValidPublicIPv6Address("::1")); // localhost
|
|
36
|
+
ok(!isValidPublicIPv6Address("fc00::1")); // ULA
|
|
37
|
+
ok(!isValidPublicIPv6Address("fe80::1")); // link-local
|
|
38
|
+
ok(!isValidPublicIPv6Address("ff00::1")); // multicast
|
|
39
|
+
ok(!isValidPublicIPv6Address("::")); // unspecified
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
test("expandIPv6Address()", () => {
|
|
43
|
+
deepStrictEqual(
|
|
44
|
+
expandIPv6Address("::"),
|
|
45
|
+
"0000:0000:0000:0000:0000:0000:0000:0000",
|
|
46
|
+
);
|
|
47
|
+
deepStrictEqual(
|
|
48
|
+
expandIPv6Address("::1"),
|
|
49
|
+
"0000:0000:0000:0000:0000:0000:0000:0001",
|
|
50
|
+
);
|
|
51
|
+
deepStrictEqual(
|
|
52
|
+
expandIPv6Address("2001:db8::"),
|
|
53
|
+
"2001:0db8:0000:0000:0000:0000:0000:0000",
|
|
54
|
+
);
|
|
55
|
+
deepStrictEqual(
|
|
56
|
+
expandIPv6Address("2001:db8::1"),
|
|
57
|
+
"2001:0db8:0000:0000:0000:0000:0000:0001",
|
|
58
|
+
);
|
|
59
|
+
});
|
package/src/url.ts
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import type { LookupAddress } from "node:dns";
|
|
2
|
+
import { lookup } from "node:dns/promises";
|
|
3
|
+
import { isIP } from "node:net";
|
|
4
|
+
|
|
5
|
+
export class UrlError extends Error {
|
|
6
|
+
constructor(message: string) {
|
|
7
|
+
super(message);
|
|
8
|
+
this.name = "UrlError";
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Validates a URL to prevent SSRF attacks.
|
|
14
|
+
*/
|
|
15
|
+
export async function validatePublicUrl(url: string): Promise<void> {
|
|
16
|
+
const parsed = new URL(url);
|
|
17
|
+
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
|
|
18
|
+
throw new UrlError(`Unsupported protocol: ${parsed.protocol}`);
|
|
19
|
+
}
|
|
20
|
+
let hostname = parsed.hostname;
|
|
21
|
+
if (hostname.startsWith("[") && hostname.endsWith("]")) {
|
|
22
|
+
hostname = hostname.substring(1, hostname.length - 2);
|
|
23
|
+
}
|
|
24
|
+
if (hostname === "localhost") {
|
|
25
|
+
throw new UrlError("Localhost is not allowed");
|
|
26
|
+
}
|
|
27
|
+
if ("Deno" in globalThis && !isIP(hostname)) {
|
|
28
|
+
// If the `net` permission is not granted, we can't resolve the hostname.
|
|
29
|
+
// However, we can safely assume that it cannot gain access to private
|
|
30
|
+
// resources.
|
|
31
|
+
const netPermission = await Deno.permissions.query({ name: "net" });
|
|
32
|
+
if (netPermission.state !== "granted") return;
|
|
33
|
+
}
|
|
34
|
+
// FIXME: This is a temporary workaround for the `Bun` runtime; for unknown
|
|
35
|
+
// reasons, the Web Crypto API does not work as expected after a DNS lookup.
|
|
36
|
+
// This workaround purposes to prevent unit tests from hanging up:
|
|
37
|
+
if ("Bun" in globalThis) {
|
|
38
|
+
if (hostname === "example.com" || hostname.endsWith(".example.com")) {
|
|
39
|
+
return;
|
|
40
|
+
} else if (hostname === "fedify-test.internal") {
|
|
41
|
+
throw new UrlError("Invalid or private address: fedify-test.internal");
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
// To prevent SSRF via DNS rebinding, we need to resolve all IP addresses
|
|
45
|
+
// and ensure that they are all public:
|
|
46
|
+
let addresses: LookupAddress[];
|
|
47
|
+
try {
|
|
48
|
+
addresses = await lookup(hostname, { all: true });
|
|
49
|
+
} catch {
|
|
50
|
+
addresses = [];
|
|
51
|
+
}
|
|
52
|
+
for (const { address, family } of addresses) {
|
|
53
|
+
if (
|
|
54
|
+
family === 4 && !isValidPublicIPv4Address(address) ||
|
|
55
|
+
family === 6 && !isValidPublicIPv6Address(address) ||
|
|
56
|
+
family < 4 || family === 5 || family > 6
|
|
57
|
+
) {
|
|
58
|
+
throw new UrlError(`Invalid or private address: ${address}`);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function isValidPublicIPv4Address(address: string): boolean {
|
|
64
|
+
const parts = address.split(".");
|
|
65
|
+
const first = parseInt(parts[0]);
|
|
66
|
+
if (first === 0 || first === 10 || first === 127) return false;
|
|
67
|
+
const second = parseInt(parts[1]);
|
|
68
|
+
if (first === 169 && second === 254) return false;
|
|
69
|
+
if (first === 172 && second >= 16 && second <= 31) return false;
|
|
70
|
+
if (first === 192 && second === 168) return false;
|
|
71
|
+
return true;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function isValidPublicIPv6Address(address: string): boolean {
|
|
75
|
+
address = expandIPv6Address(address);
|
|
76
|
+
if (address.at(4) !== ":") return false;
|
|
77
|
+
const firstWord = parseInt(address.substring(0, 4), 16);
|
|
78
|
+
return !(
|
|
79
|
+
(firstWord >= 0xfc00 && firstWord <= 0xfdff) || // ULA
|
|
80
|
+
(firstWord >= 0xfe80 && firstWord <= 0xfebf) || // Link-local
|
|
81
|
+
firstWord === 0 || firstWord >= 0xff00 // Multicast
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export function expandIPv6Address(address: string): string {
|
|
86
|
+
address = address.toLowerCase();
|
|
87
|
+
if (address === "::") return "0000:0000:0000:0000:0000:0000:0000:0000";
|
|
88
|
+
if (address.startsWith("::")) address = "0000" + address;
|
|
89
|
+
if (address.endsWith("::")) address = address + "0000";
|
|
90
|
+
address = address.replace(
|
|
91
|
+
"::",
|
|
92
|
+
":0000".repeat(8 - (address.match(/:/g) || []).length) + ":",
|
|
93
|
+
);
|
|
94
|
+
const parts = address.split(":");
|
|
95
|
+
return parts.map((part) => part.padStart(4, "0")).join(":");
|
|
96
|
+
}
|