From 897899f2daed4a52056e7efba58a5d4e8f86d0f3 Mon Sep 17 00:00:00 2001 From: null Date: Mon, 29 Jun 2026 11:18:50 -0500 Subject: [PATCH] Revert "chore(scratchpad): rules arrays and positions scratch work" This reverts commit 62696a69ea2de499b3b28204bdb4e0ffa9fdfe25. --- scratchpad/rules_arrays.js | 61 -------------------------------------- scratchpad/rules_pos.js | 22 -------------- 2 files changed, 83 deletions(-) delete mode 100644 scratchpad/rules_arrays.js delete mode 100644 scratchpad/rules_pos.js diff --git a/scratchpad/rules_arrays.js b/scratchpad/rules_arrays.js deleted file mode 100644 index bdda550b..00000000 --- a/scratchpad/rules_arrays.js +++ /dev/null @@ -1,61 +0,0 @@ -// Tier-2 rules verification (LIVE): mint a MEMBER token (QA) → raw Firestore REST → confirm the -// session-progress arrays can only grow by the writer's OWN uid. Foreign-uid adds + removals must be -// DENIED (403). These are rejected writes, so nothing mutates. (Own-uid add is verified via the app.) -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 PROJECT = 'closer-app-22014' -const APIKEY = 'AIzaSyDAD7FnEYzhMsil41SzJ1XMjUNnJWmjie8' -const COUPLE = process.env.COUPLE_ID || 'Xal3Kw3gjSdn0niERYKJ' -const QA = 'Y05AKO2IlTPMa0JQW1BiNIM0uzK2' // a real MEMBER -const SAM = 'imDjjOTTQvXGGjyUhUc5JSeHWkU2' // the partner (foreign, from QA's perspective) -admin.initializeApp({ credential: admin.credential.cert(require(SA)) }) -const db = admin.firestore() -const docBase = `https://firestore.googleapis.com/v1/projects/${PROJECT}/databases/(default)/documents` - -async function memberToken() { - const custom = await admin.auth().createCustomToken(QA) - const r = await fetch(`https://identitytoolkit.googleapis.com/v1/accounts:signInWithCustomToken?key=${APIKEY}`, { - method: 'POST', headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ token: custom, returnSecureToken: true }) - }) - const j = await r.json(); if (!j.idToken) throw new Error('no idToken: ' + JSON.stringify(j)); return j.idToken -} -const arr = (uids) => ({ arrayValue: uids.length ? { values: uids.map((u) => ({ stringValue: u })) } : {} }) -async function patch(token, sid, field, uids, label, expectDeny) { - const r = await fetch(`${docBase}/couples/${COUPLE}/sessions/${sid}?updateMask.fieldPaths=${field}`, { - method: 'PATCH', headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' }, - body: JSON.stringify({ fields: { [field]: arr(uids) } }) - }) - const denied = r.status !== 200 - const good = expectDeny ? denied : !denied - console.log(` ${good ? '✅' : '❌❌'} ${label}: ${denied ? `DENIED(${r.status})` : 'ALLOWED(200)'} (expected ${expectDeny ? 'deny' : 'allow'})`) - return good -} - -;(async () => { - // find a session with a non-empty joinedByUsers (for the removal test) + its current arrays - const snap = await db.collection('couples').doc(COUPLE).collection('sessions').get() - let target = null - snap.forEach((d) => { const x = d.data(); if (!target && Array.isArray(x.joinedByUsers) && x.joinedByUsers.length > 0) target = { id: d.id, ...x } }) - if (!target) { snap.forEach((d) => { if (!target && d.id !== '_active') target = { id: d.id, ...d.data() } }) } - console.log('target session', target.id, 'joinedByUsers=', JSON.stringify(target.joinedByUsers || []), 'completedByUsers=', JSON.stringify(target.completedByUsers || [])) - const tok = await memberToken() - const cur = Array.isArray(target.joinedByUsers) ? target.joinedByUsers : [] - let bad = 0 - // NEG1: add a foreign/bogus uid to joinedByUsers → deny - if (!await patch(tok, target.id, 'joinedByUsers', [...new Set([...cur, 'bogus-' + Date.now()])], 'foreign-add joinedByUsers', true)) bad++ - // NEG2: add the PARTNER's uid (spoof) to completedByUsers → deny - const curC = Array.isArray(target.completedByUsers) ? target.completedByUsers : [] - if (!curC.includes(SAM) || !curC.includes(QA)) { - if (!await patch(tok, target.id, 'completedByUsers', [...new Set([...curC, QA, SAM])], 'spoof-partner completedByUsers', true)) bad++ - } else { - console.log(' (skip spoof-completed: both already present on this completed session)') - } - // NEG3: removal from a non-empty joinedByUsers → deny - if (cur.length > 0) { if (!await patch(tok, target.id, 'joinedByUsers', [], 'removal joinedByUsers', true)) bad++ } - else console.log(' (skip removal: target joinedByUsers empty)') - await admin.auth().updateUser(QA, {}).catch(() => {}) // no-op; don't delete a real member - console.log(bad === 0 ? '=== Tier-2 PASS: own-uid-only enforced (foreign/spoof/removal denied) ===' : `=== Tier-2 FAIL: ${bad} unexpected ===`) - process.exit(bad === 0 ? 0 : 1) -})().catch((e) => { console.error('FATAL', e.message); process.exit(2) }) diff --git a/scratchpad/rules_pos.js b/scratchpad/rules_pos.js deleted file mode 100644 index 456685fb..00000000 --- a/scratchpad/rules_pos.js +++ /dev/null @@ -1,22 +0,0 @@ -const admin=require('firebase-admin'),path=require('path') -const SA=path.join(__dirname,'..','closer-app-22014-firebase-adminsdk-fbsvc-ed20bf6003.json') -const PROJECT='closer-app-22014',APIKEY='AIzaSyDAD7FnEYzhMsil41SzJ1XMjUNnJWmjie8',COUPLE='Xal3Kw3gjSdn0niERYKJ' -const QA='Y05AKO2IlTPMa0JQW1BiNIM0uzK2' -admin.initializeApp({credential:admin.credential.cert(require(SA))}); const db=admin.firestore() -const base=`https://firestore.googleapis.com/v1/projects/${PROJECT}/databases/(default)/documents` -const arr=u=>({arrayValue:u.length?{values:u.map(x=>({stringValue:x}))}:{}}) -;(async()=>{ - const custom=await admin.auth().createCustomToken(QA) - const tr=await fetch(`https://identitytoolkit.googleapis.com/v1/accounts:signInWithCustomToken?key=${APIKEY}`,{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({token:custom,returnSecureToken:true})}) - const tok=(await tr.json()).idToken - const snap=await db.collection('couples').doc(COUPLE).collection('sessions').get() - let pos=null,fc=null - snap.forEach(d=>{const x=d.data(); const j=x.joinedByUsers||[]; if(!pos && d.id!=='_active' && !j.includes(QA)) pos={id:d.id,j}; if(!fc && d.id!=='_active') fc={id:d.id,c:x.completedByUsers||[]}}) - if(pos){ - const r=await fetch(`${base}/couples/${COUPLE}/sessions/${pos.id}?updateMask.fieldPaths=joinedByUsers`,{method:'PATCH',headers:{Authorization:`Bearer ${tok}`,'Content-Type':'application/json'},body:JSON.stringify({fields:{joinedByUsers:arr([...new Set([...pos.j,QA])])}})}) - console.log(` ${r.status===200?'✅':'❌❌'} own-uid add joinedByUsers: ${r.status===200?'ALLOWED(200)':'DENIED('+r.status+')'} (expected allow) [${pos.id}]`) - } else console.log(' (no session without QA in joinedByUsers to test own-add)') - const r2=await fetch(`${base}/couples/${COUPLE}/sessions/${fc.id}?updateMask.fieldPaths=completedByUsers`,{method:'PATCH',headers:{Authorization:`Bearer ${tok}`,'Content-Type':'application/json'},body:JSON.stringify({fields:{completedByUsers:arr([...new Set([...fc.c,'bogus-'+Date.now()])])}})}) - console.log(` ${r2.status!==200?'✅':'❌❌'} foreign-add completedByUsers: ${r2.status!==200?'DENIED('+r2.status+')':'ALLOWED(200)'} (expected deny) [${fc.id}]`) - process.exit(0) -})().catch(e=>{console.error('FATAL',e.message);process.exit(2)})