chainlesschain 0.37.9 → 0.37.11
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/README.md +309 -19
- package/bin/chainlesschain.js +4 -0
- package/package.json +1 -1
- package/src/commands/a2a.js +374 -0
- package/src/commands/audit.js +286 -0
- package/src/commands/auth.js +387 -0
- package/src/commands/bi.js +240 -0
- package/src/commands/browse.js +184 -0
- package/src/commands/cowork.js +317 -0
- package/src/commands/did.js +376 -0
- package/src/commands/economy.js +375 -0
- package/src/commands/encrypt.js +233 -0
- package/src/commands/evolution.js +398 -0
- package/src/commands/export.js +125 -0
- package/src/commands/git.js +215 -0
- package/src/commands/hmemory.js +273 -0
- package/src/commands/hook.js +260 -0
- package/src/commands/import.js +259 -0
- package/src/commands/init.js +184 -0
- package/src/commands/instinct.js +202 -0
- package/src/commands/llm.js +155 -4
- package/src/commands/lowcode.js +320 -0
- package/src/commands/mcp.js +302 -0
- package/src/commands/memory.js +282 -0
- package/src/commands/note.js +187 -0
- package/src/commands/org.js +505 -0
- package/src/commands/p2p.js +274 -0
- package/src/commands/plugin.js +451 -0
- package/src/commands/sandbox.js +366 -0
- package/src/commands/search.js +237 -0
- package/src/commands/session.js +238 -0
- package/src/commands/skill.js +254 -201
- package/src/commands/sync.js +249 -0
- package/src/commands/tokens.js +214 -0
- package/src/commands/wallet.js +416 -0
- package/src/commands/workflow.js +359 -0
- package/src/commands/zkp.js +277 -0
- package/src/index.js +93 -1
- package/src/lib/a2a-protocol.js +371 -0
- package/src/lib/agent-coordinator.js +273 -0
- package/src/lib/agent-economy.js +369 -0
- package/src/lib/app-builder.js +377 -0
- package/src/lib/audit-logger.js +364 -0
- package/src/lib/bi-engine.js +299 -0
- package/src/lib/bm25-search.js +322 -0
- package/src/lib/browser-automation.js +216 -0
- package/src/lib/cowork/ab-comparator-cli.js +180 -0
- package/src/lib/cowork/code-knowledge-graph-cli.js +232 -0
- package/src/lib/cowork/debate-review-cli.js +144 -0
- package/src/lib/cowork/decision-kb-cli.js +153 -0
- package/src/lib/cowork/project-style-analyzer-cli.js +168 -0
- package/src/lib/cowork-adapter.js +106 -0
- package/src/lib/crypto-manager.js +246 -0
- package/src/lib/did-manager.js +270 -0
- package/src/lib/ensure-utf8.js +59 -0
- package/src/lib/evolution-system.js +508 -0
- package/src/lib/git-integration.js +220 -0
- package/src/lib/hierarchical-memory.js +471 -0
- package/src/lib/hook-manager.js +387 -0
- package/src/lib/instinct-manager.js +190 -0
- package/src/lib/knowledge-exporter.js +302 -0
- package/src/lib/knowledge-importer.js +293 -0
- package/src/lib/llm-providers.js +325 -0
- package/src/lib/mcp-client.js +413 -0
- package/src/lib/memory-manager.js +211 -0
- package/src/lib/note-versioning.js +244 -0
- package/src/lib/org-manager.js +424 -0
- package/src/lib/p2p-manager.js +317 -0
- package/src/lib/pdf-parser.js +96 -0
- package/src/lib/permission-engine.js +374 -0
- package/src/lib/plan-mode.js +333 -0
- package/src/lib/plugin-manager.js +430 -0
- package/src/lib/project-detector.js +53 -0
- package/src/lib/response-cache.js +156 -0
- package/src/lib/sandbox-v2.js +503 -0
- package/src/lib/service-container.js +183 -0
- package/src/lib/session-manager.js +189 -0
- package/src/lib/skill-loader.js +274 -0
- package/src/lib/sync-manager.js +347 -0
- package/src/lib/token-tracker.js +200 -0
- package/src/lib/wallet-manager.js +348 -0
- package/src/lib/workflow-engine.js +503 -0
- package/src/lib/zkp-engine.js +241 -0
- package/src/repl/agent-repl.js +259 -124
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cowork Adapter — bridges CLI's LLM infrastructure to cowork modules.
|
|
3
|
+
*
|
|
4
|
+
* Provides:
|
|
5
|
+
* - Unified LLM chat function (works with any configured provider)
|
|
6
|
+
* - Logger shim compatible with desktop modules
|
|
7
|
+
* - Module initialization helper
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { LLMProviderRegistry, BUILT_IN_PROVIDERS } from "./llm-providers.js";
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Create a chat completion function that routes through the active LLM provider.
|
|
14
|
+
*
|
|
15
|
+
* @param {object} [options]
|
|
16
|
+
* @param {string} [options.provider] - Provider name override
|
|
17
|
+
* @param {string} [options.model] - Model name override
|
|
18
|
+
* @param {string} [options.baseUrl] - Base URL override
|
|
19
|
+
* @param {string} [options.apiKey] - API key override
|
|
20
|
+
* @returns {(messages: object[], opts?: object) => Promise<string>}
|
|
21
|
+
*/
|
|
22
|
+
export function createChatFn(options = {}) {
|
|
23
|
+
const provider = options.provider || process.env.LLM_PROVIDER || "ollama";
|
|
24
|
+
const providerDef = BUILT_IN_PROVIDERS[provider] || BUILT_IN_PROVIDERS.ollama;
|
|
25
|
+
const model = options.model || process.env.LLM_MODEL || providerDef.models[0];
|
|
26
|
+
const baseUrl = options.baseUrl || providerDef.baseUrl;
|
|
27
|
+
|
|
28
|
+
return async function chat(messages, opts = {}) {
|
|
29
|
+
const currentModel = opts.model || model;
|
|
30
|
+
const maxTokens = opts.maxTokens || 2048;
|
|
31
|
+
|
|
32
|
+
if (provider === "ollama") {
|
|
33
|
+
const res = await fetch(`${baseUrl}/api/chat`, {
|
|
34
|
+
method: "POST",
|
|
35
|
+
headers: { "Content-Type": "application/json" },
|
|
36
|
+
body: JSON.stringify({
|
|
37
|
+
model: currentModel,
|
|
38
|
+
messages,
|
|
39
|
+
stream: false,
|
|
40
|
+
options: { num_predict: maxTokens },
|
|
41
|
+
}),
|
|
42
|
+
});
|
|
43
|
+
if (!res.ok) throw new Error(`Ollama error: ${res.status}`);
|
|
44
|
+
const data = await res.json();
|
|
45
|
+
return data.message?.content || "";
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (provider === "anthropic") {
|
|
49
|
+
const key = options.apiKey || process.env[providerDef.apiKeyEnv];
|
|
50
|
+
if (!key) throw new Error("ANTHROPIC_API_KEY not set");
|
|
51
|
+
// Extract system message if present
|
|
52
|
+
const systemMsgs = messages.filter((m) => m.role === "system");
|
|
53
|
+
const otherMsgs = messages.filter((m) => m.role !== "system");
|
|
54
|
+
const body = {
|
|
55
|
+
model: currentModel,
|
|
56
|
+
max_tokens: maxTokens,
|
|
57
|
+
messages: otherMsgs,
|
|
58
|
+
};
|
|
59
|
+
if (systemMsgs.length > 0) {
|
|
60
|
+
body.system = systemMsgs.map((m) => m.content).join("\n");
|
|
61
|
+
}
|
|
62
|
+
const res = await fetch(`${baseUrl}/messages`, {
|
|
63
|
+
method: "POST",
|
|
64
|
+
headers: {
|
|
65
|
+
"Content-Type": "application/json",
|
|
66
|
+
"x-api-key": key,
|
|
67
|
+
"anthropic-version": "2023-06-01",
|
|
68
|
+
},
|
|
69
|
+
body: JSON.stringify(body),
|
|
70
|
+
});
|
|
71
|
+
if (!res.ok) throw new Error(`Anthropic error: ${res.status}`);
|
|
72
|
+
const data = await res.json();
|
|
73
|
+
return data.content?.[0]?.text || "";
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// OpenAI-compatible (openai, deepseek, dashscope, mistral, gemini)
|
|
77
|
+
const key = options.apiKey || process.env[providerDef.apiKeyEnv];
|
|
78
|
+
if (!key) throw new Error(`${providerDef.apiKeyEnv} not set`);
|
|
79
|
+
|
|
80
|
+
const res = await fetch(`${baseUrl}/chat/completions`, {
|
|
81
|
+
method: "POST",
|
|
82
|
+
headers: {
|
|
83
|
+
"Content-Type": "application/json",
|
|
84
|
+
Authorization: `Bearer ${key}`,
|
|
85
|
+
},
|
|
86
|
+
body: JSON.stringify({
|
|
87
|
+
model: currentModel,
|
|
88
|
+
messages,
|
|
89
|
+
max_tokens: maxTokens,
|
|
90
|
+
}),
|
|
91
|
+
});
|
|
92
|
+
if (!res.ok) throw new Error(`API error: ${res.status}`);
|
|
93
|
+
const data = await res.json();
|
|
94
|
+
return data.choices?.[0]?.message?.content || "";
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Logger shim — compatible with desktop module expectations
|
|
100
|
+
*/
|
|
101
|
+
export const coworkLogger = {
|
|
102
|
+
info: (...args) => console.log("[cowork]", ...args),
|
|
103
|
+
warn: (...args) => console.warn("[cowork]", ...args),
|
|
104
|
+
error: (...args) => console.error("[cowork]", ...args),
|
|
105
|
+
debug: () => {},
|
|
106
|
+
};
|
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Crypto Manager — file encryption/decryption using AES-256-GCM.
|
|
3
|
+
* Uses Node.js built-in crypto module, no external dependencies.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import crypto from "crypto";
|
|
7
|
+
import fs from "fs";
|
|
8
|
+
import path from "path";
|
|
9
|
+
|
|
10
|
+
const ALGORITHM = "aes-256-gcm";
|
|
11
|
+
const KEY_LENGTH = 32; // 256 bits
|
|
12
|
+
const IV_LENGTH = 12; // 96 bits for GCM
|
|
13
|
+
const SALT_LENGTH = 32;
|
|
14
|
+
const TAG_LENGTH = 16;
|
|
15
|
+
const PBKDF2_ITERATIONS = 100000;
|
|
16
|
+
const MAGIC_HEADER = Buffer.from("CCLC01"); // ChainLessChain v01
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Derive a key from a password using PBKDF2.
|
|
20
|
+
*/
|
|
21
|
+
export function deriveKey(password, salt) {
|
|
22
|
+
return crypto.pbkdf2Sync(
|
|
23
|
+
password,
|
|
24
|
+
salt,
|
|
25
|
+
PBKDF2_ITERATIONS,
|
|
26
|
+
KEY_LENGTH,
|
|
27
|
+
"sha512",
|
|
28
|
+
);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Encrypt a buffer with AES-256-GCM.
|
|
33
|
+
* Returns: MAGIC(6) + SALT(32) + IV(12) + TAG(16) + CIPHERTEXT
|
|
34
|
+
*/
|
|
35
|
+
export function encryptBuffer(plaintext, password) {
|
|
36
|
+
const salt = crypto.randomBytes(SALT_LENGTH);
|
|
37
|
+
const key = deriveKey(password, salt);
|
|
38
|
+
const iv = crypto.randomBytes(IV_LENGTH);
|
|
39
|
+
|
|
40
|
+
const cipher = crypto.createCipheriv(ALGORITHM, key, iv);
|
|
41
|
+
const encrypted = Buffer.concat([cipher.update(plaintext), cipher.final()]);
|
|
42
|
+
const tag = cipher.getAuthTag();
|
|
43
|
+
|
|
44
|
+
return Buffer.concat([MAGIC_HEADER, salt, iv, tag, encrypted]);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Decrypt a buffer encrypted with encryptBuffer.
|
|
49
|
+
*/
|
|
50
|
+
export function decryptBuffer(data, password) {
|
|
51
|
+
// Validate magic header
|
|
52
|
+
if (
|
|
53
|
+
data.length <
|
|
54
|
+
MAGIC_HEADER.length + SALT_LENGTH + IV_LENGTH + TAG_LENGTH
|
|
55
|
+
) {
|
|
56
|
+
throw new Error("Invalid encrypted data: too short");
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const magic = data.subarray(0, MAGIC_HEADER.length);
|
|
60
|
+
if (!magic.equals(MAGIC_HEADER)) {
|
|
61
|
+
throw new Error(
|
|
62
|
+
"Invalid encrypted data: bad header (not a ChainlessChain encrypted file)",
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
let offset = MAGIC_HEADER.length;
|
|
67
|
+
const salt = data.subarray(offset, offset + SALT_LENGTH);
|
|
68
|
+
offset += SALT_LENGTH;
|
|
69
|
+
const iv = data.subarray(offset, offset + IV_LENGTH);
|
|
70
|
+
offset += IV_LENGTH;
|
|
71
|
+
const tag = data.subarray(offset, offset + TAG_LENGTH);
|
|
72
|
+
offset += TAG_LENGTH;
|
|
73
|
+
const ciphertext = data.subarray(offset);
|
|
74
|
+
|
|
75
|
+
const key = deriveKey(password, salt);
|
|
76
|
+
|
|
77
|
+
const decipher = crypto.createDecipheriv(ALGORITHM, key, iv);
|
|
78
|
+
decipher.setAuthTag(tag);
|
|
79
|
+
|
|
80
|
+
try {
|
|
81
|
+
return Buffer.concat([decipher.update(ciphertext), decipher.final()]);
|
|
82
|
+
} catch (_err) {
|
|
83
|
+
throw new Error("Decryption failed: wrong password or corrupted data");
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Encrypt a file to a .enc output file.
|
|
89
|
+
*/
|
|
90
|
+
export function encryptFile(inputPath, password, outputPath) {
|
|
91
|
+
if (!fs.existsSync(inputPath)) {
|
|
92
|
+
throw new Error(`File not found: ${inputPath}`);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const plaintext = fs.readFileSync(inputPath);
|
|
96
|
+
const encrypted = encryptBuffer(plaintext, password);
|
|
97
|
+
|
|
98
|
+
const outPath = outputPath || `${inputPath}.enc`;
|
|
99
|
+
fs.writeFileSync(outPath, encrypted);
|
|
100
|
+
|
|
101
|
+
return {
|
|
102
|
+
inputPath,
|
|
103
|
+
outputPath: outPath,
|
|
104
|
+
originalSize: plaintext.length,
|
|
105
|
+
encryptedSize: encrypted.length,
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Decrypt a .enc file.
|
|
111
|
+
*/
|
|
112
|
+
export function decryptFile(inputPath, password, outputPath) {
|
|
113
|
+
if (!fs.existsSync(inputPath)) {
|
|
114
|
+
throw new Error(`File not found: ${inputPath}`);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const data = fs.readFileSync(inputPath);
|
|
118
|
+
const plaintext = decryptBuffer(data, password);
|
|
119
|
+
|
|
120
|
+
// Default output: remove .enc extension, or add .dec
|
|
121
|
+
const outPath =
|
|
122
|
+
outputPath ||
|
|
123
|
+
(inputPath.endsWith(".enc") ? inputPath.slice(0, -4) : `${inputPath}.dec`);
|
|
124
|
+
|
|
125
|
+
fs.writeFileSync(outPath, plaintext);
|
|
126
|
+
|
|
127
|
+
return {
|
|
128
|
+
inputPath,
|
|
129
|
+
outputPath: outPath,
|
|
130
|
+
encryptedSize: data.length,
|
|
131
|
+
decryptedSize: plaintext.length,
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Check if a file is encrypted with our format.
|
|
137
|
+
*/
|
|
138
|
+
export function isEncryptedFile(filePath) {
|
|
139
|
+
try {
|
|
140
|
+
const fd = fs.openSync(filePath, "r");
|
|
141
|
+
const buf = Buffer.alloc(MAGIC_HEADER.length);
|
|
142
|
+
fs.readSync(fd, buf, 0, MAGIC_HEADER.length, 0);
|
|
143
|
+
fs.closeSync(fd);
|
|
144
|
+
return buf.equals(MAGIC_HEADER);
|
|
145
|
+
} catch (_err) {
|
|
146
|
+
return false;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Generate a random encryption key (hex string).
|
|
152
|
+
*/
|
|
153
|
+
export function generateKey() {
|
|
154
|
+
return crypto.randomBytes(KEY_LENGTH).toString("hex");
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Hash a password for storage (SHA-256 + salt).
|
|
159
|
+
* Returns { hash, salt } as hex strings.
|
|
160
|
+
*/
|
|
161
|
+
export function hashPassword(password) {
|
|
162
|
+
const salt = crypto.randomBytes(16).toString("hex");
|
|
163
|
+
const hash = crypto
|
|
164
|
+
.createHash("sha256")
|
|
165
|
+
.update(password + salt)
|
|
166
|
+
.digest("hex");
|
|
167
|
+
return { hash, salt };
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Verify a password against a stored hash+salt.
|
|
172
|
+
*/
|
|
173
|
+
export function verifyPassword(password, hash, salt) {
|
|
174
|
+
const computed = crypto
|
|
175
|
+
.createHash("sha256")
|
|
176
|
+
.update(password + salt)
|
|
177
|
+
.digest("hex");
|
|
178
|
+
return crypto.timingSafeEqual(
|
|
179
|
+
Buffer.from(computed, "hex"),
|
|
180
|
+
Buffer.from(hash, "hex"),
|
|
181
|
+
);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Ensure crypto metadata table exists.
|
|
186
|
+
*/
|
|
187
|
+
export function ensureCryptoTable(db) {
|
|
188
|
+
db.exec(`
|
|
189
|
+
CREATE TABLE IF NOT EXISTS crypto_metadata (
|
|
190
|
+
key TEXT PRIMARY KEY,
|
|
191
|
+
value TEXT NOT NULL,
|
|
192
|
+
created_at TEXT DEFAULT (datetime('now'))
|
|
193
|
+
)
|
|
194
|
+
`);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Set database encryption status.
|
|
199
|
+
*/
|
|
200
|
+
export function setDbEncryptionStatus(
|
|
201
|
+
db,
|
|
202
|
+
encrypted,
|
|
203
|
+
passwordHash,
|
|
204
|
+
passwordSalt,
|
|
205
|
+
) {
|
|
206
|
+
ensureCryptoTable(db);
|
|
207
|
+
const stmt = db.prepare(
|
|
208
|
+
"INSERT OR REPLACE INTO crypto_metadata (key, value) VALUES (?, ?)",
|
|
209
|
+
);
|
|
210
|
+
stmt.run("db_encrypted", encrypted ? "true" : "false");
|
|
211
|
+
if (passwordHash) {
|
|
212
|
+
stmt.run("db_password_hash", passwordHash);
|
|
213
|
+
stmt.run("db_password_salt", passwordSalt);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Get database encryption status.
|
|
219
|
+
*/
|
|
220
|
+
export function getDbEncryptionStatus(db) {
|
|
221
|
+
ensureCryptoTable(db);
|
|
222
|
+
const row = db
|
|
223
|
+
.prepare("SELECT value FROM crypto_metadata WHERE key = ?")
|
|
224
|
+
.get("db_encrypted");
|
|
225
|
+
return row?.value === "true";
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* Get file info for encrypted file.
|
|
230
|
+
*/
|
|
231
|
+
export function getEncryptedFileInfo(filePath) {
|
|
232
|
+
if (!fs.existsSync(filePath)) {
|
|
233
|
+
throw new Error(`File not found: ${filePath}`);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
const stats = fs.statSync(filePath);
|
|
237
|
+
const encrypted = isEncryptedFile(filePath);
|
|
238
|
+
|
|
239
|
+
return {
|
|
240
|
+
path: path.resolve(filePath),
|
|
241
|
+
size: stats.size,
|
|
242
|
+
encrypted,
|
|
243
|
+
extension: path.extname(filePath),
|
|
244
|
+
modified: stats.mtime.toISOString(),
|
|
245
|
+
};
|
|
246
|
+
}
|
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DID Manager — Decentralized Identity management for CLI.
|
|
3
|
+
* Ed25519 key generation, DID document creation, signing, and verification.
|
|
4
|
+
* Uses Node.js built-in crypto module (no external dependencies).
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import crypto from "crypto";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Ensure DID tables exist.
|
|
11
|
+
*/
|
|
12
|
+
export function ensureDIDTables(db) {
|
|
13
|
+
db.exec(`
|
|
14
|
+
CREATE TABLE IF NOT EXISTS did_identities (
|
|
15
|
+
did TEXT PRIMARY KEY,
|
|
16
|
+
display_name TEXT,
|
|
17
|
+
public_key TEXT NOT NULL,
|
|
18
|
+
secret_key TEXT NOT NULL,
|
|
19
|
+
did_document TEXT,
|
|
20
|
+
is_default INTEGER DEFAULT 0,
|
|
21
|
+
created_at TEXT DEFAULT (datetime('now')),
|
|
22
|
+
updated_at TEXT DEFAULT (datetime('now'))
|
|
23
|
+
)
|
|
24
|
+
`);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Generate an Ed25519 keypair.
|
|
29
|
+
* Returns { publicKey, secretKey } as hex strings.
|
|
30
|
+
*/
|
|
31
|
+
export function generateKeyPair() {
|
|
32
|
+
const { publicKey, privateKey } = crypto.generateKeyPairSync("ed25519", {
|
|
33
|
+
publicKeyEncoding: { type: "spki", format: "der" },
|
|
34
|
+
privateKeyEncoding: { type: "pkcs8", format: "der" },
|
|
35
|
+
});
|
|
36
|
+
return {
|
|
37
|
+
publicKey: publicKey.toString("hex"),
|
|
38
|
+
secretKey: privateKey.toString("hex"),
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Generate a DID from a public key.
|
|
44
|
+
* Format: did:chainless:<base64url-of-sha256-of-pubkey>
|
|
45
|
+
*/
|
|
46
|
+
export function generateDID(publicKeyHex) {
|
|
47
|
+
const hash = crypto
|
|
48
|
+
.createHash("sha256")
|
|
49
|
+
.update(Buffer.from(publicKeyHex, "hex"))
|
|
50
|
+
.digest();
|
|
51
|
+
const id = hash.toString("base64url").slice(0, 32);
|
|
52
|
+
return `did:chainless:${id}`;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Create a DID Document (W3C DID Core spec subset).
|
|
57
|
+
*/
|
|
58
|
+
export function createDIDDocument(did, publicKeyHex, displayName) {
|
|
59
|
+
return {
|
|
60
|
+
"@context": ["https://www.w3.org/ns/did/v1"],
|
|
61
|
+
id: did,
|
|
62
|
+
controller: did,
|
|
63
|
+
verificationMethod: [
|
|
64
|
+
{
|
|
65
|
+
id: `${did}#key-1`,
|
|
66
|
+
type: "Ed25519VerificationKey2020",
|
|
67
|
+
controller: did,
|
|
68
|
+
publicKeyHex: publicKeyHex,
|
|
69
|
+
},
|
|
70
|
+
],
|
|
71
|
+
authentication: [`${did}#key-1`],
|
|
72
|
+
assertionMethod: [`${did}#key-1`],
|
|
73
|
+
service: displayName
|
|
74
|
+
? [
|
|
75
|
+
{
|
|
76
|
+
id: `${did}#profile`,
|
|
77
|
+
type: "ProfileService",
|
|
78
|
+
serviceEndpoint: { name: displayName },
|
|
79
|
+
},
|
|
80
|
+
]
|
|
81
|
+
: [],
|
|
82
|
+
created: new Date().toISOString(),
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Create a new DID identity and store in DB.
|
|
88
|
+
*/
|
|
89
|
+
export function createIdentity(db, displayName) {
|
|
90
|
+
ensureDIDTables(db);
|
|
91
|
+
|
|
92
|
+
const keys = generateKeyPair();
|
|
93
|
+
const did = generateDID(keys.publicKey);
|
|
94
|
+
const doc = createDIDDocument(did, keys.publicKey, displayName);
|
|
95
|
+
|
|
96
|
+
// If no identities exist, this becomes the default
|
|
97
|
+
const count = db.prepare("SELECT COUNT(*) as c FROM did_identities").get().c;
|
|
98
|
+
const isDefault = count === 0 ? 1 : 0;
|
|
99
|
+
|
|
100
|
+
db.prepare(
|
|
101
|
+
`INSERT INTO did_identities (did, display_name, public_key, secret_key, did_document, is_default)
|
|
102
|
+
VALUES (?, ?, ?, ?, ?, ?)`,
|
|
103
|
+
).run(
|
|
104
|
+
did,
|
|
105
|
+
displayName || null,
|
|
106
|
+
keys.publicKey,
|
|
107
|
+
keys.secretKey,
|
|
108
|
+
JSON.stringify(doc),
|
|
109
|
+
isDefault,
|
|
110
|
+
);
|
|
111
|
+
|
|
112
|
+
return {
|
|
113
|
+
did,
|
|
114
|
+
displayName,
|
|
115
|
+
publicKey: keys.publicKey,
|
|
116
|
+
document: doc,
|
|
117
|
+
isDefault: isDefault === 1,
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Get identity by DID or prefix.
|
|
123
|
+
*/
|
|
124
|
+
export function getIdentity(db, didOrPrefix) {
|
|
125
|
+
ensureDIDTables(db);
|
|
126
|
+
return db
|
|
127
|
+
.prepare("SELECT * FROM did_identities WHERE did LIKE ?")
|
|
128
|
+
.get(`${didOrPrefix}%`);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Get all identities.
|
|
133
|
+
*/
|
|
134
|
+
export function getAllIdentities(db) {
|
|
135
|
+
ensureDIDTables(db);
|
|
136
|
+
return db
|
|
137
|
+
.prepare(
|
|
138
|
+
"SELECT * FROM did_identities ORDER BY is_default DESC, created_at DESC",
|
|
139
|
+
)
|
|
140
|
+
.all();
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Get the default identity.
|
|
145
|
+
*/
|
|
146
|
+
export function getDefaultIdentity(db) {
|
|
147
|
+
ensureDIDTables(db);
|
|
148
|
+
return db.prepare("SELECT * FROM did_identities WHERE is_default = 1").get();
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Set an identity as the default.
|
|
153
|
+
*/
|
|
154
|
+
export function setDefaultIdentity(db, did) {
|
|
155
|
+
ensureDIDTables(db);
|
|
156
|
+
const identity = getIdentity(db, did);
|
|
157
|
+
if (!identity) return false;
|
|
158
|
+
|
|
159
|
+
db.prepare("UPDATE did_identities SET is_default = ? WHERE did LIKE ?").run(
|
|
160
|
+
0,
|
|
161
|
+
"%",
|
|
162
|
+
);
|
|
163
|
+
db.prepare("UPDATE did_identities SET is_default = ? WHERE did = ?").run(
|
|
164
|
+
1,
|
|
165
|
+
identity.did,
|
|
166
|
+
);
|
|
167
|
+
return true;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Delete an identity by DID.
|
|
172
|
+
*/
|
|
173
|
+
export function deleteIdentity(db, did) {
|
|
174
|
+
ensureDIDTables(db);
|
|
175
|
+
const identity = getIdentity(db, did);
|
|
176
|
+
if (!identity) return false;
|
|
177
|
+
|
|
178
|
+
const result = db
|
|
179
|
+
.prepare("DELETE FROM did_identities WHERE did = ?")
|
|
180
|
+
.run(identity.did);
|
|
181
|
+
if (result.changes > 0 && identity.is_default) {
|
|
182
|
+
// Promote next identity to default
|
|
183
|
+
const next = db
|
|
184
|
+
.prepare("SELECT did FROM did_identities ORDER BY created_at ASC LIMIT 1")
|
|
185
|
+
.get();
|
|
186
|
+
if (next) {
|
|
187
|
+
db.prepare("UPDATE did_identities SET is_default = 1 WHERE did = ?").run(
|
|
188
|
+
next.did,
|
|
189
|
+
);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
return result.changes > 0;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Sign a message using an identity's secret key.
|
|
197
|
+
* Returns the signature as hex string.
|
|
198
|
+
*/
|
|
199
|
+
export function signMessage(db, did, message) {
|
|
200
|
+
ensureDIDTables(db);
|
|
201
|
+
const identity = getIdentity(db, did);
|
|
202
|
+
if (!identity) throw new Error(`Identity not found: ${did}`);
|
|
203
|
+
|
|
204
|
+
const privateKey = crypto.createPrivateKey({
|
|
205
|
+
key: Buffer.from(identity.secret_key, "hex"),
|
|
206
|
+
format: "der",
|
|
207
|
+
type: "pkcs8",
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
const signature = crypto.sign(null, Buffer.from(message, "utf8"), privateKey);
|
|
211
|
+
return signature.toString("hex");
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Verify a signature against a message and public key.
|
|
216
|
+
*/
|
|
217
|
+
export function verifySignature(publicKeyHex, message, signatureHex) {
|
|
218
|
+
try {
|
|
219
|
+
const publicKey = crypto.createPublicKey({
|
|
220
|
+
key: Buffer.from(publicKeyHex, "hex"),
|
|
221
|
+
format: "der",
|
|
222
|
+
type: "spki",
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
return crypto.verify(
|
|
226
|
+
null,
|
|
227
|
+
Buffer.from(message, "utf8"),
|
|
228
|
+
publicKey,
|
|
229
|
+
Buffer.from(signatureHex, "hex"),
|
|
230
|
+
);
|
|
231
|
+
} catch (_err) {
|
|
232
|
+
// Invalid key or signature format
|
|
233
|
+
return false;
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* Verify a signature using a DID from the database.
|
|
239
|
+
*/
|
|
240
|
+
export function verifyWithDID(db, did, message, signatureHex) {
|
|
241
|
+
const identity = getIdentity(db, did);
|
|
242
|
+
if (!identity) throw new Error(`Identity not found: ${did}`);
|
|
243
|
+
return verifySignature(identity.public_key, message, signatureHex);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* Export identity (public data only, no secret key).
|
|
248
|
+
*/
|
|
249
|
+
export function exportIdentity(db, did) {
|
|
250
|
+
const identity = getIdentity(db, did);
|
|
251
|
+
if (!identity) return null;
|
|
252
|
+
|
|
253
|
+
return {
|
|
254
|
+
did: identity.did,
|
|
255
|
+
displayName: identity.display_name,
|
|
256
|
+
publicKey: identity.public_key,
|
|
257
|
+
document: JSON.parse(identity.did_document || "{}"),
|
|
258
|
+
createdAt: identity.created_at,
|
|
259
|
+
};
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* Resolve a DID — returns the DID document.
|
|
264
|
+
* Currently local-only resolution.
|
|
265
|
+
*/
|
|
266
|
+
export function resolveDID(db, did) {
|
|
267
|
+
const identity = getIdentity(db, did);
|
|
268
|
+
if (!identity) return null;
|
|
269
|
+
return JSON.parse(identity.did_document || "{}");
|
|
270
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ensure UTF-8 encoding on Windows to prevent Chinese character garbling (乱码).
|
|
3
|
+
*
|
|
4
|
+
* Windows console defaults to the system codepage (e.g. CP936/GBK for Chinese),
|
|
5
|
+
* which causes UTF-8 output from Node.js to display as garbled text.
|
|
6
|
+
* This module sets the console codepage to 65001 (UTF-8) and configures
|
|
7
|
+
* Node.js streams to use UTF-8 encoding.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { execSync } from "child_process";
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Call this as early as possible in the process entry point.
|
|
14
|
+
*/
|
|
15
|
+
export function ensureUtf8() {
|
|
16
|
+
if (process.platform !== "win32") return;
|
|
17
|
+
|
|
18
|
+
// Set Windows console codepage to UTF-8
|
|
19
|
+
try {
|
|
20
|
+
execSync("chcp 65001", { stdio: "ignore" });
|
|
21
|
+
} catch (_err) {
|
|
22
|
+
// Ignore - may fail in non-interactive environments
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// Ensure stdout/stderr use UTF-8 encoding
|
|
26
|
+
if (process.stdout.setDefaultEncoding) {
|
|
27
|
+
process.stdout.setDefaultEncoding("utf8");
|
|
28
|
+
}
|
|
29
|
+
if (process.stderr.setDefaultEncoding) {
|
|
30
|
+
process.stderr.setDefaultEncoding("utf8");
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Set environment variable so child processes inherit UTF-8
|
|
34
|
+
process.env.PYTHONIOENCODING = "utf-8";
|
|
35
|
+
process.env.LANG = "en_US.UTF-8";
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Get spawn options that ensure UTF-8 output from child processes on Windows.
|
|
40
|
+
* Use this when spawning cmd.exe or other system processes.
|
|
41
|
+
*
|
|
42
|
+
* @param {object} [opts] - Additional spawn options to merge
|
|
43
|
+
* @returns {object} Spawn options with encoding set to utf-8
|
|
44
|
+
*/
|
|
45
|
+
export function getUtf8SpawnOptions(opts = {}) {
|
|
46
|
+
if (process.platform !== "win32") return { encoding: "utf-8", ...opts };
|
|
47
|
+
|
|
48
|
+
return {
|
|
49
|
+
encoding: "utf-8",
|
|
50
|
+
env: {
|
|
51
|
+
...process.env,
|
|
52
|
+
PYTHONIOENCODING: "utf-8",
|
|
53
|
+
// Force cmd.exe to use UTF-8 codepage
|
|
54
|
+
CHCP: "65001",
|
|
55
|
+
...opts.env,
|
|
56
|
+
},
|
|
57
|
+
...opts,
|
|
58
|
+
};
|
|
59
|
+
}
|