// D1 at-rest ciphertext verifier (READ-ONLY). Prints only field -> enc✓ / NOT-ENC + length. // NEVER prints decrypted/plaintext content (per the QA logging rule). const admin = require('firebase-admin') const path = require('path') const SA = process.env.SA_JSON || path.join(__dirname, '..', 'closer-app-22014-firebase-adminsdk-fbsvc-ed20bf6003.json') const COUPLE = process.env.COUPLE_ID || 'Xal3Kw3gjSdn0niERYKJ' admin.initializeApp({ credential: admin.credential.cert(require(SA)) }) const db = admin.firestore() const CIPHER = /^(enc:v1:|sealed:v1:)/ // fields that MUST be ciphertext if present const PRIVATE_FIELDS = ['text','content','lastMessagePreview','title','description','duration','note','message','answer','body','preview'] function checkVal(v) { if (typeof v !== 'string') return null if (CIPHER.test(v)) return `enc✓(${v.length})` // base64 Tink media or short metadata? flag anything that isn't enc/sealed return `NOT-ENC?(len=${v.length})` } async function sampleColl(ref, label, limit = 3) { try { const snap = await ref.limit(limit).get() if (snap.empty) { console.log(` [${label}] empty`); return } let i = 0 snap.forEach(doc => { if (i++ >= limit) return const d = doc.data() const hits = [] for (const f of PRIVATE_FIELDS) { if (d[f] !== undefined) hits.push(`${f}=${checkVal(d[f])}`) } console.log(` [${label}] ${doc.id}: ${hits.length ? hits.join(' ') : '(no private fields)'}`) }) } catch (e) { console.log(` [${label}] ERR ${e.code || e.message}`) } } (async () => { console.log('=== D1 at-rest check, couple', COUPLE, '===') // 1) couple doc subcollections const coupleRef = db.collection('couples').doc(COUPLE) const subs = await coupleRef.listCollections().catch(() => []) console.log('couple subcollections:', subs.map(s => s.id).join(', ') || '(none)') for (const s of subs) await sampleColl(s, `couples/${COUPLE}/${s.id}`) // 2) common top-level collections keyed by coupleId for (const c of ['messages','conversations','answers','bucket_list','date_plans','date_swipes','memory_capsules','capsules','date_plan_preferences','game_sessions']) { const ref = db.collection(c).where('coupleId','==',COUPLE) await sampleColl(ref, c) } // 3) conversations -> messages (nested) try { const convs = await db.collection('conversations').where('coupleId','==',COUPLE).limit(2).get() for (const cv of convs.docs) { await sampleColl(cv.ref.collection('messages'), `conversations/${cv.id}/messages`) } } catch (e) { console.log(' conv->messages ERR', e.code || e.message) } console.log('=== done ===') process.exit(0) })().catch(e => { console.error('FATAL', e); process.exit(1) })