// /home/def/IPFSapp/backend/expire.js // Unpins CIDs whose payments.expires_at < now(); marks status='expired'. // Uses the same .env as server.js (dotenv). import 'dotenv/config'; import { Pool } from 'pg'; const { DATABASE_URL, IPFS_API = 'http://127.0.0.1:5001/api/v0' } = process.env; if (!DATABASE_URL) { console.error('Missing DATABASE_URL'); process.exit(1); } const pool = new Pool({ connectionString: DATABASE_URL }); const sql = (q, p=[]) => pool.query(q, p); const nowIso = () => new Date().toISOString(); async function fetchJSON(url, opts = {}) { const r = await fetch(url, opts); if (!r.ok) throw new Error(`${url} → ${r.status}`); try { return await r.json(); } catch { return {}; } } async function unpinCid(cid) { // POST /api/v0/pin/rm?arg=&recursive=true const url = `${IPFS_API}/pin/rm?arg=${encodeURIComponent(cid)}&recursive=true`; await fetchJSON(url, { method: 'POST' }); } async function gcRepo() { // Trigger a GC pass (optional; Kubo also GC’s with --enable-gc) try { await fetchJSON(`${IPFS_API}/repo/gc`, { method: 'POST' }); } catch {} } async function run() { console.log(`[expirer] start ${nowIso()}`); // pick up items with expired retention that are still considered kept const { rows } = await sql(` SELECT quote_id, cid FROM payments WHERE cid IS NOT NULL AND expires_at IS NOT NULL AND expires_at < now() AND status IN ('paid','confirmed') ORDER BY expires_at ASC LIMIT 500 `); if (!rows.length) { console.log('[expirer] nothing to expire'); process.exit(0); } let ok = 0, fail = 0; for (const r of rows) { try { await unpinCid(r.cid); await sql(`UPDATE payments SET status='expired' WHERE quote_id=$1`, [r.quote_id]); console.log(`[expirer] expired ${r.cid} (quote ${r.quote_id})`); ok++; } catch (e) { console.error(`[expirer] failed ${r.cid}: ${e.message || e}`); fail++; } } await gcRepo(); console.log(`[expirer] done ok=${ok} fail=${fail} ${nowIso()}`); process.exit(0); } run().catch(e => { console.error('[expirer] fatal', e); process.exit(1); });