@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/src/jwk.ts ADDED
@@ -0,0 +1,70 @@
1
+ export function validateCryptoKey(
2
+ key: CryptoKey,
3
+ type?: "public" | "private",
4
+ ): void {
5
+ if (type != null && key.type !== type) {
6
+ throw new TypeError(`The key is not a ${type} key.`);
7
+ }
8
+ if (!key.extractable) {
9
+ throw new TypeError("The key is not extractable.");
10
+ }
11
+ if (
12
+ key.algorithm.name !== "RSASSA-PKCS1-v1_5" &&
13
+ key.algorithm.name !== "Ed25519"
14
+ ) {
15
+ throw new TypeError(
16
+ "Currently only RSASSA-PKCS1-v1_5 and Ed25519 keys are supported. " +
17
+ "More algorithms will be added in the future!",
18
+ );
19
+ }
20
+ if (key.algorithm.name === "RSASSA-PKCS1-v1_5") {
21
+ // @ts-ignore TS2304
22
+ const algorithm = key.algorithm as unknown as RsaHashedKeyAlgorithm;
23
+ if (algorithm.hash.name !== "SHA-256") {
24
+ throw new TypeError(
25
+ "For compatibility with the existing Fediverse software " +
26
+ "(e.g., Mastodon), hash algorithm for RSASSA-PKCS1-v1_5 keys " +
27
+ "must be SHA-256.",
28
+ );
29
+ }
30
+ }
31
+ }
32
+
33
+ export async function exportJwk(key: CryptoKey): Promise<JsonWebKey> {
34
+ validateCryptoKey(key);
35
+ const jwk = await crypto.subtle.exportKey("jwk", key);
36
+ if (jwk.crv === "Ed25519") jwk.alg = "Ed25519";
37
+ return jwk;
38
+ }
39
+
40
+ export async function importJwk(
41
+ jwk: JsonWebKey,
42
+ type: "public" | "private",
43
+ ): Promise<CryptoKey> {
44
+ let key: CryptoKey;
45
+ if (jwk.kty === "RSA" && jwk.alg === "RS256") {
46
+ key = await crypto.subtle.importKey(
47
+ "jwk",
48
+ jwk,
49
+ { name: "RSASSA-PKCS1-v1_5", hash: "SHA-256" },
50
+ true,
51
+ type === "public" ? ["verify"] : ["sign"],
52
+ );
53
+ } else if (jwk.kty === "OKP" && jwk.crv === "Ed25519") {
54
+ if (navigator?.userAgent === "Cloudflare-Workers") {
55
+ jwk = { ...jwk };
56
+ delete jwk.alg;
57
+ }
58
+ key = await crypto.subtle.importKey(
59
+ "jwk",
60
+ jwk,
61
+ "Ed25519",
62
+ true,
63
+ type === "public" ? ["verify"] : ["sign"],
64
+ );
65
+ } else {
66
+ throw new TypeError("Unsupported JWK format.");
67
+ }
68
+ validateCryptoKey(key, type);
69
+ return key;
70
+ }
@@ -0,0 +1,179 @@
1
+ import { deepStrictEqual } from "node:assert";
2
+ import { test } from "node:test";
3
+ import { exportJwk, importJwk } from "./jwk.ts";
4
+ import {
5
+ exportMultibaseKey,
6
+ exportSpki,
7
+ importMultibaseKey,
8
+ importPem,
9
+ importPkcs1,
10
+ importSpki,
11
+ } from "./key.ts";
12
+
13
+ // cSpell: disable
14
+ const rsaSpki = "-----BEGIN PUBLIC KEY-----\n" +
15
+ "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAxsRuvCkgJtflBTl4OVsm\n" +
16
+ "nt/J1mQfZasfJtN33dcZ3d1lJroxmgmMu69zjGEAwkNbMQaWNLqC4eogkJaeJ4RR\n" +
17
+ "5MHYXkL9nNilVoTkjX5BVit3puzs7XJ7WQnKQgQMI+ezn24GHsZ/v1JIo77lerX5\n" +
18
+ "k4HNwTNVt+yaZVQWaOMR3+6FwziQR6kd0VuG9/a9dgAnz2cEoORRC1i4W7IZaB1s\n" +
19
+ "Znh1WbHbevlGd72HSXll5rocPIHn8gq6xpBgpHwRphlRsgn4KHaJ6brXDIJjrnQh\n" +
20
+ "Ie/YUBOGj/ImSEXhRwlFerKsoAVnZ0Hwbfa46qk44TAt8CyoPMWmpK6pt0ng4pQ2\n" +
21
+ "uwIDAQAB\n" +
22
+ "-----END PUBLIC KEY-----\n";
23
+ // cSpell: enable
24
+
25
+ // cSpell: disable
26
+ const rsaPkcs1 = "-----BEGIN RSA PUBLIC KEY-----\n" +
27
+ "MIIBCgKCAQEAxsRuvCkgJtflBTl4OVsmnt/J1mQfZasfJtN33dcZ3d1lJroxmgmM\n" +
28
+ "u69zjGEAwkNbMQaWNLqC4eogkJaeJ4RR5MHYXkL9nNilVoTkjX5BVit3puzs7XJ7\n" +
29
+ "WQnKQgQMI+ezn24GHsZ/v1JIo77lerX5k4HNwTNVt+yaZVQWaOMR3+6FwziQR6kd\n" +
30
+ "0VuG9/a9dgAnz2cEoORRC1i4W7IZaB1sZnh1WbHbevlGd72HSXll5rocPIHn8gq6\n" +
31
+ "xpBgpHwRphlRsgn4KHaJ6brXDIJjrnQhIe/YUBOGj/ImSEXhRwlFerKsoAVnZ0Hw\n" +
32
+ "bfa46qk44TAt8CyoPMWmpK6pt0ng4pQ2uwIDAQAB\n" +
33
+ "-----END RSA PUBLIC KEY-----\n";
34
+ // cSpell: enable
35
+
36
+ const rsaJwk: JsonWebKey = {
37
+ alg: "RS256",
38
+ // cSpell: disable
39
+ e: "AQAB",
40
+ // cSpell: enable
41
+ ext: true,
42
+ key_ops: ["verify"],
43
+ kty: "RSA",
44
+ // cSpell: disable
45
+ n: "xsRuvCkgJtflBTl4OVsmnt_J1mQfZasfJtN33dcZ3d1lJroxmgmMu69zjGEAwkNbMQaWN" +
46
+ "LqC4eogkJaeJ4RR5MHYXkL9nNilVoTkjX5BVit3puzs7XJ7WQnKQgQMI-ezn24GHsZ_v1J" +
47
+ "Io77lerX5k4HNwTNVt-yaZVQWaOMR3-6FwziQR6kd0VuG9_a9dgAnz2cEoORRC1i4W7IZa" +
48
+ "B1sZnh1WbHbevlGd72HSXll5rocPIHn8gq6xpBgpHwRphlRsgn4KHaJ6brXDIJjrnQhIe_" +
49
+ "YUBOGj_ImSEXhRwlFerKsoAVnZ0Hwbfa46qk44TAt8CyoPMWmpK6pt0ng4pQ2uw",
50
+ // cSpell: enable
51
+ };
52
+
53
+ const rsaMultibase =
54
+ // cSpell: disable
55
+ "z4MXj1wBzi9jUstyPqYMn6Gum79JtbKFiHTibtPRoPeufjdimA24Kg8Q5N7E2eMpgVUtD61kUv" +
56
+ "my4FaT5D5G8XU3ktxeduwEw5FHTtiLCzaruadf6rit1AUPL34UtcPuHh6GxBzTxgFKMMuzcHiU" +
57
+ "zG9wvbxn7toS4H2gbmUn1r91836ET2EVgmSdzju614Wu67ukyBGivcboncdfxPSR5JXwURBaL8" +
58
+ "K2P6yhKn3NyprFV8s6QpN4zgQMAD3Q6fjAsEvGNwXaQTZmEN2yd1NQ7uBE3RJ2XywZnehmfLQT" +
59
+ "EqD7Ad5XM3qfLLd9CtdzJGBkRfunHhkH1kz8dHL7hXwtk5EMXktY4QF5gZ1uisUV5mpPjEgqz7uDz";
60
+ // cSpell: enable
61
+
62
+ // cSpell: disable
63
+ const ed25519Pem = "-----BEGIN PUBLIC KEY-----\n" +
64
+ "MCowBQYDK2VwAyEAvrabdlLgVI5jWl7GpF+fLFJVF4ccI8D7h+v5ulBCYwo=\n" +
65
+ "-----END PUBLIC KEY-----\n";
66
+ // cSpell: enable
67
+
68
+ const ed25519Jwk: JsonWebKey = {
69
+ alg: "Ed25519",
70
+ kty: "OKP",
71
+ crv: "Ed25519",
72
+ // cSpell: disable
73
+ x: "vrabdlLgVI5jWl7GpF-fLFJVF4ccI8D7h-v5ulBCYwo",
74
+ // cSpell: enable
75
+ key_ops: ["verify"],
76
+ ext: true,
77
+ };
78
+
79
+ // cSpell: disable
80
+ const ed25519Multibase = "z6MksHj1MJnidCtDiyYW9ugNFftoX9fLK4bornTxmMZ6X7vq";
81
+ // cSpell: enable
82
+
83
+ test("importSpki()", async () => {
84
+ const rsaKey = await importSpki(rsaSpki);
85
+ deepStrictEqual(await exportJwk(rsaKey), rsaJwk);
86
+
87
+ const ed25519Key = await importSpki(ed25519Pem);
88
+ deepStrictEqual(await exportJwk(ed25519Key), ed25519Jwk);
89
+ });
90
+
91
+ test("exportSpki()", async () => {
92
+ const rsaKey = await importJwk(rsaJwk, "public");
93
+ const rsaSpki = await exportSpki(rsaKey);
94
+ deepStrictEqual(rsaSpki, rsaSpki);
95
+
96
+ const ed25519Key = await importJwk(ed25519Jwk, "public");
97
+ const ed25519Spki = await exportSpki(ed25519Key);
98
+ deepStrictEqual(ed25519Spki, ed25519Pem);
99
+ });
100
+
101
+ test("importPkcs1()", async () => {
102
+ const rsaKey = await importPkcs1(rsaPkcs1);
103
+ deepStrictEqual(await exportJwk(rsaKey), rsaJwk);
104
+ });
105
+
106
+ test("importPem()", async () => {
107
+ const rsaPkcs1Key = await importPem(rsaPkcs1);
108
+ deepStrictEqual(await exportJwk(rsaPkcs1Key), rsaJwk);
109
+
110
+ const rsaSpkiKey = await importPem(rsaSpki);
111
+ deepStrictEqual(await exportJwk(rsaSpkiKey), rsaJwk);
112
+
113
+ const ed25519Key = await importPem(ed25519Pem);
114
+ deepStrictEqual(await exportJwk(ed25519Key), ed25519Jwk);
115
+ });
116
+
117
+ test("importMultibase()", async () => {
118
+ const rsaKey = await importMultibaseKey(rsaMultibase);
119
+ deepStrictEqual(await exportJwk(rsaKey), rsaJwk);
120
+
121
+ const ed25519Key = await importMultibaseKey(ed25519Multibase);
122
+ deepStrictEqual(await exportJwk(ed25519Key), ed25519Jwk);
123
+ });
124
+
125
+ test("exportMultibaseKey()", async () => {
126
+ const rsaKey = await importJwk(rsaJwk, "public");
127
+ const rsaMb = await exportMultibaseKey(rsaKey);
128
+ deepStrictEqual(rsaMb, rsaMultibase);
129
+
130
+ const ed25519Key = await importJwk(ed25519Jwk, "public");
131
+ const ed25519Mb = await exportMultibaseKey(ed25519Key);
132
+ deepStrictEqual(ed25519Mb, ed25519Multibase);
133
+
134
+ // Test vectors from <https://codeberg.org/fediverse/fep/src/branch/main/fep/521a/fep-521a.feature>:
135
+ const rsaKey2 = await importJwk({
136
+ alg: "RS256",
137
+ ext: true,
138
+ key_ops: ["verify"],
139
+ // cSpell: disable
140
+ e: "AQAB",
141
+ kty: "RSA",
142
+ n: "sbX82NTV6IylxCh7MfV4hlyvaniCajuP97GyOqSvTmoEdBOflFvZ06kR_9D6ctt45Fk6h" +
143
+ "skfnag2GG69NALVH2o4RCR6tQiLRpKcMRtDYE_thEmfBvDzm_VVkOIYfxu-Ipuo9J_S5XD" +
144
+ "NDjczx2v-3oDh5-CIHkU46hvFeCvpUS-L8TJSbgX0kjVk_m4eIb9wh63rtmD6Uz_KBtCo5" +
145
+ "mmR4TEtcLZKYdqMp3wCjN-TlgHiz_4oVXWbHUefCEe8rFnX1iQnpDHU49_SaXQoud1jCae" +
146
+ "xFn25n-Aa8f8bc5Vm-5SeRwidHa6ErvEhTvf1dz6GoNPp2iRvm-wJ1gxwWJEYPQ",
147
+ // cSpell: enable
148
+ }, "public");
149
+ const rsaMb2 = await exportMultibaseKey(rsaKey2);
150
+ deepStrictEqual(
151
+ rsaMb2,
152
+ // cSpell: disable
153
+ "z4MXj1wBzi9jUstyPMS4jQqB6KdJaiatPkAtVtGc6bQEQEEsKTic4G7Rou3iBf9vPmT5dbkm" +
154
+ "9qsZsuVNjq8HCuW1w24nhBFGkRE4cd2Uf2tfrB3N7h4mnyPp1BF3ZttHTYv3DLUPi1zMdk" +
155
+ "ULiow3M1GfXkoC6DoxDUm1jmN6GBj22SjVsr6dxezRVQc7aj9TxE7JLbMH1wh5X3kA58H3" +
156
+ "DFW8rnYMakFGbca5CB2Jf6CnGQZmL7o5uJAdTwXfy2iiiyPxXEGerMhHwhjTA1mKYobyk2" +
157
+ "CpeEcmvynADfNZ5MBvcCS7m3XkFCMNUYBS9NQ3fze6vMSUPsNa6GVYmKx2x6JrdEjCk3qR" +
158
+ "MMmyjnjCMfR4pXbRMZa3i",
159
+ // cSpell: enable
160
+ );
161
+
162
+ const ed25519Key2 = await importJwk({
163
+ alg: "Ed25519",
164
+ crv: "Ed25519",
165
+ ext: true,
166
+ key_ops: ["verify"],
167
+ kty: "OKP",
168
+ // cSpell: disable
169
+ x: "Lm_M42cB3HkUiODQsXRcweM6TByfzEHGO9ND274JcOY",
170
+ // cSpell: enable
171
+ }, "public");
172
+ const ed25519Mb2 = await exportMultibaseKey(ed25519Key2);
173
+ deepStrictEqual(
174
+ ed25519Mb2,
175
+ // cSpell: disable
176
+ "z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK",
177
+ // cSpell: enable
178
+ );
179
+ });
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
+ }
@@ -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
+ });