chainlesschain 0.47.9 → 0.49.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.
Files changed (70) hide show
  1. package/bin/chainlesschain.js +0 -0
  2. package/package.json +1 -1
  3. package/src/assets/web-panel/.build-hash +1 -1
  4. package/src/assets/web-panel/assets/{AppLayout-6SPt_8Y_.js → AppLayout-Rvi759IS.js} +1 -1
  5. package/src/assets/web-panel/assets/Dashboard-BS-tzGNj.css +1 -0
  6. package/src/assets/web-panel/assets/{Dashboard-Br7kCwKJ.js → Dashboard-DBhFxXYQ.js} +2 -2
  7. package/src/assets/web-panel/assets/{index-tN-8TosE.js → index-uL0cZ8N_.js} +2 -2
  8. package/src/assets/web-panel/index.html +2 -2
  9. package/src/commands/codegen.js +303 -0
  10. package/src/commands/collab.js +482 -0
  11. package/src/commands/crosschain.js +382 -0
  12. package/src/commands/dbevo.js +388 -0
  13. package/src/commands/dev.js +411 -0
  14. package/src/commands/federation.js +427 -0
  15. package/src/commands/fusion.js +332 -0
  16. package/src/commands/governance.js +505 -0
  17. package/src/commands/hardening.js +110 -0
  18. package/src/commands/incentive.js +373 -0
  19. package/src/commands/inference.js +304 -0
  20. package/src/commands/infra.js +361 -0
  21. package/src/commands/kg.js +371 -0
  22. package/src/commands/marketplace.js +326 -0
  23. package/src/commands/mcp.js +97 -18
  24. package/src/commands/nlprog.js +329 -0
  25. package/src/commands/ops.js +408 -0
  26. package/src/commands/perception.js +385 -0
  27. package/src/commands/pqc.js +34 -0
  28. package/src/commands/privacy.js +345 -0
  29. package/src/commands/quantization.js +280 -0
  30. package/src/commands/recommend.js +336 -0
  31. package/src/commands/reputation.js +349 -0
  32. package/src/commands/runtime.js +500 -0
  33. package/src/commands/sla.js +352 -0
  34. package/src/commands/stress.js +252 -0
  35. package/src/commands/tech.js +268 -0
  36. package/src/commands/tenant.js +576 -0
  37. package/src/commands/trust.js +366 -0
  38. package/src/harness/mcp-client.js +330 -54
  39. package/src/index.js +112 -0
  40. package/src/lib/aiops.js +523 -0
  41. package/src/lib/autonomous-developer.js +524 -0
  42. package/src/lib/code-agent.js +442 -0
  43. package/src/lib/collaboration-governance.js +556 -0
  44. package/src/lib/community-governance.js +649 -0
  45. package/src/lib/content-recommendation.js +600 -0
  46. package/src/lib/cross-chain.js +669 -0
  47. package/src/lib/dbevo.js +669 -0
  48. package/src/lib/decentral-infra.js +445 -0
  49. package/src/lib/federation-hardening.js +587 -0
  50. package/src/lib/hardening-manager.js +409 -0
  51. package/src/lib/inference-network.js +407 -0
  52. package/src/lib/knowledge-graph.js +530 -0
  53. package/src/lib/mcp-client.js +3 -0
  54. package/src/lib/multimodal.js +698 -0
  55. package/src/lib/nl-programming.js +595 -0
  56. package/src/lib/perception.js +500 -0
  57. package/src/lib/pqc-manager.js +141 -9
  58. package/src/lib/privacy-computing.js +575 -0
  59. package/src/lib/protocol-fusion.js +535 -0
  60. package/src/lib/quantization.js +362 -0
  61. package/src/lib/reputation-optimizer.js +509 -0
  62. package/src/lib/skill-marketplace.js +397 -0
  63. package/src/lib/sla-manager.js +484 -0
  64. package/src/lib/stress-tester.js +383 -0
  65. package/src/lib/tech-learning-engine.js +651 -0
  66. package/src/lib/tenant-saas.js +831 -0
  67. package/src/lib/token-incentive.js +513 -0
  68. package/src/lib/trust-security.js +473 -0
  69. package/src/lib/universal-runtime.js +771 -0
  70. package/src/assets/web-panel/assets/Dashboard-CKeMmCoT.css +0 -1
