vat-validator-mcp 2.0.1 → 2.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "vat-validator-mcp",
3
3
  "mcpName": "io.github.OjasKord/vat-validator-mcp",
4
- "version": "2.0.1",
4
+ "version": "2.0.3",
5
5
  "description": "VAT number validator for AI agents. EU VIES, UK HMRC, AU ABR — auto-detects jurisdiction. Fraud risk scoring and invoice name cross-check in one call.",
6
6
  "main": "src/server.js",
7
7
  "scripts": {
package/privacy.html ADDED
@@ -0,0 +1,501 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>Privacy Policy — Kord Agencies</title>
7
+ <link rel="preconnect" href="https://fonts.googleapis.com">
8
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
9
+ <link href="https://fonts.googleapis.com/css2?family=DM+Mono:ital,wght@0,300;0,400;0,500;1,300;1,400&display=swap" rel="stylesheet">
10
+ <style>
11
+ *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
12
+
13
+ :root {
14
+ --bg: #070910;
15
+ --bg-2: #0C1018;
16
+ --bg-3: #111722;
17
+ --teal: #00E5C3;
18
+ --amber: #F0A030;
19
+ --text: #E2E8F2;
20
+ --text-2: #8895AA;
21
+ --text-3: #505A6A;
22
+ --border: rgba(255,255,255,0.06);
23
+ --border-2: rgba(255,255,255,0.12);
24
+ --teal-dim: rgba(0,229,195,0.08);
25
+ --teal-border: rgba(0,229,195,0.25);
26
+ }
27
+
28
+ html { font-size: 14px; }
29
+
30
+ body {
31
+ background: var(--bg);
32
+ color: var(--text);
33
+ font-family: 'DM Mono', monospace;
34
+ line-height: 1.7;
35
+ min-height: 100vh;
36
+ }
37
+
38
+ /* ── Nav ── */
39
+ nav {
40
+ height: 44px;
41
+ border-bottom: 1px solid var(--border);
42
+ display: flex;
43
+ align-items: center;
44
+ padding: 0 1.5rem;
45
+ position: sticky;
46
+ top: 0;
47
+ background: var(--bg);
48
+ z-index: 10;
49
+ }
50
+ nav a {
51
+ color: var(--teal);
52
+ text-decoration: none;
53
+ font-size: 11px;
54
+ letter-spacing: 0.1em;
55
+ text-transform: uppercase;
56
+ }
57
+ nav .sep {
58
+ color: var(--text-3);
59
+ margin: 0 0.6rem;
60
+ font-size: 11px;
61
+ }
62
+ nav .current {
63
+ color: var(--text-2);
64
+ font-size: 11px;
65
+ letter-spacing: 0.05em;
66
+ }
67
+
68
+ /* ── Layout ── */
69
+ .wrap {
70
+ max-width: 780px;
71
+ margin: 0 auto;
72
+ padding: 3rem 1.5rem 5rem;
73
+ }
74
+
75
+ /* ── Header ── */
76
+ .page-header {
77
+ border-bottom: 1px solid var(--border);
78
+ padding-bottom: 2rem;
79
+ margin-bottom: 2.5rem;
80
+ }
81
+ .label {
82
+ font-size: 10px;
83
+ letter-spacing: 0.15em;
84
+ text-transform: uppercase;
85
+ color: var(--teal);
86
+ margin-bottom: 0.75rem;
87
+ }
88
+ h1 {
89
+ font-size: 1.4rem;
90
+ font-weight: 400;
91
+ color: var(--text);
92
+ letter-spacing: -0.01em;
93
+ margin-bottom: 0.75rem;
94
+ }
95
+ .meta {
96
+ font-size: 11px;
97
+ color: var(--text-3);
98
+ letter-spacing: 0.05em;
99
+ }
100
+ .meta span { color: var(--text-2); }
101
+
102
+ /* ── Sections ── */
103
+ section {
104
+ margin-bottom: 2.5rem;
105
+ padding-bottom: 2.5rem;
106
+ border-bottom: 1px solid var(--border);
107
+ }
108
+ section:last-of-type {
109
+ border-bottom: none;
110
+ }
111
+ h2 {
112
+ font-size: 11px;
113
+ font-weight: 500;
114
+ letter-spacing: 0.12em;
115
+ text-transform: uppercase;
116
+ color: var(--teal);
117
+ margin-bottom: 1rem;
118
+ }
119
+ p {
120
+ font-size: 13px;
121
+ color: var(--text-2);
122
+ margin-bottom: 0.9rem;
123
+ line-height: 1.75;
124
+ }
125
+ p:last-child { margin-bottom: 0; }
126
+
127
+ /* ── Callout box ── */
128
+ .callout {
129
+ background: var(--teal-dim);
130
+ border: 1px solid var(--teal-border);
131
+ border-radius: 4px;
132
+ padding: 1rem 1.25rem;
133
+ margin: 1.25rem 0;
134
+ font-size: 12px;
135
+ color: var(--text);
136
+ line-height: 1.7;
137
+ }
138
+ .callout strong {
139
+ color: var(--teal);
140
+ font-weight: 500;
141
+ }
142
+
143
+ /* ── Data table ── */
144
+ .data-table {
145
+ width: 100%;
146
+ border-collapse: collapse;
147
+ font-size: 12px;
148
+ margin: 1rem 0;
149
+ }
150
+ .data-table th {
151
+ text-align: left;
152
+ font-size: 10px;
153
+ letter-spacing: 0.1em;
154
+ text-transform: uppercase;
155
+ color: var(--text-3);
156
+ padding: 0.5rem 0.75rem;
157
+ border-bottom: 1px solid var(--border-2);
158
+ font-weight: 400;
159
+ }
160
+ .data-table td {
161
+ padding: 0.65rem 0.75rem;
162
+ border-bottom: 1px solid var(--border);
163
+ color: var(--text-2);
164
+ vertical-align: top;
165
+ }
166
+ .data-table tr:last-child td { border-bottom: none; }
167
+ .data-table td:first-child { color: var(--text); }
168
+ .tag {
169
+ display: inline-block;
170
+ font-size: 9px;
171
+ letter-spacing: 0.1em;
172
+ text-transform: uppercase;
173
+ padding: 2px 6px;
174
+ border-radius: 3px;
175
+ font-weight: 500;
176
+ }
177
+ .tag-no { background: rgba(248,113,113,0.1); color: #F87171; border: 1px solid rgba(248,113,113,0.2); }
178
+ .tag-yes { background: rgba(0,229,195,0.1); color: var(--teal); border: 1px solid var(--teal-border); }
179
+ .tag-ltd { background: rgba(240,160,48,0.1); color: var(--amber); border: 1px solid rgba(240,160,48,0.2); }
180
+
181
+ /* ── Inline list ── */
182
+ ul.plain {
183
+ list-style: none;
184
+ margin: 0.5rem 0 0.9rem;
185
+ padding: 0;
186
+ }
187
+ ul.plain li {
188
+ font-size: 13px;
189
+ color: var(--text-2);
190
+ padding: 0.3rem 0;
191
+ padding-left: 1rem;
192
+ position: relative;
193
+ }
194
+ ul.plain li::before {
195
+ content: '—';
196
+ position: absolute;
197
+ left: 0;
198
+ color: var(--text-3);
199
+ }
200
+
201
+ /* ── Contact block ── */
202
+ .contact-block {
203
+ background: var(--bg-2);
204
+ border: 1px solid var(--border-2);
205
+ border-radius: 4px;
206
+ padding: 1.25rem;
207
+ font-size: 12px;
208
+ color: var(--text-2);
209
+ line-height: 1.9;
210
+ }
211
+ .contact-block a {
212
+ color: var(--teal);
213
+ text-decoration: none;
214
+ }
215
+ .contact-block a:hover { text-decoration: underline; }
216
+ .contact-block .field {
217
+ display: flex;
218
+ gap: 1rem;
219
+ }
220
+ .contact-block .field-label {
221
+ color: var(--text-3);
222
+ font-size: 10px;
223
+ letter-spacing: 0.1em;
224
+ text-transform: uppercase;
225
+ min-width: 80px;
226
+ padding-top: 1px;
227
+ }
228
+
229
+ /* ── Footer ── */
230
+ footer {
231
+ border-top: 1px solid var(--border);
232
+ padding: 1.5rem;
233
+ text-align: center;
234
+ font-size: 11px;
235
+ color: var(--text-3);
236
+ letter-spacing: 0.05em;
237
+ }
238
+ footer a { color: var(--text-3); text-decoration: none; }
239
+ footer a:hover { color: var(--text-2); }
240
+ </style>
241
+ </head>
242
+ <body>
243
+
244
+ <nav>
245
+ <a href="https://kordagencies.com">kordagencies.com</a>
246
+ <span class="sep">/</span>
247
+ <span class="current">Privacy Policy</span>
248
+ </nav>
249
+
250
+ <div class="wrap">
251
+
252
+ <div class="page-header">
253
+ <div class="label">Legal</div>
254
+ <h1>Privacy Policy</h1>
255
+ <div class="meta">
256
+ Kord Agencies &nbsp;·&nbsp; <span>Last updated May 2026</span>
257
+ </div>
258
+ </div>
259
+
260
+ <!-- 1 -->
261
+ <section>
262
+ <h2>Who we are</h2>
263
+ <p>
264
+ This policy applies to VAT Validator MCP and all services operated by
265
+ <strong style="color:var(--text)">Kord Agencies</strong>, a company incorporated in Singapore.
266
+ </p>
267
+ <div class="contact-block">
268
+ <div class="field"><span class="field-label">Company</span> Kord Agencies, Singapore</div>
269
+ <div class="field"><span class="field-label">Contact</span> <a href="mailto:ojas@kordagencies.com">ojas@kordagencies.com</a></div>
270
+ <div class="field"><span class="field-label">Service</span> VAT Validator MCP — <a href="https://kordagencies.com">kordagencies.com</a></div>
271
+ </div>
272
+ </section>
273
+
274
+ <!-- 2 -->
275
+ <section>
276
+ <h2>What we collect</h2>
277
+
278
+ <div class="callout">
279
+ <strong>VAT numbers you submit are never logged or stored.</strong> Query content — the VAT numbers, company names, and invoice amounts passed to our tools — is transmitted to the relevant government API and immediately discarded. It does not touch our database.
280
+ </div>
281
+
282
+ <table class="data-table">
283
+ <thead>
284
+ <tr>
285
+ <th>Data</th>
286
+ <th>Why</th>
287
+ <th>Stored</th>
288
+ </tr>
289
+ </thead>
290
+ <tbody>
291
+ <tr>
292
+ <td>Email address</td>
293
+ <td>Paid subscribers only — to deliver your API key</td>
294
+ <td><span class="tag tag-yes">Yes</span></td>
295
+ </tr>
296
+ <tr>
297
+ <td>Payment information</td>
298
+ <td>Processed by Stripe — we never see raw card data</td>
299
+ <td><span class="tag tag-no">Stripe only</span></td>
300
+ </tr>
301
+ <tr>
302
+ <td>API key</td>
303
+ <td>Authentication and usage tracking for paid plans</td>
304
+ <td><span class="tag tag-yes">Yes</span></td>
305
+ </tr>
306
+ <tr>
307
+ <td>IP address</td>
308
+ <td>Free tier rate limiting (50 calls/month/IP). Stored as truncated hash, not full address.</td>
309
+ <td><span class="tag tag-ltd">Truncated</span></td>
310
+ </tr>
311
+ <tr>
312
+ <td>Tool name + timestamp</td>
313
+ <td>Aggregate usage statistics (e.g. "validate_vat called at 14:22"). No query content.</td>
314
+ <td><span class="tag tag-ltd">Aggregate</span></td>
315
+ </tr>
316
+ <tr>
317
+ <td>VAT numbers / query content</td>
318
+ <td>Not applicable — not collected</td>
319
+ <td><span class="tag tag-no">Never</span></td>
320
+ </tr>
321
+ </tbody>
322
+ </table>
323
+ </section>
324
+
325
+ <!-- 3 -->
326
+ <section>
327
+ <h2>Where data is stored</h2>
328
+ <p>
329
+ Subscriber API keys and rate-limiting counters are stored in
330
+ <strong style="color:var(--text)">Upstash Redis</strong> (hosted in the United States).
331
+ The application itself runs on <strong style="color:var(--text)">Railway</strong>
332
+ (hosted in the United States). Both providers maintain industry-standard
333
+ encryption at rest and in transit.
334
+ </p>
335
+ <p>
336
+ We do not operate our own database servers. No data is stored in Singapore.
337
+ </p>
338
+ </section>
339
+
340
+ <!-- 4 -->
341
+ <section>
342
+ <h2>Third-party services</h2>
343
+ <p>
344
+ When you call our tools, your VAT number is forwarded to the relevant official
345
+ government registry to perform the lookup. These are read-only API calls — no
346
+ personal data about you is sent to them.
347
+ </p>
348
+ <table class="data-table">
349
+ <thead>
350
+ <tr>
351
+ <th>Third party</th>
352
+ <th>Purpose</th>
353
+ <th>Data sent</th>
354
+ </tr>
355
+ </thead>
356
+ <tbody>
357
+ <tr>
358
+ <td>Stripe</td>
359
+ <td>Payment processing and subscription management</td>
360
+ <td>Name, email, card details (handled entirely by Stripe)</td>
361
+ </tr>
362
+ <tr>
363
+ <td>UK HMRC VAT API v2</td>
364
+ <td>UK VAT number validation</td>
365
+ <td>VAT number only</td>
366
+ </tr>
367
+ <tr>
368
+ <td>EU VIES (ec.europa.eu)</td>
369
+ <td>EU VAT number validation (all 27 member states)</td>
370
+ <td>VAT number only</td>
371
+ </tr>
372
+ <tr>
373
+ <td>Australian ABR (abr.business.gov.au)</td>
374
+ <td>Australian ABN validation</td>
375
+ <td>ABN only</td>
376
+ </tr>
377
+ <tr>
378
+ <td>Anthropic (Claude API)</td>
379
+ <td>AI-powered fraud risk analysis</td>
380
+ <td>Validation result metadata — no raw VAT numbers or personal data</td>
381
+ </tr>
382
+ <tr>
383
+ <td>Upstash Redis</td>
384
+ <td>API key storage and rate limiting</td>
385
+ <td>API keys and truncated IP counters</td>
386
+ </tr>
387
+ <tr>
388
+ <td>Railway</td>
389
+ <td>Application hosting</td>
390
+ <td>Application logs (no query content)</td>
391
+ </tr>
392
+ </tbody>
393
+ </table>
394
+ </section>
395
+
396
+ <!-- 5 -->
397
+ <section>
398
+ <h2>Legal basis and UK GDPR</h2>
399
+ <p>
400
+ Where we process personal data relating to individuals in the UK or EEA, we do so
401
+ under the following legal bases:
402
+ </p>
403
+ <ul class="plain">
404
+ <li><strong style="color:var(--text)">Contract performance</strong> — processing your email and API key to deliver the service you subscribed to.</li>
405
+ <li><strong style="color:var(--text)">Legitimate interests</strong> — rate limiting by truncated IP address to prevent abuse of the free tier.</li>
406
+ <li><strong style="color:var(--text)">Legal obligation</strong> — retaining transaction records as required for tax compliance.</li>
407
+ </ul>
408
+ <p>
409
+ We comply with the UK General Data Protection Regulation (UK GDPR) and the
410
+ Data Protection Act 2018. For EEA residents, we comply with EU GDPR (Regulation
411
+ 2016/679). Transfers of personal data to the United States (Upstash, Railway,
412
+ Anthropic) are made under Standard Contractual Clauses or equivalent adequacy
413
+ mechanisms.
414
+ </p>
415
+ </section>
416
+
417
+ <!-- 6 -->
418
+ <section>
419
+ <h2>Data retention</h2>
420
+ <ul class="plain">
421
+ <li>API keys and associated email addresses are retained for the lifetime of the subscription plus 90 days, then deleted.</li>
422
+ <li>Truncated IP rate-limit counters reset automatically each calendar month.</li>
423
+ <li>Aggregate tool usage statistics (tool name + timestamp, no content) are retained for up to 12 months for service improvement.</li>
424
+ <li>Stripe retains payment records in accordance with their own privacy policy and applicable financial regulations.</li>
425
+ </ul>
426
+ </section>
427
+
428
+ <!-- 7 -->
429
+ <section>
430
+ <h2>Your rights</h2>
431
+ <p>
432
+ Under UK GDPR and EU GDPR, you have the right to access, rectify, or erase
433
+ personal data we hold about you. You also have the right to restrict or object
434
+ to processing, and to data portability where applicable.
435
+ </p>
436
+ <p>
437
+ To exercise any of these rights — including requesting deletion of your API key
438
+ and associated email — contact us at:
439
+ </p>
440
+ <div class="contact-block" style="margin-top:0.75rem">
441
+ <div class="field">
442
+ <span class="field-label">Email</span>
443
+ <a href="mailto:ojas@kordagencies.com">ojas@kordagencies.com</a>
444
+ </div>
445
+ <div class="field">
446
+ <span class="field-label">Response</span>
447
+ Within 30 days of receipt
448
+ </div>
449
+ </div>
450
+ <p style="margin-top:1rem">
451
+ If you are located in the UK, you have the right to lodge a complaint with the
452
+ Information Commissioner's Office (ICO) at
453
+ <a href="https://ico.org.uk" style="color:var(--teal)">ico.org.uk</a>.
454
+ If you are in the EEA, you may contact your local supervisory authority.
455
+ </p>
456
+ </section>
457
+
458
+ <!-- 8 -->
459
+ <section>
460
+ <h2>Cookies and tracking</h2>
461
+ <p>
462
+ The VAT Validator MCP API does not use cookies, browser tracking, analytics
463
+ scripts, or fingerprinting of any kind. This privacy policy page itself does not
464
+ set any cookies.
465
+ </p>
466
+ </section>
467
+
468
+ <!-- 9 -->
469
+ <section>
470
+ <h2>Changes to this policy</h2>
471
+ <p>
472
+ We may update this policy from time to time. The date at the top of this page
473
+ reflects when it was last revised. Material changes will be communicated to
474
+ active subscribers by email.
475
+ </p>
476
+ </section>
477
+
478
+ <!-- 10 -->
479
+ <section>
480
+ <h2>Contact</h2>
481
+ <div class="contact-block">
482
+ <div class="field"><span class="field-label">Company</span> Kord Agencies, Singapore</div>
483
+ <div class="field"><span class="field-label">Email</span> <a href="mailto:ojas@kordagencies.com">ojas@kordagencies.com</a></div>
484
+ <div class="field"><span class="field-label">Website</span> <a href="https://kordagencies.com">kordagencies.com</a></div>
485
+ </div>
486
+ </section>
487
+
488
+ </div><!-- /wrap -->
489
+
490
+ <footer>
491
+ <a href="https://kordagencies.com">kordagencies.com</a>
492
+ &nbsp;·&nbsp;
493
+ <a href="https://kordagencies.com/terms.html">Terms</a>
494
+ &nbsp;·&nbsp;
495
+ Privacy Policy
496
+ &nbsp;·&nbsp;
497
+ &copy; 2026 Kord Agencies
498
+ </footer>
499
+
500
+ </body>
501
+ </html>
package/server.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "name": "io.github.OjasKord/vat-validator-mcp",
4
4
  "title": "VAT Validator MCP",
5
5
  "description": "Validate EU, UK, AU VAT numbers for AI agents. EU ViDA e-invoicing compliance.",
6
- "version": "1.4.13",
6
+ "version": "2.0.2",
7
7
  "websiteUrl": "https://kordagencies.com",
8
8
  "repository": {
9
9
  "url": "https://github.com/OjasKord/vat-validator-mcp",
@@ -13,7 +13,7 @@
13
13
  {
14
14
  "registryType": "npm",
15
15
  "identifier": "vat-validator-mcp",
16
- "version": "1.4.13",
16
+ "version": "2.0.2",
17
17
  "transport": { "type": "stdio" },
18
18
  "environmentVariables": [
19
19
  { "name": "ANTHROPIC_API_KEY", "description": "Anthropic API key for AI-powered fraud risk analysis", "isRequired": true, "isSecret": true },
package/src/server.js CHANGED
@@ -2,11 +2,22 @@ const http = require('http');
2
2
  const https = require('https');
3
3
  const crypto = require('crypto');
4
4
  const fs = require('fs');
5
+ const path = require('path');
5
6
  const Stripe = require('stripe');
6
7
  const stripe = Stripe(process.env.STRIPE_SECRET_KEY);
7
8
 
8
9
  const PERSIST_FILE = '/tmp/vat_stats.json';
9
- const VERSION = '2.0.1';
10
+ const VERSION = '2.0.3';
11
+
12
+ // Persistent device ID for HMRC fraud prevention headers (BATCH_PROCESS_DIRECT)
13
+ const DEVICE_ID_FILE = path.join(__dirname, '..', 'device-id.txt');
14
+ let DEVICE_ID;
15
+ try {
16
+ DEVICE_ID = fs.readFileSync(DEVICE_ID_FILE, 'utf8').trim();
17
+ } catch(e) {
18
+ DEVICE_ID = crypto.randomUUID();
19
+ try { fs.writeFileSync(DEVICE_ID_FILE, DEVICE_ID); } catch(we) {}
20
+ }
10
21
  const RESEND_API_KEY = process.env.RESEND_API_KEY || '';
11
22
  const ANTHROPIC_API_KEY = process.env.ANTHROPIC_API_KEY || '';
12
23
  const PORT = process.env.PORT || 3000;
@@ -200,6 +211,31 @@ async function validateVIES(countryCode, vatNumber) {
200
211
  });
201
212
  }
202
213
 
214
+ async function hmrcFetchWithRetry(url, options, maxRetries = 3) {
215
+ for (let attempt = 1; attempt <= maxRetries; attempt++) {
216
+ const response = await fetch(url, options);
217
+ if (response.status !== 429) return response;
218
+ if (attempt === maxRetries) return response;
219
+ await new Promise(resolve => setTimeout(resolve, attempt * 1000));
220
+ }
221
+ }
222
+
223
+ function getFraudPreventionHeaders() {
224
+ return {
225
+ 'Gov-Client-Connection-Method': 'BATCH_PROCESS_DIRECT',
226
+ 'Gov-Client-Device-ID': DEVICE_ID,
227
+ 'Gov-Client-Local-IPs': '127.0.0.1',
228
+ 'Gov-Client-Local-IPs-Timestamp': new Date().toISOString().replace(/(\.\d{3})\d*Z/, '$1Z'),
229
+ 'Gov-Client-MAC-Addresses': 'not-applicable',
230
+ 'Gov-Client-Timezone': 'UTC+00:00',
231
+ 'Gov-Client-User-Agent': 'os-family=Linux&os-version=Server&device-manufacturer=Railway&device-model=Cloud',
232
+ 'Gov-Client-User-IDs': 'os=railway-service',
233
+ 'Gov-Vendor-License-IDs': 'vat-validator-mcp=not-applicable',
234
+ 'Gov-Vendor-Product-Name': 'VAT%20Validator%20MCP',
235
+ 'Gov-Vendor-Version': 'vat-validator-mcp=2.0.3'
236
+ };
237
+ }
238
+
203
239
  // HMRC OAuth 2.0 token cache
204
240
  let hmrcToken = null;
205
241
  let hmrcTokenExpiry = 0;
@@ -218,32 +254,23 @@ async function getHMRCToken() {
218
254
 
219
255
  const body = `client_secret=${encodeURIComponent(clientSecret)}&client_id=${encodeURIComponent(clientId)}&grant_type=client_credentials&scope=read%3Avat`;
220
256
 
221
- return new Promise((resolve) => {
222
- const req = https.request({
223
- hostname,
224
- path: '/oauth/token',
257
+ try {
258
+ const response = await hmrcFetchWithRetry(`https://${hostname}/oauth/token`, {
225
259
  method: 'POST',
226
- headers: { 'Content-Type': 'application/x-www-form-urlencoded', 'Content-Length': Buffer.byteLength(body) }
227
- }, res => {
228
- let d = ''; res.on('data', c => d += c);
229
- res.on('end', () => {
230
- try {
231
- const json = JSON.parse(d);
232
- if (json.access_token) {
233
- hmrcToken = json.access_token;
234
- hmrcTokenExpiry = now + (json.expires_in || 14400) * 1000;
235
- resolve(hmrcToken);
236
- } else {
237
- resolve(null);
238
- }
239
- } catch(e) { resolve(null); }
240
- });
260
+ headers: { 'Content-Type': 'application/x-www-form-urlencoded', ...getFraudPreventionHeaders() },
261
+ body,
262
+ signal: AbortSignal.timeout(8000)
241
263
  });
242
- req.on('error', () => resolve(null));
243
- req.setTimeout(8000, () => { req.destroy(); resolve(null); });
244
- req.write(body);
245
- req.end();
246
- });
264
+ const json = await response.json();
265
+ if (json.access_token) {
266
+ hmrcToken = json.access_token;
267
+ hmrcTokenExpiry = now + (json.expires_in || 14400) * 1000;
268
+ return hmrcToken;
269
+ }
270
+ return null;
271
+ } catch(e) {
272
+ return null;
273
+ }
247
274
  }
248
275
 
249
276
  async function validateHMRC(vatNumber) {
@@ -254,23 +281,18 @@ async function validateHMRC(vatNumber) {
254
281
  const sandbox = process.env.HMRC_SANDBOX === 'true';
255
282
  const hostname = sandbox ? 'test-api.service.hmrc.gov.uk' : 'api.service.hmrc.gov.uk';
256
283
 
257
- return new Promise((resolve) => {
258
- const req = https.request({
259
- hostname,
260
- path: '/organisations/vat/check-vat-number/lookup/' + clean,
284
+ try {
285
+ const response = await hmrcFetchWithRetry(`https://${hostname}/organisations/vat/check-vat-number/lookup/${clean}`, {
261
286
  method: 'GET',
262
- headers: { 'Accept': 'application/vnd.hmrc.2.0+json', 'Authorization': 'Bearer ' + token }
263
- }, res => {
264
- let d = ''; res.on('data', c => d += c);
265
- res.on('end', () => {
266
- try { resolve({ source: 'HMRC', status: res.statusCode, data: JSON.parse(d) }); }
267
- catch(e) { resolve({ source: 'HMRC', error: 'Parse error' }); }
268
- });
287
+ headers: { 'Accept': 'application/vnd.hmrc.2.0+json', 'Authorization': 'Bearer ' + token, ...getFraudPreventionHeaders() },
288
+ signal: AbortSignal.timeout(8000)
269
289
  });
270
- req.on('error', e => resolve({ source: 'HMRC', error: e.message }));
271
- req.setTimeout(8000, () => { req.destroy(); resolve({ source: 'HMRC', error: 'Timeout' }); });
272
- req.end();
273
- });
290
+ const data = await response.json();
291
+ return { source: 'HMRC', status: response.status, data };
292
+ } catch(e) {
293
+ if (e.name === 'TimeoutError' || e.name === 'AbortError') return { source: 'HMRC', error: 'Timeout' };
294
+ return { source: 'HMRC', error: e.message };
295
+ }
274
296
  }
275
297
 
276
298
  async function validateABN(abn) {