@hoilab/ada-cli 0.84.17 → 0.84.19

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.
@@ -1 +1 @@
1
- {"version":3,"file":"security.d.ts","sourceRoot":"","sources":["../../../src/core/memory-engine/security.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,MAAM,MAAM,YAAY,GAAG,QAAQ,GAAG,UAAU,GAAG,YAAY,CAAC;AAEhE,MAAM,MAAM,iBAAiB,GAC1B,YAAY,GACZ,SAAS,GACT,mBAAmB,GACnB,WAAW,GACX,WAAW,GACX,QAAQ,GACR,UAAU,GACV,SAAS,GACT,WAAW,GACX,cAAc,CAAC;AAElB,MAAM,WAAW,iBAAiB;IACjC,gDAAgD;IAChD,OAAO,EAAE,OAAO,CAAC;IACjB,2BAA2B;IAC3B,UAAU,EAAE,iBAAiB,EAAE,CAAC;IAChC,0DAA0D;IAC1D,MAAM,EAAE,MAAM,CAAC;IACf,sEAAsE;IACtE,UAAU,EAAE,OAAO,CAAC;CACpB;AA4ID;;;;;;GAMG;AACH,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,MAAM,EAAE,KAAK,GAAE,YAAuB,EAAE,MAAM,GAAE,IAAI,GAAG,IAAW,GAAG,iBAAiB,CA+ChI;AAED;;;;;GAKG;AACH,wBAAgB,eAAe,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,GAAE,YAAuB,GAAG,MAAM,CAmBpF","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.d.ts","sourceRoot":"","sources":["../../../src/core/memory-engine/security.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,MAAM,MAAM,YAAY,GAAG,QAAQ,GAAG,UAAU,GAAG,YAAY,CAAC;AAEhE,MAAM,MAAM,iBAAiB,GAC1B,YAAY,GACZ,SAAS,GACT,mBAAmB,GACnB,WAAW,GACX,WAAW,GACX,QAAQ,GACR,UAAU,GACV,SAAS,GACT,WAAW,GACX,cAAc,CAAC;AAElB,MAAM,WAAW,iBAAiB;IACjC,gDAAgD;IAChD,OAAO,EAAE,OAAO,CAAC;IACjB,2BAA2B;IAC3B,UAAU,EAAE,iBAAiB,EAAE,CAAC;IAChC,0DAA0D;IAC1D,MAAM,EAAE,MAAM,CAAC;IACf,sEAAsE;IACtE,UAAU,EAAE,OAAO,CAAC;CACpB;AAsJD;;;;;;GAMG;AACH,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,MAAM,EAAE,KAAK,GAAE,YAAuB,EAAE,MAAM,GAAE,IAAI,GAAG,IAAW,GAAG,iBAAiB,CA+ChI;AAED;;;;;GAKG;AACH,wBAAgB,eAAe,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,GAAE,YAAuB,GAAG,MAAM,CAmBpF","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"]}
@@ -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"]}
@@ -0,0 +1,104 @@
1
+ /**
2
+ * Ada Secure Vault — encrypted secret storage.
3
+ *
4
+ * - Project vault: AES-256-GCM encrypted at rest under
5
+ * <agentDir>/vault/<project-hash>/vault.json; the key is derived from the
6
+ * user passphrase with scrypt (never stored).
7
+ * - Session vault: per-conversation secrets kept ONLY in process memory
8
+ * (never written to disk).
9
+ * - Values are exposed to the LLM only through @vault:<name> expansion in
10
+ * the user's prompt, and the persisted session file stores the redacted
11
+ * form (the expansion is stripped before persistence).
12
+ *
13
+ * Zero native dependencies: node:crypto works on Node and Bun.
14
+ */
15
+ import { existsSync } from "node:fs";
16
+ export interface VaultSecret {
17
+ name: string;
18
+ value: string;
19
+ note?: string;
20
+ category?: string;
21
+ created: string;
22
+ updated: string;
23
+ }
24
+ export interface VaultListEntry {
25
+ name: string;
26
+ note?: string;
27
+ category?: string;
28
+ created: string;
29
+ updated: string;
30
+ source: "project" | "session";
31
+ }
32
+ /** Reference syntax in user prompts: @vault:<name>. */
33
+ export declare const VAULT_REF_PATTERN: RegExp;
34
+ export interface VaultExpansion {
35
+ text: string;
36
+ /** Names that were expanded (empty when none). */
37
+ expanded: string[];
38
+ /** Names referenced but missing (not expanded). */
39
+ missing: string[];
40
+ }
41
+ /**
42
+ * Expand @vault:<name> references in a prompt. The returned text is what the
43
+ * LLM receives; the ORIGINAL input (redacted) is what gets persisted.
44
+ */
45
+ export declare function expandVaultRefs(text: string, vault: VaultManager | null): VaultExpansion;
46
+ export declare class SessionVault {
47
+ private entries;
48
+ add(name: string, value: string): void;
49
+ get(name: string): string | null;
50
+ remove(name: string): boolean;
51
+ has(name: string): boolean;
52
+ list(): string[];
53
+ clear(): void;
54
+ }
55
+ export declare class ProjectVault {
56
+ private secrets;
57
+ private key;
58
+ private constructor();
59
+ /** Create a new empty project vault (writes the encrypted file). */
60
+ static create(filePath: string, passphrase: string): Promise<ProjectVault>;
61
+ /** Open an existing vault. Throws on wrong passphrase. */
62
+ static open(filePath: string, passphrase: string): Promise<ProjectVault>;
63
+ add(name: string, value: string, note?: string, category?: string): void;
64
+ update(name: string, value: string, note?: string, category?: string): boolean;
65
+ get(name: string): string | null;
66
+ remove(name: string): boolean;
67
+ has(name: string): boolean;
68
+ list(): VaultListEntry[];
69
+ get entryCount(): number;
70
+ save(filePath: string, salt?: Buffer): Promise<void>;
71
+ }
72
+ export declare class VaultManager {
73
+ private projectVault;
74
+ readonly sessionVault: SessionVault;
75
+ private vaultDir;
76
+ private projectHash;
77
+ constructor(agentDir: string, projectHash: string | null);
78
+ get vaultPath(): string;
79
+ get isUnlocked(): boolean;
80
+ /** True when a project vault file already exists. */
81
+ exists(): boolean;
82
+ /**
83
+ * Unlock (or create) the project vault with the user passphrase.
84
+ * Returns false on wrong passphrase.
85
+ */
86
+ unlock(passphrase: string): Promise<boolean>;
87
+ lock(): void;
88
+ add(name: string, value: string, note?: string, category?: string): boolean;
89
+ update(name: string, value: string, note?: string): boolean;
90
+ remove(name: string): boolean;
91
+ getSecret(name: string): string | null;
92
+ sessionAdd(name: string, value: string): void;
93
+ sessionRemove(name: string): boolean;
94
+ list(): VaultListEntry[];
95
+ status(): {
96
+ unlocked: boolean;
97
+ vaultPath: string;
98
+ exists: boolean;
99
+ projectCount: number;
100
+ sessionCount: number;
101
+ };
102
+ }
103
+ export { existsSync };
104
+ //# sourceMappingURL=vault.d.ts.map
@@ -0,0 +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\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\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, 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\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"]}
@@ -0,0 +1,276 @@
1
+ /**
2
+ * Ada Secure Vault — encrypted secret storage.
3
+ *
4
+ * - Project vault: AES-256-GCM encrypted at rest under
5
+ * <agentDir>/vault/<project-hash>/vault.json; the key is derived from the
6
+ * user passphrase with scrypt (never stored).
7
+ * - Session vault: per-conversation secrets kept ONLY in process memory
8
+ * (never written to disk).
9
+ * - Values are exposed to the LLM only through @vault:<name> expansion in
10
+ * the user's prompt, and the persisted session file stores the redacted
11
+ * form (the expansion is stripped before persistence).
12
+ *
13
+ * Zero native dependencies: node:crypto works on Node and Bun.
14
+ */
15
+ import { createCipheriv, createDecipheriv, randomBytes, scryptSync } from "node:crypto";
16
+ import { readFile, writeFile, mkdir } from "node:fs/promises";
17
+ import { existsSync } from "node:fs";
18
+ import { dirname, join } from "node:path";
19
+ const KDF_N = 32768;
20
+ const KDF_R = 8;
21
+ const KDF_P = 1;
22
+ const KEY_LENGTH = 32;
23
+ function deriveKey(passphrase, salt, N = KDF_N, r = KDF_R, p = KDF_P) {
24
+ return scryptSync(passphrase, salt, KEY_LENGTH, { N, r, p, maxmem: 128 * 1024 * 1024 });
25
+ }
26
+ function encryptPayload(payload, key) {
27
+ const iv = randomBytes(12);
28
+ const cipher = createCipheriv("aes-256-gcm", key, iv);
29
+ const data = Buffer.concat([cipher.update(JSON.stringify(payload), "utf-8"), cipher.final()]);
30
+ const tag = cipher.getAuthTag();
31
+ return { iv, tag, data };
32
+ }
33
+ function decryptPayload(file, key) {
34
+ const decipher = createDecipheriv("aes-256-gcm", key, Buffer.from(file.iv, "base64"));
35
+ decipher.setAuthTag(Buffer.from(file.tag, "base64"));
36
+ const plain = Buffer.concat([
37
+ decipher.update(Buffer.from(file.data, "base64")),
38
+ decipher.final(), // throws on wrong passphrase (auth failure)
39
+ ]);
40
+ return JSON.parse(plain.toString("utf-8"));
41
+ }
42
+ /** Reference syntax in user prompts: @vault:<name>. */
43
+ export const VAULT_REF_PATTERN = /@vault:([A-Za-z0-9_.-]+)/g;
44
+ /**
45
+ * Expand @vault:<name> references in a prompt. The returned text is what the
46
+ * LLM receives; the ORIGINAL input (redacted) is what gets persisted.
47
+ */
48
+ export function expandVaultRefs(text, vault) {
49
+ const expanded = [];
50
+ const missing = [];
51
+ if (!vault) {
52
+ for (const match of text.matchAll(VAULT_REF_PATTERN)) {
53
+ missing.push(match[1]);
54
+ }
55
+ return { text, expanded, missing };
56
+ }
57
+ const out = text.replace(VAULT_REF_PATTERN, (_full, name) => {
58
+ const value = vault.getSecret(name);
59
+ if (value !== null) {
60
+ expanded.push(name);
61
+ return value;
62
+ }
63
+ missing.push(name);
64
+ return _full; // leave the reference intact so the user sees it failed
65
+ });
66
+ return { text: out, expanded, missing };
67
+ }
68
+ export class SessionVault {
69
+ entries = new Map();
70
+ add(name, value) {
71
+ this.entries.set(name, value);
72
+ }
73
+ get(name) {
74
+ return this.entries.get(name) ?? null;
75
+ }
76
+ remove(name) {
77
+ return this.entries.delete(name);
78
+ }
79
+ has(name) {
80
+ return this.entries.has(name);
81
+ }
82
+ list() {
83
+ return [...this.entries.keys()].sort();
84
+ }
85
+ clear() {
86
+ this.entries.clear();
87
+ }
88
+ }
89
+ export class ProjectVault {
90
+ secrets;
91
+ key;
92
+ constructor(key, secrets) {
93
+ this.key = key;
94
+ this.secrets = secrets;
95
+ }
96
+ /** Create a new empty project vault (writes the encrypted file). */
97
+ static async create(filePath, passphrase) {
98
+ const salt = randomBytes(16);
99
+ const key = deriveKey(passphrase, salt);
100
+ const vault = new ProjectVault(key, new Map());
101
+ await vault.save(filePath, salt);
102
+ return vault;
103
+ }
104
+ /** Open an existing vault. Throws on wrong passphrase. */
105
+ static async open(filePath, passphrase) {
106
+ const raw = await readFile(filePath, "utf-8");
107
+ const file = JSON.parse(raw);
108
+ const key = deriveKey(passphrase, Buffer.from(file.salt, "base64"), file.N, file.r, file.p);
109
+ const secrets = decryptPayload(file, key);
110
+ return new ProjectVault(key, new Map(secrets.map((s) => [s.name, s])));
111
+ }
112
+ add(name, value, note, category) {
113
+ const now = new Date().toISOString();
114
+ this.secrets.set(name, { name, value, note, category, created: now, updated: now });
115
+ }
116
+ update(name, value, note, category) {
117
+ const existing = this.secrets.get(name);
118
+ if (!existing)
119
+ return false;
120
+ const now = new Date().toISOString();
121
+ this.secrets.set(name, {
122
+ ...existing,
123
+ value,
124
+ note: note ?? existing.note,
125
+ category: category ?? existing.category,
126
+ updated: now,
127
+ });
128
+ return true;
129
+ }
130
+ get(name) {
131
+ return this.secrets.get(name)?.value ?? null;
132
+ }
133
+ remove(name) {
134
+ return this.secrets.delete(name);
135
+ }
136
+ has(name) {
137
+ return this.secrets.has(name);
138
+ }
139
+ list() {
140
+ return [...this.secrets.values()]
141
+ .map((s) => ({
142
+ name: s.name,
143
+ note: s.note,
144
+ category: s.category,
145
+ created: s.created,
146
+ updated: s.updated,
147
+ source: "project",
148
+ }))
149
+ .sort((a, b) => a.name.localeCompare(b.name));
150
+ }
151
+ get entryCount() {
152
+ return this.secrets.size;
153
+ }
154
+ async save(filePath, salt) {
155
+ const { iv, tag, data } = encryptPayload([...this.secrets.values()], this.key);
156
+ const payload = {
157
+ version: 1,
158
+ kdf: "scrypt",
159
+ N: KDF_N,
160
+ r: KDF_R,
161
+ p: KDF_P,
162
+ salt: (salt ?? randomBytes(16)).toString("base64"),
163
+ iv: iv.toString("base64"),
164
+ tag: tag.toString("base64"),
165
+ data: data.toString("base64"),
166
+ };
167
+ await mkdir(dirname(filePath), { recursive: true });
168
+ await writeFile(filePath, JSON.stringify(payload, null, 1), "utf-8");
169
+ }
170
+ }
171
+ export class VaultManager {
172
+ projectVault = null;
173
+ sessionVault = new SessionVault();
174
+ vaultDir;
175
+ projectHash;
176
+ constructor(agentDir, projectHash) {
177
+ this.vaultDir = join(agentDir, "vault");
178
+ this.projectHash = projectHash ?? "global";
179
+ }
180
+ get vaultPath() {
181
+ return join(this.vaultDir, this.projectHash, "vault.json");
182
+ }
183
+ get isUnlocked() {
184
+ return this.projectVault !== null;
185
+ }
186
+ /** True when a project vault file already exists. */
187
+ exists() {
188
+ return existsSync(this.vaultPath);
189
+ }
190
+ /**
191
+ * Unlock (or create) the project vault with the user passphrase.
192
+ * Returns false on wrong passphrase.
193
+ */
194
+ async unlock(passphrase) {
195
+ try {
196
+ if (this.exists()) {
197
+ this.projectVault = await ProjectVault.open(this.vaultPath, passphrase);
198
+ }
199
+ else {
200
+ this.projectVault = await ProjectVault.create(this.vaultPath, passphrase);
201
+ }
202
+ return true;
203
+ }
204
+ catch {
205
+ this.projectVault = null;
206
+ return false;
207
+ }
208
+ }
209
+ lock() {
210
+ this.projectVault = null;
211
+ }
212
+ // ─── Project vault operations (require unlock) ───
213
+ add(name, value, note, category) {
214
+ if (!this.projectVault || !name.trim() || !value)
215
+ return false;
216
+ this.projectVault.add(name.trim(), value, note, category);
217
+ void this.projectVault.save(this.vaultPath).catch(() => { });
218
+ return true;
219
+ }
220
+ update(name, value, note) {
221
+ if (!this.projectVault)
222
+ return false;
223
+ const ok = this.projectVault.update(name, value, note);
224
+ if (ok)
225
+ void this.projectVault.save(this.vaultPath).catch(() => { });
226
+ return ok;
227
+ }
228
+ remove(name) {
229
+ if (!this.projectVault)
230
+ return false;
231
+ const ok = this.projectVault.remove(name);
232
+ if (ok)
233
+ void this.projectVault.save(this.vaultPath).catch(() => { });
234
+ return ok;
235
+ }
236
+ // ─── Lookup (project first, then session) ───
237
+ getSecret(name) {
238
+ const fromProject = this.projectVault?.get(name) ?? null;
239
+ if (fromProject !== null)
240
+ return fromProject;
241
+ return this.sessionVault.get(name);
242
+ }
243
+ // ─── Session vault (memory only) ───
244
+ sessionAdd(name, value) {
245
+ this.sessionVault.add(name, value);
246
+ }
247
+ sessionRemove(name) {
248
+ return this.sessionVault.remove(name);
249
+ }
250
+ // ─── Listing ───
251
+ list() {
252
+ const entries = [];
253
+ if (this.projectVault)
254
+ entries.push(...this.projectVault.list());
255
+ for (const name of this.sessionVault.list()) {
256
+ entries.push({
257
+ name,
258
+ created: "",
259
+ updated: "",
260
+ source: "session",
261
+ });
262
+ }
263
+ return entries.sort((a, b) => a.name.localeCompare(b.name));
264
+ }
265
+ status() {
266
+ return {
267
+ unlocked: this.isUnlocked,
268
+ vaultPath: this.vaultPath,
269
+ exists: this.exists(),
270
+ projectCount: this.projectVault?.entryCount ?? 0,
271
+ sessionCount: this.sessionVault.list().length,
272
+ };
273
+ }
274
+ }
275
+ export { existsSync };
276
+ //# sourceMappingURL=vault.js.map