@fedify/vocab-runtime 2.0.0-pr.451.1730
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 +5183 -0
- package/dist/mod.d.cts +274 -0
- package/dist/mod.d.ts +276 -0
- package/dist/mod.js +5149 -0
- package/package.json +52 -0
- package/src/contexts.ts +4237 -0
- package/src/docloader.test.ts +365 -0
- package/src/docloader.ts +311 -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 +31 -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.ts +115 -0
- package/src/url.test.ts +59 -0
- package/src/url.ts +96 -0
- package/tsdown.config.ts +9 -0
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import type { CodecFactory } from "./types.d.ts";
|
|
2
|
+
|
|
3
|
+
const decode = (
|
|
4
|
+
string: string,
|
|
5
|
+
alphabet: string,
|
|
6
|
+
bitsPerChar: number,
|
|
7
|
+
): Uint8Array => {
|
|
8
|
+
// Build the character lookup table:
|
|
9
|
+
const codes: Record<string, number> = {};
|
|
10
|
+
for (let i = 0; i < alphabet.length; ++i) {
|
|
11
|
+
codes[alphabet[i]] = i;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
// Count the padding bytes:
|
|
15
|
+
let end = string.length;
|
|
16
|
+
while (string[end - 1] === "=") {
|
|
17
|
+
--end;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// Allocate the output:
|
|
21
|
+
const out = new Uint8Array((end * bitsPerChar / 8) | 0);
|
|
22
|
+
|
|
23
|
+
// Parse the data:
|
|
24
|
+
let bits = 0; // Number of bits currently in the buffer
|
|
25
|
+
let buffer = 0; // Bits waiting to be written out, MSB first
|
|
26
|
+
let written = 0; // Next byte to write
|
|
27
|
+
for (let i = 0; i < end; ++i) {
|
|
28
|
+
// Read one character from the string:
|
|
29
|
+
const value = codes[string[i]];
|
|
30
|
+
if (value === undefined) {
|
|
31
|
+
throw new SyntaxError("Invalid character " + string[i]);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Append the bits to the buffer:
|
|
35
|
+
buffer = (buffer << bitsPerChar) | value;
|
|
36
|
+
bits += bitsPerChar;
|
|
37
|
+
|
|
38
|
+
// Write out some bits if the buffer has a byte's worth:
|
|
39
|
+
if (bits >= 8) {
|
|
40
|
+
bits -= 8;
|
|
41
|
+
out[written++] = 0xff & (buffer >> bits);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Verify that we have received just enough bits:
|
|
46
|
+
if (bits >= bitsPerChar || 0xff & (buffer << (8 - bits))) {
|
|
47
|
+
throw new SyntaxError("Unexpected end of data");
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return out;
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
const encode = (
|
|
54
|
+
data: Uint8Array,
|
|
55
|
+
alphabet: string,
|
|
56
|
+
bitsPerChar: number,
|
|
57
|
+
): string => {
|
|
58
|
+
const pad = alphabet[alphabet.length - 1] === "=";
|
|
59
|
+
const mask = (1 << bitsPerChar) - 1;
|
|
60
|
+
let out = "";
|
|
61
|
+
|
|
62
|
+
let bits = 0; // Number of bits currently in the buffer
|
|
63
|
+
let buffer = 0; // Bits waiting to be written out, MSB first
|
|
64
|
+
for (let i = 0; i < data.length; ++i) {
|
|
65
|
+
// Slurp data into the buffer:
|
|
66
|
+
buffer = (buffer << 8) | data[i];
|
|
67
|
+
bits += 8;
|
|
68
|
+
|
|
69
|
+
// Write out as much as we can:
|
|
70
|
+
while (bits > bitsPerChar) {
|
|
71
|
+
bits -= bitsPerChar;
|
|
72
|
+
out += alphabet[mask & (buffer >> bits)];
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Partial character:
|
|
77
|
+
if (bits) {
|
|
78
|
+
out += alphabet[mask & (buffer << (bitsPerChar - bits))];
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Add padding characters until we hit a byte boundary:
|
|
82
|
+
if (pad) {
|
|
83
|
+
while ((out.length * bitsPerChar) & 7) {
|
|
84
|
+
out += "=";
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return out;
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* RFC4648 Factory
|
|
93
|
+
*/
|
|
94
|
+
export const rfc4648 = (bitsPerChar: number): CodecFactory => (alphabet) => {
|
|
95
|
+
return {
|
|
96
|
+
encode(input: Uint8Array): string {
|
|
97
|
+
return encode(input, alphabet, bitsPerChar);
|
|
98
|
+
},
|
|
99
|
+
decode(input: string): Uint8Array {
|
|
100
|
+
return decode(input, alphabet, bitsPerChar);
|
|
101
|
+
},
|
|
102
|
+
};
|
|
103
|
+
};
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
export type BaseCode =
|
|
2
|
+
| "\x00"
|
|
3
|
+
| "0"
|
|
4
|
+
| "7"
|
|
5
|
+
| "9"
|
|
6
|
+
| "f"
|
|
7
|
+
| "F"
|
|
8
|
+
| "v"
|
|
9
|
+
| "V"
|
|
10
|
+
| "t"
|
|
11
|
+
| "T"
|
|
12
|
+
| "b"
|
|
13
|
+
| "B"
|
|
14
|
+
| "c"
|
|
15
|
+
| "C"
|
|
16
|
+
| "h"
|
|
17
|
+
| "k"
|
|
18
|
+
| "K"
|
|
19
|
+
| "z"
|
|
20
|
+
| "Z"
|
|
21
|
+
| "m"
|
|
22
|
+
| "M"
|
|
23
|
+
| "u"
|
|
24
|
+
| "U";
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* - Names of the supported encodings
|
|
28
|
+
*/
|
|
29
|
+
export type BaseName =
|
|
30
|
+
| "identity"
|
|
31
|
+
| "base2"
|
|
32
|
+
| "base8"
|
|
33
|
+
| "base10"
|
|
34
|
+
| "base16"
|
|
35
|
+
| "base16upper"
|
|
36
|
+
| "base32hex"
|
|
37
|
+
| "base32hexupper"
|
|
38
|
+
| "base32hexpad"
|
|
39
|
+
| "base32hexpadupper"
|
|
40
|
+
| "base32"
|
|
41
|
+
| "base32upper"
|
|
42
|
+
| "base32pad"
|
|
43
|
+
| "base32padupper"
|
|
44
|
+
| "base32z"
|
|
45
|
+
| "base36"
|
|
46
|
+
| "base36upper"
|
|
47
|
+
| "base58btc"
|
|
48
|
+
| "base58flickr"
|
|
49
|
+
| "base64"
|
|
50
|
+
| "base64pad"
|
|
51
|
+
| "base64url"
|
|
52
|
+
| "base64urlpad";
|
|
53
|
+
|
|
54
|
+
export type BaseNameOrCode = BaseCode | BaseName;
|
|
55
|
+
export interface Codec {
|
|
56
|
+
encode: (buffer: Uint8Array) => string;
|
|
57
|
+
decode: (hash: string) => Uint8Array;
|
|
58
|
+
}
|
|
59
|
+
export interface CodecFactory {
|
|
60
|
+
(input: string): Codec;
|
|
61
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
const textDecoder = new TextDecoder();
|
|
2
|
+
export const decodeText = (bytes: DataView | Uint8Array): string =>
|
|
3
|
+
textDecoder.decode(bytes);
|
|
4
|
+
|
|
5
|
+
const textEncoder = new TextEncoder();
|
|
6
|
+
export const encodeText = (text: string): Uint8Array =>
|
|
7
|
+
textEncoder.encode(text);
|
|
8
|
+
|
|
9
|
+
export function concat(
|
|
10
|
+
arrs: Array<ArrayLike<number>>,
|
|
11
|
+
length: number,
|
|
12
|
+
): Uint8Array {
|
|
13
|
+
const output = new Uint8Array(length);
|
|
14
|
+
let offset = 0;
|
|
15
|
+
|
|
16
|
+
for (const arr of arrs) {
|
|
17
|
+
output.set(arr, offset);
|
|
18
|
+
offset += arr.length;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
return output;
|
|
22
|
+
}
|
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 createRequest(
|
|
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) {
|
|
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
|
+
}
|