Closer/scratchpad/d1_atrest.js

60 lines
2.7 KiB
JavaScript
Raw Permalink Normal View History

// 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) })