chainlesschain 0.47.8 → 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.
- package/bin/chainlesschain.js +0 -0
- package/package.json +10 -8
- package/src/assets/web-panel/.build-hash +1 -1
- package/src/assets/web-panel/assets/{AppLayout-6SPt_8Y_.js → AppLayout-Rvi759IS.js} +1 -1
- package/src/assets/web-panel/assets/Dashboard-BS-tzGNj.css +1 -0
- package/src/assets/web-panel/assets/{Dashboard-Br7kCwKJ.js → Dashboard-DBhFxXYQ.js} +2 -2
- package/src/assets/web-panel/assets/{index-tN-8TosE.js → index-uL0cZ8N_.js} +2 -2
- package/src/assets/web-panel/index.html +2 -2
- package/src/commands/activitypub.js +533 -0
- package/src/commands/codegen.js +303 -0
- package/src/commands/collab.js +482 -0
- package/src/commands/compliance.js +597 -6
- package/src/commands/crosschain.js +382 -0
- package/src/commands/dbevo.js +388 -0
- package/src/commands/dev.js +411 -0
- package/src/commands/federation.js +427 -0
- package/src/commands/fusion.js +332 -0
- package/src/commands/governance.js +505 -0
- package/src/commands/hardening.js +110 -0
- package/src/commands/incentive.js +373 -0
- package/src/commands/inference.js +304 -0
- package/src/commands/infra.js +361 -0
- package/src/commands/kg.js +371 -0
- package/src/commands/marketplace.js +326 -0
- package/src/commands/matrix.js +283 -0
- package/src/commands/mcp.js +441 -18
- package/src/commands/nlprog.js +329 -0
- package/src/commands/nostr.js +196 -7
- package/src/commands/ops.js +408 -0
- package/src/commands/perception.js +385 -0
- package/src/commands/pqc.js +34 -0
- package/src/commands/privacy.js +345 -0
- package/src/commands/quantization.js +280 -0
- package/src/commands/recommend.js +336 -0
- package/src/commands/reputation.js +349 -0
- package/src/commands/runtime.js +500 -0
- package/src/commands/sla.js +352 -0
- package/src/commands/social.js +265 -0
- package/src/commands/stress.js +252 -0
- package/src/commands/tech.js +268 -0
- package/src/commands/tenant.js +576 -0
- package/src/commands/trust.js +366 -0
- package/src/harness/mcp-client.js +330 -54
- package/src/index.js +114 -0
- package/src/lib/activitypub-bridge.js +623 -0
- package/src/lib/aiops.js +523 -0
- package/src/lib/autonomous-developer.js +524 -0
- package/src/lib/code-agent.js +442 -0
- package/src/lib/collaboration-governance.js +556 -0
- package/src/lib/community-governance.js +649 -0
- package/src/lib/compliance-framework-reporter.js +600 -0
- package/src/lib/content-recommendation.js +600 -0
- package/src/lib/cross-chain.js +669 -0
- package/src/lib/dbevo.js +669 -0
- package/src/lib/decentral-infra.js +445 -0
- package/src/lib/federation-hardening.js +587 -0
- package/src/lib/hardening-manager.js +409 -0
- package/src/lib/inference-network.js +407 -0
- package/src/lib/knowledge-graph.js +530 -0
- package/src/lib/matrix-bridge.js +252 -0
- package/src/lib/mcp-client.js +3 -0
- package/src/lib/mcp-registry.js +347 -0
- package/src/lib/mcp-scaffold.js +385 -0
- package/src/lib/multimodal.js +698 -0
- package/src/lib/nl-programming.js +595 -0
- package/src/lib/nostr-bridge.js +214 -38
- package/src/lib/perception.js +500 -0
- package/src/lib/pqc-manager.js +141 -9
- package/src/lib/privacy-computing.js +575 -0
- package/src/lib/protocol-fusion.js +535 -0
- package/src/lib/quantization.js +362 -0
- package/src/lib/reputation-optimizer.js +509 -0
- package/src/lib/skill-marketplace.js +397 -0
- package/src/lib/sla-manager.js +484 -0
- package/src/lib/social-graph.js +408 -0
- package/src/lib/stix-parser.js +167 -0
- package/src/lib/stress-tester.js +383 -0
- package/src/lib/tech-learning-engine.js +651 -0
- package/src/lib/tenant-saas.js +831 -0
- package/src/lib/threat-intel.js +268 -0
- package/src/lib/token-incentive.js +513 -0
- package/src/lib/topic-classifier.js +400 -0
- package/src/lib/trust-security.js +473 -0
- package/src/lib/ueba.js +403 -0
- package/src/lib/universal-runtime.js +771 -0
- 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
|
+
}
|
package/src/lib/pqc-manager.js
CHANGED
|
@@ -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
|
|
95
|
-
|
|
96
|
-
const publicKey = crypto.randomBytes(
|
|
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 =
|
|
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
|
-
|
|
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() {
|