@fedify/vocab-runtime 2.0.0-dev.100
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 +30 -0
- package/dist/mod.cjs +5230 -0
- package/dist/mod.d.cts +331 -0
- package/dist/mod.d.ts +331 -0
- package/dist/mod.js +5186 -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/docloader.ts
ADDED
|
@@ -0,0 +1,367 @@
|
|
|
1
|
+
import { getLogger } from "@logtape/logtape";
|
|
2
|
+
import { SpanKind, SpanStatusCode, trace } from "@opentelemetry/api";
|
|
3
|
+
import metadata from "../deno.json" with { type: "json" };
|
|
4
|
+
import preloadedContexts from "./contexts.ts";
|
|
5
|
+
import { HttpHeaderLink } from "./link.ts";
|
|
6
|
+
import {
|
|
7
|
+
createActivityPubRequest,
|
|
8
|
+
FetchError,
|
|
9
|
+
type GetUserAgentOptions,
|
|
10
|
+
logRequest,
|
|
11
|
+
} from "./request.ts";
|
|
12
|
+
import { UrlError, validatePublicUrl } from "./url.ts";
|
|
13
|
+
|
|
14
|
+
const logger = getLogger(["fedify", "runtime", "docloader"]);
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* A remote JSON-LD document and its context fetched by
|
|
18
|
+
* a {@link DocumentLoader}.
|
|
19
|
+
*/
|
|
20
|
+
export interface RemoteDocument {
|
|
21
|
+
/**
|
|
22
|
+
* The URL of the context document.
|
|
23
|
+
*/
|
|
24
|
+
contextUrl: string | null;
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* The fetched JSON-LD document.
|
|
28
|
+
*/
|
|
29
|
+
document: unknown;
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* The URL of the fetched document.
|
|
33
|
+
*/
|
|
34
|
+
documentUrl: string;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Options for {@link DocumentLoader}.
|
|
39
|
+
* @since 1.8.0
|
|
40
|
+
*/
|
|
41
|
+
export interface DocumentLoaderOptions {
|
|
42
|
+
/**
|
|
43
|
+
* An `AbortSignal` for cancellation.
|
|
44
|
+
* @since 1.8.0
|
|
45
|
+
*/
|
|
46
|
+
signal?: AbortSignal;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* A JSON-LD document loader that fetches documents from the Web.
|
|
51
|
+
* @param url The URL of the document to load.
|
|
52
|
+
* @param options The options for the document loader.
|
|
53
|
+
* @returns The loaded remote document.
|
|
54
|
+
*/
|
|
55
|
+
export type DocumentLoader = (
|
|
56
|
+
url: string,
|
|
57
|
+
options?: DocumentLoaderOptions,
|
|
58
|
+
) => Promise<RemoteDocument>;
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* A factory function that creates a {@link DocumentLoader} with options.
|
|
62
|
+
* @param options The options for the document loader.
|
|
63
|
+
* @returns The document loader.
|
|
64
|
+
* @since 1.4.0
|
|
65
|
+
*/
|
|
66
|
+
export type DocumentLoaderFactory = (
|
|
67
|
+
options?: DocumentLoaderFactoryOptions,
|
|
68
|
+
) => DocumentLoader;
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Options for {@link DocumentLoaderFactory}.
|
|
72
|
+
* @see {@link DocumentLoaderFactory}
|
|
73
|
+
* @see {@link AuthenticatedDocumentLoaderFactory}
|
|
74
|
+
* @since 1.4.0
|
|
75
|
+
*/
|
|
76
|
+
export interface DocumentLoaderFactoryOptions {
|
|
77
|
+
/**
|
|
78
|
+
* Whether to allow fetching private network addresses.
|
|
79
|
+
* Turned off by default.
|
|
80
|
+
* @default `false``
|
|
81
|
+
*/
|
|
82
|
+
allowPrivateAddress?: boolean;
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Options for making `User-Agent` string.
|
|
86
|
+
* If a string is given, it is used as the `User-Agent` header value.
|
|
87
|
+
* If an object is given, it is passed to {@link getUserAgent} function.
|
|
88
|
+
*/
|
|
89
|
+
userAgent?: GetUserAgentOptions | string;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* A factory function that creates an authenticated {@link DocumentLoader} for
|
|
94
|
+
* a given identity. This is used for fetching documents that require
|
|
95
|
+
* authentication.
|
|
96
|
+
* @param identity The identity to create the document loader for.
|
|
97
|
+
* The actor's key pair.
|
|
98
|
+
* @param options The options for the document loader.
|
|
99
|
+
* @returns The authenticated document loader.
|
|
100
|
+
* @since 0.4.0
|
|
101
|
+
*/
|
|
102
|
+
export type AuthenticatedDocumentLoaderFactory = (
|
|
103
|
+
identity: { keyId: URL; privateKey: CryptoKey },
|
|
104
|
+
options?: DocumentLoaderFactoryOptions,
|
|
105
|
+
) => DocumentLoader;
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Gets a {@link RemoteDocument} from the given response.
|
|
109
|
+
* @param url The URL of the document to load.
|
|
110
|
+
* @param response The response to get the document from.
|
|
111
|
+
* @param fetch The function to fetch the document.
|
|
112
|
+
* @returns The loaded remote document.
|
|
113
|
+
* @throws {FetchError} If the response is not OK.
|
|
114
|
+
* @internal
|
|
115
|
+
*/
|
|
116
|
+
export async function getRemoteDocument(
|
|
117
|
+
url: string,
|
|
118
|
+
response: Response,
|
|
119
|
+
fetch: (
|
|
120
|
+
url: string,
|
|
121
|
+
options?: DocumentLoaderOptions,
|
|
122
|
+
) => Promise<RemoteDocument>,
|
|
123
|
+
): Promise<RemoteDocument> {
|
|
124
|
+
const documentUrl = response.url === "" ? url : response.url;
|
|
125
|
+
const docUrl = new URL(documentUrl);
|
|
126
|
+
if (!response.ok) {
|
|
127
|
+
logger.error(
|
|
128
|
+
"Failed to fetch document: {status} {url} {headers}",
|
|
129
|
+
{
|
|
130
|
+
status: response.status,
|
|
131
|
+
url: documentUrl,
|
|
132
|
+
headers: Object.fromEntries(response.headers.entries()),
|
|
133
|
+
},
|
|
134
|
+
);
|
|
135
|
+
throw new FetchError(
|
|
136
|
+
documentUrl,
|
|
137
|
+
`HTTP ${response.status}: ${documentUrl}`,
|
|
138
|
+
);
|
|
139
|
+
}
|
|
140
|
+
const contentType = response.headers.get("Content-Type");
|
|
141
|
+
const jsonLd = contentType == null ||
|
|
142
|
+
contentType === "application/activity+json" ||
|
|
143
|
+
contentType.startsWith("application/activity+json;") ||
|
|
144
|
+
contentType === "application/ld+json" ||
|
|
145
|
+
contentType.startsWith("application/ld+json;");
|
|
146
|
+
const linkHeader = response.headers.get("Link");
|
|
147
|
+
let contextUrl: string | null = null;
|
|
148
|
+
if (linkHeader != null) {
|
|
149
|
+
let link: HttpHeaderLink;
|
|
150
|
+
try {
|
|
151
|
+
link = new HttpHeaderLink(linkHeader);
|
|
152
|
+
} catch (e) {
|
|
153
|
+
if (e instanceof SyntaxError) {
|
|
154
|
+
link = new HttpHeaderLink();
|
|
155
|
+
} else {
|
|
156
|
+
throw e;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
if (jsonLd) {
|
|
160
|
+
const entries = link.getByRel("http://www.w3.org/ns/json-ld#context");
|
|
161
|
+
for (const [uri, params] of entries) {
|
|
162
|
+
if ("type" in params && params.type === "application/ld+json") {
|
|
163
|
+
contextUrl = uri;
|
|
164
|
+
break;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
} else {
|
|
168
|
+
const entries = link.getByRel("alternate");
|
|
169
|
+
for (const [uri, params] of entries) {
|
|
170
|
+
const altUri = new URL(uri, docUrl);
|
|
171
|
+
if (
|
|
172
|
+
"type" in params &&
|
|
173
|
+
(params.type === "application/activity+json" ||
|
|
174
|
+
params.type === "application/ld+json" ||
|
|
175
|
+
params.type.startsWith("application/ld+json;")) &&
|
|
176
|
+
altUri.href !== docUrl.href
|
|
177
|
+
) {
|
|
178
|
+
logger.debug(
|
|
179
|
+
"Found alternate document: {alternateUrl} from {url}",
|
|
180
|
+
{ alternateUrl: altUri.href, url: documentUrl },
|
|
181
|
+
);
|
|
182
|
+
return await fetch(altUri.href);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
let document: unknown;
|
|
188
|
+
if (
|
|
189
|
+
!jsonLd &&
|
|
190
|
+
(contentType === "text/html" || contentType?.startsWith("text/html;") ||
|
|
191
|
+
contentType === "application/xhtml+xml" ||
|
|
192
|
+
contentType?.startsWith("application/xhtml+xml;"))
|
|
193
|
+
) {
|
|
194
|
+
// Security: Limit HTML response size to mitigate ReDoS attacks
|
|
195
|
+
const MAX_HTML_SIZE = 1024 * 1024; // 1MB
|
|
196
|
+
const html = await response.text();
|
|
197
|
+
if (html.length > MAX_HTML_SIZE) {
|
|
198
|
+
logger.warn(
|
|
199
|
+
"HTML response too large, skipping alternate link discovery: {url}",
|
|
200
|
+
{ url: documentUrl, size: html.length },
|
|
201
|
+
);
|
|
202
|
+
document = JSON.parse(html);
|
|
203
|
+
} else {
|
|
204
|
+
// Safe regex patterns without nested quantifiers to prevent ReDoS
|
|
205
|
+
// (CVE-2025-68475)
|
|
206
|
+
// Step 1: Extract <a ...> or <link ...> tags
|
|
207
|
+
const tagPattern = /<(a|link)\s+([^>]*?)\s*\/?>/gi;
|
|
208
|
+
// Step 2: Parse attributes
|
|
209
|
+
const attrPattern =
|
|
210
|
+
/([a-z][a-z:_-]*)=(?:"([^"]*)"|'([^']*)'|([^\s>]+))/gi;
|
|
211
|
+
|
|
212
|
+
let tagMatch: RegExpExecArray | null;
|
|
213
|
+
while ((tagMatch = tagPattern.exec(html)) !== null) {
|
|
214
|
+
const tagContent = tagMatch[2];
|
|
215
|
+
let attrMatch: RegExpExecArray | null;
|
|
216
|
+
const attribs: Record<string, string> = {};
|
|
217
|
+
|
|
218
|
+
// Reset regex state for attribute parsing
|
|
219
|
+
attrPattern.lastIndex = 0;
|
|
220
|
+
while ((attrMatch = attrPattern.exec(tagContent)) !== null) {
|
|
221
|
+
const key = attrMatch[1].toLowerCase();
|
|
222
|
+
const value = attrMatch[2] ?? attrMatch[3] ?? attrMatch[4] ?? "";
|
|
223
|
+
attribs[key] = value;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
if (
|
|
227
|
+
attribs.rel === "alternate" && "type" in attribs && (
|
|
228
|
+
attribs.type === "application/activity+json" ||
|
|
229
|
+
attribs.type === "application/ld+json" ||
|
|
230
|
+
attribs.type.startsWith("application/ld+json;")
|
|
231
|
+
) && "href" in attribs &&
|
|
232
|
+
new URL(attribs.href, docUrl).href !== docUrl.href
|
|
233
|
+
) {
|
|
234
|
+
logger.debug(
|
|
235
|
+
"Found alternate document: {alternateUrl} from {url}",
|
|
236
|
+
{ alternateUrl: attribs.href, url: documentUrl },
|
|
237
|
+
);
|
|
238
|
+
return await fetch(new URL(attribs.href, docUrl).href);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
document = JSON.parse(html);
|
|
242
|
+
}
|
|
243
|
+
} else {
|
|
244
|
+
document = await response.json();
|
|
245
|
+
}
|
|
246
|
+
logger.debug(
|
|
247
|
+
"Fetched document: {status} {url} {headers}",
|
|
248
|
+
{
|
|
249
|
+
status: response.status,
|
|
250
|
+
url: documentUrl,
|
|
251
|
+
headers: Object.fromEntries(response.headers.entries()),
|
|
252
|
+
},
|
|
253
|
+
);
|
|
254
|
+
return { contextUrl, document, documentUrl };
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* Options for {@link getDocumentLoader}.
|
|
259
|
+
* @since 1.3.0
|
|
260
|
+
*/
|
|
261
|
+
export interface GetDocumentLoaderOptions extends DocumentLoaderFactoryOptions {
|
|
262
|
+
/**
|
|
263
|
+
* Whether to preload the frequently used contexts.
|
|
264
|
+
*/
|
|
265
|
+
skipPreloadedContexts?: boolean;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* Creates a JSON-LD document loader that utilizes the browser's `fetch` API.
|
|
270
|
+
*
|
|
271
|
+
* The created loader preloads the below frequently used contexts by default
|
|
272
|
+
* (unless `options.ignorePreloadedContexts` is set to `true`):
|
|
273
|
+
*
|
|
274
|
+
* - <https://www.w3.org/ns/activitystreams>
|
|
275
|
+
* - <https://w3id.org/security/v1>
|
|
276
|
+
* - <https://w3id.org/security/data-integrity/v1>
|
|
277
|
+
* - <https://www.w3.org/ns/did/v1>
|
|
278
|
+
* - <https://w3id.org/security/multikey/v1>
|
|
279
|
+
* - <https://purl.archive.org/socialweb/webfinger>
|
|
280
|
+
* - <http://schema.org/>
|
|
281
|
+
* @param options Options for the document loader.
|
|
282
|
+
* @returns The document loader.
|
|
283
|
+
* @since 1.3.0
|
|
284
|
+
*/
|
|
285
|
+
export function getDocumentLoader(
|
|
286
|
+
{ allowPrivateAddress, skipPreloadedContexts, userAgent }:
|
|
287
|
+
GetDocumentLoaderOptions = {},
|
|
288
|
+
): DocumentLoader {
|
|
289
|
+
const tracerProvider = trace.getTracerProvider();
|
|
290
|
+
const tracer = tracerProvider.getTracer(metadata.name, metadata.version);
|
|
291
|
+
|
|
292
|
+
async function load(
|
|
293
|
+
url: string,
|
|
294
|
+
options?: DocumentLoaderOptions,
|
|
295
|
+
): Promise<RemoteDocument> {
|
|
296
|
+
options?.signal?.throwIfAborted();
|
|
297
|
+
if (!skipPreloadedContexts && url in preloadedContexts) {
|
|
298
|
+
logger.debug("Using preloaded context: {url}.", { url });
|
|
299
|
+
return {
|
|
300
|
+
contextUrl: null,
|
|
301
|
+
document: preloadedContexts[url],
|
|
302
|
+
documentUrl: url,
|
|
303
|
+
};
|
|
304
|
+
}
|
|
305
|
+
if (!allowPrivateAddress) {
|
|
306
|
+
try {
|
|
307
|
+
await validatePublicUrl(url);
|
|
308
|
+
} catch (error) {
|
|
309
|
+
if (error instanceof UrlError) {
|
|
310
|
+
logger.error("Disallowed private URL: {url}", { url, error });
|
|
311
|
+
}
|
|
312
|
+
throw error;
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
return await tracer.startActiveSpan(
|
|
317
|
+
"activitypub.fetch_document",
|
|
318
|
+
{
|
|
319
|
+
kind: SpanKind.CLIENT,
|
|
320
|
+
attributes: {
|
|
321
|
+
"url.full": url,
|
|
322
|
+
},
|
|
323
|
+
},
|
|
324
|
+
async (span) => {
|
|
325
|
+
try {
|
|
326
|
+
const request = createActivityPubRequest(url, { userAgent });
|
|
327
|
+
logRequest(logger, request);
|
|
328
|
+
const response = await fetch(request, {
|
|
329
|
+
// Since Bun has a bug that ignores the `Request.redirect` option,
|
|
330
|
+
// to work around it we specify `redirect: "manual"` here too:
|
|
331
|
+
// https://github.com/oven-sh/bun/issues/10754
|
|
332
|
+
redirect: "manual",
|
|
333
|
+
signal: options?.signal,
|
|
334
|
+
});
|
|
335
|
+
span.setAttribute("http.response.status_code", response.status);
|
|
336
|
+
|
|
337
|
+
// Follow redirects manually to get the final URL:
|
|
338
|
+
if (
|
|
339
|
+
response.status >= 300 && response.status < 400 &&
|
|
340
|
+
response.headers.has("Location")
|
|
341
|
+
) {
|
|
342
|
+
const redirectUrl = response.headers.get("Location")!;
|
|
343
|
+
span.setAttribute("http.redirect.url", redirectUrl);
|
|
344
|
+
return await load(redirectUrl, options);
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
const result = await getRemoteDocument(url, response, load);
|
|
348
|
+
span.setAttribute("docloader.document_url", result.documentUrl);
|
|
349
|
+
if (result.contextUrl != null) {
|
|
350
|
+
span.setAttribute("docloader.context_url", result.contextUrl);
|
|
351
|
+
}
|
|
352
|
+
return result;
|
|
353
|
+
} catch (error) {
|
|
354
|
+
span.recordException(error as Error);
|
|
355
|
+
span.setStatus({
|
|
356
|
+
code: SpanStatusCode.ERROR,
|
|
357
|
+
message: String(error),
|
|
358
|
+
});
|
|
359
|
+
throw error;
|
|
360
|
+
} finally {
|
|
361
|
+
span.end();
|
|
362
|
+
}
|
|
363
|
+
},
|
|
364
|
+
);
|
|
365
|
+
}
|
|
366
|
+
return load;
|
|
367
|
+
}
|
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
|
+
}
|
package/src/key.test.ts
ADDED
|
@@ -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
|
+
});
|