'use strict' /** * Secret redaction helpers (agent/SECURITY.md S4). * Invites, caps, seeds must never appear unredacted in progress logs. */ // z32 alphabet includes 1 and 8 (not only 2–7) const Z32_CHARS = 'a-z0-9' const INVITE_RE = new RegExp(`\\bfj1\\.[${Z32_CHARS}]+`, 'gi') // Long z32 blobs (keys/caps) — avoid short words const Z32_LONG_RE = new RegExp(`\\b[${Z32_CHARS}]{40,}\\b`, 'gi') /** * Redact a full fj1. invite string for logs. * @param {string} invite * @returns {string} */ function redactInvite(invite) { const s = String(invite || '') if (!s.startsWith('fj1.') || s.length < 12) return s ? '[redacted-invite]' : '' const body = s.slice(4) if (body.length <= 12) return 'fj1.' + '*'.repeat(Math.min(8, body.length)) return `fj1.${body.slice(0, 6)}…${body.slice(-4)} (len=${s.length})` } /** * Redact a z32 key/cap for logs. * @param {string} key * @returns {string} */ function redactKey(key) { const s = String(key || '') if (s.length < 8) return s ? '[redacted-key]' : '' return `${s.slice(0, 4)}…${s.slice(-4)} (len=${s.length})` } /** * Redact any fj1. tokens (and long z32 blobs) inside a free-form message. * @param {string} text * @returns {string} */ function redactSecretsInText(text) { let s = String(text || '') s = s.replace(INVITE_RE, (m) => redactInvite(m)) // Avoid mangling short words; only long z32-looking tokens s = s.replace(Z32_LONG_RE, (m) => { if (m.startsWith('fj1')) return m return redactKey(m) }) return s } /** * True if string looks like an invite or long secret. * @param {string} s * @returns {boolean} */ function looksLikeSecret(s) { const t = String(s || '') if (t.startsWith('fj1.')) return true if (/^[a-z2-7]{40,}$/i.test(t)) return true if (/seed|private.?key|cap=/i.test(t) && t.length > 20) return true return false } module.exports = { redactInvite, redactKey, redactSecretsInText, looksLikeSecret }