@@ -0,0 +1,500 @@
1
+ /**
2
+ * Multimodal Perception Engine — CLI port of Phase 84
3
+ * (docs/design/modules/49_多模态感知层.md).
4
+ *
5
+ * Desktop uses real-time screen capture, ASR/TTS voice sessions,
6
+ * document parsers (PDF/Word/Excel), video keyframe extraction,
7
+ * and cross-modal vector search.
8
+ * CLI port ships:
9
+ *
10
+ * - Perception result recording (screen/voice/document/video)
11
+ * - Voice session lifecycle (idle → listening → processing → speaking)
12
+ * - Multimodal index entries with content summaries and tags
13
+ * - Cross-modal query simulation (keyword matching on summaries)
14
+ * - Stats and context aggregation
15
+ *
16
+ * What does NOT port: real screen capture (Sharp/Tesseract), ASR/TTS
17
+ * streaming, document binary parsing, video keyframe extraction,
18
+ * vector embeddings, real-time VAD.
19
+ */
20
+
21
+ import crypto from "crypto";
22
+
23
+ /* ── Constants ──────────────────────────────────────────── */
24
+
25
+ export const MODALITY = Object.freeze({
26
+ SCREEN: "screen",
27
+ VOICE: "voice",
28
+ DOCUMENT: "document",
29
+ VIDEO: "video",
30
+ });
31
+
32
+ export const VOICE_STATUS = Object.freeze({
33
+ IDLE: "idle",
34
+ LISTENING: "listening",
35
+ PROCESSING: "processing",
36
+ SPEAKING: "speaking",
37
+ });
38
+
39
+ export const ANALYSIS_TYPE = Object.freeze({
40
+ OCR: "ocr",
41
+ OBJECT_DETECTION: "object_detection",
42
+ SCENE_RECOGNITION: "scene_recognition",
43
+ ACTION_DETECTION: "action_detection",
44
+ });
45
+
46
+ /* ── State ──────────────────────────────────────────────── */
47
+
48
+ let _results = new Map();
49
+ let _voiceSessions = new Map();
50
+ let _indexEntries = new Map();
51
+
52
+ function _id() {
53
+ return crypto.randomUUID();
54
+ }
55
+ function _now() {
56
+ return Date.now();
57
+ }
58
+
59
+ function _strip(row) {
60
+ if (!row) return null;
61
+ const out = {};
62
+ for (const [k, v] of Object.entries(row)) {
63
+ if (k !== "_rowid_" && k !== "rowid") out[k] = v;
64
+ }
65
+ return out;
66
+ }
67
+
68
+ /* ── Schema ─────────────────────────────────────────────── */
69
+
70
+ export function ensurePerceptionTables(db) {
71
+ db.exec(`CREATE TABLE IF NOT EXISTS perception_results (
72
+ id TEXT PRIMARY KEY,
73
+ modality TEXT NOT NULL,
74
+ analysis_type TEXT,
75
+ input_source TEXT,
76
+ result_data TEXT,
77
+ confidence REAL DEFAULT 0.0,
78
+ metadata TEXT,
79
+ created_at INTEGER
80
+ )`);
81
+
82
+ db.exec(`CREATE TABLE IF NOT EXISTS voice_sessions (
83
+ id TEXT PRIMARY KEY,
84
+ status TEXT DEFAULT 'idle',
85
+ language TEXT DEFAULT 'zh-CN',
86
+ transcript TEXT,
87
+ duration_ms INTEGER DEFAULT 0,
88
+ model TEXT,
89
+ started_at INTEGER,
90
+ ended_at INTEGER
91
+ )`);
92
+
93
+ db.exec(`CREATE TABLE IF NOT EXISTS multimodal_index (
94
+ id TEXT PRIMARY KEY,
95
+ modality TEXT NOT NULL,
96
+ source_id TEXT NOT NULL,
97
+ embedding TEXT,
98
+ content_summary TEXT,
99
+ tags TEXT,
100
+ created_at INTEGER
101
+ )`);
102
+
103
+ _loadAll(db);
104
+ }
105
+
106
+ function _loadAll(db) {
107
+ _results.clear();
108
+ _voiceSessions.clear();
109
+ _indexEntries.clear();
110
+
111
+ const tables = [
112
+ ["perception_results", _results, "id"],
113
+ ["voice_sessions", _voiceSessions, "id"],
114
+ ["multimodal_index", _indexEntries, "id"],
115
+ ];
116
+ for (const [table, map, key] of tables) {
117
+ try {
118
+ for (const row of db.prepare(`SELECT * FROM ${table}`).all()) {
119
+ const r = _strip(row);
120
+ map.set(r[key], r);
121
+ }
122
+ } catch (_e) {
123
+ /* table may not exist */
124
+ }
125
+ }
126
+ }
127
+
128
+ /* ── Perception Results ─────────────────────────────────── */
129
+
130
+ const VALID_MODALITIES = new Set(Object.values(MODALITY));
131
+ const VALID_ANALYSIS_TYPES = new Set(Object.values(ANALYSIS_TYPE));
132
+
133
+ export function recordPerception(
134
+ db,
135
+ {
136
+ modality,
137
+ analysisType,
138
+ inputSource,
139
+ resultData,
140
+ confidence,
141
+ metadata,
142
+ } = {},
143
+ ) {
144
+ if (!modality || !VALID_MODALITIES.has(modality))
145
+ return { recorded: false, reason: "invalid_modality" };
146
+
147
+ const id = _id();
148
+ const now = _now();
149
+
150
+ const entry = {
151
+ id,
152
+ modality,
153
+ analysis_type:
154
+ analysisType && VALID_ANALYSIS_TYPES.has(analysisType)
155
+ ? analysisType
156
+ : null,
157
+ input_source: inputSource || null,
158
+ result_data: resultData || null,
159
+ confidence: confidence != null ? Math.max(0, Math.min(1, confidence)) : 0,
160
+ metadata: metadata || null,
161
+ created_at: now,
162
+ };
163
+
164
+ db.prepare(
165
+ `INSERT INTO perception_results (id, modality, analysis_type, input_source, result_data, confidence, metadata, created_at)
166
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
167
+ ).run(
168
+ id,
169
+ entry.modality,
170
+ entry.analysis_type,
171
+ entry.input_source,
172
+ entry.result_data,
173
+ entry.confidence,
174
+ entry.metadata,
175
+ now,
176
+ );
177
+
178
+ _results.set(id, entry);
179
+ return { recorded: true, resultId: id };
180
+ }
181
+
182
+ export function getPerception(db, id) {
183
+ const r = _results.get(id);
184
+ return r ? { ...r } : null;
185
+ }
186
+
187
+ export function listPerceptions(
188
+ db,
189
+ { modality, analysisType, limit = 50 } = {},
190
+ ) {
191
+ let results = [..._results.values()];
192
+ if (modality) results = results.filter((r) => r.modality === modality);
193
+ if (analysisType)
194
+ results = results.filter((r) => r.analysis_type === analysisType);
195
+ return results
196
+ .sort((a, b) => b.created_at - a.created_at)
197
+ .slice(0, limit)
198
+ .map((r) => ({ ...r }));
199
+ }
200
+
201
+ /* ── Voice Sessions ─────────────────────────────────────── */
202
+
203
+ export function startVoice(db, { language, model } = {}) {
204
+ const id = _id();
205
+ const now = _now();
206
+
207
+ const entry = {
208
+ id,
209
+ status: "listening",
210
+ language: language || "zh-CN",
211
+ transcript: null,
212
+ duration_ms: 0,
213
+ model: model || null,
214
+ started_at: now,
215
+ ended_at: null,
216
+ };
217
+
218
+ db.prepare(
219
+ `INSERT INTO voice_sessions (id, status, language, transcript, duration_ms, model, started_at, ended_at)
220
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
221
+ ).run(id, "listening", entry.language, null, 0, entry.model, now, null);
222
+
223
+ _voiceSessions.set(id, entry);
224
+ return { sessionId: id, status: "listening" };
225
+ }
226
+
227
+ export function updateVoiceStatus(db, id, status) {
228
+ const s = _voiceSessions.get(id);
229
+ if (!s) return { updated: false, reason: "not_found" };
230
+
231
+ const validTransitions = {
232
+ listening: ["processing", "idle"],
233
+ processing: ["speaking", "idle"],
234
+ speaking: ["idle", "listening"],
235
+ idle: ["listening"],
236
+ };
237
+
238
+ const allowed = validTransitions[s.status] || [];
239
+ if (!allowed.includes(status))
240
+ return {
241
+ updated: false,
242
+ reason: "invalid_transition",
243
+ from: s.status,
244
+ to: status,
245
+ };
246
+
247
+ s.status = status;
248
+ if (status === "idle") {
249
+ s.ended_at = _now();
250
+ s.duration_ms = s.ended_at - s.started_at;
251
+ }
252
+
253
+ db.prepare(
254
+ "UPDATE voice_sessions SET status = ?, ended_at = ?, duration_ms = ? WHERE id = ?",
255
+ ).run(s.status, s.ended_at, s.duration_ms, id);
256
+
257
+ return { updated: true, status: s.status };
258
+ }
259
+
260
+ export function setTranscript(db, id, transcript) {
261
+ const s = _voiceSessions.get(id);
262
+ if (!s) return { updated: false, reason: "not_found" };
263
+ if (!transcript) return { updated: false, reason: "empty_transcript" };
264
+
265
+ s.transcript = transcript;
266
+ db.prepare("UPDATE voice_sessions SET transcript = ? WHERE id = ?").run(
267
+ transcript,
268
+ id,
269
+ );
270
+
271
+ return { updated: true };
272
+ }
273
+
274
+ export function getVoiceSession(db, id) {
275
+ const s = _voiceSessions.get(id);
276
+ return s ? { ...s } : null;
277
+ }
278
+
279
+ export function listVoiceSessions(db, { status, language, limit = 50 } = {}) {
280
+ let sessions = [..._voiceSessions.values()];
281
+ if (status) sessions = sessions.filter((s) => s.status === status);
282
+ if (language) sessions = sessions.filter((s) => s.language === language);
283
+ return sessions
284
+ .sort((a, b) => b.started_at - a.started_at)
285
+ .slice(0, limit)
286
+ .map((s) => ({ ...s }));
287
+ }
288
+
289
+ /* ── Multimodal Index ───────────────────────────────────── */
290
+
291
+ export function addIndexEntry(
292
+ db,
293
+ { modality, sourceId, contentSummary, tags, embedding } = {},
294
+ ) {
295
+ if (!modality || !VALID_MODALITIES.has(modality))
296
+ return { added: false, reason: "invalid_modality" };
297
+ if (!sourceId) return { added: false, reason: "missing_source_id" };
298
+
299
+ const id = _id();
300
+ const now = _now();
301
+ const tagsJson = Array.isArray(tags) ? JSON.stringify(tags) : tags || null;
302
+
303
+ const entry = {
304
+ id,
305
+ modality,
306
+ source_id: sourceId,
307
+ embedding: embedding || null,
308
+ content_summary: contentSummary || null,
309
+ tags: tagsJson,
310
+ created_at: now,
311
+ };
312
+
313
+ db.prepare(
314
+ `INSERT INTO multimodal_index (id, modality, source_id, embedding, content_summary, tags, created_at)
315
+ VALUES (?, ?, ?, ?, ?, ?, ?)`,
316
+ ).run(
317
+ id,
318
+ modality,
319
+ sourceId,
320
+ entry.embedding,
321
+ entry.content_summary,
322
+ tagsJson,
323
+ now,
324
+ );
325
+
326
+ _indexEntries.set(id, entry);
327
+ return { added: true, indexId: id };
328
+ }
329
+
330
+ export function getIndexEntry(db, id) {
331
+ const e = _indexEntries.get(id);
332
+ return e ? { ...e } : null;
333
+ }
334
+
335
+ export function listIndexEntries(db, { modality, limit = 50 } = {}) {
336
+ let entries = [..._indexEntries.values()];
337
+ if (modality) entries = entries.filter((e) => e.modality === modality);
338
+ return entries
339
+ .sort((a, b) => b.created_at - a.created_at)
340
+ .slice(0, limit)
341
+ .map((e) => ({ ...e }));
342
+ }
343
+
344
+ export function removeIndexEntry(db, id) {
345
+ const e = _indexEntries.get(id);
346
+ if (!e) return { removed: false, reason: "not_found" };
347
+
348
+ _indexEntries.delete(id);
349
+ db.prepare("DELETE FROM multimodal_index WHERE id = ?").run(id);
350
+
351
+ return { removed: true };
352
+ }
353
+
354
+ /* ── Cross-Modal Query ──────────────────────────────────── */
355
+
356
+ export function crossModalQuery(db, { query, modalities, limit = 20 } = {}) {
357
+ if (!query) return { results: [], reason: "missing_query" };
358
+
359
+ let entries = [..._indexEntries.values()];
360
+
361
+ // Filter by modalities if specified
362
+ if (modalities && modalities.length > 0) {
363
+ entries = entries.filter((e) => modalities.includes(e.modality));
364
+ }
365
+
366
+ // Keyword matching on content_summary and tags (simulated semantic search)
367
+ const queryLower = query.toLowerCase();
368
+ const scored = entries
369
+ .map((e) => {
370
+ let score = 0;
371
+ const summary = (e.content_summary || "").toLowerCase();
372
+ const tagStr = (e.tags || "").toLowerCase();
373
+
374
+ // Check summary match
375
+ if (summary.includes(queryLower)) {
376
+ score += 0.8;
377
+ } else {
378
+ // Partial word matching
379
+ const words = queryLower.split(/\s+/);
380
+ const matches = words.filter(
381
+ (w) => summary.includes(w) || tagStr.includes(w),
382
+ );
383
+ score += (matches.length / words.length) * 0.6;
384
+ }
385
+
386
+ // Check tag match
387
+ if (tagStr.includes(queryLower)) {
388
+ score += 0.2;
389
+ }
390
+
391
+ return { ...e, score: Math.round(score * 1000) / 1000 };
392
+ })
393
+ .filter((e) => e.score > 0)
394
+ .sort((a, b) => b.score - a.score)
395
+ .slice(0, limit);
396
+
397
+ return { results: scored, total: scored.length };
398
+ }
399
+
400
+ /* ── Context ────────────────────────────────────────────── */
401
+
402
+ export function getPerceptionContext(db) {
403
+ const results = [..._results.values()];
404
+ const sessions = [..._voiceSessions.values()];
405
+ const entries = [..._indexEntries.values()];
406
+
407
+ const activeSessions = sessions.filter((s) => s.status !== "idle");
408
+ const recentResults = results
409
+ .sort((a, b) => b.created_at - a.created_at)
410
+ .slice(0, 5)
411
+ .map((r) => ({
412
+ modality: r.modality,
413
+ analysisType: r.analysis_type,
414
+ confidence: r.confidence,
415
+ }));
416
+
417
+ return {
418
+ activeSessions: activeSessions.length,
419
+ activeSessionIds: activeSessions.map((s) => s.id),
420
+ totalResults: results.length,
421
+ totalIndexEntries: entries.length,
422
+ recentResults,
423
+ modalityCoverage: {
424
+ screen: results.filter((r) => r.modality === "screen").length,
425
+ voice: results.filter((r) => r.modality === "voice").length,
426
+ document: results.filter((r) => r.modality === "document").length,
427
+ video: results.filter((r) => r.modality === "video").length,
428
+ },
429
+ };
430
+ }
431
+
432
+ /* ── Stats ──────────────────────────────────────────────── */
433
+
434
+ export function getPerceptionStats(db) {
435
+ const results = [..._results.values()];
436
+ const sessions = [..._voiceSessions.values()];
437
+ const entries = [..._indexEntries.values()];
438
+
439
+ const byModality = {};
440
+ for (const mod of Object.values(MODALITY)) byModality[mod] = 0;
441
+ for (const r of results)
442
+ byModality[r.modality] = (byModality[r.modality] || 0) + 1;
443
+
444
+ const byAnalysisType = {};
445
+ for (const r of results) {
446
+ if (r.analysis_type) {
447
+ byAnalysisType[r.analysis_type] =
448
+ (byAnalysisType[r.analysis_type] || 0) + 1;
449
+ }
450
+ }
451
+
452
+ const completedSessions = sessions.filter(
453
+ (s) => s.status === "idle" && s.ended_at,
454
+ );
455
+ const avgSessionDuration =
456
+ completedSessions.length > 0
457
+ ? Math.round(
458
+ completedSessions.reduce((s, v) => s + v.duration_ms, 0) /
459
+ completedSessions.length,
460
+ )
461
+ : 0;
462
+
463
+ const avgConfidence =
464
+ results.length > 0
465
+ ? Math.round(
466
+ (results.reduce((s, r) => s + r.confidence, 0) / results.length) *
467
+ 1000,
468
+ ) / 1000
469
+ : 0;
470
+
471
+ return {
472
+ results: {
473
+ total: results.length,
474
+ byModality,
475
+ byAnalysisType,
476
+ avgConfidence,
477
+ },
478
+ voiceSessions: {
479
+ total: sessions.length,
480
+ active: sessions.filter((s) => s.status !== "idle").length,
481
+ completed: completedSessions.length,
482
+ avgDurationMs: avgSessionDuration,
483
+ },
484
+ index: {
485
+ total: entries.length,
486
+ byModality: entries.reduce((acc, e) => {
487
+ acc[e.modality] = (acc[e.modality] || 0) + 1;
488
+ return acc;
489
+ }, {}),
490
+ },
491
+ };
492
+ }
493
+
494
+ /* ── Reset (tests) ──────────────────────────────────────── */
495
+
496
+ export function _resetState() {
497
+ _results.clear();
498
+ _voiceSessions.clear();
499
+ _indexEntries.clear();
500
+ }
@@ -14,10 +14,123 @@ const PQC_ALGORITHMS = {
14
14
  ML_KEM_1024: "ML-KEM-1024",
15
15
  ML_DSA_65: "ML-DSA-65",
16
16
  ML_DSA_87: "ML-DSA-87",
17
+ // FIPS 205 — SLH-DSA (Stateless Hash-Based Signatures).
18
+ // Suffix s = small-signature/slow-sign, f = fast-sign/large-signature.
19
+ SLH_DSA_128S: "SLH-DSA-128s",
20
+ SLH_DSA_128F: "SLH-DSA-128f",
21
+ SLH_DSA_192S: "SLH-DSA-192s",
22
+ SLH_DSA_192F: "SLH-DSA-192f",
23
+ SLH_DSA_256S: "SLH-DSA-256s",
24
+ SLH_DSA_256F: "SLH-DSA-256f",
17
25
  HYBRID_X25519_ML_KEM: "HYBRID-X25519-ML-KEM",
18
26
  HYBRID_ED25519_ML_DSA: "HYBRID-ED25519-ML-DSA",
27
+ HYBRID_ED25519_SLH_DSA: "HYBRID-ED25519-SLH-DSA",
19
28
  };
