← back to Secrets Manager
stage/swap-prod-bb-key.mjs
71 lines
#!/usr/bin/env node
// One-shot: swap Kamatera PROD all-designerwallcoverings onto its DEDICATED Browserbase key.
// Secrets are read from a chmod-600 file (BB_STAGE_FILE), NEVER from argv/command line.
//
// printf 'BROWSERBASE_API_KEY=bb_live_NEW\nBROWSERBASE_PROJECT_ID=NEWUUID\n' > stage/new-bb-prod.env
// chmod 600 stage/new-bb-prod.env
// node stage/swap-prod-bb-key.mjs
//
// Does: validate new key vs Browserbase API -> write prod .env via ssh+tee (stdin) ->
// pm2 restart --update-env -> one live verify scrape -> confirm old key gone.
import fs from 'node:fs';
import https from 'node:https';
import { spawnSync } from 'node:child_process';
const HOST = '45.61.58.125', USER = 'root';
const PROD_ENV = '/root/Projects/all-designerwallcoverings/.env';
const PM2 = 'all-designerwallcoverings';
const STAGE = process.env.BB_STAGE_FILE || new URL('./new-bb-prod.env', import.meta.url).pathname;
const VERIFY_SKU = process.env.VERIFY_SKU || 'DWQW-56261';
const last4 = v => '…' + String(v).slice(-4);
function parseEnv(txt) { const kv={}; for (const l of txt.split('\n')) { const m=l.match(/^([A-Z0-9_]+)=(.*)$/); if(m) kv[m[1]]=m[2].replace(/^["']|["']$/g,''); } return kv; }
function ssh(cmd, stdin) { return spawnSync('ssh', ['-o','StrictHostKeyChecking=accept-new',`${USER}@${HOST}`, cmd], { input: stdin, encoding: 'utf8' }); }
function bbVerify(key) { return new Promise(res => { const r=https.request('https://api.browserbase.com/v1/projects',{headers:{'x-bb-api-key':key}},x=>{let b='';x.on('data',d=>b+=d);x.on('end',()=>res({code:x.statusCode,body:b}));}); r.on('error',e=>res({code:0,body:e.message})); r.end(); }); }
(async () => {
if (!fs.existsSync(STAGE)) { console.error(`✗ stage file missing: ${STAGE}\n Create it (chmod 600) with the new key+project, then re-run.`); process.exit(2); }
const nw = parseEnv(fs.readFileSync(STAGE,'utf8'));
const NEWKEY = nw.BROWSERBASE_API_KEY, NEWPROJ = nw.BROWSERBASE_PROJECT_ID;
if (!NEWKEY || !NEWPROJ) { console.error('✗ stage file needs BOTH BROWSERBASE_API_KEY and BROWSERBASE_PROJECT_ID'); process.exit(2); }
console.log(`new key ${last4(NEWKEY)} · new project ${last4(NEWPROJ)}`);
// 1. validate the new key against Browserbase
const v = await bbVerify(NEWKEY);
if (v.code !== 200) { console.error(`✗ new key failed Browserbase verify (HTTP ${v.code}). Aborting — prod untouched.`); process.exit(1); }
const projOk = v.body.includes(NEWPROJ);
console.log(`✓ new key valid (HTTP 200)${projOk?` · project ${last4(NEWPROJ)} visible`:' · ⚠ project id not in project list — check it'}`);
// 2. read prod .env, swap the two keys, preserve everything else
const cur = ssh(`cat ${PROD_ENV}`);
if (cur.status !== 0) { console.error('✗ could not read prod .env:', (cur.stderr||'').slice(0,200)); process.exit(1); }
const oldKey = parseEnv(cur.stdout).BROWSERBASE_API_KEY || '(none)';
let lines = cur.stdout.split('\n'); const seen=new Set();
lines = lines.map(l => { const m=l.match(/^(BROWSERBASE_API_KEY|BROWSERBASE_PROJECT_ID)=/); if(!m) return l; seen.add(m[1]); return m[1]==='BROWSERBASE_API_KEY'?`BROWSERBASE_API_KEY=${NEWKEY}`:`BROWSERBASE_PROJECT_ID=${NEWPROJ}`; });
if(!seen.has('BROWSERBASE_API_KEY')) lines.push(`BROWSERBASE_API_KEY=${NEWKEY}`);
if(!seen.has('BROWSERBASE_PROJECT_ID')) lines.push(`BROWSERBASE_PROJECT_ID=${NEWPROJ}`);
const body = lines.join('\n').replace(/\n+$/,'')+'\n';
// 3. write back via ssh+tee over stdin (secret never on the command line), chmod 600
const w = ssh(`cat > ${PROD_ENV} && chmod 600 ${PROD_ENV}`, body);
if (w.status !== 0) { console.error('✗ prod .env write failed:', (w.stderr||'').slice(0,200)); process.exit(1); }
console.log(`✓ prod .env swapped ${last4(oldKey)} → ${last4(NEWKEY)} (other vars preserved)`);
// 4. restart pm2 with fresh env
const rs = ssh(`cd /root/Projects/all-designerwallcoverings && pm2 restart ${PM2} --update-env && sleep 2 && curl -sf http://127.0.0.1:9958/healthz`);
console.log(rs.status===0 ? '✓ pm2 restarted + healthz ok' : '⚠ pm2 restart/health check: '+(rs.stderr||rs.stdout||'').slice(0,200));
// 5. confirm old shared key is GONE from prod
const after = parseEnv(ssh(`cat ${PROD_ENV}`).stdout);
console.log(after.BROWSERBASE_API_KEY===NEWKEY ? `✓ prod now on dedicated key ${last4(NEWKEY)}; shared ${last4(oldKey)} gone` : '✗ prod key mismatch after write — inspect manually');
// 6. one live verify scrape through the public endpoint (~$0.10)
console.log(`\nrunning ONE live verify scrape (SKU ${VERIFY_SKU}, ~$0.10)…`);
const c = spawnSync('curl',['-s','-u','admin:DW2024!',`https://all.designerwallcoverings.com/api/livestock?sku=${VERIFY_SKU}&live=1`],{encoding:'utf8'});
try { const j=JSON.parse(c.stdout); console.log(' live:',j.live,'| in_stock:',j.in_stock,'| source:',j.source,'| cost_usd:',j.cost_usd,'| cached:',j.cached); console.log(j.live===true?'✓ prod live-check WORKS on the dedicated key':'⚠ live not true — inspect: '+c.stdout.slice(0,200)); }
catch(e){ console.log(' raw:', c.stdout.slice(0,300)); }
// 7. wipe the stage file so the plaintext key doesn't linger
fs.rmSync(STAGE,{force:true}); console.log(`\n✓ stage file wiped. Rotation complete.`);
})();