@festapp/banksync 0.0.0-bootstrap.20260831

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.
@@ -0,0 +1,696 @@
1
+ "use strict";Object.defineProperty(exports, "__esModule", {value: true}); function _nullishCoalesce(lhs, rhsFn) { if (lhs != null) { return lhs; } else { return rhsFn(); } } function _optionalChain(ops) { let lastAccessLHS = undefined; let value = ops[0]; let i = 1; while (i < ops.length) { const op = ops[i]; const fn = ops[i + 1]; i += 2; if ((op === 'optionalAccess' || op === 'optionalCall') && value == null) { return undefined; } if (op === 'access' || op === 'optionalAccess') { lastAccessLHS = value; value = fn(value); } else if (op === 'call' || op === 'optionalCall') { value = fn((...args) => value.call(lastAccessLHS, ...args)); lastAccessLHS = undefined; } } return value; }// src/normalize.ts
2
+ var ISO_ALIASES = {
3
+ "k\u010D": "CZK",
4
+ "kc": "CZK",
5
+ "czk": "CZK",
6
+ "\u20AC": "EUR",
7
+ "eur": "EUR",
8
+ "$": "USD",
9
+ "usd": "USD"
10
+ };
11
+ var MINOR_UNIT_DECIMALS = {
12
+ CZK: 2,
13
+ EUR: 2,
14
+ USD: 2
15
+ };
16
+ function normalizeCurrency(raw) {
17
+ const key = raw.trim().toLowerCase();
18
+ const out = ISO_ALIASES[key];
19
+ if (!out) {
20
+ throw new Error(`unknown_currency: ${JSON.stringify(raw)}`);
21
+ }
22
+ return out;
23
+ }
24
+ function toCents(amount, currency) {
25
+ const dec = MINOR_UNIT_DECIMALS[currency];
26
+ if (dec === void 0) {
27
+ throw new Error(`unsupported_currency_minor_unit: ${currency}`);
28
+ }
29
+ return Math.round(amount * 10 ** dec);
30
+ }
31
+
32
+ // src/parser.ts
33
+ var BANK_CODES = {
34
+ "0100": "Komer\u010Dn\xED banka, a.s.",
35
+ "0300": "\u010Ceskoslovensk\xE1 obchodn\xED banka, a. s.",
36
+ "0600": "MONETA Money Bank, a.s.",
37
+ "0800": "\u010Cesk\xE1 spo\u0159itelna, a.s.",
38
+ "2010": "Fio banka, a.s.",
39
+ "2700": "UniCredit Bank Czech Republic and Slovakia, a.s.",
40
+ "3030": "Air Bank a.s.",
41
+ "5500": "Raiffeisenbank a.s.",
42
+ "6210": "mBank S.A.",
43
+ "6000": "PPF banka a.s.",
44
+ "4000": "Expobank CZ a.s.",
45
+ "2250": "Banka CREDITAS a.s."
46
+ };
47
+ function detectProvider(text, accountNumber) {
48
+ if (accountNumber) {
49
+ const acc = accountNumber.replace(/\s/g, "");
50
+ if (acc.includes("/2010")) return "fio_email";
51
+ if (acc.includes("/3030")) return "airbank_email";
52
+ if (acc.toUpperCase().startsWith("CZ") && acc.length >= 8) {
53
+ const bankCode = acc.substring(4, 8);
54
+ if (bankCode === "2010") return "fio_email";
55
+ if (bankCode === "3030") return "airbank_email";
56
+ }
57
+ }
58
+ if (/fio/i.test(text)) return "fio_email";
59
+ if (/air\s*bank/i.test(text)) return "airbank_email";
60
+ return "unknown";
61
+ }
62
+ function parseAmount(raw) {
63
+ let s = raw.replace(/\s/g, "");
64
+ if (s.includes(",") && s.includes(".")) {
65
+ const lastComma = s.lastIndexOf(",");
66
+ const lastDot = s.lastIndexOf(".");
67
+ if (lastComma > lastDot) {
68
+ s = s.replace(".", "").replace(",", ".");
69
+ } else {
70
+ s = s.replace(",", "");
71
+ }
72
+ } else if (s.includes(",")) {
73
+ s = s.replace(",", ".");
74
+ }
75
+ return parseFloat(s);
76
+ }
77
+ var MAX_AMOUNT_FIELD_CHARS = 64;
78
+ function isAsciiDigit(char) {
79
+ const code = char.charCodeAt(0);
80
+ return code >= 48 && code <= 57;
81
+ }
82
+ function isAsciiLetter(char) {
83
+ const code = char.charCodeAt(0);
84
+ return code >= 65 && code <= 90 || code >= 97 && code <= 122;
85
+ }
86
+ function isAmountChar(char) {
87
+ return isAsciiDigit(char) || char === " " || char === " " || char === "\xA0" || char === "," || char === "." || char === "-";
88
+ }
89
+ function findLabeledLineValue(text, labels) {
90
+ for (const line of text.split(/\r?\n/)) {
91
+ const trimmed = line.trimStart();
92
+ const lower = trimmed.toLowerCase();
93
+ for (const label of labels) {
94
+ let searchFrom = 0;
95
+ while (searchFrom < lower.length) {
96
+ const labelAt = lower.indexOf(label, searchFrom);
97
+ if (labelAt < 0) break;
98
+ const before = labelAt > 0 ? _nullishCoalesce(lower[labelAt - 1], () => ( "")) : "";
99
+ let cursor = labelAt + label.length;
100
+ while (cursor < trimmed.length && (trimmed[cursor] === " " || trimmed[cursor] === " ")) cursor += 1;
101
+ if (!isAsciiLetter(before) && trimmed[cursor] === ":") return trimmed.slice(cursor + 1).trimStart();
102
+ searchFrom = labelAt + label.length;
103
+ }
104
+ }
105
+ }
106
+ return null;
107
+ }
108
+ function parseLabeledAmount(text, labels) {
109
+ const value = findLabeledLineValue(text, labels);
110
+ if (!value) return null;
111
+ let cursor = 0;
112
+ while (cursor < value.length && cursor <= MAX_AMOUNT_FIELD_CHARS && isAmountChar(_nullishCoalesce(value[cursor], () => ( "")))) cursor += 1;
113
+ if (cursor === 0 || cursor > MAX_AMOUNT_FIELD_CHARS) return null;
114
+ const rawAmount = value.slice(0, cursor).trim();
115
+ if (![...rawAmount].some(isAsciiDigit)) return null;
116
+ const currencyStart = cursor;
117
+ while (cursor < value.length && cursor - currencyStart < 3 && isAsciiLetter(_nullishCoalesce(value[cursor], () => ( "")))) cursor += 1;
118
+ const currency = value.slice(currencyStart, cursor);
119
+ if (currency.length < 2 || currency.length > 3 || isAsciiLetter(_nullishCoalesce(value[cursor], () => ( "")))) return null;
120
+ return { rawAmount, currency };
121
+ }
122
+ function parseAccountPrefix(value) {
123
+ let cursor = 0;
124
+ while (cursor < value.length && cursor < 20 && isAsciiDigit(_nullishCoalesce(value[cursor], () => ( "")))) cursor += 1;
125
+ if (cursor === 0 || value[cursor] !== "/") return null;
126
+ cursor += 1;
127
+ for (let digits = 0; digits < 4; digits += 1, cursor += 1) {
128
+ if (!isAsciiDigit(_nullishCoalesce(value[cursor], () => ( "")))) return null;
129
+ }
130
+ if (isAsciiDigit(_nullishCoalesce(value[cursor], () => ( "")))) return null;
131
+ return value.slice(0, cursor);
132
+ }
133
+ function parseAirbankCounterparty(text) {
134
+ for (const line of text.split(/\r?\n/)) {
135
+ const trimmed = line.trim();
136
+ const lower = trimmed.toLowerCase();
137
+ const marker = "z \xFA\u010Dtu";
138
+ const markerAt = lower.indexOf(marker);
139
+ if (markerAt < 0) continue;
140
+ const remainder = trimmed.slice(markerAt + marker.length).trimStart();
141
+ const numberMarker = " \u010D\xEDslo ";
142
+ const numberAt = remainder.toLowerCase().lastIndexOf(numberMarker);
143
+ const accountText = numberAt >= 0 ? remainder.slice(numberAt + numberMarker.length).trimStart() : remainder;
144
+ const account = parseAccountPrefix(accountText);
145
+ if (!account) continue;
146
+ const senderName = numberAt >= 0 ? remainder.slice(0, numberAt).trim().replace(/\s+/g, " ") || null : null;
147
+ return { account, senderName };
148
+ }
149
+ return null;
150
+ }
151
+ function g(m, i, fallback = "") {
152
+ return _nullishCoalesce(m[i], () => ( fallback));
153
+ }
154
+ function parseDateToUTC(raw) {
155
+ const withOffsetRe = /(\d{2})\.(\d{2})\.(\d{4})\s+(\d{2}):(\d{2})(?::(\d{2}))?\s*([+-])(\d{2}):?(\d{2})/;
156
+ const wm = raw.match(withOffsetRe);
157
+ if (wm) {
158
+ const sign = g(wm, 7);
159
+ const offsetMin = (parseInt(g(wm, 8), 10) * 60 + parseInt(g(wm, 9), 10)) * (sign === "+" ? 1 : -1);
160
+ const localMs = Date.UTC(
161
+ parseInt(g(wm, 3), 10),
162
+ parseInt(g(wm, 2), 10) - 1,
163
+ parseInt(g(wm, 1), 10),
164
+ parseInt(g(wm, 4), 10),
165
+ parseInt(g(wm, 5), 10),
166
+ parseInt(g(wm, 6, "0"), 10)
167
+ ) - offsetMin * 6e4;
168
+ return { date: new Date(localMs).toISOString(), date_offset_min: offsetMin };
169
+ }
170
+ const noOffsetRe = /(\d{2})\.(\d{2})\.(\d{4})\s+(\d{2}):(\d{2})(?::(\d{2}))?/;
171
+ const nm = raw.match(noOffsetRe);
172
+ if (nm) {
173
+ const month = parseInt(g(nm, 2), 10);
174
+ const pragueOffsetMin = month >= 4 && month <= 10 ? 120 : 60;
175
+ const localMs = Date.UTC(
176
+ parseInt(g(nm, 3), 10),
177
+ parseInt(g(nm, 2), 10) - 1,
178
+ parseInt(g(nm, 1), 10),
179
+ parseInt(g(nm, 4), 10),
180
+ parseInt(g(nm, 5), 10),
181
+ parseInt(g(nm, 6, "0"), 10)
182
+ ) - pragueOffsetMin * 6e4;
183
+ return { date: new Date(localMs).toISOString(), date_offset_min: pragueOffsetMin };
184
+ }
185
+ const dateOnlyRe = /(\d{2})\.(\d{2})\.(\d{4})/;
186
+ const dm = raw.match(dateOnlyRe);
187
+ if (dm) {
188
+ const iso = new Date(
189
+ Date.UTC(parseInt(g(dm, 3), 10), parseInt(g(dm, 2), 10) - 1, parseInt(g(dm, 1), 10), 12, 0, 0)
190
+ ).toISOString();
191
+ return { date: iso, date_offset_min: null };
192
+ }
193
+ return { date: null, date_offset_min: null };
194
+ }
195
+ function parseEmail(text, provider) {
196
+ if (provider === "fio_email") {
197
+ const amountField = parseLabeledAmount(text, ["\u010D\xE1stka", "castka", "amount"]);
198
+ if (!amountField) return null;
199
+ const rawCurrencyStr = amountField.currency;
200
+ const currency = normalizeCurrency(rawCurrencyStr);
201
+ const amount = parseAmount(amountField.rawAmount);
202
+ if (isNaN(amount)) return null;
203
+ if (amount < 0) return null;
204
+ const amount_cents = toCents(amount, currency);
205
+ const accountMatch = text.match(/(?:Protiúčet|Protiucet|Account):\s*([0-9\/\s]+)/i);
206
+ const vsMatch = text.match(/VS:\s*([0-9]+)/i);
207
+ const ksMatch = text.match(/KS:\s*([0-9]+)/i);
208
+ const ssMatch = text.match(/SS:\s*([0-9]+)/i);
209
+ const msgMatch = text.match(/(?:Zpráva pro příjemce|Message):\s*(.*)/i);
210
+ const nameMatch = text.match(/(?:Název protiúčtu|Account Name):\s*(.*)/i);
211
+ const idMatch = text.match(/(?:ID pokynu|Transaction ID):\s*([0-9]+)/i);
212
+ const dateMatch = text.match(
213
+ /Datum(?:\s+pohybu|\s+provedení|\s+zaúčtování)?:\s*(\d{2}\.\d{2}\.\d{4}(?:\s+\d{2}:\d{2}(?::\d{2})?(?:\s*[+-]\d{2}:?\d{2})?)?)/i
214
+ );
215
+ let counter_account = null;
216
+ let bank_code = null;
217
+ if (accountMatch) {
218
+ const rawAcc = (_nullishCoalesce(accountMatch[1], () => ( ""))).replace(/\s/g, "");
219
+ const parts = rawAcc.split("/");
220
+ counter_account = _nullishCoalesce(parts[0], () => ( null));
221
+ bank_code = parts.length > 1 ? _nullishCoalesce(parts[1], () => ( null)) : null;
222
+ }
223
+ const { date, date_offset_min } = dateMatch ? parseDateToUTC(_nullishCoalesce(dateMatch[1], () => ( ""))) : { date: null, date_offset_min: null };
224
+ return {
225
+ amount_cents,
226
+ currency,
227
+ counter_account: counter_account || null,
228
+ bank_code: bank_code || null,
229
+ bank_name: bank_code ? _nullishCoalesce(BANK_CODES[bank_code], () => ( null)) : null,
230
+ vs: vsMatch ? _nullishCoalesce(vsMatch[1], () => ( null)) : null,
231
+ ks: ksMatch ? _nullishCoalesce(ksMatch[1], () => ( null)) : null,
232
+ ss: ssMatch ? _nullishCoalesce(ssMatch[1], () => ( null)) : null,
233
+ message: msgMatch ? (_nullishCoalesce(msgMatch[1], () => ( ""))).trim() || null : null,
234
+ sender_name: nameMatch ? (_nullishCoalesce(nameMatch[1], () => ( ""))).trim() || null : null,
235
+ user_identification: null,
236
+ transaction_type: null,
237
+ performed_by: null,
238
+ comment: null,
239
+ command_id: null,
240
+ source: "email",
241
+ date,
242
+ date_offset_min,
243
+ transaction_id: idMatch ? _nullishCoalesce(idMatch[1], () => ( null)) : null,
244
+ external_id: null
245
+ };
246
+ }
247
+ if (provider === "airbank_email") {
248
+ const amountField = parseLabeledAmount(text, ["\u010D\xE1stka", "castka", "amount"]);
249
+ if (!amountField) return null;
250
+ const rawCurrencyStr = amountField.currency;
251
+ const currency = normalizeCurrency(rawCurrencyStr);
252
+ const amountStr = amountField.rawAmount.replace(/\s/g, "");
253
+ if (!amountStr) return null;
254
+ const amount = parseAmount(amountField.rawAmount);
255
+ if (isNaN(amount)) return null;
256
+ if (amount < 0) return null;
257
+ const amount_cents = toCents(amount, currency);
258
+ const counterparty = parseAirbankCounterparty(text);
259
+ const vsMatch = text.match(/(?:Variabilní symbol|\bVS\b)\s*:\s*([0-9]+)/i);
260
+ const ksMatch = text.match(/(?:Konstantní symbol|\bKS\b)\s*:\s*([0-9]+)/i);
261
+ const ssMatch = text.match(/(?:Specifický symbol|\bSS\b)\s*:\s*([0-9]+)/i);
262
+ const msgMatch = text.match(/(?:Zpráva pro příjemce|Zprava)\s*:\s*(.*)/i);
263
+ const idMatch = text.match(/(?:Kód transakce|Kod transakce)\s*:\s*([0-9]+)/i);
264
+ const dateMatch = text.match(
265
+ /(?:Datum zaúčtování|Datum zauctovani)\s*:\s*(\d{2}\.\d{2}\.\d{4}(?:\s+\d{2}:\d{2}(?::\d{2})?(?:\s*[+-]\d{2}:?\d{2})?)?)/i
266
+ );
267
+ let counter_account = null;
268
+ let bank_code = null;
269
+ let sender_name = null;
270
+ if (counterparty) {
271
+ sender_name = counterparty.senderName;
272
+ const rawAcc = counterparty.account;
273
+ const parts = rawAcc.split("/");
274
+ counter_account = _nullishCoalesce(parts[0], () => ( null));
275
+ bank_code = parts.length > 1 ? _nullishCoalesce(parts[1], () => ( null)) : null;
276
+ }
277
+ const { date, date_offset_min } = dateMatch ? parseDateToUTC(_nullishCoalesce(dateMatch[1], () => ( ""))) : { date: null, date_offset_min: null };
278
+ return {
279
+ amount_cents,
280
+ currency,
281
+ counter_account: counter_account || null,
282
+ bank_code: bank_code || null,
283
+ bank_name: bank_code ? _nullishCoalesce(BANK_CODES[bank_code], () => ( null)) : null,
284
+ vs: vsMatch ? _nullishCoalesce(vsMatch[1], () => ( null)) : null,
285
+ ks: ksMatch ? _nullishCoalesce(ksMatch[1], () => ( null)) : null,
286
+ ss: ssMatch ? _nullishCoalesce(ssMatch[1], () => ( null)) : null,
287
+ message: msgMatch ? (_nullishCoalesce(msgMatch[1], () => ( ""))).trim() || null : null,
288
+ sender_name,
289
+ user_identification: null,
290
+ transaction_type: null,
291
+ performed_by: null,
292
+ comment: null,
293
+ command_id: null,
294
+ source: "email",
295
+ date,
296
+ date_offset_min,
297
+ transaction_id: idMatch ? _nullishCoalesce(idMatch[1], () => ( null)) : null,
298
+ external_id: null
299
+ };
300
+ }
301
+ return null;
302
+ }
303
+
304
+ // src/iso11649.ts
305
+ function mod97(numeric) {
306
+ let remainder = 0;
307
+ for (let index = 0; index < numeric.length; index += 7) {
308
+ remainder = Number(String(remainder) + numeric.slice(index, index + 7)) % 97;
309
+ }
310
+ return remainder;
311
+ }
312
+ function toNumeric(value) {
313
+ let output = "";
314
+ for (const character of value) {
315
+ const code = character.charCodeAt(0);
316
+ if (code >= 48 && code <= 57) output += character;
317
+ else if (code >= 65 && code <= 90) output += String(code - 55);
318
+ else return "";
319
+ }
320
+ return output;
321
+ }
322
+ function encodeRf(input) {
323
+ const payload = input.trim().toUpperCase();
324
+ const numeric = toNumeric(payload + "RF00");
325
+ if (!numeric) throw new TypeError("ISO 11649 payload must be alphanumeric");
326
+ const checkDigits = 98 - mod97(numeric);
327
+ return "RF" + String(checkDigits).padStart(2, "0") + payload;
328
+ }
329
+ function decodeRf(input) {
330
+ if (typeof input !== "string") return null;
331
+ const reference = input.replace(/\s+/g, "").toUpperCase();
332
+ if (!/^RF\d{2}[0-9A-Z]+$/.test(reference)) return null;
333
+ const numeric = toNumeric(reference.slice(4) + reference.slice(0, 4));
334
+ if (!numeric || mod97(numeric) !== 1) return null;
335
+ return reference.slice(4);
336
+ }
337
+
338
+ // src/referenceCandidates.ts
339
+ var MAX_CANDIDATES = 5;
340
+ function normalizeText(s) {
341
+ return s.normalize("NFKD").replace(/[̀-ͯ]/g, "").toUpperCase();
342
+ }
343
+ function validVs(value) {
344
+ const normalized = _nullishCoalesce(_optionalChain([value, 'optionalAccess', _ => _.trim, 'call', _2 => _2()]), () => ( ""));
345
+ return /^\d{1,10}$/.test(normalized) ? normalized : null;
346
+ }
347
+ function rfCores(text) {
348
+ const compact = normalizeText(text).replace(/\s+/g, "");
349
+ const cores = [];
350
+ for (const match of compact.matchAll(/RF\d{3,12}(?!\d)/g)) {
351
+ const decoded = decodeRf(match[0]);
352
+ if (decoded && /^\d{1,10}$/.test(decoded) && !cores.includes(decoded)) {
353
+ cores.push(decoded);
354
+ }
355
+ }
356
+ return cores;
357
+ }
358
+ function resolveVariableSymbol(src) {
359
+ const structured = validVs(src.vs);
360
+ if (structured) return structured;
361
+ for (const text of [src.vs, src.message, src.ss, src.user_identification, src.comment]) {
362
+ if (typeof text !== "string") continue;
363
+ const [decoded] = rfCores(text);
364
+ if (decoded) return decoded;
365
+ }
366
+ return null;
367
+ }
368
+ function extractReferenceCandidates(src) {
369
+ const out = [];
370
+ const push = (v) => {
371
+ if (!v) return;
372
+ const t = v.trim();
373
+ if (t && !out.includes(t)) out.push(t);
374
+ };
375
+ push(_nullishCoalesce(validVs(src.vs), () => ( void 0)));
376
+ const texts = [src.message, src.ss, src.user_identification, src.comment].filter((x) => typeof x === "string" && x.length > 0).map(normalizeText);
377
+ for (const t of texts) {
378
+ const compact = t.replace(/\s+/g, "");
379
+ for (const m of compact.matchAll(/VS[:/]?(\d{4,10})/g)) push(m[1]);
380
+ for (const core of rfCores(t)) push(core);
381
+ }
382
+ for (const t of texts) {
383
+ for (const m of t.matchAll(/(?<!\d)(\d{6,10})(?!\d)/g)) push(m[1]);
384
+ }
385
+ return out.slice(0, MAX_CANDIDATES);
386
+ }
387
+
388
+ // src/relay.ts
389
+ function hex(buffer) {
390
+ return Array.from(new Uint8Array(buffer)).map((b) => b.toString(16).padStart(2, "0")).join("");
391
+ }
392
+ function concatBytes(...parts) {
393
+ const total = parts.reduce((n, p) => n + p.length, 0);
394
+ const out = new Uint8Array(total);
395
+ let offset = 0;
396
+ for (const part of parts) {
397
+ out.set(part, offset);
398
+ offset += part.length;
399
+ }
400
+ return out;
401
+ }
402
+ function buildWebhookEnvelope(args) {
403
+ return {
404
+ event: "transaction.received",
405
+ event_version: "1",
406
+ delivery_id: args.delivery_id,
407
+ pairing_code: args.pairing_code,
408
+ data: args.transaction
409
+ };
410
+ }
411
+ async function signWebhook(args) {
412
+ const timestamp = _nullishCoalesce(args.timestamp, () => ( Math.floor(Date.now() / 1e3)));
413
+ const enc = new TextEncoder();
414
+ const bodyBytes = enc.encode(JSON.stringify(args.envelope));
415
+ const signingInput = concatBytes(
416
+ enc.encode(String(timestamp)),
417
+ new Uint8Array([46]),
418
+ enc.encode(args.envelope.delivery_id),
419
+ new Uint8Array([46]),
420
+ bodyBytes
421
+ );
422
+ const secretBytes = enc.encode(args.secret);
423
+ const key = await globalThis.crypto.subtle.importKey(
424
+ "raw",
425
+ secretBytes.buffer,
426
+ { name: "HMAC", hash: "SHA-256" },
427
+ false,
428
+ ["sign"]
429
+ );
430
+ const mac = await globalThis.crypto.subtle.sign("HMAC", key, signingInput.buffer);
431
+ return {
432
+ bodyBytes,
433
+ headers: {
434
+ "Content-Type": "application/json",
435
+ "X-BankSync-Timestamp": String(timestamp),
436
+ "X-BankSync-Delivery-Id": args.envelope.delivery_id,
437
+ "X-BankSync-Signature": "sha256=" + hex(mac)
438
+ }
439
+ };
440
+ }
441
+ var WebhookVerificationError = class extends Error {
442
+ constructor(code) {
443
+ super(code);
444
+ this.code = code;
445
+ this.name = "WebhookVerificationError";
446
+ }
447
+
448
+ };
449
+ function isNullableString(value) {
450
+ return value === null || typeof value === "string";
451
+ }
452
+ function isPositiveSafeInteger(value) {
453
+ return typeof value === "number" && Number.isSafeInteger(value) && value > 0;
454
+ }
455
+ function isTransaction(value) {
456
+ if (!value || typeof value !== "object" || Array.isArray(value)) return false;
457
+ const data = value;
458
+ const nullableStrings = [
459
+ "counter_account",
460
+ "bank_code",
461
+ "bank_name",
462
+ "vs",
463
+ "ks",
464
+ "ss",
465
+ "message",
466
+ "sender_name",
467
+ "user_identification",
468
+ "transaction_type",
469
+ "performed_by",
470
+ "comment",
471
+ "command_id",
472
+ "transaction_id",
473
+ "external_id"
474
+ ];
475
+ return isPositiveSafeInteger(data.id) && isPositiveSafeInteger(data.bank_account_id) && isPositiveSafeInteger(data.amount_cents) && typeof data.currency === "string" && /^[A-Z]{3}$/.test(data.currency) && nullableStrings.every((field) => isNullableString(data[field])) && (data.source === "email" || data.source === "fio_api") && typeof data.date === "string" && data.date.length > 0 && (data.date_offset_min === null || typeof data.date_offset_min === "number" && Number.isSafeInteger(data.date_offset_min));
476
+ }
477
+ async function verifyWebhook(args) {
478
+ if (!/^\d{10}$/.test(args.timestamp)) throw new WebhookVerificationError("timestamp_invalid");
479
+ const timestamp = Number(args.timestamp);
480
+ const now = _nullishCoalesce(args.nowSeconds, () => ( Math.floor(Date.now() / 1e3)));
481
+ const tolerance = _nullishCoalesce(args.toleranceSeconds, () => ( 300));
482
+ if (!Number.isSafeInteger(timestamp) || Math.abs(now - timestamp) > tolerance) {
483
+ throw new WebhookVerificationError("timestamp_out_of_range");
484
+ }
485
+ if (!/^sha256=[0-9a-f]{64}$/i.test(args.signature)) {
486
+ throw new WebhookVerificationError("signature_invalid");
487
+ }
488
+ const encoder = new TextEncoder();
489
+ const signingInput = concatBytes(
490
+ encoder.encode(args.timestamp),
491
+ new Uint8Array([46]),
492
+ encoder.encode(args.deliveryId),
493
+ new Uint8Array([46]),
494
+ args.bodyBytes
495
+ );
496
+ const key = await globalThis.crypto.subtle.importKey(
497
+ "raw",
498
+ encoder.encode(args.secret),
499
+ { name: "HMAC", hash: "SHA-256" },
500
+ false,
501
+ ["sign"]
502
+ );
503
+ const mac = await globalThis.crypto.subtle.sign(
504
+ "HMAC",
505
+ key,
506
+ signingInput.buffer
507
+ );
508
+ const expected = "sha256=" + hex(mac);
509
+ if (expected.length !== args.signature.length) throw new WebhookVerificationError("signature_invalid");
510
+ let difference = 0;
511
+ for (let index = 0; index < expected.length; index += 1) {
512
+ difference |= expected.charCodeAt(index) ^ args.signature.charCodeAt(index);
513
+ }
514
+ if (difference !== 0) throw new WebhookVerificationError("signature_invalid");
515
+ let envelope;
516
+ try {
517
+ envelope = JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(args.bodyBytes));
518
+ } catch (e) {
519
+ throw new WebhookVerificationError("body_invalid");
520
+ }
521
+ if (!envelope || typeof envelope !== "object" || Array.isArray(envelope)) {
522
+ throw new WebhookVerificationError("body_invalid");
523
+ }
524
+ const value = envelope;
525
+ if (value.delivery_id !== args.deliveryId) throw new WebhookVerificationError("delivery_id_mismatch");
526
+ if (typeof value.delivery_id !== "string" || !/^[0-9A-HJKMNP-TV-Z]{26}$/.test(value.delivery_id)) {
527
+ throw new WebhookVerificationError("body_invalid");
528
+ }
529
+ if (value.event !== "transaction.received") throw new WebhookVerificationError("event_unsupported");
530
+ if (value.event_version !== "1") throw new WebhookVerificationError("event_version_unsupported");
531
+ if (typeof value.pairing_code !== "string" || !/^[0-9a-f]{10}$/i.test(value.pairing_code) || !isTransaction(value.data)) {
532
+ throw new WebhookVerificationError("body_invalid");
533
+ }
534
+ return envelope;
535
+ }
536
+
537
+ // src/fio.ts
538
+ var FIO_BASE = "https://fioapi.fio.cz/v1/rest";
539
+ var FioApiError = class extends Error {
540
+ constructor(message, status) {
541
+ super(message);
542
+ this.status = status;
543
+ this.name = "FioApiError";
544
+ }
545
+
546
+ };
547
+ var FioRateLimited = class extends FioApiError {
548
+ constructor(status, retryAfterS) {
549
+ super(`Fio API ${status}`, status);
550
+ this.retryAfterS = retryAfterS;
551
+ this.name = "FioRateLimited";
552
+ }
553
+
554
+ };
555
+ var FioTransientFailure = class extends FioApiError {
556
+ constructor(status) {
557
+ super(`Fio API ${status}`, status);
558
+ this.name = "FioTransientFailure";
559
+ }
560
+ };
561
+ function endpoint(path, token) {
562
+ return `${FIO_BASE}/${path}/${encodeURIComponent(token)}`;
563
+ }
564
+ async function fioRequest(op, token, directUrl, proxy, date) {
565
+ if (_optionalChain([proxy, 'optionalAccess', _3 => _3.url]) && proxy.secret) {
566
+ return fetch(proxy.url, {
567
+ method: "POST",
568
+ headers: { "content-type": "application/json", "x-fio-proxy-secret": proxy.secret },
569
+ body: JSON.stringify(date === void 0 ? { op, token } : { op, token, date })
570
+ });
571
+ }
572
+ return fetch(directUrl);
573
+ }
574
+ function retryAfterSeconds(headers) {
575
+ const raw = headers.get("Retry-After");
576
+ if (!raw) return null;
577
+ const n = Number.parseInt(raw, 10);
578
+ return Number.isFinite(n) && n >= 0 ? n : null;
579
+ }
580
+ async function ensureFioResponse(res) {
581
+ if (res.status === 429) {
582
+ throw new FioRateLimited(res.status, retryAfterSeconds(res.headers));
583
+ }
584
+ if (res.status >= 500) {
585
+ throw new FioTransientFailure(res.status);
586
+ }
587
+ if (!res.ok) {
588
+ throw new FioApiError(`Fio API ${res.status}`, res.status);
589
+ }
590
+ }
591
+ async function fetchNewTransactions(token, proxy) {
592
+ const res = await fioRequest("transactions", token, `${endpoint("last", token)}/transactions.json`, proxy);
593
+ await ensureFioResponse(res);
594
+ const json = await res.json();
595
+ const transactions = _nullishCoalesce(_optionalChain([json, 'access', _4 => _4.accountStatement, 'optionalAccess', _5 => _5.transactionList, 'optionalAccess', _6 => _6.transaction]), () => ( []));
596
+ return Array.isArray(transactions) ? transactions : [transactions];
597
+ }
598
+ async function setFioPointer(token, yyyyMmDd, proxy) {
599
+ if (!/^\d{4}-\d{2}-\d{2}$/.test(yyyyMmDd)) {
600
+ throw new Error("invalid_fio_pointer_date");
601
+ }
602
+ const res = await fioRequest("set-last-date", token, `${endpoint("set-last-date", token)}/${yyyyMmDd}/`, proxy, yyyyMmDd);
603
+ await ensureFioResponse(res);
604
+ }
605
+ function column(raw, idx) {
606
+ const value = _optionalChain([raw, 'access', _7 => _7[`column${idx}`], 'optionalAccess', _8 => _8.value]);
607
+ if (value === null || value === void 0) return null;
608
+ const s = String(value).trim();
609
+ return s.length > 0 ? s : null;
610
+ }
611
+ function parseAmount2(raw) {
612
+ if (raw === null) return 0;
613
+ const normalized = raw.replace(/\s/g, "").replace(",", ".");
614
+ return Number.parseFloat(normalized);
615
+ }
616
+ function parseOffsetMinutes(raw) {
617
+ const m = raw.match(/([+-])(\d{2}):?(\d{2})$/);
618
+ if (!m) return null;
619
+ const sign = m[1] === "+" ? 1 : -1;
620
+ return sign * (Number.parseInt(_nullishCoalesce(m[2], () => ( "0")), 10) * 60 + Number.parseInt(_nullishCoalesce(m[3], () => ( "0")), 10));
621
+ }
622
+ function parseFioDate(raw) {
623
+ if (!raw) return { date: (/* @__PURE__ */ new Date()).toISOString(), date_offset_min: null };
624
+ const isoDateOnly = raw.match(/^(\d{4}-\d{2}-\d{2})(?:[+-]\d{2}:?\d{2})?$/);
625
+ if (isoDateOnly) {
626
+ return {
627
+ date: `${isoDateOnly[1]}T12:00:00.000Z`,
628
+ date_offset_min: parseOffsetMinutes(raw)
629
+ };
630
+ }
631
+ const czechDateOnly = raw.match(/^(\d{2})\.(\d{2})\.(\d{4})$/);
632
+ if (czechDateOnly) {
633
+ return {
634
+ date: `${czechDateOnly[3]}-${czechDateOnly[2]}-${czechDateOnly[1]}T12:00:00.000Z`,
635
+ date_offset_min: null
636
+ };
637
+ }
638
+ const parsed = new Date(raw);
639
+ if (!Number.isNaN(parsed.getTime())) {
640
+ return {
641
+ date: parsed.toISOString(),
642
+ date_offset_min: parseOffsetMinutes(raw)
643
+ };
644
+ }
645
+ throw new Error(`invalid_fio_date: ${JSON.stringify(raw)}`);
646
+ }
647
+ function mapFioTransaction(raw) {
648
+ const amount = parseAmount2(column(raw, 1));
649
+ if (!(amount > 0)) return null;
650
+ const currency = normalizeCurrency(_nullishCoalesce(column(raw, 14), () => ( "")));
651
+ const { date, date_offset_min } = parseFioDate(column(raw, 0));
652
+ return {
653
+ amount_cents: toCents(amount, currency),
654
+ currency,
655
+ counter_account: column(raw, 2),
656
+ bank_code: column(raw, 3),
657
+ bank_name: column(raw, 12),
658
+ vs: column(raw, 5),
659
+ ks: column(raw, 4),
660
+ ss: column(raw, 6),
661
+ message: column(raw, 16),
662
+ sender_name: column(raw, 10),
663
+ user_identification: column(raw, 7),
664
+ transaction_type: column(raw, 8),
665
+ performed_by: column(raw, 9),
666
+ comment: column(raw, 25),
667
+ command_id: column(raw, 17),
668
+ source: "fio_api",
669
+ date,
670
+ date_offset_min,
671
+ transaction_id: column(raw, 22),
672
+ external_id: null
673
+ };
674
+ }
675
+
676
+
677
+
678
+
679
+
680
+
681
+
682
+
683
+
684
+
685
+
686
+
687
+
688
+
689
+
690
+
691
+
692
+
693
+
694
+
695
+
696
+ exports.normalizeCurrency = normalizeCurrency; exports.toCents = toCents; exports.BANK_CODES = BANK_CODES; exports.detectProvider = detectProvider; exports.parseEmail = parseEmail; exports.encodeRf = encodeRf; exports.decodeRf = decodeRf; exports.resolveVariableSymbol = resolveVariableSymbol; exports.extractReferenceCandidates = extractReferenceCandidates; exports.buildWebhookEnvelope = buildWebhookEnvelope; exports.signWebhook = signWebhook; exports.WebhookVerificationError = WebhookVerificationError; exports.verifyWebhook = verifyWebhook; exports.FioApiError = FioApiError; exports.FioRateLimited = FioRateLimited; exports.FioTransientFailure = FioTransientFailure; exports.fetchNewTransactions = fetchNewTransactions; exports.setFioPointer = setFioPointer; exports.mapFioTransaction = mapFioTransaction;