// Node.js 22.18+ (native TypeScript), no dependencies. import { readFile, writeFile } from 'node:fs/promises'; import { createHash, randomUUID } from 'node:crypto'; async function main() { const [input, output] = process.argv.slice(2); if (!input || !output) throw Error('Usage: node generate.ts invoice.json invoice.xml'); const source = await readFile(input); if (source.length > 5 * 1024 * 1024) throw Error('Input exceeds 5 MiB'); const base = (process.env.FINANCEWOLF_API_BASE || 'https://api.ironfang.uk/financewolf').replace(/\/$/, ''); const headers: Record = { 'Content-Type': 'application/json', Accept: 'application/json' }; if (process.env.IRONFANG_API_KEY) { headers.Authorization = 'Bearer ' + process.env.IRONFANG_API_KEY; headers['Idempotency-Key'] = process.env.FINANCEWOLF_IDEMPOTENCY_KEY || randomUUID(); } const response = await fetch(base + '/v1/einvoices/generate?ruleset=latest&profile=peppol-bis-billing-3', { method: 'POST', headers, body: source, signal: AbortSignal.timeout(30000), redirect: 'error' }); const chunks: Uint8Array[] = []; let length = 0; if (!response.body) throw Error('Empty response'); for await (const chunk of response.body) { length += chunk.length; if (length > (response.ok ? 12 * 1024 * 1024 : 65536)) throw Error('Response exceeds size limit'); chunks.push(chunk); } const raw = Buffer.concat(chunks).toString('utf8'); if (!response.ok) throw Error(`HTTP ${response.status}: ${raw}`); if (response.headers.get('Content-Type')?.split(';')[0] !== 'application/json') throw Error('Unexpected response type'); const result = JSON.parse(raw); const validation = result.validation, artifact = result.artifact; if (result.schema !== 'financewolf/einvoice/generation-result/v1' || result.status !== 'completed' || result.outcome !== 'valid' || validation.outcome !== 'valid' || validation.layers.map((l: { layer: string }) => l.layer).join(',') !== 'input,xml,xsd,en16931,peppol' || validation.layers.some((l: { status: string }) => l.status !== 'passed')) throw Error('Generation did not pass every validation layer'); const xml = Buffer.from(artifact.data_base64, 'base64'); const digest = createHash('sha256').update(xml).digest('hex'); if (!xml.length || xml.length > 5 * 1024 * 1024 || xml.length !== artifact.bytes || digest !== artifact.sha256 || digest !== validation.input.sha256 || xml.length !== validation.input.bytes) throw Error('XML does not match its validation hash/length'); await writeFile(output, xml, { mode: 0o600 }); console.log(JSON.stringify({ xml: output, sha256: digest, ruleset: validation.ruleset.id, operation_id: result.operation_id })); } main().catch(error => { console.error(error.message); process.exitCode = 1; });