@interop/did-method-webvh 3.0.0
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/CHANGELOG.md +345 -0
- package/LICENSE +201 -0
- package/README.md +294 -0
- package/dist/assertions.d.ts +5 -0
- package/dist/assertions.js +82 -0
- package/dist/cli.d.ts +21 -0
- package/dist/cli.js +533 -0
- package/dist/config.d.ts +7 -0
- package/dist/config.js +35 -0
- package/dist/constants.d.ts +3 -0
- package/dist/constants.js +3 -0
- package/dist/cryptography.d.ts +52 -0
- package/dist/cryptography.js +95 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +6 -0
- package/dist/interfaces.d.ts +231 -0
- package/dist/interfaces.js +17 -0
- package/dist/method.d.ts +73 -0
- package/dist/method.js +95 -0
- package/dist/method_versions/method.v1.0.d.ts +23 -0
- package/dist/method_versions/method.v1.0.js +554 -0
- package/dist/types.d.ts +5 -0
- package/dist/types.js +5 -0
- package/dist/utils/buffer.d.ts +3 -0
- package/dist/utils/buffer.js +62 -0
- package/dist/utils/canonicalize.d.ts +3 -0
- package/dist/utils/canonicalize.js +80 -0
- package/dist/utils/crypto.d.ts +2 -0
- package/dist/utils/crypto.js +17 -0
- package/dist/utils/multiformats.d.ts +100 -0
- package/dist/utils/multiformats.js +283 -0
- package/dist/utils.d.ts +49 -0
- package/dist/utils.js +659 -0
- package/dist/witness.d.ts +39 -0
- package/dist/witness.js +216 -0
- package/package.json +70 -0
package/dist/utils.js
ADDED
|
@@ -0,0 +1,659 @@
|
|
|
1
|
+
import { config } from './config.js';
|
|
2
|
+
import { BASE_CONTEXT } from './constants.js';
|
|
3
|
+
import { resolveDIDFromLog } from './method.js';
|
|
4
|
+
import { bufferToString, createBuffer } from './utils/buffer.js';
|
|
5
|
+
import { canonicalizeStrict } from './utils/canonicalize.js';
|
|
6
|
+
import { createHash } from './utils/crypto.js';
|
|
7
|
+
import { createMultihash, encodeBase58Btc, MultihashAlgorithm, multibaseDecode } from './utils/multiformats.js';
|
|
8
|
+
const DID_KEY_PREFIX = 'did:key:';
|
|
9
|
+
function validateDidKeyMultibase(keyMultibase) {
|
|
10
|
+
if (!keyMultibase) {
|
|
11
|
+
throw new Error('Malformed did:key identifier');
|
|
12
|
+
}
|
|
13
|
+
try {
|
|
14
|
+
multibaseDecode(keyMultibase);
|
|
15
|
+
}
|
|
16
|
+
catch (error) {
|
|
17
|
+
throw new Error(`Malformed did:key identifier: ${error.message}`);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
export function parseDidKeyDid(input) {
|
|
21
|
+
if (typeof input !== 'string') {
|
|
22
|
+
throw new Error('did:key DID must be a string');
|
|
23
|
+
}
|
|
24
|
+
const match = input.match(/^did:key:([^#/?]+)$/);
|
|
25
|
+
if (!match) {
|
|
26
|
+
throw new Error('Malformed did:key DID');
|
|
27
|
+
}
|
|
28
|
+
const keyMultibase = match[1];
|
|
29
|
+
validateDidKeyMultibase(keyMultibase);
|
|
30
|
+
return {
|
|
31
|
+
did: `${DID_KEY_PREFIX}${keyMultibase}`,
|
|
32
|
+
keyMultibase,
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
export function parseDidKeyVerificationMethod(input) {
|
|
36
|
+
if (typeof input !== 'string') {
|
|
37
|
+
throw new Error('did:key verificationMethod must be a string');
|
|
38
|
+
}
|
|
39
|
+
if (input.startsWith('#')) {
|
|
40
|
+
throw new Error('did:key verificationMethod must be an absolute DID URL');
|
|
41
|
+
}
|
|
42
|
+
const match = input.match(/^did:key:([^#/?]+)(?:#([^#/?]+))?$/);
|
|
43
|
+
if (!match) {
|
|
44
|
+
throw new Error('Malformed did:key verificationMethod');
|
|
45
|
+
}
|
|
46
|
+
const parsedDid = parseDidKeyDid(`${DID_KEY_PREFIX}${match[1]}`);
|
|
47
|
+
const fragment = match[2];
|
|
48
|
+
return {
|
|
49
|
+
did: parsedDid.did,
|
|
50
|
+
fragment,
|
|
51
|
+
keyMultibase: parsedDid.keyMultibase,
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
function isIPAddress(host) {
|
|
55
|
+
// Reject IPv4
|
|
56
|
+
if (/^\d+\.\d+\.\d+\.\d+$/.test(host))
|
|
57
|
+
return true;
|
|
58
|
+
// Reject IPv6 (with or without brackets)
|
|
59
|
+
const bare = host.replace(/^\[|\]$/g, '');
|
|
60
|
+
if (/^[0-9a-f:]+$/i.test(bare))
|
|
61
|
+
return true;
|
|
62
|
+
return false;
|
|
63
|
+
}
|
|
64
|
+
function isDoubleEncoded(value) {
|
|
65
|
+
// Detect %25 (which is percent-encoded %)
|
|
66
|
+
return value.includes('%25');
|
|
67
|
+
}
|
|
68
|
+
export function parseCanonicalAddress(input) {
|
|
69
|
+
if (!input || typeof input !== 'string') {
|
|
70
|
+
throw new Error('Address input must be a non-empty string');
|
|
71
|
+
}
|
|
72
|
+
// Parse did:webvh form
|
|
73
|
+
if (input.startsWith('did:webvh:')) {
|
|
74
|
+
const parts = input.substring(10).split(':');
|
|
75
|
+
if (parts.length < 2) {
|
|
76
|
+
throw new Error('Invalid did:webvh identifier: must contain SCID (or {SCID} placeholder) and domain');
|
|
77
|
+
}
|
|
78
|
+
const scid = parts[0];
|
|
79
|
+
const domainPart = parts[1];
|
|
80
|
+
const pathParts = parts.slice(2);
|
|
81
|
+
// Detect double encoding
|
|
82
|
+
if (isDoubleEncoded(domainPart)) {
|
|
83
|
+
throw new Error('Domain is double-encoded (detected %25)');
|
|
84
|
+
}
|
|
85
|
+
// Extract port from domain if %3A-encoded
|
|
86
|
+
let host = domainPart;
|
|
87
|
+
let port;
|
|
88
|
+
if (domainPart.includes('%3A')) {
|
|
89
|
+
const [h, p] = domainPart.split('%3A');
|
|
90
|
+
host = h;
|
|
91
|
+
const portNum = parseInt(p, 10);
|
|
92
|
+
if (Number.isNaN(portNum) || portNum <= 0 || portNum > 65535) {
|
|
93
|
+
throw new Error(`Invalid port number: ${p}`);
|
|
94
|
+
}
|
|
95
|
+
port = portNum;
|
|
96
|
+
}
|
|
97
|
+
if (isIPAddress(host)) {
|
|
98
|
+
throw new Error('IP addresses are not allowed as hosts');
|
|
99
|
+
}
|
|
100
|
+
return {
|
|
101
|
+
canonicalHost: host,
|
|
102
|
+
canonicalPort: port,
|
|
103
|
+
didDomainComponent: domainPart,
|
|
104
|
+
paths: pathParts.length > 0 ? pathParts : undefined,
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
// Parse URL form: HTTPS everywhere, with localhost-only HTTP for local testing.
|
|
108
|
+
if (input.startsWith('https://') || input.startsWith('http://')) {
|
|
109
|
+
try {
|
|
110
|
+
const url = new URL(input);
|
|
111
|
+
if (url.protocol === 'http:' && url.hostname !== 'localhost') {
|
|
112
|
+
throw new Error('HTTP is only allowed for localhost; use HTTPS for non-local hosts');
|
|
113
|
+
}
|
|
114
|
+
const host = url.hostname;
|
|
115
|
+
const port = url.port ? parseInt(url.port, 10) : undefined;
|
|
116
|
+
if (isIPAddress(host)) {
|
|
117
|
+
throw new Error('IP addresses are not allowed as hosts');
|
|
118
|
+
}
|
|
119
|
+
let didDomainComponent = host;
|
|
120
|
+
if (port) {
|
|
121
|
+
didDomainComponent += `%3A${port}`;
|
|
122
|
+
}
|
|
123
|
+
const pathParts = [];
|
|
124
|
+
if (url.pathname && url.pathname !== '/') {
|
|
125
|
+
url.pathname
|
|
126
|
+
.split('/')
|
|
127
|
+
.filter((p) => p.length > 0)
|
|
128
|
+
.forEach((p) => {
|
|
129
|
+
pathParts.push(p);
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
return {
|
|
133
|
+
canonicalHost: host,
|
|
134
|
+
canonicalPort: port,
|
|
135
|
+
didDomainComponent,
|
|
136
|
+
paths: pathParts.length > 0 ? pathParts : undefined,
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
catch (e) {
|
|
140
|
+
if (e.message?.includes('not allowed'))
|
|
141
|
+
throw e;
|
|
142
|
+
throw new Error(`Invalid URL: ${e.message}`);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
// Parse domain string form (host or host:port)
|
|
146
|
+
// Detect double encoding
|
|
147
|
+
if (isDoubleEncoded(input)) {
|
|
148
|
+
throw new Error('Domain is double-encoded (detected %25)');
|
|
149
|
+
}
|
|
150
|
+
let host = input;
|
|
151
|
+
let port;
|
|
152
|
+
// Check if pre-encoded with %3A
|
|
153
|
+
if (input.includes('%3A')) {
|
|
154
|
+
const parts = input.split('%3A');
|
|
155
|
+
if (parts.length !== 2) {
|
|
156
|
+
throw new Error('Invalid pre-encoded port separator');
|
|
157
|
+
}
|
|
158
|
+
host = parts[0];
|
|
159
|
+
const portNum = parseInt(parts[1], 10);
|
|
160
|
+
if (Number.isNaN(portNum) || portNum <= 0 || portNum > 65535) {
|
|
161
|
+
throw new Error(`Invalid port number: ${parts[1]}`);
|
|
162
|
+
}
|
|
163
|
+
port = portNum;
|
|
164
|
+
}
|
|
165
|
+
else if (input.includes(':')) {
|
|
166
|
+
// Raw host:port form
|
|
167
|
+
const parts = input.split(':');
|
|
168
|
+
if (parts.length !== 2) {
|
|
169
|
+
throw new Error('Invalid host:port format');
|
|
170
|
+
}
|
|
171
|
+
host = parts[0];
|
|
172
|
+
const portNum = parseInt(parts[1], 10);
|
|
173
|
+
if (Number.isNaN(portNum) || portNum <= 0 || portNum > 65535) {
|
|
174
|
+
throw new Error(`Invalid port number: ${parts[1]}`);
|
|
175
|
+
}
|
|
176
|
+
port = portNum;
|
|
177
|
+
}
|
|
178
|
+
if (isIPAddress(host)) {
|
|
179
|
+
throw new Error('IP addresses are not allowed as hosts');
|
|
180
|
+
}
|
|
181
|
+
let didDomainComponent = host;
|
|
182
|
+
if (port) {
|
|
183
|
+
didDomainComponent += `%3A${port}`;
|
|
184
|
+
}
|
|
185
|
+
return {
|
|
186
|
+
canonicalHost: host,
|
|
187
|
+
canonicalPort: port,
|
|
188
|
+
didDomainComponent,
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
// Environment detection - treat React Native like a browser, but Bun as Node-like
|
|
192
|
+
const isNodeEnvironment = typeof process !== 'undefined' &&
|
|
193
|
+
typeof window === 'undefined' &&
|
|
194
|
+
!!((process.versions && process.versions.node) || process.versions.bun);
|
|
195
|
+
const getFS = async () => {
|
|
196
|
+
if (!isNodeEnvironment) {
|
|
197
|
+
throw new Error('Filesystem access is not available in this environment (React Native or browser)');
|
|
198
|
+
}
|
|
199
|
+
// The magic comments keep browser bundlers from trying to resolve fs
|
|
200
|
+
return import(/* @vite-ignore */ /* webpackIgnore: true */ 'node:fs');
|
|
201
|
+
};
|
|
202
|
+
const toASCII = (domain) => {
|
|
203
|
+
try {
|
|
204
|
+
const scheme = domain.includes('localhost') ? 'http' : 'https';
|
|
205
|
+
return new URL(`${scheme}://${domain}`).hostname;
|
|
206
|
+
}
|
|
207
|
+
catch {
|
|
208
|
+
return domain;
|
|
209
|
+
}
|
|
210
|
+
};
|
|
211
|
+
export const DID_PLACEHOLDER = '{DID}';
|
|
212
|
+
export function validateCreateDidDocument(didDocument) {
|
|
213
|
+
if (!didDocument || typeof didDocument !== 'object') {
|
|
214
|
+
throw new Error('didDocument must be an object');
|
|
215
|
+
}
|
|
216
|
+
if (typeof didDocument.id !== 'string') {
|
|
217
|
+
throw new Error("didDocument 'id' field must be a string");
|
|
218
|
+
}
|
|
219
|
+
if (!didDocument.id.includes('{SCID}') && !didDocument.id.includes(DID_PLACEHOLDER)) {
|
|
220
|
+
throw new Error("didDocument.id must contain a '{SCID}' or '{DID}' placeholder");
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
export function replaceCreateDidPlaceholders(input, scid, did) {
|
|
224
|
+
const withScid = replaceValueInObject(input, '{SCID}', scid);
|
|
225
|
+
return replaceValueInObject(withScid, DID_PLACEHOLDER, did);
|
|
226
|
+
}
|
|
227
|
+
export function convertWebvhIdToWebId(id) {
|
|
228
|
+
const parts = id.split(':');
|
|
229
|
+
if (parts.length < 4 || parts[0] !== 'did' || parts[1] !== 'webvh') {
|
|
230
|
+
throw new Error(`Invalid did:webvh id '${id}'`);
|
|
231
|
+
}
|
|
232
|
+
return `did:web:${parts.slice(3).join(':')}`;
|
|
233
|
+
}
|
|
234
|
+
export function enrichAlsoKnownAs(doc, did, opts) {
|
|
235
|
+
if (doc.alsoKnownAs !== undefined && !Array.isArray(doc.alsoKnownAs)) {
|
|
236
|
+
throw new Error('alsoKnownAs is not an array');
|
|
237
|
+
}
|
|
238
|
+
const aliases = Array.isArray(doc.alsoKnownAs) ? [...doc.alsoKnownAs] : [];
|
|
239
|
+
const addAlias = (alias) => {
|
|
240
|
+
if (!aliases.includes(alias)) {
|
|
241
|
+
aliases.push(alias);
|
|
242
|
+
}
|
|
243
|
+
};
|
|
244
|
+
if (opts.alsoKnownAsWeb) {
|
|
245
|
+
addAlias(convertWebvhIdToWebId(did));
|
|
246
|
+
}
|
|
247
|
+
if (aliases.length === 0) {
|
|
248
|
+
return doc;
|
|
249
|
+
}
|
|
250
|
+
return {
|
|
251
|
+
...doc,
|
|
252
|
+
alsoKnownAs: aliases,
|
|
253
|
+
};
|
|
254
|
+
}
|
|
255
|
+
export function generateParallelDidWeb(didwebvhDid, didwebvhDoc) {
|
|
256
|
+
let webDoc = deepClone(didwebvhDoc);
|
|
257
|
+
const domainPath = didwebvhDid.replace(/^did:webvh:[^:]+:/, '');
|
|
258
|
+
const httpsBase = `https://${decodeURIComponent(domainPath.replace(/:/g, '/'))}/`;
|
|
259
|
+
const existingServiceIds = (webDoc.service ?? []).map((service) => service.id ?? '');
|
|
260
|
+
const implicitServices = [];
|
|
261
|
+
if (!existingServiceIds.some((id) => id.endsWith('#files'))) {
|
|
262
|
+
implicitServices.push({
|
|
263
|
+
id: '#files',
|
|
264
|
+
type: 'relativeRef',
|
|
265
|
+
serviceEndpoint: httpsBase,
|
|
266
|
+
});
|
|
267
|
+
}
|
|
268
|
+
if (!existingServiceIds.some((id) => id.endsWith('#whois'))) {
|
|
269
|
+
implicitServices.push({
|
|
270
|
+
'@context': 'https://identity.foundation/linked-vp/contexts/v1',
|
|
271
|
+
id: '#whois',
|
|
272
|
+
type: 'LinkedVerifiablePresentation',
|
|
273
|
+
serviceEndpoint: `${httpsBase}whois.vp`,
|
|
274
|
+
});
|
|
275
|
+
}
|
|
276
|
+
if (implicitServices.length > 0) {
|
|
277
|
+
webDoc = { ...webDoc, service: [...(webDoc.service ?? []), ...implicitServices] };
|
|
278
|
+
}
|
|
279
|
+
const scidPrefix = didwebvhDid.replace(/^did:webvh:([^:]+):.*$/, 'did:webvh:$1:');
|
|
280
|
+
webDoc = replaceValueInObject(webDoc, scidPrefix, 'did:web:');
|
|
281
|
+
const webDid = webDoc.id;
|
|
282
|
+
const aliases = (Array.isArray(webDoc.alsoKnownAs) ? [...webDoc.alsoKnownAs] : []).filter((alias) => alias !== webDid);
|
|
283
|
+
if (!aliases.includes(didwebvhDid)) {
|
|
284
|
+
aliases.push(didwebvhDid);
|
|
285
|
+
}
|
|
286
|
+
return {
|
|
287
|
+
...webDoc,
|
|
288
|
+
alsoKnownAs: [...new Set(aliases)],
|
|
289
|
+
};
|
|
290
|
+
}
|
|
291
|
+
export const readLogFromDisk = async (path) => {
|
|
292
|
+
const fs = await getFS();
|
|
293
|
+
return readLogFromString(fs.readFileSync(path, 'utf8'));
|
|
294
|
+
};
|
|
295
|
+
export const readLogFromString = (str) => {
|
|
296
|
+
return str
|
|
297
|
+
.trim()
|
|
298
|
+
.split('\n')
|
|
299
|
+
.map((l) => JSON.parse(l));
|
|
300
|
+
};
|
|
301
|
+
export const writeLogToDisk = async (path, log) => {
|
|
302
|
+
const fs = await getFS();
|
|
303
|
+
try {
|
|
304
|
+
const dir = path.substring(0, path.lastIndexOf('/'));
|
|
305
|
+
if (dir && !fs.existsSync(dir)) {
|
|
306
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
307
|
+
}
|
|
308
|
+
fs.writeFileSync(path, `${JSON.stringify(log[0])}\n`);
|
|
309
|
+
for (let i = 1; i < log.length; i++) {
|
|
310
|
+
fs.appendFileSync(path, `${JSON.stringify(log[i])}\n`);
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
catch (error) {
|
|
314
|
+
console.error('Error writing log to disk:', error);
|
|
315
|
+
throw error;
|
|
316
|
+
}
|
|
317
|
+
};
|
|
318
|
+
export const maybeWriteTestLog = async (did, log) => {
|
|
319
|
+
if (!config.isTestEnvironment)
|
|
320
|
+
return;
|
|
321
|
+
try {
|
|
322
|
+
const fileSafe = did.replace(/[^a-zA-Z0-9]+/g, '_');
|
|
323
|
+
const path = `./test/logs/${fileSafe}.jsonl`;
|
|
324
|
+
await writeLogToDisk(path, log);
|
|
325
|
+
}
|
|
326
|
+
catch (error) {
|
|
327
|
+
console.error('Error writing test log:', error);
|
|
328
|
+
}
|
|
329
|
+
};
|
|
330
|
+
export const writeVerificationMethodToEnv = async (verificationMethod) => {
|
|
331
|
+
const envFilePath = `${process.cwd()}/.env`;
|
|
332
|
+
const vmData = {
|
|
333
|
+
id: verificationMethod.id,
|
|
334
|
+
type: verificationMethod.type,
|
|
335
|
+
controller: verificationMethod.controller || '',
|
|
336
|
+
publicKeyMultibase: verificationMethod.publicKeyMultibase,
|
|
337
|
+
secretKeyMultibase: verificationMethod.secretKeyMultibase || '',
|
|
338
|
+
};
|
|
339
|
+
const fs = await getFS();
|
|
340
|
+
try {
|
|
341
|
+
let envContent = '';
|
|
342
|
+
let existingData = [];
|
|
343
|
+
if (fs.existsSync(envFilePath)) {
|
|
344
|
+
envContent = fs.readFileSync(envFilePath, 'utf8');
|
|
345
|
+
const match = envContent.match(/DID_VERIFICATION_METHODS=(.*)/);
|
|
346
|
+
if (match?.[1]) {
|
|
347
|
+
const decodedData = bufferToString(createBuffer(match[1], 'base64'));
|
|
348
|
+
existingData = JSON.parse(decodedData);
|
|
349
|
+
// Check if verification method with same ID already exists
|
|
350
|
+
const existingIndex = existingData.findIndex((vm) => vm.id === vmData.id);
|
|
351
|
+
if (existingIndex !== -1) {
|
|
352
|
+
// Update existing verification method
|
|
353
|
+
existingData[existingIndex] = vmData;
|
|
354
|
+
}
|
|
355
|
+
else {
|
|
356
|
+
// Add new verification method
|
|
357
|
+
existingData.push(vmData);
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
else {
|
|
361
|
+
// No existing verification methods, create new array
|
|
362
|
+
existingData = [vmData];
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
else {
|
|
366
|
+
// No .env file exists, create new array
|
|
367
|
+
existingData = [vmData];
|
|
368
|
+
}
|
|
369
|
+
const jsonData = JSON.stringify(existingData);
|
|
370
|
+
const encodedData = bufferToString(createBuffer(jsonData), 'base64');
|
|
371
|
+
// If DID_VERIFICATION_METHODS already exists, replace it
|
|
372
|
+
if (envContent.includes('DID_VERIFICATION_METHODS=')) {
|
|
373
|
+
envContent = envContent.replace(/DID_VERIFICATION_METHODS=.*\n?/, `DID_VERIFICATION_METHODS=${encodedData}\n`);
|
|
374
|
+
}
|
|
375
|
+
else {
|
|
376
|
+
// Otherwise append it
|
|
377
|
+
envContent += `DID_VERIFICATION_METHODS=${encodedData}\n`;
|
|
378
|
+
}
|
|
379
|
+
fs.writeFileSync(envFilePath, `${envContent.trim()}\n`);
|
|
380
|
+
console.log('Verification method written to .env file successfully.');
|
|
381
|
+
}
|
|
382
|
+
catch (error) {
|
|
383
|
+
console.error('Error writing verification method to .env file:', error);
|
|
384
|
+
}
|
|
385
|
+
};
|
|
386
|
+
export const clone = (input) => JSON.parse(JSON.stringify(input));
|
|
387
|
+
export function deepClone(obj) {
|
|
388
|
+
if (obj === null || typeof obj !== 'object')
|
|
389
|
+
return obj;
|
|
390
|
+
if (obj instanceof Date)
|
|
391
|
+
return new Date(obj.getTime());
|
|
392
|
+
if (Array.isArray(obj))
|
|
393
|
+
return obj.map((item) => deepClone(item));
|
|
394
|
+
const cloned = {};
|
|
395
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
396
|
+
cloned[key] = deepClone(value);
|
|
397
|
+
}
|
|
398
|
+
return cloned;
|
|
399
|
+
}
|
|
400
|
+
export const getBaseUrl = (id) => {
|
|
401
|
+
const parts = id.split(':');
|
|
402
|
+
if (!id.startsWith('did:webvh:') || parts.length < 4) {
|
|
403
|
+
throw new Error(`${id} is not a valid did:webvh identifier`);
|
|
404
|
+
}
|
|
405
|
+
const remainder = decodeURIComponent(parts.slice(3).join('/'));
|
|
406
|
+
const protocol = remainder.includes('localhost') ? 'http' : 'https';
|
|
407
|
+
const [hostPart, ...pathParts] = remainder.split('/');
|
|
408
|
+
let [host, port] = decodeURIComponent(hostPart).split(':');
|
|
409
|
+
host = toASCII(host.normalize('NFC'));
|
|
410
|
+
const normalizedHost = port ? `${host}:${port}` : host;
|
|
411
|
+
const path = pathParts.join('/');
|
|
412
|
+
return `${protocol}://${normalizedHost}${path ? `/${path}` : ''}`;
|
|
413
|
+
};
|
|
414
|
+
export const getFileUrl = (id) => {
|
|
415
|
+
const baseUrl = getBaseUrl(id);
|
|
416
|
+
const domainEndIndex = baseUrl.indexOf('/', baseUrl.indexOf('://') + 3);
|
|
417
|
+
if (domainEndIndex !== -1) {
|
|
418
|
+
return `${baseUrl}/did.jsonl`;
|
|
419
|
+
}
|
|
420
|
+
return `${baseUrl}/.well-known/did.jsonl`;
|
|
421
|
+
};
|
|
422
|
+
export async function fetchLogFromIdentifier(identifier) {
|
|
423
|
+
try {
|
|
424
|
+
const url = getFileUrl(identifier);
|
|
425
|
+
const response = await fetch(url);
|
|
426
|
+
if (!response.ok) {
|
|
427
|
+
throw new Error(`HTTP error! status: ${response.status}`);
|
|
428
|
+
}
|
|
429
|
+
const text = (await response.text()).trim();
|
|
430
|
+
if (!text) {
|
|
431
|
+
throw new Error(`DID log not found for ${identifier}`);
|
|
432
|
+
}
|
|
433
|
+
return text.split('\n').map((line) => JSON.parse(line));
|
|
434
|
+
}
|
|
435
|
+
catch (error) {
|
|
436
|
+
console.error('Error fetching DID log:', error);
|
|
437
|
+
throw error;
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
export const createDate = (created) => `${new Date(created ?? Date.now()).toISOString().slice(0, -5)}Z`;
|
|
441
|
+
export function bytesToHex(bytes) {
|
|
442
|
+
return Array.from(bytes)
|
|
443
|
+
.map((byte) => byte.toString(16).padStart(2, '0'))
|
|
444
|
+
.join('');
|
|
445
|
+
}
|
|
446
|
+
export const createSCID = async (logEntryHash) => {
|
|
447
|
+
return logEntryHash;
|
|
448
|
+
};
|
|
449
|
+
// Cache for deriveHash operations to avoid redundant computation
|
|
450
|
+
const hashCache = new Map();
|
|
451
|
+
function getCachedHash(input) {
|
|
452
|
+
try {
|
|
453
|
+
const key = JSON.stringify(input);
|
|
454
|
+
return hashCache.get(key);
|
|
455
|
+
}
|
|
456
|
+
catch {
|
|
457
|
+
return undefined;
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
function setCachedHash(input, hash) {
|
|
461
|
+
try {
|
|
462
|
+
const key = JSON.stringify(input);
|
|
463
|
+
hashCache.set(key, hash);
|
|
464
|
+
}
|
|
465
|
+
catch {
|
|
466
|
+
// Ignore caching errors
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
// Input must be strict JSON-compatible and must not contain explicit undefined values.
|
|
470
|
+
export async function deriveHash(input) {
|
|
471
|
+
const cached = getCachedHash(input);
|
|
472
|
+
if (cached) {
|
|
473
|
+
return cached;
|
|
474
|
+
}
|
|
475
|
+
const data = canonicalizeStrict(input);
|
|
476
|
+
const hash = await createHash(data);
|
|
477
|
+
const multihash = createMultihash(new Uint8Array(hash), MultihashAlgorithm.SHA2_256);
|
|
478
|
+
const result = encodeBase58Btc(multihash);
|
|
479
|
+
setCachedHash(input, result);
|
|
480
|
+
return result;
|
|
481
|
+
}
|
|
482
|
+
export const deriveNextKeyHash = async (input) => {
|
|
483
|
+
const hash = await createHash(input);
|
|
484
|
+
const multihash = createMultihash(new Uint8Array(hash), MultihashAlgorithm.SHA2_256);
|
|
485
|
+
return encodeBase58Btc(multihash);
|
|
486
|
+
};
|
|
487
|
+
export const createDIDDoc = async (options) => {
|
|
488
|
+
const { controller } = options;
|
|
489
|
+
const all = normalizeVMs(options.verificationMethods, controller);
|
|
490
|
+
// Create the base document
|
|
491
|
+
const doc = {
|
|
492
|
+
'@context': options.context || BASE_CONTEXT,
|
|
493
|
+
id: controller,
|
|
494
|
+
controller,
|
|
495
|
+
};
|
|
496
|
+
// Add verification methods and relationships from normalizeVMs
|
|
497
|
+
if (all && typeof all === 'object') {
|
|
498
|
+
if (all.verificationMethod) {
|
|
499
|
+
doc.verificationMethod = all.verificationMethod;
|
|
500
|
+
}
|
|
501
|
+
if (all.authentication) {
|
|
502
|
+
doc.authentication = all.authentication;
|
|
503
|
+
}
|
|
504
|
+
if (all.assertionMethod) {
|
|
505
|
+
doc.assertionMethod = all.assertionMethod;
|
|
506
|
+
}
|
|
507
|
+
if (all.keyAgreement) {
|
|
508
|
+
doc.keyAgreement = all.keyAgreement;
|
|
509
|
+
}
|
|
510
|
+
if (all.capabilityDelegation) {
|
|
511
|
+
doc.capabilityDelegation = all.capabilityDelegation;
|
|
512
|
+
}
|
|
513
|
+
if (all.capabilityInvocation) {
|
|
514
|
+
doc.capabilityInvocation = all.capabilityInvocation;
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
// Add direct properties from options
|
|
518
|
+
if (options.authentication) {
|
|
519
|
+
doc.authentication = options.authentication;
|
|
520
|
+
}
|
|
521
|
+
if (options.assertionMethod) {
|
|
522
|
+
doc.assertionMethod = options.assertionMethod;
|
|
523
|
+
}
|
|
524
|
+
if (options.keyAgreement) {
|
|
525
|
+
doc.keyAgreement = options.keyAgreement;
|
|
526
|
+
}
|
|
527
|
+
if (options.alsoKnownAs) {
|
|
528
|
+
doc.alsoKnownAs = options.alsoKnownAs;
|
|
529
|
+
}
|
|
530
|
+
if (options.services) {
|
|
531
|
+
doc.service = options.services;
|
|
532
|
+
}
|
|
533
|
+
return { doc };
|
|
534
|
+
};
|
|
535
|
+
// Helper function to generate a random string (replacement for nanoid)
|
|
536
|
+
export const generateRandomId = (length = 8) => {
|
|
537
|
+
const characters = 'abcdefghijklmnopqrstuvwxyz0123456789';
|
|
538
|
+
let result = '';
|
|
539
|
+
const charactersLength = characters.length;
|
|
540
|
+
for (let i = 0; i < length; i++) {
|
|
541
|
+
result += characters.charAt(Math.floor(Math.random() * charactersLength));
|
|
542
|
+
}
|
|
543
|
+
return result;
|
|
544
|
+
};
|
|
545
|
+
export const createVMID = (vm, did) => {
|
|
546
|
+
return `${did ?? ''}#${vm.publicKeyMultibase?.slice(-8) || generateRandomId(8)}`;
|
|
547
|
+
};
|
|
548
|
+
export const normalizeVMs = (verificationMethod, did = null) => {
|
|
549
|
+
const all = {
|
|
550
|
+
verificationMethod: [],
|
|
551
|
+
authentication: [],
|
|
552
|
+
assertionMethod: [],
|
|
553
|
+
keyAgreement: [],
|
|
554
|
+
capabilityDelegation: [],
|
|
555
|
+
capabilityInvocation: [],
|
|
556
|
+
};
|
|
557
|
+
if (!verificationMethod || verificationMethod.length === 0) {
|
|
558
|
+
return all;
|
|
559
|
+
}
|
|
560
|
+
// First collect all VMs
|
|
561
|
+
const vms = verificationMethod.map((vm) => ({
|
|
562
|
+
...vm,
|
|
563
|
+
id: vm.id ?? createVMID(vm, did),
|
|
564
|
+
// Default controller to the DID — required by W3C DID Core §5.2
|
|
565
|
+
controller: vm.controller ?? did,
|
|
566
|
+
}));
|
|
567
|
+
all.verificationMethod = vms;
|
|
568
|
+
// Then handle relationships - default to authentication if no purpose is specified
|
|
569
|
+
all.authentication = verificationMethod
|
|
570
|
+
.filter((vm) => !vm.purpose || vm.purpose === 'authentication')
|
|
571
|
+
.map((vm) => vm.id ?? createVMID(vm, did));
|
|
572
|
+
all.assertionMethod = verificationMethod
|
|
573
|
+
.filter((vm) => vm.purpose === 'assertionMethod')
|
|
574
|
+
.map((vm) => vm.id ?? createVMID(vm, did));
|
|
575
|
+
all.keyAgreement = verificationMethod
|
|
576
|
+
.filter((vm) => vm.purpose === 'keyAgreement')
|
|
577
|
+
.map((vm) => vm.id ?? createVMID(vm, did));
|
|
578
|
+
all.capabilityDelegation = verificationMethod
|
|
579
|
+
.filter((vm) => vm.purpose === 'capabilityDelegation')
|
|
580
|
+
.map((vm) => vm.id ?? createVMID(vm, did));
|
|
581
|
+
all.capabilityInvocation = verificationMethod
|
|
582
|
+
.filter((vm) => vm.purpose === 'capabilityInvocation')
|
|
583
|
+
.map((vm) => vm.id ?? createVMID(vm, did));
|
|
584
|
+
return all;
|
|
585
|
+
};
|
|
586
|
+
export const resolveVM = async (vm) => {
|
|
587
|
+
try {
|
|
588
|
+
if (vm.startsWith('did:key:')) {
|
|
589
|
+
const parsedVerificationMethod = parseDidKeyVerificationMethod(vm);
|
|
590
|
+
return { publicKeyMultibase: parsedVerificationMethod.keyMultibase };
|
|
591
|
+
}
|
|
592
|
+
else if (vm.startsWith('did:webvh:')) {
|
|
593
|
+
const url = getFileUrl(vm.split('#')[0]);
|
|
594
|
+
const didLog = await (await fetch(url)).text();
|
|
595
|
+
const logEntries = didLog
|
|
596
|
+
.trim()
|
|
597
|
+
.split('\n')
|
|
598
|
+
.map((l) => JSON.parse(l));
|
|
599
|
+
const { doc } = await resolveDIDFromLog(logEntries, { verificationMethod: vm });
|
|
600
|
+
return findVerificationMethod(doc, vm);
|
|
601
|
+
}
|
|
602
|
+
throw new Error(`Verification method ${vm} not found`);
|
|
603
|
+
}
|
|
604
|
+
catch (e) {
|
|
605
|
+
throw new Error(`Error resolving VM ${vm}`);
|
|
606
|
+
}
|
|
607
|
+
};
|
|
608
|
+
export const findVerificationMethod = (doc, vmId) => {
|
|
609
|
+
// Check in the verificationMethod array
|
|
610
|
+
if (doc.verificationMethod?.some((vm) => vm.id === vmId)) {
|
|
611
|
+
return doc.verificationMethod.find((vm) => vm.id === vmId);
|
|
612
|
+
}
|
|
613
|
+
// Check in other verification method relationship arrays
|
|
614
|
+
const vmRelationships = [
|
|
615
|
+
'authentication',
|
|
616
|
+
'assertionMethod',
|
|
617
|
+
'keyAgreement',
|
|
618
|
+
'capabilityInvocation',
|
|
619
|
+
'capabilityDelegation',
|
|
620
|
+
];
|
|
621
|
+
for (const relationship of vmRelationships) {
|
|
622
|
+
if (doc[relationship]) {
|
|
623
|
+
if (doc[relationship].some((item) => item.id === vmId)) {
|
|
624
|
+
return doc[relationship].find((item) => item.id === vmId);
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
return null;
|
|
629
|
+
};
|
|
630
|
+
export async function fetchWitnessProofs(did) {
|
|
631
|
+
try {
|
|
632
|
+
const url = getFileUrl(did).replace('did.jsonl', 'did-witness.json');
|
|
633
|
+
const response = await fetch(url);
|
|
634
|
+
if (!response.ok) {
|
|
635
|
+
return [];
|
|
636
|
+
}
|
|
637
|
+
return await response.json();
|
|
638
|
+
}
|
|
639
|
+
catch (error) {
|
|
640
|
+
console.error('Error fetching witness proofs:', error);
|
|
641
|
+
return [];
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
export function replaceValueInObject(obj, searchValue, replaceValue) {
|
|
645
|
+
if (typeof obj === 'string') {
|
|
646
|
+
return obj.replaceAll(searchValue, replaceValue);
|
|
647
|
+
}
|
|
648
|
+
if (Array.isArray(obj)) {
|
|
649
|
+
return obj.map((item) => replaceValueInObject(item, searchValue, replaceValue));
|
|
650
|
+
}
|
|
651
|
+
if (obj && typeof obj === 'object') {
|
|
652
|
+
const result = {};
|
|
653
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
654
|
+
result[key] = replaceValueInObject(value, searchValue, replaceValue);
|
|
655
|
+
}
|
|
656
|
+
return result;
|
|
657
|
+
}
|
|
658
|
+
return obj;
|
|
659
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import type { DataIntegrityProof, DataIntegrityProofTemplate, DIDLogEntry, Signer, Verifier, WitnessEntry, WitnessParameterResolution, WitnessProofFileEntry, WitnessSigningOptions, WitnessSigningResult } from './interfaces.js';
|
|
2
|
+
import { fetchWitnessProofs } from './utils.js';
|
|
3
|
+
/**
|
|
4
|
+
* Creates a single witness DataIntegrityProof for one `versionId`.
|
|
5
|
+
*
|
|
6
|
+
* @param signer Proof signer callback.
|
|
7
|
+
* @param versionId Target DID log version id.
|
|
8
|
+
* @param verificationMethod Witness verification method DID URL.
|
|
9
|
+
* @param created Optional proof creation time in ISO format.
|
|
10
|
+
* @returns A complete DataIntegrityProof for did-witness processing.
|
|
11
|
+
*/
|
|
12
|
+
export declare function createWitnessProof(signer: (doc: {
|
|
13
|
+
versionId: string;
|
|
14
|
+
}, proofTemplate?: DataIntegrityProofTemplate) => Promise<{
|
|
15
|
+
proof: Partial<DataIntegrityProof>;
|
|
16
|
+
}>, versionId: string, verificationMethod: string, created?: string): Promise<DataIntegrityProof>;
|
|
17
|
+
/**
|
|
18
|
+
* Signs one did-witness proof entry for a single target `versionId`.
|
|
19
|
+
*
|
|
20
|
+
* The signer map is keyed by witness DID (`did:key:...`).
|
|
21
|
+
*
|
|
22
|
+
* @param options Witness signing options for one target version.
|
|
23
|
+
* @returns A witness proof file entry for the target version.
|
|
24
|
+
*/
|
|
25
|
+
export declare function signWitnessProofEntry(options: WitnessSigningOptions): Promise<WitnessSigningResult>;
|
|
26
|
+
/**
|
|
27
|
+
* Signs did-witness proof entries for multiple target `versionId`s.
|
|
28
|
+
*
|
|
29
|
+
* @param versionIds Target DID log version ids.
|
|
30
|
+
* @param witnesses Witness DID entries used to sign.
|
|
31
|
+
* @param witnessSignersByDid Signer map keyed by witness did:key DID.
|
|
32
|
+
* @param created Optional proof creation time in ISO format.
|
|
33
|
+
* @returns A witness proof file entry per version id.
|
|
34
|
+
*/
|
|
35
|
+
export declare function signWitnessProofEntries(versionIds: string[], witnesses: WitnessEntry[], witnessSignersByDid: Record<string, Signer>, created?: string): Promise<WitnessSigningResult[]>;
|
|
36
|
+
export declare function validateWitnessParameter(witness: WitnessParameterResolution): void;
|
|
37
|
+
export declare function countWitnessApprovals(proofs: DataIntegrityProof[], witnesses: WitnessEntry[]): number;
|
|
38
|
+
export declare function countVerifiedWitnessApprovals(logEntry: DIDLogEntry, witnessProofs: WitnessProofFileEntry[], currentWitness: WitnessParameterResolution, verifier?: Verifier): Promise<number>;
|
|
39
|
+
export { fetchWitnessProofs };
|