@hoilab/ada-cli 0.84.18 → 0.84.20
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/dist/core/agent-session.d.ts +10 -2
- package/dist/core/agent-session.d.ts.map +1 -1
- package/dist/core/agent-session.js +70 -18
- package/dist/core/agent-session.js.map +1 -1
- package/dist/core/memory-engine/audit.d.ts +1 -1
- package/dist/core/memory-engine/audit.d.ts.map +1 -1
- package/dist/core/memory-engine/audit.js.map +1 -1
- package/dist/core/memory-engine/engine.d.ts +2 -0
- package/dist/core/memory-engine/engine.d.ts.map +1 -1
- package/dist/core/memory-engine/engine.js +4 -0
- package/dist/core/memory-engine/engine.js.map +1 -1
- package/dist/core/memory-engine/keychain.d.ts +18 -0
- package/dist/core/memory-engine/keychain.d.ts.map +1 -0
- package/dist/core/memory-engine/keychain.js +93 -0
- package/dist/core/memory-engine/keychain.js.map +1 -0
- package/dist/core/memory-engine/security.d.ts.map +1 -1
- package/dist/core/memory-engine/security.js +10 -0
- package/dist/core/memory-engine/security.js.map +1 -1
- package/dist/core/memory-engine/vault.d.ts +22 -5
- package/dist/core/memory-engine/vault.d.ts.map +1 -1
- package/dist/core/memory-engine/vault.js +64 -8
- package/dist/core/memory-engine/vault.js.map +1 -1
- package/dist/core/resource-loader.d.ts.map +1 -1
- package/dist/core/resource-loader.js +1 -0
- package/dist/core/resource-loader.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
- package/dist/modes/interactive/interactive-mode.js +24 -0
- package/dist/modes/interactive/interactive-mode.js.map +1 -1
- package/npm-shrinkwrap.json +2 -2
- package/package.json +1 -1
|
@@ -28,6 +28,16 @@ const ALWAYS_BLOCKED = [
|
|
|
28
28
|
{ category: "secret-assignment", pattern: /\b(password|passwd|pwd)\s*[=:]\s*\S{6,}\b/i, always: true, label: "password value" },
|
|
29
29
|
{ category: "secret-assignment", pattern: /\b(?:contraseña|contraseña|clave)\s*[=:]\s*\S{6,}\b/i, always: true, label: "contraseña valor" },
|
|
30
30
|
{ category: "secret-assignment", pattern: /\b(?:contraseña|password|passwd|pwd|clave)\b[^\n]{0,40}\b\d{6,}\b/i, always: true, label: "password-like value" },
|
|
31
|
+
{
|
|
32
|
+
category: "secret-assignment",
|
|
33
|
+
// Secret keyword followed by a value on the same line: the match spans
|
|
34
|
+
// keyword + value so redaction never leaves the value exposed. The
|
|
35
|
+
// value must be an alnum token (≥4 chars) containing a digit or symbol,
|
|
36
|
+
// which avoids false positives like "contraseña por defecto".
|
|
37
|
+
pattern: /\b(?:contraseña|password|passwd|pwd|clave|token|api[_-]?key|secret)\b[^\n]{0,40}\b(?=[A-Za-z0-9_\-!@#$%^&*]*\d)[A-Za-z0-9_\-!@#$%^&*]{4,}/i,
|
|
38
|
+
always: true,
|
|
39
|
+
label: "secret value after keyword",
|
|
40
|
+
},
|
|
31
41
|
{ category: "secret-assignment", pattern: /\b(?:recuerda|guarda|memoriza|remember|save|store)\b.{0,50}\b(?:contraseña|password|passwd|pwd|clave|token|api[_-]?key)\b/i, always: true, label: "request to store a secret" },
|
|
32
42
|
{ category: "secret-assignment", pattern: /\b(secret|token|api[_-]?key|access[_-]?key)\s*[=:]\s*\S{10,}\b/i, always: true, label: "secret/token value" },
|
|
33
43
|
{ category: "credential", pattern: /\b(ANTHROPIC|OPENAI|OPENROUTER|GEMINI|DEEPSEEK|MISTRAL|AZURE)_?(API)?_?KEY\b/i, always: true, label: "provider API key env var" },
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"security.js","sourceRoot":"","sources":["../../../src/core/memory-engine/security.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Security & privacy classification for the Ada Memory Engine.\n *\n * Enterprise-grade guards:\n * - Credentials/secrets/payment data are ALWAYS blocked at every level.\n * - PII (email, phone, national IDs, IBAN, DOB) blocking depends on the\n * configured privacy level (RGPD / AI Act friendly).\n * - Every rejection is written to the audit log with a reason.\n * - Prompt-injection/exfiltration patterns are always blocked.\n */\n\nexport type PrivacyLevel = \"strict\" | \"balanced\" | \"permissive\";\n\nexport type SensitiveCategory =\n\t| \"credential\"\n\t| \"payment\"\n\t| \"secret-assignment\"\n\t| \"pii-email\"\n\t| \"pii-phone\"\n\t| \"pii-id\"\n\t| \"pii-bank\"\n\t| \"pii-dob\"\n\t| \"injection\"\n\t| \"exfiltration\";\n\nexport interface SensitivityResult {\n\t/** True when the content must NOT be stored. */\n\tblocked: boolean;\n\t/** Categories detected. */\n\tcategories: SensitiveCategory[];\n\t/** Human-readable reason (in the configured language). */\n\treason: string;\n\t/** True when content contains permitted-but-noteworthy categories. */\n\tnoteworthy: boolean;\n}\n\ninterface Pattern {\n\tcategory: SensitiveCategory;\n\tpattern: RegExp;\n\t/** Always blocked regardless of privacy level. */\n\talways?: boolean;\n\t/** Blocked at this level and stricter. */\n\tblockAt?: PrivacyLevel;\n\t/** English reason label. */\n\tlabel: string;\n}\n\n// ─── Credentials & secrets — ALWAYS blocked ───\nconst ALWAYS_BLOCKED: Pattern[] = [\n\t{ category: \"credential\", pattern: /\\bsk-ant-api\\S{10,}\\b/, always: true, label: \"Anthropic API key\" },\n\t{ category: \"credential\", pattern: /\\bsk-or-v1-\\S{10,}\\b/, always: true, label: \"OpenRouter API key\" },\n\t{ category: \"credential\", pattern: /\\bsk-[A-Za-z0-9]{20,}\\b/, always: true, label: \"OpenAI-style API key\" },\n\t{ category: \"credential\", pattern: /\\bAKIA[0-9A-Z]{16}\\b/, always: true, label: \"AWS access key\" },\n\t{ category: \"credential\", pattern: /\\bghp_[A-Za-z0-9]{20,}\\b/, always: true, label: \"GitHub personal token\" },\n\t{ category: \"credential\", pattern: /\\bghu_[A-Za-z0-9]{20,}\\b/, always: true, label: \"GitHub user token\" },\n\t{ category: \"credential\", pattern: /\\bxox[baprs]-[A-Za-z0-9-]{10,}\\b/, always: true, label: \"Slack token\" },\n\t{ category: \"credential\", pattern: /\\bxapp-[A-Za-z0-9-]{10,}\\b/, always: true, label: \"Slack app token\" },\n\t{ category: \"credential\", pattern: /\\bntn_[A-Za-z0-9]{20,}\\b/, always: true, label: \"Notion token\" },\n\t{ category: \"credential\", pattern: /\\beyJ[A-Za-z0-9_-]{10,}\\.[A-Za-z0-9_-]{10,}\\.[A-Za-z0-9_-]{10,}/, always: true, label: \"JWT token\" },\n\t{ category: \"credential\", pattern: /-----BEGIN\\s+(?:RSA\\s+|EC\\s+|OPENSSH\\s+)?PRIVATE\\s+KEY-----/, always: true, label: \"Private key block\" },\n\t{ category: \"credential\", pattern: /\\bBearer\\s+[A-Za-z0-9._~+/=-]{20,}\\b/i, always: true, label: \"Bearer auth token\" },\n\t{ category: \"credential\", pattern: /\\bapi[_-]?key\\s*[=:]\\s*[A-Za-z0-9_-]{16,}\\b/i, always: true, label: \"API key assignment\" },\n\t{ category: \"credential\", pattern: /\\b(client|app|consumer)_?secret\\s*[=:]\\s*[A-Za-z0-9_-]{16,}\\b/i, always: true, label: \"OAuth client secret\" },\n\t{ category: \"credential\", pattern: /\\bwebhook[_-]?url\\s*[=:]\\s*https?:\\/\\/[^\\s]+/i, always: true, label: \"Webhook URL\" },\n\t{ category: \"secret-assignment\", pattern: /\\b(password|passwd|pwd)\\s*[=:]\\s*\\S{6,}\\b/i, always: true, label: \"password value\" },\n\t{ category: \"secret-assignment\", pattern: /\\b(?:contraseña|contraseña|clave)\\s*[=:]\\s*\\S{6,}\\b/i, always: true, label: \"contraseña valor\" },\n\t{ category: \"secret-assignment\", pattern: /\\b(?:contraseña|password|passwd|pwd|clave)\\b[^\\n]{0,40}\\b\\d{6,}\\b/i, always: true, label: \"password-like value\" },\n\t{ category: \"secret-assignment\", pattern: /\\b(?:recuerda|guarda|memoriza|remember|save|store)\\b.{0,50}\\b(?:contraseña|password|passwd|pwd|clave|token|api[_-]?key)\\b/i, always: true, label: \"request to store a secret\" },\n\t{ category: \"secret-assignment\", pattern: /\\b(secret|token|api[_-]?key|access[_-]?key)\\s*[=:]\\s*\\S{10,}\\b/i, always: true, label: \"secret/token value\" },\n\t{ category: \"credential\", pattern: /\\b(ANTHROPIC|OPENAI|OPENROUTER|GEMINI|DEEPSEEK|MISTRAL|AZURE)_?(API)?_?KEY\\b/i, always: true, label: \"provider API key env var\" },\n\t{ category: \"credential\", pattern: /\\bGITHUB_TOKEN\\b|\\bAWS_SECRET_ACCESS_KEY\\b|\\bDATABASE_URL\\b/i, always: true, label: \"secret env var name\" },\n\t{ category: \"credential\", pattern: /(?:mongodb|postgres|mysql|redis|amqp):\\/\\/[^\\s]*:[^\\s@]+@/, always: true, label: \"connection string with credentials\" },\n];\n\n// ─── Payment data — ALWAYS blocked ───\nconst PAYMENT: Pattern[] = [\n\t{\n\t\tcategory: \"payment\",\n\t\tpattern: /\\b(?:4\\d{3}|5[1-5]\\d{2}|3[47]\\d{2}|6(?:011|5\\d{2}))[\\s-]?\\d{4}[\\s-]?\\d{4}[\\s-]?\\d{4}\\b/,\n\t\talways: true,\n\t\tlabel: \"payment card number\",\n\t},\n\t{ category: \"payment\", pattern: /\\bcvv\\s*[=:]\\s*\\d{3,4}\\b/i, always: true, label: \"card verification value\" },\n\t{ category: \"payment\", pattern: /\\b(?:expiry|exp)\\s*[=:]\\s*(?:0[1-9]|1[0-2])\\/?\\d{2}\\b/i, always: true, label: \"card expiry\" },\n];\n\n// ─── PII — level-dependent ───\nconst PII: Pattern[] = [\n\t{\n\t\tcategory: \"pii-email\",\n\t\tpattern: /\\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,}\\b/,\n\t\tblockAt: \"strict\",\n\t\tlabel: \"personal email address\",\n\t},\n\t{\n\t\tcategory: \"pii-phone\",\n\t\tpattern: /(?:\\+\\d{1,3}[\\s-]?)?(?:\\(?\\d{2,4}\\)?[\\s-]?)?\\d{3}[\\s-]?\\d{3}[\\s-]?\\d{3,4}\\b/,\n\t\tblockAt: \"strict\",\n\t\tlabel: \"phone number\",\n\t},\n\t{\n\t\tcategory: \"pii-id\",\n\t\tpattern: /\\b\\d{8}[A-Z]\\b|\\b[A-Z]\\d{7}[A-Z0-9]\\b/, // DNI / NIE (Spain)\n\t\tblockAt: \"strict\",\n\t\tlabel: \"national ID (DNI/NIE)\",\n\t},\n\t{\n\t\tcategory: \"pii-bank\",\n\t\tpattern: /\\b[A-Z]{2}\\d{2}[A-Z0-9]{11,30}\\b/, // IBAN\n\t\tblockAt: \"balanced\",\n\t\tlabel: \"IBAN / bank account\",\n\t},\n\t{\n\t\tcategory: \"pii-dob\",\n\t\tpattern: /\\b(?:birth|birthday|born|nacimiento|fecha de nacimiento)\\s*[=:]\\s*(?:\\d{1,2}[\\/\\-.]\\d{1,2}[\\/\\-.]\\d{2,4})/i,\n\t\tblockAt: \"strict\",\n\t\tlabel: \"date of birth\",\n\t},\n];\n\n// ─── Prompt injection / exfiltration — ALWAYS blocked ───\nconst INJECTION: Pattern[] = [\n\t{ category: \"injection\", pattern: /ignore\\s+(previous|all|above|prior)\\s+instructions/i, always: true, label: \"prompt injection\" },\n\t{ category: \"injection\", pattern: /you\\s+are\\s+now\\s+/i, always: true, label: \"role hijack\" },\n\t{ category: \"injection\", pattern: /do\\s+not\\s+tell\\s+the\\s+user/i, always: true, label: \"deceptive instruction\" },\n\t{ category: \"injection\", pattern: /disregard\\s+(your|all|any)\\s+(instructions|rules|guidelines)/i, always: true, label: \"instruction override\" },\n\t{ category: \"injection\", pattern: /system\\s+prompt\\s+override/i, always: true, label: \"system prompt override\" },\n\t{ category: \"exfiltration\", pattern: /curl\\s+[^\\n]*(KEY|TOKEN|SECRET|PASSWORD|CREDENTIAL|API)/i, always: true, label: \"credential exfiltration (curl)\" },\n\t{ category: \"exfiltration\", pattern: /wget\\s+[^\\n]*(KEY|TOKEN|SECRET|PASSWORD|CREDENTIAL|API)/i, always: true, label: \"credential exfiltration (wget)\" },\n\t{ category: \"exfiltration\", pattern: /cat\\s+[^\\n]*(\\.env|credentials|\\.netrc|\\.pgpass|\\.npmrc|\\.pypirc)/i, always: true, label: \"secret file read\" },\n\t{ category: \"exfiltration\", pattern: /\\$HOME\\/\\.ssh|~\\/\\.ssh/i, always: true, label: \"SSH key path\" },\n];\n\nconst LEVEL_ORDER: Record<PrivacyLevel, number> = { strict: 0, balanced: 1, permissive: 2 };\n\nconst INVISIBLE_CHARS = new Set([\n\t\"\\u200b\", \"\\u200c\", \"\\u200d\", \"\\u2060\", \"\\ufeff\",\n\t\"\\u202a\", \"\\u202b\", \"\\u202c\", \"\\u202d\", \"\\u202e\",\n]);\n\nconst REASON_EN = {\n\tcredential: \"Looks like a credential or secret (secrets are never stored in memory)\",\n\tpayment: \"Looks like payment card data (never stored in memory)\",\n\t\"secret-assignment\": \"Looks like a password/secret/token value (never stored in memory)\",\n\t\"pii-email\": \"Looks like a personal email address (blocked by the strict privacy level)\",\n\t\"pii-phone\": \"Looks like a phone number (blocked by the strict privacy level)\",\n\t\"pii-id\": \"Looks like a national ID (DNI/NIE) (blocked by the strict privacy level)\",\n\t\"pii-bank\": \"Looks like a bank account / IBAN (blocked by the privacy level)\",\n\t\"pii-dob\": \"Looks like a date of birth (blocked by the strict privacy level)\",\n\tinjection: \"Looks like a prompt-injection attempt (never stored)\",\n\texfiltration: \"Looks like data exfiltration (never stored)\",\n};\n\nconst REASON_ES: Record<SensitiveCategory, string> = {\n\tcredential: \"Parece una credencial o secreto (los secretos nunca se almacenan en memoria)\",\n\tpayment: \"Parecen datos de tarjeta de pago (nunca se almacenan en memoria)\",\n\t\"secret-assignment\": \"Parece una contraseña/secreto/token (nunca se almacena en memoria)\",\n\t\"pii-email\": \"Parece un correo electrónico personal (bloqueado por el nivel de privacidad estricto)\",\n\t\"pii-phone\": \"Parece un número de teléfono (bloqueado por el nivel de privacidad estricto)\",\n\t\"pii-id\": \"Parece un documento de identidad (DNI/NIE) (bloqueado por el nivel de privacidad estricto)\",\n\t\"pii-bank\": \"Parece una cuenta bancaria / IBAN (bloqueado por el nivel de privacidad)\",\n\t\"pii-dob\": \"Parece una fecha de nacimiento (bloqueada por el nivel de privacidad estricto)\",\n\tinjection: \"Parece un intento de prompt injection (nunca se almacena)\",\n\texfiltration: \"Parece exfiltración de datos (nunca se almacena)\",\n};\n\nfunction testPattern(pattern: Pattern, content: string): boolean {\n\ttry {\n\t\treturn pattern.pattern.test(content);\n\t} catch {\n\t\treturn false;\n\t}\n}\n\n/**\n * Classify content against the security policy.\n *\n * @param content Text to classify.\n * @param level Configured privacy level.\n * @param locale \"es\" or \"en\" for the reason language.\n */\nexport function classifySensitive(content: string, level: PrivacyLevel = \"strict\", locale: \"es\" | \"en\" = \"es\"): SensitivityResult {\n\tconst categories = new Set<SensitiveCategory>();\n\tconst reasons: string[] = [];\n\tconst labels: string[] = [];\n\n\t// Invisible characters are always suspicious (injection vectors).\n\tif ([...content].some((ch) => INVISIBLE_CHARS.has(ch))) {\n\t\treturn {\n\t\t\tblocked: true,\n\t\t\tcategories: [\"injection\"],\n\t\t\treason: locale === \"es\"\n\t\t\t\t? \"El contenido contiene caracteres invisibles/zero-width y fue rechazado por seguridad.\"\n\t\t\t\t: \"Content contains invisible/zero-width characters and was rejected for security.\",\n\t\t\tnoteworthy: true,\n\t\t};\n\t}\n\n\tconst allPatterns = [...ALWAYS_BLOCKED, ...PAYMENT, ...PII, ...INJECTION];\n\tfor (const pattern of allPatterns) {\n\t\tif (!testPattern(pattern, content)) continue;\n\t\tconst levelIdx = LEVEL_ORDER[level];\n\t\tconst blockAtIdx = pattern.blockAt ? LEVEL_ORDER[pattern.blockAt] : 0;\n\t\tconst blocked = pattern.always === true || levelIdx <= blockAtIdx;\n\t\tcategories.add(pattern.category);\n\t\tif (blocked) {\n\t\t\tlabels.push(pattern.label);\n\t\t\tif (!reasons.includes(pattern.category)) reasons.push(pattern.category);\n\t\t}\n\t}\n\n\tif (reasons.length > 0) {\n\t\tconst localeMap = locale === \"es\" ? REASON_ES : REASON_EN;\n\t\tconst detail = reasons.map((r) => localeMap[r as SensitiveCategory]).join(\"; \");\n\t\treturn {\n\t\t\tblocked: true,\n\t\t\tcategories: [...categories],\n\t\t\treason: detail,\n\t\t\tnoteworthy: true,\n\t\t};\n\t}\n\n\treturn {\n\t\tblocked: false,\n\t\tcategories: [...categories],\n\t\treason: \"\",\n\t\tnoteworthy: categories.size > 0,\n\t};\n}\n\n/**\n * Redact sensitive data from user text so it never reaches the LLM, the\n * session file, or the chat transcript. Only high-confidence patterns are\n * redacted (credentials, payment data, explicit secret assignments, and\n * PII like emails / national IDs / IBANs / DOB).\n */\nexport function redactSensitive(text: string, level: PrivacyLevel = \"strict\"): string {\n\tlet out = text;\n\tconst levelIdx = LEVEL_ORDER[level];\n\tfor (const pattern of [...ALWAYS_BLOCKED, ...PAYMENT, ...PII]) {\n\t\tif (pattern.category === \"pii-phone\") {\n\t\t\t// Phone redaction requires separators or a + prefix to avoid\n\t\t\t// mangling ordinary numbers in code.\n\t\t\tif (!/(?:\\+|\\s|\\()?\\d{3}[\\s.-]\\d{3}[\\s.-]\\d{3,4}/.test(text)) continue;\n\t\t}\n\t\tconst blockAtIdx = pattern.blockAt ? LEVEL_ORDER[pattern.blockAt] : 0;\n\t\tconst active = pattern.always === true || levelIdx <= blockAtIdx;\n\t\tif (!active) continue;\n\t\ttry {\n\t\t\tout = out.replace(pattern.pattern, (match) => \"█\".repeat(Math.min(match.length, 14)));\n\t\t} catch {\n\t\t\t// A bad pattern must never break redaction.\n\t\t}\n\t}\n\treturn out;\n}\n"]}
|
|
1
|
+
{"version":3,"file":"security.js","sourceRoot":"","sources":["../../../src/core/memory-engine/security.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Security & privacy classification for the Ada Memory Engine.\n *\n * Enterprise-grade guards:\n * - Credentials/secrets/payment data are ALWAYS blocked at every level.\n * - PII (email, phone, national IDs, IBAN, DOB) blocking depends on the\n * configured privacy level (RGPD / AI Act friendly).\n * - Every rejection is written to the audit log with a reason.\n * - Prompt-injection/exfiltration patterns are always blocked.\n */\n\nexport type PrivacyLevel = \"strict\" | \"balanced\" | \"permissive\";\n\nexport type SensitiveCategory =\n\t| \"credential\"\n\t| \"payment\"\n\t| \"secret-assignment\"\n\t| \"pii-email\"\n\t| \"pii-phone\"\n\t| \"pii-id\"\n\t| \"pii-bank\"\n\t| \"pii-dob\"\n\t| \"injection\"\n\t| \"exfiltration\";\n\nexport interface SensitivityResult {\n\t/** True when the content must NOT be stored. */\n\tblocked: boolean;\n\t/** Categories detected. */\n\tcategories: SensitiveCategory[];\n\t/** Human-readable reason (in the configured language). */\n\treason: string;\n\t/** True when content contains permitted-but-noteworthy categories. */\n\tnoteworthy: boolean;\n}\n\ninterface Pattern {\n\tcategory: SensitiveCategory;\n\tpattern: RegExp;\n\t/** Always blocked regardless of privacy level. */\n\talways?: boolean;\n\t/** Blocked at this level and stricter. */\n\tblockAt?: PrivacyLevel;\n\t/** English reason label. */\n\tlabel: string;\n}\n\n// ─── Credentials & secrets — ALWAYS blocked ───\nconst ALWAYS_BLOCKED: Pattern[] = [\n\t{ category: \"credential\", pattern: /\\bsk-ant-api\\S{10,}\\b/, always: true, label: \"Anthropic API key\" },\n\t{ category: \"credential\", pattern: /\\bsk-or-v1-\\S{10,}\\b/, always: true, label: \"OpenRouter API key\" },\n\t{ category: \"credential\", pattern: /\\bsk-[A-Za-z0-9]{20,}\\b/, always: true, label: \"OpenAI-style API key\" },\n\t{ category: \"credential\", pattern: /\\bAKIA[0-9A-Z]{16}\\b/, always: true, label: \"AWS access key\" },\n\t{ category: \"credential\", pattern: /\\bghp_[A-Za-z0-9]{20,}\\b/, always: true, label: \"GitHub personal token\" },\n\t{ category: \"credential\", pattern: /\\bghu_[A-Za-z0-9]{20,}\\b/, always: true, label: \"GitHub user token\" },\n\t{ category: \"credential\", pattern: /\\bxox[baprs]-[A-Za-z0-9-]{10,}\\b/, always: true, label: \"Slack token\" },\n\t{ category: \"credential\", pattern: /\\bxapp-[A-Za-z0-9-]{10,}\\b/, always: true, label: \"Slack app token\" },\n\t{ category: \"credential\", pattern: /\\bntn_[A-Za-z0-9]{20,}\\b/, always: true, label: \"Notion token\" },\n\t{ category: \"credential\", pattern: /\\beyJ[A-Za-z0-9_-]{10,}\\.[A-Za-z0-9_-]{10,}\\.[A-Za-z0-9_-]{10,}/, always: true, label: \"JWT token\" },\n\t{ category: \"credential\", pattern: /-----BEGIN\\s+(?:RSA\\s+|EC\\s+|OPENSSH\\s+)?PRIVATE\\s+KEY-----/, always: true, label: \"Private key block\" },\n\t{ category: \"credential\", pattern: /\\bBearer\\s+[A-Za-z0-9._~+/=-]{20,}\\b/i, always: true, label: \"Bearer auth token\" },\n\t{ category: \"credential\", pattern: /\\bapi[_-]?key\\s*[=:]\\s*[A-Za-z0-9_-]{16,}\\b/i, always: true, label: \"API key assignment\" },\n\t{ category: \"credential\", pattern: /\\b(client|app|consumer)_?secret\\s*[=:]\\s*[A-Za-z0-9_-]{16,}\\b/i, always: true, label: \"OAuth client secret\" },\n\t{ category: \"credential\", pattern: /\\bwebhook[_-]?url\\s*[=:]\\s*https?:\\/\\/[^\\s]+/i, always: true, label: \"Webhook URL\" },\n\t{ category: \"secret-assignment\", pattern: /\\b(password|passwd|pwd)\\s*[=:]\\s*\\S{6,}\\b/i, always: true, label: \"password value\" },\n\t{ category: \"secret-assignment\", pattern: /\\b(?:contraseña|contraseña|clave)\\s*[=:]\\s*\\S{6,}\\b/i, always: true, label: \"contraseña valor\" },\n\t{ category: \"secret-assignment\", pattern: /\\b(?:contraseña|password|passwd|pwd|clave)\\b[^\\n]{0,40}\\b\\d{6,}\\b/i, always: true, label: \"password-like value\" },\n\t{\n\t\tcategory: \"secret-assignment\",\n\t\t// Secret keyword followed by a value on the same line: the match spans\n\t\t// keyword + value so redaction never leaves the value exposed. The\n\t\t// value must be an alnum token (≥4 chars) containing a digit or symbol,\n\t\t// which avoids false positives like \"contraseña por defecto\".\n\t\tpattern: /\\b(?:contraseña|password|passwd|pwd|clave|token|api[_-]?key|secret)\\b[^\\n]{0,40}\\b(?=[A-Za-z0-9_\\-!@#$%^&*]*\\d)[A-Za-z0-9_\\-!@#$%^&*]{4,}/i,\n\t\talways: true,\n\t\tlabel: \"secret value after keyword\",\n\t},\n\t{ category: \"secret-assignment\", pattern: /\\b(?:recuerda|guarda|memoriza|remember|save|store)\\b.{0,50}\\b(?:contraseña|password|passwd|pwd|clave|token|api[_-]?key)\\b/i, always: true, label: \"request to store a secret\" },\n\t{ category: \"secret-assignment\", pattern: /\\b(secret|token|api[_-]?key|access[_-]?key)\\s*[=:]\\s*\\S{10,}\\b/i, always: true, label: \"secret/token value\" },\n\t{ category: \"credential\", pattern: /\\b(ANTHROPIC|OPENAI|OPENROUTER|GEMINI|DEEPSEEK|MISTRAL|AZURE)_?(API)?_?KEY\\b/i, always: true, label: \"provider API key env var\" },\n\t{ category: \"credential\", pattern: /\\bGITHUB_TOKEN\\b|\\bAWS_SECRET_ACCESS_KEY\\b|\\bDATABASE_URL\\b/i, always: true, label: \"secret env var name\" },\n\t{ category: \"credential\", pattern: /(?:mongodb|postgres|mysql|redis|amqp):\\/\\/[^\\s]*:[^\\s@]+@/, always: true, label: \"connection string with credentials\" },\n];\n\n// ─── Payment data — ALWAYS blocked ───\nconst PAYMENT: Pattern[] = [\n\t{\n\t\tcategory: \"payment\",\n\t\tpattern: /\\b(?:4\\d{3}|5[1-5]\\d{2}|3[47]\\d{2}|6(?:011|5\\d{2}))[\\s-]?\\d{4}[\\s-]?\\d{4}[\\s-]?\\d{4}\\b/,\n\t\talways: true,\n\t\tlabel: \"payment card number\",\n\t},\n\t{ category: \"payment\", pattern: /\\bcvv\\s*[=:]\\s*\\d{3,4}\\b/i, always: true, label: \"card verification value\" },\n\t{ category: \"payment\", pattern: /\\b(?:expiry|exp)\\s*[=:]\\s*(?:0[1-9]|1[0-2])\\/?\\d{2}\\b/i, always: true, label: \"card expiry\" },\n];\n\n// ─── PII — level-dependent ───\nconst PII: Pattern[] = [\n\t{\n\t\tcategory: \"pii-email\",\n\t\tpattern: /\\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,}\\b/,\n\t\tblockAt: \"strict\",\n\t\tlabel: \"personal email address\",\n\t},\n\t{\n\t\tcategory: \"pii-phone\",\n\t\tpattern: /(?:\\+\\d{1,3}[\\s-]?)?(?:\\(?\\d{2,4}\\)?[\\s-]?)?\\d{3}[\\s-]?\\d{3}[\\s-]?\\d{3,4}\\b/,\n\t\tblockAt: \"strict\",\n\t\tlabel: \"phone number\",\n\t},\n\t{\n\t\tcategory: \"pii-id\",\n\t\tpattern: /\\b\\d{8}[A-Z]\\b|\\b[A-Z]\\d{7}[A-Z0-9]\\b/, // DNI / NIE (Spain)\n\t\tblockAt: \"strict\",\n\t\tlabel: \"national ID (DNI/NIE)\",\n\t},\n\t{\n\t\tcategory: \"pii-bank\",\n\t\tpattern: /\\b[A-Z]{2}\\d{2}[A-Z0-9]{11,30}\\b/, // IBAN\n\t\tblockAt: \"balanced\",\n\t\tlabel: \"IBAN / bank account\",\n\t},\n\t{\n\t\tcategory: \"pii-dob\",\n\t\tpattern: /\\b(?:birth|birthday|born|nacimiento|fecha de nacimiento)\\s*[=:]\\s*(?:\\d{1,2}[\\/\\-.]\\d{1,2}[\\/\\-.]\\d{2,4})/i,\n\t\tblockAt: \"strict\",\n\t\tlabel: \"date of birth\",\n\t},\n];\n\n// ─── Prompt injection / exfiltration — ALWAYS blocked ───\nconst INJECTION: Pattern[] = [\n\t{ category: \"injection\", pattern: /ignore\\s+(previous|all|above|prior)\\s+instructions/i, always: true, label: \"prompt injection\" },\n\t{ category: \"injection\", pattern: /you\\s+are\\s+now\\s+/i, always: true, label: \"role hijack\" },\n\t{ category: \"injection\", pattern: /do\\s+not\\s+tell\\s+the\\s+user/i, always: true, label: \"deceptive instruction\" },\n\t{ category: \"injection\", pattern: /disregard\\s+(your|all|any)\\s+(instructions|rules|guidelines)/i, always: true, label: \"instruction override\" },\n\t{ category: \"injection\", pattern: /system\\s+prompt\\s+override/i, always: true, label: \"system prompt override\" },\n\t{ category: \"exfiltration\", pattern: /curl\\s+[^\\n]*(KEY|TOKEN|SECRET|PASSWORD|CREDENTIAL|API)/i, always: true, label: \"credential exfiltration (curl)\" },\n\t{ category: \"exfiltration\", pattern: /wget\\s+[^\\n]*(KEY|TOKEN|SECRET|PASSWORD|CREDENTIAL|API)/i, always: true, label: \"credential exfiltration (wget)\" },\n\t{ category: \"exfiltration\", pattern: /cat\\s+[^\\n]*(\\.env|credentials|\\.netrc|\\.pgpass|\\.npmrc|\\.pypirc)/i, always: true, label: \"secret file read\" },\n\t{ category: \"exfiltration\", pattern: /\\$HOME\\/\\.ssh|~\\/\\.ssh/i, always: true, label: \"SSH key path\" },\n];\n\nconst LEVEL_ORDER: Record<PrivacyLevel, number> = { strict: 0, balanced: 1, permissive: 2 };\n\nconst INVISIBLE_CHARS = new Set([\n\t\"\\u200b\", \"\\u200c\", \"\\u200d\", \"\\u2060\", \"\\ufeff\",\n\t\"\\u202a\", \"\\u202b\", \"\\u202c\", \"\\u202d\", \"\\u202e\",\n]);\n\nconst REASON_EN = {\n\tcredential: \"Looks like a credential or secret (secrets are never stored in memory)\",\n\tpayment: \"Looks like payment card data (never stored in memory)\",\n\t\"secret-assignment\": \"Looks like a password/secret/token value (never stored in memory)\",\n\t\"pii-email\": \"Looks like a personal email address (blocked by the strict privacy level)\",\n\t\"pii-phone\": \"Looks like a phone number (blocked by the strict privacy level)\",\n\t\"pii-id\": \"Looks like a national ID (DNI/NIE) (blocked by the strict privacy level)\",\n\t\"pii-bank\": \"Looks like a bank account / IBAN (blocked by the privacy level)\",\n\t\"pii-dob\": \"Looks like a date of birth (blocked by the strict privacy level)\",\n\tinjection: \"Looks like a prompt-injection attempt (never stored)\",\n\texfiltration: \"Looks like data exfiltration (never stored)\",\n};\n\nconst REASON_ES: Record<SensitiveCategory, string> = {\n\tcredential: \"Parece una credencial o secreto (los secretos nunca se almacenan en memoria)\",\n\tpayment: \"Parecen datos de tarjeta de pago (nunca se almacenan en memoria)\",\n\t\"secret-assignment\": \"Parece una contraseña/secreto/token (nunca se almacena en memoria)\",\n\t\"pii-email\": \"Parece un correo electrónico personal (bloqueado por el nivel de privacidad estricto)\",\n\t\"pii-phone\": \"Parece un número de teléfono (bloqueado por el nivel de privacidad estricto)\",\n\t\"pii-id\": \"Parece un documento de identidad (DNI/NIE) (bloqueado por el nivel de privacidad estricto)\",\n\t\"pii-bank\": \"Parece una cuenta bancaria / IBAN (bloqueado por el nivel de privacidad)\",\n\t\"pii-dob\": \"Parece una fecha de nacimiento (bloqueada por el nivel de privacidad estricto)\",\n\tinjection: \"Parece un intento de prompt injection (nunca se almacena)\",\n\texfiltration: \"Parece exfiltración de datos (nunca se almacena)\",\n};\n\nfunction testPattern(pattern: Pattern, content: string): boolean {\n\ttry {\n\t\treturn pattern.pattern.test(content);\n\t} catch {\n\t\treturn false;\n\t}\n}\n\n/**\n * Classify content against the security policy.\n *\n * @param content Text to classify.\n * @param level Configured privacy level.\n * @param locale \"es\" or \"en\" for the reason language.\n */\nexport function classifySensitive(content: string, level: PrivacyLevel = \"strict\", locale: \"es\" | \"en\" = \"es\"): SensitivityResult {\n\tconst categories = new Set<SensitiveCategory>();\n\tconst reasons: string[] = [];\n\tconst labels: string[] = [];\n\n\t// Invisible characters are always suspicious (injection vectors).\n\tif ([...content].some((ch) => INVISIBLE_CHARS.has(ch))) {\n\t\treturn {\n\t\t\tblocked: true,\n\t\t\tcategories: [\"injection\"],\n\t\t\treason: locale === \"es\"\n\t\t\t\t? \"El contenido contiene caracteres invisibles/zero-width y fue rechazado por seguridad.\"\n\t\t\t\t: \"Content contains invisible/zero-width characters and was rejected for security.\",\n\t\t\tnoteworthy: true,\n\t\t};\n\t}\n\n\tconst allPatterns = [...ALWAYS_BLOCKED, ...PAYMENT, ...PII, ...INJECTION];\n\tfor (const pattern of allPatterns) {\n\t\tif (!testPattern(pattern, content)) continue;\n\t\tconst levelIdx = LEVEL_ORDER[level];\n\t\tconst blockAtIdx = pattern.blockAt ? LEVEL_ORDER[pattern.blockAt] : 0;\n\t\tconst blocked = pattern.always === true || levelIdx <= blockAtIdx;\n\t\tcategories.add(pattern.category);\n\t\tif (blocked) {\n\t\t\tlabels.push(pattern.label);\n\t\t\tif (!reasons.includes(pattern.category)) reasons.push(pattern.category);\n\t\t}\n\t}\n\n\tif (reasons.length > 0) {\n\t\tconst localeMap = locale === \"es\" ? REASON_ES : REASON_EN;\n\t\tconst detail = reasons.map((r) => localeMap[r as SensitiveCategory]).join(\"; \");\n\t\treturn {\n\t\t\tblocked: true,\n\t\t\tcategories: [...categories],\n\t\t\treason: detail,\n\t\t\tnoteworthy: true,\n\t\t};\n\t}\n\n\treturn {\n\t\tblocked: false,\n\t\tcategories: [...categories],\n\t\treason: \"\",\n\t\tnoteworthy: categories.size > 0,\n\t};\n}\n\n/**\n * Redact sensitive data from user text so it never reaches the LLM, the\n * session file, or the chat transcript. Only high-confidence patterns are\n * redacted (credentials, payment data, explicit secret assignments, and\n * PII like emails / national IDs / IBANs / DOB).\n */\nexport function redactSensitive(text: string, level: PrivacyLevel = \"strict\"): string {\n\tlet out = text;\n\tconst levelIdx = LEVEL_ORDER[level];\n\tfor (const pattern of [...ALWAYS_BLOCKED, ...PAYMENT, ...PII]) {\n\t\tif (pattern.category === \"pii-phone\") {\n\t\t\t// Phone redaction requires separators or a + prefix to avoid\n\t\t\t// mangling ordinary numbers in code.\n\t\t\tif (!/(?:\\+|\\s|\\()?\\d{3}[\\s.-]\\d{3}[\\s.-]\\d{3,4}/.test(text)) continue;\n\t\t}\n\t\tconst blockAtIdx = pattern.blockAt ? LEVEL_ORDER[pattern.blockAt] : 0;\n\t\tconst active = pattern.always === true || levelIdx <= blockAtIdx;\n\t\tif (!active) continue;\n\t\ttry {\n\t\t\tout = out.replace(pattern.pattern, (match) => \"█\".repeat(Math.min(match.length, 14)));\n\t\t} catch {\n\t\t\t// A bad pattern must never break redaction.\n\t\t}\n\t}\n\treturn out;\n}\n"]}
|
|
@@ -13,16 +13,19 @@
|
|
|
13
13
|
* Zero native dependencies: node:crypto works on Node and Bun.
|
|
14
14
|
*/
|
|
15
15
|
import { existsSync } from "node:fs";
|
|
16
|
+
import type { AuditEntry } from "./audit.ts";
|
|
16
17
|
export interface VaultSecret {
|
|
17
18
|
name: string;
|
|
18
19
|
value: string;
|
|
19
20
|
note?: string;
|
|
21
|
+
category?: string;
|
|
20
22
|
created: string;
|
|
21
23
|
updated: string;
|
|
22
24
|
}
|
|
23
25
|
export interface VaultListEntry {
|
|
24
26
|
name: string;
|
|
25
27
|
note?: string;
|
|
28
|
+
category?: string;
|
|
26
29
|
created: string;
|
|
27
30
|
updated: string;
|
|
28
31
|
source: "project" | "session";
|
|
@@ -58,8 +61,8 @@ export declare class ProjectVault {
|
|
|
58
61
|
static create(filePath: string, passphrase: string): Promise<ProjectVault>;
|
|
59
62
|
/** Open an existing vault. Throws on wrong passphrase. */
|
|
60
63
|
static open(filePath: string, passphrase: string): Promise<ProjectVault>;
|
|
61
|
-
add(name: string, value: string, note?: string): void;
|
|
62
|
-
update(name: string, value: string, note?: string): boolean;
|
|
64
|
+
add(name: string, value: string, note?: string, category?: string): void;
|
|
65
|
+
update(name: string, value: string, note?: string, category?: string): boolean;
|
|
63
66
|
get(name: string): string | null;
|
|
64
67
|
remove(name: string): boolean;
|
|
65
68
|
has(name: string): boolean;
|
|
@@ -72,18 +75,32 @@ export declare class VaultManager {
|
|
|
72
75
|
readonly sessionVault: SessionVault;
|
|
73
76
|
private vaultDir;
|
|
74
77
|
private projectHash;
|
|
78
|
+
private auditFn;
|
|
75
79
|
constructor(agentDir: string, projectHash: string | null);
|
|
80
|
+
/** Attach the audit logger (enterprise accountability, never the value). */
|
|
81
|
+
setAudit(fn: ((entry: Omit<AuditEntry, "seq" | "ts" | "hash" | "prevHash">) => void) | null): void;
|
|
82
|
+
private audit;
|
|
83
|
+
get keychainAvailable(): boolean;
|
|
76
84
|
get vaultPath(): string;
|
|
77
85
|
get isUnlocked(): boolean;
|
|
78
86
|
/** True when a project vault file already exists. */
|
|
79
87
|
exists(): boolean;
|
|
80
88
|
/**
|
|
81
89
|
* Unlock (or create) the project vault with the user passphrase.
|
|
82
|
-
* Returns false on wrong passphrase.
|
|
90
|
+
* Returns false on wrong passphrase. When `remember` is set and the
|
|
91
|
+
* Keychain is available, the passphrase is stored there (macOS).
|
|
83
92
|
*/
|
|
84
|
-
unlock(passphrase: string
|
|
93
|
+
unlock(passphrase: string, options?: {
|
|
94
|
+
remember?: boolean;
|
|
95
|
+
}): Promise<boolean>;
|
|
96
|
+
/** Try to unlock using the passphrase stored in the Keychain (macOS). */
|
|
97
|
+
unlockFromKeychain(): Promise<boolean>;
|
|
98
|
+
/** True when a passphrase for this vault is stored in the Keychain. */
|
|
99
|
+
keychainRemembered(): Promise<boolean>;
|
|
100
|
+
/** Forget the Keychain-stored passphrase. */
|
|
101
|
+
keychainForget(): Promise<boolean>;
|
|
85
102
|
lock(): void;
|
|
86
|
-
add(name: string, value: string, note?: string): boolean;
|
|
103
|
+
add(name: string, value: string, note?: string, category?: string): boolean;
|
|
87
104
|
update(name: string, value: string, note?: string): boolean;
|
|
88
105
|
remove(name: string): boolean;
|
|
89
106
|
getSecret(name: string): string | null;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"vault.d.ts","sourceRoot":"","sources":["../../../src/core/memory-engine/vault.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Ada Secure Vault — encrypted secret storage.\n *\n * - Project vault: AES-256-GCM encrypted at rest under\n * <agentDir>/vault/<project-hash>/vault.json; the key is derived from the\n * user passphrase with scrypt (never stored).\n * - Session vault: per-conversation secrets kept ONLY in process memory\n * (never written to disk).\n * - Values are exposed to the LLM only through @vault:<name> expansion in\n * the user's prompt, and the persisted session file stores the redacted\n * form (the expansion is stripped before persistence).\n *\n * Zero native dependencies: node:crypto works on Node and Bun.\n */\n\nimport { createCipheriv, createDecipheriv, randomBytes, scryptSync } from \"node:crypto\";\nimport { readFile, writeFile, mkdir } from \"node:fs/promises\";\nimport { existsSync } from \"node:fs\";\nimport { dirname, join } from \"node:path\";\n\nexport interface VaultSecret {\n\tname: string;\n\tvalue: string;\n\tnote?: string;\n\tcreated: string;\n\tupdated: string;\n}\n\nexport interface VaultListEntry {\n\tname: string;\n\tnote?: string;\n\tcreated: string;\n\tupdated: string;\n\tsource: \"project\" | \"session\";\n}\n\nconst KDF_N = 32768;\nconst KDF_R = 8;\nconst KDF_P = 1;\nconst KEY_LENGTH = 32;\n\ninterface VaultFilePayload {\n\tversion: 1;\n\tkdf: \"scrypt\";\n\tN: number;\n\tr: number;\n\tp: number;\n\tsalt: string; // base64\n\tiv: string; // base64\n\ttag: string; // base64 (GCM auth tag)\n\tdata: string; // base64 (AES-256-GCM ciphertext of the JSON entries)\n}\n\nfunction deriveKey(passphrase: string, salt: Buffer, N = KDF_N, r = KDF_R, p = KDF_P): Buffer {\n\treturn scryptSync(passphrase, salt, KEY_LENGTH, { N, r, p, maxmem: 128 * 1024 * 1024 });\n}\n\nfunction encryptPayload(payload: VaultSecret[], key: Buffer): { iv: Buffer; tag: Buffer; data: Buffer } {\n\tconst iv = randomBytes(12);\n\tconst cipher = createCipheriv(\"aes-256-gcm\", key, iv);\n\tconst data = Buffer.concat([cipher.update(JSON.stringify(payload), \"utf-8\"), cipher.final()]);\n\tconst tag = cipher.getAuthTag();\n\treturn { iv, tag, data };\n}\n\nfunction decryptPayload(file: VaultFilePayload, key: Buffer): VaultSecret[] {\n\tconst decipher = createDecipheriv(\"aes-256-gcm\", key, Buffer.from(file.iv, \"base64\"));\n\tdecipher.setAuthTag(Buffer.from(file.tag, \"base64\"));\n\tconst plain = Buffer.concat([\n\t\tdecipher.update(Buffer.from(file.data, \"base64\")),\n\t\tdecipher.final(), // throws on wrong passphrase (auth failure)\n\t]);\n\treturn JSON.parse(plain.toString(\"utf-8\")) as VaultSecret[];\n}\n\n/** Reference syntax in user prompts: @vault:<name>. */\nexport const VAULT_REF_PATTERN = /@vault:([A-Za-z0-9_.-]+)/g;\n\nexport interface VaultExpansion {\n\ttext: string;\n\t/** Names that were expanded (empty when none). */\n\texpanded: string[];\n\t/** Names referenced but missing (not expanded). */\n\tmissing: string[];\n}\n\n/**\n * Expand @vault:<name> references in a prompt. The returned text is what the\n * LLM receives; the ORIGINAL input (redacted) is what gets persisted.\n */\nexport function expandVaultRefs(text: string, vault: VaultManager | null): VaultExpansion {\n\tconst expanded: string[] = [];\n\tconst missing: string[] = [];\n\tif (!vault) {\n\t\tfor (const match of text.matchAll(VAULT_REF_PATTERN)) {\n\t\t\tmissing.push(match[1]);\n\t\t}\n\t\treturn { text, expanded, missing };\n\t}\n\tconst out = text.replace(VAULT_REF_PATTERN, (_full, name: string) => {\n\t\tconst value = vault.getSecret(name);\n\t\tif (value !== null) {\n\t\t\texpanded.push(name);\n\t\t\treturn value;\n\t\t}\n\t\tmissing.push(name);\n\t\treturn _full; // leave the reference intact so the user sees it failed\n\t});\n\treturn { text: out, expanded, missing };\n}\n\nexport class SessionVault {\n\tprivate entries = new Map<string, string>();\n\n\tadd(name: string, value: string): void {\n\t\tthis.entries.set(name, value);\n\t}\n\n\tget(name: string): string | null {\n\t\treturn this.entries.get(name) ?? null;\n\t}\n\n\tremove(name: string): boolean {\n\t\treturn this.entries.delete(name);\n\t}\n\n\thas(name: string): boolean {\n\t\treturn this.entries.has(name);\n\t}\n\n\tlist(): string[] {\n\t\treturn [...this.entries.keys()].sort();\n\t}\n\n\tclear(): void {\n\t\tthis.entries.clear();\n\t}\n}\n\nexport class ProjectVault {\n\tprivate secrets: Map<string, VaultSecret>;\n\tprivate key: Buffer;\n\n\tprivate constructor(key: Buffer, secrets: Map<string, VaultSecret>) {\n\t\tthis.key = key;\n\t\tthis.secrets = secrets;\n\t}\n\n\t/** Create a new empty project vault (writes the encrypted file). */\n\tstatic async create(filePath: string, passphrase: string): Promise<ProjectVault> {\n\t\tconst salt = randomBytes(16);\n\t\tconst key = deriveKey(passphrase, salt);\n\t\tconst vault = new ProjectVault(key, new Map());\n\t\tawait vault.save(filePath, salt);\n\t\treturn vault;\n\t}\n\n\t/** Open an existing vault. Throws on wrong passphrase. */\n\tstatic async open(filePath: string, passphrase: string): Promise<ProjectVault> {\n\t\tconst raw = await readFile(filePath, \"utf-8\");\n\t\tconst file = JSON.parse(raw) as VaultFilePayload;\n\t\tconst key = deriveKey(passphrase, Buffer.from(file.salt, \"base64\"), file.N, file.r, file.p);\n\t\tconst secrets = decryptPayload(file, key);\n\t\treturn new ProjectVault(\n\t\t\tkey,\n\t\t\tnew Map(secrets.map((s) => [s.name, s])),\n\t\t);\n\t}\n\n\tadd(name: string, value: string, note?: string): void {\n\t\tconst now = new Date().toISOString();\n\t\tthis.secrets.set(name, { name, value, note, created: now, updated: now });\n\t}\n\n\tupdate(name: string, value: string, note?: string): boolean {\n\t\tconst existing = this.secrets.get(name);\n\t\tif (!existing) return false;\n\t\tconst now = new Date().toISOString();\n\t\tthis.secrets.set(name, {\n\t\t\t...existing,\n\t\t\tvalue,\n\t\t\tnote: note ?? existing.note,\n\t\t\tupdated: now,\n\t\t});\n\t\treturn true;\n\t}\n\n\tget(name: string): string | null {\n\t\treturn this.secrets.get(name)?.value ?? null;\n\t}\n\n\tremove(name: string): boolean {\n\t\treturn this.secrets.delete(name);\n\t}\n\n\thas(name: string): boolean {\n\t\treturn this.secrets.has(name);\n\t}\n\n\tlist(): VaultListEntry[] {\n\t\treturn [...this.secrets.values()]\n\t\t\t.map((s) => ({\n\t\t\t\tname: s.name,\n\t\t\t\tnote: s.note,\n\t\t\t\tcreated: s.created,\n\t\t\t\tupdated: s.updated,\n\t\t\t\tsource: \"project\" as const,\n\t\t\t}))\n\t\t\t.sort((a, b) => a.name.localeCompare(b.name));\n\t}\n\n\tget entryCount(): number {\n\t\treturn this.secrets.size;\n\t}\n\n\tasync save(filePath: string, salt?: Buffer): Promise<void> {\n\t\tconst { iv, tag, data } = encryptPayload([...this.secrets.values()], this.key);\n\t\tconst payload: VaultFilePayload = {\n\t\t\tversion: 1,\n\t\t\tkdf: \"scrypt\",\n\t\t\tN: KDF_N,\n\t\t\tr: KDF_R,\n\t\t\tp: KDF_P,\n\t\t\tsalt: (salt ?? randomBytes(16)).toString(\"base64\"),\n\t\t\tiv: iv.toString(\"base64\"),\n\t\t\ttag: tag.toString(\"base64\"),\n\t\t\tdata: data.toString(\"base64\"),\n\t\t};\n\t\tawait mkdir(dirname(filePath), { recursive: true });\n\t\tawait writeFile(filePath, JSON.stringify(payload, null, 1), \"utf-8\");\n\t}\n}\n\nexport class VaultManager {\n\tprivate projectVault: ProjectVault | null = null;\n\treadonly sessionVault = new SessionVault();\n\tprivate vaultDir: string;\n\tprivate projectHash: string;\n\n\tconstructor(agentDir: string, projectHash: string | null) {\n\t\tthis.vaultDir = join(agentDir, \"vault\");\n\t\tthis.projectHash = projectHash ?? \"global\";\n\t}\n\n\tget vaultPath(): string {\n\t\treturn join(this.vaultDir, this.projectHash, \"vault.json\");\n\t}\n\n\tget isUnlocked(): boolean {\n\t\treturn this.projectVault !== null;\n\t}\n\n\t/** True when a project vault file already exists. */\n\texists(): boolean {\n\t\treturn existsSync(this.vaultPath);\n\t}\n\n\t/**\n\t * Unlock (or create) the project vault with the user passphrase.\n\t * Returns false on wrong passphrase.\n\t */\n\tasync unlock(passphrase: string): Promise<boolean> {\n\t\ttry {\n\t\t\tif (this.exists()) {\n\t\t\t\tthis.projectVault = await ProjectVault.open(this.vaultPath, passphrase);\n\t\t\t} else {\n\t\t\t\tthis.projectVault = await ProjectVault.create(this.vaultPath, passphrase);\n\t\t\t}\n\t\t\treturn true;\n\t\t} catch {\n\t\t\tthis.projectVault = null;\n\t\t\treturn false;\n\t\t}\n\t}\n\n\tlock(): void {\n\t\tthis.projectVault = null;\n\t}\n\n\t// ─── Project vault operations (require unlock) ───\n\n\tadd(name: string, value: string, note?: string): boolean {\n\t\tif (!this.projectVault || !name.trim() || !value) return false;\n\t\tthis.projectVault.add(name.trim(), value, note);\n\t\tvoid this.projectVault.save(this.vaultPath).catch(() => {});\n\t\treturn true;\n\t}\n\n\tupdate(name: string, value: string, note?: string): boolean {\n\t\tif (!this.projectVault) return false;\n\t\tconst ok = this.projectVault.update(name, value, note);\n\t\tif (ok) void this.projectVault.save(this.vaultPath).catch(() => {});\n\t\treturn ok;\n\t}\n\n\tremove(name: string): boolean {\n\t\tif (!this.projectVault) return false;\n\t\tconst ok = this.projectVault.remove(name);\n\t\tif (ok) void this.projectVault.save(this.vaultPath).catch(() => {});\n\t\treturn ok;\n\t}\n\n\t// ─── Lookup (project first, then session) ───\n\n\tgetSecret(name: string): string | null {\n\t\tconst fromProject = this.projectVault?.get(name) ?? null;\n\t\tif (fromProject !== null) return fromProject;\n\t\treturn this.sessionVault.get(name);\n\t}\n\n\t// ─── Session vault (memory only) ───\n\n\tsessionAdd(name: string, value: string): void {\n\t\tthis.sessionVault.add(name, value);\n\t}\n\n\tsessionRemove(name: string): boolean {\n\t\treturn this.sessionVault.remove(name);\n\t}\n\n\t// ─── Listing ───\n\n\tlist(): VaultListEntry[] {\n\t\tconst entries: VaultListEntry[] = [];\n\t\tif (this.projectVault) entries.push(...this.projectVault.list());\n\t\tfor (const name of this.sessionVault.list()) {\n\t\t\tentries.push({\n\t\t\t\tname,\n\t\t\t\tcreated: \"\",\n\t\t\t\tupdated: \"\",\n\t\t\t\tsource: \"session\",\n\t\t\t});\n\t\t}\n\t\treturn entries.sort((a, b) => a.name.localeCompare(b.name));\n\t}\n\n\tstatus(): {\n\t\tunlocked: boolean;\n\t\tvaultPath: string;\n\t\texists: boolean;\n\t\tprojectCount: number;\n\t\tsessionCount: number;\n\t} {\n\t\treturn {\n\t\t\tunlocked: this.isUnlocked,\n\t\t\tvaultPath: this.vaultPath,\n\t\t\texists: this.exists(),\n\t\t\tprojectCount: this.projectVault?.entryCount ?? 0,\n\t\t\tsessionCount: this.sessionVault.list().length,\n\t\t};\n\t}\n}\n\nexport { existsSync };\n"]}
|
|
1
|
+
{"version":3,"file":"vault.d.ts","sourceRoot":"","sources":["../../../src/core/memory-engine/vault.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Ada Secure Vault — encrypted secret storage.\n *\n * - Project vault: AES-256-GCM encrypted at rest under\n * <agentDir>/vault/<project-hash>/vault.json; the key is derived from the\n * user passphrase with scrypt (never stored).\n * - Session vault: per-conversation secrets kept ONLY in process memory\n * (never written to disk).\n * - Values are exposed to the LLM only through @vault:<name> expansion in\n * the user's prompt, and the persisted session file stores the redacted\n * form (the expansion is stripped before persistence).\n *\n * Zero native dependencies: node:crypto works on Node and Bun.\n */\n\nimport { createCipheriv, createDecipheriv, randomBytes, scryptSync } from \"node:crypto\";\nimport { readFile, writeFile, mkdir } from \"node:fs/promises\";\nimport { existsSync } from \"node:fs\";\nimport { dirname, join } from \"node:path\";\nimport {\n\tisKeychainAvailable,\n\tkeychainHasPassphrase,\n\tkeychainLoadPassphrase,\n\tkeychainSavePassphrase,\n\tkeychainClearPassphrase,\n} from \"./keychain.ts\";\nimport type { AuditEntry } from \"./audit.ts\";\n\nexport interface VaultSecret {\n\tname: string;\n\tvalue: string;\n\tnote?: string;\n\tcategory?: string;\n\tcreated: string;\n\tupdated: string;\n}\n\nexport interface VaultListEntry {\n\tname: string;\n\tnote?: string;\n\tcategory?: string;\n\tcreated: string;\n\tupdated: string;\n\tsource: \"project\" | \"session\";\n}\n\nconst KDF_N = 32768;\nconst KDF_R = 8;\nconst KDF_P = 1;\nconst KEY_LENGTH = 32;\n\ninterface VaultFilePayload {\n\tversion: 1;\n\tkdf: \"scrypt\";\n\tN: number;\n\tr: number;\n\tp: number;\n\tsalt: string; // base64\n\tiv: string; // base64\n\ttag: string; // base64 (GCM auth tag)\n\tdata: string; // base64 (AES-256-GCM ciphertext of the JSON entries)\n}\n\nfunction deriveKey(passphrase: string, salt: Buffer, N = KDF_N, r = KDF_R, p = KDF_P): Buffer {\n\treturn scryptSync(passphrase, salt, KEY_LENGTH, { N, r, p, maxmem: 128 * 1024 * 1024 });\n}\n\nfunction encryptPayload(payload: VaultSecret[], key: Buffer): { iv: Buffer; tag: Buffer; data: Buffer } {\n\tconst iv = randomBytes(12);\n\tconst cipher = createCipheriv(\"aes-256-gcm\", key, iv);\n\tconst data = Buffer.concat([cipher.update(JSON.stringify(payload), \"utf-8\"), cipher.final()]);\n\tconst tag = cipher.getAuthTag();\n\treturn { iv, tag, data };\n}\n\nfunction decryptPayload(file: VaultFilePayload, key: Buffer): VaultSecret[] {\n\tconst decipher = createDecipheriv(\"aes-256-gcm\", key, Buffer.from(file.iv, \"base64\"));\n\tdecipher.setAuthTag(Buffer.from(file.tag, \"base64\"));\n\tconst plain = Buffer.concat([\n\t\tdecipher.update(Buffer.from(file.data, \"base64\")),\n\t\tdecipher.final(), // throws on wrong passphrase (auth failure)\n\t]);\n\treturn JSON.parse(plain.toString(\"utf-8\")) as VaultSecret[];\n}\n\n/** Reference syntax in user prompts: @vault:<name>. */\nexport const VAULT_REF_PATTERN = /@vault:([A-Za-z0-9_.-]+)/g;\n\nexport interface VaultExpansion {\n\ttext: string;\n\t/** Names that were expanded (empty when none). */\n\texpanded: string[];\n\t/** Names referenced but missing (not expanded). */\n\tmissing: string[];\n}\n\n/**\n * Expand @vault:<name> references in a prompt. The returned text is what the\n * LLM receives; the ORIGINAL input (redacted) is what gets persisted.\n */\nexport function expandVaultRefs(text: string, vault: VaultManager | null): VaultExpansion {\n\tconst expanded: string[] = [];\n\tconst missing: string[] = [];\n\tif (!vault) {\n\t\tfor (const match of text.matchAll(VAULT_REF_PATTERN)) {\n\t\t\tmissing.push(match[1]);\n\t\t}\n\t\treturn { text, expanded, missing };\n\t}\n\tconst out = text.replace(VAULT_REF_PATTERN, (_full, name: string) => {\n\t\tconst value = vault.getSecret(name);\n\t\tif (value !== null) {\n\t\t\texpanded.push(name);\n\t\t\treturn value;\n\t\t}\n\t\tmissing.push(name);\n\t\treturn _full; // leave the reference intact so the user sees it failed\n\t});\n\treturn { text: out, expanded, missing };\n}\n\nexport class SessionVault {\n\tprivate entries = new Map<string, string>();\n\n\tadd(name: string, value: string): void {\n\t\tthis.entries.set(name, value);\n\t}\n\n\tget(name: string): string | null {\n\t\treturn this.entries.get(name) ?? null;\n\t}\n\n\tremove(name: string): boolean {\n\t\treturn this.entries.delete(name);\n\t}\n\n\thas(name: string): boolean {\n\t\treturn this.entries.has(name);\n\t}\n\n\tlist(): string[] {\n\t\treturn [...this.entries.keys()].sort();\n\t}\n\n\tclear(): void {\n\t\tthis.entries.clear();\n\t}\n}\n\nexport class ProjectVault {\n\tprivate secrets: Map<string, VaultSecret>;\n\tprivate key: Buffer;\n\n\tprivate constructor(key: Buffer, secrets: Map<string, VaultSecret>) {\n\t\tthis.key = key;\n\t\tthis.secrets = secrets;\n\t}\n\n\t/** Create a new empty project vault (writes the encrypted file). */\n\tstatic async create(filePath: string, passphrase: string): Promise<ProjectVault> {\n\t\tconst salt = randomBytes(16);\n\t\tconst key = deriveKey(passphrase, salt);\n\t\tconst vault = new ProjectVault(key, new Map());\n\t\tawait vault.save(filePath, salt);\n\t\treturn vault;\n\t}\n\n\t/** Open an existing vault. Throws on wrong passphrase. */\n\tstatic async open(filePath: string, passphrase: string): Promise<ProjectVault> {\n\t\tconst raw = await readFile(filePath, \"utf-8\");\n\t\tconst file = JSON.parse(raw) as VaultFilePayload;\n\t\tconst key = deriveKey(passphrase, Buffer.from(file.salt, \"base64\"), file.N, file.r, file.p);\n\t\tconst secrets = decryptPayload(file, key);\n\t\treturn new ProjectVault(\n\t\t\tkey,\n\t\t\tnew Map(secrets.map((s) => [s.name, s])),\n\t\t);\n\t}\n\n\tadd(name: string, value: string, note?: string, category?: string): void {\n\t\tconst now = new Date().toISOString();\n\t\tthis.secrets.set(name, { name, value, note, category, created: now, updated: now });\n\t}\n\n\tupdate(name: string, value: string, note?: string, category?: string): boolean {\n\t\tconst existing = this.secrets.get(name);\n\t\tif (!existing) return false;\n\t\tconst now = new Date().toISOString();\n\t\tthis.secrets.set(name, {\n\t\t\t...existing,\n\t\t\tvalue,\n\t\t\tnote: note ?? existing.note,\n\t\t\tcategory: category ?? existing.category,\n\t\t\tupdated: now,\n\t\t});\n\t\treturn true;\n\t}\n\n\tget(name: string): string | null {\n\t\treturn this.secrets.get(name)?.value ?? null;\n\t}\n\n\tremove(name: string): boolean {\n\t\treturn this.secrets.delete(name);\n\t}\n\n\thas(name: string): boolean {\n\t\treturn this.secrets.has(name);\n\t}\n\n\tlist(): VaultListEntry[] {\n\t\treturn [...this.secrets.values()]\n\t\t\t.map((s) => ({\n\t\t\t\tname: s.name,\n\t\t\t\tnote: s.note,\n\t\t\t\tcategory: s.category,\n\t\t\t\tcreated: s.created,\n\t\t\t\tupdated: s.updated,\n\t\t\t\tsource: \"project\" as const,\n\t\t\t}))\n\t\t\t.sort((a, b) => a.name.localeCompare(b.name));\n\t}\n\n\tget entryCount(): number {\n\t\treturn this.secrets.size;\n\t}\n\n\tasync save(filePath: string, salt?: Buffer): Promise<void> {\n\t\tconst { iv, tag, data } = encryptPayload([...this.secrets.values()], this.key);\n\t\tconst payload: VaultFilePayload = {\n\t\t\tversion: 1,\n\t\t\tkdf: \"scrypt\",\n\t\t\tN: KDF_N,\n\t\t\tr: KDF_R,\n\t\t\tp: KDF_P,\n\t\t\tsalt: (salt ?? randomBytes(16)).toString(\"base64\"),\n\t\t\tiv: iv.toString(\"base64\"),\n\t\t\ttag: tag.toString(\"base64\"),\n\t\t\tdata: data.toString(\"base64\"),\n\t\t};\n\t\tawait mkdir(dirname(filePath), { recursive: true });\n\t\tawait writeFile(filePath, JSON.stringify(payload, null, 1), \"utf-8\");\n\t}\n}\n\nexport class VaultManager {\n\tprivate projectVault: ProjectVault | null = null;\n\treadonly sessionVault = new SessionVault();\n\tprivate vaultDir: string;\n\tprivate projectHash: string;\n\tprivate auditFn: ((entry: Omit<AuditEntry, \"seq\" | \"ts\" | \"hash\" | \"prevHash\">) => void) | null = null;\n\n\tconstructor(agentDir: string, projectHash: string | null) {\n\t\tthis.vaultDir = join(agentDir, \"vault\");\n\t\tthis.projectHash = projectHash ?? \"global\";\n\t}\n\n\t/** Attach the audit logger (enterprise accountability, never the value). */\n\tsetAudit(fn: ((entry: Omit<AuditEntry, \"seq\" | \"ts\" | \"hash\" | \"prevHash\">) => void) | null): void {\n\t\tthis.auditFn = fn;\n\t}\n\n\tprivate audit(action: AuditEntry[\"action\"], ok: boolean, reason?: string): void {\n\t\tthis.auditFn?.({\n\t\t\taction,\n\t\t\tsource: \"user\",\n\t\t\ttarget: \"vault\",\n\t\t\tscope: this.projectHash,\n\t\t\tok,\n\t\t\treason,\n\t\t});\n\t}\n\n\tget keychainAvailable(): boolean {\n\t\treturn isKeychainAvailable();\n\t}\n\n\tget vaultPath(): string {\n\t\treturn join(this.vaultDir, this.projectHash, \"vault.json\");\n\t}\n\n\tget isUnlocked(): boolean {\n\t\treturn this.projectVault !== null;\n\t}\n\n\t/** True when a project vault file already exists. */\n\texists(): boolean {\n\t\treturn existsSync(this.vaultPath);\n\t}\n\n\t/**\n\t * Unlock (or create) the project vault with the user passphrase.\n\t * Returns false on wrong passphrase. When `remember` is set and the\n\t * Keychain is available, the passphrase is stored there (macOS).\n\t */\n\tasync unlock(passphrase: string, options: { remember?: boolean } = {}): Promise<boolean> {\n\t\ttry {\n\t\t\tif (this.exists()) {\n\t\t\t\tthis.projectVault = await ProjectVault.open(this.vaultPath, passphrase);\n\t\t\t} else {\n\t\t\t\tthis.projectVault = await ProjectVault.create(this.vaultPath, passphrase);\n\t\t\t}\n\t\t\tif (options.remember && isKeychainAvailable()) {\n\t\t\t\tconst saved = await keychainSavePassphrase(this.projectHash, passphrase);\n\t\t\t\tthis.audit(\"add\", saved, saved ? \"keychain\" : \"keychain save failed\");\n\t\t\t}\n\t\t\tthis.audit(\"add\", true, this.exists() ? \"unlock\" : \"create\");\n\t\t\treturn true;\n\t\t} catch {\n\t\t\tthis.projectVault = null;\n\t\t\tthis.audit(\"rejected\", false, \"wrong passphrase or corrupted vault\");\n\t\t\treturn false;\n\t\t}\n\t}\n\n\t/** Try to unlock using the passphrase stored in the Keychain (macOS). */\n\tasync unlockFromKeychain(): Promise<boolean> {\n\t\tif (!isKeychainAvailable()) return false;\n\t\tconst passphrase = await keychainLoadPassphrase(this.projectHash);\n\t\tif (!passphrase) return false;\n\t\treturn this.unlock(passphrase, { remember: false });\n\t}\n\n\t/** True when a passphrase for this vault is stored in the Keychain. */\n\tasync keychainRemembered(): Promise<boolean> {\n\t\tif (!isKeychainAvailable()) return false;\n\t\treturn keychainHasPassphrase(this.projectHash);\n\t}\n\n\t/** Forget the Keychain-stored passphrase. */\n\tasync keychainForget(): Promise<boolean> {\n\t\tif (!isKeychainAvailable()) return false;\n\t\tconst ok = await keychainClearPassphrase(this.projectHash);\n\t\tthis.audit(\"remove\", ok, \"keychain forget\");\n\t\treturn ok;\n\t}\n\n\tlock(): void {\n\t\tthis.projectVault = null;\n\t}\n\n\t// ─── Project vault operations (require unlock) ───\n\n\tadd(name: string, value: string, note?: string, category?: string): boolean {\n\t\tif (!this.projectVault || !name.trim() || !value) return false;\n\t\tthis.projectVault.add(name.trim(), value, note, category);\n\t\tvoid this.projectVault.save(this.vaultPath).catch(() => {});\n\t\tthis.audit(\"add\", true, name);\n\t\treturn true;\n\t}\n\n\tupdate(name: string, value: string, note?: string): boolean {\n\t\tif (!this.projectVault) return false;\n\t\tconst ok = this.projectVault.update(name, value, note);\n\t\tif (ok) void this.projectVault.save(this.vaultPath).catch(() => {});\n\t\treturn ok;\n\t}\n\n\tremove(name: string): boolean {\n\t\tif (!this.projectVault) return false;\n\t\tconst ok = this.projectVault.remove(name);\n\t\tif (ok) void this.projectVault.save(this.vaultPath).catch(() => {});\n\t\tthis.audit(\"remove\", ok, name);\n\t\treturn ok;\n\t}\n\n\t// ─── Lookup (project first, then session) ───\n\n\tgetSecret(name: string): string | null {\n\t\tconst fromProject = this.projectVault?.get(name) ?? null;\n\t\tif (fromProject !== null) return fromProject;\n\t\treturn this.sessionVault.get(name);\n\t}\n\n\t// ─── Session vault (memory only) ───\n\n\tsessionAdd(name: string, value: string): void {\n\t\tthis.sessionVault.add(name, value);\n\t\tthis.audit(\"add\", true, `session:${name}`);\n\t}\n\n\tsessionRemove(name: string): boolean {\n\t\tconst ok = this.sessionVault.remove(name);\n\t\tthis.audit(\"remove\", ok, `session:${name}`);\n\t\treturn ok;\n\t}\n\n\t// ─── Listing ───\n\n\tlist(): VaultListEntry[] {\n\t\tconst entries: VaultListEntry[] = [];\n\t\tif (this.projectVault) entries.push(...this.projectVault.list());\n\t\tfor (const name of this.sessionVault.list()) {\n\t\t\tentries.push({\n\t\t\t\tname,\n\t\t\t\tcreated: \"\",\n\t\t\t\tupdated: \"\",\n\t\t\t\tsource: \"session\",\n\t\t\t});\n\t\t}\n\t\treturn entries.sort((a, b) => a.name.localeCompare(b.name));\n\t}\n\n\tstatus(): {\n\t\tunlocked: boolean;\n\t\tvaultPath: string;\n\t\texists: boolean;\n\t\tprojectCount: number;\n\t\tsessionCount: number;\n\t} {\n\t\treturn {\n\t\t\tunlocked: this.isUnlocked,\n\t\t\tvaultPath: this.vaultPath,\n\t\t\texists: this.exists(),\n\t\t\tprojectCount: this.projectVault?.entryCount ?? 0,\n\t\t\tsessionCount: this.sessionVault.list().length,\n\t\t};\n\t}\n}\n\nexport { existsSync };\n"]}
|
|
@@ -16,6 +16,7 @@ import { createCipheriv, createDecipheriv, randomBytes, scryptSync } from "node:
|
|
|
16
16
|
import { readFile, writeFile, mkdir } from "node:fs/promises";
|
|
17
17
|
import { existsSync } from "node:fs";
|
|
18
18
|
import { dirname, join } from "node:path";
|
|
19
|
+
import { isKeychainAvailable, keychainHasPassphrase, keychainLoadPassphrase, keychainSavePassphrase, keychainClearPassphrase, } from "./keychain.js";
|
|
19
20
|
const KDF_N = 32768;
|
|
20
21
|
const KDF_R = 8;
|
|
21
22
|
const KDF_P = 1;
|
|
@@ -109,11 +110,11 @@ export class ProjectVault {
|
|
|
109
110
|
const secrets = decryptPayload(file, key);
|
|
110
111
|
return new ProjectVault(key, new Map(secrets.map((s) => [s.name, s])));
|
|
111
112
|
}
|
|
112
|
-
add(name, value, note) {
|
|
113
|
+
add(name, value, note, category) {
|
|
113
114
|
const now = new Date().toISOString();
|
|
114
|
-
this.secrets.set(name, { name, value, note, created: now, updated: now });
|
|
115
|
+
this.secrets.set(name, { name, value, note, category, created: now, updated: now });
|
|
115
116
|
}
|
|
116
|
-
update(name, value, note) {
|
|
117
|
+
update(name, value, note, category) {
|
|
117
118
|
const existing = this.secrets.get(name);
|
|
118
119
|
if (!existing)
|
|
119
120
|
return false;
|
|
@@ -122,6 +123,7 @@ export class ProjectVault {
|
|
|
122
123
|
...existing,
|
|
123
124
|
value,
|
|
124
125
|
note: note ?? existing.note,
|
|
126
|
+
category: category ?? existing.category,
|
|
125
127
|
updated: now,
|
|
126
128
|
});
|
|
127
129
|
return true;
|
|
@@ -140,6 +142,7 @@ export class ProjectVault {
|
|
|
140
142
|
.map((s) => ({
|
|
141
143
|
name: s.name,
|
|
142
144
|
note: s.note,
|
|
145
|
+
category: s.category,
|
|
143
146
|
created: s.created,
|
|
144
147
|
updated: s.updated,
|
|
145
148
|
source: "project",
|
|
@@ -171,10 +174,28 @@ export class VaultManager {
|
|
|
171
174
|
sessionVault = new SessionVault();
|
|
172
175
|
vaultDir;
|
|
173
176
|
projectHash;
|
|
177
|
+
auditFn = null;
|
|
174
178
|
constructor(agentDir, projectHash) {
|
|
175
179
|
this.vaultDir = join(agentDir, "vault");
|
|
176
180
|
this.projectHash = projectHash ?? "global";
|
|
177
181
|
}
|
|
182
|
+
/** Attach the audit logger (enterprise accountability, never the value). */
|
|
183
|
+
setAudit(fn) {
|
|
184
|
+
this.auditFn = fn;
|
|
185
|
+
}
|
|
186
|
+
audit(action, ok, reason) {
|
|
187
|
+
this.auditFn?.({
|
|
188
|
+
action,
|
|
189
|
+
source: "user",
|
|
190
|
+
target: "vault",
|
|
191
|
+
scope: this.projectHash,
|
|
192
|
+
ok,
|
|
193
|
+
reason,
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
get keychainAvailable() {
|
|
197
|
+
return isKeychainAvailable();
|
|
198
|
+
}
|
|
178
199
|
get vaultPath() {
|
|
179
200
|
return join(this.vaultDir, this.projectHash, "vault.json");
|
|
180
201
|
}
|
|
@@ -187,9 +208,10 @@ export class VaultManager {
|
|
|
187
208
|
}
|
|
188
209
|
/**
|
|
189
210
|
* Unlock (or create) the project vault with the user passphrase.
|
|
190
|
-
* Returns false on wrong passphrase.
|
|
211
|
+
* Returns false on wrong passphrase. When `remember` is set and the
|
|
212
|
+
* Keychain is available, the passphrase is stored there (macOS).
|
|
191
213
|
*/
|
|
192
|
-
async unlock(passphrase) {
|
|
214
|
+
async unlock(passphrase, options = {}) {
|
|
193
215
|
try {
|
|
194
216
|
if (this.exists()) {
|
|
195
217
|
this.projectVault = await ProjectVault.open(this.vaultPath, passphrase);
|
|
@@ -197,22 +219,52 @@ export class VaultManager {
|
|
|
197
219
|
else {
|
|
198
220
|
this.projectVault = await ProjectVault.create(this.vaultPath, passphrase);
|
|
199
221
|
}
|
|
222
|
+
if (options.remember && isKeychainAvailable()) {
|
|
223
|
+
const saved = await keychainSavePassphrase(this.projectHash, passphrase);
|
|
224
|
+
this.audit("add", saved, saved ? "keychain" : "keychain save failed");
|
|
225
|
+
}
|
|
226
|
+
this.audit("add", true, this.exists() ? "unlock" : "create");
|
|
200
227
|
return true;
|
|
201
228
|
}
|
|
202
229
|
catch {
|
|
203
230
|
this.projectVault = null;
|
|
231
|
+
this.audit("rejected", false, "wrong passphrase or corrupted vault");
|
|
204
232
|
return false;
|
|
205
233
|
}
|
|
206
234
|
}
|
|
235
|
+
/** Try to unlock using the passphrase stored in the Keychain (macOS). */
|
|
236
|
+
async unlockFromKeychain() {
|
|
237
|
+
if (!isKeychainAvailable())
|
|
238
|
+
return false;
|
|
239
|
+
const passphrase = await keychainLoadPassphrase(this.projectHash);
|
|
240
|
+
if (!passphrase)
|
|
241
|
+
return false;
|
|
242
|
+
return this.unlock(passphrase, { remember: false });
|
|
243
|
+
}
|
|
244
|
+
/** True when a passphrase for this vault is stored in the Keychain. */
|
|
245
|
+
async keychainRemembered() {
|
|
246
|
+
if (!isKeychainAvailable())
|
|
247
|
+
return false;
|
|
248
|
+
return keychainHasPassphrase(this.projectHash);
|
|
249
|
+
}
|
|
250
|
+
/** Forget the Keychain-stored passphrase. */
|
|
251
|
+
async keychainForget() {
|
|
252
|
+
if (!isKeychainAvailable())
|
|
253
|
+
return false;
|
|
254
|
+
const ok = await keychainClearPassphrase(this.projectHash);
|
|
255
|
+
this.audit("remove", ok, "keychain forget");
|
|
256
|
+
return ok;
|
|
257
|
+
}
|
|
207
258
|
lock() {
|
|
208
259
|
this.projectVault = null;
|
|
209
260
|
}
|
|
210
261
|
// ─── Project vault operations (require unlock) ───
|
|
211
|
-
add(name, value, note) {
|
|
262
|
+
add(name, value, note, category) {
|
|
212
263
|
if (!this.projectVault || !name.trim() || !value)
|
|
213
264
|
return false;
|
|
214
|
-
this.projectVault.add(name.trim(), value, note);
|
|
265
|
+
this.projectVault.add(name.trim(), value, note, category);
|
|
215
266
|
void this.projectVault.save(this.vaultPath).catch(() => { });
|
|
267
|
+
this.audit("add", true, name);
|
|
216
268
|
return true;
|
|
217
269
|
}
|
|
218
270
|
update(name, value, note) {
|
|
@@ -229,6 +281,7 @@ export class VaultManager {
|
|
|
229
281
|
const ok = this.projectVault.remove(name);
|
|
230
282
|
if (ok)
|
|
231
283
|
void this.projectVault.save(this.vaultPath).catch(() => { });
|
|
284
|
+
this.audit("remove", ok, name);
|
|
232
285
|
return ok;
|
|
233
286
|
}
|
|
234
287
|
// ─── Lookup (project first, then session) ───
|
|
@@ -241,9 +294,12 @@ export class VaultManager {
|
|
|
241
294
|
// ─── Session vault (memory only) ───
|
|
242
295
|
sessionAdd(name, value) {
|
|
243
296
|
this.sessionVault.add(name, value);
|
|
297
|
+
this.audit("add", true, `session:${name}`);
|
|
244
298
|
}
|
|
245
299
|
sessionRemove(name) {
|
|
246
|
-
|
|
300
|
+
const ok = this.sessionVault.remove(name);
|
|
301
|
+
this.audit("remove", ok, `session:${name}`);
|
|
302
|
+
return ok;
|
|
247
303
|
}
|
|
248
304
|
// ─── Listing ───
|
|
249
305
|
list() {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"vault.js","sourceRoot":"","sources":["../../../src/core/memory-engine/vault.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Ada Secure Vault — encrypted secret storage.\n *\n * - Project vault: AES-256-GCM encrypted at rest under\n * <agentDir>/vault/<project-hash>/vault.json; the key is derived from the\n * user passphrase with scrypt (never stored).\n * - Session vault: per-conversation secrets kept ONLY in process memory\n * (never written to disk).\n * - Values are exposed to the LLM only through @vault:<name> expansion in\n * the user's prompt, and the persisted session file stores the redacted\n * form (the expansion is stripped before persistence).\n *\n * Zero native dependencies: node:crypto works on Node and Bun.\n */\n\nimport { createCipheriv, createDecipheriv, randomBytes, scryptSync } from \"node:crypto\";\nimport { readFile, writeFile, mkdir } from \"node:fs/promises\";\nimport { existsSync } from \"node:fs\";\nimport { dirname, join } from \"node:path\";\n\nexport interface VaultSecret {\n\tname: string;\n\tvalue: string;\n\tnote?: string;\n\tcreated: string;\n\tupdated: string;\n}\n\nexport interface VaultListEntry {\n\tname: string;\n\tnote?: string;\n\tcreated: string;\n\tupdated: string;\n\tsource: \"project\" | \"session\";\n}\n\nconst KDF_N = 32768;\nconst KDF_R = 8;\nconst KDF_P = 1;\nconst KEY_LENGTH = 32;\n\ninterface VaultFilePayload {\n\tversion: 1;\n\tkdf: \"scrypt\";\n\tN: number;\n\tr: number;\n\tp: number;\n\tsalt: string; // base64\n\tiv: string; // base64\n\ttag: string; // base64 (GCM auth tag)\n\tdata: string; // base64 (AES-256-GCM ciphertext of the JSON entries)\n}\n\nfunction deriveKey(passphrase: string, salt: Buffer, N = KDF_N, r = KDF_R, p = KDF_P): Buffer {\n\treturn scryptSync(passphrase, salt, KEY_LENGTH, { N, r, p, maxmem: 128 * 1024 * 1024 });\n}\n\nfunction encryptPayload(payload: VaultSecret[], key: Buffer): { iv: Buffer; tag: Buffer; data: Buffer } {\n\tconst iv = randomBytes(12);\n\tconst cipher = createCipheriv(\"aes-256-gcm\", key, iv);\n\tconst data = Buffer.concat([cipher.update(JSON.stringify(payload), \"utf-8\"), cipher.final()]);\n\tconst tag = cipher.getAuthTag();\n\treturn { iv, tag, data };\n}\n\nfunction decryptPayload(file: VaultFilePayload, key: Buffer): VaultSecret[] {\n\tconst decipher = createDecipheriv(\"aes-256-gcm\", key, Buffer.from(file.iv, \"base64\"));\n\tdecipher.setAuthTag(Buffer.from(file.tag, \"base64\"));\n\tconst plain = Buffer.concat([\n\t\tdecipher.update(Buffer.from(file.data, \"base64\")),\n\t\tdecipher.final(), // throws on wrong passphrase (auth failure)\n\t]);\n\treturn JSON.parse(plain.toString(\"utf-8\")) as VaultSecret[];\n}\n\n/** Reference syntax in user prompts: @vault:<name>. */\nexport const VAULT_REF_PATTERN = /@vault:([A-Za-z0-9_.-]+)/g;\n\nexport interface VaultExpansion {\n\ttext: string;\n\t/** Names that were expanded (empty when none). */\n\texpanded: string[];\n\t/** Names referenced but missing (not expanded). */\n\tmissing: string[];\n}\n\n/**\n * Expand @vault:<name> references in a prompt. The returned text is what the\n * LLM receives; the ORIGINAL input (redacted) is what gets persisted.\n */\nexport function expandVaultRefs(text: string, vault: VaultManager | null): VaultExpansion {\n\tconst expanded: string[] = [];\n\tconst missing: string[] = [];\n\tif (!vault) {\n\t\tfor (const match of text.matchAll(VAULT_REF_PATTERN)) {\n\t\t\tmissing.push(match[1]);\n\t\t}\n\t\treturn { text, expanded, missing };\n\t}\n\tconst out = text.replace(VAULT_REF_PATTERN, (_full, name: string) => {\n\t\tconst value = vault.getSecret(name);\n\t\tif (value !== null) {\n\t\t\texpanded.push(name);\n\t\t\treturn value;\n\t\t}\n\t\tmissing.push(name);\n\t\treturn _full; // leave the reference intact so the user sees it failed\n\t});\n\treturn { text: out, expanded, missing };\n}\n\nexport class SessionVault {\n\tprivate entries = new Map<string, string>();\n\n\tadd(name: string, value: string): void {\n\t\tthis.entries.set(name, value);\n\t}\n\n\tget(name: string): string | null {\n\t\treturn this.entries.get(name) ?? null;\n\t}\n\n\tremove(name: string): boolean {\n\t\treturn this.entries.delete(name);\n\t}\n\n\thas(name: string): boolean {\n\t\treturn this.entries.has(name);\n\t}\n\n\tlist(): string[] {\n\t\treturn [...this.entries.keys()].sort();\n\t}\n\n\tclear(): void {\n\t\tthis.entries.clear();\n\t}\n}\n\nexport class ProjectVault {\n\tprivate secrets: Map<string, VaultSecret>;\n\tprivate key: Buffer;\n\n\tprivate constructor(key: Buffer, secrets: Map<string, VaultSecret>) {\n\t\tthis.key = key;\n\t\tthis.secrets = secrets;\n\t}\n\n\t/** Create a new empty project vault (writes the encrypted file). */\n\tstatic async create(filePath: string, passphrase: string): Promise<ProjectVault> {\n\t\tconst salt = randomBytes(16);\n\t\tconst key = deriveKey(passphrase, salt);\n\t\tconst vault = new ProjectVault(key, new Map());\n\t\tawait vault.save(filePath, salt);\n\t\treturn vault;\n\t}\n\n\t/** Open an existing vault. Throws on wrong passphrase. */\n\tstatic async open(filePath: string, passphrase: string): Promise<ProjectVault> {\n\t\tconst raw = await readFile(filePath, \"utf-8\");\n\t\tconst file = JSON.parse(raw) as VaultFilePayload;\n\t\tconst key = deriveKey(passphrase, Buffer.from(file.salt, \"base64\"), file.N, file.r, file.p);\n\t\tconst secrets = decryptPayload(file, key);\n\t\treturn new ProjectVault(\n\t\t\tkey,\n\t\t\tnew Map(secrets.map((s) => [s.name, s])),\n\t\t);\n\t}\n\n\tadd(name: string, value: string, note?: string): void {\n\t\tconst now = new Date().toISOString();\n\t\tthis.secrets.set(name, { name, value, note, created: now, updated: now });\n\t}\n\n\tupdate(name: string, value: string, note?: string): boolean {\n\t\tconst existing = this.secrets.get(name);\n\t\tif (!existing) return false;\n\t\tconst now = new Date().toISOString();\n\t\tthis.secrets.set(name, {\n\t\t\t...existing,\n\t\t\tvalue,\n\t\t\tnote: note ?? existing.note,\n\t\t\tupdated: now,\n\t\t});\n\t\treturn true;\n\t}\n\n\tget(name: string): string | null {\n\t\treturn this.secrets.get(name)?.value ?? null;\n\t}\n\n\tremove(name: string): boolean {\n\t\treturn this.secrets.delete(name);\n\t}\n\n\thas(name: string): boolean {\n\t\treturn this.secrets.has(name);\n\t}\n\n\tlist(): VaultListEntry[] {\n\t\treturn [...this.secrets.values()]\n\t\t\t.map((s) => ({\n\t\t\t\tname: s.name,\n\t\t\t\tnote: s.note,\n\t\t\t\tcreated: s.created,\n\t\t\t\tupdated: s.updated,\n\t\t\t\tsource: \"project\" as const,\n\t\t\t}))\n\t\t\t.sort((a, b) => a.name.localeCompare(b.name));\n\t}\n\n\tget entryCount(): number {\n\t\treturn this.secrets.size;\n\t}\n\n\tasync save(filePath: string, salt?: Buffer): Promise<void> {\n\t\tconst { iv, tag, data } = encryptPayload([...this.secrets.values()], this.key);\n\t\tconst payload: VaultFilePayload = {\n\t\t\tversion: 1,\n\t\t\tkdf: \"scrypt\",\n\t\t\tN: KDF_N,\n\t\t\tr: KDF_R,\n\t\t\tp: KDF_P,\n\t\t\tsalt: (salt ?? randomBytes(16)).toString(\"base64\"),\n\t\t\tiv: iv.toString(\"base64\"),\n\t\t\ttag: tag.toString(\"base64\"),\n\t\t\tdata: data.toString(\"base64\"),\n\t\t};\n\t\tawait mkdir(dirname(filePath), { recursive: true });\n\t\tawait writeFile(filePath, JSON.stringify(payload, null, 1), \"utf-8\");\n\t}\n}\n\nexport class VaultManager {\n\tprivate projectVault: ProjectVault | null = null;\n\treadonly sessionVault = new SessionVault();\n\tprivate vaultDir: string;\n\tprivate projectHash: string;\n\n\tconstructor(agentDir: string, projectHash: string | null) {\n\t\tthis.vaultDir = join(agentDir, \"vault\");\n\t\tthis.projectHash = projectHash ?? \"global\";\n\t}\n\n\tget vaultPath(): string {\n\t\treturn join(this.vaultDir, this.projectHash, \"vault.json\");\n\t}\n\n\tget isUnlocked(): boolean {\n\t\treturn this.projectVault !== null;\n\t}\n\n\t/** True when a project vault file already exists. */\n\texists(): boolean {\n\t\treturn existsSync(this.vaultPath);\n\t}\n\n\t/**\n\t * Unlock (or create) the project vault with the user passphrase.\n\t * Returns false on wrong passphrase.\n\t */\n\tasync unlock(passphrase: string): Promise<boolean> {\n\t\ttry {\n\t\t\tif (this.exists()) {\n\t\t\t\tthis.projectVault = await ProjectVault.open(this.vaultPath, passphrase);\n\t\t\t} else {\n\t\t\t\tthis.projectVault = await ProjectVault.create(this.vaultPath, passphrase);\n\t\t\t}\n\t\t\treturn true;\n\t\t} catch {\n\t\t\tthis.projectVault = null;\n\t\t\treturn false;\n\t\t}\n\t}\n\n\tlock(): void {\n\t\tthis.projectVault = null;\n\t}\n\n\t// ─── Project vault operations (require unlock) ───\n\n\tadd(name: string, value: string, note?: string): boolean {\n\t\tif (!this.projectVault || !name.trim() || !value) return false;\n\t\tthis.projectVault.add(name.trim(), value, note);\n\t\tvoid this.projectVault.save(this.vaultPath).catch(() => {});\n\t\treturn true;\n\t}\n\n\tupdate(name: string, value: string, note?: string): boolean {\n\t\tif (!this.projectVault) return false;\n\t\tconst ok = this.projectVault.update(name, value, note);\n\t\tif (ok) void this.projectVault.save(this.vaultPath).catch(() => {});\n\t\treturn ok;\n\t}\n\n\tremove(name: string): boolean {\n\t\tif (!this.projectVault) return false;\n\t\tconst ok = this.projectVault.remove(name);\n\t\tif (ok) void this.projectVault.save(this.vaultPath).catch(() => {});\n\t\treturn ok;\n\t}\n\n\t// ─── Lookup (project first, then session) ───\n\n\tgetSecret(name: string): string | null {\n\t\tconst fromProject = this.projectVault?.get(name) ?? null;\n\t\tif (fromProject !== null) return fromProject;\n\t\treturn this.sessionVault.get(name);\n\t}\n\n\t// ─── Session vault (memory only) ───\n\n\tsessionAdd(name: string, value: string): void {\n\t\tthis.sessionVault.add(name, value);\n\t}\n\n\tsessionRemove(name: string): boolean {\n\t\treturn this.sessionVault.remove(name);\n\t}\n\n\t// ─── Listing ───\n\n\tlist(): VaultListEntry[] {\n\t\tconst entries: VaultListEntry[] = [];\n\t\tif (this.projectVault) entries.push(...this.projectVault.list());\n\t\tfor (const name of this.sessionVault.list()) {\n\t\t\tentries.push({\n\t\t\t\tname,\n\t\t\t\tcreated: \"\",\n\t\t\t\tupdated: \"\",\n\t\t\t\tsource: \"session\",\n\t\t\t});\n\t\t}\n\t\treturn entries.sort((a, b) => a.name.localeCompare(b.name));\n\t}\n\n\tstatus(): {\n\t\tunlocked: boolean;\n\t\tvaultPath: string;\n\t\texists: boolean;\n\t\tprojectCount: number;\n\t\tsessionCount: number;\n\t} {\n\t\treturn {\n\t\t\tunlocked: this.isUnlocked,\n\t\t\tvaultPath: this.vaultPath,\n\t\t\texists: this.exists(),\n\t\t\tprojectCount: this.projectVault?.entryCount ?? 0,\n\t\t\tsessionCount: this.sessionVault.list().length,\n\t\t};\n\t}\n}\n\nexport { existsSync };\n"]}
|
|
1
|
+
{"version":3,"file":"vault.js","sourceRoot":"","sources":["../../../src/core/memory-engine/vault.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Ada Secure Vault — encrypted secret storage.\n *\n * - Project vault: AES-256-GCM encrypted at rest under\n * <agentDir>/vault/<project-hash>/vault.json; the key is derived from the\n * user passphrase with scrypt (never stored).\n * - Session vault: per-conversation secrets kept ONLY in process memory\n * (never written to disk).\n * - Values are exposed to the LLM only through @vault:<name> expansion in\n * the user's prompt, and the persisted session file stores the redacted\n * form (the expansion is stripped before persistence).\n *\n * Zero native dependencies: node:crypto works on Node and Bun.\n */\n\nimport { createCipheriv, createDecipheriv, randomBytes, scryptSync } from \"node:crypto\";\nimport { readFile, writeFile, mkdir } from \"node:fs/promises\";\nimport { existsSync } from \"node:fs\";\nimport { dirname, join } from \"node:path\";\nimport {\n\tisKeychainAvailable,\n\tkeychainHasPassphrase,\n\tkeychainLoadPassphrase,\n\tkeychainSavePassphrase,\n\tkeychainClearPassphrase,\n} from \"./keychain.ts\";\nimport type { AuditEntry } from \"./audit.ts\";\n\nexport interface VaultSecret {\n\tname: string;\n\tvalue: string;\n\tnote?: string;\n\tcategory?: string;\n\tcreated: string;\n\tupdated: string;\n}\n\nexport interface VaultListEntry {\n\tname: string;\n\tnote?: string;\n\tcategory?: string;\n\tcreated: string;\n\tupdated: string;\n\tsource: \"project\" | \"session\";\n}\n\nconst KDF_N = 32768;\nconst KDF_R = 8;\nconst KDF_P = 1;\nconst KEY_LENGTH = 32;\n\ninterface VaultFilePayload {\n\tversion: 1;\n\tkdf: \"scrypt\";\n\tN: number;\n\tr: number;\n\tp: number;\n\tsalt: string; // base64\n\tiv: string; // base64\n\ttag: string; // base64 (GCM auth tag)\n\tdata: string; // base64 (AES-256-GCM ciphertext of the JSON entries)\n}\n\nfunction deriveKey(passphrase: string, salt: Buffer, N = KDF_N, r = KDF_R, p = KDF_P): Buffer {\n\treturn scryptSync(passphrase, salt, KEY_LENGTH, { N, r, p, maxmem: 128 * 1024 * 1024 });\n}\n\nfunction encryptPayload(payload: VaultSecret[], key: Buffer): { iv: Buffer; tag: Buffer; data: Buffer } {\n\tconst iv = randomBytes(12);\n\tconst cipher = createCipheriv(\"aes-256-gcm\", key, iv);\n\tconst data = Buffer.concat([cipher.update(JSON.stringify(payload), \"utf-8\"), cipher.final()]);\n\tconst tag = cipher.getAuthTag();\n\treturn { iv, tag, data };\n}\n\nfunction decryptPayload(file: VaultFilePayload, key: Buffer): VaultSecret[] {\n\tconst decipher = createDecipheriv(\"aes-256-gcm\", key, Buffer.from(file.iv, \"base64\"));\n\tdecipher.setAuthTag(Buffer.from(file.tag, \"base64\"));\n\tconst plain = Buffer.concat([\n\t\tdecipher.update(Buffer.from(file.data, \"base64\")),\n\t\tdecipher.final(), // throws on wrong passphrase (auth failure)\n\t]);\n\treturn JSON.parse(plain.toString(\"utf-8\")) as VaultSecret[];\n}\n\n/** Reference syntax in user prompts: @vault:<name>. */\nexport const VAULT_REF_PATTERN = /@vault:([A-Za-z0-9_.-]+)/g;\n\nexport interface VaultExpansion {\n\ttext: string;\n\t/** Names that were expanded (empty when none). */\n\texpanded: string[];\n\t/** Names referenced but missing (not expanded). */\n\tmissing: string[];\n}\n\n/**\n * Expand @vault:<name> references in a prompt. The returned text is what the\n * LLM receives; the ORIGINAL input (redacted) is what gets persisted.\n */\nexport function expandVaultRefs(text: string, vault: VaultManager | null): VaultExpansion {\n\tconst expanded: string[] = [];\n\tconst missing: string[] = [];\n\tif (!vault) {\n\t\tfor (const match of text.matchAll(VAULT_REF_PATTERN)) {\n\t\t\tmissing.push(match[1]);\n\t\t}\n\t\treturn { text, expanded, missing };\n\t}\n\tconst out = text.replace(VAULT_REF_PATTERN, (_full, name: string) => {\n\t\tconst value = vault.getSecret(name);\n\t\tif (value !== null) {\n\t\t\texpanded.push(name);\n\t\t\treturn value;\n\t\t}\n\t\tmissing.push(name);\n\t\treturn _full; // leave the reference intact so the user sees it failed\n\t});\n\treturn { text: out, expanded, missing };\n}\n\nexport class SessionVault {\n\tprivate entries = new Map<string, string>();\n\n\tadd(name: string, value: string): void {\n\t\tthis.entries.set(name, value);\n\t}\n\n\tget(name: string): string | null {\n\t\treturn this.entries.get(name) ?? null;\n\t}\n\n\tremove(name: string): boolean {\n\t\treturn this.entries.delete(name);\n\t}\n\n\thas(name: string): boolean {\n\t\treturn this.entries.has(name);\n\t}\n\n\tlist(): string[] {\n\t\treturn [...this.entries.keys()].sort();\n\t}\n\n\tclear(): void {\n\t\tthis.entries.clear();\n\t}\n}\n\nexport class ProjectVault {\n\tprivate secrets: Map<string, VaultSecret>;\n\tprivate key: Buffer;\n\n\tprivate constructor(key: Buffer, secrets: Map<string, VaultSecret>) {\n\t\tthis.key = key;\n\t\tthis.secrets = secrets;\n\t}\n\n\t/** Create a new empty project vault (writes the encrypted file). */\n\tstatic async create(filePath: string, passphrase: string): Promise<ProjectVault> {\n\t\tconst salt = randomBytes(16);\n\t\tconst key = deriveKey(passphrase, salt);\n\t\tconst vault = new ProjectVault(key, new Map());\n\t\tawait vault.save(filePath, salt);\n\t\treturn vault;\n\t}\n\n\t/** Open an existing vault. Throws on wrong passphrase. */\n\tstatic async open(filePath: string, passphrase: string): Promise<ProjectVault> {\n\t\tconst raw = await readFile(filePath, \"utf-8\");\n\t\tconst file = JSON.parse(raw) as VaultFilePayload;\n\t\tconst key = deriveKey(passphrase, Buffer.from(file.salt, \"base64\"), file.N, file.r, file.p);\n\t\tconst secrets = decryptPayload(file, key);\n\t\treturn new ProjectVault(\n\t\t\tkey,\n\t\t\tnew Map(secrets.map((s) => [s.name, s])),\n\t\t);\n\t}\n\n\tadd(name: string, value: string, note?: string, category?: string): void {\n\t\tconst now = new Date().toISOString();\n\t\tthis.secrets.set(name, { name, value, note, category, created: now, updated: now });\n\t}\n\n\tupdate(name: string, value: string, note?: string, category?: string): boolean {\n\t\tconst existing = this.secrets.get(name);\n\t\tif (!existing) return false;\n\t\tconst now = new Date().toISOString();\n\t\tthis.secrets.set(name, {\n\t\t\t...existing,\n\t\t\tvalue,\n\t\t\tnote: note ?? existing.note,\n\t\t\tcategory: category ?? existing.category,\n\t\t\tupdated: now,\n\t\t});\n\t\treturn true;\n\t}\n\n\tget(name: string): string | null {\n\t\treturn this.secrets.get(name)?.value ?? null;\n\t}\n\n\tremove(name: string): boolean {\n\t\treturn this.secrets.delete(name);\n\t}\n\n\thas(name: string): boolean {\n\t\treturn this.secrets.has(name);\n\t}\n\n\tlist(): VaultListEntry[] {\n\t\treturn [...this.secrets.values()]\n\t\t\t.map((s) => ({\n\t\t\t\tname: s.name,\n\t\t\t\tnote: s.note,\n\t\t\t\tcategory: s.category,\n\t\t\t\tcreated: s.created,\n\t\t\t\tupdated: s.updated,\n\t\t\t\tsource: \"project\" as const,\n\t\t\t}))\n\t\t\t.sort((a, b) => a.name.localeCompare(b.name));\n\t}\n\n\tget entryCount(): number {\n\t\treturn this.secrets.size;\n\t}\n\n\tasync save(filePath: string, salt?: Buffer): Promise<void> {\n\t\tconst { iv, tag, data } = encryptPayload([...this.secrets.values()], this.key);\n\t\tconst payload: VaultFilePayload = {\n\t\t\tversion: 1,\n\t\t\tkdf: \"scrypt\",\n\t\t\tN: KDF_N,\n\t\t\tr: KDF_R,\n\t\t\tp: KDF_P,\n\t\t\tsalt: (salt ?? randomBytes(16)).toString(\"base64\"),\n\t\t\tiv: iv.toString(\"base64\"),\n\t\t\ttag: tag.toString(\"base64\"),\n\t\t\tdata: data.toString(\"base64\"),\n\t\t};\n\t\tawait mkdir(dirname(filePath), { recursive: true });\n\t\tawait writeFile(filePath, JSON.stringify(payload, null, 1), \"utf-8\");\n\t}\n}\n\nexport class VaultManager {\n\tprivate projectVault: ProjectVault | null = null;\n\treadonly sessionVault = new SessionVault();\n\tprivate vaultDir: string;\n\tprivate projectHash: string;\n\tprivate auditFn: ((entry: Omit<AuditEntry, \"seq\" | \"ts\" | \"hash\" | \"prevHash\">) => void) | null = null;\n\n\tconstructor(agentDir: string, projectHash: string | null) {\n\t\tthis.vaultDir = join(agentDir, \"vault\");\n\t\tthis.projectHash = projectHash ?? \"global\";\n\t}\n\n\t/** Attach the audit logger (enterprise accountability, never the value). */\n\tsetAudit(fn: ((entry: Omit<AuditEntry, \"seq\" | \"ts\" | \"hash\" | \"prevHash\">) => void) | null): void {\n\t\tthis.auditFn = fn;\n\t}\n\n\tprivate audit(action: AuditEntry[\"action\"], ok: boolean, reason?: string): void {\n\t\tthis.auditFn?.({\n\t\t\taction,\n\t\t\tsource: \"user\",\n\t\t\ttarget: \"vault\",\n\t\t\tscope: this.projectHash,\n\t\t\tok,\n\t\t\treason,\n\t\t});\n\t}\n\n\tget keychainAvailable(): boolean {\n\t\treturn isKeychainAvailable();\n\t}\n\n\tget vaultPath(): string {\n\t\treturn join(this.vaultDir, this.projectHash, \"vault.json\");\n\t}\n\n\tget isUnlocked(): boolean {\n\t\treturn this.projectVault !== null;\n\t}\n\n\t/** True when a project vault file already exists. */\n\texists(): boolean {\n\t\treturn existsSync(this.vaultPath);\n\t}\n\n\t/**\n\t * Unlock (or create) the project vault with the user passphrase.\n\t * Returns false on wrong passphrase. When `remember` is set and the\n\t * Keychain is available, the passphrase is stored there (macOS).\n\t */\n\tasync unlock(passphrase: string, options: { remember?: boolean } = {}): Promise<boolean> {\n\t\ttry {\n\t\t\tif (this.exists()) {\n\t\t\t\tthis.projectVault = await ProjectVault.open(this.vaultPath, passphrase);\n\t\t\t} else {\n\t\t\t\tthis.projectVault = await ProjectVault.create(this.vaultPath, passphrase);\n\t\t\t}\n\t\t\tif (options.remember && isKeychainAvailable()) {\n\t\t\t\tconst saved = await keychainSavePassphrase(this.projectHash, passphrase);\n\t\t\t\tthis.audit(\"add\", saved, saved ? \"keychain\" : \"keychain save failed\");\n\t\t\t}\n\t\t\tthis.audit(\"add\", true, this.exists() ? \"unlock\" : \"create\");\n\t\t\treturn true;\n\t\t} catch {\n\t\t\tthis.projectVault = null;\n\t\t\tthis.audit(\"rejected\", false, \"wrong passphrase or corrupted vault\");\n\t\t\treturn false;\n\t\t}\n\t}\n\n\t/** Try to unlock using the passphrase stored in the Keychain (macOS). */\n\tasync unlockFromKeychain(): Promise<boolean> {\n\t\tif (!isKeychainAvailable()) return false;\n\t\tconst passphrase = await keychainLoadPassphrase(this.projectHash);\n\t\tif (!passphrase) return false;\n\t\treturn this.unlock(passphrase, { remember: false });\n\t}\n\n\t/** True when a passphrase for this vault is stored in the Keychain. */\n\tasync keychainRemembered(): Promise<boolean> {\n\t\tif (!isKeychainAvailable()) return false;\n\t\treturn keychainHasPassphrase(this.projectHash);\n\t}\n\n\t/** Forget the Keychain-stored passphrase. */\n\tasync keychainForget(): Promise<boolean> {\n\t\tif (!isKeychainAvailable()) return false;\n\t\tconst ok = await keychainClearPassphrase(this.projectHash);\n\t\tthis.audit(\"remove\", ok, \"keychain forget\");\n\t\treturn ok;\n\t}\n\n\tlock(): void {\n\t\tthis.projectVault = null;\n\t}\n\n\t// ─── Project vault operations (require unlock) ───\n\n\tadd(name: string, value: string, note?: string, category?: string): boolean {\n\t\tif (!this.projectVault || !name.trim() || !value) return false;\n\t\tthis.projectVault.add(name.trim(), value, note, category);\n\t\tvoid this.projectVault.save(this.vaultPath).catch(() => {});\n\t\tthis.audit(\"add\", true, name);\n\t\treturn true;\n\t}\n\n\tupdate(name: string, value: string, note?: string): boolean {\n\t\tif (!this.projectVault) return false;\n\t\tconst ok = this.projectVault.update(name, value, note);\n\t\tif (ok) void this.projectVault.save(this.vaultPath).catch(() => {});\n\t\treturn ok;\n\t}\n\n\tremove(name: string): boolean {\n\t\tif (!this.projectVault) return false;\n\t\tconst ok = this.projectVault.remove(name);\n\t\tif (ok) void this.projectVault.save(this.vaultPath).catch(() => {});\n\t\tthis.audit(\"remove\", ok, name);\n\t\treturn ok;\n\t}\n\n\t// ─── Lookup (project first, then session) ───\n\n\tgetSecret(name: string): string | null {\n\t\tconst fromProject = this.projectVault?.get(name) ?? null;\n\t\tif (fromProject !== null) return fromProject;\n\t\treturn this.sessionVault.get(name);\n\t}\n\n\t// ─── Session vault (memory only) ───\n\n\tsessionAdd(name: string, value: string): void {\n\t\tthis.sessionVault.add(name, value);\n\t\tthis.audit(\"add\", true, `session:${name}`);\n\t}\n\n\tsessionRemove(name: string): boolean {\n\t\tconst ok = this.sessionVault.remove(name);\n\t\tthis.audit(\"remove\", ok, `session:${name}`);\n\t\treturn ok;\n\t}\n\n\t// ─── Listing ───\n\n\tlist(): VaultListEntry[] {\n\t\tconst entries: VaultListEntry[] = [];\n\t\tif (this.projectVault) entries.push(...this.projectVault.list());\n\t\tfor (const name of this.sessionVault.list()) {\n\t\t\tentries.push({\n\t\t\t\tname,\n\t\t\t\tcreated: \"\",\n\t\t\t\tupdated: \"\",\n\t\t\t\tsource: \"session\",\n\t\t\t});\n\t\t}\n\t\treturn entries.sort((a, b) => a.name.localeCompare(b.name));\n\t}\n\n\tstatus(): {\n\t\tunlocked: boolean;\n\t\tvaultPath: string;\n\t\texists: boolean;\n\t\tprojectCount: number;\n\t\tsessionCount: number;\n\t} {\n\t\treturn {\n\t\t\tunlocked: this.isUnlocked,\n\t\t\tvaultPath: this.vaultPath,\n\t\t\texists: this.exists(),\n\t\t\tprojectCount: this.projectVault?.entryCount ?? 0,\n\t\t\tsessionCount: this.sessionVault.list().length,\n\t\t};\n\t}\n}\n\nexport { existsSync };\n"]}
|