60 lines
2.7 KiB
JavaScript
60 lines
2.7 KiB
JavaScript
// 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) })
|