@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/key.ts
ADDED
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
import { Integer, Sequence } from "asn1js";
|
|
2
|
+
import { decodeBase64, encodeBase64 } from "byte-encodings/base64";
|
|
3
|
+
import { decodeBase64Url } from "byte-encodings/base64url";
|
|
4
|
+
import { decodeHex } from "byte-encodings/hex";
|
|
5
|
+
import { addPrefix, getCodeFromData, rmPrefix } from "multicodec";
|
|
6
|
+
import { createPublicKey } from "node:crypto";
|
|
7
|
+
import { PublicKeyInfo } from "pkijs";
|
|
8
|
+
import { validateCryptoKey } from "./jwk.ts";
|
|
9
|
+
import { decodeMultibase, encodeMultibase } from "./multibase/mod.ts";
|
|
10
|
+
|
|
11
|
+
const algorithms: Record<
|
|
12
|
+
string,
|
|
13
|
+
| AlgorithmIdentifier
|
|
14
|
+
| HmacImportParams
|
|
15
|
+
| RsaHashedImportParams
|
|
16
|
+
| EcKeyImportParams
|
|
17
|
+
> = {
|
|
18
|
+
"1.2.840.113549.1.1.1": { name: "RSASSA-PKCS1-v1_5", hash: "SHA-256" },
|
|
19
|
+
"1.3.101.112": "Ed25519",
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Imports a PEM-SPKI formatted public key.
|
|
24
|
+
* @param pem The PEM-SPKI formatted public key.
|
|
25
|
+
* @returns The imported public key.
|
|
26
|
+
* @throws {TypeError} If the key is invalid or unsupported.
|
|
27
|
+
* @since 0.5.0
|
|
28
|
+
*/
|
|
29
|
+
export async function importSpki(pem: string): Promise<CryptoKey> {
|
|
30
|
+
pem = pem.replace(/(?:-----(?:BEGIN|END) PUBLIC KEY-----|\s)/g, "");
|
|
31
|
+
let spki: Uint8Array<ArrayBuffer>;
|
|
32
|
+
try {
|
|
33
|
+
spki = decodeBase64(pem);
|
|
34
|
+
} catch (_) {
|
|
35
|
+
throw new TypeError("Invalid PEM-SPKI format.");
|
|
36
|
+
}
|
|
37
|
+
const pki = PublicKeyInfo.fromBER(spki);
|
|
38
|
+
const oid = pki.algorithm.algorithmId;
|
|
39
|
+
const algorithm = algorithms[oid];
|
|
40
|
+
if (algorithm == null) {
|
|
41
|
+
throw new TypeError("Unsupported algorithm: " + oid);
|
|
42
|
+
}
|
|
43
|
+
return await crypto.subtle.importKey(
|
|
44
|
+
"spki",
|
|
45
|
+
spki,
|
|
46
|
+
algorithm,
|
|
47
|
+
true,
|
|
48
|
+
["verify"],
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Exports a public key in PEM-SPKI format.
|
|
54
|
+
* @param key The public key to export.
|
|
55
|
+
* @returns The exported public key in PEM-SPKI format.
|
|
56
|
+
* @throws {TypeError} If the key is invalid or unsupported.
|
|
57
|
+
* @since 0.5.0
|
|
58
|
+
*/
|
|
59
|
+
export async function exportSpki(key: CryptoKey): Promise<string> {
|
|
60
|
+
validateCryptoKey(key);
|
|
61
|
+
const spki = await crypto.subtle.exportKey("spki", key);
|
|
62
|
+
let pem = encodeBase64(spki);
|
|
63
|
+
pem = (pem.match(/.{1,64}/g) || []).join("\n");
|
|
64
|
+
return `-----BEGIN PUBLIC KEY-----\n${pem}\n-----END PUBLIC KEY-----\n`;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Imports a PEM-PKCS#1 formatted public key.
|
|
69
|
+
* @param pem The PEM-PKCS#1 formatted public key.
|
|
70
|
+
* @returns The imported public key.
|
|
71
|
+
* @throws {TypeError} If the key is invalid or unsupported.
|
|
72
|
+
* @since 1.5.0
|
|
73
|
+
*/
|
|
74
|
+
export function importPkcs1(pem: string): Promise<CryptoKey> {
|
|
75
|
+
const key = createPublicKey({ key: pem, format: "pem", type: "pkcs1" });
|
|
76
|
+
const spki = key.export({ type: "spki", format: "pem" }) as string;
|
|
77
|
+
return importSpki(spki);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const PKCS1_HEADER = /^\s*-----BEGIN\s+RSA\s+PUBLIC\s+KEY-----\s*\n/;
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Imports a PEM formatted public key (SPKI or PKCS#1).
|
|
84
|
+
* @param pem The PEM formatted public key to import (SPKI or PKCS#1).
|
|
85
|
+
* @returns The imported public key.
|
|
86
|
+
* @throws {TypeError} If the key is invalid or unsupported.
|
|
87
|
+
* @since 1.5.0
|
|
88
|
+
*/
|
|
89
|
+
export function importPem(pem: string): Promise<CryptoKey> {
|
|
90
|
+
return PKCS1_HEADER.test(pem) ? importPkcs1(pem) : importSpki(pem);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Imports a [Multibase]-encoded public key.
|
|
95
|
+
*
|
|
96
|
+
* [Multibase]: https://www.w3.org/TR/vc-data-integrity/#multibase-0
|
|
97
|
+
* @param key The Multibase-encoded public key.
|
|
98
|
+
* @returns The imported public key.
|
|
99
|
+
* @throws {TypeError} If the key is invalid or unsupported.
|
|
100
|
+
* @since 0.10.0
|
|
101
|
+
*/
|
|
102
|
+
export async function importMultibaseKey(key: string): Promise<CryptoKey> {
|
|
103
|
+
const decoded = decodeMultibase(key);
|
|
104
|
+
const code: number = getCodeFromData(decoded);
|
|
105
|
+
const content = rmPrefix(decoded);
|
|
106
|
+
if (code === 0x1205) { // rsa-pub
|
|
107
|
+
const keyObject = createPublicKey({
|
|
108
|
+
// deno-lint-ignore no-explicit-any
|
|
109
|
+
key: content as any,
|
|
110
|
+
format: "der",
|
|
111
|
+
type: "pkcs1",
|
|
112
|
+
});
|
|
113
|
+
const exported = keyObject.export({ type: "spki", format: "der" });
|
|
114
|
+
const spki = exported instanceof Uint8Array
|
|
115
|
+
? exported
|
|
116
|
+
: new Uint8Array(exported);
|
|
117
|
+
return await crypto.subtle.importKey(
|
|
118
|
+
"spki",
|
|
119
|
+
new Uint8Array(spki),
|
|
120
|
+
{ name: "RSASSA-PKCS1-v1_5", hash: "SHA-256" },
|
|
121
|
+
true,
|
|
122
|
+
["verify"],
|
|
123
|
+
);
|
|
124
|
+
} else if (code === 0xed) { // ed25519-pub
|
|
125
|
+
return await crypto.subtle.importKey(
|
|
126
|
+
"raw",
|
|
127
|
+
content.slice(),
|
|
128
|
+
"Ed25519",
|
|
129
|
+
true,
|
|
130
|
+
["verify"],
|
|
131
|
+
);
|
|
132
|
+
} else {
|
|
133
|
+
throw new TypeError("Unsupported key type: 0x" + code.toString(16));
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Exports a public key in [Multibase] format.
|
|
139
|
+
*
|
|
140
|
+
* [Multibase]: https://www.w3.org/TR/vc-data-integrity/#multibase-0
|
|
141
|
+
* @param key The public key to export.
|
|
142
|
+
* @returns The exported public key in Multibase format.
|
|
143
|
+
* @throws {TypeError} If the key is invalid or unsupported.
|
|
144
|
+
* @since 0.10.0
|
|
145
|
+
*/
|
|
146
|
+
export async function exportMultibaseKey(key: CryptoKey): Promise<string> {
|
|
147
|
+
let content: ArrayBuffer;
|
|
148
|
+
let code: number;
|
|
149
|
+
if (key.algorithm.name === "Ed25519") {
|
|
150
|
+
content = await crypto.subtle.exportKey("raw", key);
|
|
151
|
+
code = 0xed; // ed25519-pub
|
|
152
|
+
} else if (
|
|
153
|
+
key.algorithm.name === "RSASSA-PKCS1-v1_5" &&
|
|
154
|
+
(key.algorithm as unknown as { hash: { name: string } }).hash.name ===
|
|
155
|
+
"SHA-256"
|
|
156
|
+
) {
|
|
157
|
+
const jwk = await crypto.subtle.exportKey("jwk", key);
|
|
158
|
+
const decodedN = decodeBase64Url(jwk.n!);
|
|
159
|
+
const n = new Uint8Array(decodedN.length + 1);
|
|
160
|
+
n.set(decodedN, 1);
|
|
161
|
+
const sequence = new Sequence({
|
|
162
|
+
value: [
|
|
163
|
+
new Integer({
|
|
164
|
+
isHexOnly: true,
|
|
165
|
+
valueHex: n,
|
|
166
|
+
}),
|
|
167
|
+
new Integer({
|
|
168
|
+
isHexOnly: true,
|
|
169
|
+
valueHex: decodeBase64Url(jwk.e!),
|
|
170
|
+
}),
|
|
171
|
+
],
|
|
172
|
+
});
|
|
173
|
+
content = sequence.toBER(false);
|
|
174
|
+
code = 0x1205; // rsa-pub
|
|
175
|
+
} else {
|
|
176
|
+
throw new TypeError(
|
|
177
|
+
"Unsupported key type: " + JSON.stringify(key.algorithm),
|
|
178
|
+
);
|
|
179
|
+
}
|
|
180
|
+
const codeHex = code.toString(16);
|
|
181
|
+
const codeBytes = decodeHex(codeHex.length % 2 < 1 ? codeHex : "0" + codeHex);
|
|
182
|
+
const prefixed = addPrefix(codeBytes, new Uint8Array(content));
|
|
183
|
+
const encoded = encodeMultibase("base58btc", prefixed);
|
|
184
|
+
return new TextDecoder().decode(encoded);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// cSpell: ignore multicodec pkijs
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { deepStrictEqual } from "node:assert";
|
|
2
|
+
import { test } from "node:test";
|
|
3
|
+
import util from "node:util";
|
|
4
|
+
import { LanguageString } from "./langstr.ts";
|
|
5
|
+
|
|
6
|
+
test("new LanguageString()", () => {
|
|
7
|
+
const langStr = new LanguageString("Hello", "en");
|
|
8
|
+
deepStrictEqual(langStr.toString(), "Hello");
|
|
9
|
+
deepStrictEqual(langStr.locale, new Intl.Locale("en"));
|
|
10
|
+
|
|
11
|
+
deepStrictEqual(new LanguageString("Hello", new Intl.Locale("en")), langStr);
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
test("Deno.inspect(LanguageString)", () => {
|
|
15
|
+
const langStr = new LanguageString("Hello, 'world'", "en");
|
|
16
|
+
deepStrictEqual(
|
|
17
|
+
util.inspect(langStr, { colors: false }),
|
|
18
|
+
"<en> \"Hello, 'world'\"",
|
|
19
|
+
);
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
test("util.inspect(LanguageString)", () => {
|
|
23
|
+
const langStr = new LanguageString("Hello, 'world'", "en");
|
|
24
|
+
deepStrictEqual(
|
|
25
|
+
util.inspect(langStr, { colors: false }),
|
|
26
|
+
"<en> \"Hello, 'world'\"",
|
|
27
|
+
);
|
|
28
|
+
});
|
package/src/langstr.ts
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A language-tagged string which corresponds to the `rdf:langString` type.
|
|
3
|
+
*/
|
|
4
|
+
export class LanguageString extends String {
|
|
5
|
+
/**
|
|
6
|
+
* The locale of the string.
|
|
7
|
+
* @since 2.0.0
|
|
8
|
+
*/
|
|
9
|
+
readonly locale: Intl.Locale;
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Constructs a new `LanguageString`.
|
|
13
|
+
* @param value A string value written in the given language.
|
|
14
|
+
* @param locale The language of the string. If a string is given, it will
|
|
15
|
+
* be parsed as a `Intl.Locale` object.
|
|
16
|
+
*/
|
|
17
|
+
constructor(value: string, language: Intl.Locale | string) {
|
|
18
|
+
super(value);
|
|
19
|
+
this.locale = typeof language === "string"
|
|
20
|
+
? new Intl.Locale(language)
|
|
21
|
+
: language;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
[Symbol.for("Deno.customInspect")](
|
|
25
|
+
inspect: typeof Deno.inspect,
|
|
26
|
+
options: Deno.InspectOptions,
|
|
27
|
+
): string {
|
|
28
|
+
return `<${this.locale.baseName}> ${inspect(this.toString(), options)}`;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
[Symbol.for("nodejs.util.inspect.custom")](
|
|
32
|
+
_depth: number,
|
|
33
|
+
options: unknown,
|
|
34
|
+
inspect: (value: unknown, options: unknown) => string,
|
|
35
|
+
): string {
|
|
36
|
+
return `<${this.locale.baseName}> ${inspect(this.toString(), options)}`;
|
|
37
|
+
}
|
|
38
|
+
}
|
package/src/link.test.ts
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
// Borrowed from https://github.com/hugoalh/http-header-link-es
|
|
2
|
+
import { deepStrictEqual, throws } from "node:assert";
|
|
3
|
+
import { test } from "node:test";
|
|
4
|
+
import { HttpHeaderLink } from "./link.ts";
|
|
5
|
+
|
|
6
|
+
test("String Good 1", () => {
|
|
7
|
+
const instance = new HttpHeaderLink(
|
|
8
|
+
`<https://example.com>; rel="preconnect"`,
|
|
9
|
+
);
|
|
10
|
+
deepStrictEqual(instance.hasParameter("rel", "preconnect"), true);
|
|
11
|
+
deepStrictEqual(instance.hasParameter("rel", "connect"), false);
|
|
12
|
+
deepStrictEqual(instance.hasParameter("rel", "postconnect"), false);
|
|
13
|
+
deepStrictEqual(instance.getByRel("preconnect")[0][0], "https://example.com");
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
test("String Good 2", () => {
|
|
17
|
+
const instance = new HttpHeaderLink(`<https://example.com>; rel=preconnect`);
|
|
18
|
+
deepStrictEqual(instance.hasParameter("rel", "preconnect"), true);
|
|
19
|
+
deepStrictEqual(instance.hasParameter("rel", "connect"), false);
|
|
20
|
+
deepStrictEqual(instance.hasParameter("rel", "postconnect"), false);
|
|
21
|
+
deepStrictEqual(instance.getByRel("preconnect")[0][0], "https://example.com");
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
test("String Good 3", () => {
|
|
25
|
+
const instance = new HttpHeaderLink(
|
|
26
|
+
`<https://example.com/%E8%8B%97%E6%9D%A1>; rel="preconnect"`,
|
|
27
|
+
);
|
|
28
|
+
deepStrictEqual(instance.hasParameter("rel", "preconnect"), true);
|
|
29
|
+
deepStrictEqual(instance.hasParameter("rel", "connect"), false);
|
|
30
|
+
deepStrictEqual(instance.hasParameter("rel", "postconnect"), false);
|
|
31
|
+
deepStrictEqual(
|
|
32
|
+
instance.getByRel("preconnect")[0][0],
|
|
33
|
+
"https://example.com/苗条",
|
|
34
|
+
);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
test("String Good 4", () => {
|
|
38
|
+
const instance = new HttpHeaderLink(
|
|
39
|
+
`<https://one.example.com>; rel="preconnect", <https://two.example.com>; rel="preconnect", <https://three.example.com>; rel="preconnect"`,
|
|
40
|
+
);
|
|
41
|
+
deepStrictEqual(instance.hasParameter("rel", "preconnect"), true);
|
|
42
|
+
deepStrictEqual(instance.hasParameter("rel", "connect"), false);
|
|
43
|
+
deepStrictEqual(instance.hasParameter("rel", "postconnect"), false);
|
|
44
|
+
deepStrictEqual(
|
|
45
|
+
instance.getByRel("preconnect")[0][0],
|
|
46
|
+
"https://one.example.com",
|
|
47
|
+
);
|
|
48
|
+
deepStrictEqual(
|
|
49
|
+
instance.getByRel("preconnect")[1][0],
|
|
50
|
+
"https://two.example.com",
|
|
51
|
+
);
|
|
52
|
+
deepStrictEqual(
|
|
53
|
+
instance.getByRel("preconnect")[2][0],
|
|
54
|
+
"https://three.example.com",
|
|
55
|
+
);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
test("String Good 5", () => {
|
|
59
|
+
const instance = new HttpHeaderLink();
|
|
60
|
+
deepStrictEqual(instance.hasParameter("rel", "preconnect"), false);
|
|
61
|
+
deepStrictEqual(instance.hasParameter("rel", "connect"), false);
|
|
62
|
+
deepStrictEqual(instance.hasParameter("rel", "postconnect"), false);
|
|
63
|
+
deepStrictEqual(instance.entries().length, 0);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
test("Entries Good 1", () => {
|
|
67
|
+
const instance = new HttpHeaderLink([["https://one.example.com", {
|
|
68
|
+
rel: "preconnect",
|
|
69
|
+
}]]);
|
|
70
|
+
deepStrictEqual(instance.hasParameter("rel", "preconnect"), true);
|
|
71
|
+
deepStrictEqual(instance.entries().length, 1);
|
|
72
|
+
deepStrictEqual(
|
|
73
|
+
instance.toString(),
|
|
74
|
+
`<https://one.example.com>; rel="preconnect"`,
|
|
75
|
+
);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
test("String Bad 1", () => {
|
|
79
|
+
throws(() => {
|
|
80
|
+
new HttpHeaderLink(`https://bad.example; rel="preconnect"`);
|
|
81
|
+
});
|
|
82
|
+
});
|
package/src/link.ts
ADDED
|
@@ -0,0 +1,345 @@
|
|
|
1
|
+
// Borrowed from https://github.com/hugoalh/http-header-link-es
|
|
2
|
+
const parametersNeedLowerCase: readonly string[] = ["rel", "type"];
|
|
3
|
+
|
|
4
|
+
const regexpLinkWhitespace = /[\n\r\s\t]/;
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* HTTP header `Link` entry.
|
|
8
|
+
*/
|
|
9
|
+
export type HttpHeaderLinkEntry = [
|
|
10
|
+
uri: string,
|
|
11
|
+
parameters: { [key: string]: string },
|
|
12
|
+
];
|
|
13
|
+
|
|
14
|
+
function validateURI(uri: string): void {
|
|
15
|
+
if (uri.includes("\n") || regexpLinkWhitespace.test(uri)) {
|
|
16
|
+
throw new SyntaxError(`\`${uri}\` is not a valid URI!`);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function* parseLinkFromString(input: string): Generator<HttpHeaderLinkEntry> {
|
|
21
|
+
// Remove Unicode characters of BOM (Byte Order Mark) and no-break space.
|
|
22
|
+
const inputFmt: string = input.replaceAll("\u00A0", "").replaceAll(
|
|
23
|
+
"\uFEFF",
|
|
24
|
+
"",
|
|
25
|
+
);
|
|
26
|
+
for (let cursor: number = 0; cursor < inputFmt.length; cursor += 1) {
|
|
27
|
+
while (regexpLinkWhitespace.test(inputFmt.charAt(cursor))) {
|
|
28
|
+
cursor += 1;
|
|
29
|
+
}
|
|
30
|
+
if (inputFmt.charAt(cursor) !== "<") {
|
|
31
|
+
throw new SyntaxError(
|
|
32
|
+
`Unexpected character \`${
|
|
33
|
+
inputFmt.charAt(cursor)
|
|
34
|
+
}\` at position ${cursor}; Expect character \`<\`!`,
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
cursor += 1;
|
|
38
|
+
const cursorEndUri: number = inputFmt.indexOf(">", cursor);
|
|
39
|
+
if (cursorEndUri === -1) {
|
|
40
|
+
throw new SyntaxError(
|
|
41
|
+
`Missing end of URI delimiter character \`>\` after position ${cursor}!`,
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
if (cursorEndUri === cursor) {
|
|
45
|
+
throw new SyntaxError(`Missing URI at position ${cursor}!`);
|
|
46
|
+
}
|
|
47
|
+
const uriSlice: string = inputFmt.slice(cursor, cursorEndUri);
|
|
48
|
+
validateURI(uriSlice);
|
|
49
|
+
const uri: HttpHeaderLinkEntry[0] = decodeURI(uriSlice);
|
|
50
|
+
const parameters: HttpHeaderLinkEntry[1] = {};
|
|
51
|
+
cursor = cursorEndUri + 1;
|
|
52
|
+
while (regexpLinkWhitespace.test(inputFmt.charAt(cursor))) {
|
|
53
|
+
cursor += 1;
|
|
54
|
+
}
|
|
55
|
+
if (
|
|
56
|
+
cursor === inputFmt.length ||
|
|
57
|
+
inputFmt.charAt(cursor) === ","
|
|
58
|
+
) {
|
|
59
|
+
yield [uri, parameters];
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
if (inputFmt.charAt(cursor) !== ";") {
|
|
63
|
+
throw new SyntaxError(
|
|
64
|
+
`Unexpected character \`${
|
|
65
|
+
inputFmt.charAt(cursor)
|
|
66
|
+
}\` at position ${cursor}; Expect character \`;\`!`,
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
cursor += 1;
|
|
70
|
+
while (cursor < inputFmt.length) {
|
|
71
|
+
while (regexpLinkWhitespace.test(inputFmt.charAt(cursor))) {
|
|
72
|
+
cursor += 1;
|
|
73
|
+
}
|
|
74
|
+
const parameterKey: string | undefined = inputFmt.slice(cursor).match(
|
|
75
|
+
/^[\w-]+\*?/,
|
|
76
|
+
)?.[0].toLowerCase();
|
|
77
|
+
if (typeof parameterKey === "undefined") {
|
|
78
|
+
throw new SyntaxError(
|
|
79
|
+
`Unexpected character \`${
|
|
80
|
+
inputFmt.charAt(cursor)
|
|
81
|
+
}\` at position ${cursor}; Expect a valid parameter key!`,
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
cursor += parameterKey.length;
|
|
85
|
+
while (regexpLinkWhitespace.test(inputFmt.charAt(cursor))) {
|
|
86
|
+
cursor += 1;
|
|
87
|
+
}
|
|
88
|
+
if (
|
|
89
|
+
cursor === inputFmt.length ||
|
|
90
|
+
inputFmt.charAt(cursor) === ","
|
|
91
|
+
) {
|
|
92
|
+
parameters[parameterKey] = "";
|
|
93
|
+
break;
|
|
94
|
+
}
|
|
95
|
+
if (inputFmt.charAt(cursor) === ";") {
|
|
96
|
+
parameters[parameterKey] = "";
|
|
97
|
+
cursor += 1;
|
|
98
|
+
continue;
|
|
99
|
+
}
|
|
100
|
+
if (inputFmt.charAt(cursor) !== "=") {
|
|
101
|
+
throw new SyntaxError(
|
|
102
|
+
`Unexpected character \`${
|
|
103
|
+
inputFmt.charAt(cursor)
|
|
104
|
+
}\` at position ${cursor}; Expect character \`=\`!`,
|
|
105
|
+
);
|
|
106
|
+
}
|
|
107
|
+
cursor += 1;
|
|
108
|
+
while (regexpLinkWhitespace.test(inputFmt.charAt(cursor))) {
|
|
109
|
+
cursor += 1;
|
|
110
|
+
}
|
|
111
|
+
let parameterValue: string = "";
|
|
112
|
+
if (inputFmt.charAt(cursor) === '"') {
|
|
113
|
+
cursor += 1;
|
|
114
|
+
while (cursor < inputFmt.length) {
|
|
115
|
+
if (inputFmt.charAt(cursor) === '"') {
|
|
116
|
+
cursor += 1;
|
|
117
|
+
break;
|
|
118
|
+
}
|
|
119
|
+
if (inputFmt.charAt(cursor) === "\\") {
|
|
120
|
+
cursor += 1;
|
|
121
|
+
}
|
|
122
|
+
parameterValue += inputFmt.charAt(cursor);
|
|
123
|
+
cursor += 1;
|
|
124
|
+
}
|
|
125
|
+
} else {
|
|
126
|
+
const cursorDiffParameterValue: number = inputFmt.slice(cursor).search(
|
|
127
|
+
/[\s;,]/,
|
|
128
|
+
);
|
|
129
|
+
if (cursorDiffParameterValue === -1) {
|
|
130
|
+
parameterValue += inputFmt.slice(cursor);
|
|
131
|
+
cursor += parameterValue.length;
|
|
132
|
+
} else {
|
|
133
|
+
parameterValue += inputFmt.slice(cursor, cursorDiffParameterValue);
|
|
134
|
+
cursor += cursorDiffParameterValue;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
parameters[parameterKey] = parametersNeedLowerCase.includes(parameterKey)
|
|
138
|
+
? parameterValue.toLowerCase()
|
|
139
|
+
: parameterValue;
|
|
140
|
+
while (regexpLinkWhitespace.test(inputFmt.charAt(cursor))) {
|
|
141
|
+
cursor += 1;
|
|
142
|
+
}
|
|
143
|
+
if (
|
|
144
|
+
cursor === inputFmt.length ||
|
|
145
|
+
inputFmt.charAt(cursor) === ","
|
|
146
|
+
) {
|
|
147
|
+
break;
|
|
148
|
+
}
|
|
149
|
+
if (inputFmt.charAt(cursor) === ";") {
|
|
150
|
+
cursor += 1;
|
|
151
|
+
continue;
|
|
152
|
+
}
|
|
153
|
+
throw new SyntaxError(
|
|
154
|
+
`Unexpected character \`${
|
|
155
|
+
inputFmt.charAt(cursor)
|
|
156
|
+
}\` at position ${cursor}; Expect character \`,\`, character \`;\`, or end of the string!`,
|
|
157
|
+
);
|
|
158
|
+
}
|
|
159
|
+
yield [uri, parameters];
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Handle the HTTP header `Link` according to the specification RFC 8288.
|
|
165
|
+
*/
|
|
166
|
+
export class HttpHeaderLink {
|
|
167
|
+
get [Symbol.toStringTag](): string {
|
|
168
|
+
return "HTTPHeaderLink";
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
#entries: HttpHeaderLinkEntry[] = [];
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Handle the HTTP header `Link` according to the specification RFC 8288.
|
|
175
|
+
* @param {...(string | Headers | HttpHeaderLink | HttpHeaderLinkEntry[] | Response)} inputs Input.
|
|
176
|
+
*/
|
|
177
|
+
constructor(
|
|
178
|
+
...inputs:
|
|
179
|
+
(string | Headers | HttpHeaderLink | HttpHeaderLinkEntry[] | Response)[]
|
|
180
|
+
) {
|
|
181
|
+
if (inputs.length > 0) {
|
|
182
|
+
this.add(...inputs);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Add entries.
|
|
188
|
+
* @param {...(string | Headers | HttpHeaderLink | HttpHeaderLinkEntry[] | Response)} inputs Input.
|
|
189
|
+
* @returns {this}
|
|
190
|
+
*/
|
|
191
|
+
add(
|
|
192
|
+
...inputs:
|
|
193
|
+
(string | Headers | HttpHeaderLink | HttpHeaderLinkEntry[] | Response)[]
|
|
194
|
+
): this {
|
|
195
|
+
for (const input of inputs) {
|
|
196
|
+
if (input instanceof HttpHeaderLink) {
|
|
197
|
+
this.#entries.push(...structuredClone(input.#entries));
|
|
198
|
+
} else if (Array.isArray(input)) {
|
|
199
|
+
this.#entries.push(
|
|
200
|
+
...input.map(
|
|
201
|
+
([uri, parameters]: HttpHeaderLinkEntry): HttpHeaderLinkEntry => {
|
|
202
|
+
validateURI(uri);
|
|
203
|
+
Object.entries(parameters).forEach(
|
|
204
|
+
([key, value]: [string, string]): void => {
|
|
205
|
+
if (
|
|
206
|
+
key !== key.toLowerCase() ||
|
|
207
|
+
!(/^[\w-]+\*?$/.test(key))
|
|
208
|
+
) {
|
|
209
|
+
throw new SyntaxError(
|
|
210
|
+
`\`${key}\` is not a valid parameter key!`,
|
|
211
|
+
);
|
|
212
|
+
}
|
|
213
|
+
if (
|
|
214
|
+
parametersNeedLowerCase.includes(key) &&
|
|
215
|
+
value !== value.toLowerCase()
|
|
216
|
+
) {
|
|
217
|
+
throw new SyntaxError(
|
|
218
|
+
`\`${value}\` is not a valid parameter value!`,
|
|
219
|
+
);
|
|
220
|
+
}
|
|
221
|
+
},
|
|
222
|
+
);
|
|
223
|
+
return [uri, structuredClone(parameters)];
|
|
224
|
+
},
|
|
225
|
+
),
|
|
226
|
+
);
|
|
227
|
+
} else {
|
|
228
|
+
for (
|
|
229
|
+
const entry of parseLinkFromString(
|
|
230
|
+
((
|
|
231
|
+
input instanceof Headers ||
|
|
232
|
+
input instanceof Response
|
|
233
|
+
)
|
|
234
|
+
? ((input instanceof Headers) ? input : input.headers).get("Link")
|
|
235
|
+
: input) ?? "",
|
|
236
|
+
)
|
|
237
|
+
) {
|
|
238
|
+
this.#entries.push(entry);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
return this;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* Return all of the entries.
|
|
247
|
+
* @returns {HttpHeaderLinkEntry[]} Entries.
|
|
248
|
+
*/
|
|
249
|
+
entries(): HttpHeaderLinkEntry[] {
|
|
250
|
+
return structuredClone(this.#entries);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* Get entries by parameter.
|
|
255
|
+
* @param {string} key Key of the parameter.
|
|
256
|
+
* @param {string} value Value of the parameter.
|
|
257
|
+
* @returns {HttpHeaderLinkEntry[]} Entries which match the parameter.
|
|
258
|
+
*/
|
|
259
|
+
getByParameter(key: string, value: string): HttpHeaderLinkEntry[] {
|
|
260
|
+
if (key !== key.toLowerCase()) {
|
|
261
|
+
throw new SyntaxError(`\`${key}\` is not a valid parameter key!`);
|
|
262
|
+
}
|
|
263
|
+
if (key === "rel") {
|
|
264
|
+
return this.getByRel(value);
|
|
265
|
+
}
|
|
266
|
+
return structuredClone(
|
|
267
|
+
this.#entries.filter((entry: HttpHeaderLinkEntry): boolean => {
|
|
268
|
+
return (entry[1][key] === value);
|
|
269
|
+
}),
|
|
270
|
+
);
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* Get entries by parameter `rel`.
|
|
275
|
+
* @param {string} value Value of the parameter `rel`.
|
|
276
|
+
* @returns {HttpHeaderLinkEntry[]} Entries which match the parameter.
|
|
277
|
+
*/
|
|
278
|
+
getByRel(value: string): HttpHeaderLinkEntry[] {
|
|
279
|
+
if (value !== value.toLowerCase()) {
|
|
280
|
+
throw new SyntaxError(
|
|
281
|
+
`\`${value}\` is not a valid parameter \`rel\` value!`,
|
|
282
|
+
);
|
|
283
|
+
}
|
|
284
|
+
return structuredClone(
|
|
285
|
+
this.#entries.filter((entity: HttpHeaderLinkEntry): boolean => {
|
|
286
|
+
return (entity[1].rel?.toLowerCase() === value);
|
|
287
|
+
}),
|
|
288
|
+
);
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
/**
|
|
292
|
+
* Whether have entries that match parameter.
|
|
293
|
+
* @param {string} key Key of the parameter.
|
|
294
|
+
* @param {string} value Value of the parameter.
|
|
295
|
+
* @returns {boolean} Determine result.
|
|
296
|
+
*/
|
|
297
|
+
hasParameter(key: string, value: string): boolean {
|
|
298
|
+
return (this.getByParameter(key, value).length > 0);
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
/**
|
|
302
|
+
* Stringify entries.
|
|
303
|
+
* @returns {string} Stringified entries.
|
|
304
|
+
*/
|
|
305
|
+
toString(): string {
|
|
306
|
+
return this.#entries.map(
|
|
307
|
+
([uri, parameters]: HttpHeaderLinkEntry): string => {
|
|
308
|
+
return [
|
|
309
|
+
`<${encodeURI(uri)}>`,
|
|
310
|
+
...Object.entries(parameters).map(
|
|
311
|
+
([key, value]: [string, string]): string => {
|
|
312
|
+
return ((value.length > 0)
|
|
313
|
+
? `${key}="${value.replaceAll('"', '\\"')}"`
|
|
314
|
+
: key);
|
|
315
|
+
},
|
|
316
|
+
),
|
|
317
|
+
].join("; ");
|
|
318
|
+
},
|
|
319
|
+
).join(", ");
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
/**
|
|
323
|
+
* Parse the HTTP header `Link` according to the specification RFC 8288.
|
|
324
|
+
* @param {...(string | Headers | HttpHeaderLink | HttpHeaderLinkEntry[] | Response)} inputs Input.
|
|
325
|
+
* @returns {HttpHeaderLink}
|
|
326
|
+
*/
|
|
327
|
+
static parse(
|
|
328
|
+
...inputs:
|
|
329
|
+
(string | Headers | HttpHeaderLink | HttpHeaderLinkEntry[] | Response)[]
|
|
330
|
+
): HttpHeaderLink {
|
|
331
|
+
return new this(...inputs);
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
/**
|
|
335
|
+
* Stringify as the HTTP header `Link` according to the specification RFC 8288.
|
|
336
|
+
* @param {...(string | Headers | HttpHeaderLink | HttpHeaderLinkEntry[] | Response)} inputs Input.
|
|
337
|
+
* @returns {string}
|
|
338
|
+
*/
|
|
339
|
+
static stringify(
|
|
340
|
+
...inputs:
|
|
341
|
+
(string | Headers | HttpHeaderLink | HttpHeaderLinkEntry[] | Response)[]
|
|
342
|
+
): string {
|
|
343
|
+
return new this(...inputs).toString();
|
|
344
|
+
}
|
|
345
|
+
}
|