← Back
/** Detach the Variant A "polycopy-orders-only" policy from every embedded wallet that carries
 *  a policy (restores pre-15-Jun behaviour: auto-redeem / wrap / approve work again).
 *
 *  ⚠️ Variant B (Rick, 16 Jun): fast restore. This RE-OPENS the withdraw vector — the
 *  delegate can move funds again. Revisit policies later (secure-redeem plan). See MEMORY.
 *
 *  Enumerates our own users via the copy /wallets feed (Privy has no app-wide wallet list),
 *  resolves each embedded wallet id via Privy getUser, reads its policy_ids, detaches.
 *
 *  Modes (default = DRY, lists wallets + their policy_ids only):
 *    SUBS_URL=… SERVICE_TOKEN=… node privy-detach.mjs            → list, no change
 *    APPLY=1 SUBS_URL=… SERVICE_TOKEN=… node privy-detach.mjs    → set policyIds:[] on policied wallets
 *
 *  Run on the main/DE server (.env.privy + reachable poly-dev API). */
import fs from 'node:fs';
import { PrivyClient } from '@privy-io/server-auth';

const APPLY = process.env.APPLY === '1';
const SUBS_URL = process.env.SUBS_URL || 'https://poly-dev.szhub.space/api/copy/active';

function loadEnv() {
  const env = {};
  const url = fs.existsSync(new URL('../.env.privy', import.meta.url)) ? new URL('../.env.privy', import.meta.url) : new URL('./.env.privy', import.meta.url);
  for (const l of fs.readFileSync(url, 'utf8').split('\n')) { const t = l.trim(); if (!t || t.startsWith('#')) continue; const i = t.indexOf('='); if (i > 0) env[t.slice(0, i).trim()] = t.slice(i + 1).trim(); }
  return env;
}
const env = loadEnv();
const APP_ID = env.PRIVY_APP_ID, APP_SECRET = env.PRIVY_APP_SECRET;
const SERVICE_TOKEN = process.env.SERVICE_TOKEN || env.COPY_SERVICE_TOKEN;
if (!APP_ID || !APP_SECRET) { console.log('missing PRIVY_APP_ID / PRIVY_APP_SECRET'); process.exit(1); }
const baseHeaders = { 'content-type': 'application/json', 'privy-app-id': APP_ID, authorization: 'Basic ' + Buffer.from(`${APP_ID}:${APP_SECRET}`).toString('base64') };
const privy = new PrivyClient(APP_ID, APP_SECRET, { walletApi: { authorizationPrivateKey: env.PRIVY_AUTHORIZATION_KEY } });

async function listUsers() {
  const out = new Set();
  for (const path of ['/wallets', '/active']) {
    try {
      const url = SUBS_URL.replace(/\/active$/, path);
      const r = await fetch(url, { headers: { 'x-service-token': SERVICE_TOKEN }, signal: AbortSignal.timeout(12000) });
      if (!r.ok) { console.log(`[feed ${path}] http`, r.status); continue; }
      const d = await r.json();
      for (const s of (d.data || [])) if (s.userAddress) out.add(String(s.userAddress).toLowerCase());
    } catch (e) { console.log(`[feed ${path}] err`, e.message); }
  }
  return [...out];
}

const users = await listUsers();
console.log(`users from feed: ${users.length}`);
const affected = [];
for (const ua of users) {
  let walletId = null, addr = null, walletPolicies = [], signers = [];
  try {
    const pu = await privy.getUser(ua);
    const w = (pu.linkedAccounts || []).find((a) => a.type === 'wallet' && a.walletClientType === 'privy');
    if (!w) { console.log(`  ${ua.slice(0, 20)} — no privy wallet`); continue; }
    walletId = w.id; addr = w.address;
  } catch (e) { console.log(`  ${ua.slice(0, 20)} getUser err: ${e.message}`); continue; }
  if (!walletId) { console.log(`  ${ua.slice(0, 20)} (${addr}) — wallet has no id field`); continue; }
  try {
    const r = await fetch(`https://api.privy.io/v1/wallets/${walletId}`, { headers: baseHeaders });
    const wd = await r.json().catch(() => ({}));
    walletPolicies = wd.policy_ids || [];
    signers = wd.additional_signers || [];
  } catch (e) { console.log(`  ${walletId} GET wallet err: ${e.message}`); }
  // The Variant A policy is attached as an override on the server-delegate additional signer.
  const policiedSigners = signers.filter((s) => Array.isArray(s.override_policy_ids) && s.override_policy_ids.length);
  const hasRestriction = walletPolicies.length || policiedSigners.length;
  const detail = `policy_ids=${JSON.stringify(walletPolicies)} signers=[${signers.map((s) => `${s.signer_id.slice(0, 8)}:override=${JSON.stringify(s.override_policy_ids || [])}`).join(', ')}]`;
  console.log(`  ${ua.slice(0, 20)} wallet=${walletId} ${addr} ${hasRestriction ? '🔒' : 'open'} ${detail}`);
  if (hasRestriction) affected.push({ ua, walletId, addr, signers });
}

console.log(`\n${affected.length} wallet(s) carry a restriction.`);
if (!affected.length) { console.log('nothing to detach.'); process.exit(0); }
if (!APPLY) { console.log('DRY — re-run with APPLY=1 to clear wallet policy + signer override policies (keeps delegate as signer).'); process.exit(0); }

for (const w of affected) {
  // Keep every additional signer, but strip its override policies so the delegate can sign
  // redeem/approve/wrap again. Also clear any wallet-level policy_ids.
  const additionalSigners = (w.signers || []).map((s) => ({ signerId: s.signer_id, overridePolicyIds: [] }));
  try {
    const res = await privy.walletApi.updateWallet({ id: w.walletId, policyIds: [], additionalSigners });
    console.log(`✅ detached ${w.walletId} (${w.addr}) → signers=${JSON.stringify(res?.additionalSigners ?? res?.additional_signers ?? [])}`);
  } catch (e) { console.log(`❌ updateWallet failed ${w.walletId}: ${e?.message || e}`); }
}
console.log('\nVerify next: run deposit-watch LIVE → redeem must CONFIRM (no policy-violation).');