chainlesschain 0.51.0 → 0.81.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/package.json +1 -1
- package/src/assets/web-panel/.build-hash +1 -1
- package/src/assets/web-panel/assets/{AppLayout-Rvi759IS.js → AppLayout-6SPt_8Y_.js} +1 -1
- package/src/assets/web-panel/assets/{Dashboard-DBhFxXYQ.js → Dashboard-Br7kCwKJ.js} +2 -2
- package/src/assets/web-panel/assets/Dashboard-CKeMmCoT.css +1 -0
- package/src/assets/web-panel/assets/{index-uL0cZ8N_.js → index-tN-8TosE.js} +2 -2
- package/src/assets/web-panel/index.html +2 -2
- package/src/commands/a2a.js +380 -0
- package/src/commands/agent-network.js +785 -0
- package/src/commands/automation.js +654 -0
- package/src/commands/bi.js +348 -0
- package/src/commands/crosschain.js +218 -0
- package/src/commands/dao.js +565 -0
- package/src/commands/did-v2.js +620 -0
- package/src/commands/dlp.js +341 -0
- package/src/commands/economy.js +578 -0
- package/src/commands/evolution.js +391 -0
- package/src/commands/evomap.js +394 -0
- package/src/commands/federation.js +283 -0
- package/src/commands/hmemory.js +442 -0
- package/src/commands/inference.js +318 -0
- package/src/commands/lowcode.js +356 -0
- package/src/commands/marketplace.js +256 -0
- package/src/commands/perf.js +433 -0
- package/src/commands/pipeline.js +449 -0
- package/src/commands/plugin-ecosystem.js +517 -0
- package/src/commands/privacy.js +321 -0
- package/src/commands/reputation.js +261 -0
- package/src/commands/sandbox.js +401 -0
- package/src/commands/siem.js +246 -0
- package/src/commands/sla.js +259 -0
- package/src/commands/social.js +311 -0
- package/src/commands/sso.js +798 -0
- package/src/commands/stress.js +230 -0
- package/src/commands/terraform.js +245 -0
- package/src/commands/workflow.js +320 -0
- package/src/commands/zkp.js +562 -1
- package/src/index.js +21 -0
- package/src/lib/a2a-protocol.js +451 -0
- package/src/lib/agent-economy.js +479 -0
- package/src/lib/agent-network.js +1121 -0
- package/src/lib/app-builder.js +239 -0
- package/src/lib/automation-engine.js +948 -0
- package/src/lib/bi-engine.js +338 -0
- package/src/lib/cross-chain.js +345 -0
- package/src/lib/dao-governance.js +569 -0
- package/src/lib/did-v2-manager.js +1127 -0
- package/src/lib/dlp-engine.js +389 -0
- package/src/lib/evolution-system.js +453 -0
- package/src/lib/evomap-federation.js +177 -0
- package/src/lib/evomap-governance.js +276 -0
- package/src/lib/federation-hardening.js +259 -0
- package/src/lib/hierarchical-memory.js +481 -0
- package/src/lib/inference-network.js +330 -0
- package/src/lib/perf-tuning.js +734 -0
- package/src/lib/pipeline-orchestrator.js +928 -0
- package/src/lib/plugin-ecosystem.js +1109 -0
- package/src/lib/privacy-computing.js +427 -0
- package/src/lib/reputation-optimizer.js +299 -0
- package/src/lib/sandbox-v2.js +306 -0
- package/src/lib/siem-exporter.js +333 -0
- package/src/lib/skill-marketplace.js +325 -0
- package/src/lib/sla-manager.js +275 -0
- package/src/lib/social-graph-analytics.js +707 -0
- package/src/lib/sso-manager.js +841 -0
- package/src/lib/stress-tester.js +330 -0
- package/src/lib/terraform-manager.js +363 -0
- package/src/lib/workflow-engine.js +454 -1
- package/src/lib/zkp-engine.js +523 -20
- package/src/assets/web-panel/assets/Dashboard-BS-tzGNj.css +0 -1
|
@@ -0,0 +1,1127 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DID v2.0 Manager — CLI port of Phase 55 去中心化身份2.0
|
|
3
|
+
* (docs/design/modules/55_去中心化身份2.0.md).
|
|
4
|
+
*
|
|
5
|
+
* Desktop ships 8 IPC handlers for W3C DID v2.0: multi-method DID
|
|
6
|
+
* (did:key / did:web / did:chain), Verifiable Presentations with
|
|
7
|
+
* selective disclosure, social recovery via k-of-n threshold shards,
|
|
8
|
+
* cross-platform identity roaming, and multi-source reputation
|
|
9
|
+
* aggregation.
|
|
10
|
+
*
|
|
11
|
+
* CLI port is headless & single-process:
|
|
12
|
+
* - ZKP integration (`zkp_proof_id` field) is a placeholder — real
|
|
13
|
+
* proof generation stays Desktop-only
|
|
14
|
+
* - "Threshold" recovery uses simple k-of-n share matching, not
|
|
15
|
+
* Shamir — documented in memory
|
|
16
|
+
* - Reputation aggregation uses weighted mean over caller-supplied
|
|
17
|
+
* sources (no real on-chain/social oracle)
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import crypto from "crypto";
|
|
21
|
+
|
|
22
|
+
/* ── Constants ───────────────────────────────────────────── */
|
|
23
|
+
|
|
24
|
+
export const DID_METHOD = Object.freeze({
|
|
25
|
+
KEY: "key",
|
|
26
|
+
WEB: "web",
|
|
27
|
+
CHAIN: "chain",
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
export const CREDENTIAL_STATUS = Object.freeze({
|
|
31
|
+
ACTIVE: "active",
|
|
32
|
+
REVOKED: "revoked",
|
|
33
|
+
EXPIRED: "expired",
|
|
34
|
+
SUSPENDED: "suspended",
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
export const RECOVERY_STATUS = Object.freeze({
|
|
38
|
+
PENDING: "pending",
|
|
39
|
+
THRESHOLD_MET: "threshold_met",
|
|
40
|
+
RECOVERED: "recovered",
|
|
41
|
+
FAILED: "failed",
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
export const DID_STATUS = Object.freeze({
|
|
45
|
+
ACTIVE: "active",
|
|
46
|
+
REVOKED: "revoked",
|
|
47
|
+
ROAMED: "roamed",
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
export const REPUTATION_SOURCE_WEIGHTS = Object.freeze({
|
|
51
|
+
"on-chain": 1.3,
|
|
52
|
+
social: 1.0,
|
|
53
|
+
marketplace: 1.1,
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
export const DEFAULT_RECOVERY_THRESHOLD = 3;
|
|
57
|
+
export const DEFAULT_GUARDIAN_COUNT = 5;
|
|
58
|
+
export const VP_DEFAULT_TTL_MS = 30 * 60_000;
|
|
59
|
+
|
|
60
|
+
/* ── Schema ──────────────────────────────────────────────── */
|
|
61
|
+
|
|
62
|
+
export function ensureDIDv2Tables(db) {
|
|
63
|
+
db.exec(`
|
|
64
|
+
CREATE TABLE IF NOT EXISTS did_v2_documents (
|
|
65
|
+
id TEXT PRIMARY KEY,
|
|
66
|
+
did TEXT NOT NULL,
|
|
67
|
+
method TEXT NOT NULL,
|
|
68
|
+
document TEXT NOT NULL,
|
|
69
|
+
public_key TEXT,
|
|
70
|
+
private_key TEXT,
|
|
71
|
+
authentication TEXT,
|
|
72
|
+
service_endpoints TEXT,
|
|
73
|
+
recovery_guardians TEXT,
|
|
74
|
+
recovery_threshold INTEGER DEFAULT 3,
|
|
75
|
+
reputation_score REAL DEFAULT 0.0,
|
|
76
|
+
status TEXT DEFAULT 'active',
|
|
77
|
+
created_at INTEGER NOT NULL,
|
|
78
|
+
updated_at INTEGER NOT NULL
|
|
79
|
+
)
|
|
80
|
+
`);
|
|
81
|
+
|
|
82
|
+
db.exec(`
|
|
83
|
+
CREATE TABLE IF NOT EXISTS did_v2_credentials (
|
|
84
|
+
id TEXT PRIMARY KEY,
|
|
85
|
+
holder_did TEXT NOT NULL,
|
|
86
|
+
issuer_did TEXT NOT NULL,
|
|
87
|
+
type TEXT NOT NULL,
|
|
88
|
+
credential_subject TEXT,
|
|
89
|
+
proof TEXT,
|
|
90
|
+
issuance_date INTEGER NOT NULL,
|
|
91
|
+
expiration_date INTEGER,
|
|
92
|
+
status TEXT DEFAULT 'active',
|
|
93
|
+
revocation_reason TEXT,
|
|
94
|
+
created_at INTEGER NOT NULL
|
|
95
|
+
)
|
|
96
|
+
`);
|
|
97
|
+
|
|
98
|
+
db.exec(`
|
|
99
|
+
CREATE TABLE IF NOT EXISTS did_v2_presentations (
|
|
100
|
+
id TEXT PRIMARY KEY,
|
|
101
|
+
holder_did TEXT NOT NULL,
|
|
102
|
+
recipient_did TEXT,
|
|
103
|
+
credential_ids TEXT NOT NULL,
|
|
104
|
+
disclosed_fields TEXT,
|
|
105
|
+
proof TEXT NOT NULL,
|
|
106
|
+
zkp_proof_id TEXT,
|
|
107
|
+
verified INTEGER DEFAULT 0,
|
|
108
|
+
verification_time_ms REAL,
|
|
109
|
+
created_at INTEGER NOT NULL,
|
|
110
|
+
expires_at INTEGER NOT NULL
|
|
111
|
+
)
|
|
112
|
+
`);
|
|
113
|
+
|
|
114
|
+
db.exec(`
|
|
115
|
+
CREATE TABLE IF NOT EXISTS did_v2_recovery_attempts (
|
|
116
|
+
id TEXT PRIMARY KEY,
|
|
117
|
+
did TEXT NOT NULL,
|
|
118
|
+
guardians TEXT NOT NULL,
|
|
119
|
+
shares_submitted TEXT NOT NULL,
|
|
120
|
+
threshold INTEGER NOT NULL,
|
|
121
|
+
status TEXT NOT NULL,
|
|
122
|
+
new_public_key TEXT,
|
|
123
|
+
created_at INTEGER NOT NULL,
|
|
124
|
+
completed_at INTEGER
|
|
125
|
+
)
|
|
126
|
+
`);
|
|
127
|
+
|
|
128
|
+
db.exec(`
|
|
129
|
+
CREATE TABLE IF NOT EXISTS did_v2_roaming_log (
|
|
130
|
+
id TEXT PRIMARY KEY,
|
|
131
|
+
did TEXT NOT NULL,
|
|
132
|
+
source_platform TEXT,
|
|
133
|
+
target_platform TEXT NOT NULL,
|
|
134
|
+
migration_proof TEXT,
|
|
135
|
+
credentials_migrated INTEGER DEFAULT 0,
|
|
136
|
+
reputation_transferred REAL DEFAULT 0.0,
|
|
137
|
+
created_at INTEGER NOT NULL
|
|
138
|
+
)
|
|
139
|
+
`);
|
|
140
|
+
|
|
141
|
+
db.exec(`
|
|
142
|
+
CREATE TABLE IF NOT EXISTS did_v2_reputation_sources (
|
|
143
|
+
id TEXT PRIMARY KEY,
|
|
144
|
+
did TEXT NOT NULL,
|
|
145
|
+
source TEXT NOT NULL,
|
|
146
|
+
score REAL NOT NULL,
|
|
147
|
+
weight REAL NOT NULL,
|
|
148
|
+
evidence TEXT,
|
|
149
|
+
recorded_at INTEGER NOT NULL
|
|
150
|
+
)
|
|
151
|
+
`);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/* ── Internals ───────────────────────────────────────────── */
|
|
155
|
+
|
|
156
|
+
const _now = () => Date.now();
|
|
157
|
+
const _uuid = () => crypto.randomBytes(8).toString("hex");
|
|
158
|
+
|
|
159
|
+
function _sha256(buf) {
|
|
160
|
+
return crypto.createHash("sha256").update(buf).digest();
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function _generateEd25519() {
|
|
164
|
+
const { publicKey, privateKey } = crypto.generateKeyPairSync("ed25519", {
|
|
165
|
+
publicKeyEncoding: { type: "spki", format: "der" },
|
|
166
|
+
privateKeyEncoding: { type: "pkcs8", format: "der" },
|
|
167
|
+
});
|
|
168
|
+
return {
|
|
169
|
+
publicKey: publicKey.toString("hex"),
|
|
170
|
+
privateKey: privateKey.toString("hex"),
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function _signCanonical(privateKeyHex, payload) {
|
|
175
|
+
const key = crypto.createPrivateKey({
|
|
176
|
+
key: Buffer.from(privateKeyHex, "hex"),
|
|
177
|
+
format: "der",
|
|
178
|
+
type: "pkcs8",
|
|
179
|
+
});
|
|
180
|
+
const data = Buffer.from(JSON.stringify(payload));
|
|
181
|
+
return crypto.sign(null, data, key).toString("hex");
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function _verifyCanonical(publicKeyHex, payload, signatureHex) {
|
|
185
|
+
try {
|
|
186
|
+
const key = crypto.createPublicKey({
|
|
187
|
+
key: Buffer.from(publicKeyHex, "hex"),
|
|
188
|
+
format: "der",
|
|
189
|
+
type: "spki",
|
|
190
|
+
});
|
|
191
|
+
const data = Buffer.from(JSON.stringify(payload));
|
|
192
|
+
const sig = Buffer.from(signatureHex, "hex");
|
|
193
|
+
return crypto.verify(null, data, key, sig);
|
|
194
|
+
} catch {
|
|
195
|
+
return false;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function _deriveDID(method, publicKeyHex, domain) {
|
|
200
|
+
if (method === DID_METHOD.KEY) {
|
|
201
|
+
const hash = _sha256(Buffer.from(publicKeyHex, "hex"))
|
|
202
|
+
.toString("base64url")
|
|
203
|
+
.slice(0, 32);
|
|
204
|
+
return `did:key:z${hash}`;
|
|
205
|
+
}
|
|
206
|
+
if (method === DID_METHOD.WEB) {
|
|
207
|
+
const host = (domain || "chainless.local").replace(/[^a-z0-9.-]/gi, "");
|
|
208
|
+
const id = _sha256(Buffer.from(publicKeyHex, "hex"))
|
|
209
|
+
.toString("base64url")
|
|
210
|
+
.slice(0, 16);
|
|
211
|
+
return `did:web:${host}:${id}`;
|
|
212
|
+
}
|
|
213
|
+
// CHAIN
|
|
214
|
+
const hash = _sha256(Buffer.from(publicKeyHex, "hex"))
|
|
215
|
+
.toString("base64url")
|
|
216
|
+
.slice(0, 32);
|
|
217
|
+
return `did:chain:${hash}`;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function _buildDocument(did, method, publicKeyHex, services = []) {
|
|
221
|
+
return {
|
|
222
|
+
"@context": [
|
|
223
|
+
"https://www.w3.org/ns/did/v1",
|
|
224
|
+
"https://w3id.org/security/suites/ed25519-2020/v1",
|
|
225
|
+
],
|
|
226
|
+
id: did,
|
|
227
|
+
verificationMethod: [
|
|
228
|
+
{
|
|
229
|
+
id: `${did}#keys-1`,
|
|
230
|
+
type: "Ed25519VerificationKey2020",
|
|
231
|
+
controller: did,
|
|
232
|
+
publicKeyHex,
|
|
233
|
+
},
|
|
234
|
+
],
|
|
235
|
+
authentication: [`${did}#keys-1`],
|
|
236
|
+
assertionMethod: [`${did}#keys-1`],
|
|
237
|
+
service: services.map((s, i) => ({
|
|
238
|
+
id: `${did}#service-${i}`,
|
|
239
|
+
type: s.type || "Service",
|
|
240
|
+
serviceEndpoint: s.endpoint,
|
|
241
|
+
})),
|
|
242
|
+
method,
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
function _parseJSON(value, fallback) {
|
|
247
|
+
if (!value) return fallback;
|
|
248
|
+
try {
|
|
249
|
+
return JSON.parse(value);
|
|
250
|
+
} catch {
|
|
251
|
+
return fallback;
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
function _rowToDID(row) {
|
|
256
|
+
if (!row) return null;
|
|
257
|
+
return {
|
|
258
|
+
did: row.did,
|
|
259
|
+
method: row.method,
|
|
260
|
+
document: _parseJSON(row.document, {}),
|
|
261
|
+
publicKey: row.public_key,
|
|
262
|
+
authentication: _parseJSON(row.authentication, []),
|
|
263
|
+
serviceEndpoints: _parseJSON(row.service_endpoints, []),
|
|
264
|
+
recoveryGuardians: _parseJSON(row.recovery_guardians, []),
|
|
265
|
+
recoveryThreshold: row.recovery_threshold,
|
|
266
|
+
reputationScore: row.reputation_score,
|
|
267
|
+
status: row.status,
|
|
268
|
+
createdAt: row.created_at,
|
|
269
|
+
updatedAt: row.updated_at,
|
|
270
|
+
};
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
function _rowToCredential(row) {
|
|
274
|
+
if (!row) return null;
|
|
275
|
+
return {
|
|
276
|
+
id: row.id,
|
|
277
|
+
holderDid: row.holder_did,
|
|
278
|
+
issuerDid: row.issuer_did,
|
|
279
|
+
type: row.type,
|
|
280
|
+
credentialSubject: _parseJSON(row.credential_subject, {}),
|
|
281
|
+
proof: _parseJSON(row.proof, null),
|
|
282
|
+
issuanceDate: row.issuance_date,
|
|
283
|
+
expirationDate: row.expiration_date,
|
|
284
|
+
status: row.status,
|
|
285
|
+
revocationReason: row.revocation_reason,
|
|
286
|
+
createdAt: row.created_at,
|
|
287
|
+
};
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
function _rowToPresentation(row) {
|
|
291
|
+
if (!row) return null;
|
|
292
|
+
return {
|
|
293
|
+
id: row.id,
|
|
294
|
+
holderDid: row.holder_did,
|
|
295
|
+
recipientDid: row.recipient_did,
|
|
296
|
+
credentialIds: _parseJSON(row.credential_ids, []),
|
|
297
|
+
disclosedFields: _parseJSON(row.disclosed_fields, []),
|
|
298
|
+
proof: _parseJSON(row.proof, null),
|
|
299
|
+
zkpProofId: row.zkp_proof_id,
|
|
300
|
+
verified: row.verified === 1,
|
|
301
|
+
verificationTimeMs: row.verification_time_ms,
|
|
302
|
+
createdAt: row.created_at,
|
|
303
|
+
expiresAt: row.expires_at,
|
|
304
|
+
};
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
function _rowToRecovery(row) {
|
|
308
|
+
if (!row) return null;
|
|
309
|
+
return {
|
|
310
|
+
id: row.id,
|
|
311
|
+
did: row.did,
|
|
312
|
+
guardians: _parseJSON(row.guardians, []),
|
|
313
|
+
sharesSubmitted: _parseJSON(row.shares_submitted, []),
|
|
314
|
+
threshold: row.threshold,
|
|
315
|
+
status: row.status,
|
|
316
|
+
newPublicKey: row.new_public_key,
|
|
317
|
+
createdAt: row.created_at,
|
|
318
|
+
completedAt: row.completed_at,
|
|
319
|
+
};
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
function _rowToRoamingEntry(row) {
|
|
323
|
+
if (!row) return null;
|
|
324
|
+
return {
|
|
325
|
+
id: row.id,
|
|
326
|
+
did: row.did,
|
|
327
|
+
sourcePlatform: row.source_platform,
|
|
328
|
+
targetPlatform: row.target_platform,
|
|
329
|
+
migrationProof: row.migration_proof,
|
|
330
|
+
credentialsMigrated: row.credentials_migrated,
|
|
331
|
+
reputationTransferred: row.reputation_transferred,
|
|
332
|
+
createdAt: row.created_at,
|
|
333
|
+
};
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
/* ── DID lifecycle ───────────────────────────────────────── */
|
|
337
|
+
|
|
338
|
+
export function createDID(
|
|
339
|
+
db,
|
|
340
|
+
{
|
|
341
|
+
method = DID_METHOD.KEY,
|
|
342
|
+
domain,
|
|
343
|
+
services = [],
|
|
344
|
+
guardians = [],
|
|
345
|
+
threshold = DEFAULT_RECOVERY_THRESHOLD,
|
|
346
|
+
reputationScore = 0.0,
|
|
347
|
+
} = {},
|
|
348
|
+
) {
|
|
349
|
+
const validMethods = Object.values(DID_METHOD);
|
|
350
|
+
if (!validMethods.includes(method)) {
|
|
351
|
+
throw new Error(
|
|
352
|
+
`invalid method: ${method} (expected one of ${validMethods.join(", ")})`,
|
|
353
|
+
);
|
|
354
|
+
}
|
|
355
|
+
if (guardians.length > 0 && threshold > guardians.length) {
|
|
356
|
+
throw new Error(
|
|
357
|
+
`recovery threshold ${threshold} exceeds guardian count ${guardians.length}`,
|
|
358
|
+
);
|
|
359
|
+
}
|
|
360
|
+
const { publicKey, privateKey } = _generateEd25519();
|
|
361
|
+
const did = _deriveDID(method, publicKey, domain);
|
|
362
|
+
|
|
363
|
+
const existing = db
|
|
364
|
+
.prepare(`SELECT id FROM did_v2_documents WHERE did = ?`)
|
|
365
|
+
.get(did);
|
|
366
|
+
if (existing) {
|
|
367
|
+
throw new Error(`did already exists: ${did}`);
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
const doc = _buildDocument(did, method, publicKey, services);
|
|
371
|
+
const now = _now();
|
|
372
|
+
const id = _uuid();
|
|
373
|
+
|
|
374
|
+
db.prepare(
|
|
375
|
+
`INSERT INTO did_v2_documents
|
|
376
|
+
(id, did, method, document, public_key, private_key, authentication,
|
|
377
|
+
service_endpoints, recovery_guardians, recovery_threshold,
|
|
378
|
+
reputation_score, status, created_at, updated_at)
|
|
379
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
380
|
+
).run(
|
|
381
|
+
id,
|
|
382
|
+
did,
|
|
383
|
+
method,
|
|
384
|
+
JSON.stringify(doc),
|
|
385
|
+
publicKey,
|
|
386
|
+
privateKey,
|
|
387
|
+
JSON.stringify(doc.authentication || []),
|
|
388
|
+
JSON.stringify(services),
|
|
389
|
+
JSON.stringify(guardians),
|
|
390
|
+
threshold,
|
|
391
|
+
reputationScore,
|
|
392
|
+
DID_STATUS.ACTIVE,
|
|
393
|
+
now,
|
|
394
|
+
now,
|
|
395
|
+
);
|
|
396
|
+
return { did, method, document: doc, privateKey, publicKey };
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
export function resolveDID(db, did) {
|
|
400
|
+
if (!did) return null;
|
|
401
|
+
const row = db
|
|
402
|
+
.prepare(`SELECT * FROM did_v2_documents WHERE did = ?`)
|
|
403
|
+
.get(did);
|
|
404
|
+
return _rowToDID(row);
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
export function listDIDs(db, { method, status } = {}) {
|
|
408
|
+
const wheres = [];
|
|
409
|
+
const params = [];
|
|
410
|
+
if (method) {
|
|
411
|
+
wheres.push(`method = ?`);
|
|
412
|
+
params.push(method);
|
|
413
|
+
}
|
|
414
|
+
if (status) {
|
|
415
|
+
wheres.push(`status = ?`);
|
|
416
|
+
params.push(status);
|
|
417
|
+
}
|
|
418
|
+
const sql = `SELECT * FROM did_v2_documents ${
|
|
419
|
+
wheres.length ? "WHERE " + wheres.join(" AND ") : ""
|
|
420
|
+
} ORDER BY created_at DESC`;
|
|
421
|
+
return db
|
|
422
|
+
.prepare(sql)
|
|
423
|
+
.all(...params)
|
|
424
|
+
.map(_rowToDID);
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
export function updateDIDStatus(db, did, status) {
|
|
428
|
+
const validStatuses = Object.values(DID_STATUS);
|
|
429
|
+
if (!validStatuses.includes(status)) {
|
|
430
|
+
throw new Error(
|
|
431
|
+
`invalid status: ${status} (expected one of ${validStatuses.join(", ")})`,
|
|
432
|
+
);
|
|
433
|
+
}
|
|
434
|
+
const res = db
|
|
435
|
+
.prepare(
|
|
436
|
+
`UPDATE did_v2_documents SET status = ?, updated_at = ? WHERE did = ?`,
|
|
437
|
+
)
|
|
438
|
+
.run(status, _now(), did);
|
|
439
|
+
return res.changes > 0;
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
/* ── Credentials ─────────────────────────────────────────── */
|
|
443
|
+
|
|
444
|
+
export function issueCredential(
|
|
445
|
+
db,
|
|
446
|
+
{ holderDid, issuerDid, type, credentialSubject = {}, expiresInMs } = {},
|
|
447
|
+
) {
|
|
448
|
+
if (!holderDid) throw new Error("holderDid required");
|
|
449
|
+
if (!issuerDid) throw new Error("issuerDid required");
|
|
450
|
+
if (!type) throw new Error("type required");
|
|
451
|
+
|
|
452
|
+
const issuer = resolveDID(db, issuerDid);
|
|
453
|
+
if (!issuer) throw new Error(`issuer not found: ${issuerDid}`);
|
|
454
|
+
|
|
455
|
+
const issuerKeyRow = db
|
|
456
|
+
.prepare(`SELECT private_key FROM did_v2_documents WHERE did = ?`)
|
|
457
|
+
.get(issuerDid);
|
|
458
|
+
if (!issuerKeyRow?.private_key) {
|
|
459
|
+
throw new Error(`issuer has no signing key: ${issuerDid}`);
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
const id = `urn:uuid:${_uuid()}`;
|
|
463
|
+
const issuanceDate = _now();
|
|
464
|
+
const expirationDate = expiresInMs ? issuanceDate + expiresInMs : null;
|
|
465
|
+
|
|
466
|
+
const vc = {
|
|
467
|
+
"@context": [
|
|
468
|
+
"https://www.w3.org/2018/credentials/v1",
|
|
469
|
+
"https://w3id.org/security/suites/ed25519-2020/v1",
|
|
470
|
+
],
|
|
471
|
+
id,
|
|
472
|
+
type: ["VerifiableCredential", type],
|
|
473
|
+
issuer: issuerDid,
|
|
474
|
+
credentialSubject: { id: holderDid, ...credentialSubject },
|
|
475
|
+
issuanceDate: new Date(issuanceDate).toISOString(),
|
|
476
|
+
expirationDate: expirationDate
|
|
477
|
+
? new Date(expirationDate).toISOString()
|
|
478
|
+
: undefined,
|
|
479
|
+
};
|
|
480
|
+
const signature = _signCanonical(issuerKeyRow.private_key, vc);
|
|
481
|
+
const proof = {
|
|
482
|
+
type: "Ed25519Signature2020",
|
|
483
|
+
created: new Date(issuanceDate).toISOString(),
|
|
484
|
+
verificationMethod: `${issuerDid}#keys-1`,
|
|
485
|
+
proofPurpose: "assertionMethod",
|
|
486
|
+
proofValue: signature,
|
|
487
|
+
};
|
|
488
|
+
|
|
489
|
+
db.prepare(
|
|
490
|
+
`INSERT INTO did_v2_credentials
|
|
491
|
+
(id, holder_did, issuer_did, type, credential_subject, proof,
|
|
492
|
+
issuance_date, expiration_date, status, created_at)
|
|
493
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
494
|
+
).run(
|
|
495
|
+
id,
|
|
496
|
+
holderDid,
|
|
497
|
+
issuerDid,
|
|
498
|
+
type,
|
|
499
|
+
JSON.stringify(credentialSubject),
|
|
500
|
+
JSON.stringify(proof),
|
|
501
|
+
issuanceDate,
|
|
502
|
+
expirationDate,
|
|
503
|
+
CREDENTIAL_STATUS.ACTIVE,
|
|
504
|
+
issuanceDate,
|
|
505
|
+
);
|
|
506
|
+
|
|
507
|
+
return { ...vc, proof, _id: id };
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
export function getCredential(db, id) {
|
|
511
|
+
const row = db
|
|
512
|
+
.prepare(`SELECT * FROM did_v2_credentials WHERE id = ?`)
|
|
513
|
+
.get(id);
|
|
514
|
+
return _rowToCredential(row);
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
export function listCredentials(db, { holderDid, issuerDid, status } = {}) {
|
|
518
|
+
const wheres = [];
|
|
519
|
+
const params = [];
|
|
520
|
+
if (holderDid) {
|
|
521
|
+
wheres.push(`holder_did = ?`);
|
|
522
|
+
params.push(holderDid);
|
|
523
|
+
}
|
|
524
|
+
if (issuerDid) {
|
|
525
|
+
wheres.push(`issuer_did = ?`);
|
|
526
|
+
params.push(issuerDid);
|
|
527
|
+
}
|
|
528
|
+
if (status) {
|
|
529
|
+
wheres.push(`status = ?`);
|
|
530
|
+
params.push(status);
|
|
531
|
+
}
|
|
532
|
+
const sql = `SELECT * FROM did_v2_credentials ${
|
|
533
|
+
wheres.length ? "WHERE " + wheres.join(" AND ") : ""
|
|
534
|
+
} ORDER BY issuance_date DESC`;
|
|
535
|
+
return db
|
|
536
|
+
.prepare(sql)
|
|
537
|
+
.all(...params)
|
|
538
|
+
.map(_rowToCredential);
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
export function revokeCredential(db, id, reason) {
|
|
542
|
+
const res = db
|
|
543
|
+
.prepare(
|
|
544
|
+
`UPDATE did_v2_credentials SET status = ?, revocation_reason = ? WHERE id = ?`,
|
|
545
|
+
)
|
|
546
|
+
.run(CREDENTIAL_STATUS.REVOKED, reason || null, id);
|
|
547
|
+
return res.changes > 0;
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
/* ── Verifiable Presentations ────────────────────────────── */
|
|
551
|
+
|
|
552
|
+
export function createPresentation(
|
|
553
|
+
db,
|
|
554
|
+
{
|
|
555
|
+
holderDid,
|
|
556
|
+
credentialIds = [],
|
|
557
|
+
recipientDid,
|
|
558
|
+
disclosedFields = [],
|
|
559
|
+
ttlMs = VP_DEFAULT_TTL_MS,
|
|
560
|
+
zkpEnabled = false,
|
|
561
|
+
} = {},
|
|
562
|
+
) {
|
|
563
|
+
if (!holderDid) throw new Error("holderDid required");
|
|
564
|
+
if (!Array.isArray(credentialIds) || credentialIds.length === 0) {
|
|
565
|
+
throw new Error("credentialIds must be a non-empty array");
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
const holder = resolveDID(db, holderDid);
|
|
569
|
+
if (!holder) throw new Error(`holder not found: ${holderDid}`);
|
|
570
|
+
|
|
571
|
+
const holderKeyRow = db
|
|
572
|
+
.prepare(`SELECT private_key FROM did_v2_documents WHERE did = ?`)
|
|
573
|
+
.get(holderDid);
|
|
574
|
+
if (!holderKeyRow?.private_key) {
|
|
575
|
+
throw new Error(`holder has no signing key: ${holderDid}`);
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
// Verify all credentials belong to holder and are active
|
|
579
|
+
const creds = [];
|
|
580
|
+
for (const cid of credentialIds) {
|
|
581
|
+
const row = db
|
|
582
|
+
.prepare(`SELECT * FROM did_v2_credentials WHERE id = ?`)
|
|
583
|
+
.get(cid);
|
|
584
|
+
if (!row) throw new Error(`credential not found: ${cid}`);
|
|
585
|
+
if (row.holder_did !== holderDid) {
|
|
586
|
+
throw new Error(
|
|
587
|
+
`credential ${cid} belongs to ${row.holder_did}, not ${holderDid}`,
|
|
588
|
+
);
|
|
589
|
+
}
|
|
590
|
+
if (row.status !== CREDENTIAL_STATUS.ACTIVE) {
|
|
591
|
+
throw new Error(`credential ${cid} is ${row.status}, not active`);
|
|
592
|
+
}
|
|
593
|
+
if (row.expiration_date && row.expiration_date < _now()) {
|
|
594
|
+
throw new Error(`credential ${cid} is expired`);
|
|
595
|
+
}
|
|
596
|
+
creds.push(row);
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
const id = `urn:uuid:${_uuid()}`;
|
|
600
|
+
const now = _now();
|
|
601
|
+
const expiresAt = now + ttlMs;
|
|
602
|
+
|
|
603
|
+
// Build payload for signing (selective disclosure just echoes which
|
|
604
|
+
// fields are disclosed — the CLI does not redact claims in storage)
|
|
605
|
+
const payload = {
|
|
606
|
+
id,
|
|
607
|
+
type: ["VerifiablePresentation"],
|
|
608
|
+
holder: holderDid,
|
|
609
|
+
verifiableCredential: creds.map((c) => ({
|
|
610
|
+
id: c.id,
|
|
611
|
+
type: c.type,
|
|
612
|
+
issuer: c.issuer_did,
|
|
613
|
+
})),
|
|
614
|
+
recipient: recipientDid || null,
|
|
615
|
+
disclosedFields,
|
|
616
|
+
created: now,
|
|
617
|
+
expires: expiresAt,
|
|
618
|
+
};
|
|
619
|
+
const signature = _signCanonical(holderKeyRow.private_key, payload);
|
|
620
|
+
const proof = {
|
|
621
|
+
type: "Ed25519Signature2020",
|
|
622
|
+
created: new Date(now).toISOString(),
|
|
623
|
+
verificationMethod: `${holderDid}#keys-1`,
|
|
624
|
+
proofPurpose: "authentication",
|
|
625
|
+
proofValue: signature,
|
|
626
|
+
};
|
|
627
|
+
const zkpProofId = zkpEnabled ? `zkp:${_uuid()}` : null;
|
|
628
|
+
|
|
629
|
+
db.prepare(
|
|
630
|
+
`INSERT INTO did_v2_presentations
|
|
631
|
+
(id, holder_did, recipient_did, credential_ids, disclosed_fields,
|
|
632
|
+
proof, zkp_proof_id, verified, verification_time_ms,
|
|
633
|
+
created_at, expires_at)
|
|
634
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
635
|
+
).run(
|
|
636
|
+
id,
|
|
637
|
+
holderDid,
|
|
638
|
+
recipientDid || null,
|
|
639
|
+
JSON.stringify(credentialIds),
|
|
640
|
+
JSON.stringify(disclosedFields),
|
|
641
|
+
JSON.stringify(proof),
|
|
642
|
+
zkpProofId,
|
|
643
|
+
0,
|
|
644
|
+
null,
|
|
645
|
+
now,
|
|
646
|
+
expiresAt,
|
|
647
|
+
);
|
|
648
|
+
|
|
649
|
+
return {
|
|
650
|
+
id,
|
|
651
|
+
holderDid,
|
|
652
|
+
recipientDid: recipientDid || null,
|
|
653
|
+
credentialIds,
|
|
654
|
+
disclosedFields,
|
|
655
|
+
proof,
|
|
656
|
+
zkpProofId,
|
|
657
|
+
expiresAt,
|
|
658
|
+
};
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
export function getPresentation(db, id) {
|
|
662
|
+
const row = db
|
|
663
|
+
.prepare(`SELECT * FROM did_v2_presentations WHERE id = ?`)
|
|
664
|
+
.get(id);
|
|
665
|
+
return _rowToPresentation(row);
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
export function listPresentations(db, { holderDid } = {}) {
|
|
669
|
+
const wheres = [];
|
|
670
|
+
const params = [];
|
|
671
|
+
if (holderDid) {
|
|
672
|
+
wheres.push(`holder_did = ?`);
|
|
673
|
+
params.push(holderDid);
|
|
674
|
+
}
|
|
675
|
+
const sql = `SELECT * FROM did_v2_presentations ${
|
|
676
|
+
wheres.length ? "WHERE " + wheres.join(" AND ") : ""
|
|
677
|
+
} ORDER BY created_at DESC`;
|
|
678
|
+
return db
|
|
679
|
+
.prepare(sql)
|
|
680
|
+
.all(...params)
|
|
681
|
+
.map(_rowToPresentation);
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
export function verifyPresentation(db, id) {
|
|
685
|
+
const start = _now();
|
|
686
|
+
const row = db
|
|
687
|
+
.prepare(`SELECT * FROM did_v2_presentations WHERE id = ?`)
|
|
688
|
+
.get(id);
|
|
689
|
+
if (!row) {
|
|
690
|
+
return { ok: false, reason: "not_found" };
|
|
691
|
+
}
|
|
692
|
+
if (row.expires_at < start) {
|
|
693
|
+
return { ok: false, reason: "expired", verificationTimeMs: 0 };
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
const holder = resolveDID(db, row.holder_did);
|
|
697
|
+
if (!holder) return { ok: false, reason: "holder_missing" };
|
|
698
|
+
|
|
699
|
+
const credIds = _parseJSON(row.credential_ids, []);
|
|
700
|
+
for (const cid of credIds) {
|
|
701
|
+
const c = db
|
|
702
|
+
.prepare(
|
|
703
|
+
`SELECT status, expiration_date FROM did_v2_credentials WHERE id = ?`,
|
|
704
|
+
)
|
|
705
|
+
.get(cid);
|
|
706
|
+
if (!c) return { ok: false, reason: `credential_missing:${cid}` };
|
|
707
|
+
if (c.status !== CREDENTIAL_STATUS.ACTIVE) {
|
|
708
|
+
return { ok: false, reason: `credential_${c.status}:${cid}` };
|
|
709
|
+
}
|
|
710
|
+
if (c.expiration_date && c.expiration_date < start) {
|
|
711
|
+
return { ok: false, reason: `credential_expired:${cid}` };
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
// Re-derive signing payload from stored fields
|
|
716
|
+
const proof = _parseJSON(row.proof, {});
|
|
717
|
+
const creds = credIds.map((cid) => {
|
|
718
|
+
const c = db
|
|
719
|
+
.prepare(
|
|
720
|
+
`SELECT id, type, issuer_did FROM did_v2_credentials WHERE id = ?`,
|
|
721
|
+
)
|
|
722
|
+
.get(cid);
|
|
723
|
+
return { id: c.id, type: c.type, issuer: c.issuer_did };
|
|
724
|
+
});
|
|
725
|
+
const payload = {
|
|
726
|
+
id: row.id,
|
|
727
|
+
type: ["VerifiablePresentation"],
|
|
728
|
+
holder: row.holder_did,
|
|
729
|
+
verifiableCredential: creds,
|
|
730
|
+
recipient: row.recipient_did || null,
|
|
731
|
+
disclosedFields: _parseJSON(row.disclosed_fields, []),
|
|
732
|
+
created: row.created_at,
|
|
733
|
+
expires: row.expires_at,
|
|
734
|
+
};
|
|
735
|
+
const ok = _verifyCanonical(holder.publicKey, payload, proof.proofValue);
|
|
736
|
+
const verificationTimeMs = _now() - start;
|
|
737
|
+
|
|
738
|
+
db.prepare(
|
|
739
|
+
`UPDATE did_v2_presentations SET verified = ?, verification_time_ms = ? WHERE id = ?`,
|
|
740
|
+
).run(ok ? 1 : 0, verificationTimeMs, id);
|
|
741
|
+
|
|
742
|
+
return {
|
|
743
|
+
ok,
|
|
744
|
+
reason: ok ? null : "bad_signature",
|
|
745
|
+
verificationTimeMs,
|
|
746
|
+
};
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
/* ── Social recovery ─────────────────────────────────────── */
|
|
750
|
+
|
|
751
|
+
export function startRecovery(db, { did, shares = [] } = {}) {
|
|
752
|
+
if (!did) throw new Error("did required");
|
|
753
|
+
if (!Array.isArray(shares) || shares.length === 0) {
|
|
754
|
+
throw new Error("shares must be a non-empty array");
|
|
755
|
+
}
|
|
756
|
+
const row = db
|
|
757
|
+
.prepare(`SELECT * FROM did_v2_documents WHERE did = ?`)
|
|
758
|
+
.get(did);
|
|
759
|
+
if (!row) throw new Error(`did not found: ${did}`);
|
|
760
|
+
|
|
761
|
+
const guardians = _parseJSON(row.recovery_guardians, []);
|
|
762
|
+
const threshold = row.recovery_threshold;
|
|
763
|
+
|
|
764
|
+
// Validate shares — each must include guardian identifier matching the DID's guardian list
|
|
765
|
+
const validShares = shares.filter(
|
|
766
|
+
(s) =>
|
|
767
|
+
s &&
|
|
768
|
+
typeof s === "object" &&
|
|
769
|
+
s.guardian &&
|
|
770
|
+
guardians.includes(s.guardian) &&
|
|
771
|
+
s.share,
|
|
772
|
+
);
|
|
773
|
+
|
|
774
|
+
const status =
|
|
775
|
+
validShares.length >= threshold
|
|
776
|
+
? RECOVERY_STATUS.THRESHOLD_MET
|
|
777
|
+
: RECOVERY_STATUS.PENDING;
|
|
778
|
+
|
|
779
|
+
const id = _uuid();
|
|
780
|
+
const now = _now();
|
|
781
|
+
db.prepare(
|
|
782
|
+
`INSERT INTO did_v2_recovery_attempts
|
|
783
|
+
(id, did, guardians, shares_submitted, threshold, status, created_at)
|
|
784
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)`,
|
|
785
|
+
).run(
|
|
786
|
+
id,
|
|
787
|
+
did,
|
|
788
|
+
JSON.stringify(guardians),
|
|
789
|
+
JSON.stringify(validShares),
|
|
790
|
+
threshold,
|
|
791
|
+
status,
|
|
792
|
+
now,
|
|
793
|
+
);
|
|
794
|
+
|
|
795
|
+
return {
|
|
796
|
+
id,
|
|
797
|
+
did,
|
|
798
|
+
status,
|
|
799
|
+
validShares: validShares.length,
|
|
800
|
+
threshold,
|
|
801
|
+
};
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
export function completeRecovery(db, recoveryId) {
|
|
805
|
+
const row = db
|
|
806
|
+
.prepare(`SELECT * FROM did_v2_recovery_attempts WHERE id = ?`)
|
|
807
|
+
.get(recoveryId);
|
|
808
|
+
if (!row) throw new Error(`recovery not found: ${recoveryId}`);
|
|
809
|
+
if (row.status !== RECOVERY_STATUS.THRESHOLD_MET) {
|
|
810
|
+
throw new Error(`recovery ${recoveryId} not ready: status=${row.status}`);
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
const { publicKey, privateKey } = _generateEd25519();
|
|
814
|
+
const didRow = db
|
|
815
|
+
.prepare(`SELECT * FROM did_v2_documents WHERE did = ?`)
|
|
816
|
+
.get(row.did);
|
|
817
|
+
if (!didRow) {
|
|
818
|
+
db.prepare(
|
|
819
|
+
`UPDATE did_v2_recovery_attempts SET status = ?, completed_at = ? WHERE id = ?`,
|
|
820
|
+
).run(RECOVERY_STATUS.FAILED, _now(), recoveryId);
|
|
821
|
+
throw new Error(`did disappeared during recovery: ${row.did}`);
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
const doc = _buildDocument(
|
|
825
|
+
row.did,
|
|
826
|
+
didRow.method,
|
|
827
|
+
publicKey,
|
|
828
|
+
_parseJSON(didRow.service_endpoints, []),
|
|
829
|
+
);
|
|
830
|
+
|
|
831
|
+
const now = _now();
|
|
832
|
+
db.prepare(
|
|
833
|
+
`UPDATE did_v2_documents SET public_key = ?, private_key = ?, document = ?, authentication = ?, updated_at = ? WHERE did = ?`,
|
|
834
|
+
).run(
|
|
835
|
+
publicKey,
|
|
836
|
+
privateKey,
|
|
837
|
+
JSON.stringify(doc),
|
|
838
|
+
JSON.stringify(doc.authentication || []),
|
|
839
|
+
now,
|
|
840
|
+
row.did,
|
|
841
|
+
);
|
|
842
|
+
db.prepare(
|
|
843
|
+
`UPDATE did_v2_recovery_attempts SET status = ?, new_public_key = ?, completed_at = ? WHERE id = ?`,
|
|
844
|
+
).run(RECOVERY_STATUS.RECOVERED, publicKey, now, recoveryId);
|
|
845
|
+
|
|
846
|
+
return { did: row.did, newPublicKey: publicKey, privateKey };
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
export function getRecovery(db, recoveryId) {
|
|
850
|
+
const row = db
|
|
851
|
+
.prepare(`SELECT * FROM did_v2_recovery_attempts WHERE id = ?`)
|
|
852
|
+
.get(recoveryId);
|
|
853
|
+
return _rowToRecovery(row);
|
|
854
|
+
}
|
|
855
|
+
|
|
856
|
+
export function listRecoveries(db, { did } = {}) {
|
|
857
|
+
const wheres = [];
|
|
858
|
+
const params = [];
|
|
859
|
+
if (did) {
|
|
860
|
+
wheres.push(`did = ?`);
|
|
861
|
+
params.push(did);
|
|
862
|
+
}
|
|
863
|
+
const sql = `SELECT * FROM did_v2_recovery_attempts ${
|
|
864
|
+
wheres.length ? "WHERE " + wheres.join(" AND ") : ""
|
|
865
|
+
} ORDER BY created_at DESC`;
|
|
866
|
+
return db
|
|
867
|
+
.prepare(sql)
|
|
868
|
+
.all(...params)
|
|
869
|
+
.map(_rowToRecovery);
|
|
870
|
+
}
|
|
871
|
+
|
|
872
|
+
/* ── Identity roaming ────────────────────────────────────── */
|
|
873
|
+
|
|
874
|
+
export function roamIdentity(
|
|
875
|
+
db,
|
|
876
|
+
{ did, targetPlatform, sourcePlatform, migrationProof } = {},
|
|
877
|
+
) {
|
|
878
|
+
if (!did) throw new Error("did required");
|
|
879
|
+
if (!targetPlatform) throw new Error("targetPlatform required");
|
|
880
|
+
|
|
881
|
+
const didRow = db
|
|
882
|
+
.prepare(`SELECT * FROM did_v2_documents WHERE did = ?`)
|
|
883
|
+
.get(did);
|
|
884
|
+
if (!didRow) throw new Error(`did not found: ${did}`);
|
|
885
|
+
|
|
886
|
+
const creds = db
|
|
887
|
+
.prepare(
|
|
888
|
+
`SELECT COUNT(*) as cnt FROM did_v2_credentials WHERE holder_did = ? AND status = ?`,
|
|
889
|
+
)
|
|
890
|
+
.get(did, CREDENTIAL_STATUS.ACTIVE);
|
|
891
|
+
|
|
892
|
+
const credentialsMigrated = creds?.cnt || 0;
|
|
893
|
+
const reputationTransferred = didRow.reputation_score || 0.0;
|
|
894
|
+
|
|
895
|
+
const id = _uuid();
|
|
896
|
+
const now = _now();
|
|
897
|
+
db.prepare(
|
|
898
|
+
`INSERT INTO did_v2_roaming_log
|
|
899
|
+
(id, did, source_platform, target_platform, migration_proof,
|
|
900
|
+
credentials_migrated, reputation_transferred, created_at)
|
|
901
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
902
|
+
).run(
|
|
903
|
+
id,
|
|
904
|
+
did,
|
|
905
|
+
sourcePlatform || null,
|
|
906
|
+
targetPlatform,
|
|
907
|
+
migrationProof || null,
|
|
908
|
+
credentialsMigrated,
|
|
909
|
+
reputationTransferred,
|
|
910
|
+
now,
|
|
911
|
+
);
|
|
912
|
+
|
|
913
|
+
db.prepare(
|
|
914
|
+
`UPDATE did_v2_documents SET status = ?, updated_at = ? WHERE did = ?`,
|
|
915
|
+
).run(DID_STATUS.ROAMED, now, did);
|
|
916
|
+
|
|
917
|
+
return {
|
|
918
|
+
id,
|
|
919
|
+
did,
|
|
920
|
+
sourcePlatform: sourcePlatform || null,
|
|
921
|
+
targetPlatform,
|
|
922
|
+
credentialsMigrated,
|
|
923
|
+
reputationTransferred,
|
|
924
|
+
};
|
|
925
|
+
}
|
|
926
|
+
|
|
927
|
+
export function listRoamingLog(db, { did } = {}) {
|
|
928
|
+
const wheres = [];
|
|
929
|
+
const params = [];
|
|
930
|
+
if (did) {
|
|
931
|
+
wheres.push(`did = ?`);
|
|
932
|
+
params.push(did);
|
|
933
|
+
}
|
|
934
|
+
const sql = `SELECT * FROM did_v2_roaming_log ${
|
|
935
|
+
wheres.length ? "WHERE " + wheres.join(" AND ") : ""
|
|
936
|
+
} ORDER BY created_at DESC`;
|
|
937
|
+
return db
|
|
938
|
+
.prepare(sql)
|
|
939
|
+
.all(...params)
|
|
940
|
+
.map(_rowToRoamingEntry);
|
|
941
|
+
}
|
|
942
|
+
|
|
943
|
+
/* ── Reputation aggregation ──────────────────────────────── */
|
|
944
|
+
|
|
945
|
+
export function recordReputationSource(
|
|
946
|
+
db,
|
|
947
|
+
{ did, source, score, evidence } = {},
|
|
948
|
+
) {
|
|
949
|
+
if (!did) throw new Error("did required");
|
|
950
|
+
if (!source) throw new Error("source required");
|
|
951
|
+
if (typeof score !== "number") throw new Error("score must be a number");
|
|
952
|
+
|
|
953
|
+
const weight = REPUTATION_SOURCE_WEIGHTS[source] ?? 1.0;
|
|
954
|
+
const id = _uuid();
|
|
955
|
+
db.prepare(
|
|
956
|
+
`INSERT INTO did_v2_reputation_sources
|
|
957
|
+
(id, did, source, score, weight, evidence, recorded_at)
|
|
958
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)`,
|
|
959
|
+
).run(
|
|
960
|
+
id,
|
|
961
|
+
did,
|
|
962
|
+
source,
|
|
963
|
+
score,
|
|
964
|
+
weight,
|
|
965
|
+
evidence ? JSON.stringify(evidence) : null,
|
|
966
|
+
_now(),
|
|
967
|
+
);
|
|
968
|
+
return { id, weight };
|
|
969
|
+
}
|
|
970
|
+
|
|
971
|
+
export function aggregateReputation(db, did, { sources } = {}) {
|
|
972
|
+
if (!did) throw new Error("did required");
|
|
973
|
+
|
|
974
|
+
const wheres = [`did = ?`];
|
|
975
|
+
const params = [did];
|
|
976
|
+
if (Array.isArray(sources) && sources.length > 0) {
|
|
977
|
+
// Use multiple OR conditions via parameterized literals —
|
|
978
|
+
// MockDB supports one "= ?" per condition, so filter in JS instead
|
|
979
|
+
}
|
|
980
|
+
const rows = db
|
|
981
|
+
.prepare(
|
|
982
|
+
`SELECT * FROM did_v2_reputation_sources ${
|
|
983
|
+
wheres.length ? "WHERE " + wheres.join(" AND ") : ""
|
|
984
|
+
}`,
|
|
985
|
+
)
|
|
986
|
+
.all(...params);
|
|
987
|
+
|
|
988
|
+
const filtered =
|
|
989
|
+
Array.isArray(sources) && sources.length > 0
|
|
990
|
+
? rows.filter((r) => sources.includes(r.source))
|
|
991
|
+
: rows;
|
|
992
|
+
|
|
993
|
+
if (filtered.length === 0) {
|
|
994
|
+
return { did, aggregatedScore: 0.0, sourceCount: 0, sources: [] };
|
|
995
|
+
}
|
|
996
|
+
|
|
997
|
+
const numer = filtered.reduce((s, r) => s + r.score * r.weight, 0);
|
|
998
|
+
const denom = filtered.reduce((s, r) => s + r.weight, 0);
|
|
999
|
+
const aggregated = denom > 0 ? numer / denom : 0.0;
|
|
1000
|
+
|
|
1001
|
+
db.prepare(
|
|
1002
|
+
`UPDATE did_v2_documents SET reputation_score = ?, updated_at = ? WHERE did = ?`,
|
|
1003
|
+
).run(aggregated, _now(), did);
|
|
1004
|
+
|
|
1005
|
+
const bySource = new Map();
|
|
1006
|
+
for (const r of filtered) {
|
|
1007
|
+
if (!bySource.has(r.source)) {
|
|
1008
|
+
bySource.set(r.source, {
|
|
1009
|
+
source: r.source,
|
|
1010
|
+
score: 0,
|
|
1011
|
+
weight: r.weight,
|
|
1012
|
+
count: 0,
|
|
1013
|
+
});
|
|
1014
|
+
}
|
|
1015
|
+
const entry = bySource.get(r.source);
|
|
1016
|
+
entry.score += r.score;
|
|
1017
|
+
entry.count += 1;
|
|
1018
|
+
}
|
|
1019
|
+
const sourceBreakdown = [...bySource.values()].map((e) => ({
|
|
1020
|
+
source: e.source,
|
|
1021
|
+
avgScore: e.count > 0 ? e.score / e.count : 0,
|
|
1022
|
+
weight: e.weight,
|
|
1023
|
+
sampleCount: e.count,
|
|
1024
|
+
}));
|
|
1025
|
+
|
|
1026
|
+
return {
|
|
1027
|
+
did,
|
|
1028
|
+
aggregatedScore: aggregated,
|
|
1029
|
+
sourceCount: filtered.length,
|
|
1030
|
+
sources: sourceBreakdown,
|
|
1031
|
+
};
|
|
1032
|
+
}
|
|
1033
|
+
|
|
1034
|
+
/* ── Export ──────────────────────────────────────────────── */
|
|
1035
|
+
|
|
1036
|
+
export function exportDID(db, did, { format = "json-ld" } = {}) {
|
|
1037
|
+
const row = db
|
|
1038
|
+
.prepare(`SELECT * FROM did_v2_documents WHERE did = ?`)
|
|
1039
|
+
.get(did);
|
|
1040
|
+
if (!row) throw new Error(`did not found: ${did}`);
|
|
1041
|
+
|
|
1042
|
+
const document = _parseJSON(row.document, {});
|
|
1043
|
+
const creds = db
|
|
1044
|
+
.prepare(`SELECT * FROM did_v2_credentials WHERE holder_did = ?`)
|
|
1045
|
+
.all(did)
|
|
1046
|
+
.map(_rowToCredential);
|
|
1047
|
+
|
|
1048
|
+
if (format === "jwt") {
|
|
1049
|
+
// Minimal JWT-style packaging (header.payload.signature) —
|
|
1050
|
+
// not a real JWS, signed with DID's Ed25519 key as payload hash
|
|
1051
|
+
const header = { alg: "EdDSA", typ: "DIDv2+JWT" };
|
|
1052
|
+
const payload = { did, document, credentials: creds };
|
|
1053
|
+
const h = Buffer.from(JSON.stringify(header)).toString("base64url");
|
|
1054
|
+
const p = Buffer.from(JSON.stringify(payload)).toString("base64url");
|
|
1055
|
+
const sig = row.private_key
|
|
1056
|
+
? _signCanonical(row.private_key, { h, p })
|
|
1057
|
+
: "";
|
|
1058
|
+
return { format: "jwt", token: `${h}.${p}.${sig}` };
|
|
1059
|
+
}
|
|
1060
|
+
|
|
1061
|
+
return {
|
|
1062
|
+
format: "json-ld",
|
|
1063
|
+
document,
|
|
1064
|
+
credentials: creds,
|
|
1065
|
+
reputationScore: row.reputation_score,
|
|
1066
|
+
status: row.status,
|
|
1067
|
+
};
|
|
1068
|
+
}
|
|
1069
|
+
|
|
1070
|
+
/* ── Stats ───────────────────────────────────────────────── */
|
|
1071
|
+
|
|
1072
|
+
export function getStats(db) {
|
|
1073
|
+
const didCount =
|
|
1074
|
+
db.prepare(`SELECT COUNT(*) as cnt FROM did_v2_documents`).get()?.cnt || 0;
|
|
1075
|
+
const activeDIDs =
|
|
1076
|
+
db
|
|
1077
|
+
.prepare(`SELECT COUNT(*) as cnt FROM did_v2_documents WHERE status = ?`)
|
|
1078
|
+
.get(DID_STATUS.ACTIVE)?.cnt || 0;
|
|
1079
|
+
const credCount =
|
|
1080
|
+
db.prepare(`SELECT COUNT(*) as cnt FROM did_v2_credentials`).get()?.cnt ||
|
|
1081
|
+
0;
|
|
1082
|
+
const activeCreds =
|
|
1083
|
+
db
|
|
1084
|
+
.prepare(
|
|
1085
|
+
`SELECT COUNT(*) as cnt FROM did_v2_credentials WHERE status = ?`,
|
|
1086
|
+
)
|
|
1087
|
+
.get(CREDENTIAL_STATUS.ACTIVE)?.cnt || 0;
|
|
1088
|
+
const presentationCount =
|
|
1089
|
+
db.prepare(`SELECT COUNT(*) as cnt FROM did_v2_presentations`).get()?.cnt ||
|
|
1090
|
+
0;
|
|
1091
|
+
const verifiedPresentations =
|
|
1092
|
+
db
|
|
1093
|
+
.prepare(
|
|
1094
|
+
`SELECT COUNT(*) as cnt FROM did_v2_presentations WHERE verified = 1`,
|
|
1095
|
+
)
|
|
1096
|
+
.get()?.cnt || 0;
|
|
1097
|
+
const recoveryCount =
|
|
1098
|
+
db.prepare(`SELECT COUNT(*) as cnt FROM did_v2_recovery_attempts`).get()
|
|
1099
|
+
?.cnt || 0;
|
|
1100
|
+
const roamingCount =
|
|
1101
|
+
db.prepare(`SELECT COUNT(*) as cnt FROM did_v2_roaming_log`).get()?.cnt ||
|
|
1102
|
+
0;
|
|
1103
|
+
|
|
1104
|
+
return {
|
|
1105
|
+
didCount,
|
|
1106
|
+
activeDIDs,
|
|
1107
|
+
credentialCount: credCount,
|
|
1108
|
+
activeCredentials: activeCreds,
|
|
1109
|
+
presentationCount,
|
|
1110
|
+
verifiedPresentations,
|
|
1111
|
+
recoveryCount,
|
|
1112
|
+
roamingCount,
|
|
1113
|
+
};
|
|
1114
|
+
}
|
|
1115
|
+
|
|
1116
|
+
export function getConfig() {
|
|
1117
|
+
return {
|
|
1118
|
+
methods: Object.values(DID_METHOD),
|
|
1119
|
+
credentialStatuses: Object.values(CREDENTIAL_STATUS),
|
|
1120
|
+
recoveryStatuses: Object.values(RECOVERY_STATUS),
|
|
1121
|
+
didStatuses: Object.values(DID_STATUS),
|
|
1122
|
+
reputationSourceWeights: { ...REPUTATION_SOURCE_WEIGHTS },
|
|
1123
|
+
defaultRecoveryThreshold: DEFAULT_RECOVERY_THRESHOLD,
|
|
1124
|
+
defaultGuardianCount: DEFAULT_GUARDIAN_COUNT,
|
|
1125
|
+
vpDefaultTTLMs: VP_DEFAULT_TTL_MS,
|
|
1126
|
+
};
|
|
1127
|
+
}
|