// Node.js 24+ receiver. Set TURBOPULSA_WEBHOOK_SECRET; persist DB on your server. import http from 'node:http' import crypto from 'node:crypto' import {DatabaseSync} from 'node:sqlite' const secret=process.env.TURBOPULSA_WEBHOOK_SECRET if(!secret)throw new Error('Set webhook secret in server environment.') const db=new DatabaseSync(process.env.TURBOPULSA_WEBHOOK_DB||'turbopulsa-webhook.sqlite') db.exec('CREATE TABLE IF NOT EXISTS events(id TEXT PRIMARY KEY); CREATE TABLE IF NOT EXISTS orders(id TEXT PRIMARY KEY, version INTEGER NOT NULL, status TEXT NOT NULL, client_reference TEXT NOT NULL)') export function verify(body,header,now=Date.now()){ const m=/^t=(\d+),v1=([a-f0-9]{64})$/.exec(header||'') if(!m||Math.abs(now/1000-Number(m[1]))>300)return false const expected=crypto.createHmac('sha256',secret).update(m[1]+'.').update(body).digest() return crypto.timingSafeEqual(expected,Buffer.from(m[2],'hex')) } export function applyEvent(e){ if(e.type!=='order.status_changed'||!/^[-a-f0-9]{36}$/.test(e.id)||!/^[-a-f0-9]{36}$/.test(e.data?.id)||!Number.isSafeInteger(e.data?.version)||e.data.version<1||typeof e.data?.status!=='string'||typeof e.data?.client_reference!=='string')return false db.exec('BEGIN IMMEDIATE') try{ if(db.prepare('SELECT id FROM events WHERE id=?').get(e.id)){db.exec('COMMIT');return true} db.prepare('INSERT INTO events(id) VALUES(?)').run(e.id) db.prepare('INSERT INTO orders(id,version,status,client_reference) VALUES(?,?,?,?) ON CONFLICT(id) DO UPDATE SET version=excluded.version,status=excluded.status,client_reference=excluded.client_reference WHERE excluded.version>orders.version').run(e.data.id,e.data.version,e.data.status,e.data.client_reference) db.exec('COMMIT');return true }catch{db.exec('ROLLBACK');return false} } const server=http.createServer(async(req,res)=>{ if(req.method!=='POST'||req.url!=='/webhooks/turbopulsa'){res.writeHead(404);res.end();return} const chunks=[];let bytes=0 for await(const chunk of req){bytes+=chunk.length;if(bytes>32768){res.writeHead(413);res.end();return}chunks.push(chunk)} const raw=Buffer.concat(chunks) if(!verify(raw,req.headers['x-turbopulsa-signature'])){res.writeHead(401);res.end();return} let event;try{event=JSON.parse(raw.toString('utf8'))}catch{res.writeHead(400);res.end();return} const ok=applyEvent(event) res.writeHead(ok?200:503,{'Content-Type':'application/json'});res.end(JSON.stringify({ok})) // A webhook updates local status only. It must NEVER create another purchase. }) server.listen(Number(process.env.PORT||8080),'127.0.0.1')