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.
Files changed (3) hide show
  1. package/README.md +5 -0
  2. package/dist/stellavault.js +744 -594
  3. package/package.json +66 -70
@@ -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
- return {
5316
- async start() {
5317
- return new Promise((resolve22) => {
5318
- app.listen(port, "127.0.0.1", () => {
5319
- console.error(`\u{1F310} API server running at http://127.0.0.1:${port}`);
5320
- resolve22();
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 readFileSync11, writeFileSync as writeFileSync10, existsSync as existsSync10, mkdirSync as mkdirSync10 } from "node:fs";
5564
- import { join as join11 } from "node:path";
5565
- import { homedir as homedir4 } from "node:os";
5566
- var VAULTS_FILE = join11(homedir4(), ".stellavault", "vaults.json");
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 (!existsSync10(VAULTS_FILE))
6250
+ if (!existsSync12(VAULTS_FILE))
5569
6251
  return [];
5570
- return JSON.parse(readFileSync11(VAULTS_FILE, "utf-8"));
6252
+ return JSON.parse(readFileSync13(VAULTS_FILE, "utf-8"));
5571
6253
  }
5572
6254
  function saveVaults(vaults) {
5573
- mkdirSync10(join11(homedir4(), ".stellavault"), { recursive: true });
5574
- writeFileSync10(VAULTS_FILE, JSON.stringify(vaults, null, 2), "utf-8");
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 (!existsSync10(vault2.dbPath))
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 writeFileSync11, mkdirSync as mkdirSync11, existsSync as existsSync11 } from "node:fs";
5634
- import { join as join12, basename as basename4 } from "node:path";
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 (!existsSync11(audioPath)) {
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
- mkdirSync11("/tmp/sv-whisper", { recursive: true });
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(join12("/tmp/sv-whisper", outputName), "utf-8").trim();
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 = join12(vaultPath, folder);
5728
- mkdirSync11(dir, { recursive: true });
5729
- const filePath = join12(dir, `${date.slice(0, 10)} ${safeTitle}.md`);
5730
- writeFileSync11(filePath, content, "utf-8");
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 createHash3 } from "node:crypto";
5752
- import { readFileSync as readFileSync12, writeFileSync as writeFileSync12, existsSync as existsSync12, mkdirSync as mkdirSync12, chmodSync } from "node:fs";
5753
- import { join as join13 } from "node:path";
5754
- import { homedir as homedir5 } from "node:os";
5755
- var CLOUD_DIR = join13(homedir5(), ".stellavault", "cloud");
5756
- var KEY_FILE = join13(CLOUD_DIR, "encryption.key");
5757
- var SYNC_STATE_FILE = join13(CLOUD_DIR, "sync-state.json");
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 = randomBytes(16);
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
- mkdirSync12(CLOUD_DIR, { recursive: true });
6453
+ mkdirSync14(CLOUD_DIR, { recursive: true });
5772
6454
  if (userKey) {
5773
- const key2 = createHash3("sha256").update(userKey).digest();
5774
- writeFileSync12(KEY_FILE, key2.toString("hex"), "utf-8");
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 (existsSync12(KEY_FILE)) {
5778
- return Buffer.from(readFileSync12(KEY_FILE, "utf-8").trim(), "hex");
6459
+ if (existsSync14(KEY_FILE)) {
6460
+ return Buffer.from(readFileSync14(KEY_FILE, "utf-8").trim(), "hex");
5779
6461
  }
5780
- const key = randomBytes(32);
5781
- writeFileSync12(KEY_FILE, key.toString("hex"), { encoding: "utf-8", mode: 384 });
6462
+ const key = randomBytes2(32);
6463
+ writeFileSync14(KEY_FILE, key.toString("hex"), { encoding: "utf-8", mode: 384 });
5782
6464
  try {
5783
- chmodSync(KEY_FILE, 384);
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": createHash3("sha256").update(data).digest("hex"),
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 (!existsSync12(dbPath)) {
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 = readFileSync12(dbPath);
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
- mkdirSync12(CLOUD_DIR, { recursive: true });
5838
- writeFileSync12(SYNC_STATE_FILE, JSON.stringify(state, null, 2), "utf-8");
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 (existsSync12(dbPath)) {
5858
- writeFileSync12(dbPath + ".backup", readFileSync12(dbPath));
6539
+ if (existsSync14(dbPath)) {
6540
+ writeFileSync14(dbPath + ".backup", readFileSync14(dbPath));
5859
6541
  }
5860
- writeFileSync12(dbPath, dbData);
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 (!existsSync12(SYNC_STATE_FILE))
6549
+ if (!existsSync14(SYNC_STATE_FILE))
5868
6550
  return null;
5869
- return JSON.parse(readFileSync12(SYNC_STATE_FILE, "utf-8"));
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 LEVEL_LABELS = {
6162
- 0: "Blocked (not searchable)",
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/federation/search.js
6336
- var FederatedSearch = class {
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
- program.name("stellavault").description("Stellavault \u2014 Turn your Obsidian vault into a 3D neural knowledge graph").version("0.3.0").option("--json", "Output in JSON format (for scripting)").option("--quiet", "Suppress non-essential output");
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);