thunder-bridge 1.4.1 → 1.5.0
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/README.md +356 -536
- package/dist/bank-B3mHISn1.d.cts +92 -0
- package/dist/bank-B3mHISn1.d.ts +92 -0
- package/dist/bank.cjs +141 -0
- package/dist/bank.d.cts +44 -0
- package/dist/bank.d.ts +44 -0
- package/dist/bank.js +114 -0
- package/dist/client-Cc5gtjGV.d.ts +733 -0
- package/dist/client-CzGZcByI.d.cts +733 -0
- package/dist/errors-0vbVoISA.d.ts +126 -0
- package/dist/errors-Dmh-Uoh8.d.cts +126 -0
- package/dist/index.cjs +1576 -1094
- package/dist/index.d.cts +11 -503
- package/dist/index.d.ts +11 -503
- package/dist/index.js +1556 -1052
- package/dist/{server.cjs → nwc.cjs} +261 -689
- package/dist/nwc.d.cts +120 -0
- package/dist/nwc.d.ts +120 -0
- package/dist/{server.js → nwc.js} +256 -667
- package/dist/price.cjs +240 -0
- package/dist/price.d.cts +17 -0
- package/dist/price.d.ts +17 -0
- package/dist/price.js +205 -0
- package/dist/qr-CF-YeXU1.d.cts +55 -0
- package/dist/qr-CF-YeXU1.d.ts +55 -0
- package/dist/qr.cjs +262 -0
- package/dist/qr.d.cts +1 -0
- package/dist/qr.d.ts +1 -0
- package/dist/qr.js +227 -0
- package/dist/types-BNPmVnA7.d.cts +252 -0
- package/dist/types-BNPmVnA7.d.ts +252 -0
- package/openapi.yaml +1 -1
- package/package.json +48 -9
- package/dist/rail-CqUfuYXJ.d.cts +0 -615
- package/dist/rail-CqUfuYXJ.d.ts +0 -615
- package/dist/server.d.cts +0 -145
- package/dist/server.d.ts +0 -145
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/** One incoming payment as the bank booked it, in the smallest unit of its currency */
|
|
2
|
+
interface Credit {
|
|
3
|
+
amountMinor: number;
|
|
4
|
+
currency: string;
|
|
5
|
+
/** Whatever the payer wrote, wherever this bank puts it. Matching is a substring, so noise around it is fine */
|
|
6
|
+
reference: string;
|
|
7
|
+
/**
|
|
8
|
+
* Unix seconds. A bank that books a day rather than an instant, as Fio does,
|
|
9
|
+
* gives the day's midnight in its own zone, so rendering this in UTC can show
|
|
10
|
+
* the day before. Nothing here matches on it, it is yours to read
|
|
11
|
+
*/
|
|
12
|
+
bookedAt: number;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Recent credits on one account, oldest or newest first, it makes no difference.
|
|
16
|
+
* This is the whole plugin seam: a bank is a function of this shape, and
|
|
17
|
+
* `fioStatement` is one implementation of it
|
|
18
|
+
*/
|
|
19
|
+
type Statement = (sinceUnix: number) => Promise<Credit[]>;
|
|
20
|
+
/** One transfer to ask for: what is owed, where it lands, and where its arrival is read back from */
|
|
21
|
+
interface BankTransferParams {
|
|
22
|
+
/** Long lived and server side. The preimage is derived from it, so losing it loses every proof */
|
|
23
|
+
secret: string;
|
|
24
|
+
/** What the payer must leave on the transfer, an order id or a nonce. It is matched, not stored */
|
|
25
|
+
reference: string;
|
|
26
|
+
/** The price in the smallest unit, so 48055 is 480.55 CZK */
|
|
27
|
+
amountMinor: number;
|
|
28
|
+
/** The account the money goes to, as an IBAN */
|
|
29
|
+
iban: string;
|
|
30
|
+
/** Where `bankVerifyEndpoint` is mounted, a public https URL with no query of its own */
|
|
31
|
+
verifyUrl: string;
|
|
32
|
+
/** When the offer dies, in unix seconds. Money in a bank moves on banking days, so give it days */
|
|
33
|
+
expiresAt: number;
|
|
34
|
+
/** Defaults to CZK */
|
|
35
|
+
currency?: string;
|
|
36
|
+
/** Up to ten digits, for accounting systems that still want one */
|
|
37
|
+
variableSymbol?: string;
|
|
38
|
+
/**
|
|
39
|
+
* Groups this transfer with everything else paid to the same secret, so one
|
|
40
|
+
* `followTrigger` socket hears about it. Give the Lightning leg of the same
|
|
41
|
+
* order the same secret and both rails arrive on one stream
|
|
42
|
+
*/
|
|
43
|
+
trigger?: string;
|
|
44
|
+
/** How many settlements of that trigger the gateway keeps replayable past the hour, needs `trigger` */
|
|
45
|
+
replay?: number;
|
|
46
|
+
/**
|
|
47
|
+
* Handed back untouched on that stream, so a watcher learns which order settled
|
|
48
|
+
* without asking anyone. `seal` it and the gateway cannot read it either
|
|
49
|
+
*/
|
|
50
|
+
sealed?: string;
|
|
51
|
+
/**
|
|
52
|
+
* Where the gateway posts once the money lands, a public https URL. Without one
|
|
53
|
+
* a transfer is only ever learned by following the trigger or asking
|
|
54
|
+
*/
|
|
55
|
+
webhookUrl?: string;
|
|
56
|
+
/**
|
|
57
|
+
* Register on a gateway you do not own anyway. The verify URL names the amount
|
|
58
|
+
* and the reference, so its operator ends up reading your order book, and the
|
|
59
|
+
* URL itself answers whether that order was paid. Say true only when the order
|
|
60
|
+
* book is not worth hiding
|
|
61
|
+
*/
|
|
62
|
+
allowPublicGateway?: boolean;
|
|
63
|
+
}
|
|
64
|
+
/** A transfer the gateway is now watching, and the descriptor the payer scans */
|
|
65
|
+
interface BankTransfer {
|
|
66
|
+
/** The watched payment's id at the gateway, which is how you read this order back */
|
|
67
|
+
id: string;
|
|
68
|
+
/** What the gateway was given, and what the preimage has to hash to */
|
|
69
|
+
paymentHash: string;
|
|
70
|
+
/** The same URL you mounted, carrying what to look for and a signature over it */
|
|
71
|
+
verifyUrl: string;
|
|
72
|
+
/** The payer scans this, it is a Short Payment Descriptor, the Czech QR platba format */
|
|
73
|
+
spd: string;
|
|
74
|
+
}
|
|
75
|
+
/** The endpoint the gateway polls for a bank transfer, answering off your own statement */
|
|
76
|
+
interface BankVerifyConfig {
|
|
77
|
+
/** The same secret `bankTransfer` was given */
|
|
78
|
+
secret: string;
|
|
79
|
+
/** The account to read */
|
|
80
|
+
statement: Statement;
|
|
81
|
+
/** How far back a credit still counts, seven days by default */
|
|
82
|
+
lookBackSecs?: number;
|
|
83
|
+
/**
|
|
84
|
+
* How often you want the gateway to ask, in seconds. It goes out as
|
|
85
|
+
* `Cache-Control: max-age`, so the pace is yours to set rather than the
|
|
86
|
+
* gateway's, and a bank that updates once a minute should say so instead of
|
|
87
|
+
* being polled every few seconds. Thirty by default, clamped to an hour
|
|
88
|
+
*/
|
|
89
|
+
pollEverySecs?: number;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export type { BankTransfer as B, Credit as C, Statement as S, BankTransferParams as a, BankVerifyConfig as b };
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/** One incoming payment as the bank booked it, in the smallest unit of its currency */
|
|
2
|
+
interface Credit {
|
|
3
|
+
amountMinor: number;
|
|
4
|
+
currency: string;
|
|
5
|
+
/** Whatever the payer wrote, wherever this bank puts it. Matching is a substring, so noise around it is fine */
|
|
6
|
+
reference: string;
|
|
7
|
+
/**
|
|
8
|
+
* Unix seconds. A bank that books a day rather than an instant, as Fio does,
|
|
9
|
+
* gives the day's midnight in its own zone, so rendering this in UTC can show
|
|
10
|
+
* the day before. Nothing here matches on it, it is yours to read
|
|
11
|
+
*/
|
|
12
|
+
bookedAt: number;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Recent credits on one account, oldest or newest first, it makes no difference.
|
|
16
|
+
* This is the whole plugin seam: a bank is a function of this shape, and
|
|
17
|
+
* `fioStatement` is one implementation of it
|
|
18
|
+
*/
|
|
19
|
+
type Statement = (sinceUnix: number) => Promise<Credit[]>;
|
|
20
|
+
/** One transfer to ask for: what is owed, where it lands, and where its arrival is read back from */
|
|
21
|
+
interface BankTransferParams {
|
|
22
|
+
/** Long lived and server side. The preimage is derived from it, so losing it loses every proof */
|
|
23
|
+
secret: string;
|
|
24
|
+
/** What the payer must leave on the transfer, an order id or a nonce. It is matched, not stored */
|
|
25
|
+
reference: string;
|
|
26
|
+
/** The price in the smallest unit, so 48055 is 480.55 CZK */
|
|
27
|
+
amountMinor: number;
|
|
28
|
+
/** The account the money goes to, as an IBAN */
|
|
29
|
+
iban: string;
|
|
30
|
+
/** Where `bankVerifyEndpoint` is mounted, a public https URL with no query of its own */
|
|
31
|
+
verifyUrl: string;
|
|
32
|
+
/** When the offer dies, in unix seconds. Money in a bank moves on banking days, so give it days */
|
|
33
|
+
expiresAt: number;
|
|
34
|
+
/** Defaults to CZK */
|
|
35
|
+
currency?: string;
|
|
36
|
+
/** Up to ten digits, for accounting systems that still want one */
|
|
37
|
+
variableSymbol?: string;
|
|
38
|
+
/**
|
|
39
|
+
* Groups this transfer with everything else paid to the same secret, so one
|
|
40
|
+
* `followTrigger` socket hears about it. Give the Lightning leg of the same
|
|
41
|
+
* order the same secret and both rails arrive on one stream
|
|
42
|
+
*/
|
|
43
|
+
trigger?: string;
|
|
44
|
+
/** How many settlements of that trigger the gateway keeps replayable past the hour, needs `trigger` */
|
|
45
|
+
replay?: number;
|
|
46
|
+
/**
|
|
47
|
+
* Handed back untouched on that stream, so a watcher learns which order settled
|
|
48
|
+
* without asking anyone. `seal` it and the gateway cannot read it either
|
|
49
|
+
*/
|
|
50
|
+
sealed?: string;
|
|
51
|
+
/**
|
|
52
|
+
* Where the gateway posts once the money lands, a public https URL. Without one
|
|
53
|
+
* a transfer is only ever learned by following the trigger or asking
|
|
54
|
+
*/
|
|
55
|
+
webhookUrl?: string;
|
|
56
|
+
/**
|
|
57
|
+
* Register on a gateway you do not own anyway. The verify URL names the amount
|
|
58
|
+
* and the reference, so its operator ends up reading your order book, and the
|
|
59
|
+
* URL itself answers whether that order was paid. Say true only when the order
|
|
60
|
+
* book is not worth hiding
|
|
61
|
+
*/
|
|
62
|
+
allowPublicGateway?: boolean;
|
|
63
|
+
}
|
|
64
|
+
/** A transfer the gateway is now watching, and the descriptor the payer scans */
|
|
65
|
+
interface BankTransfer {
|
|
66
|
+
/** The watched payment's id at the gateway, which is how you read this order back */
|
|
67
|
+
id: string;
|
|
68
|
+
/** What the gateway was given, and what the preimage has to hash to */
|
|
69
|
+
paymentHash: string;
|
|
70
|
+
/** The same URL you mounted, carrying what to look for and a signature over it */
|
|
71
|
+
verifyUrl: string;
|
|
72
|
+
/** The payer scans this, it is a Short Payment Descriptor, the Czech QR platba format */
|
|
73
|
+
spd: string;
|
|
74
|
+
}
|
|
75
|
+
/** The endpoint the gateway polls for a bank transfer, answering off your own statement */
|
|
76
|
+
interface BankVerifyConfig {
|
|
77
|
+
/** The same secret `bankTransfer` was given */
|
|
78
|
+
secret: string;
|
|
79
|
+
/** The account to read */
|
|
80
|
+
statement: Statement;
|
|
81
|
+
/** How far back a credit still counts, seven days by default */
|
|
82
|
+
lookBackSecs?: number;
|
|
83
|
+
/**
|
|
84
|
+
* How often you want the gateway to ask, in seconds. It goes out as
|
|
85
|
+
* `Cache-Control: max-age`, so the pace is yours to set rather than the
|
|
86
|
+
* gateway's, and a bank that updates once a minute should say so instead of
|
|
87
|
+
* being polled every few seconds. Thirty by default, clamped to an hour
|
|
88
|
+
*/
|
|
89
|
+
pollEverySecs?: number;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export type { BankTransfer as B, Credit as C, Statement as S, BankTransferParams as a, BankVerifyConfig as b };
|
package/dist/bank.cjs
ADDED
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/entry/bank.ts
|
|
21
|
+
var bank_exports = {};
|
|
22
|
+
__export(bank_exports, {
|
|
23
|
+
fioStatement: () => fioStatement
|
|
24
|
+
});
|
|
25
|
+
module.exports = __toCommonJS(bank_exports);
|
|
26
|
+
|
|
27
|
+
// src/fio.ts
|
|
28
|
+
var BASE_URL = "https://fioapi.fio.cz/v1/rest";
|
|
29
|
+
var MINOR_UNITS = 100;
|
|
30
|
+
var MILLIS = 1e3;
|
|
31
|
+
var DATE_CHARS = "2026-08-04".length;
|
|
32
|
+
var DEFAULT_MIN_INTERVAL_SECS = 30;
|
|
33
|
+
var TOO_SOON = 409;
|
|
34
|
+
var BOOKED_AT = "column0";
|
|
35
|
+
var AMOUNT = "column1";
|
|
36
|
+
var CURRENCY = "column14";
|
|
37
|
+
var VARIABLE_SYMBOL = "column5";
|
|
38
|
+
var USER_IDENTIFICATION = "column7";
|
|
39
|
+
var MESSAGE_FOR_RECIPIENT = "column16";
|
|
40
|
+
var PAYER_REFERENCE = "column27";
|
|
41
|
+
var WHERE_A_PAYER_WRITES = [
|
|
42
|
+
MESSAGE_FOR_RECIPIENT,
|
|
43
|
+
VARIABLE_SYMBOL,
|
|
44
|
+
USER_IDENTIFICATION,
|
|
45
|
+
PAYER_REFERENCE
|
|
46
|
+
];
|
|
47
|
+
var UTC_OFFSET = /^[+-]\d{4}$/;
|
|
48
|
+
function fioStatement(config) {
|
|
49
|
+
const named = typeof config.token === "string" ? [config.token] : config.token;
|
|
50
|
+
if (named.length === 0) {
|
|
51
|
+
throw new Error("fioStatement needs at least one token to read with");
|
|
52
|
+
}
|
|
53
|
+
const usedAt = new Map(named.map((token) => [token, Number.NEGATIVE_INFINITY]));
|
|
54
|
+
const tokenWindowMs = (config.minIntervalSecs ?? DEFAULT_MIN_INTERVAL_SECS) * MILLIS;
|
|
55
|
+
const paceMs = tokenWindowMs / usedAt.size;
|
|
56
|
+
let lastRead = Number.NEGATIVE_INFINITY;
|
|
57
|
+
let credits = [];
|
|
58
|
+
return async (sinceUnix) => {
|
|
59
|
+
const now = Date.now();
|
|
60
|
+
if (now - lastRead < paceMs) {
|
|
61
|
+
return credits;
|
|
62
|
+
}
|
|
63
|
+
const idlest = longestUnused(usedAt);
|
|
64
|
+
if (now - idlest.usedAt < tokenWindowMs) {
|
|
65
|
+
return credits;
|
|
66
|
+
}
|
|
67
|
+
usedAt.set(idlest.token, now);
|
|
68
|
+
lastRead = now;
|
|
69
|
+
const url = [
|
|
70
|
+
config.baseUrl ?? BASE_URL,
|
|
71
|
+
"periods",
|
|
72
|
+
idlest.token,
|
|
73
|
+
asDate(sinceUnix),
|
|
74
|
+
asDate(Math.floor(now / MILLIS)),
|
|
75
|
+
"transactions.json"
|
|
76
|
+
].join("/");
|
|
77
|
+
const answer = await fetch(url, { headers: { accept: "application/json" } });
|
|
78
|
+
if (answer.status === TOO_SOON) {
|
|
79
|
+
throw new Error("fio refuses a second read of this token inside its 30 second window");
|
|
80
|
+
}
|
|
81
|
+
if (!answer.ok) {
|
|
82
|
+
throw new Error(`fio answered ${answer.status} reading the statement`);
|
|
83
|
+
}
|
|
84
|
+
const read = await answer.json();
|
|
85
|
+
credits = (read.accountStatement?.transactionList?.transaction ?? []).filter(isCredit).map(asCredit);
|
|
86
|
+
return credits;
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
function longestUnused(usedAt) {
|
|
90
|
+
let token = "";
|
|
91
|
+
let idleSince = Number.POSITIVE_INFINITY;
|
|
92
|
+
for (const [candidate, at] of usedAt) {
|
|
93
|
+
if (at < idleSince) {
|
|
94
|
+
token = candidate;
|
|
95
|
+
idleSince = at;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
return { token, usedAt: idleSince };
|
|
99
|
+
}
|
|
100
|
+
function isCredit(transaction) {
|
|
101
|
+
return numberIn(transaction[AMOUNT]) > 0;
|
|
102
|
+
}
|
|
103
|
+
function asCredit(transaction) {
|
|
104
|
+
return {
|
|
105
|
+
amountMinor: Math.round(numberIn(transaction[AMOUNT]) * MINOR_UNITS),
|
|
106
|
+
currency: textIn(transaction[CURRENCY]),
|
|
107
|
+
reference: WHERE_A_PAYER_WRITES.map((column) => textIn(transaction[column])).filter((written) => written.length > 0).join(" "),
|
|
108
|
+
bookedAt: bookedAtIn(transaction[BOOKED_AT])
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
function bookedAtIn(cell) {
|
|
112
|
+
const value = cell?.value;
|
|
113
|
+
if (typeof value === "number") {
|
|
114
|
+
return Math.floor(value / MILLIS);
|
|
115
|
+
}
|
|
116
|
+
const at = Date.parse(asIso8601(textIn(cell)));
|
|
117
|
+
return Number.isFinite(at) ? Math.floor(at / MILLIS) : 0;
|
|
118
|
+
}
|
|
119
|
+
function asIso8601(booked) {
|
|
120
|
+
const day = booked.slice(0, DATE_CHARS);
|
|
121
|
+
const zone = booked.slice(DATE_CHARS);
|
|
122
|
+
if (!UTC_OFFSET.test(zone)) {
|
|
123
|
+
return `${day}T00:00:00Z`;
|
|
124
|
+
}
|
|
125
|
+
return `${day}T00:00:00${zone.slice(0, 3)}:${zone.slice(3)}`;
|
|
126
|
+
}
|
|
127
|
+
function numberIn(cell) {
|
|
128
|
+
const value = Number(cell?.value ?? Number.NaN);
|
|
129
|
+
return Number.isFinite(value) ? value : 0;
|
|
130
|
+
}
|
|
131
|
+
function textIn(cell) {
|
|
132
|
+
const value = cell?.value;
|
|
133
|
+
return value === null || value === void 0 ? "" : String(value).trim();
|
|
134
|
+
}
|
|
135
|
+
function asDate(unix) {
|
|
136
|
+
return new Date(unix * MILLIS).toISOString().slice(0, DATE_CHARS);
|
|
137
|
+
}
|
|
138
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
139
|
+
0 && (module.exports = {
|
|
140
|
+
fioStatement
|
|
141
|
+
});
|
package/dist/bank.d.cts
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { S as Statement } from './bank-B3mHISn1.cjs';
|
|
2
|
+
export { B as BankTransfer, a as BankTransferParams, b as BankVerifyConfig, C as Credit } from './bank-B3mHISn1.cjs';
|
|
3
|
+
|
|
4
|
+
/** A Fio account to read credits from, as its own API describes one */
|
|
5
|
+
interface FioConfig {
|
|
6
|
+
/**
|
|
7
|
+
* A token with "Sledování účtu" rights, which is read only and cannot move
|
|
8
|
+
* money. One token is one account, which is why this takes no account number.
|
|
9
|
+
*
|
|
10
|
+
* Give it several and they are used in turn. Fio's window is per token rather
|
|
11
|
+
* than per account, so five tokens on one account is a read every six seconds,
|
|
12
|
+
* and generating another token for the same account is what Fio's own
|
|
13
|
+
* documentation suggests when one is not enough
|
|
14
|
+
*/
|
|
15
|
+
token: string | string[];
|
|
16
|
+
/**
|
|
17
|
+
* Fio's window for one token, 30 seconds. No token is ever asked twice inside
|
|
18
|
+
* it, and the gap between reads is this divided by however many tokens were
|
|
19
|
+
* given, so the answers stay evenly spaced rather than arriving in bursts.
|
|
20
|
+
* Inside that gap the last answer is handed back. Only helps a process that
|
|
21
|
+
* stays up
|
|
22
|
+
*/
|
|
23
|
+
minIntervalSecs?: number;
|
|
24
|
+
/** Override to point at a mock */
|
|
25
|
+
baseUrl?: string;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Read one Fio account as a `Statement`, so a bank transfer proves itself the
|
|
29
|
+
* way a Lightning payment does.
|
|
30
|
+
*
|
|
31
|
+
* The token is the read only kind, generated in internetbanking under Nastavení
|
|
32
|
+
* and API, and it is the whole configuration: a token belongs to one account, so
|
|
33
|
+
* there is no account number to get wrong. It cannot pay anyone, and the worst a
|
|
34
|
+
* leaked one costs you is that someone else can read the statement.
|
|
35
|
+
*
|
|
36
|
+
* Every field on a Fio transaction is optional and arrives as `null` when it is
|
|
37
|
+
* absent, the amount carries its direction in its sign rather than in a flag,
|
|
38
|
+
* and the date is a day and a UTC offset, `2026-07-15+0200`. This reads all
|
|
39
|
+
* three the way the bank answers them and treats a missing field as absent
|
|
40
|
+
* rather than guessing.
|
|
41
|
+
*/
|
|
42
|
+
declare function fioStatement(config: FioConfig): Statement;
|
|
43
|
+
|
|
44
|
+
export { type FioConfig, Statement, fioStatement };
|
package/dist/bank.d.ts
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { S as Statement } from './bank-B3mHISn1.js';
|
|
2
|
+
export { B as BankTransfer, a as BankTransferParams, b as BankVerifyConfig, C as Credit } from './bank-B3mHISn1.js';
|
|
3
|
+
|
|
4
|
+
/** A Fio account to read credits from, as its own API describes one */
|
|
5
|
+
interface FioConfig {
|
|
6
|
+
/**
|
|
7
|
+
* A token with "Sledování účtu" rights, which is read only and cannot move
|
|
8
|
+
* money. One token is one account, which is why this takes no account number.
|
|
9
|
+
*
|
|
10
|
+
* Give it several and they are used in turn. Fio's window is per token rather
|
|
11
|
+
* than per account, so five tokens on one account is a read every six seconds,
|
|
12
|
+
* and generating another token for the same account is what Fio's own
|
|
13
|
+
* documentation suggests when one is not enough
|
|
14
|
+
*/
|
|
15
|
+
token: string | string[];
|
|
16
|
+
/**
|
|
17
|
+
* Fio's window for one token, 30 seconds. No token is ever asked twice inside
|
|
18
|
+
* it, and the gap between reads is this divided by however many tokens were
|
|
19
|
+
* given, so the answers stay evenly spaced rather than arriving in bursts.
|
|
20
|
+
* Inside that gap the last answer is handed back. Only helps a process that
|
|
21
|
+
* stays up
|
|
22
|
+
*/
|
|
23
|
+
minIntervalSecs?: number;
|
|
24
|
+
/** Override to point at a mock */
|
|
25
|
+
baseUrl?: string;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Read one Fio account as a `Statement`, so a bank transfer proves itself the
|
|
29
|
+
* way a Lightning payment does.
|
|
30
|
+
*
|
|
31
|
+
* The token is the read only kind, generated in internetbanking under Nastavení
|
|
32
|
+
* and API, and it is the whole configuration: a token belongs to one account, so
|
|
33
|
+
* there is no account number to get wrong. It cannot pay anyone, and the worst a
|
|
34
|
+
* leaked one costs you is that someone else can read the statement.
|
|
35
|
+
*
|
|
36
|
+
* Every field on a Fio transaction is optional and arrives as `null` when it is
|
|
37
|
+
* absent, the amount carries its direction in its sign rather than in a flag,
|
|
38
|
+
* and the date is a day and a UTC offset, `2026-07-15+0200`. This reads all
|
|
39
|
+
* three the way the bank answers them and treats a missing field as absent
|
|
40
|
+
* rather than guessing.
|
|
41
|
+
*/
|
|
42
|
+
declare function fioStatement(config: FioConfig): Statement;
|
|
43
|
+
|
|
44
|
+
export { type FioConfig, Statement, fioStatement };
|
package/dist/bank.js
ADDED
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
// src/fio.ts
|
|
2
|
+
var BASE_URL = "https://fioapi.fio.cz/v1/rest";
|
|
3
|
+
var MINOR_UNITS = 100;
|
|
4
|
+
var MILLIS = 1e3;
|
|
5
|
+
var DATE_CHARS = "2026-08-04".length;
|
|
6
|
+
var DEFAULT_MIN_INTERVAL_SECS = 30;
|
|
7
|
+
var TOO_SOON = 409;
|
|
8
|
+
var BOOKED_AT = "column0";
|
|
9
|
+
var AMOUNT = "column1";
|
|
10
|
+
var CURRENCY = "column14";
|
|
11
|
+
var VARIABLE_SYMBOL = "column5";
|
|
12
|
+
var USER_IDENTIFICATION = "column7";
|
|
13
|
+
var MESSAGE_FOR_RECIPIENT = "column16";
|
|
14
|
+
var PAYER_REFERENCE = "column27";
|
|
15
|
+
var WHERE_A_PAYER_WRITES = [
|
|
16
|
+
MESSAGE_FOR_RECIPIENT,
|
|
17
|
+
VARIABLE_SYMBOL,
|
|
18
|
+
USER_IDENTIFICATION,
|
|
19
|
+
PAYER_REFERENCE
|
|
20
|
+
];
|
|
21
|
+
var UTC_OFFSET = /^[+-]\d{4}$/;
|
|
22
|
+
function fioStatement(config) {
|
|
23
|
+
const named = typeof config.token === "string" ? [config.token] : config.token;
|
|
24
|
+
if (named.length === 0) {
|
|
25
|
+
throw new Error("fioStatement needs at least one token to read with");
|
|
26
|
+
}
|
|
27
|
+
const usedAt = new Map(named.map((token) => [token, Number.NEGATIVE_INFINITY]));
|
|
28
|
+
const tokenWindowMs = (config.minIntervalSecs ?? DEFAULT_MIN_INTERVAL_SECS) * MILLIS;
|
|
29
|
+
const paceMs = tokenWindowMs / usedAt.size;
|
|
30
|
+
let lastRead = Number.NEGATIVE_INFINITY;
|
|
31
|
+
let credits = [];
|
|
32
|
+
return async (sinceUnix) => {
|
|
33
|
+
const now = Date.now();
|
|
34
|
+
if (now - lastRead < paceMs) {
|
|
35
|
+
return credits;
|
|
36
|
+
}
|
|
37
|
+
const idlest = longestUnused(usedAt);
|
|
38
|
+
if (now - idlest.usedAt < tokenWindowMs) {
|
|
39
|
+
return credits;
|
|
40
|
+
}
|
|
41
|
+
usedAt.set(idlest.token, now);
|
|
42
|
+
lastRead = now;
|
|
43
|
+
const url = [
|
|
44
|
+
config.baseUrl ?? BASE_URL,
|
|
45
|
+
"periods",
|
|
46
|
+
idlest.token,
|
|
47
|
+
asDate(sinceUnix),
|
|
48
|
+
asDate(Math.floor(now / MILLIS)),
|
|
49
|
+
"transactions.json"
|
|
50
|
+
].join("/");
|
|
51
|
+
const answer = await fetch(url, { headers: { accept: "application/json" } });
|
|
52
|
+
if (answer.status === TOO_SOON) {
|
|
53
|
+
throw new Error("fio refuses a second read of this token inside its 30 second window");
|
|
54
|
+
}
|
|
55
|
+
if (!answer.ok) {
|
|
56
|
+
throw new Error(`fio answered ${answer.status} reading the statement`);
|
|
57
|
+
}
|
|
58
|
+
const read = await answer.json();
|
|
59
|
+
credits = (read.accountStatement?.transactionList?.transaction ?? []).filter(isCredit).map(asCredit);
|
|
60
|
+
return credits;
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
function longestUnused(usedAt) {
|
|
64
|
+
let token = "";
|
|
65
|
+
let idleSince = Number.POSITIVE_INFINITY;
|
|
66
|
+
for (const [candidate, at] of usedAt) {
|
|
67
|
+
if (at < idleSince) {
|
|
68
|
+
token = candidate;
|
|
69
|
+
idleSince = at;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
return { token, usedAt: idleSince };
|
|
73
|
+
}
|
|
74
|
+
function isCredit(transaction) {
|
|
75
|
+
return numberIn(transaction[AMOUNT]) > 0;
|
|
76
|
+
}
|
|
77
|
+
function asCredit(transaction) {
|
|
78
|
+
return {
|
|
79
|
+
amountMinor: Math.round(numberIn(transaction[AMOUNT]) * MINOR_UNITS),
|
|
80
|
+
currency: textIn(transaction[CURRENCY]),
|
|
81
|
+
reference: WHERE_A_PAYER_WRITES.map((column) => textIn(transaction[column])).filter((written) => written.length > 0).join(" "),
|
|
82
|
+
bookedAt: bookedAtIn(transaction[BOOKED_AT])
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
function bookedAtIn(cell) {
|
|
86
|
+
const value = cell?.value;
|
|
87
|
+
if (typeof value === "number") {
|
|
88
|
+
return Math.floor(value / MILLIS);
|
|
89
|
+
}
|
|
90
|
+
const at = Date.parse(asIso8601(textIn(cell)));
|
|
91
|
+
return Number.isFinite(at) ? Math.floor(at / MILLIS) : 0;
|
|
92
|
+
}
|
|
93
|
+
function asIso8601(booked) {
|
|
94
|
+
const day = booked.slice(0, DATE_CHARS);
|
|
95
|
+
const zone = booked.slice(DATE_CHARS);
|
|
96
|
+
if (!UTC_OFFSET.test(zone)) {
|
|
97
|
+
return `${day}T00:00:00Z`;
|
|
98
|
+
}
|
|
99
|
+
return `${day}T00:00:00${zone.slice(0, 3)}:${zone.slice(3)}`;
|
|
100
|
+
}
|
|
101
|
+
function numberIn(cell) {
|
|
102
|
+
const value = Number(cell?.value ?? Number.NaN);
|
|
103
|
+
return Number.isFinite(value) ? value : 0;
|
|
104
|
+
}
|
|
105
|
+
function textIn(cell) {
|
|
106
|
+
const value = cell?.value;
|
|
107
|
+
return value === null || value === void 0 ? "" : String(value).trim();
|
|
108
|
+
}
|
|
109
|
+
function asDate(unix) {
|
|
110
|
+
return new Date(unix * MILLIS).toISOString().slice(0, DATE_CHARS);
|
|
111
|
+
}
|
|
112
|
+
export {
|
|
113
|
+
fioStatement
|
|
114
|
+
};
|