stellavault 0.5.0 → 0.5.2
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 +5 -0
- package/dist/stellavault.js +744 -594
- package/package.json +66 -70
package/dist/stellavault.js
CHANGED
|
@@ -2240,6 +2240,592 @@ var init_file_extractors = __esm({
|
|
|
2240
2240
|
}
|
|
2241
2241
|
});
|
|
2242
2242
|
|
|
2243
|
+
// packages/core/dist/federation/identity.js
|
|
2244
|
+
import { randomBytes, createHash as createHash3, createHmac } from "node:crypto";
|
|
2245
|
+
import { readFileSync as readFileSync11, writeFileSync as writeFileSync10, existsSync as existsSync10, mkdirSync as mkdirSync10, chmodSync } from "node:fs";
|
|
2246
|
+
import { join as join11 } from "node:path";
|
|
2247
|
+
import { homedir as homedir4 } from "node:os";
|
|
2248
|
+
function getOrCreateIdentity(displayName) {
|
|
2249
|
+
if (existsSync10(IDENTITY_FILE)) {
|
|
2250
|
+
const raw = JSON.parse(readFileSync11(IDENTITY_FILE, "utf-8"));
|
|
2251
|
+
return {
|
|
2252
|
+
peerId: raw.peerId,
|
|
2253
|
+
publicKey: Buffer.from(raw.publicKey, "hex"),
|
|
2254
|
+
secretKey: Buffer.from(raw.secretKey, "hex"),
|
|
2255
|
+
displayName: raw.displayName,
|
|
2256
|
+
createdAt: raw.createdAt
|
|
2257
|
+
};
|
|
2258
|
+
}
|
|
2259
|
+
const secretKey = randomBytes(32);
|
|
2260
|
+
const publicKey = createHash3("sha256").update(secretKey).digest();
|
|
2261
|
+
const peerId = createHash3("sha256").update(publicKey).digest("hex").slice(0, 16);
|
|
2262
|
+
const identity = {
|
|
2263
|
+
peerId,
|
|
2264
|
+
publicKey,
|
|
2265
|
+
secretKey,
|
|
2266
|
+
displayName: displayName ?? `node-${peerId.slice(0, 6)}`,
|
|
2267
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
2268
|
+
};
|
|
2269
|
+
mkdirSync10(IDENTITY_DIR, { recursive: true });
|
|
2270
|
+
const content = JSON.stringify({
|
|
2271
|
+
peerId: identity.peerId,
|
|
2272
|
+
publicKey: publicKey.toString("hex"),
|
|
2273
|
+
secretKey: secretKey.toString("hex"),
|
|
2274
|
+
displayName: identity.displayName,
|
|
2275
|
+
createdAt: identity.createdAt
|
|
2276
|
+
}, null, 2);
|
|
2277
|
+
writeFileSync10(IDENTITY_FILE, content, { encoding: "utf-8", mode: 384 });
|
|
2278
|
+
try {
|
|
2279
|
+
chmodSync(IDENTITY_FILE, 384);
|
|
2280
|
+
} catch {
|
|
2281
|
+
}
|
|
2282
|
+
return identity;
|
|
2283
|
+
}
|
|
2284
|
+
var IDENTITY_DIR, IDENTITY_FILE;
|
|
2285
|
+
var init_identity = __esm({
|
|
2286
|
+
"packages/core/dist/federation/identity.js"() {
|
|
2287
|
+
"use strict";
|
|
2288
|
+
IDENTITY_DIR = join11(homedir4(), ".stellavault", "federation");
|
|
2289
|
+
IDENTITY_FILE = join11(IDENTITY_DIR, "identity.json");
|
|
2290
|
+
}
|
|
2291
|
+
});
|
|
2292
|
+
|
|
2293
|
+
// packages/core/dist/federation/node.js
|
|
2294
|
+
import { createHash as createHash4 } from "node:crypto";
|
|
2295
|
+
import { EventEmitter } from "node:events";
|
|
2296
|
+
var FEDERATION_TOPIC, FederationNode;
|
|
2297
|
+
var init_node = __esm({
|
|
2298
|
+
"packages/core/dist/federation/node.js"() {
|
|
2299
|
+
"use strict";
|
|
2300
|
+
init_identity();
|
|
2301
|
+
FEDERATION_TOPIC = createHash4("sha256").update("stellavault-federation-v1").digest();
|
|
2302
|
+
FederationNode = class extends EventEmitter {
|
|
2303
|
+
swarm = null;
|
|
2304
|
+
identity;
|
|
2305
|
+
peers = /* @__PURE__ */ new Map();
|
|
2306
|
+
running = false;
|
|
2307
|
+
documentCount = 0;
|
|
2308
|
+
topTopics = [];
|
|
2309
|
+
constructor(displayName) {
|
|
2310
|
+
super();
|
|
2311
|
+
this.identity = getOrCreateIdentity(displayName);
|
|
2312
|
+
}
|
|
2313
|
+
get peerId() {
|
|
2314
|
+
return this.identity.peerId;
|
|
2315
|
+
}
|
|
2316
|
+
get displayName() {
|
|
2317
|
+
return this.identity.displayName;
|
|
2318
|
+
}
|
|
2319
|
+
get peerCount() {
|
|
2320
|
+
return this.peers.size;
|
|
2321
|
+
}
|
|
2322
|
+
get isRunning() {
|
|
2323
|
+
return this.running;
|
|
2324
|
+
}
|
|
2325
|
+
setLocalStats(documentCount, topTopics) {
|
|
2326
|
+
this.documentCount = documentCount;
|
|
2327
|
+
this.topTopics = topTopics.slice(0, 5);
|
|
2328
|
+
}
|
|
2329
|
+
// Design Ref: §4 — join()
|
|
2330
|
+
async join() {
|
|
2331
|
+
if (this.running)
|
|
2332
|
+
return;
|
|
2333
|
+
const Hyperswarm = (await import("hyperswarm")).default;
|
|
2334
|
+
this.swarm = new Hyperswarm({ maxPeers: 50 });
|
|
2335
|
+
this.swarm.on("connection", (conn, _info) => {
|
|
2336
|
+
this.handleConnection(conn);
|
|
2337
|
+
});
|
|
2338
|
+
const discovery = this.swarm.join(FEDERATION_TOPIC, { server: true, client: true });
|
|
2339
|
+
await discovery.flushed();
|
|
2340
|
+
this.running = true;
|
|
2341
|
+
this.emit("joined", { peerId: this.peerId, topic: FEDERATION_TOPIC.toString("hex").slice(0, 16) });
|
|
2342
|
+
}
|
|
2343
|
+
// Design Ref: §4 — joinDirect() 수동 IP 폴백
|
|
2344
|
+
async joinDirect(host, port) {
|
|
2345
|
+
const net = await import("node:net");
|
|
2346
|
+
const conn = net.connect(port, host);
|
|
2347
|
+
await new Promise((resolve22, reject) => {
|
|
2348
|
+
conn.on("connect", () => {
|
|
2349
|
+
this.handleConnection(conn);
|
|
2350
|
+
resolve22();
|
|
2351
|
+
});
|
|
2352
|
+
conn.on("error", reject);
|
|
2353
|
+
setTimeout(() => reject(new Error("Connection timeout")), 15e3);
|
|
2354
|
+
});
|
|
2355
|
+
if (!this.running)
|
|
2356
|
+
this.running = true;
|
|
2357
|
+
}
|
|
2358
|
+
async leave() {
|
|
2359
|
+
if (!this.running)
|
|
2360
|
+
return;
|
|
2361
|
+
for (const [, peer] of this.peers) {
|
|
2362
|
+
try {
|
|
2363
|
+
this.sendMessage(peer.conn, { type: "leave", peerId: this.peerId });
|
|
2364
|
+
peer.conn.end();
|
|
2365
|
+
} catch {
|
|
2366
|
+
}
|
|
2367
|
+
}
|
|
2368
|
+
this.peers.clear();
|
|
2369
|
+
await this.swarm?.destroy();
|
|
2370
|
+
this.swarm = null;
|
|
2371
|
+
this.running = false;
|
|
2372
|
+
this.emit("left");
|
|
2373
|
+
}
|
|
2374
|
+
getPeers() {
|
|
2375
|
+
return [...this.peers.values()].map((p) => p.info);
|
|
2376
|
+
}
|
|
2377
|
+
// 피어에게 검색 쿼리 전송 (FederatedSearch에서 사용)
|
|
2378
|
+
sendSearchQuery(peerId, queryId, embedding, limit) {
|
|
2379
|
+
const peer = this.peers.get(peerId);
|
|
2380
|
+
if (!peer)
|
|
2381
|
+
return;
|
|
2382
|
+
this.sendMessage(peer.conn, { type: "search_query", queryId, embedding, limit });
|
|
2383
|
+
}
|
|
2384
|
+
// 피어에게 검색 결과 응답 (FederatedSearch에서 사용)
|
|
2385
|
+
sendSearchResult(peerId, queryId, results) {
|
|
2386
|
+
const peer = this.peers.get(peerId);
|
|
2387
|
+
if (!peer)
|
|
2388
|
+
return;
|
|
2389
|
+
this.sendMessage(peer.conn, { type: "search_result", queryId, results });
|
|
2390
|
+
}
|
|
2391
|
+
// --- Private ---
|
|
2392
|
+
handleConnection(conn) {
|
|
2393
|
+
this.sendMessage(conn, {
|
|
2394
|
+
type: "handshake",
|
|
2395
|
+
peerId: this.peerId,
|
|
2396
|
+
displayName: this.identity.displayName,
|
|
2397
|
+
version: "0.1.0",
|
|
2398
|
+
documentCount: this.documentCount,
|
|
2399
|
+
topTopics: this.topTopics
|
|
2400
|
+
});
|
|
2401
|
+
let buffer = "";
|
|
2402
|
+
const MAX_BUFFER = 1024 * 1024;
|
|
2403
|
+
const MAX_MESSAGE = 64 * 1024;
|
|
2404
|
+
conn.on("data", (data) => {
|
|
2405
|
+
buffer += data.toString();
|
|
2406
|
+
if (buffer.length > MAX_BUFFER) {
|
|
2407
|
+
console.error("Federation: buffer overflow from peer, disconnecting");
|
|
2408
|
+
buffer = "";
|
|
2409
|
+
conn.end();
|
|
2410
|
+
return;
|
|
2411
|
+
}
|
|
2412
|
+
const lines = buffer.split("\n");
|
|
2413
|
+
buffer = lines.pop() ?? "";
|
|
2414
|
+
for (const line of lines) {
|
|
2415
|
+
if (!line.trim())
|
|
2416
|
+
continue;
|
|
2417
|
+
if (line.length > MAX_MESSAGE)
|
|
2418
|
+
continue;
|
|
2419
|
+
try {
|
|
2420
|
+
const msg = JSON.parse(line);
|
|
2421
|
+
this.handleMessage(conn, msg);
|
|
2422
|
+
} catch {
|
|
2423
|
+
}
|
|
2424
|
+
}
|
|
2425
|
+
});
|
|
2426
|
+
conn.on("close", () => {
|
|
2427
|
+
for (const [peerId, peer] of this.peers) {
|
|
2428
|
+
if (peer.conn === conn) {
|
|
2429
|
+
this.peers.delete(peerId);
|
|
2430
|
+
this.emit("peer_left", { peerId });
|
|
2431
|
+
break;
|
|
2432
|
+
}
|
|
2433
|
+
}
|
|
2434
|
+
});
|
|
2435
|
+
conn.on("error", () => {
|
|
2436
|
+
});
|
|
2437
|
+
}
|
|
2438
|
+
// MED: 메시지 스키마 기본 검증
|
|
2439
|
+
validateMessage(msg) {
|
|
2440
|
+
if (!msg || typeof msg !== "object" || typeof msg.type !== "string")
|
|
2441
|
+
return false;
|
|
2442
|
+
if (msg.type === "handshake" && (typeof msg.peerId !== "string" || typeof msg.displayName !== "string"))
|
|
2443
|
+
return false;
|
|
2444
|
+
if (msg.type === "search_query" && (!Array.isArray(msg.embedding) || msg.embedding.length !== 384))
|
|
2445
|
+
return false;
|
|
2446
|
+
if (msg.type === "search_result" && !Array.isArray(msg.results))
|
|
2447
|
+
return false;
|
|
2448
|
+
return true;
|
|
2449
|
+
}
|
|
2450
|
+
handleMessage(conn, msg) {
|
|
2451
|
+
if (!this.validateMessage(msg))
|
|
2452
|
+
return;
|
|
2453
|
+
switch (msg.type) {
|
|
2454
|
+
case "handshake": {
|
|
2455
|
+
const safeName = (msg.displayName ?? "").slice(0, 50);
|
|
2456
|
+
const peerInfo = {
|
|
2457
|
+
peerId: msg.peerId,
|
|
2458
|
+
displayName: safeName,
|
|
2459
|
+
documentCount: Math.min(msg.documentCount ?? 0, 1e6),
|
|
2460
|
+
// 합리적 상한
|
|
2461
|
+
topTopics: (msg.topTopics ?? []).slice(0, 10),
|
|
2462
|
+
joinedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2463
|
+
lastSeen: (/* @__PURE__ */ new Date()).toISOString()
|
|
2464
|
+
};
|
|
2465
|
+
this.peers.set(msg.peerId, { info: peerInfo, conn });
|
|
2466
|
+
this.emit("peer_joined", peerInfo);
|
|
2467
|
+
break;
|
|
2468
|
+
}
|
|
2469
|
+
case "search_query": {
|
|
2470
|
+
this.emit("search_request", {
|
|
2471
|
+
peerId: msg.queryId,
|
|
2472
|
+
// queryId를 추적용으로 사용
|
|
2473
|
+
queryId: msg.queryId,
|
|
2474
|
+
embedding: msg.embedding,
|
|
2475
|
+
limit: msg.limit,
|
|
2476
|
+
// respond 함수: 호출 측에서 사용
|
|
2477
|
+
respondTo: (() => {
|
|
2478
|
+
for (const [pid, peer] of this.peers) {
|
|
2479
|
+
if (peer.conn === conn)
|
|
2480
|
+
return pid;
|
|
2481
|
+
}
|
|
2482
|
+
return null;
|
|
2483
|
+
})()
|
|
2484
|
+
});
|
|
2485
|
+
break;
|
|
2486
|
+
}
|
|
2487
|
+
case "search_result": {
|
|
2488
|
+
this.emit("search_response", {
|
|
2489
|
+
queryId: msg.queryId,
|
|
2490
|
+
results: msg.results,
|
|
2491
|
+
peerId: (() => {
|
|
2492
|
+
for (const [pid, peer] of this.peers) {
|
|
2493
|
+
if (peer.conn === conn)
|
|
2494
|
+
return pid;
|
|
2495
|
+
}
|
|
2496
|
+
return "unknown";
|
|
2497
|
+
})()
|
|
2498
|
+
});
|
|
2499
|
+
break;
|
|
2500
|
+
}
|
|
2501
|
+
case "leave": {
|
|
2502
|
+
this.peers.delete(msg.peerId);
|
|
2503
|
+
this.emit("peer_left", { peerId: msg.peerId });
|
|
2504
|
+
break;
|
|
2505
|
+
}
|
|
2506
|
+
}
|
|
2507
|
+
}
|
|
2508
|
+
// Design Ref: §7 — JSON + newline delimiter
|
|
2509
|
+
sendMessage(conn, msg) {
|
|
2510
|
+
try {
|
|
2511
|
+
conn.write(JSON.stringify(msg) + "\n");
|
|
2512
|
+
} catch {
|
|
2513
|
+
}
|
|
2514
|
+
}
|
|
2515
|
+
};
|
|
2516
|
+
}
|
|
2517
|
+
});
|
|
2518
|
+
|
|
2519
|
+
// packages/core/dist/federation/privacy.js
|
|
2520
|
+
function maskSnippet(snippet, maskRate = 0.3) {
|
|
2521
|
+
const words = snippet.split(/\s+/);
|
|
2522
|
+
return words.map((w) => {
|
|
2523
|
+
if (Math.random() < maskRate && w.length > 2) {
|
|
2524
|
+
return w[0] + "***";
|
|
2525
|
+
}
|
|
2526
|
+
return w;
|
|
2527
|
+
}).join(" ");
|
|
2528
|
+
}
|
|
2529
|
+
var init_privacy = __esm({
|
|
2530
|
+
"packages/core/dist/federation/privacy.js"() {
|
|
2531
|
+
"use strict";
|
|
2532
|
+
}
|
|
2533
|
+
});
|
|
2534
|
+
|
|
2535
|
+
// packages/core/dist/federation/sharing.js
|
|
2536
|
+
import { readFileSync as readFileSync12, writeFileSync as writeFileSync11, existsSync as existsSync11, mkdirSync as mkdirSync11 } from "node:fs";
|
|
2537
|
+
import { join as join12 } from "node:path";
|
|
2538
|
+
import { homedir as homedir5 } from "node:os";
|
|
2539
|
+
function loadSharingConfig() {
|
|
2540
|
+
if (existsSync11(SHARING_FILE)) {
|
|
2541
|
+
const raw = JSON.parse(readFileSync12(SHARING_FILE, "utf-8"));
|
|
2542
|
+
return { ...DEFAULT_CONFIG2, ...raw };
|
|
2543
|
+
}
|
|
2544
|
+
return { ...DEFAULT_CONFIG2 };
|
|
2545
|
+
}
|
|
2546
|
+
function saveSharingConfig(config) {
|
|
2547
|
+
mkdirSync11(join12(homedir5(), ".stellavault", "federation"), { recursive: true });
|
|
2548
|
+
writeFileSync11(SHARING_FILE, JSON.stringify(config, null, 2), "utf-8");
|
|
2549
|
+
}
|
|
2550
|
+
function getDocumentLevel(doc, config) {
|
|
2551
|
+
const cfg = config ?? loadSharingConfig();
|
|
2552
|
+
if (cfg.blockedDocIds.includes(doc.id))
|
|
2553
|
+
return 0;
|
|
2554
|
+
if (cfg.blockSensitivePatterns) {
|
|
2555
|
+
for (const pattern of SENSITIVE_PATTERNS) {
|
|
2556
|
+
if (pattern.test(doc.content))
|
|
2557
|
+
return 0;
|
|
2558
|
+
}
|
|
2559
|
+
}
|
|
2560
|
+
let matchedLevel = null;
|
|
2561
|
+
for (const rule of cfg.rules) {
|
|
2562
|
+
if (rule.type === "doc" && rule.pattern === doc.id) {
|
|
2563
|
+
return rule.level;
|
|
2564
|
+
}
|
|
2565
|
+
}
|
|
2566
|
+
const docTags = doc.tags.map((t2) => t2.toLowerCase());
|
|
2567
|
+
for (const rule of cfg.rules) {
|
|
2568
|
+
if (rule.type === "tag" && docTags.includes(rule.pattern.toLowerCase())) {
|
|
2569
|
+
if (matchedLevel === null || rule.level < matchedLevel) {
|
|
2570
|
+
matchedLevel = rule.level;
|
|
2571
|
+
}
|
|
2572
|
+
}
|
|
2573
|
+
}
|
|
2574
|
+
for (const rule of cfg.rules) {
|
|
2575
|
+
if (rule.type === "folder" && doc.filePath.startsWith(rule.pattern)) {
|
|
2576
|
+
if (matchedLevel === null || rule.level < matchedLevel) {
|
|
2577
|
+
matchedLevel = rule.level;
|
|
2578
|
+
}
|
|
2579
|
+
}
|
|
2580
|
+
}
|
|
2581
|
+
return matchedLevel ?? cfg.defaultLevel;
|
|
2582
|
+
}
|
|
2583
|
+
function isDocumentShareable(doc, config) {
|
|
2584
|
+
return getDocumentLevel(doc, config) > 0;
|
|
2585
|
+
}
|
|
2586
|
+
function approveRequest(requestId) {
|
|
2587
|
+
const cfg = loadSharingConfig();
|
|
2588
|
+
const req = cfg.pendingRequests.find((r) => r.requestId === requestId);
|
|
2589
|
+
if (!req)
|
|
2590
|
+
return false;
|
|
2591
|
+
req.status = "approved";
|
|
2592
|
+
saveSharingConfig(cfg);
|
|
2593
|
+
return true;
|
|
2594
|
+
}
|
|
2595
|
+
function denyRequest(requestId) {
|
|
2596
|
+
const cfg = loadSharingConfig();
|
|
2597
|
+
const req = cfg.pendingRequests.find((r) => r.requestId === requestId);
|
|
2598
|
+
if (!req)
|
|
2599
|
+
return false;
|
|
2600
|
+
req.status = "denied";
|
|
2601
|
+
saveSharingConfig(cfg);
|
|
2602
|
+
return true;
|
|
2603
|
+
}
|
|
2604
|
+
function getPendingRequests() {
|
|
2605
|
+
return loadSharingConfig().pendingRequests.filter((r) => r.status === "pending");
|
|
2606
|
+
}
|
|
2607
|
+
function sanitizeSnippet(snippet) {
|
|
2608
|
+
let safe = snippet;
|
|
2609
|
+
for (const pattern of SENSITIVE_PATTERNS) {
|
|
2610
|
+
safe = safe.replace(pattern, "[REDACTED]");
|
|
2611
|
+
}
|
|
2612
|
+
return safe;
|
|
2613
|
+
}
|
|
2614
|
+
function setTagLevel(tag, level) {
|
|
2615
|
+
const cfg = loadSharingConfig();
|
|
2616
|
+
const existing = cfg.rules.findIndex((r) => r.type === "tag" && r.pattern.toLowerCase() === tag.toLowerCase());
|
|
2617
|
+
if (existing >= 0)
|
|
2618
|
+
cfg.rules[existing].level = level;
|
|
2619
|
+
else
|
|
2620
|
+
cfg.rules.push({ pattern: tag.toLowerCase(), type: "tag", level });
|
|
2621
|
+
saveSharingConfig(cfg);
|
|
2622
|
+
}
|
|
2623
|
+
function setFolderLevel(folder, level) {
|
|
2624
|
+
const cfg = loadSharingConfig();
|
|
2625
|
+
const existing = cfg.rules.findIndex((r) => r.type === "folder" && r.pattern === folder);
|
|
2626
|
+
if (existing >= 0)
|
|
2627
|
+
cfg.rules[existing].level = level;
|
|
2628
|
+
else
|
|
2629
|
+
cfg.rules.push({ pattern: folder, type: "folder", level });
|
|
2630
|
+
saveSharingConfig(cfg);
|
|
2631
|
+
}
|
|
2632
|
+
function setNodeLevel(level) {
|
|
2633
|
+
const cfg = loadSharingConfig();
|
|
2634
|
+
cfg.myNodeLevel = level;
|
|
2635
|
+
saveSharingConfig(cfg);
|
|
2636
|
+
}
|
|
2637
|
+
function getSharingSummary(config) {
|
|
2638
|
+
const cfg = config ?? loadSharingConfig();
|
|
2639
|
+
const lines = [];
|
|
2640
|
+
lines.push(`My Node Level: ${LEVEL_ICONS[cfg.myNodeLevel]} Level ${cfg.myNodeLevel} (${LEVEL_LABELS[cfg.myNodeLevel]})`);
|
|
2641
|
+
lines.push(`Default Doc Level: ${LEVEL_ICONS[cfg.defaultLevel]} Level ${cfg.defaultLevel}`);
|
|
2642
|
+
lines.push(`Credit Multiplier: ${LEVEL_CREDIT_MULTIPLIER[cfg.myNodeLevel]}x`);
|
|
2643
|
+
lines.push("");
|
|
2644
|
+
lines.push("Rules:");
|
|
2645
|
+
for (const rule of cfg.rules) {
|
|
2646
|
+
lines.push(` ${LEVEL_ICONS[rule.level]} [${rule.type}] ${rule.pattern} \u2192 Level ${rule.level}`);
|
|
2647
|
+
}
|
|
2648
|
+
if (cfg.blockedDocIds.length > 0) {
|
|
2649
|
+
lines.push(` \u{1F6AB} ${cfg.blockedDocIds.length} docs individually blocked`);
|
|
2650
|
+
}
|
|
2651
|
+
lines.push("");
|
|
2652
|
+
const pending = cfg.pendingRequests.filter((r) => r.status === "pending");
|
|
2653
|
+
if (pending.length > 0) {
|
|
2654
|
+
lines.push(`\u{1F4EC} ${pending.length} pending full-text requests`);
|
|
2655
|
+
}
|
|
2656
|
+
lines.push(`Sensitive pattern filter: ${cfg.blockSensitivePatterns ? "ON" : "OFF"}`);
|
|
2657
|
+
return lines.join("\n");
|
|
2658
|
+
}
|
|
2659
|
+
var LEVEL_LABELS, LEVEL_ICONS, LEVEL_CREDIT_MULTIPLIER, SHARING_FILE, SENSITIVE_PATTERNS, DEFAULT_CONFIG2;
|
|
2660
|
+
var init_sharing = __esm({
|
|
2661
|
+
"packages/core/dist/federation/sharing.js"() {
|
|
2662
|
+
"use strict";
|
|
2663
|
+
LEVEL_LABELS = {
|
|
2664
|
+
0: "Blocked (not searchable)",
|
|
2665
|
+
1: "Title + similarity only",
|
|
2666
|
+
2: "Title + snippet (50 chars)",
|
|
2667
|
+
3: "Full text on request (approval needed)",
|
|
2668
|
+
4: "Full text auto-shared"
|
|
2669
|
+
};
|
|
2670
|
+
LEVEL_ICONS = {
|
|
2671
|
+
0: "\u{1F6AB}",
|
|
2672
|
+
1: "\u{1F4CC}",
|
|
2673
|
+
2: "\u{1F4DD}",
|
|
2674
|
+
3: "\u{1F4D6}",
|
|
2675
|
+
4: "\u{1F310}"
|
|
2676
|
+
};
|
|
2677
|
+
LEVEL_CREDIT_MULTIPLIER = {
|
|
2678
|
+
0: 0,
|
|
2679
|
+
1: 1,
|
|
2680
|
+
2: 2,
|
|
2681
|
+
3: 5,
|
|
2682
|
+
4: 10
|
|
2683
|
+
};
|
|
2684
|
+
SHARING_FILE = join12(homedir5(), ".stellavault", "federation", "sharing.json");
|
|
2685
|
+
SENSITIVE_PATTERNS = [
|
|
2686
|
+
/\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b/,
|
|
2687
|
+
/\b\d{3}[-.]?\d{4}[-.]?\d{4}\b/,
|
|
2688
|
+
/\b(sk-|pk-|api[_-]?key|token|secret)[a-zA-Z0-9_-]{10,}\b/i,
|
|
2689
|
+
/\bpassword\s*[:=]\s*\S+/i,
|
|
2690
|
+
/\b\d{6}[-]\d{7}\b/
|
|
2691
|
+
];
|
|
2692
|
+
DEFAULT_CONFIG2 = {
|
|
2693
|
+
defaultLevel: 2,
|
|
2694
|
+
// 기본: 스니펫까지
|
|
2695
|
+
myNodeLevel: 2,
|
|
2696
|
+
// 내 노드 기본: 스니펫까지
|
|
2697
|
+
rules: [
|
|
2698
|
+
// 기본 규칙
|
|
2699
|
+
{ pattern: "public", type: "tag", level: 4 },
|
|
2700
|
+
{ pattern: "opensource", type: "tag", level: 4 },
|
|
2701
|
+
{ pattern: "personal", type: "tag", level: 1 },
|
|
2702
|
+
{ pattern: "private", type: "tag", level: 0 },
|
|
2703
|
+
{ pattern: "secret", type: "tag", level: 0 },
|
|
2704
|
+
{ pattern: "diary", type: "tag", level: 0 },
|
|
2705
|
+
{ pattern: "salary", type: "tag", level: 0 },
|
|
2706
|
+
{ pattern: "password", type: "tag", level: 0 },
|
|
2707
|
+
{ pattern: "credential", type: "tag", level: 0 },
|
|
2708
|
+
{ pattern: "03_Daily", type: "folder", level: 1 },
|
|
2709
|
+
{ pattern: "06_Archive", type: "folder", level: 1 },
|
|
2710
|
+
{ pattern: ".obsidian", type: "folder", level: 0 }
|
|
2711
|
+
],
|
|
2712
|
+
blockedDocIds: [],
|
|
2713
|
+
blockSensitivePatterns: true,
|
|
2714
|
+
pendingRequests: []
|
|
2715
|
+
};
|
|
2716
|
+
}
|
|
2717
|
+
});
|
|
2718
|
+
|
|
2719
|
+
// packages/core/dist/federation/search.js
|
|
2720
|
+
import { randomUUID } from "node:crypto";
|
|
2721
|
+
var FederatedSearch;
|
|
2722
|
+
var init_search = __esm({
|
|
2723
|
+
"packages/core/dist/federation/search.js"() {
|
|
2724
|
+
"use strict";
|
|
2725
|
+
init_privacy();
|
|
2726
|
+
init_sharing();
|
|
2727
|
+
FederatedSearch = class {
|
|
2728
|
+
node;
|
|
2729
|
+
store;
|
|
2730
|
+
embedder;
|
|
2731
|
+
additionalStores = [];
|
|
2732
|
+
constructor(node, store, embedder) {
|
|
2733
|
+
this.node = node;
|
|
2734
|
+
this.store = store;
|
|
2735
|
+
this.embedder = embedder;
|
|
2736
|
+
}
|
|
2737
|
+
// Multi-vault: 추가 store 등록 (Federation 검색 응답 시 전체 vault 검색)
|
|
2738
|
+
addStore(store) {
|
|
2739
|
+
this.additionalStores.push(store);
|
|
2740
|
+
}
|
|
2741
|
+
// Design Ref: §5 — search() 요청 측
|
|
2742
|
+
async search(query, options = {}) {
|
|
2743
|
+
const { limit = 5, timeout = 5e3 } = options;
|
|
2744
|
+
const peers = this.node.getPeers();
|
|
2745
|
+
if (peers.length === 0) {
|
|
2746
|
+
return [];
|
|
2747
|
+
}
|
|
2748
|
+
const embedding = await this.embedder.embed(query);
|
|
2749
|
+
const queryId = randomUUID().slice(0, 8);
|
|
2750
|
+
const results = [];
|
|
2751
|
+
const peerMap = new Map(peers.map((p) => [p.peerId, p.displayName]));
|
|
2752
|
+
const responsePromises = peers.map((peer) => {
|
|
2753
|
+
return new Promise((resolve22) => {
|
|
2754
|
+
const timer = setTimeout(resolve22, timeout);
|
|
2755
|
+
const handler = (data) => {
|
|
2756
|
+
if (data.queryId !== queryId)
|
|
2757
|
+
return;
|
|
2758
|
+
for (const r of data.results) {
|
|
2759
|
+
results.push({
|
|
2760
|
+
title: r.title,
|
|
2761
|
+
similarity: r.similarity,
|
|
2762
|
+
snippet: r.snippet,
|
|
2763
|
+
peerId: peer.peerId,
|
|
2764
|
+
peerName: peer.displayName
|
|
2765
|
+
});
|
|
2766
|
+
}
|
|
2767
|
+
clearTimeout(timer);
|
|
2768
|
+
this.node.removeListener("search_response", handler);
|
|
2769
|
+
resolve22();
|
|
2770
|
+
};
|
|
2771
|
+
this.node.on("search_response", handler);
|
|
2772
|
+
this.node.sendSearchQuery(peer.peerId, queryId, Array.from(embedding), limit);
|
|
2773
|
+
});
|
|
2774
|
+
});
|
|
2775
|
+
await Promise.allSettled(responsePromises);
|
|
2776
|
+
return results.sort((a, b) => b.similarity - a.similarity).slice(0, limit);
|
|
2777
|
+
}
|
|
2778
|
+
// Design Ref: §5 — startResponder() 피어 요청 수신 측
|
|
2779
|
+
startResponder() {
|
|
2780
|
+
this.node.on("search_request", async (req) => {
|
|
2781
|
+
if (!req.respondTo)
|
|
2782
|
+
return;
|
|
2783
|
+
try {
|
|
2784
|
+
const allStores = [this.store, ...this.additionalStores];
|
|
2785
|
+
const allScored = await Promise.all(allStores.map((s) => s.searchSemantic(req.embedding, req.limit).catch(() => [])));
|
|
2786
|
+
const scored = allScored.flat().sort((a, b) => b.score - a.score).slice(0, req.limit);
|
|
2787
|
+
const safe = [];
|
|
2788
|
+
for (const s of scored) {
|
|
2789
|
+
const chunk = await this.store.getChunk(s.chunkId);
|
|
2790
|
+
if (!chunk)
|
|
2791
|
+
continue;
|
|
2792
|
+
const doc = await this.store.getDocument(chunk.documentId);
|
|
2793
|
+
if (!doc)
|
|
2794
|
+
continue;
|
|
2795
|
+
if (!isDocumentShareable({ tags: doc.tags, filePath: doc.filePath, id: doc.id, content: doc.content }))
|
|
2796
|
+
continue;
|
|
2797
|
+
safe.push({
|
|
2798
|
+
title: doc.title ?? chunk.heading ?? "Untitled",
|
|
2799
|
+
similarity: Math.round(s.score * 1e3) / 1e3,
|
|
2800
|
+
snippet: sanitizeSnippet(maskSnippet(chunk.content.slice(0, 50), 0.2))
|
|
2801
|
+
});
|
|
2802
|
+
}
|
|
2803
|
+
this.node.sendSearchResult(req.respondTo, req.queryId, safe);
|
|
2804
|
+
} catch (err) {
|
|
2805
|
+
this.node.sendSearchResult(req.respondTo, req.queryId, []);
|
|
2806
|
+
}
|
|
2807
|
+
});
|
|
2808
|
+
}
|
|
2809
|
+
};
|
|
2810
|
+
}
|
|
2811
|
+
});
|
|
2812
|
+
|
|
2813
|
+
// packages/core/dist/federation/index.js
|
|
2814
|
+
var federation_exports = {};
|
|
2815
|
+
__export(federation_exports, {
|
|
2816
|
+
FederatedSearch: () => FederatedSearch,
|
|
2817
|
+
FederationNode: () => FederationNode,
|
|
2818
|
+
getOrCreateIdentity: () => getOrCreateIdentity
|
|
2819
|
+
});
|
|
2820
|
+
var init_federation = __esm({
|
|
2821
|
+
"packages/core/dist/federation/index.js"() {
|
|
2822
|
+
"use strict";
|
|
2823
|
+
init_node();
|
|
2824
|
+
init_search();
|
|
2825
|
+
init_identity();
|
|
2826
|
+
}
|
|
2827
|
+
});
|
|
2828
|
+
|
|
2243
2829
|
// packages/cli/dist/index.js
|
|
2244
2830
|
import { Command } from "commander";
|
|
2245
2831
|
|
|
@@ -5312,12 +5898,108 @@ ${content}`;
|
|
|
5312
5898
|
res.status(500).json({ error: "Clip failed" });
|
|
5313
5899
|
}
|
|
5314
5900
|
});
|
|
5315
|
-
|
|
5316
|
-
|
|
5317
|
-
|
|
5318
|
-
|
|
5319
|
-
|
|
5320
|
-
|
|
5901
|
+
let federationNode = null;
|
|
5902
|
+
let federationAvailable = null;
|
|
5903
|
+
async function probeFederationAvailable() {
|
|
5904
|
+
if (federationAvailable !== null)
|
|
5905
|
+
return federationAvailable;
|
|
5906
|
+
try {
|
|
5907
|
+
await import("hyperswarm");
|
|
5908
|
+
federationAvailable = true;
|
|
5909
|
+
} catch {
|
|
5910
|
+
federationAvailable = false;
|
|
5911
|
+
}
|
|
5912
|
+
return federationAvailable;
|
|
5913
|
+
}
|
|
5914
|
+
app.get("/api/federate/status", async (_req, res) => {
|
|
5915
|
+
const available = await probeFederationAvailable();
|
|
5916
|
+
if (!available) {
|
|
5917
|
+
return res.json({ available: false, active: false, peerCount: 0, peers: [], displayName: null, peerId: null });
|
|
5918
|
+
}
|
|
5919
|
+
if (!federationNode || !federationNode.isRunning) {
|
|
5920
|
+
return res.json({ available: true, active: false, peerCount: 0, peers: [], displayName: null, peerId: null });
|
|
5921
|
+
}
|
|
5922
|
+
try {
|
|
5923
|
+
const peers = federationNode.getPeers().map((p) => ({
|
|
5924
|
+
peerId: p.peerId || "",
|
|
5925
|
+
displayName: p.displayName || "Peer",
|
|
5926
|
+
documentCount: p.documentCount ?? 0,
|
|
5927
|
+
topTopics: p.topTopics ?? []
|
|
5928
|
+
}));
|
|
5929
|
+
res.json({
|
|
5930
|
+
available: true,
|
|
5931
|
+
active: true,
|
|
5932
|
+
peerCount: peers.length,
|
|
5933
|
+
peers,
|
|
5934
|
+
displayName: federationNode.displayName,
|
|
5935
|
+
peerId: federationNode.peerId
|
|
5936
|
+
});
|
|
5937
|
+
} catch (err) {
|
|
5938
|
+
console.error(err);
|
|
5939
|
+
res.json({ available: true, active: false, peerCount: 0, peers: [], displayName: null, peerId: null });
|
|
5940
|
+
}
|
|
5941
|
+
});
|
|
5942
|
+
app.post("/api/federate/join", async (req, res) => {
|
|
5943
|
+
const available = await probeFederationAvailable();
|
|
5944
|
+
if (!available) {
|
|
5945
|
+
return res.status(501).json({
|
|
5946
|
+
success: false,
|
|
5947
|
+
error: "federation-unavailable",
|
|
5948
|
+
message: 'Federation requires optional dependency "hyperswarm". Reinstall stellavault with hyperswarm enabled.'
|
|
5949
|
+
});
|
|
5950
|
+
}
|
|
5951
|
+
if (federationNode && federationNode.isRunning) {
|
|
5952
|
+
return res.json({
|
|
5953
|
+
success: true,
|
|
5954
|
+
active: true,
|
|
5955
|
+
displayName: federationNode.displayName,
|
|
5956
|
+
peerId: federationNode.peerId,
|
|
5957
|
+
peerCount: federationNode.peerCount,
|
|
5958
|
+
message: "Already joined"
|
|
5959
|
+
});
|
|
5960
|
+
}
|
|
5961
|
+
try {
|
|
5962
|
+
const { FederationNode: FederationNode2 } = await Promise.resolve().then(() => (init_federation(), federation_exports));
|
|
5963
|
+
const displayName = req.body?.displayName || void 0;
|
|
5964
|
+
federationNode = new FederationNode2(displayName);
|
|
5965
|
+
try {
|
|
5966
|
+
const stats = await store.getStats();
|
|
5967
|
+
federationNode.setLocalStats(stats.documentCount ?? 0, []);
|
|
5968
|
+
} catch {
|
|
5969
|
+
}
|
|
5970
|
+
await federationNode.join();
|
|
5971
|
+
res.json({
|
|
5972
|
+
success: true,
|
|
5973
|
+
active: true,
|
|
5974
|
+
displayName: federationNode.displayName,
|
|
5975
|
+
peerId: federationNode.peerId,
|
|
5976
|
+
peerCount: federationNode.peerCount
|
|
5977
|
+
});
|
|
5978
|
+
} catch (err) {
|
|
5979
|
+
console.error("Federation join failed:", err);
|
|
5980
|
+
federationNode = null;
|
|
5981
|
+
res.status(500).json({ success: false, error: err?.message || "Federation join failed" });
|
|
5982
|
+
}
|
|
5983
|
+
});
|
|
5984
|
+
app.post("/api/federate/leave", async (_req, res) => {
|
|
5985
|
+
if (!federationNode || !federationNode.isRunning) {
|
|
5986
|
+
return res.json({ success: true, active: false, message: "Not active" });
|
|
5987
|
+
}
|
|
5988
|
+
try {
|
|
5989
|
+
await federationNode.leave();
|
|
5990
|
+
federationNode = null;
|
|
5991
|
+
res.json({ success: true, active: false });
|
|
5992
|
+
} catch (err) {
|
|
5993
|
+
console.error("Federation leave failed:", err);
|
|
5994
|
+
res.status(500).json({ success: false, error: err?.message || "Federation leave failed" });
|
|
5995
|
+
}
|
|
5996
|
+
});
|
|
5997
|
+
return {
|
|
5998
|
+
async start() {
|
|
5999
|
+
return new Promise((resolve22) => {
|
|
6000
|
+
app.listen(port, "127.0.0.1", () => {
|
|
6001
|
+
console.error(`\u{1F310} API server running at http://127.0.0.1:${port}`);
|
|
6002
|
+
resolve22();
|
|
5321
6003
|
});
|
|
5322
6004
|
});
|
|
5323
6005
|
},
|
|
@@ -5560,18 +6242,18 @@ init_zettelkasten();
|
|
|
5560
6242
|
init_ingest_pipeline();
|
|
5561
6243
|
|
|
5562
6244
|
// packages/core/dist/multi-vault/index.js
|
|
5563
|
-
import { readFileSync as
|
|
5564
|
-
import { join as
|
|
5565
|
-
import { homedir as
|
|
5566
|
-
var VAULTS_FILE =
|
|
6245
|
+
import { readFileSync as readFileSync13, writeFileSync as writeFileSync12, existsSync as existsSync12, mkdirSync as mkdirSync12 } from "node:fs";
|
|
6246
|
+
import { join as join13 } from "node:path";
|
|
6247
|
+
import { homedir as homedir6 } from "node:os";
|
|
6248
|
+
var VAULTS_FILE = join13(homedir6(), ".stellavault", "vaults.json");
|
|
5567
6249
|
function loadVaults() {
|
|
5568
|
-
if (!
|
|
6250
|
+
if (!existsSync12(VAULTS_FILE))
|
|
5569
6251
|
return [];
|
|
5570
|
-
return JSON.parse(
|
|
6252
|
+
return JSON.parse(readFileSync13(VAULTS_FILE, "utf-8"));
|
|
5571
6253
|
}
|
|
5572
6254
|
function saveVaults(vaults) {
|
|
5573
|
-
|
|
5574
|
-
|
|
6255
|
+
mkdirSync12(join13(homedir6(), ".stellavault"), { recursive: true });
|
|
6256
|
+
writeFileSync12(VAULTS_FILE, JSON.stringify(vaults, null, 2), "utf-8");
|
|
5575
6257
|
}
|
|
5576
6258
|
function addVault(id, name, vaultPath, dbPath, shared = false) {
|
|
5577
6259
|
const vaults = loadVaults();
|
|
@@ -5601,7 +6283,7 @@ async function searchAllVaults(query, embedder, createStore, options = {}) {
|
|
|
5601
6283
|
const embedding = await embedder.embed(query);
|
|
5602
6284
|
const searches = vaults.map(async (vault2) => {
|
|
5603
6285
|
try {
|
|
5604
|
-
if (!
|
|
6286
|
+
if (!existsSync12(vault2.dbPath))
|
|
5605
6287
|
return;
|
|
5606
6288
|
const store = createStore(vault2.dbPath);
|
|
5607
6289
|
await store.initialize();
|
|
@@ -5630,8 +6312,8 @@ async function searchAllVaults(query, embedder, createStore, options = {}) {
|
|
|
5630
6312
|
|
|
5631
6313
|
// packages/core/dist/capture/voice.js
|
|
5632
6314
|
import { execFileSync, execSync } from "node:child_process";
|
|
5633
|
-
import { writeFileSync as
|
|
5634
|
-
import { join as
|
|
6315
|
+
import { writeFileSync as writeFileSync13, mkdirSync as mkdirSync13, existsSync as existsSync13 } from "node:fs";
|
|
6316
|
+
import { join as join14, basename as basename4 } from "node:path";
|
|
5635
6317
|
var ALLOWED_MODELS = ["tiny", "base", "small", "medium", "large"];
|
|
5636
6318
|
var ALLOWED_LANGUAGES = ["auto", "en", "ko", "ja", "zh", "es", "fr", "de", "it", "pt", "ru", "ar", "hi"];
|
|
5637
6319
|
function validateModel(model) {
|
|
@@ -5654,7 +6336,7 @@ function isWhisperAvailable() {
|
|
|
5654
6336
|
}
|
|
5655
6337
|
async function transcribeAudio(audioPath, options = {}) {
|
|
5656
6338
|
const { model = "base", language } = options;
|
|
5657
|
-
if (!
|
|
6339
|
+
if (!existsSync13(audioPath)) {
|
|
5658
6340
|
throw new Error(`Audio file not found: ${audioPath}`);
|
|
5659
6341
|
}
|
|
5660
6342
|
if (isWhisperAvailable()) {
|
|
@@ -5662,12 +6344,12 @@ async function transcribeAudio(audioPath, options = {}) {
|
|
|
5662
6344
|
const args = [audioPath, "--model", safeModel, "--output_format", "txt", "--output_dir", "/tmp/sv-whisper"];
|
|
5663
6345
|
if (language)
|
|
5664
6346
|
args.push("--language", validateLanguage(language));
|
|
5665
|
-
|
|
6347
|
+
mkdirSync13("/tmp/sv-whisper", { recursive: true });
|
|
5666
6348
|
try {
|
|
5667
6349
|
execFileSync("whisper", args, { stdio: "pipe", timeout: 3e5 });
|
|
5668
6350
|
const outputName = basename4(audioPath).replace(/\.[^.]+$/, ".txt");
|
|
5669
6351
|
const { readFileSync: readFileSync18 } = await import("node:fs");
|
|
5670
|
-
return readFileSync18(
|
|
6352
|
+
return readFileSync18(join14("/tmp/sv-whisper", outputName), "utf-8").trim();
|
|
5671
6353
|
} catch (err) {
|
|
5672
6354
|
throw new Error(`Whisper failed: ${err instanceof Error ? err.message : err}`);
|
|
5673
6355
|
}
|
|
@@ -5724,10 +6406,10 @@ async function captureVoice(audioPath, options) {
|
|
|
5724
6406
|
transcript,
|
|
5725
6407
|
""
|
|
5726
6408
|
].join("\n");
|
|
5727
|
-
const dir =
|
|
5728
|
-
|
|
5729
|
-
const filePath =
|
|
5730
|
-
|
|
6409
|
+
const dir = join14(vaultPath, folder);
|
|
6410
|
+
mkdirSync13(dir, { recursive: true });
|
|
6411
|
+
const filePath = join14(dir, `${date.slice(0, 10)} ${safeTitle}.md`);
|
|
6412
|
+
writeFileSync13(filePath, content, "utf-8");
|
|
5731
6413
|
return {
|
|
5732
6414
|
title: safeTitle,
|
|
5733
6415
|
filePath,
|
|
@@ -5748,15 +6430,15 @@ async function captureVoice(audioPath, options) {
|
|
|
5748
6430
|
}
|
|
5749
6431
|
|
|
5750
6432
|
// packages/core/dist/cloud/sync.js
|
|
5751
|
-
import { createCipheriv, createDecipheriv, randomBytes, createHash as
|
|
5752
|
-
import { readFileSync as
|
|
5753
|
-
import { join as
|
|
5754
|
-
import { homedir as
|
|
5755
|
-
var CLOUD_DIR =
|
|
5756
|
-
var KEY_FILE =
|
|
5757
|
-
var SYNC_STATE_FILE =
|
|
6433
|
+
import { createCipheriv, createDecipheriv, randomBytes as randomBytes2, createHash as createHash5 } from "node:crypto";
|
|
6434
|
+
import { readFileSync as readFileSync14, writeFileSync as writeFileSync14, existsSync as existsSync14, mkdirSync as mkdirSync14, chmodSync as chmodSync2 } from "node:fs";
|
|
6435
|
+
import { join as join15 } from "node:path";
|
|
6436
|
+
import { homedir as homedir7 } from "node:os";
|
|
6437
|
+
var CLOUD_DIR = join15(homedir7(), ".stellavault", "cloud");
|
|
6438
|
+
var KEY_FILE = join15(CLOUD_DIR, "encryption.key");
|
|
6439
|
+
var SYNC_STATE_FILE = join15(CLOUD_DIR, "sync-state.json");
|
|
5758
6440
|
function encrypt(data, key) {
|
|
5759
|
-
const iv =
|
|
6441
|
+
const iv = randomBytes2(16);
|
|
5760
6442
|
const cipher = createCipheriv("aes-256-gcm", key, iv);
|
|
5761
6443
|
const encrypted = Buffer.concat([cipher.update(data), cipher.final()]);
|
|
5762
6444
|
const tag = cipher.getAuthTag();
|
|
@@ -5768,19 +6450,19 @@ function decrypt(encrypted, key, iv, tag) {
|
|
|
5768
6450
|
return Buffer.concat([decipher.update(encrypted), decipher.final()]);
|
|
5769
6451
|
}
|
|
5770
6452
|
function getOrCreateEncryptionKey(userKey) {
|
|
5771
|
-
|
|
6453
|
+
mkdirSync14(CLOUD_DIR, { recursive: true });
|
|
5772
6454
|
if (userKey) {
|
|
5773
|
-
const key2 =
|
|
5774
|
-
|
|
6455
|
+
const key2 = createHash5("sha256").update(userKey).digest();
|
|
6456
|
+
writeFileSync14(KEY_FILE, key2.toString("hex"), "utf-8");
|
|
5775
6457
|
return key2;
|
|
5776
6458
|
}
|
|
5777
|
-
if (
|
|
5778
|
-
return Buffer.from(
|
|
6459
|
+
if (existsSync14(KEY_FILE)) {
|
|
6460
|
+
return Buffer.from(readFileSync14(KEY_FILE, "utf-8").trim(), "hex");
|
|
5779
6461
|
}
|
|
5780
|
-
const key =
|
|
5781
|
-
|
|
6462
|
+
const key = randomBytes2(32);
|
|
6463
|
+
writeFileSync14(KEY_FILE, key.toString("hex"), { encoding: "utf-8", mode: 384 });
|
|
5782
6464
|
try {
|
|
5783
|
-
|
|
6465
|
+
chmodSync2(KEY_FILE, 384);
|
|
5784
6466
|
} catch {
|
|
5785
6467
|
}
|
|
5786
6468
|
return key;
|
|
@@ -5804,7 +6486,7 @@ async function s3Put(config, objectKey, data, contentType = "application/octet-s
|
|
|
5804
6486
|
"Content-Type": contentType,
|
|
5805
6487
|
"Content-Length": String(data.length),
|
|
5806
6488
|
"x-amz-date": date,
|
|
5807
|
-
"x-amz-content-sha256":
|
|
6489
|
+
"x-amz-content-sha256": createHash5("sha256").update(data).digest("hex"),
|
|
5808
6490
|
// R2는 Bearer token 지원
|
|
5809
6491
|
"Authorization": `Bearer ${secretAccessKey}`
|
|
5810
6492
|
},
|
|
@@ -5824,18 +6506,18 @@ async function s3Get(config, objectKey) {
|
|
|
5824
6506
|
async function syncToCloud(dbPath, config) {
|
|
5825
6507
|
const timestamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
5826
6508
|
try {
|
|
5827
|
-
if (!
|
|
6509
|
+
if (!existsSync14(dbPath)) {
|
|
5828
6510
|
return { action: "upload", dbSize: 0, encryptedSize: 0, timestamp, success: false, error: "DB not found" };
|
|
5829
6511
|
}
|
|
5830
|
-
const dbData =
|
|
6512
|
+
const dbData = readFileSync14(dbPath);
|
|
5831
6513
|
const key = getOrCreateEncryptionKey(config.encryptionKey);
|
|
5832
6514
|
const { encrypted, iv, tag } = encrypt(dbData, key);
|
|
5833
6515
|
const payload = Buffer.concat([iv, tag, encrypted]);
|
|
5834
6516
|
const objectKey = `stellavault/index.db.enc`;
|
|
5835
6517
|
const success = await s3Put(config, objectKey, payload);
|
|
5836
6518
|
const state = { lastSync: timestamp, dbSize: dbData.length, encryptedSize: payload.length, objectKey };
|
|
5837
|
-
|
|
5838
|
-
|
|
6519
|
+
mkdirSync14(CLOUD_DIR, { recursive: true });
|
|
6520
|
+
writeFileSync14(SYNC_STATE_FILE, JSON.stringify(state, null, 2), "utf-8");
|
|
5839
6521
|
return { action: "upload", dbSize: dbData.length, encryptedSize: payload.length, timestamp, success };
|
|
5840
6522
|
} catch (err) {
|
|
5841
6523
|
return { action: "upload", dbSize: 0, encryptedSize: 0, timestamp, success: false, error: err instanceof Error ? err.message : String(err) };
|
|
@@ -5854,579 +6536,46 @@ async function restoreFromCloud(dbPath, config) {
|
|
|
5854
6536
|
const tag = payload.subarray(16, 32);
|
|
5855
6537
|
const encrypted = payload.subarray(32);
|
|
5856
6538
|
const dbData = decrypt(encrypted, key, iv, tag);
|
|
5857
|
-
if (
|
|
5858
|
-
|
|
6539
|
+
if (existsSync14(dbPath)) {
|
|
6540
|
+
writeFileSync14(dbPath + ".backup", readFileSync14(dbPath));
|
|
5859
6541
|
}
|
|
5860
|
-
|
|
6542
|
+
writeFileSync14(dbPath, dbData);
|
|
5861
6543
|
return { action: "download", dbSize: dbData.length, encryptedSize: payload.length, timestamp, success: true };
|
|
5862
6544
|
} catch (err) {
|
|
5863
6545
|
return { action: "download", dbSize: 0, encryptedSize: 0, timestamp, success: false, error: err instanceof Error ? err.message : String(err) };
|
|
5864
6546
|
}
|
|
5865
6547
|
}
|
|
5866
6548
|
function getSyncState() {
|
|
5867
|
-
if (!
|
|
6549
|
+
if (!existsSync14(SYNC_STATE_FILE))
|
|
5868
6550
|
return null;
|
|
5869
|
-
return JSON.parse(
|
|
6551
|
+
return JSON.parse(readFileSync14(SYNC_STATE_FILE, "utf-8"));
|
|
5870
6552
|
}
|
|
5871
6553
|
|
|
5872
6554
|
// packages/core/dist/team/index.js
|
|
5873
|
-
import { join as join14 } from "node:path";
|
|
5874
|
-
import { homedir as homedir6 } from "node:os";
|
|
5875
|
-
var TEAM_DIR = join14(homedir6(), ".stellavault", "team");
|
|
5876
|
-
var TEAM_FILE = join14(TEAM_DIR, "team.json");
|
|
5877
|
-
|
|
5878
|
-
// packages/core/dist/federation/node.js
|
|
5879
|
-
import { createHash as createHash5 } from "node:crypto";
|
|
5880
|
-
import { EventEmitter } from "node:events";
|
|
5881
|
-
|
|
5882
|
-
// packages/core/dist/federation/identity.js
|
|
5883
|
-
import { randomBytes as randomBytes2, createHash as createHash4, createHmac } from "node:crypto";
|
|
5884
|
-
import { readFileSync as readFileSync13, writeFileSync as writeFileSync13, existsSync as existsSync13, mkdirSync as mkdirSync13, chmodSync as chmodSync2 } from "node:fs";
|
|
5885
|
-
import { join as join15 } from "node:path";
|
|
5886
|
-
import { homedir as homedir7 } from "node:os";
|
|
5887
|
-
var IDENTITY_DIR = join15(homedir7(), ".stellavault", "federation");
|
|
5888
|
-
var IDENTITY_FILE = join15(IDENTITY_DIR, "identity.json");
|
|
5889
|
-
function getOrCreateIdentity(displayName) {
|
|
5890
|
-
if (existsSync13(IDENTITY_FILE)) {
|
|
5891
|
-
const raw = JSON.parse(readFileSync13(IDENTITY_FILE, "utf-8"));
|
|
5892
|
-
return {
|
|
5893
|
-
peerId: raw.peerId,
|
|
5894
|
-
publicKey: Buffer.from(raw.publicKey, "hex"),
|
|
5895
|
-
secretKey: Buffer.from(raw.secretKey, "hex"),
|
|
5896
|
-
displayName: raw.displayName,
|
|
5897
|
-
createdAt: raw.createdAt
|
|
5898
|
-
};
|
|
5899
|
-
}
|
|
5900
|
-
const secretKey = randomBytes2(32);
|
|
5901
|
-
const publicKey = createHash4("sha256").update(secretKey).digest();
|
|
5902
|
-
const peerId = createHash4("sha256").update(publicKey).digest("hex").slice(0, 16);
|
|
5903
|
-
const identity = {
|
|
5904
|
-
peerId,
|
|
5905
|
-
publicKey,
|
|
5906
|
-
secretKey,
|
|
5907
|
-
displayName: displayName ?? `node-${peerId.slice(0, 6)}`,
|
|
5908
|
-
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
5909
|
-
};
|
|
5910
|
-
mkdirSync13(IDENTITY_DIR, { recursive: true });
|
|
5911
|
-
const content = JSON.stringify({
|
|
5912
|
-
peerId: identity.peerId,
|
|
5913
|
-
publicKey: publicKey.toString("hex"),
|
|
5914
|
-
secretKey: secretKey.toString("hex"),
|
|
5915
|
-
displayName: identity.displayName,
|
|
5916
|
-
createdAt: identity.createdAt
|
|
5917
|
-
}, null, 2);
|
|
5918
|
-
writeFileSync13(IDENTITY_FILE, content, { encoding: "utf-8", mode: 384 });
|
|
5919
|
-
try {
|
|
5920
|
-
chmodSync2(IDENTITY_FILE, 384);
|
|
5921
|
-
} catch {
|
|
5922
|
-
}
|
|
5923
|
-
return identity;
|
|
5924
|
-
}
|
|
5925
|
-
|
|
5926
|
-
// packages/core/dist/federation/node.js
|
|
5927
|
-
var FEDERATION_TOPIC = createHash5("sha256").update("stellavault-federation-v1").digest();
|
|
5928
|
-
var FederationNode = class extends EventEmitter {
|
|
5929
|
-
swarm = null;
|
|
5930
|
-
identity;
|
|
5931
|
-
peers = /* @__PURE__ */ new Map();
|
|
5932
|
-
running = false;
|
|
5933
|
-
documentCount = 0;
|
|
5934
|
-
topTopics = [];
|
|
5935
|
-
constructor(displayName) {
|
|
5936
|
-
super();
|
|
5937
|
-
this.identity = getOrCreateIdentity(displayName);
|
|
5938
|
-
}
|
|
5939
|
-
get peerId() {
|
|
5940
|
-
return this.identity.peerId;
|
|
5941
|
-
}
|
|
5942
|
-
get displayName() {
|
|
5943
|
-
return this.identity.displayName;
|
|
5944
|
-
}
|
|
5945
|
-
get peerCount() {
|
|
5946
|
-
return this.peers.size;
|
|
5947
|
-
}
|
|
5948
|
-
get isRunning() {
|
|
5949
|
-
return this.running;
|
|
5950
|
-
}
|
|
5951
|
-
setLocalStats(documentCount, topTopics) {
|
|
5952
|
-
this.documentCount = documentCount;
|
|
5953
|
-
this.topTopics = topTopics.slice(0, 5);
|
|
5954
|
-
}
|
|
5955
|
-
// Design Ref: §4 — join()
|
|
5956
|
-
async join() {
|
|
5957
|
-
if (this.running)
|
|
5958
|
-
return;
|
|
5959
|
-
const Hyperswarm = (await import("hyperswarm")).default;
|
|
5960
|
-
this.swarm = new Hyperswarm({ maxPeers: 50 });
|
|
5961
|
-
this.swarm.on("connection", (conn, _info) => {
|
|
5962
|
-
this.handleConnection(conn);
|
|
5963
|
-
});
|
|
5964
|
-
const discovery = this.swarm.join(FEDERATION_TOPIC, { server: true, client: true });
|
|
5965
|
-
await discovery.flushed();
|
|
5966
|
-
this.running = true;
|
|
5967
|
-
this.emit("joined", { peerId: this.peerId, topic: FEDERATION_TOPIC.toString("hex").slice(0, 16) });
|
|
5968
|
-
}
|
|
5969
|
-
// Design Ref: §4 — joinDirect() 수동 IP 폴백
|
|
5970
|
-
async joinDirect(host, port) {
|
|
5971
|
-
const net = await import("node:net");
|
|
5972
|
-
const conn = net.connect(port, host);
|
|
5973
|
-
await new Promise((resolve22, reject) => {
|
|
5974
|
-
conn.on("connect", () => {
|
|
5975
|
-
this.handleConnection(conn);
|
|
5976
|
-
resolve22();
|
|
5977
|
-
});
|
|
5978
|
-
conn.on("error", reject);
|
|
5979
|
-
setTimeout(() => reject(new Error("Connection timeout")), 15e3);
|
|
5980
|
-
});
|
|
5981
|
-
if (!this.running)
|
|
5982
|
-
this.running = true;
|
|
5983
|
-
}
|
|
5984
|
-
async leave() {
|
|
5985
|
-
if (!this.running)
|
|
5986
|
-
return;
|
|
5987
|
-
for (const [, peer] of this.peers) {
|
|
5988
|
-
try {
|
|
5989
|
-
this.sendMessage(peer.conn, { type: "leave", peerId: this.peerId });
|
|
5990
|
-
peer.conn.end();
|
|
5991
|
-
} catch {
|
|
5992
|
-
}
|
|
5993
|
-
}
|
|
5994
|
-
this.peers.clear();
|
|
5995
|
-
await this.swarm?.destroy();
|
|
5996
|
-
this.swarm = null;
|
|
5997
|
-
this.running = false;
|
|
5998
|
-
this.emit("left");
|
|
5999
|
-
}
|
|
6000
|
-
getPeers() {
|
|
6001
|
-
return [...this.peers.values()].map((p) => p.info);
|
|
6002
|
-
}
|
|
6003
|
-
// 피어에게 검색 쿼리 전송 (FederatedSearch에서 사용)
|
|
6004
|
-
sendSearchQuery(peerId, queryId, embedding, limit) {
|
|
6005
|
-
const peer = this.peers.get(peerId);
|
|
6006
|
-
if (!peer)
|
|
6007
|
-
return;
|
|
6008
|
-
this.sendMessage(peer.conn, { type: "search_query", queryId, embedding, limit });
|
|
6009
|
-
}
|
|
6010
|
-
// 피어에게 검색 결과 응답 (FederatedSearch에서 사용)
|
|
6011
|
-
sendSearchResult(peerId, queryId, results) {
|
|
6012
|
-
const peer = this.peers.get(peerId);
|
|
6013
|
-
if (!peer)
|
|
6014
|
-
return;
|
|
6015
|
-
this.sendMessage(peer.conn, { type: "search_result", queryId, results });
|
|
6016
|
-
}
|
|
6017
|
-
// --- Private ---
|
|
6018
|
-
handleConnection(conn) {
|
|
6019
|
-
this.sendMessage(conn, {
|
|
6020
|
-
type: "handshake",
|
|
6021
|
-
peerId: this.peerId,
|
|
6022
|
-
displayName: this.identity.displayName,
|
|
6023
|
-
version: "0.1.0",
|
|
6024
|
-
documentCount: this.documentCount,
|
|
6025
|
-
topTopics: this.topTopics
|
|
6026
|
-
});
|
|
6027
|
-
let buffer = "";
|
|
6028
|
-
const MAX_BUFFER = 1024 * 1024;
|
|
6029
|
-
const MAX_MESSAGE = 64 * 1024;
|
|
6030
|
-
conn.on("data", (data) => {
|
|
6031
|
-
buffer += data.toString();
|
|
6032
|
-
if (buffer.length > MAX_BUFFER) {
|
|
6033
|
-
console.error("Federation: buffer overflow from peer, disconnecting");
|
|
6034
|
-
buffer = "";
|
|
6035
|
-
conn.end();
|
|
6036
|
-
return;
|
|
6037
|
-
}
|
|
6038
|
-
const lines = buffer.split("\n");
|
|
6039
|
-
buffer = lines.pop() ?? "";
|
|
6040
|
-
for (const line of lines) {
|
|
6041
|
-
if (!line.trim())
|
|
6042
|
-
continue;
|
|
6043
|
-
if (line.length > MAX_MESSAGE)
|
|
6044
|
-
continue;
|
|
6045
|
-
try {
|
|
6046
|
-
const msg = JSON.parse(line);
|
|
6047
|
-
this.handleMessage(conn, msg);
|
|
6048
|
-
} catch {
|
|
6049
|
-
}
|
|
6050
|
-
}
|
|
6051
|
-
});
|
|
6052
|
-
conn.on("close", () => {
|
|
6053
|
-
for (const [peerId, peer] of this.peers) {
|
|
6054
|
-
if (peer.conn === conn) {
|
|
6055
|
-
this.peers.delete(peerId);
|
|
6056
|
-
this.emit("peer_left", { peerId });
|
|
6057
|
-
break;
|
|
6058
|
-
}
|
|
6059
|
-
}
|
|
6060
|
-
});
|
|
6061
|
-
conn.on("error", () => {
|
|
6062
|
-
});
|
|
6063
|
-
}
|
|
6064
|
-
// MED: 메시지 스키마 기본 검증
|
|
6065
|
-
validateMessage(msg) {
|
|
6066
|
-
if (!msg || typeof msg !== "object" || typeof msg.type !== "string")
|
|
6067
|
-
return false;
|
|
6068
|
-
if (msg.type === "handshake" && (typeof msg.peerId !== "string" || typeof msg.displayName !== "string"))
|
|
6069
|
-
return false;
|
|
6070
|
-
if (msg.type === "search_query" && (!Array.isArray(msg.embedding) || msg.embedding.length !== 384))
|
|
6071
|
-
return false;
|
|
6072
|
-
if (msg.type === "search_result" && !Array.isArray(msg.results))
|
|
6073
|
-
return false;
|
|
6074
|
-
return true;
|
|
6075
|
-
}
|
|
6076
|
-
handleMessage(conn, msg) {
|
|
6077
|
-
if (!this.validateMessage(msg))
|
|
6078
|
-
return;
|
|
6079
|
-
switch (msg.type) {
|
|
6080
|
-
case "handshake": {
|
|
6081
|
-
const safeName = (msg.displayName ?? "").slice(0, 50);
|
|
6082
|
-
const peerInfo = {
|
|
6083
|
-
peerId: msg.peerId,
|
|
6084
|
-
displayName: safeName,
|
|
6085
|
-
documentCount: Math.min(msg.documentCount ?? 0, 1e6),
|
|
6086
|
-
// 합리적 상한
|
|
6087
|
-
topTopics: (msg.topTopics ?? []).slice(0, 10),
|
|
6088
|
-
joinedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
6089
|
-
lastSeen: (/* @__PURE__ */ new Date()).toISOString()
|
|
6090
|
-
};
|
|
6091
|
-
this.peers.set(msg.peerId, { info: peerInfo, conn });
|
|
6092
|
-
this.emit("peer_joined", peerInfo);
|
|
6093
|
-
break;
|
|
6094
|
-
}
|
|
6095
|
-
case "search_query": {
|
|
6096
|
-
this.emit("search_request", {
|
|
6097
|
-
peerId: msg.queryId,
|
|
6098
|
-
// queryId를 추적용으로 사용
|
|
6099
|
-
queryId: msg.queryId,
|
|
6100
|
-
embedding: msg.embedding,
|
|
6101
|
-
limit: msg.limit,
|
|
6102
|
-
// respond 함수: 호출 측에서 사용
|
|
6103
|
-
respondTo: (() => {
|
|
6104
|
-
for (const [pid, peer] of this.peers) {
|
|
6105
|
-
if (peer.conn === conn)
|
|
6106
|
-
return pid;
|
|
6107
|
-
}
|
|
6108
|
-
return null;
|
|
6109
|
-
})()
|
|
6110
|
-
});
|
|
6111
|
-
break;
|
|
6112
|
-
}
|
|
6113
|
-
case "search_result": {
|
|
6114
|
-
this.emit("search_response", {
|
|
6115
|
-
queryId: msg.queryId,
|
|
6116
|
-
results: msg.results,
|
|
6117
|
-
peerId: (() => {
|
|
6118
|
-
for (const [pid, peer] of this.peers) {
|
|
6119
|
-
if (peer.conn === conn)
|
|
6120
|
-
return pid;
|
|
6121
|
-
}
|
|
6122
|
-
return "unknown";
|
|
6123
|
-
})()
|
|
6124
|
-
});
|
|
6125
|
-
break;
|
|
6126
|
-
}
|
|
6127
|
-
case "leave": {
|
|
6128
|
-
this.peers.delete(msg.peerId);
|
|
6129
|
-
this.emit("peer_left", { peerId: msg.peerId });
|
|
6130
|
-
break;
|
|
6131
|
-
}
|
|
6132
|
-
}
|
|
6133
|
-
}
|
|
6134
|
-
// Design Ref: §7 — JSON + newline delimiter
|
|
6135
|
-
sendMessage(conn, msg) {
|
|
6136
|
-
try {
|
|
6137
|
-
conn.write(JSON.stringify(msg) + "\n");
|
|
6138
|
-
} catch {
|
|
6139
|
-
}
|
|
6140
|
-
}
|
|
6141
|
-
};
|
|
6142
|
-
|
|
6143
|
-
// packages/core/dist/federation/search.js
|
|
6144
|
-
import { randomUUID } from "node:crypto";
|
|
6145
|
-
|
|
6146
|
-
// packages/core/dist/federation/privacy.js
|
|
6147
|
-
function maskSnippet(snippet, maskRate = 0.3) {
|
|
6148
|
-
const words = snippet.split(/\s+/);
|
|
6149
|
-
return words.map((w) => {
|
|
6150
|
-
if (Math.random() < maskRate && w.length > 2) {
|
|
6151
|
-
return w[0] + "***";
|
|
6152
|
-
}
|
|
6153
|
-
return w;
|
|
6154
|
-
}).join(" ");
|
|
6155
|
-
}
|
|
6156
|
-
|
|
6157
|
-
// packages/core/dist/federation/sharing.js
|
|
6158
|
-
import { readFileSync as readFileSync14, writeFileSync as writeFileSync14, existsSync as existsSync14, mkdirSync as mkdirSync14 } from "node:fs";
|
|
6159
6555
|
import { join as join16 } from "node:path";
|
|
6160
6556
|
import { homedir as homedir8 } from "node:os";
|
|
6161
|
-
var
|
|
6162
|
-
|
|
6163
|
-
1: "Title + similarity only",
|
|
6164
|
-
2: "Title + snippet (50 chars)",
|
|
6165
|
-
3: "Full text on request (approval needed)",
|
|
6166
|
-
4: "Full text auto-shared"
|
|
6167
|
-
};
|
|
6168
|
-
var LEVEL_ICONS = {
|
|
6169
|
-
0: "\u{1F6AB}",
|
|
6170
|
-
1: "\u{1F4CC}",
|
|
6171
|
-
2: "\u{1F4DD}",
|
|
6172
|
-
3: "\u{1F4D6}",
|
|
6173
|
-
4: "\u{1F310}"
|
|
6174
|
-
};
|
|
6175
|
-
var LEVEL_CREDIT_MULTIPLIER = {
|
|
6176
|
-
0: 0,
|
|
6177
|
-
1: 1,
|
|
6178
|
-
2: 2,
|
|
6179
|
-
3: 5,
|
|
6180
|
-
4: 10
|
|
6181
|
-
};
|
|
6182
|
-
var SHARING_FILE = join16(homedir8(), ".stellavault", "federation", "sharing.json");
|
|
6183
|
-
var SENSITIVE_PATTERNS = [
|
|
6184
|
-
/\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b/,
|
|
6185
|
-
/\b\d{3}[-.]?\d{4}[-.]?\d{4}\b/,
|
|
6186
|
-
/\b(sk-|pk-|api[_-]?key|token|secret)[a-zA-Z0-9_-]{10,}\b/i,
|
|
6187
|
-
/\bpassword\s*[:=]\s*\S+/i,
|
|
6188
|
-
/\b\d{6}[-]\d{7}\b/
|
|
6189
|
-
];
|
|
6190
|
-
var DEFAULT_CONFIG2 = {
|
|
6191
|
-
defaultLevel: 2,
|
|
6192
|
-
// 기본: 스니펫까지
|
|
6193
|
-
myNodeLevel: 2,
|
|
6194
|
-
// 내 노드 기본: 스니펫까지
|
|
6195
|
-
rules: [
|
|
6196
|
-
// 기본 규칙
|
|
6197
|
-
{ pattern: "public", type: "tag", level: 4 },
|
|
6198
|
-
{ pattern: "opensource", type: "tag", level: 4 },
|
|
6199
|
-
{ pattern: "personal", type: "tag", level: 1 },
|
|
6200
|
-
{ pattern: "private", type: "tag", level: 0 },
|
|
6201
|
-
{ pattern: "secret", type: "tag", level: 0 },
|
|
6202
|
-
{ pattern: "diary", type: "tag", level: 0 },
|
|
6203
|
-
{ pattern: "salary", type: "tag", level: 0 },
|
|
6204
|
-
{ pattern: "password", type: "tag", level: 0 },
|
|
6205
|
-
{ pattern: "credential", type: "tag", level: 0 },
|
|
6206
|
-
{ pattern: "03_Daily", type: "folder", level: 1 },
|
|
6207
|
-
{ pattern: "06_Archive", type: "folder", level: 1 },
|
|
6208
|
-
{ pattern: ".obsidian", type: "folder", level: 0 }
|
|
6209
|
-
],
|
|
6210
|
-
blockedDocIds: [],
|
|
6211
|
-
blockSensitivePatterns: true,
|
|
6212
|
-
pendingRequests: []
|
|
6213
|
-
};
|
|
6214
|
-
function loadSharingConfig() {
|
|
6215
|
-
if (existsSync14(SHARING_FILE)) {
|
|
6216
|
-
const raw = JSON.parse(readFileSync14(SHARING_FILE, "utf-8"));
|
|
6217
|
-
return { ...DEFAULT_CONFIG2, ...raw };
|
|
6218
|
-
}
|
|
6219
|
-
return { ...DEFAULT_CONFIG2 };
|
|
6220
|
-
}
|
|
6221
|
-
function saveSharingConfig(config) {
|
|
6222
|
-
mkdirSync14(join16(homedir8(), ".stellavault", "federation"), { recursive: true });
|
|
6223
|
-
writeFileSync14(SHARING_FILE, JSON.stringify(config, null, 2), "utf-8");
|
|
6224
|
-
}
|
|
6225
|
-
function getDocumentLevel(doc, config) {
|
|
6226
|
-
const cfg = config ?? loadSharingConfig();
|
|
6227
|
-
if (cfg.blockedDocIds.includes(doc.id))
|
|
6228
|
-
return 0;
|
|
6229
|
-
if (cfg.blockSensitivePatterns) {
|
|
6230
|
-
for (const pattern of SENSITIVE_PATTERNS) {
|
|
6231
|
-
if (pattern.test(doc.content))
|
|
6232
|
-
return 0;
|
|
6233
|
-
}
|
|
6234
|
-
}
|
|
6235
|
-
let matchedLevel = null;
|
|
6236
|
-
for (const rule of cfg.rules) {
|
|
6237
|
-
if (rule.type === "doc" && rule.pattern === doc.id) {
|
|
6238
|
-
return rule.level;
|
|
6239
|
-
}
|
|
6240
|
-
}
|
|
6241
|
-
const docTags = doc.tags.map((t2) => t2.toLowerCase());
|
|
6242
|
-
for (const rule of cfg.rules) {
|
|
6243
|
-
if (rule.type === "tag" && docTags.includes(rule.pattern.toLowerCase())) {
|
|
6244
|
-
if (matchedLevel === null || rule.level < matchedLevel) {
|
|
6245
|
-
matchedLevel = rule.level;
|
|
6246
|
-
}
|
|
6247
|
-
}
|
|
6248
|
-
}
|
|
6249
|
-
for (const rule of cfg.rules) {
|
|
6250
|
-
if (rule.type === "folder" && doc.filePath.startsWith(rule.pattern)) {
|
|
6251
|
-
if (matchedLevel === null || rule.level < matchedLevel) {
|
|
6252
|
-
matchedLevel = rule.level;
|
|
6253
|
-
}
|
|
6254
|
-
}
|
|
6255
|
-
}
|
|
6256
|
-
return matchedLevel ?? cfg.defaultLevel;
|
|
6257
|
-
}
|
|
6258
|
-
function isDocumentShareable(doc, config) {
|
|
6259
|
-
return getDocumentLevel(doc, config) > 0;
|
|
6260
|
-
}
|
|
6261
|
-
function approveRequest(requestId) {
|
|
6262
|
-
const cfg = loadSharingConfig();
|
|
6263
|
-
const req = cfg.pendingRequests.find((r) => r.requestId === requestId);
|
|
6264
|
-
if (!req)
|
|
6265
|
-
return false;
|
|
6266
|
-
req.status = "approved";
|
|
6267
|
-
saveSharingConfig(cfg);
|
|
6268
|
-
return true;
|
|
6269
|
-
}
|
|
6270
|
-
function denyRequest(requestId) {
|
|
6271
|
-
const cfg = loadSharingConfig();
|
|
6272
|
-
const req = cfg.pendingRequests.find((r) => r.requestId === requestId);
|
|
6273
|
-
if (!req)
|
|
6274
|
-
return false;
|
|
6275
|
-
req.status = "denied";
|
|
6276
|
-
saveSharingConfig(cfg);
|
|
6277
|
-
return true;
|
|
6278
|
-
}
|
|
6279
|
-
function getPendingRequests() {
|
|
6280
|
-
return loadSharingConfig().pendingRequests.filter((r) => r.status === "pending");
|
|
6281
|
-
}
|
|
6282
|
-
function sanitizeSnippet(snippet) {
|
|
6283
|
-
let safe = snippet;
|
|
6284
|
-
for (const pattern of SENSITIVE_PATTERNS) {
|
|
6285
|
-
safe = safe.replace(pattern, "[REDACTED]");
|
|
6286
|
-
}
|
|
6287
|
-
return safe;
|
|
6288
|
-
}
|
|
6289
|
-
function setTagLevel(tag, level) {
|
|
6290
|
-
const cfg = loadSharingConfig();
|
|
6291
|
-
const existing = cfg.rules.findIndex((r) => r.type === "tag" && r.pattern.toLowerCase() === tag.toLowerCase());
|
|
6292
|
-
if (existing >= 0)
|
|
6293
|
-
cfg.rules[existing].level = level;
|
|
6294
|
-
else
|
|
6295
|
-
cfg.rules.push({ pattern: tag.toLowerCase(), type: "tag", level });
|
|
6296
|
-
saveSharingConfig(cfg);
|
|
6297
|
-
}
|
|
6298
|
-
function setFolderLevel(folder, level) {
|
|
6299
|
-
const cfg = loadSharingConfig();
|
|
6300
|
-
const existing = cfg.rules.findIndex((r) => r.type === "folder" && r.pattern === folder);
|
|
6301
|
-
if (existing >= 0)
|
|
6302
|
-
cfg.rules[existing].level = level;
|
|
6303
|
-
else
|
|
6304
|
-
cfg.rules.push({ pattern: folder, type: "folder", level });
|
|
6305
|
-
saveSharingConfig(cfg);
|
|
6306
|
-
}
|
|
6307
|
-
function setNodeLevel(level) {
|
|
6308
|
-
const cfg = loadSharingConfig();
|
|
6309
|
-
cfg.myNodeLevel = level;
|
|
6310
|
-
saveSharingConfig(cfg);
|
|
6311
|
-
}
|
|
6312
|
-
function getSharingSummary(config) {
|
|
6313
|
-
const cfg = config ?? loadSharingConfig();
|
|
6314
|
-
const lines = [];
|
|
6315
|
-
lines.push(`My Node Level: ${LEVEL_ICONS[cfg.myNodeLevel]} Level ${cfg.myNodeLevel} (${LEVEL_LABELS[cfg.myNodeLevel]})`);
|
|
6316
|
-
lines.push(`Default Doc Level: ${LEVEL_ICONS[cfg.defaultLevel]} Level ${cfg.defaultLevel}`);
|
|
6317
|
-
lines.push(`Credit Multiplier: ${LEVEL_CREDIT_MULTIPLIER[cfg.myNodeLevel]}x`);
|
|
6318
|
-
lines.push("");
|
|
6319
|
-
lines.push("Rules:");
|
|
6320
|
-
for (const rule of cfg.rules) {
|
|
6321
|
-
lines.push(` ${LEVEL_ICONS[rule.level]} [${rule.type}] ${rule.pattern} \u2192 Level ${rule.level}`);
|
|
6322
|
-
}
|
|
6323
|
-
if (cfg.blockedDocIds.length > 0) {
|
|
6324
|
-
lines.push(` \u{1F6AB} ${cfg.blockedDocIds.length} docs individually blocked`);
|
|
6325
|
-
}
|
|
6326
|
-
lines.push("");
|
|
6327
|
-
const pending = cfg.pendingRequests.filter((r) => r.status === "pending");
|
|
6328
|
-
if (pending.length > 0) {
|
|
6329
|
-
lines.push(`\u{1F4EC} ${pending.length} pending full-text requests`);
|
|
6330
|
-
}
|
|
6331
|
-
lines.push(`Sensitive pattern filter: ${cfg.blockSensitivePatterns ? "ON" : "OFF"}`);
|
|
6332
|
-
return lines.join("\n");
|
|
6333
|
-
}
|
|
6557
|
+
var TEAM_DIR = join16(homedir8(), ".stellavault", "team");
|
|
6558
|
+
var TEAM_FILE = join16(TEAM_DIR, "team.json");
|
|
6334
6559
|
|
|
6335
|
-
// packages/core/dist/
|
|
6336
|
-
|
|
6337
|
-
node;
|
|
6338
|
-
store;
|
|
6339
|
-
embedder;
|
|
6340
|
-
additionalStores = [];
|
|
6341
|
-
constructor(node, store, embedder) {
|
|
6342
|
-
this.node = node;
|
|
6343
|
-
this.store = store;
|
|
6344
|
-
this.embedder = embedder;
|
|
6345
|
-
}
|
|
6346
|
-
// Multi-vault: 추가 store 등록 (Federation 검색 응답 시 전체 vault 검색)
|
|
6347
|
-
addStore(store) {
|
|
6348
|
-
this.additionalStores.push(store);
|
|
6349
|
-
}
|
|
6350
|
-
// Design Ref: §5 — search() 요청 측
|
|
6351
|
-
async search(query, options = {}) {
|
|
6352
|
-
const { limit = 5, timeout = 5e3 } = options;
|
|
6353
|
-
const peers = this.node.getPeers();
|
|
6354
|
-
if (peers.length === 0) {
|
|
6355
|
-
return [];
|
|
6356
|
-
}
|
|
6357
|
-
const embedding = await this.embedder.embed(query);
|
|
6358
|
-
const queryId = randomUUID().slice(0, 8);
|
|
6359
|
-
const results = [];
|
|
6360
|
-
const peerMap = new Map(peers.map((p) => [p.peerId, p.displayName]));
|
|
6361
|
-
const responsePromises = peers.map((peer) => {
|
|
6362
|
-
return new Promise((resolve22) => {
|
|
6363
|
-
const timer = setTimeout(resolve22, timeout);
|
|
6364
|
-
const handler = (data) => {
|
|
6365
|
-
if (data.queryId !== queryId)
|
|
6366
|
-
return;
|
|
6367
|
-
for (const r of data.results) {
|
|
6368
|
-
results.push({
|
|
6369
|
-
title: r.title,
|
|
6370
|
-
similarity: r.similarity,
|
|
6371
|
-
snippet: r.snippet,
|
|
6372
|
-
peerId: peer.peerId,
|
|
6373
|
-
peerName: peer.displayName
|
|
6374
|
-
});
|
|
6375
|
-
}
|
|
6376
|
-
clearTimeout(timer);
|
|
6377
|
-
this.node.removeListener("search_response", handler);
|
|
6378
|
-
resolve22();
|
|
6379
|
-
};
|
|
6380
|
-
this.node.on("search_response", handler);
|
|
6381
|
-
this.node.sendSearchQuery(peer.peerId, queryId, Array.from(embedding), limit);
|
|
6382
|
-
});
|
|
6383
|
-
});
|
|
6384
|
-
await Promise.allSettled(responsePromises);
|
|
6385
|
-
return results.sort((a, b) => b.similarity - a.similarity).slice(0, limit);
|
|
6386
|
-
}
|
|
6387
|
-
// Design Ref: §5 — startResponder() 피어 요청 수신 측
|
|
6388
|
-
startResponder() {
|
|
6389
|
-
this.node.on("search_request", async (req) => {
|
|
6390
|
-
if (!req.respondTo)
|
|
6391
|
-
return;
|
|
6392
|
-
try {
|
|
6393
|
-
const allStores = [this.store, ...this.additionalStores];
|
|
6394
|
-
const allScored = await Promise.all(allStores.map((s) => s.searchSemantic(req.embedding, req.limit).catch(() => [])));
|
|
6395
|
-
const scored = allScored.flat().sort((a, b) => b.score - a.score).slice(0, req.limit);
|
|
6396
|
-
const safe = [];
|
|
6397
|
-
for (const s of scored) {
|
|
6398
|
-
const chunk = await this.store.getChunk(s.chunkId);
|
|
6399
|
-
if (!chunk)
|
|
6400
|
-
continue;
|
|
6401
|
-
const doc = await this.store.getDocument(chunk.documentId);
|
|
6402
|
-
if (!doc)
|
|
6403
|
-
continue;
|
|
6404
|
-
if (!isDocumentShareable({ tags: doc.tags, filePath: doc.filePath, id: doc.id, content: doc.content }))
|
|
6405
|
-
continue;
|
|
6406
|
-
safe.push({
|
|
6407
|
-
title: doc.title ?? chunk.heading ?? "Untitled",
|
|
6408
|
-
similarity: Math.round(s.score * 1e3) / 1e3,
|
|
6409
|
-
snippet: sanitizeSnippet(maskSnippet(chunk.content.slice(0, 50), 0.2))
|
|
6410
|
-
});
|
|
6411
|
-
}
|
|
6412
|
-
this.node.sendSearchResult(req.respondTo, req.queryId, safe);
|
|
6413
|
-
} catch (err) {
|
|
6414
|
-
this.node.sendSearchResult(req.respondTo, req.queryId, []);
|
|
6415
|
-
}
|
|
6416
|
-
});
|
|
6417
|
-
}
|
|
6418
|
-
};
|
|
6560
|
+
// packages/core/dist/index.js
|
|
6561
|
+
init_federation();
|
|
6419
6562
|
|
|
6420
6563
|
// packages/core/dist/federation/trust.js
|
|
6421
6564
|
import { join as join17 } from "node:path";
|
|
6422
6565
|
import { homedir as homedir9 } from "node:os";
|
|
6423
6566
|
var TRUST_FILE = join17(homedir9(), ".stellavault", "federation", "trust.json");
|
|
6424
6567
|
|
|
6568
|
+
// packages/core/dist/index.js
|
|
6569
|
+
init_sharing();
|
|
6570
|
+
|
|
6425
6571
|
// packages/core/dist/federation/reputation.js
|
|
6426
6572
|
import { join as join18 } from "node:path";
|
|
6427
6573
|
import { homedir as homedir10 } from "node:os";
|
|
6428
6574
|
var REP_FILE = join18(homedir10(), ".stellavault", "federation", "reputation.json");
|
|
6429
6575
|
|
|
6576
|
+
// packages/core/dist/index.js
|
|
6577
|
+
init_privacy();
|
|
6578
|
+
|
|
6430
6579
|
// packages/core/dist/federation/credits.js
|
|
6431
6580
|
import { join as join19 } from "node:path";
|
|
6432
6581
|
import { homedir as homedir11 } from "node:os";
|
|
@@ -8917,7 +9066,8 @@ async function autopilotCommand(options) {
|
|
|
8917
9066
|
|
|
8918
9067
|
// packages/cli/dist/index.js
|
|
8919
9068
|
var program = new Command();
|
|
8920
|
-
|
|
9069
|
+
var SV_VERSION = true ? "0.5.2" : "0.0.0-dev";
|
|
9070
|
+
program.name("stellavault").description("Stellavault \u2014 Turn your Obsidian vault into a 3D neural knowledge graph").version(SV_VERSION).option("--json", "Output in JSON format (for scripting)").option("--quiet", "Suppress non-essential output");
|
|
8921
9071
|
program.command("init").description("Interactive setup wizard \u2014 get started in 3 minutes").action(initCommand);
|
|
8922
9072
|
program.command("index [vault-path]").description("Obsidian vault\uB97C \uBCA1\uD130\uD654\uD558\uC5EC \uC778\uB371\uC2F1\uD569\uB2C8\uB2E4").action(indexCommand);
|
|
8923
9073
|
program.command("search <query>").description("\uC9C0\uC2DD \uBCA0\uC774\uC2A4\uC5D0\uC11C \uAC80\uC0C9\uD569\uB2C8\uB2E4").option("-l, --limit <n>", "\uACB0\uACFC \uC218", "5").action(searchCommand);
|