@fedify/vocab-runtime 2.4.0-dev.1567 → 2.4.0-dev.1570
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/deno.json +1 -2
- package/dist/mod.cjs +190 -178
- package/dist/mod.d.cts +190 -59
- package/dist/mod.d.ts +190 -59
- package/dist/mod.js +184 -165
- package/dist/tests/decimal.test.cjs +3 -3
- package/dist/tests/decimal.test.mjs +3 -3
- package/dist/tests/{docloader-D61oV0K1.mjs → docloader-C76ldE5C.mjs} +10 -97
- package/dist/tests/{docloader-BcaQHb0Q.cjs → docloader-mvgIWKI7.cjs} +9 -102
- package/dist/tests/docloader.test.cjs +6 -58
- package/dist/tests/docloader.test.mjs +6 -58
- package/dist/tests/{key-CDGDH_vC.mjs → key-CrrK9mYh.mjs} +1 -69
- package/dist/tests/{key-_wXwomh_.cjs → key-pMmqUKuo.cjs} +0 -86
- package/dist/tests/key.test.cjs +1 -48
- package/dist/tests/key.test.mjs +1 -48
- package/dist/tests/{request-Cn0vP7Hj.mjs → request-BEXkv1ul.mjs} +1 -1
- package/dist/tests/{request-DnNWah97.cjs → request-SworbvLc.cjs} +1 -1
- package/dist/tests/request.test.cjs +1 -1
- package/dist/tests/request.test.mjs +1 -1
- package/dist/tests/{url-2XwVbUS_.cjs → url-C20FhC7p.cjs} +0 -108
- package/dist/tests/{url-YWJbnRlf.mjs → url-m9Qzxy-Y.mjs} +1 -85
- package/dist/tests/url.test.cjs +1 -104
- package/dist/tests/url.test.mjs +2 -105
- package/package.json +1 -11
- package/src/contexts.ts +0 -2
- package/src/docloader.test.ts +6 -102
- package/src/docloader.ts +8 -76
- package/src/key.test.ts +0 -86
- package/src/key.ts +0 -125
- package/src/mod.ts +0 -8
- package/src/url.test.ts +1 -252
- package/src/url.ts +0 -141
- package/tsdown.config.ts +1 -6
- package/dist/docloader-DnUMWHaJ.d.cts +0 -202
- package/dist/docloader-xRGn1azD.d.ts +0 -202
- package/dist/internal/jsonld-cache.cjs +0 -279
- package/dist/internal/jsonld-cache.d.cts +0 -48
- package/dist/internal/jsonld-cache.d.ts +0 -48
- package/dist/internal/jsonld-cache.js +0 -275
- package/dist/tests/jsonld-cache.test.cjs +0 -652
- package/dist/tests/jsonld-cache.test.d.cts +0 -1
- package/dist/tests/jsonld-cache.test.d.mts +0 -1
- package/dist/tests/jsonld-cache.test.mjs +0 -651
- package/dist/url-BAdyyqAa.cjs +0 -315
- package/dist/url-BuxPHxK2.js +0 -261
- package/src/contexts/fep-7aa9.json +0 -24
- package/src/internal/jsonld-cache.ts +0 -538
- package/src/jsonld-cache.test.ts +0 -554
package/src/docloader.test.ts
CHANGED
|
@@ -2,7 +2,7 @@ import fetchMock from "fetch-mock";
|
|
|
2
2
|
import { deepStrictEqual, ok, rejects } from "node:assert";
|
|
3
3
|
import { test } from "node:test";
|
|
4
4
|
import preloadedContexts from "./contexts.ts";
|
|
5
|
-
import { getDocumentLoader
|
|
5
|
+
import { getDocumentLoader } from "./docloader.ts";
|
|
6
6
|
import { FetchError } from "./request.ts";
|
|
7
7
|
import { UrlError } from "./url.ts";
|
|
8
8
|
|
|
@@ -90,7 +90,7 @@ test("getDocumentLoader()", async (t) => {
|
|
|
90
90
|
type: "Object",
|
|
91
91
|
},
|
|
92
92
|
headers: {
|
|
93
|
-
"Content-Type": "
|
|
93
|
+
"Content-Type": "text/html; charset=utf-8",
|
|
94
94
|
Link: '<https://example.com/object>; rel="alternate"; ' +
|
|
95
95
|
'type="application/ld+json; profile="https://www.w3.org/ns/activitystreams""',
|
|
96
96
|
},
|
|
@@ -247,44 +247,6 @@ test("getDocumentLoader()", async (t) => {
|
|
|
247
247
|
});
|
|
248
248
|
});
|
|
249
249
|
|
|
250
|
-
fetchMock.get("https://example.com/html-no-alternate", {
|
|
251
|
-
body: `<!DOCTYPE html>
|
|
252
|
-
<html>
|
|
253
|
-
<head>
|
|
254
|
-
<title>Not an ActivityPub document</title>
|
|
255
|
-
</head>
|
|
256
|
-
<body>Not found</body>
|
|
257
|
-
</html>`,
|
|
258
|
-
headers: { "Content-Type": "text/html; charset=utf-8" },
|
|
259
|
-
});
|
|
260
|
-
|
|
261
|
-
await t.test("HTML without ActivityPub alternate link", async () => {
|
|
262
|
-
await rejects(
|
|
263
|
-
() => fetchDocumentLoader("https://example.com/html-no-alternate"),
|
|
264
|
-
(error) => {
|
|
265
|
-
ok(error instanceof FetchError);
|
|
266
|
-
ok(
|
|
267
|
-
error.message.includes(
|
|
268
|
-
"HTML document has no ActivityPub alternate link",
|
|
269
|
-
),
|
|
270
|
-
);
|
|
271
|
-
ok(
|
|
272
|
-
error.message.includes("Content-Type: text/html; charset=utf-8"),
|
|
273
|
-
);
|
|
274
|
-
deepStrictEqual(
|
|
275
|
-
error.url,
|
|
276
|
-
new URL("https://example.com/html-no-alternate"),
|
|
277
|
-
);
|
|
278
|
-
ok(error.response != null);
|
|
279
|
-
deepStrictEqual(
|
|
280
|
-
error.response.headers.get("Content-Type"),
|
|
281
|
-
"text/html; charset=utf-8",
|
|
282
|
-
);
|
|
283
|
-
return true;
|
|
284
|
-
},
|
|
285
|
-
);
|
|
286
|
-
});
|
|
287
|
-
|
|
288
250
|
fetchMock.get("https://example.com/wrong-content-type", {
|
|
289
251
|
body: {
|
|
290
252
|
"@context": "https://www.w3.org/ns/activitystreams",
|
|
@@ -295,7 +257,7 @@ test("getDocumentLoader()", async (t) => {
|
|
|
295
257
|
headers: { "Content-Type": "text/html; charset=utf-8" },
|
|
296
258
|
});
|
|
297
259
|
|
|
298
|
-
await t.test("
|
|
260
|
+
await t.test("Wrong Content-Type", async () => {
|
|
299
261
|
deepStrictEqual(
|
|
300
262
|
await fetchDocumentLoader("https://example.com/wrong-content-type"),
|
|
301
263
|
{
|
|
@@ -311,64 +273,6 @@ test("getDocumentLoader()", async (t) => {
|
|
|
311
273
|
);
|
|
312
274
|
});
|
|
313
275
|
|
|
314
|
-
fetchMock.get("https://example.com/large-html", {
|
|
315
|
-
body: "<!DOCTYPE html>",
|
|
316
|
-
headers: {
|
|
317
|
-
"Content-Length": String(1024 * 1024 + 1),
|
|
318
|
-
"Content-Type": "text/html; charset=utf-8",
|
|
319
|
-
},
|
|
320
|
-
});
|
|
321
|
-
|
|
322
|
-
await t.test("HTML Content-Length over limit", async () => {
|
|
323
|
-
await rejects(
|
|
324
|
-
() => fetchDocumentLoader("https://example.com/large-html"),
|
|
325
|
-
(error) => {
|
|
326
|
-
ok(error instanceof FetchError);
|
|
327
|
-
ok(
|
|
328
|
-
error.message.includes(
|
|
329
|
-
"HTML document is too large to scan for an ActivityPub alternate link",
|
|
330
|
-
),
|
|
331
|
-
);
|
|
332
|
-
ok(error.response != null);
|
|
333
|
-
deepStrictEqual(error.response.status, 200);
|
|
334
|
-
deepStrictEqual(
|
|
335
|
-
error.response.headers.get("Content-Type"),
|
|
336
|
-
"text/html; charset=utf-8",
|
|
337
|
-
);
|
|
338
|
-
return true;
|
|
339
|
-
},
|
|
340
|
-
);
|
|
341
|
-
});
|
|
342
|
-
|
|
343
|
-
await t.test("HTML Content-Length over limit cancels body", async () => {
|
|
344
|
-
let canceled = false;
|
|
345
|
-
const response = new Response("<!DOCTYPE html>", {
|
|
346
|
-
headers: {
|
|
347
|
-
"Content-Length": String(1024 * 1024 + 1),
|
|
348
|
-
"Content-Type": "text/html; charset=utf-8",
|
|
349
|
-
},
|
|
350
|
-
});
|
|
351
|
-
Object.defineProperty(response, "body", {
|
|
352
|
-
value: {
|
|
353
|
-
cancel: () => {
|
|
354
|
-
canceled = true;
|
|
355
|
-
},
|
|
356
|
-
},
|
|
357
|
-
});
|
|
358
|
-
await rejects(
|
|
359
|
-
() =>
|
|
360
|
-
getRemoteDocument(
|
|
361
|
-
"https://example.com/large-html-cancel",
|
|
362
|
-
response,
|
|
363
|
-
() => {
|
|
364
|
-
throw new Error("unexpected alternate fetch");
|
|
365
|
-
},
|
|
366
|
-
),
|
|
367
|
-
FetchError,
|
|
368
|
-
);
|
|
369
|
-
deepStrictEqual(canceled, true);
|
|
370
|
-
});
|
|
371
|
-
|
|
372
276
|
fetchMock.get("https://example.com/404", { status: 404 });
|
|
373
277
|
|
|
374
278
|
await t.test("not ok", async () => {
|
|
@@ -555,11 +459,11 @@ test("getDocumentLoader()", async (t) => {
|
|
|
555
459
|
|
|
556
460
|
await t.test("ReDoS resistance (CVE-2025-68475)", async () => {
|
|
557
461
|
const start = performance.now();
|
|
558
|
-
// The malicious HTML will fail
|
|
559
|
-
//
|
|
462
|
+
// The malicious HTML will fail JSON parsing, but the important thing is
|
|
463
|
+
// that it should complete quickly (not hang due to ReDoS)
|
|
560
464
|
await rejects(
|
|
561
465
|
() => fetchDocumentLoader("https://example.com/redos"),
|
|
562
|
-
|
|
466
|
+
SyntaxError,
|
|
563
467
|
);
|
|
564
468
|
const elapsed = performance.now() - start;
|
|
565
469
|
|
package/src/docloader.ts
CHANGED
|
@@ -13,7 +13,6 @@ import { UrlError, validatePublicUrl } from "./url.ts";
|
|
|
13
13
|
|
|
14
14
|
const logger = getLogger(["fedify", "runtime", "docloader"]);
|
|
15
15
|
const DEFAULT_MAX_REDIRECTION = 20;
|
|
16
|
-
const MAX_HTML_SIZE = 1024 * 1024; // 1MB
|
|
17
16
|
|
|
18
17
|
/**
|
|
19
18
|
* A remote JSON-LD document and its context fetched by
|
|
@@ -113,59 +112,6 @@ export type AuthenticatedDocumentLoaderFactory = (
|
|
|
113
112
|
options?: DocumentLoaderFactoryOptions,
|
|
114
113
|
) => DocumentLoader;
|
|
115
114
|
|
|
116
|
-
function createResponseMetadata(response: Response): Response {
|
|
117
|
-
return new Response(null, {
|
|
118
|
-
headers: response.headers,
|
|
119
|
-
status: response.status,
|
|
120
|
-
statusText: response.statusText,
|
|
121
|
-
});
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
async function cancelResponseBody(response: Response): Promise<void> {
|
|
125
|
-
if (response.body != null) {
|
|
126
|
-
await response.body.cancel();
|
|
127
|
-
}
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
async function readBoundedText(
|
|
131
|
-
response: Response,
|
|
132
|
-
maxBytes: number,
|
|
133
|
-
): Promise<{ text: string; size: number; tooLarge: boolean }> {
|
|
134
|
-
const contentLength = response.headers.get("Content-Length");
|
|
135
|
-
if (contentLength != null) {
|
|
136
|
-
const size = Number(contentLength);
|
|
137
|
-
if (size > maxBytes) {
|
|
138
|
-
await cancelResponseBody(response);
|
|
139
|
-
return { text: "", size, tooLarge: true };
|
|
140
|
-
}
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
if (response.body == null) return { text: "", size: 0, tooLarge: false };
|
|
144
|
-
|
|
145
|
-
const reader = response.body.getReader();
|
|
146
|
-
const decoder = new TextDecoder();
|
|
147
|
-
let text = "";
|
|
148
|
-
let size = 0;
|
|
149
|
-
try {
|
|
150
|
-
while (true) {
|
|
151
|
-
const result = await reader.read();
|
|
152
|
-
if (result.done) break;
|
|
153
|
-
const chunkSize = result.value.byteLength;
|
|
154
|
-
if (size + chunkSize > maxBytes) {
|
|
155
|
-
size += chunkSize;
|
|
156
|
-
await reader.cancel();
|
|
157
|
-
return { text: "", size, tooLarge: true };
|
|
158
|
-
}
|
|
159
|
-
size += chunkSize;
|
|
160
|
-
text += decoder.decode(result.value, { stream: true });
|
|
161
|
-
}
|
|
162
|
-
text += decoder.decode();
|
|
163
|
-
return { text, size, tooLarge: false };
|
|
164
|
-
} finally {
|
|
165
|
-
reader.releaseLock();
|
|
166
|
-
}
|
|
167
|
-
}
|
|
168
|
-
|
|
169
115
|
/**
|
|
170
116
|
* Gets a {@link RemoteDocument} from the given response.
|
|
171
117
|
* @param url The URL of the document to load.
|
|
@@ -254,19 +200,15 @@ export async function getRemoteDocument(
|
|
|
254
200
|
contentType === "application/xhtml+xml" ||
|
|
255
201
|
contentType?.startsWith("application/xhtml+xml;"))
|
|
256
202
|
) {
|
|
257
|
-
|
|
258
|
-
const
|
|
259
|
-
|
|
203
|
+
// Security: Limit HTML response size to mitigate ReDoS attacks
|
|
204
|
+
const MAX_HTML_SIZE = 1024 * 1024; // 1MB
|
|
205
|
+
const html = await response.text();
|
|
206
|
+
if (html.length > MAX_HTML_SIZE) {
|
|
260
207
|
logger.warn(
|
|
261
208
|
"HTML response too large, skipping alternate link discovery: {url}",
|
|
262
|
-
{ url: documentUrl, size: html.
|
|
263
|
-
);
|
|
264
|
-
throw new FetchError(
|
|
265
|
-
documentUrl,
|
|
266
|
-
`HTML document is too large to scan for an ActivityPub alternate link ` +
|
|
267
|
-
`(Content-Type: ${contentType})`,
|
|
268
|
-
errorResponse,
|
|
209
|
+
{ url: documentUrl, size: html.length },
|
|
269
210
|
);
|
|
211
|
+
document = JSON.parse(html);
|
|
270
212
|
} else {
|
|
271
213
|
// Safe regex patterns without nested quantifiers to prevent ReDoS
|
|
272
214
|
// (CVE-2025-68475)
|
|
@@ -277,7 +219,7 @@ export async function getRemoteDocument(
|
|
|
277
219
|
/([a-z][a-z:_-]*)=(?:"([^"]*)"|'([^']*)'|([^\s>]+))/gi;
|
|
278
220
|
|
|
279
221
|
let tagMatch: RegExpExecArray | null;
|
|
280
|
-
while ((tagMatch = tagPattern.exec(html
|
|
222
|
+
while ((tagMatch = tagPattern.exec(html)) !== null) {
|
|
281
223
|
const tagContent = tagMatch[2];
|
|
282
224
|
let attrMatch: RegExpExecArray | null;
|
|
283
225
|
const attribs: Record<string, string> = {};
|
|
@@ -305,17 +247,7 @@ export async function getRemoteDocument(
|
|
|
305
247
|
return await fetch(new URL(attribs.href, docUrl).href);
|
|
306
248
|
}
|
|
307
249
|
}
|
|
308
|
-
|
|
309
|
-
document = JSON.parse(html.text);
|
|
310
|
-
} catch (error) {
|
|
311
|
-
if (!(error instanceof SyntaxError)) throw error;
|
|
312
|
-
throw new FetchError(
|
|
313
|
-
documentUrl,
|
|
314
|
-
`HTML document has no ActivityPub alternate link ` +
|
|
315
|
-
`(Content-Type: ${contentType})`,
|
|
316
|
-
errorResponse,
|
|
317
|
-
);
|
|
318
|
-
}
|
|
250
|
+
document = JSON.parse(html);
|
|
319
251
|
}
|
|
320
252
|
} else {
|
|
321
253
|
document = await response.json();
|
package/src/key.test.ts
CHANGED
|
@@ -2,17 +2,13 @@ import { deepStrictEqual, rejects } from "node:assert";
|
|
|
2
2
|
import { test } from "node:test";
|
|
3
3
|
import { exportJwk, importJwk } from "./jwk.ts";
|
|
4
4
|
import {
|
|
5
|
-
exportDidKey,
|
|
6
5
|
exportMultibaseKey,
|
|
7
6
|
exportSpki,
|
|
8
|
-
importDidKey,
|
|
9
7
|
importMultibaseKey,
|
|
10
8
|
importPem,
|
|
11
9
|
importPkcs1,
|
|
12
10
|
importSpki,
|
|
13
|
-
parseDidKeyVerificationMethod,
|
|
14
11
|
} from "./key.ts";
|
|
15
|
-
import { addMulticodecPrefix } from "./internal/multicodec.ts";
|
|
16
12
|
import { encodeMultibase } from "./multibase/mod.ts";
|
|
17
13
|
|
|
18
14
|
// cSpell: disable
|
|
@@ -202,85 +198,3 @@ test("exportMultibaseKey()", async () => {
|
|
|
202
198
|
// cSpell: enable
|
|
203
199
|
);
|
|
204
200
|
});
|
|
205
|
-
|
|
206
|
-
test("exportDidKey() and importDidKey()", async () => {
|
|
207
|
-
const ed25519Key = await importJwk(ed25519Jwk, "public");
|
|
208
|
-
const did = await exportDidKey(ed25519Key);
|
|
209
|
-
deepStrictEqual(did, `did:key:${ed25519Multibase}`);
|
|
210
|
-
deepStrictEqual(await exportJwk(await importDidKey(did)), ed25519Jwk);
|
|
211
|
-
deepStrictEqual(
|
|
212
|
-
await exportJwk(await importDidKey(new URL(did))),
|
|
213
|
-
ed25519Jwk,
|
|
214
|
-
);
|
|
215
|
-
|
|
216
|
-
const rsaKey = await importJwk(rsaJwk, "public");
|
|
217
|
-
await rejects(
|
|
218
|
-
() => exportDidKey(rsaKey),
|
|
219
|
-
TypeError,
|
|
220
|
-
);
|
|
221
|
-
await rejects(
|
|
222
|
-
() => importDidKey(`did:key:${rsaMultibase}`),
|
|
223
|
-
new TypeError("Unsupported did:key type: 0x1205"),
|
|
224
|
-
);
|
|
225
|
-
});
|
|
226
|
-
|
|
227
|
-
test("parseDidKeyVerificationMethod()", async () => {
|
|
228
|
-
const verificationMethod = `did:key:${ed25519Multibase}#${ed25519Multibase}`;
|
|
229
|
-
const parsed = await parseDidKeyVerificationMethod(verificationMethod);
|
|
230
|
-
deepStrictEqual(parsed.id, new URL(verificationMethod));
|
|
231
|
-
deepStrictEqual(parsed.controller, new URL(`did:key:${ed25519Multibase}`));
|
|
232
|
-
deepStrictEqual(parsed.publicKeyMultibase, ed25519Multibase);
|
|
233
|
-
deepStrictEqual(await exportJwk(parsed.publicKey), ed25519Jwk);
|
|
234
|
-
});
|
|
235
|
-
|
|
236
|
-
test("did:key helpers reject malformed values", async () => {
|
|
237
|
-
const ed25519Key = await importJwk(ed25519Jwk, "public");
|
|
238
|
-
const raw = new Uint8Array(await crypto.subtle.exportKey("raw", ed25519Key));
|
|
239
|
-
const shortEd25519 = new TextDecoder().decode(
|
|
240
|
-
encodeMultibase(
|
|
241
|
-
"base58btc",
|
|
242
|
-
addMulticodecPrefix(0xed, raw.slice(0, 31)),
|
|
243
|
-
),
|
|
244
|
-
);
|
|
245
|
-
const base64UrlEd25519 = new TextDecoder().decode(
|
|
246
|
-
encodeMultibase(
|
|
247
|
-
"base64url",
|
|
248
|
-
addMulticodecPrefix(0xed, raw),
|
|
249
|
-
),
|
|
250
|
-
);
|
|
251
|
-
const malformedMulticodec = new TextDecoder().decode(
|
|
252
|
-
encodeMultibase("base58btc", Uint8Array.from([0x80])),
|
|
253
|
-
);
|
|
254
|
-
|
|
255
|
-
for (
|
|
256
|
-
const value of [
|
|
257
|
-
"did:web:example.com",
|
|
258
|
-
"did:key:",
|
|
259
|
-
`did:key:${ed25519Multibase}/path`,
|
|
260
|
-
`did:key:${ed25519Multibase}?query`,
|
|
261
|
-
`did:key:${ed25519Multibase}#${ed25519Multibase}`,
|
|
262
|
-
"did:key:z0",
|
|
263
|
-
`did:key:${base64UrlEd25519}`,
|
|
264
|
-
`did:key:${shortEd25519}`,
|
|
265
|
-
`did:key:${malformedMulticodec}`,
|
|
266
|
-
]
|
|
267
|
-
) {
|
|
268
|
-
await rejects(() => importDidKey(value), TypeError);
|
|
269
|
-
}
|
|
270
|
-
|
|
271
|
-
for (
|
|
272
|
-
const value of [
|
|
273
|
-
`did:key:${ed25519Multibase}`,
|
|
274
|
-
`did:key:${ed25519Multibase}#`,
|
|
275
|
-
`did:key:${ed25519Multibase}#z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK`,
|
|
276
|
-
`did:key:${ed25519Multibase}#${base64UrlEd25519}`,
|
|
277
|
-
"did:key:z0#z0",
|
|
278
|
-
`did:key:${rsaMultibase}#${rsaMultibase}`,
|
|
279
|
-
`did:key:${shortEd25519}#${shortEd25519}`,
|
|
280
|
-
`did:key:${malformedMulticodec}#${malformedMulticodec}`,
|
|
281
|
-
"https://example.com/key",
|
|
282
|
-
]
|
|
283
|
-
) {
|
|
284
|
-
await rejects(() => parseDidKeyVerificationMethod(value), TypeError);
|
|
285
|
-
}
|
|
286
|
-
});
|
package/src/key.ts
CHANGED
|
@@ -22,39 +22,6 @@ const algorithms: Record<
|
|
|
22
22
|
"1.3.101.112": "Ed25519",
|
|
23
23
|
};
|
|
24
24
|
|
|
25
|
-
const DID_KEY_PREFIX = "did:key:";
|
|
26
|
-
const ED25519_PUBLIC_KEY_MULTICODEC = 0xed;
|
|
27
|
-
const ED25519_PUBLIC_KEY_LENGTH = 32;
|
|
28
|
-
const DID_KEY_PATTERN = /^did:key:([^/?#]+)$/;
|
|
29
|
-
const DID_KEY_VERIFICATION_METHOD_PATTERN = /^did:key:([^/?#]+)#([^/?#]+)$/;
|
|
30
|
-
|
|
31
|
-
/**
|
|
32
|
-
* Parsed `did:key` verification method.
|
|
33
|
-
*
|
|
34
|
-
* @since 2.4.0
|
|
35
|
-
*/
|
|
36
|
-
export interface DidKeyVerificationMethod {
|
|
37
|
-
/**
|
|
38
|
-
* The DID URL identifying the verification method.
|
|
39
|
-
*/
|
|
40
|
-
readonly id: URL;
|
|
41
|
-
|
|
42
|
-
/**
|
|
43
|
-
* The controller DID.
|
|
44
|
-
*/
|
|
45
|
-
readonly controller: URL;
|
|
46
|
-
|
|
47
|
-
/**
|
|
48
|
-
* The Ed25519 public key encoded as a Multibase Multikey value.
|
|
49
|
-
*/
|
|
50
|
-
readonly publicKeyMultibase: string;
|
|
51
|
-
|
|
52
|
-
/**
|
|
53
|
-
* The Ed25519 public key.
|
|
54
|
-
*/
|
|
55
|
-
readonly publicKey: CryptoKey;
|
|
56
|
-
}
|
|
57
|
-
|
|
58
25
|
/**
|
|
59
26
|
* Imports a PEM-SPKI formatted public key.
|
|
60
27
|
* @param pem The PEM-SPKI formatted public key.
|
|
@@ -126,98 +93,6 @@ export function importPem(pem: string): Promise<CryptoKey> {
|
|
|
126
93
|
return PKCS1_HEADER.test(pem) ? importPkcs1(pem) : importSpki(pem);
|
|
127
94
|
}
|
|
128
95
|
|
|
129
|
-
function decodeEd25519DidKeyMultibase(multibaseKey: string): Uint8Array {
|
|
130
|
-
if (!multibaseKey.startsWith("z")) {
|
|
131
|
-
throw new TypeError("did:key must use base58-btc Multibase encoding.");
|
|
132
|
-
}
|
|
133
|
-
let decoded: Uint8Array;
|
|
134
|
-
try {
|
|
135
|
-
decoded = decodeMultibase(multibaseKey);
|
|
136
|
-
} catch (error) {
|
|
137
|
-
throw new TypeError("Invalid did:key Multibase encoding.", {
|
|
138
|
-
cause: error,
|
|
139
|
-
});
|
|
140
|
-
}
|
|
141
|
-
const { code } = getMulticodecPrefix(decoded);
|
|
142
|
-
if (code !== ED25519_PUBLIC_KEY_MULTICODEC) {
|
|
143
|
-
throw new TypeError("Unsupported did:key type: 0x" + code.toString(16));
|
|
144
|
-
}
|
|
145
|
-
const content = removeMulticodecPrefix(decoded);
|
|
146
|
-
if (content.length !== ED25519_PUBLIC_KEY_LENGTH) {
|
|
147
|
-
throw new TypeError("Invalid Ed25519 did:key length.");
|
|
148
|
-
}
|
|
149
|
-
return content;
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
/**
|
|
153
|
-
* Imports an Ed25519 `did:key` DID.
|
|
154
|
-
*
|
|
155
|
-
* @param did The `did:key` DID.
|
|
156
|
-
* @returns The imported Ed25519 public key.
|
|
157
|
-
* @throws {TypeError} If the DID is malformed or uses an unsupported key type.
|
|
158
|
-
* @since 2.4.0
|
|
159
|
-
*/
|
|
160
|
-
export async function importDidKey(did: string | URL): Promise<CryptoKey> {
|
|
161
|
-
const didString = did instanceof URL ? did.href : did;
|
|
162
|
-
const match = didString.match(DID_KEY_PATTERN);
|
|
163
|
-
if (match == null) throw new TypeError("Invalid did:key DID.");
|
|
164
|
-
const content = decodeEd25519DidKeyMultibase(match[1]);
|
|
165
|
-
return await crypto.subtle.importKey(
|
|
166
|
-
"raw",
|
|
167
|
-
content.slice(),
|
|
168
|
-
"Ed25519",
|
|
169
|
-
true,
|
|
170
|
-
["verify"],
|
|
171
|
-
);
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
/**
|
|
175
|
-
* Exports an Ed25519 public key as a `did:key` DID.
|
|
176
|
-
*
|
|
177
|
-
* @param key The Ed25519 public key.
|
|
178
|
-
* @returns The `did:key` DID.
|
|
179
|
-
* @throws {TypeError} If the key is invalid or unsupported.
|
|
180
|
-
* @since 2.4.0
|
|
181
|
-
*/
|
|
182
|
-
export async function exportDidKey(key: CryptoKey): Promise<string> {
|
|
183
|
-
if (key.algorithm.name !== "Ed25519") {
|
|
184
|
-
throw new TypeError(
|
|
185
|
-
"Unsupported key type: " + JSON.stringify(key.algorithm),
|
|
186
|
-
);
|
|
187
|
-
}
|
|
188
|
-
return DID_KEY_PREFIX + await exportMultibaseKey(key);
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
/**
|
|
192
|
-
* Parses an Ed25519 `did:key` verification method DID URL.
|
|
193
|
-
*
|
|
194
|
-
* @param didUrl The `did:key` DID URL.
|
|
195
|
-
* @returns The parsed verification method.
|
|
196
|
-
* @throws {TypeError} If the DID URL is malformed, unsupported, or its
|
|
197
|
-
* fragment does not identify the same key as the DID.
|
|
198
|
-
* @since 2.4.0
|
|
199
|
-
*/
|
|
200
|
-
export async function parseDidKeyVerificationMethod(
|
|
201
|
-
didUrl: string | URL,
|
|
202
|
-
): Promise<DidKeyVerificationMethod> {
|
|
203
|
-
const didUrlString = didUrl instanceof URL ? didUrl.href : didUrl;
|
|
204
|
-
const match = didUrlString.match(DID_KEY_VERIFICATION_METHOD_PATTERN);
|
|
205
|
-
if (match == null) {
|
|
206
|
-
throw new TypeError("Invalid did:key verification method.");
|
|
207
|
-
}
|
|
208
|
-
const [, publicKeyMultibase, fragment] = match;
|
|
209
|
-
if (publicKeyMultibase !== fragment) {
|
|
210
|
-
throw new TypeError("Invalid did:key verification method fragment.");
|
|
211
|
-
}
|
|
212
|
-
const publicKey = await importDidKey(DID_KEY_PREFIX + publicKeyMultibase);
|
|
213
|
-
return {
|
|
214
|
-
id: new URL(didUrlString),
|
|
215
|
-
controller: new URL(DID_KEY_PREFIX + publicKeyMultibase),
|
|
216
|
-
publicKeyMultibase,
|
|
217
|
-
publicKey,
|
|
218
|
-
};
|
|
219
|
-
}
|
|
220
|
-
|
|
221
96
|
/**
|
|
222
97
|
* Imports a [Multibase]-encoded public key.
|
|
223
98
|
*
|
package/src/mod.ts
CHANGED
|
@@ -17,16 +17,12 @@ export {
|
|
|
17
17
|
type RemoteDocument,
|
|
18
18
|
} from "./docloader.ts";
|
|
19
19
|
export {
|
|
20
|
-
type DidKeyVerificationMethod,
|
|
21
|
-
exportDidKey,
|
|
22
20
|
exportMultibaseKey,
|
|
23
21
|
exportSpki,
|
|
24
|
-
importDidKey,
|
|
25
22
|
importMultibaseKey,
|
|
26
23
|
importPem,
|
|
27
24
|
importPkcs1,
|
|
28
25
|
importSpki,
|
|
29
|
-
parseDidKeyVerificationMethod,
|
|
30
26
|
} from "./key.ts";
|
|
31
27
|
export {
|
|
32
28
|
canParseDecimal,
|
|
@@ -55,12 +51,8 @@ export {
|
|
|
55
51
|
} from "./preprocessor.ts";
|
|
56
52
|
export {
|
|
57
53
|
expandIPv6Address,
|
|
58
|
-
formatIri,
|
|
59
|
-
haveSameIriOrigin,
|
|
60
54
|
isValidPublicIPv4Address,
|
|
61
55
|
isValidPublicIPv6Address,
|
|
62
|
-
parseIri,
|
|
63
|
-
parseJsonLdId,
|
|
64
56
|
UrlError,
|
|
65
57
|
validatePublicUrl,
|
|
66
58
|
} from "./url.ts";
|