@kanadego/dsh-heartbeat 1.9.0 → 1.9.1
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 +33 -68
- package/assets/frontwin.ps1 +1 -0
- package/assets/idle.ps1 +1 -0
- package/assets/screenpulse.ps1 +4 -2
- package/config/profile-schema.json +306 -306
- package/dist/{bindings-XPPSKILN.js → bindings-225SI7KC.js} +3 -2
- package/dist/{chunk-IPOZFZPY.js → chunk-2NB4QILF.js} +48 -10
- package/dist/chunk-2NB4QILF.js.map +1 -0
- package/dist/{chunk-IFTFDHZX.js → chunk-7TW6DD6Q.js} +11 -6
- package/dist/chunk-7TW6DD6Q.js.map +1 -0
- package/dist/{chunk-63FUJLIF.js → chunk-BQCXYE7N.js} +17 -2
- package/dist/chunk-BQCXYE7N.js.map +1 -0
- package/dist/chunk-HT57FOEF.js +2454 -0
- package/dist/chunk-HT57FOEF.js.map +1 -0
- package/dist/chunk-IV2ZWQA3.js +56 -0
- package/dist/chunk-IV2ZWQA3.js.map +1 -0
- package/dist/{chunk-PQTSXW4F.js → chunk-KOOQOMQY.js} +97 -71
- package/dist/chunk-KOOQOMQY.js.map +1 -0
- package/dist/{chunk-VIZNIQLK.js → chunk-OYVNWB5G.js} +82 -74
- package/dist/chunk-OYVNWB5G.js.map +1 -0
- package/dist/{chunk-RB3NLLGF.js → chunk-QOQ7EPZZ.js} +3 -3
- package/dist/{chunk-J6ZTRFFW.js → chunk-TY6UP74X.js} +6 -2
- package/dist/chunk-TY6UP74X.js.map +1 -0
- package/dist/chunk-WRJCINZ7.js +569 -0
- package/dist/chunk-WRJCINZ7.js.map +1 -0
- package/dist/chunk-WRUTATW4.js +47 -0
- package/dist/chunk-WRUTATW4.js.map +1 -0
- package/dist/chunk-ZLV4NMFX.js +333 -0
- package/dist/chunk-ZLV4NMFX.js.map +1 -0
- package/dist/cli/index.d.ts +3 -0
- package/dist/cli/index.js +565 -497
- package/dist/cli/index.js.map +1 -1
- package/dist/{inbox-57NHQHY3.js → inbox-JLT5G7TL.js} +5 -3
- package/dist/index.d.ts +517 -0
- package/dist/index.js +570 -2719
- package/dist/index.js.map +1 -1
- package/dist/{migrate-N6NXMZ3G.js → migrate-WDVFHUP7.js} +4 -3
- package/dist/orchestrator-2ZLU7RGE.js +60 -0
- package/dist/{pool-7I4VACG2.js → pool-B34NTH6D.js} +5 -3
- package/dist/{snapshot-ZBCSX67Y.js → snapshot-JQDJ346Z.js} +5 -4
- package/dist/snapshot-JQDJ346Z.js.map +1 -0
- package/package.json +10 -3
- package/config/watchlist.json +0 -9
- package/dist/chunk-2SYXKKZF.js +0 -837
- package/dist/chunk-2SYXKKZF.js.map +0 -1
- package/dist/chunk-63FUJLIF.js.map +0 -1
- package/dist/chunk-IFTFDHZX.js.map +0 -1
- package/dist/chunk-IPOZFZPY.js.map +0 -1
- package/dist/chunk-J6ZTRFFW.js.map +0 -1
- package/dist/chunk-PQTSXW4F.js.map +0 -1
- package/dist/chunk-VIZNIQLK.js.map +0 -1
- /package/dist/{bindings-XPPSKILN.js.map → bindings-225SI7KC.js.map} +0 -0
- /package/dist/{chunk-RB3NLLGF.js.map → chunk-QOQ7EPZZ.js.map} +0 -0
- /package/dist/{inbox-57NHQHY3.js.map → inbox-JLT5G7TL.js.map} +0 -0
- /package/dist/{migrate-N6NXMZ3G.js.map → migrate-WDVFHUP7.js.map} +0 -0
- /package/dist/{pool-7I4VACG2.js.map → orchestrator-2ZLU7RGE.js.map} +0 -0
- /package/dist/{snapshot-ZBCSX67Y.js.map → pool-B34NTH6D.js.map} +0 -0
|
@@ -2,7 +2,7 @@ import {
|
|
|
2
2
|
isEncrypted,
|
|
3
3
|
loadEncryptedText,
|
|
4
4
|
saveEncryptedText
|
|
5
|
-
} from "./chunk-
|
|
5
|
+
} from "./chunk-7TW6DD6Q.js";
|
|
6
6
|
|
|
7
7
|
// src/vault/migrate.ts
|
|
8
8
|
import fs from "fs";
|
|
@@ -139,6 +139,7 @@ function applyMigrationEntries(guard, paths, files, now = Date.now()) {
|
|
|
139
139
|
const backedUp = [];
|
|
140
140
|
const skipped = [];
|
|
141
141
|
const stamp = new Date(now).toISOString().replace(/[-:T]/g, "").slice(0, 14);
|
|
142
|
+
const staged = [];
|
|
142
143
|
for (const f of files) {
|
|
143
144
|
const rel = String(f.path ?? "");
|
|
144
145
|
if (!rel || rel.includes("..") || path.isAbsolute(rel)) {
|
|
@@ -153,16 +154,53 @@ function applyMigrationEntries(guard, paths, files, now = Date.now()) {
|
|
|
153
154
|
skipped.push(rel);
|
|
154
155
|
continue;
|
|
155
156
|
}
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
fs.
|
|
159
|
-
|
|
157
|
+
const incoming = `${absCanon}.incoming-${stamp}`;
|
|
158
|
+
try {
|
|
159
|
+
fs.mkdirSync(path.dirname(absCanon), { recursive: true });
|
|
160
|
+
if (f.encrypted) saveEncryptedText(guard, incoming, f.content);
|
|
161
|
+
else fs.writeFileSync(incoming, f.content, "utf8");
|
|
162
|
+
staged.push({ rel, abs: absCanon, incoming });
|
|
163
|
+
} catch {
|
|
164
|
+
skipped.push(rel);
|
|
165
|
+
try {
|
|
166
|
+
fs.rmSync(incoming, { force: true });
|
|
167
|
+
} catch {
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
const committed = [];
|
|
172
|
+
try {
|
|
173
|
+
for (const s of staged) {
|
|
174
|
+
let backup = null;
|
|
175
|
+
if (fs.existsSync(s.abs)) {
|
|
176
|
+
backup = `${s.abs}.bak-migrate-${stamp}`;
|
|
177
|
+
fs.copyFileSync(s.abs, backup);
|
|
178
|
+
backedUp.push(s.rel);
|
|
179
|
+
}
|
|
180
|
+
fs.renameSync(s.incoming, s.abs);
|
|
181
|
+
committed.push({ rel: s.rel, abs: s.abs, backup });
|
|
182
|
+
restored.push(s.rel);
|
|
183
|
+
}
|
|
184
|
+
return { restored, backedUp, skipped };
|
|
185
|
+
} catch (e) {
|
|
186
|
+
const rolledBack = [];
|
|
187
|
+
for (const c of committed.reverse()) {
|
|
188
|
+
try {
|
|
189
|
+
if (c.backup) fs.renameSync(c.backup, c.abs);
|
|
190
|
+
else fs.rmSync(c.abs, { force: true });
|
|
191
|
+
rolledBack.push(c.rel);
|
|
192
|
+
} catch {
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
for (const s of staged) {
|
|
196
|
+
if (committed.some((c) => c.abs === s.abs)) continue;
|
|
197
|
+
try {
|
|
198
|
+
fs.rmSync(s.incoming, { force: true });
|
|
199
|
+
} catch {
|
|
200
|
+
}
|
|
160
201
|
}
|
|
161
|
-
|
|
162
|
-
else fs.writeFileSync(absCanon, f.content, "utf8");
|
|
163
|
-
restored.push(rel);
|
|
202
|
+
return { restored: [], backedUp, skipped, rolledBack, error: String(e) };
|
|
164
203
|
}
|
|
165
|
-
return { restored, backedUp, skipped };
|
|
166
204
|
}
|
|
167
205
|
|
|
168
206
|
export {
|
|
@@ -174,4 +212,4 @@ export {
|
|
|
174
212
|
decryptContainer,
|
|
175
213
|
applyMigrationEntries
|
|
176
214
|
};
|
|
177
|
-
//# sourceMappingURL=chunk-
|
|
215
|
+
//# sourceMappingURL=chunk-2NB4QILF.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/vault/migrate.ts"],"sourcesContent":["// Cross-machine migration (v1.8.0). DPAPI binds the sensitive stores to one\r\n// machine + account, so a new machine cannot read them — this module packs\r\n// the memory files into ONE passphrase-encrypted container (AES-256-GCM,\r\n// scrypt-derived key, node:crypto only) that can travel safely, and restores\r\n// them on the other side re-encrypted with the LOCAL DPAPI.\r\n//\r\n// Restore is byte-identical: for each file we record whether it was DPAPI\r\n// encrypted on disk (isEncrypted magic check) and write it back through the\r\n// same path — no per-module write semantics to keep in sync. Existing files\r\n// are backed up before overwrite (rule 4).\r\n\r\nimport fs from 'node:fs';\r\nimport path from 'node:path';\r\nimport {\r\n createCipheriv,\r\n createDecipheriv,\r\n randomBytes,\r\n scryptSync,\r\n} from 'node:crypto';\r\nimport type { PathGuard } from '../core/path-guard.js';\r\nimport type { WorkspacePaths } from '../core/paths.js';\r\nimport { isEncrypted, loadEncryptedText, saveEncryptedText } from './vault.js';\r\n\r\nexport const MIGRATE_MAGIC = 'HBMIG1';\r\nexport const MIGRATE_VERSION = 1;\r\n/** scrypt cost for NEW containers (2^14 = Node's default, 16 MiB of work).\r\n * Reading is parameterised by the container's own kdf.N, so raising this\r\n * never orphans an existing export. */\r\nconst KDF_N = 163_84;\r\n/** Hard bounds for a container-supplied N: a hostile/broken container must not\r\n * be able to make the import allocate unbounded memory. */\r\nconst KDF_N_MIN = 1 << 12; // 2^12 = 4 MiB\r\nconst KDF_N_MAX = 1 << 16; // 2^16 = 64 MiB\r\n/** Node's scrypt maxmem defaults to 32 MiB, which N=2^15 already exceeds — a\r\n * legal container with a larger N would fail without an explicit cap. */\r\nconst SCRYPT_MAXMEM = 128 * 1024 * 1024;\r\nconst KEY_LEN = 32;\r\n\r\n/** Memory-relevant runtime files (dataDir-relative). Deliberately NOT here:\r\n * gate.json (machine-local counters + home session id — the new machine\r\n * builds its own) and screen snapshots (per-beat transients, not memory). */\r\nexport const MIGRATE_FILES = [\r\n 'profile.json',\r\n 'profile_snapshot.json',\r\n 'profile_inbox.jsonl',\r\n 'profile_journal.jsonl',\r\n 'profile_rhythm.json',\r\n 'sent.json',\r\n 'browse.json',\r\n 'seeds.jsonl',\r\n 'ledger.md',\r\n 'cursors.json',\r\n // v1.9.0: persona report log + topic preference stats\r\n 'seed_report.jsonl',\r\n 'preference.json',\r\n];\r\n\r\nexport interface MigrateEntry {\r\n path: string; // dataDir-relative\r\n encrypted: boolean; // was DPAPI-encrypted on the source machine\r\n content: string;\r\n}\r\n\r\nexport interface MigrateProgress {\r\n packed: string[];\r\n missing: string[];\r\n}\r\n\r\nfunction collectWeeklyFiles(guard: PathGuard, paths: WorkspacePaths): string[] {\r\n const dir = path.join(paths.dataDir, 'weekly');\r\n try {\r\n return fs.readdirSync(guard.assert(dir))\r\n .filter((f) => f.endsWith('.json'))\r\n .map((f) => path.join('weekly', f).replace(/\\\\/g, '/'));\r\n } catch {\r\n return [];\r\n }\r\n}\r\n\r\n/** Journal archive shards (snapshot rotation output) travel with the memory. */\r\nfunction collectJournalArchives(guard: PathGuard, paths: WorkspacePaths): string[] {\r\n try {\r\n return fs.readdirSync(guard.assert(paths.dataDir))\r\n .filter((f) => f.startsWith('profile_journal.archive-') && f.endsWith('.jsonl'));\r\n } catch {\r\n return [];\r\n }\r\n}\r\n\r\n/** Read every migration file. A DPAPI file that fails to decrypt aborts the\r\n * export — a silent half-memory container would be worse than a loud error. */\r\nexport function collectMigrationEntries(guard: PathGuard, paths: WorkspacePaths): { entries: MigrateEntry[]; progress: MigrateProgress } {\r\n const entries: MigrateEntry[] = [];\r\n const packed: string[] = [];\r\n const missing: string[] = [];\r\n const rels = [...MIGRATE_FILES, ...collectJournalArchives(guard, paths), ...collectWeeklyFiles(guard, paths)];\r\n for (const rel of rels) {\r\n const abs = path.join(paths.dataDir, rel);\r\n let absCanon: string;\r\n try {\r\n absCanon = guard.assert(abs);\r\n } catch {\r\n missing.push(rel);\r\n continue;\r\n }\r\n if (!fs.existsSync(absCanon)) {\r\n missing.push(rel);\r\n continue;\r\n }\r\n const encrypted = isEncrypted(absCanon);\r\n const content = encrypted\r\n ? loadEncryptedText(guard, absCanon) ?? (() => { throw new Error(`cannot decrypt ${rel} on this machine`); })()\r\n : fs.readFileSync(absCanon, 'utf8');\r\n entries.push({ path: rel.replace(/\\\\/g, '/'), encrypted, content });\r\n packed.push(rel);\r\n }\r\n return { entries, progress: { packed, missing } };\r\n}\r\n\r\n/** Derive the key with the container's OWN cost parameter. `n` must already\r\n * have passed assertKdfN (export uses the current default). */\r\nfunction deriveKey(passphrase: string, salt: Buffer, n: number): Buffer {\r\n return scryptSync(passphrase, salt, KEY_LEN, { N: n, r: 8, p: 1, maxmem: SCRYPT_MAXMEM });\r\n}\r\n\r\n/** Validate the scrypt cost carried by a container. A missing/absurd N is a\r\n * BROKEN container — reported as such, never as \"wrong passphrase\" (before\r\n * 2026-10-06 the N in the container was ignored and the two were the same\r\n * error, so raising the cost would have orphaned every old export). */\r\nfunction assertKdfN(v: unknown): number {\r\n const n = Number(v);\r\n if (!Number.isInteger(n) || n <= 0 || (n & (n - 1)) !== 0) {\r\n throw new Error('migrate: container has invalid kdf parameters (N must be a power of two)');\r\n }\r\n if (n < KDF_N_MIN || n > KDF_N_MAX) {\r\n throw new Error(`migrate: container kdf N ${n} is outside the supported range [${KDF_N_MIN}, ${KDF_N_MAX}]`);\r\n }\r\n return n;\r\n}\r\n\r\n/** Build the container file content. Everything sensitive lives inside the\r\n * ciphertext; the outer JSON carries only crypto parameters. */\r\nexport function encryptContainer(entries: MigrateEntry[], passphrase: string, now = new Date()): string {\r\n if (!passphrase) throw new Error('migrate: passphrase must not be empty');\r\n const salt = randomBytes(16);\r\n const iv = randomBytes(12);\r\n const key = deriveKey(passphrase, salt, KDF_N);\r\n const cipher = createCipheriv('aes-256-gcm', key, iv);\r\n const plain = Buffer.from(JSON.stringify({ version: MIGRATE_VERSION, createdAt: now.toISOString(), files: entries }), 'utf8');\r\n const data = Buffer.concat([cipher.update(plain), cipher.final()]);\r\n const tag = cipher.getAuthTag();\r\n return JSON.stringify({\r\n magic: MIGRATE_MAGIC,\r\n kdf: { name: 'scrypt', salt: salt.toString('hex'), N: KDF_N },\r\n iv: iv.toString('hex'),\r\n tag: tag.toString('hex'),\r\n data: data.toString('base64'),\r\n }, null, 1) + '\\n';\r\n}\r\n\r\n/** Parse + decrypt a container. Throws (loudly) on a wrong passphrase or a\r\n * corrupted file — never returns partial data. */\r\nexport function decryptContainer(text: string, passphrase: string): { createdAt: string; files: MigrateEntry[] } {\r\n let outer: { magic?: string; kdf?: { salt?: string; N?: unknown }; iv?: string; tag?: string; data?: string };\r\n try {\r\n outer = JSON.parse(text);\r\n } catch {\r\n throw new Error('migrate: not a migration container (bad JSON)');\r\n }\r\n if (outer.magic !== MIGRATE_MAGIC) throw new Error('migrate: not a migration container (bad magic)');\r\n const n = assertKdfN(outer.kdf?.N);\r\n const salt = Buffer.from(String(outer.kdf?.salt ?? ''), 'hex');\r\n const iv = Buffer.from(String(outer.iv ?? ''), 'hex');\r\n const tag = Buffer.from(String(outer.tag ?? ''), 'hex');\r\n const data = Buffer.from(String(outer.data ?? ''), 'base64');\r\n const key = deriveKey(passphrase, salt, n);\r\n const decipher = createDecipheriv('aes-256-gcm', key, iv);\r\n decipher.setAuthTag(tag);\r\n let plain: Buffer;\r\n try {\r\n plain = Buffer.concat([decipher.update(data), decipher.final()]);\r\n } catch {\r\n throw new Error('migrate: wrong passphrase or corrupted container');\r\n }\r\n const inner = JSON.parse(plain.toString('utf8')) as { version: number; createdAt: string; files: MigrateEntry[] };\r\n if (inner.version !== MIGRATE_VERSION) throw new Error(`migrate: unsupported container version ${inner.version}`);\r\n if (!Array.isArray(inner.files)) throw new Error('migrate: container has no file list');\r\n return { createdAt: inner.createdAt, files: inner.files };\r\n}\r\n\r\nexport interface MigrateImportResult {\r\n restored: string[];\r\n backedUp: string[];\r\n skipped: string[];\r\n /** H-21: files put back from their backups after a failed commit phase. */\r\n rolledBack?: string[];\r\n /** H-21: set when the commit phase failed and the import was rolled back. */\r\n error?: string;\r\n}\r\n\r\n/**\r\n * Restore entries into the local dataDir, re-encrypting DPAPI files with the\r\n * LOCAL machine key.\r\n *\r\n * H-21: this used to be a per-file \"back up, then overwrite\" loop with no\r\n * pre-flight and no rollback — a failure halfway through left a mixed memory\r\n * (a new `profile.json` next to a stale `profile_journal.jsonl`). Now it runs\r\n * in two phases: stage every entry next to its target (nothing visible changes\r\n * yet, and a staging failure only skips that entry), then commit each one by\r\n * rename. A commit failure rolls the already-committed files back from their\r\n * backups, so the import is all-or-nothing apart from entries that never\r\n * staged.\r\n */\r\nexport function applyMigrationEntries(guard: PathGuard, paths: WorkspacePaths, files: MigrateEntry[], now = Date.now()): MigrateImportResult {\r\n const restored: string[] = [];\r\n const backedUp: string[] = [];\r\n const skipped: string[] = [];\r\n const stamp = new Date(now).toISOString().replace(/[-:T]/g, '').slice(0, 14);\r\n const staged: Array<{ rel: string; abs: string; incoming: string }> = [];\r\n for (const f of files) {\r\n const rel = String(f.path ?? '');\r\n // String-level rejection BEFORE the path is resolved: the guard's realpath\r\n // check already refuses escapes, but an import must not depend on another\r\n // module staying correct.\r\n if (!rel || rel.includes('..') || path.isAbsolute(rel)) {\r\n skipped.push(rel);\r\n continue;\r\n }\r\n const abs = path.join(paths.dataDir, rel);\r\n let absCanon: string;\r\n try {\r\n absCanon = guard.assert(abs);\r\n } catch {\r\n skipped.push(rel); // path refused by the guard — never write it\r\n continue;\r\n }\r\n const incoming = `${absCanon}.incoming-${stamp}`;\r\n try {\r\n fs.mkdirSync(path.dirname(absCanon), { recursive: true });\r\n if (f.encrypted) saveEncryptedText(guard, incoming, f.content);\r\n else fs.writeFileSync(incoming, f.content, 'utf8');\r\n staged.push({ rel, abs: absCanon, incoming });\r\n } catch {\r\n skipped.push(rel);\r\n try {\r\n fs.rmSync(incoming, { force: true });\r\n } catch {\r\n /* nothing staged */\r\n }\r\n }\r\n }\r\n const committed: Array<{ rel: string; abs: string; backup: string | null }> = [];\r\n try {\r\n for (const s of staged) {\r\n let backup: string | null = null;\r\n if (fs.existsSync(s.abs)) {\r\n backup = `${s.abs}.bak-migrate-${stamp}`;\r\n fs.copyFileSync(s.abs, backup);\r\n backedUp.push(s.rel);\r\n }\r\n fs.renameSync(s.incoming, s.abs);\r\n committed.push({ rel: s.rel, abs: s.abs, backup });\r\n restored.push(s.rel);\r\n }\r\n return { restored, backedUp, skipped };\r\n } catch (e) {\r\n // Roll back so the dataDir never holds a half-migrated mix; staged files\r\n // that were never committed are simply dropped.\r\n const rolledBack: string[] = [];\r\n for (const c of committed.reverse()) {\r\n try {\r\n if (c.backup) fs.renameSync(c.backup, c.abs);\r\n else fs.rmSync(c.abs, { force: true });\r\n rolledBack.push(c.rel);\r\n } catch {\r\n /* best effort — the backup file itself is still on disk */\r\n }\r\n }\r\n for (const s of staged) {\r\n if (committed.some((c) => c.abs === s.abs)) continue;\r\n try {\r\n fs.rmSync(s.incoming, { force: true });\r\n } catch {\r\n /* already gone */\r\n }\r\n }\r\n return { restored: [], backedUp, skipped, rolledBack, error: String(e) };\r\n }\r\n}\r\n"],"mappings":";;;;;;;AAWA,OAAO,QAAQ;AACf,OAAO,UAAU;AACjB;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AAKA,IAAM,gBAAgB;AACtB,IAAM,kBAAkB;AAI/B,IAAM,QAAQ;AAGd,IAAM,YAAY,KAAK;AACvB,IAAM,YAAY,KAAK;AAGvB,IAAM,gBAAgB,MAAM,OAAO;AACnC,IAAM,UAAU;AAKT,IAAM,gBAAgB;AAAA,EAC3B;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA;AAAA,EAEA;AAAA,EACA;AACF;AAaA,SAAS,mBAAmB,OAAkB,OAAiC;AAC7E,QAAM,MAAM,KAAK,KAAK,MAAM,SAAS,QAAQ;AAC7C,MAAI;AACF,WAAO,GAAG,YAAY,MAAM,OAAO,GAAG,CAAC,EACpC,OAAO,CAAC,MAAM,EAAE,SAAS,OAAO,CAAC,EACjC,IAAI,CAAC,MAAM,KAAK,KAAK,UAAU,CAAC,EAAE,QAAQ,OAAO,GAAG,CAAC;AAAA,EAC1D,QAAQ;AACN,WAAO,CAAC;AAAA,EACV;AACF;AAGA,SAAS,uBAAuB,OAAkB,OAAiC;AACjF,MAAI;AACF,WAAO,GAAG,YAAY,MAAM,OAAO,MAAM,OAAO,CAAC,EAC9C,OAAO,CAAC,MAAM,EAAE,WAAW,0BAA0B,KAAK,EAAE,SAAS,QAAQ,CAAC;AAAA,EACnF,QAAQ;AACN,WAAO,CAAC;AAAA,EACV;AACF;AAIO,SAAS,wBAAwB,OAAkB,OAA+E;AACvI,QAAM,UAA0B,CAAC;AACjC,QAAM,SAAmB,CAAC;AAC1B,QAAM,UAAoB,CAAC;AAC3B,QAAM,OAAO,CAAC,GAAG,eAAe,GAAG,uBAAuB,OAAO,KAAK,GAAG,GAAG,mBAAmB,OAAO,KAAK,CAAC;AAC5G,aAAW,OAAO,MAAM;AACtB,UAAM,MAAM,KAAK,KAAK,MAAM,SAAS,GAAG;AACxC,QAAI;AACJ,QAAI;AACF,iBAAW,MAAM,OAAO,GAAG;AAAA,IAC7B,QAAQ;AACN,cAAQ,KAAK,GAAG;AAChB;AAAA,IACF;AACA,QAAI,CAAC,GAAG,WAAW,QAAQ,GAAG;AAC5B,cAAQ,KAAK,GAAG;AAChB;AAAA,IACF;AACA,UAAM,YAAY,YAAY,QAAQ;AACtC,UAAM,UAAU,YACZ,kBAAkB,OAAO,QAAQ,MAAM,MAAM;AAAE,YAAM,IAAI,MAAM,kBAAkB,GAAG,kBAAkB;AAAA,IAAG,GAAG,IAC5G,GAAG,aAAa,UAAU,MAAM;AACpC,YAAQ,KAAK,EAAE,MAAM,IAAI,QAAQ,OAAO,GAAG,GAAG,WAAW,QAAQ,CAAC;AAClE,WAAO,KAAK,GAAG;AAAA,EACjB;AACA,SAAO,EAAE,SAAS,UAAU,EAAE,QAAQ,QAAQ,EAAE;AAClD;AAIA,SAAS,UAAU,YAAoB,MAAc,GAAmB;AACtE,SAAO,WAAW,YAAY,MAAM,SAAS,EAAE,GAAG,GAAG,GAAG,GAAG,GAAG,GAAG,QAAQ,cAAc,CAAC;AAC1F;AAMA,SAAS,WAAW,GAAoB;AACtC,QAAM,IAAI,OAAO,CAAC;AAClB,MAAI,CAAC,OAAO,UAAU,CAAC,KAAK,KAAK,MAAM,IAAK,IAAI,OAAQ,GAAG;AACzD,UAAM,IAAI,MAAM,0EAA0E;AAAA,EAC5F;AACA,MAAI,IAAI,aAAa,IAAI,WAAW;AAClC,UAAM,IAAI,MAAM,4BAA4B,CAAC,oCAAoC,SAAS,KAAK,SAAS,GAAG;AAAA,EAC7G;AACA,SAAO;AACT;AAIO,SAAS,iBAAiB,SAAyB,YAAoB,MAAM,oBAAI,KAAK,GAAW;AACtG,MAAI,CAAC,WAAY,OAAM,IAAI,MAAM,uCAAuC;AACxE,QAAM,OAAO,YAAY,EAAE;AAC3B,QAAM,KAAK,YAAY,EAAE;AACzB,QAAM,MAAM,UAAU,YAAY,MAAM,KAAK;AAC7C,QAAM,SAAS,eAAe,eAAe,KAAK,EAAE;AACpD,QAAM,QAAQ,OAAO,KAAK,KAAK,UAAU,EAAE,SAAS,iBAAiB,WAAW,IAAI,YAAY,GAAG,OAAO,QAAQ,CAAC,GAAG,MAAM;AAC5H,QAAM,OAAO,OAAO,OAAO,CAAC,OAAO,OAAO,KAAK,GAAG,OAAO,MAAM,CAAC,CAAC;AACjE,QAAM,MAAM,OAAO,WAAW;AAC9B,SAAO,KAAK,UAAU;AAAA,IACpB,OAAO;AAAA,IACP,KAAK,EAAE,MAAM,UAAU,MAAM,KAAK,SAAS,KAAK,GAAG,GAAG,MAAM;AAAA,IAC5D,IAAI,GAAG,SAAS,KAAK;AAAA,IACrB,KAAK,IAAI,SAAS,KAAK;AAAA,IACvB,MAAM,KAAK,SAAS,QAAQ;AAAA,EAC9B,GAAG,MAAM,CAAC,IAAI;AAChB;AAIO,SAAS,iBAAiB,MAAc,YAAkE;AAC/G,MAAI;AACJ,MAAI;AACF,YAAQ,KAAK,MAAM,IAAI;AAAA,EACzB,QAAQ;AACN,UAAM,IAAI,MAAM,+CAA+C;AAAA,EACjE;AACA,MAAI,MAAM,UAAU,cAAe,OAAM,IAAI,MAAM,gDAAgD;AACnG,QAAM,IAAI,WAAW,MAAM,KAAK,CAAC;AACjC,QAAM,OAAO,OAAO,KAAK,OAAO,MAAM,KAAK,QAAQ,EAAE,GAAG,KAAK;AAC7D,QAAM,KAAK,OAAO,KAAK,OAAO,MAAM,MAAM,EAAE,GAAG,KAAK;AACpD,QAAM,MAAM,OAAO,KAAK,OAAO,MAAM,OAAO,EAAE,GAAG,KAAK;AACtD,QAAM,OAAO,OAAO,KAAK,OAAO,MAAM,QAAQ,EAAE,GAAG,QAAQ;AAC3D,QAAM,MAAM,UAAU,YAAY,MAAM,CAAC;AACzC,QAAM,WAAW,iBAAiB,eAAe,KAAK,EAAE;AACxD,WAAS,WAAW,GAAG;AACvB,MAAI;AACJ,MAAI;AACF,YAAQ,OAAO,OAAO,CAAC,SAAS,OAAO,IAAI,GAAG,SAAS,MAAM,CAAC,CAAC;AAAA,EACjE,QAAQ;AACN,UAAM,IAAI,MAAM,kDAAkD;AAAA,EACpE;AACA,QAAM,QAAQ,KAAK,MAAM,MAAM,SAAS,MAAM,CAAC;AAC/C,MAAI,MAAM,YAAY,gBAAiB,OAAM,IAAI,MAAM,0CAA0C,MAAM,OAAO,EAAE;AAChH,MAAI,CAAC,MAAM,QAAQ,MAAM,KAAK,EAAG,OAAM,IAAI,MAAM,qCAAqC;AACtF,SAAO,EAAE,WAAW,MAAM,WAAW,OAAO,MAAM,MAAM;AAC1D;AAyBO,SAAS,sBAAsB,OAAkB,OAAuB,OAAuB,MAAM,KAAK,IAAI,GAAwB;AAC3I,QAAM,WAAqB,CAAC;AAC5B,QAAM,WAAqB,CAAC;AAC5B,QAAM,UAAoB,CAAC;AAC3B,QAAM,QAAQ,IAAI,KAAK,GAAG,EAAE,YAAY,EAAE,QAAQ,UAAU,EAAE,EAAE,MAAM,GAAG,EAAE;AAC3E,QAAM,SAAgE,CAAC;AACvE,aAAW,KAAK,OAAO;AACrB,UAAM,MAAM,OAAO,EAAE,QAAQ,EAAE;AAI/B,QAAI,CAAC,OAAO,IAAI,SAAS,IAAI,KAAK,KAAK,WAAW,GAAG,GAAG;AACtD,cAAQ,KAAK,GAAG;AAChB;AAAA,IACF;AACA,UAAM,MAAM,KAAK,KAAK,MAAM,SAAS,GAAG;AACxC,QAAI;AACJ,QAAI;AACF,iBAAW,MAAM,OAAO,GAAG;AAAA,IAC7B,QAAQ;AACN,cAAQ,KAAK,GAAG;AAChB;AAAA,IACF;AACA,UAAM,WAAW,GAAG,QAAQ,aAAa,KAAK;AAC9C,QAAI;AACF,SAAG,UAAU,KAAK,QAAQ,QAAQ,GAAG,EAAE,WAAW,KAAK,CAAC;AACxD,UAAI,EAAE,UAAW,mBAAkB,OAAO,UAAU,EAAE,OAAO;AAAA,UACxD,IAAG,cAAc,UAAU,EAAE,SAAS,MAAM;AACjD,aAAO,KAAK,EAAE,KAAK,KAAK,UAAU,SAAS,CAAC;AAAA,IAC9C,QAAQ;AACN,cAAQ,KAAK,GAAG;AAChB,UAAI;AACF,WAAG,OAAO,UAAU,EAAE,OAAO,KAAK,CAAC;AAAA,MACrC,QAAQ;AAAA,MAER;AAAA,IACF;AAAA,EACF;AACA,QAAM,YAAwE,CAAC;AAC/E,MAAI;AACF,eAAW,KAAK,QAAQ;AACtB,UAAI,SAAwB;AAC5B,UAAI,GAAG,WAAW,EAAE,GAAG,GAAG;AACxB,iBAAS,GAAG,EAAE,GAAG,gBAAgB,KAAK;AACtC,WAAG,aAAa,EAAE,KAAK,MAAM;AAC7B,iBAAS,KAAK,EAAE,GAAG;AAAA,MACrB;AACA,SAAG,WAAW,EAAE,UAAU,EAAE,GAAG;AAC/B,gBAAU,KAAK,EAAE,KAAK,EAAE,KAAK,KAAK,EAAE,KAAK,OAAO,CAAC;AACjD,eAAS,KAAK,EAAE,GAAG;AAAA,IACrB;AACA,WAAO,EAAE,UAAU,UAAU,QAAQ;AAAA,EACvC,SAAS,GAAG;AAGV,UAAM,aAAuB,CAAC;AAC9B,eAAW,KAAK,UAAU,QAAQ,GAAG;AACnC,UAAI;AACF,YAAI,EAAE,OAAQ,IAAG,WAAW,EAAE,QAAQ,EAAE,GAAG;AAAA,YACtC,IAAG,OAAO,EAAE,KAAK,EAAE,OAAO,KAAK,CAAC;AACrC,mBAAW,KAAK,EAAE,GAAG;AAAA,MACvB,QAAQ;AAAA,MAER;AAAA,IACF;AACA,eAAW,KAAK,QAAQ;AACtB,UAAI,UAAU,KAAK,CAAC,MAAM,EAAE,QAAQ,EAAE,GAAG,EAAG;AAC5C,UAAI;AACF,WAAG,OAAO,EAAE,UAAU,EAAE,OAAO,KAAK,CAAC;AAAA,MACvC,QAAQ;AAAA,MAER;AAAA,IACF;AACA,WAAO,EAAE,UAAU,CAAC,GAAG,UAAU,SAAS,YAAY,OAAO,OAAO,CAAC,EAAE;AAAA,EACzE;AACF;","names":[]}
|
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
import {
|
|
2
|
+
burnFileSync
|
|
3
|
+
} from "./chunk-WRUTATW4.js";
|
|
4
|
+
|
|
1
5
|
// src/core/paths.ts
|
|
2
6
|
import fs from "fs";
|
|
3
7
|
import path from "path";
|
|
@@ -107,7 +111,7 @@ function loadJson(guard, file) {
|
|
|
107
111
|
runVault(guard, ["unprotect", "-InFile", f, "-OutFile", tmp]);
|
|
108
112
|
return JSON.parse(fs2.readFileSync(tmp, "utf8"));
|
|
109
113
|
} finally {
|
|
110
|
-
|
|
114
|
+
burnFileSync(tmp);
|
|
111
115
|
}
|
|
112
116
|
}
|
|
113
117
|
function saveJson(guard, file, value) {
|
|
@@ -120,7 +124,7 @@ function saveJson(guard, file, value) {
|
|
|
120
124
|
runVault(guard, ["protect", "-InFile", plain, "-OutFile", enc]);
|
|
121
125
|
fs2.renameSync(enc, f);
|
|
122
126
|
} finally {
|
|
123
|
-
|
|
127
|
+
burnFileSync(plain);
|
|
124
128
|
fs2.rmSync(enc, { force: true });
|
|
125
129
|
}
|
|
126
130
|
}
|
|
@@ -140,7 +144,7 @@ function writeText(guard, file, content) {
|
|
|
140
144
|
fs2.writeFileSync(tmp, content, "utf8");
|
|
141
145
|
fs2.renameSync(tmp, f);
|
|
142
146
|
} finally {
|
|
143
|
-
|
|
147
|
+
burnFileSync(tmp);
|
|
144
148
|
}
|
|
145
149
|
}
|
|
146
150
|
function loadEncryptedText(guard, file) {
|
|
@@ -152,7 +156,7 @@ function loadEncryptedText(guard, file) {
|
|
|
152
156
|
runVault(guard, ["unprotect", "-InFile", f, "-OutFile", tmp]);
|
|
153
157
|
return fs2.readFileSync(tmp, "utf8");
|
|
154
158
|
} finally {
|
|
155
|
-
|
|
159
|
+
burnFileSync(tmp);
|
|
156
160
|
}
|
|
157
161
|
}
|
|
158
162
|
function saveEncryptedText(guard, file, content) {
|
|
@@ -165,7 +169,7 @@ function saveEncryptedText(guard, file, content) {
|
|
|
165
169
|
runVault(guard, ["protect", "-InFile", plain, "-OutFile", enc]);
|
|
166
170
|
fs2.renameSync(enc, f);
|
|
167
171
|
} finally {
|
|
168
|
-
|
|
172
|
+
burnFileSync(plain);
|
|
169
173
|
fs2.rmSync(enc, { force: true });
|
|
170
174
|
}
|
|
171
175
|
}
|
|
@@ -190,6 +194,7 @@ function decryptFile(guard, inFile, outFile) {
|
|
|
190
194
|
|
|
191
195
|
export {
|
|
192
196
|
initWorkspace,
|
|
197
|
+
workspace,
|
|
193
198
|
isEncrypted,
|
|
194
199
|
loadJson,
|
|
195
200
|
saveJson,
|
|
@@ -200,4 +205,4 @@ export {
|
|
|
200
205
|
encryptFile,
|
|
201
206
|
decryptFile
|
|
202
207
|
};
|
|
203
|
-
//# sourceMappingURL=chunk-
|
|
208
|
+
//# sourceMappingURL=chunk-7TW6DD6Q.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/core/paths.ts","../src/vault/vault.ts"],"sourcesContent":["// Workspace/data directory resolution and package asset location.\n//\n// The data dir is the ONLY directory the plugin writes to at runtime\n// (requirement 8). Resolution order: env override > plugin config > default\n// (packageRoot/data). All fs writes must go through the path guard created\n// from this dir (see core/path-guard.ts).\n\nimport fs from 'node:fs';\nimport path from 'node:path';\nimport { fileURLToPath } from 'node:url';\n\nexport interface WorkspacePaths {\n /** Guard boundary and the only writable tree at runtime. */\n dataDir: string;\n settingsDir: string;\n logsDir: string;\n tmpDir: string;\n exportsDir: string;\n /** Package root (where package.json lives). Read-only at runtime. */\n packageRoot: string;\n configDir: string;\n assetsDir: string;\n}\n\nexport interface WorkspaceConfigInput {\n dataDir?: string | null;\n}\n\nlet resolved: WorkspacePaths | null = null;\n\n/** Walk up from a module file to the directory containing package.json. */\nexport function findPackageRoot(startFile: string): string {\n let dir = path.dirname(path.resolve(startFile));\n for (;;) {\n if (fs.existsSync(path.join(dir, 'package.json'))) return dir;\n const parent = path.dirname(dir);\n if (parent === dir) throw new Error('package.json not found above ' + startFile);\n dir = parent;\n }\n}\n\nexport function packageRoot(): string {\n return findPackageRoot(fileURLToPath(import.meta.url));\n}\n\n/**\n * Resolve and create the workspace layout. Idempotent; must be called once\n * from the plugin entry before any fs write happens.\n */\nexport function initWorkspace(config: WorkspaceConfigInput = {}): WorkspacePaths {\n if (resolved) return resolved;\n const root = packageRoot();\n const dataDir = path.resolve(\n process.env.HEARTBEAT_DATA_DIR || config.dataDir || path.join(root, 'data'),\n );\n const paths: WorkspacePaths = {\n dataDir,\n settingsDir: path.join(dataDir, 'settings'),\n logsDir: path.join(dataDir, 'logs'),\n tmpDir: path.join(dataDir, 'tmp'),\n exportsDir: path.join(dataDir, 'exports'),\n packageRoot: root,\n configDir: path.join(root, 'config'),\n assetsDir: path.join(root, 'assets'),\n };\n // Bootstrap creation happens before the guard exists; creating exactly one\n // directory tree at a known location is the safe window for this.\n for (const dir of [\n paths.dataDir,\n paths.settingsDir,\n paths.logsDir,\n paths.tmpDir,\n paths.exportsDir,\n ]) {\n fs.mkdirSync(dir, { recursive: true });\n }\n resolved = paths;\n return paths;\n}\n\n/** Access already-initialized paths. Throws if initWorkspace was not called. */\nexport function workspace(): WorkspacePaths {\n if (!resolved) throw new Error('workspace not initialized: call initWorkspace() first');\n return resolved;\n}\n\n/** Test-only: forget the resolved singleton so a later init re-resolves. */\nexport function resetWorkspaceForTest(): void {\n resolved = null;\n}\n","// Transparent encryption read/write layer (port of v1 lib/vault.mjs).\n//\n// Mechanism: Node <-> vault.ps1 hand off through temp files (plain text\n// windows are millisecond-scale). v2 hardening over v1:\n// - plaintext temp files live ONLY under data/tmp (never beside targets);\n// - every path passes the workspace guard before reaching PowerShell;\n// - encrypted output lands via same-volume rename (atomic replace).\n// Encryption failure is fail-closed: throw, never fall back to plaintext.\n\nimport fs from 'node:fs';\nimport path from 'node:path';\nimport { spawnSync } from 'node:child_process';\nimport { randomUUID } from 'node:crypto';\nimport type { PathGuard } from '../core/path-guard.js';\nimport { workspace } from '../core/paths.js';\nimport { burnFileSync } from '../core/atomic-fs.js';\n\nconst MAGIC = Buffer.from('KHBV1', 'ascii');\nconst VAULT_TIMEOUT_MS = 30_000;\n\nlet cachedScriptPath: string | null = null;\n\nfunction vaultScriptPath(): string {\n if (cachedScriptPath) return cachedScriptPath;\n const script = path.join(workspace().assetsDir, 'vault.ps1');\n if (!fs.existsSync(script)) throw new Error(`vault script missing: ${script}`);\n cachedScriptPath = script;\n return script;\n}\n\nfunction tmpPlainName(target: string): string {\n return path.join(workspace().tmpDir, `.${path.basename(target)}.${randomUUID().slice(0, 8)}.plain`);\n}\n\nfunction tmpEncName(target: string): string {\n return path.join(workspace().tmpDir, `.${path.basename(target)}.${randomUUID().slice(0, 8)}.enc`);\n}\n\nfunction runVault(guard: PathGuard, args: string[]): void {\n const r = spawnSync(\n 'powershell.exe',\n ['-NoProfile', '-ExecutionPolicy', 'Bypass', '-File', vaultScriptPath(), ...args],\n { timeout: VAULT_TIMEOUT_MS, encoding: 'utf8' },\n );\n if (r.error) throw new Error(`vault spawn failed: ${String(r.error)}`);\n if (r.status !== 0) {\n const detail = (r.stderr || r.stdout || '').trim().split('\\n').slice(-3).join('; ');\n throw new Error(`vault ${args[0]} failed (exit ${r.status}): ${detail}`);\n }\n}\n\n/** KHBV1 header check without reading the whole file. */\nexport function isEncrypted(file: string): boolean {\n if (!fs.existsSync(file)) return false;\n const fd = fs.openSync(file, 'r');\n try {\n const hdr = Buffer.alloc(MAGIC.length);\n const read = fs.readSync(fd, hdr, 0, hdr.length, 0);\n return read === MAGIC.length && hdr.equals(MAGIC);\n } finally {\n fs.closeSync(fd);\n }\n}\n\n/**\n * Read JSON: encrypted files are decrypted to a data/tmp plaintext window\n * that is shredded immediately after parse; plaintext stays compatible.\n */\nexport function loadJson<T>(guard: PathGuard, file: string): T | null {\n const f = guard.assert(file);\n if (!fs.existsSync(f)) return null;\n if (!isEncrypted(f)) {\n try {\n return JSON.parse(fs.readFileSync(f, 'utf8')) as T;\n } catch {\n return null;\n }\n }\n const tmp = tmpPlainName(f);\n try {\n runVault(guard, ['unprotect', '-InFile', f, '-OutFile', tmp]);\n return JSON.parse(fs.readFileSync(tmp, 'utf8')) as T;\n } finally {\n burnFileSync(tmp);\n }\n}\n\n/** Write JSON: plaintext window in data/tmp -> DPAPI encrypt -> atomic rename into place. */\nexport function saveJson(guard: PathGuard, file: string, value: unknown): void {\n const f = guard.assert(file);\n fs.mkdirSync(path.dirname(f), { recursive: true });\n const plain = tmpPlainName(f);\n const enc = tmpEncName(f);\n try {\n fs.writeFileSync(plain, JSON.stringify(value, null, 2), 'utf8');\n runVault(guard, ['protect', '-InFile', plain, '-OutFile', enc]);\n fs.renameSync(enc, f);\n } finally {\n burnFileSync(plain);\n fs.rmSync(enc, { force: true });\n }\n}\n\n/** Read a plaintext UTF-8 text file (ledger etc.), with fallback. */\nexport function readText(guard: PathGuard, file: string, fallback = ''): string {\n const f = guard.assert(file);\n try {\n return fs.readFileSync(f, 'utf8');\n } catch {\n return fallback;\n }\n}\n\n/** Write a plaintext UTF-8 text file atomically (ledger etc.). */\nexport function writeText(guard: PathGuard, file: string, content: string): void {\n const f = guard.assert(file);\n fs.mkdirSync(path.dirname(f), { recursive: true });\n const tmp = tmpPlainName(f);\n try {\n fs.writeFileSync(tmp, content, 'utf8');\n fs.renameSync(tmp, f);\n } finally {\n burnFileSync(tmp);\n }\n}\n\n/** Read an encrypted UTF-8 text file (e.g. seeds.jsonl). Missing -> null. */\nexport function loadEncryptedText(guard: PathGuard, file: string): string | null {\n const f = guard.assert(file);\n if (!fs.existsSync(f)) return null;\n if (!isEncrypted(f)) return fs.readFileSync(f, 'utf8');\n const tmp = tmpPlainName(f);\n try {\n runVault(guard, ['unprotect', '-InFile', f, '-OutFile', tmp]);\n return fs.readFileSync(tmp, 'utf8');\n } finally {\n burnFileSync(tmp);\n }\n}\n\n/** Write an encrypted UTF-8 text file atomically. */\nexport function saveEncryptedText(guard: PathGuard, file: string, content: string): void {\n const f = guard.assert(file);\n fs.mkdirSync(path.dirname(f), { recursive: true });\n const plain = tmpPlainName(f);\n const enc = tmpEncName(f);\n try {\n fs.writeFileSync(plain, content, 'utf8');\n runVault(guard, ['protect', '-InFile', plain, '-OutFile', enc]);\n fs.renameSync(enc, f);\n } finally {\n burnFileSync(plain);\n fs.rmSync(enc, { force: true });\n }\n}\n\n/** Encrypt any binary file (e.g. screenshot): inFile -> encrypted outFile. */\nexport function encryptFile(guard: PathGuard, inFile: string, outFile: string): void {\n const inPath = guard.assert(inFile);\n const outPath = guard.assert(outFile);\n fs.mkdirSync(path.dirname(outPath), { recursive: true });\n const enc = tmpEncName(outPath);\n try {\n runVault(guard, ['protect', '-InFile', inPath, '-OutFile', enc]);\n fs.renameSync(enc, outPath);\n } finally {\n fs.rmSync(enc, { force: true });\n }\n}\n\n/**\n * Decrypt an encrypted file to a plaintext output (caller must place the\n * output under data/tmp and delete it after use; \"use then burn\" discipline).\n */\nexport function decryptFile(guard: PathGuard, inFile: string, outFile: string): void {\n const inPath = guard.assert(inFile);\n const outPath = guard.assert(outFile);\n fs.mkdirSync(path.dirname(outPath), { recursive: true });\n runVault(guard, ['unprotect', '-InFile', inPath, '-OutFile', outPath]);\n}\n"],"mappings":";;;;;AAOA,OAAO,QAAQ;AACf,OAAO,UAAU;AACjB,SAAS,qBAAqB;AAmB9B,IAAI,WAAkC;AAG/B,SAAS,gBAAgB,WAA2B;AACzD,MAAI,MAAM,KAAK,QAAQ,KAAK,QAAQ,SAAS,CAAC;AAC9C,aAAS;AACP,QAAI,GAAG,WAAW,KAAK,KAAK,KAAK,cAAc,CAAC,EAAG,QAAO;AAC1D,UAAM,SAAS,KAAK,QAAQ,GAAG;AAC/B,QAAI,WAAW,IAAK,OAAM,IAAI,MAAM,kCAAkC,SAAS;AAC/E,UAAM;AAAA,EACR;AACF;AAEO,SAAS,cAAsB;AACpC,SAAO,gBAAgB,cAAc,YAAY,GAAG,CAAC;AACvD;AAMO,SAAS,cAAc,SAA+B,CAAC,GAAmB;AAC/E,MAAI,SAAU,QAAO;AACrB,QAAM,OAAO,YAAY;AACzB,QAAM,UAAU,KAAK;AAAA,IACnB,QAAQ,IAAI,sBAAsB,OAAO,WAAW,KAAK,KAAK,MAAM,MAAM;AAAA,EAC5E;AACA,QAAM,QAAwB;AAAA,IAC5B;AAAA,IACA,aAAa,KAAK,KAAK,SAAS,UAAU;AAAA,IAC1C,SAAS,KAAK,KAAK,SAAS,MAAM;AAAA,IAClC,QAAQ,KAAK,KAAK,SAAS,KAAK;AAAA,IAChC,YAAY,KAAK,KAAK,SAAS,SAAS;AAAA,IACxC,aAAa;AAAA,IACb,WAAW,KAAK,KAAK,MAAM,QAAQ;AAAA,IACnC,WAAW,KAAK,KAAK,MAAM,QAAQ;AAAA,EACrC;AAGA,aAAW,OAAO;AAAA,IAChB,MAAM;AAAA,IACN,MAAM;AAAA,IACN,MAAM;AAAA,IACN,MAAM;AAAA,IACN,MAAM;AAAA,EACR,GAAG;AACD,OAAG,UAAU,KAAK,EAAE,WAAW,KAAK,CAAC;AAAA,EACvC;AACA,aAAW;AACX,SAAO;AACT;AAGO,SAAS,YAA4B;AAC1C,MAAI,CAAC,SAAU,OAAM,IAAI,MAAM,uDAAuD;AACtF,SAAO;AACT;;;AC3EA,OAAOA,SAAQ;AACf,OAAOC,WAAU;AACjB,SAAS,iBAAiB;AAC1B,SAAS,kBAAkB;AAK3B,IAAM,QAAQ,OAAO,KAAK,SAAS,OAAO;AAC1C,IAAM,mBAAmB;AAEzB,IAAI,mBAAkC;AAEtC,SAAS,kBAA0B;AACjC,MAAI,iBAAkB,QAAO;AAC7B,QAAM,SAASC,MAAK,KAAK,UAAU,EAAE,WAAW,WAAW;AAC3D,MAAI,CAACC,IAAG,WAAW,MAAM,EAAG,OAAM,IAAI,MAAM,yBAAyB,MAAM,EAAE;AAC7E,qBAAmB;AACnB,SAAO;AACT;AAEA,SAAS,aAAa,QAAwB;AAC5C,SAAOD,MAAK,KAAK,UAAU,EAAE,QAAQ,IAAIA,MAAK,SAAS,MAAM,CAAC,IAAI,WAAW,EAAE,MAAM,GAAG,CAAC,CAAC,QAAQ;AACpG;AAEA,SAAS,WAAW,QAAwB;AAC1C,SAAOA,MAAK,KAAK,UAAU,EAAE,QAAQ,IAAIA,MAAK,SAAS,MAAM,CAAC,IAAI,WAAW,EAAE,MAAM,GAAG,CAAC,CAAC,MAAM;AAClG;AAEA,SAAS,SAAS,OAAkB,MAAsB;AACxD,QAAM,IAAI;AAAA,IACR;AAAA,IACA,CAAC,cAAc,oBAAoB,UAAU,SAAS,gBAAgB,GAAG,GAAG,IAAI;AAAA,IAChF,EAAE,SAAS,kBAAkB,UAAU,OAAO;AAAA,EAChD;AACA,MAAI,EAAE,MAAO,OAAM,IAAI,MAAM,uBAAuB,OAAO,EAAE,KAAK,CAAC,EAAE;AACrE,MAAI,EAAE,WAAW,GAAG;AAClB,UAAM,UAAU,EAAE,UAAU,EAAE,UAAU,IAAI,KAAK,EAAE,MAAM,IAAI,EAAE,MAAM,EAAE,EAAE,KAAK,IAAI;AAClF,UAAM,IAAI,MAAM,SAAS,KAAK,CAAC,CAAC,iBAAiB,EAAE,MAAM,MAAM,MAAM,EAAE;AAAA,EACzE;AACF;AAGO,SAAS,YAAY,MAAuB;AACjD,MAAI,CAACC,IAAG,WAAW,IAAI,EAAG,QAAO;AACjC,QAAM,KAAKA,IAAG,SAAS,MAAM,GAAG;AAChC,MAAI;AACF,UAAM,MAAM,OAAO,MAAM,MAAM,MAAM;AACrC,UAAM,OAAOA,IAAG,SAAS,IAAI,KAAK,GAAG,IAAI,QAAQ,CAAC;AAClD,WAAO,SAAS,MAAM,UAAU,IAAI,OAAO,KAAK;AAAA,EAClD,UAAE;AACA,IAAAA,IAAG,UAAU,EAAE;AAAA,EACjB;AACF;AAMO,SAAS,SAAY,OAAkB,MAAwB;AACpE,QAAM,IAAI,MAAM,OAAO,IAAI;AAC3B,MAAI,CAACA,IAAG,WAAW,CAAC,EAAG,QAAO;AAC9B,MAAI,CAAC,YAAY,CAAC,GAAG;AACnB,QAAI;AACF,aAAO,KAAK,MAAMA,IAAG,aAAa,GAAG,MAAM,CAAC;AAAA,IAC9C,QAAQ;AACN,aAAO;AAAA,IACT;AAAA,EACF;AACA,QAAM,MAAM,aAAa,CAAC;AAC1B,MAAI;AACF,aAAS,OAAO,CAAC,aAAa,WAAW,GAAG,YAAY,GAAG,CAAC;AAC5D,WAAO,KAAK,MAAMA,IAAG,aAAa,KAAK,MAAM,CAAC;AAAA,EAChD,UAAE;AACA,iBAAa,GAAG;AAAA,EAClB;AACF;AAGO,SAAS,SAAS,OAAkB,MAAc,OAAsB;AAC7E,QAAM,IAAI,MAAM,OAAO,IAAI;AAC3B,EAAAA,IAAG,UAAUD,MAAK,QAAQ,CAAC,GAAG,EAAE,WAAW,KAAK,CAAC;AACjD,QAAM,QAAQ,aAAa,CAAC;AAC5B,QAAM,MAAM,WAAW,CAAC;AACxB,MAAI;AACF,IAAAC,IAAG,cAAc,OAAO,KAAK,UAAU,OAAO,MAAM,CAAC,GAAG,MAAM;AAC9D,aAAS,OAAO,CAAC,WAAW,WAAW,OAAO,YAAY,GAAG,CAAC;AAC9D,IAAAA,IAAG,WAAW,KAAK,CAAC;AAAA,EACtB,UAAE;AACA,iBAAa,KAAK;AAClB,IAAAA,IAAG,OAAO,KAAK,EAAE,OAAO,KAAK,CAAC;AAAA,EAChC;AACF;AAGO,SAAS,SAAS,OAAkB,MAAc,WAAW,IAAY;AAC9E,QAAM,IAAI,MAAM,OAAO,IAAI;AAC3B,MAAI;AACF,WAAOA,IAAG,aAAa,GAAG,MAAM;AAAA,EAClC,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAGO,SAAS,UAAU,OAAkB,MAAc,SAAuB;AAC/E,QAAM,IAAI,MAAM,OAAO,IAAI;AAC3B,EAAAA,IAAG,UAAUD,MAAK,QAAQ,CAAC,GAAG,EAAE,WAAW,KAAK,CAAC;AACjD,QAAM,MAAM,aAAa,CAAC;AAC1B,MAAI;AACF,IAAAC,IAAG,cAAc,KAAK,SAAS,MAAM;AACrC,IAAAA,IAAG,WAAW,KAAK,CAAC;AAAA,EACtB,UAAE;AACA,iBAAa,GAAG;AAAA,EAClB;AACF;AAGO,SAAS,kBAAkB,OAAkB,MAA6B;AAC/E,QAAM,IAAI,MAAM,OAAO,IAAI;AAC3B,MAAI,CAACA,IAAG,WAAW,CAAC,EAAG,QAAO;AAC9B,MAAI,CAAC,YAAY,CAAC,EAAG,QAAOA,IAAG,aAAa,GAAG,MAAM;AACrD,QAAM,MAAM,aAAa,CAAC;AAC1B,MAAI;AACF,aAAS,OAAO,CAAC,aAAa,WAAW,GAAG,YAAY,GAAG,CAAC;AAC5D,WAAOA,IAAG,aAAa,KAAK,MAAM;AAAA,EACpC,UAAE;AACA,iBAAa,GAAG;AAAA,EAClB;AACF;AAGO,SAAS,kBAAkB,OAAkB,MAAc,SAAuB;AACvF,QAAM,IAAI,MAAM,OAAO,IAAI;AAC3B,EAAAA,IAAG,UAAUD,MAAK,QAAQ,CAAC,GAAG,EAAE,WAAW,KAAK,CAAC;AACjD,QAAM,QAAQ,aAAa,CAAC;AAC5B,QAAM,MAAM,WAAW,CAAC;AACxB,MAAI;AACF,IAAAC,IAAG,cAAc,OAAO,SAAS,MAAM;AACvC,aAAS,OAAO,CAAC,WAAW,WAAW,OAAO,YAAY,GAAG,CAAC;AAC9D,IAAAA,IAAG,WAAW,KAAK,CAAC;AAAA,EACtB,UAAE;AACA,iBAAa,KAAK;AAClB,IAAAA,IAAG,OAAO,KAAK,EAAE,OAAO,KAAK,CAAC;AAAA,EAChC;AACF;AAGO,SAAS,YAAY,OAAkB,QAAgB,SAAuB;AACnF,QAAM,SAAS,MAAM,OAAO,MAAM;AAClC,QAAM,UAAU,MAAM,OAAO,OAAO;AACpC,EAAAA,IAAG,UAAUD,MAAK,QAAQ,OAAO,GAAG,EAAE,WAAW,KAAK,CAAC;AACvD,QAAM,MAAM,WAAW,OAAO;AAC9B,MAAI;AACF,aAAS,OAAO,CAAC,WAAW,WAAW,QAAQ,YAAY,GAAG,CAAC;AAC/D,IAAAC,IAAG,WAAW,KAAK,OAAO;AAAA,EAC5B,UAAE;AACA,IAAAA,IAAG,OAAO,KAAK,EAAE,OAAO,KAAK,CAAC;AAAA,EAChC;AACF;AAMO,SAAS,YAAY,OAAkB,QAAgB,SAAuB;AACnF,QAAM,SAAS,MAAM,OAAO,MAAM;AAClC,QAAM,UAAU,MAAM,OAAO,OAAO;AACpC,EAAAA,IAAG,UAAUD,MAAK,QAAQ,OAAO,GAAG,EAAE,WAAW,KAAK,CAAC;AACvD,WAAS,OAAO,CAAC,aAAa,WAAW,QAAQ,YAAY,OAAO,CAAC;AACvE;","names":["fs","path","path","fs"]}
|
|
@@ -1,7 +1,10 @@
|
|
|
1
|
+
import {
|
|
2
|
+
withFileLock
|
|
3
|
+
} from "./chunk-IV2ZWQA3.js";
|
|
1
4
|
import {
|
|
2
5
|
loadEncryptedText,
|
|
3
6
|
saveEncryptedText
|
|
4
|
-
} from "./chunk-
|
|
7
|
+
} from "./chunk-7TW6DD6Q.js";
|
|
5
8
|
|
|
6
9
|
// src/profile/inbox.ts
|
|
7
10
|
import fs from "fs";
|
|
@@ -16,6 +19,9 @@ function truncateNote(note) {
|
|
|
16
19
|
return oneLine.length > MAX_NOTE_CHARS ? oneLine.slice(0, MAX_NOTE_CHARS) + "\u2026" : oneLine;
|
|
17
20
|
}
|
|
18
21
|
function inboxAppend(guard, file, item) {
|
|
22
|
+
return withFileLock(file, () => inboxAppendLocked(guard, file, item));
|
|
23
|
+
}
|
|
24
|
+
function inboxAppendLocked(guard, file, item) {
|
|
19
25
|
const full = {
|
|
20
26
|
id: item.id ?? randomUUID().slice(0, 8),
|
|
21
27
|
kind: item.kind,
|
|
@@ -24,6 +30,15 @@ function inboxAppend(guard, file, item) {
|
|
|
24
30
|
note: truncateNote(item.note)
|
|
25
31
|
};
|
|
26
32
|
const prev = loadEncryptedText(guard, file) ?? "";
|
|
33
|
+
for (const line of prev.split("\n")) {
|
|
34
|
+
const trimmed = line.trim();
|
|
35
|
+
if (!trimmed) continue;
|
|
36
|
+
try {
|
|
37
|
+
const existing = JSON.parse(trimmed);
|
|
38
|
+
if (existing.kind === full.kind && existing.ref === full.ref) return existing;
|
|
39
|
+
} catch {
|
|
40
|
+
}
|
|
41
|
+
}
|
|
27
42
|
saveEncryptedText(guard, file, prev + JSON.stringify(full) + "\n");
|
|
28
43
|
return full;
|
|
29
44
|
}
|
|
@@ -95,4 +110,4 @@ export {
|
|
|
95
110
|
inboxHealthCheck,
|
|
96
111
|
inboxFileExists
|
|
97
112
|
};
|
|
98
|
-
//# sourceMappingURL=chunk-
|
|
113
|
+
//# sourceMappingURL=chunk-BQCXYE7N.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/profile/inbox.ts"],"sourcesContent":["// Observation inbox (design doc §3.2): append-only queue of pending\n// observations; drained by the consolidation run. DPAPI-encrypted. Items hold\n// pointers + at most one short sentence - never raw conversation/screen text.\n\nimport fs from 'node:fs';\nimport path from 'node:path';\nimport { randomUUID } from 'node:crypto';\nimport type { PathGuard } from '../core/path-guard.js';\nimport { loadEncryptedText, saveEncryptedText } from '../vault/vault.js';\nimport { withFileLock } from '../core/file-lock.js';\nimport type { InboxItem } from './types.js';\n\nconst MAX_NOTE_CHARS = 120;\n\nexport function inboxFilePath(dataDir: string): string {\n return path.join(dataDir, 'profile_inbox.jsonl');\n}\n\nfunction truncateNote(note: string): string {\n const oneLine = note.replace(/\\r?\\n/g, ' ').trim();\n return oneLine.length > MAX_NOTE_CHARS ? oneLine.slice(0, MAX_NOTE_CHARS) + '…' : oneLine;\n}\n\nexport function inboxAppend(\n guard: PathGuard,\n file: string,\n item: Omit<InboxItem, 'id'> & { id?: string },\n): InboxItem {\n // H-20 (minimal tier): the append is a whole-file encrypted rewrite, so it\n // needs the same cross-process lock the pool uses — the CLI can be draining\n // or writing the same file while a beat appends.\n return withFileLock(file, () => inboxAppendLocked(guard, file, item));\n}\n\nfunction inboxAppendLocked(\n guard: PathGuard,\n file: string,\n item: Omit<InboxItem, 'id'> & { id?: string },\n): InboxItem {\n const full: InboxItem = {\n id: item.id ?? randomUUID().slice(0, 8),\n kind: item.kind,\n at: item.at,\n ref: item.ref,\n note: truncateNote(item.note),\n };\n const prev = loadEncryptedText(guard, file) ?? '';\n // H-16 (minimal tier): this file is a whole-file encrypted rewrite, so a\n // repeated observation used to cost a full decrypt + encrypt + write for a\n // duplicate row. The §3.7 dedupe key is kind+ref — if that row is already\n // queued, hand it back instead of appending a second copy. (A stalled cursor\n // used to grow the queue here and rely on the consumer's dedupeItems.)\n for (const line of prev.split('\\n')) {\n const trimmed = line.trim();\n if (!trimmed) continue;\n try {\n const existing = JSON.parse(trimmed) as InboxItem;\n if (existing.kind === full.kind && existing.ref === full.ref) return existing;\n } catch {\n // corrupt line: ignore (the queue is re-derivable from its sources)\n }\n }\n saveEncryptedText(guard, file, prev + JSON.stringify(full) + '\\n');\n return full;\n}\n\nexport function inboxCount(guard: PathGuard, file: string): number {\n const raw = loadEncryptedText(guard, file);\n if (!raw) return 0;\n return raw.split('\\n').filter((l) => l.trim()).length;\n}\n\n/**\n * Drain all items. `commit` controls crash semantics: on consolidation failure\n * the inbox must SURVIVE (§3.7), so the caller drains with commit=false first,\n * and only rewrites an empty inbox after the run succeeded.\n */\nexport function inboxDrain(guard: PathGuard, file: string): InboxItem[] {\n const raw = loadEncryptedText(guard, file);\n if (!raw) return [];\n const items: InboxItem[] = [];\n for (const line of raw.split('\\n')) {\n const trimmed = line.trim();\n if (!trimmed) continue;\n try {\n items.push(JSON.parse(trimmed) as InboxItem);\n } catch {\n // corrupt line: drop (queue is re-derivable from sources)\n }\n }\n return items;\n}\n\nexport function inboxClear(guard: PathGuard, file: string): void {\n saveEncryptedText(guard, file, '');\n}\n\n/** Dedupe key (§3.7): same source kind + same ref must not flood the queue. */\nexport function dedupeItems(items: InboxItem[]): InboxItem[] {\n const seen = new Set<string>();\n const out: InboxItem[] = [];\n for (const it of items) {\n const key = `${it.kind}#${it.ref}`;\n if (seen.has(key)) continue;\n seen.add(key);\n out.push(it);\n }\n return out;\n}\n\nexport function inboxHealthCheck(guard: PathGuard, file: string): { total: number; corrupt: number; duplicates: number } {\n const raw = loadEncryptedText(guard, file) ?? '';\n let total = 0;\n let corrupt = 0;\n const keys = new Set<string>();\n let duplicates = 0;\n for (const line of raw.split('\\n')) {\n const trimmed = line.trim();\n if (!trimmed) continue;\n try {\n const it = JSON.parse(trimmed) as InboxItem;\n total += 1;\n const key = `${it.kind}#${it.ref}`;\n if (keys.has(key)) duplicates += 1;\n keys.add(key);\n } catch {\n corrupt += 1;\n }\n }\n return { total, corrupt, duplicates };\n}\n\nexport function inboxFileExists(guard: PathGuard, file: string): boolean {\n return fs.existsSync(guard.assert(file));\n}\n"],"mappings":"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","names":[]}
|