Bun
Stack: Bun ≥1.1 + zfiscoo-sdk (ESM nativo).
bun add zfiscoo-sdkServidor com webhook + emissão
server.ts
import { ZfiscooClient } from 'zfiscoo-sdk';
import crypto from 'node:crypto';
const zfiscoo = new ZfiscooClient({
apiUrl: Bun.env.ZFISCOO_API_URL!,
apiKey: Bun.env.ZFISCOO_API_KEY!,
});
const seen = new Set<string>();
Bun.serve({
port: 3000,
async fetch(req) {
const url = new URL(req.url);
if (url.pathname === '/emitir' && req.method === 'POST') {
const pedido = await req.json();
const result = await zfiscoo.nfce.create({
body: {
issuer_id: Bun.env.ZFISCOO_ISSUER_ID!,
external_ref: pedido.id,
items: pedido.items,
payment: pedido.payment,
},
idempotencyKey: `nfce:${pedido.id}`,
});
return Response.json(result);
}
if (url.pathname === '/zfiscoo/webhook' && req.method === 'POST') {
const raw = await req.text();
const signature = req.headers.get('x-signature') ?? '';
const expected = crypto
.createHmac('sha256', Bun.env.ZFISCOO_WEBHOOK_SECRET!)
.update(raw, 'utf8')
.digest('hex');
const received = signature.replace(/^sha256=/, '');
if (
expected.length !== received.length ||
!crypto.timingSafeEqual(
Buffer.from(expected, 'hex'),
Buffer.from(received, 'hex'),
)
) {
return new Response('invalid signature', { status: 401 });
}
const event = JSON.parse(raw);
if (seen.has(event.id)) return Response.json({ ok: true, deduped: true });
seen.add(event.id);
// Process event
console.log(`[zfiscoo] ${event.type} → ${event.data.id}`);
return Response.json({ ok: true });
}
return new Response('Not Found', { status: 404 });
},
});
console.log('listening on 3000');Bun usa Bun.env em vez de process.env, mas o crypto é compatível Node.
Em prod, troca o Set por Redis ou KV (de-dupe persiste reboot).