20
29
 
30
+ /**
31
+ * Per-algorithm spec lookup. Sizes follow NIST FIPS 203 / 204 / 205.
32
+ * - keySize: security-category label in bits (mirrors legacy 768/1024 semantics
33
+ * for ML-* and uses 128/192/256 for SLH-DSA).
34
+ * - publicKeyBytes: public-key length in bytes (used for placeholder key bytes).
35
+ * - signatureBytes: signature length in bytes (null for KEM / hybrid-KEM rows).
36
+ * - family: "ml-kem" | "ml-dsa" | "slh-dsa" | "hybrid".
37
+ */
38
+ const ALGORITHM_SPECS = {
39
+ "ML-KEM-768": {
40
+ keySize: 768,
41
+ publicKeyBytes: 1184,
42
+ signatureBytes: null,
43
+ family: "ml-kem",
44
+ },
45
+ "ML-KEM-1024": {
46
+ keySize: 1024,
47
+ publicKeyBytes: 1568,
48
+ signatureBytes: null,
49
+ family: "ml-kem",
50
+ },
51
+ "ML-DSA-65": {
52
+ keySize: 768,
53
+ publicKeyBytes: 1952,
54
+ signatureBytes: 3309,
55
+ family: "ml-dsa",
56
+ },
57
+ "ML-DSA-87": {
58
+ keySize: 1024,
59
+ publicKeyBytes: 2592,
60
+ signatureBytes: 4627,
61
+ family: "ml-dsa",
62
+ },
63
+ "SLH-DSA-128s": {
64
+ keySize: 128,
65
+ publicKeyBytes: 32,
66
+ signatureBytes: 7856,
67
+ family: "slh-dsa",
68
+ },
69
+ "SLH-DSA-128f": {
70
+ keySize: 128,
71
+ publicKeyBytes: 32,
72
+ signatureBytes: 17088,
73
+ family: "slh-dsa",
74
+ },
75
+ "SLH-DSA-192s": {
76
+ keySize: 192,
77
+ publicKeyBytes: 48,
78
+ signatureBytes: 16224,
79
+ family: "slh-dsa",
80
+ },
81
+ "SLH-DSA-192f": {
82
+ keySize: 192,
83
+ publicKeyBytes: 48,
84
+ signatureBytes: 35664,
85
+ family: "slh-dsa",
86
+ },
87
+ "SLH-DSA-256s": {
88
+ keySize: 256,
89
+ publicKeyBytes: 64,
90
+ signatureBytes: 29792,
91
+ family: "slh-dsa",
92
+ },
93
+ "SLH-DSA-256f": {
94
+ keySize: 256,
95
+ publicKeyBytes: 64,
96
+ signatureBytes: 49856,
97
+ family: "slh-dsa",
98
+ },
99
+ "HYBRID-X25519-ML-KEM": {
100
+ keySize: 768,
101
+ publicKeyBytes: 1216,
102
+ signatureBytes: null,
103
+ family: "hybrid",
104
+ },
105
+ "HYBRID-ED25519-ML-DSA": {
106
+ keySize: 768,
107
+ publicKeyBytes: 1984,
108
+ signatureBytes: 3373,
109
+ family: "hybrid",
110
+ },
111
+ "HYBRID-ED25519-SLH-DSA": {
112
+ keySize: 128,
113
+ publicKeyBytes: 64,
114
+ signatureBytes: 7920,
115
+ family: "hybrid",
116
+ },
117
+ };
118
+
119
+ function _defaultPurposeFor(algorithm) {
120
+ const spec = ALGORITHM_SPECS[algorithm];
121
+ if (!spec) return KEY_PURPOSES.ENCRYPTION;
122
+ if (spec.family === "ml-kem") return KEY_PURPOSES.KEY_EXCHANGE;
123
+ if (spec.family === "ml-dsa" || spec.family === "slh-dsa")
124
+ return KEY_PURPOSES.SIGNING;
125
+ return KEY_PURPOSES.ENCRYPTION;
126
+ }
127
+
128
+ function _classicalPartner(algorithm) {
129
+ if (!algorithm.startsWith("HYBRID")) return null;
130
+ if (algorithm.includes("X25519")) return "X25519";
131
+ return "Ed25519";
132
+ }
133
+
21
134
  const KEY_PURPOSES = {
22
135
  ENCRYPTION: "encryption",
23
136
  SIGNING: "signing",
@@ -91,22 +204,21 @@ export function generateKey(db, algorithm, purpose, opts = {}) {
91
204
  const id = crypto.randomUUID();
92
205
  const now = new Date().toISOString();
93
206
 
94
- const keySize =
95
- algorithm.includes("1024") || algorithm.includes("87") ? 1024 : 768;
96
- const publicKey = crypto.randomBytes(keySize / 8).toString("hex");
207
+ const spec = ALGORITHM_SPECS[algorithm];
208
+ const keySize = spec.keySize;
209
+ const publicKey = crypto.randomBytes(spec.publicKeyBytes).toString("hex");
97
210
  const isHybrid = algorithm.startsWith("HYBRID");
98
- const classicalAlgorithm = isHybrid
99
- ? algorithm.includes("X25519")
100
- ? "X25519"
101
- : "Ed25519"
102
- : null;
211
+ const classicalAlgorithm = _classicalPartner(algorithm);
103
212
 
104
213
  const key = {
105
214
  id,
106
215
  algorithm,
107
- purpose: purpose || KEY_PURPOSES.ENCRYPTION,
216
+ family: spec.family,
217
+ purpose: purpose || _defaultPurposeFor(algorithm),
108
218
  publicKey,
109
219
  keySize,
220
+ publicKeyBytes: spec.publicKeyBytes,
221
+ signatureBytes: spec.signatureBytes,
110
222
  hybridMode: isHybrid,
111
223
  classicalAlgorithm,
112
224
  status: "active",
@@ -188,6 +300,26 @@ export function migrate(db, planName, sourceAlgorithm, targetAlgorithm) {
188
300
  return plan;
189
301
  }
190
302
 
303
+ /* ── Algorithm catalog helpers ─────────────────────────────── */
304
+
305
+ export { PQC_ALGORITHMS, KEY_PURPOSES, MIGRATION_STATUS };
306
+
307
+ export function listAlgorithms(filter = {}) {
308
+ const entries = Object.entries(ALGORITHM_SPECS).map(([name, spec]) => ({
309
+ algorithm: name,
310
+ ...spec,
311
+ }));
312
+ if (filter.family) {
313
+ return entries.filter((e) => e.family === filter.family);
314
+ }
315
+ return entries;
316
+ }
317
+
318
+ export function algorithmSpec(algorithm) {
319
+ const spec = ALGORITHM_SPECS[algorithm];
320
+ return spec ? { algorithm, ...spec } : null;
321
+ }
322
+
191
323
  /* ── Reset (for testing) ───────────────────────────────────── */
192
324
 
193
325
  export function _resetState() {