@coderoftime/fints 0.1.0 → 0.3.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.
Files changed (289) hide show
  1. package/CHANGELOG.md +519 -1
  2. package/README.md +138 -40
  3. package/dist/client.d.ts +49 -0
  4. package/dist/client.d.ts.map +1 -0
  5. package/dist/client.js +242 -0
  6. package/dist/client.js.map +1 -0
  7. package/dist/core/account.d.ts +46 -7
  8. package/dist/core/account.d.ts.map +1 -1
  9. package/dist/core/account.js +143 -24
  10. package/dist/core/account.js.map +1 -1
  11. package/dist/core/amount.d.ts +15 -4
  12. package/dist/core/amount.d.ts.map +1 -1
  13. package/dist/core/amount.js +46 -9
  14. package/dist/core/amount.js.map +1 -1
  15. package/dist/core/balance.d.ts +4 -0
  16. package/dist/core/balance.d.ts.map +1 -1
  17. package/dist/core/balance.js +11 -5
  18. package/dist/core/balance.js.map +1 -1
  19. package/dist/core/bank-info.d.ts +21 -4
  20. package/dist/core/bank-info.d.ts.map +1 -1
  21. package/dist/core/bank-info.js +26 -5
  22. package/dist/core/bank-info.js.map +1 -1
  23. package/dist/core/client.d.ts +81 -19
  24. package/dist/core/client.d.ts.map +1 -1
  25. package/dist/core/config.d.ts +40 -20
  26. package/dist/core/config.d.ts.map +1 -1
  27. package/dist/core/config.js +85 -30
  28. package/dist/core/config.js.map +1 -1
  29. package/dist/core/errors.d.ts +100 -26
  30. package/dist/core/errors.d.ts.map +1 -1
  31. package/dist/core/errors.js +136 -53
  32. package/dist/core/errors.js.map +1 -1
  33. package/dist/core/feedback.d.ts +11 -1
  34. package/dist/core/feedback.d.ts.map +1 -1
  35. package/dist/core/feedback.js +14 -2
  36. package/dist/core/feedback.js.map +1 -1
  37. package/dist/core/transaction.d.ts +11 -1
  38. package/dist/core/transaction.d.ts.map +1 -1
  39. package/dist/core/transaction.js +21 -4
  40. package/dist/core/transaction.js.map +1 -1
  41. package/dist/core/transport.d.ts +48 -6
  42. package/dist/core/transport.d.ts.map +1 -1
  43. package/dist/core/transport.js +126 -29
  44. package/dist/core/transport.js.map +1 -1
  45. package/dist/formats/camt.d.ts +28 -5
  46. package/dist/formats/camt.d.ts.map +1 -1
  47. package/dist/formats/camt.js +132 -50
  48. package/dist/formats/camt.js.map +1 -1
  49. package/dist/formats/mt940.d.ts +29 -5
  50. package/dist/formats/mt940.d.ts.map +1 -1
  51. package/dist/formats/mt940.js +209 -113
  52. package/dist/formats/mt940.js.map +1 -1
  53. package/dist/formats/readers.d.ts +6 -2
  54. package/dist/formats/readers.d.ts.map +1 -1
  55. package/dist/formats/readers.js +28 -29
  56. package/dist/formats/readers.js.map +1 -1
  57. package/dist/formats/record-budget.d.ts +7 -0
  58. package/dist/formats/record-budget.d.ts.map +1 -0
  59. package/dist/formats/record-budget.js +17 -0
  60. package/dist/formats/record-budget.js.map +1 -0
  61. package/dist/formats/sepa-purpose.d.ts +9 -0
  62. package/dist/formats/sepa-purpose.d.ts.map +1 -1
  63. package/dist/formats/sepa-purpose.js +32 -2
  64. package/dist/formats/sepa-purpose.js.map +1 -1
  65. package/dist/index.d.ts +3 -3
  66. package/dist/index.d.ts.map +1 -1
  67. package/dist/index.js +2 -2
  68. package/dist/index.js.map +1 -1
  69. package/dist/session/app-approval.d.ts +11 -5
  70. package/dist/session/app-approval.d.ts.map +1 -1
  71. package/dist/session/app-approval.js +23 -12
  72. package/dist/session/app-approval.js.map +1 -1
  73. package/dist/session/dialog-runner.d.ts +11 -11
  74. package/dist/session/dialog-runner.d.ts.map +1 -1
  75. package/dist/session/dialog-runner.js +34 -19
  76. package/dist/session/dialog-runner.js.map +1 -1
  77. package/dist/session/login-guard.d.ts +54 -49
  78. package/dist/session/login-guard.d.ts.map +1 -1
  79. package/dist/session/login-guard.js +170 -83
  80. package/dist/session/login-guard.js.map +1 -1
  81. package/dist/session/sepa-accounts.d.ts +20 -14
  82. package/dist/session/sepa-accounts.d.ts.map +1 -1
  83. package/dist/session/sepa-accounts.js +62 -46
  84. package/dist/session/sepa-accounts.js.map +1 -1
  85. package/dist/session/session-setup.d.ts +41 -24
  86. package/dist/session/session-setup.d.ts.map +1 -1
  87. package/dist/session/session-setup.js +106 -63
  88. package/dist/session/session-setup.js.map +1 -1
  89. package/dist/session/session-state.d.ts +10 -6
  90. package/dist/session/session-state.d.ts.map +1 -1
  91. package/dist/session/session-state.js +72 -18
  92. package/dist/session/session-state.js.map +1 -1
  93. package/dist/session/signed-dialog.d.ts +83 -0
  94. package/dist/session/signed-dialog.d.ts.map +1 -0
  95. package/dist/session/signed-dialog.js +185 -0
  96. package/dist/session/signed-dialog.js.map +1 -0
  97. package/dist/session/tan-selection.d.ts +26 -18
  98. package/dist/session/tan-selection.d.ts.map +1 -1
  99. package/dist/session/tan-selection.js +44 -33
  100. package/dist/session/tan-selection.js.map +1 -1
  101. package/dist/session/user-prompts.d.ts +14 -5
  102. package/dist/session/user-prompts.d.ts.map +1 -1
  103. package/dist/session/user-prompts.js +53 -18
  104. package/dist/session/user-prompts.js.map +1 -1
  105. package/dist/tan/challenge.d.ts +47 -24
  106. package/dist/tan/challenge.d.ts.map +1 -1
  107. package/dist/tan/challenge.js +56 -51
  108. package/dist/tan/challenge.js.map +1 -1
  109. package/dist/tan/flicker.d.ts +11 -1
  110. package/dist/tan/flicker.d.ts.map +1 -1
  111. package/dist/tan/flicker.js +70 -24
  112. package/dist/tan/flicker.js.map +1 -1
  113. package/dist/tan/tan-image.d.ts +6 -2
  114. package/dist/tan/tan-image.d.ts.map +1 -1
  115. package/dist/tan/tan-image.js +14 -6
  116. package/dist/tan/tan-image.js.map +1 -1
  117. package/dist/tan/tan-models.d.ts +14 -2
  118. package/dist/tan/tan-models.d.ts.map +1 -1
  119. package/dist/tan/tan-models.js +24 -7
  120. package/dist/tan/tan-models.js.map +1 -1
  121. package/dist/util/arguments.d.ts +13 -0
  122. package/dist/util/arguments.d.ts.map +1 -0
  123. package/dist/util/arguments.js +40 -0
  124. package/dist/util/arguments.js.map +1 -0
  125. package/dist/util/async.d.ts +4 -3
  126. package/dist/util/async.d.ts.map +1 -1
  127. package/dist/util/async.js +6 -7
  128. package/dist/util/async.js.map +1 -1
  129. package/dist/util/bytes.d.ts +3 -4
  130. package/dist/util/bytes.d.ts.map +1 -1
  131. package/dist/util/bytes.js +13 -21
  132. package/dist/util/bytes.js.map +1 -1
  133. package/dist/util/call-context.d.ts.map +1 -1
  134. package/dist/util/call-context.js +3 -2
  135. package/dist/util/call-context.js.map +1 -1
  136. package/dist/util/dates.d.ts +6 -20
  137. package/dist/util/dates.d.ts.map +1 -1
  138. package/dist/util/dates.js +58 -28
  139. package/dist/util/dates.js.map +1 -1
  140. package/dist/util/errors.d.ts +13 -3
  141. package/dist/util/errors.d.ts.map +1 -1
  142. package/dist/util/errors.js +19 -6
  143. package/dist/util/errors.js.map +1 -1
  144. package/dist/util/immutable-map.d.ts +5 -2
  145. package/dist/util/immutable-map.d.ts.map +1 -1
  146. package/dist/util/immutable-map.js +15 -12
  147. package/dist/util/immutable-map.js.map +1 -1
  148. package/dist/util/inspect.d.ts +11 -0
  149. package/dist/util/inspect.d.ts.map +1 -0
  150. package/dist/util/inspect.js +14 -0
  151. package/dist/util/inspect.js.map +1 -0
  152. package/dist/util/json.d.ts +4 -0
  153. package/dist/util/json.d.ts.map +1 -1
  154. package/dist/util/json.js +18 -0
  155. package/dist/util/json.js.map +1 -1
  156. package/dist/util/text.d.ts +38 -5
  157. package/dist/util/text.d.ts.map +1 -1
  158. package/dist/util/text.js +90 -16
  159. package/dist/util/text.js.map +1 -1
  160. package/dist/util/xml.d.ts +9 -6
  161. package/dist/util/xml.d.ts.map +1 -1
  162. package/dist/util/xml.js +129 -56
  163. package/dist/util/xml.js.map +1 -1
  164. package/dist/v3/account-extension.d.ts.map +1 -1
  165. package/dist/v3/account-extension.js +7 -10
  166. package/dist/v3/account-extension.js.map +1 -1
  167. package/dist/v3/bank-connection.d.ts +54 -0
  168. package/dist/v3/bank-connection.d.ts.map +1 -0
  169. package/dist/v3/bank-connection.js +118 -0
  170. package/dist/v3/bank-connection.js.map +1 -0
  171. package/dist/v3/dialog.d.ts +84 -46
  172. package/dist/v3/dialog.d.ts.map +1 -1
  173. package/dist/v3/dialog.js +174 -60
  174. package/dist/v3/dialog.js.map +1 -1
  175. package/dist/v3/message.d.ts +32 -11
  176. package/dist/v3/message.d.ts.map +1 -1
  177. package/dist/v3/message.js +144 -83
  178. package/dist/v3/message.js.map +1 -1
  179. package/dist/v3/parameter-store.d.ts +111 -0
  180. package/dist/v3/parameter-store.d.ts.map +1 -0
  181. package/dist/v3/parameter-store.js +197 -0
  182. package/dist/v3/parameter-store.js.map +1 -0
  183. package/dist/v3/parameters.d.ts +44 -17
  184. package/dist/v3/parameters.d.ts.map +1 -1
  185. package/dist/v3/parameters.js +136 -60
  186. package/dist/v3/parameters.js.map +1 -1
  187. package/dist/v3/protocol.d.ts +50 -50
  188. package/dist/v3/protocol.d.ts.map +1 -1
  189. package/dist/v3/protocol.js +91 -210
  190. package/dist/v3/protocol.js.map +1 -1
  191. package/dist/v3/queries.d.ts +51 -0
  192. package/dist/v3/queries.d.ts.map +1 -0
  193. package/dist/v3/queries.js +128 -0
  194. package/dist/v3/queries.js.map +1 -0
  195. package/dist/v3/segments.d.ts +43 -19
  196. package/dist/v3/segments.d.ts.map +1 -1
  197. package/dist/v3/segments.js +84 -33
  198. package/dist/v3/segments.js.map +1 -1
  199. package/dist/v3/syntax.d.ts +68 -13
  200. package/dist/v3/syntax.d.ts.map +1 -1
  201. package/dist/v3/syntax.js +183 -102
  202. package/dist/v3/syntax.js.map +1 -1
  203. package/package.json +10 -6
  204. package/src/client.ts +293 -0
  205. package/src/core/account.ts +153 -24
  206. package/src/core/amount.ts +45 -10
  207. package/src/core/balance.ts +12 -5
  208. package/src/core/bank-info.ts +29 -5
  209. package/src/core/client.ts +83 -19
  210. package/src/core/config.ts +95 -35
  211. package/src/core/errors.ts +148 -54
  212. package/src/core/feedback.ts +16 -2
  213. package/src/core/transaction.ts +22 -4
  214. package/src/core/transport.ts +153 -32
  215. package/src/formats/camt.ts +153 -45
  216. package/src/formats/mt940.ts +244 -122
  217. package/src/formats/readers.ts +28 -31
  218. package/src/formats/record-budget.ts +18 -0
  219. package/src/formats/sepa-purpose.ts +29 -2
  220. package/src/index.ts +4 -2
  221. package/src/session/app-approval.ts +51 -38
  222. package/src/session/dialog-runner.ts +61 -31
  223. package/src/session/login-guard.ts +189 -87
  224. package/src/session/sepa-accounts.ts +63 -45
  225. package/src/session/session-setup.ts +105 -68
  226. package/src/session/session-state.ts +74 -23
  227. package/src/session/signed-dialog.ts +222 -0
  228. package/src/session/tan-selection.ts +50 -39
  229. package/src/session/user-prompts.ts +62 -18
  230. package/src/tan/challenge.ts +87 -64
  231. package/src/tan/flicker.ts +72 -23
  232. package/src/tan/tan-image.ts +15 -6
  233. package/src/tan/tan-models.ts +26 -7
  234. package/src/util/arguments.ts +40 -0
  235. package/src/util/async.ts +10 -9
  236. package/src/util/bytes.ts +14 -20
  237. package/src/util/call-context.ts +3 -2
  238. package/src/util/dates.ts +64 -33
  239. package/src/util/errors.ts +22 -6
  240. package/src/util/immutable-map.ts +15 -12
  241. package/src/util/inspect.ts +14 -0
  242. package/src/util/json.ts +16 -0
  243. package/src/util/text.ts +87 -15
  244. package/src/util/xml.ts +130 -52
  245. package/src/v3/account-extension.ts +7 -12
  246. package/src/v3/bank-connection.ts +126 -0
  247. package/src/v3/dialog.ts +186 -92
  248. package/src/v3/message.ts +140 -79
  249. package/src/v3/parameter-store.ts +226 -0
  250. package/src/v3/parameters.ts +166 -66
  251. package/src/v3/protocol.ts +103 -253
  252. package/src/v3/queries.ts +198 -0
  253. package/src/v3/segments.ts +86 -31
  254. package/src/v3/syntax.ts +172 -93
  255. package/dist/session/bank-connection.d.ts +0 -28
  256. package/dist/session/bank-connection.d.ts.map +0 -1
  257. package/dist/session/bank-connection.js +0 -48
  258. package/dist/session/bank-connection.js.map +0 -1
  259. package/dist/session/parameter-data.d.ts +0 -83
  260. package/dist/session/parameter-data.d.ts.map +0 -1
  261. package/dist/session/parameter-data.js +0 -68
  262. package/dist/session/parameter-data.js.map +0 -1
  263. package/dist/session/pin-tan-client.d.ts +0 -54
  264. package/dist/session/pin-tan-client.d.ts.map +0 -1
  265. package/dist/session/pin-tan-client.js +0 -153
  266. package/dist/session/pin-tan-client.js.map +0 -1
  267. package/dist/session/pin-tan-dialog.d.ts +0 -71
  268. package/dist/session/pin-tan-dialog.d.ts.map +0 -1
  269. package/dist/session/pin-tan-dialog.js +0 -124
  270. package/dist/session/pin-tan-dialog.js.map +0 -1
  271. package/dist/session/protocol.d.ts +0 -209
  272. package/dist/session/protocol.d.ts.map +0 -1
  273. package/dist/session/protocol.js +0 -75
  274. package/dist/session/protocol.js.map +0 -1
  275. package/dist/session/response-handler.d.ts +0 -48
  276. package/dist/session/response-handler.d.ts.map +0 -1
  277. package/dist/session/response-handler.js +0 -121
  278. package/dist/session/response-handler.js.map +0 -1
  279. package/dist/v3/client.d.ts +0 -24
  280. package/dist/v3/client.d.ts.map +0 -1
  281. package/dist/v3/client.js +0 -16
  282. package/dist/v3/client.js.map +0 -1
  283. package/src/session/bank-connection.ts +0 -51
  284. package/src/session/parameter-data.ts +0 -137
  285. package/src/session/pin-tan-client.ts +0 -186
  286. package/src/session/pin-tan-dialog.ts +0 -160
  287. package/src/session/protocol.ts +0 -311
  288. package/src/session/response-handler.ts +0 -130
  289. package/src/v3/client.ts +0 -29
@@ -1,4 +1,5 @@
1
- import { formatIsoDate } from '../util/dates.js';
1
+ import { copyDate, formatIsoDate } from '../util/dates.js';
2
+ import { inspectCustom } from '../util/inspect.js';
2
3
  import type { Account } from './account.js';
3
4
  import type { Amount } from './amount.js';
4
5
 
@@ -59,18 +60,19 @@ export class Balance {
59
60
  /** The bank's name for the account, for example `Girokonto`. */
60
61
  readonly productName: string | undefined;
61
62
 
63
+ /** Takes copies of the dates of `init`: each balance has its own, which no other one shares. */
62
64
  constructor(init: BalanceInit) {
63
65
  this.account = init.account;
64
66
  this.booked = init.booked;
65
- this.bookedAsOf = init.bookedAsOf;
66
- this.bookedAsOfTime = init.bookedAsOfTime;
67
+ this.bookedAsOf = copyDate(init.bookedAsOf);
68
+ this.bookedAsOfTime = copyDate(init.bookedAsOfTime);
67
69
  this.pending = init.pending;
68
70
  this.creditLine = init.creditLine;
69
71
  this.available = init.available;
70
72
  this.used = init.used;
71
73
  this.overdraft = init.overdraft;
72
- this.bookingDate = init.bookingDate;
73
- this.bookingTime = init.bookingTime;
74
+ this.bookingDate = copyDate(init.bookingDate);
75
+ this.bookingTime = copyDate(init.bookingTime);
74
76
  this.productName = init.productName;
75
77
  }
76
78
 
@@ -81,4 +83,9 @@ export class Balance {
81
83
  toString(): string {
82
84
  return `Balance(${this.account.toString()}, ${formatIsoDate(this.bookedAsOf)})`;
83
85
  }
86
+
87
+ /** What Node.js's `util.inspect` and `console.log` show: `toString`, not the fields. */
88
+ [inspectCustom](): string {
89
+ return this.toString();
90
+ }
84
91
  }
@@ -1,4 +1,9 @@
1
- /** General bank information from the bank parameter data (BPD). */
1
+ import { inspectCustom } from '../util/inspect.js';
2
+
3
+ /**
4
+ * General bank information from the bank parameter data (BPD). The maximum message size and the timeouts are
5
+ * `undefined` where the bank states none, or more than 9999, the four digits HIBPA allows.
6
+ */
2
7
  export class BankInfo {
3
8
  /** German bank code (BLZ). */
4
9
  readonly bankCode: string;
@@ -15,7 +20,7 @@ export class BankInfo {
15
20
  /** Languages the bank's messages come in, as codes the bank defines. */
16
21
  readonly languages: readonly string[];
17
22
 
18
- /** Largest message in kilobytes (1024 bytes) the bank accepts; the client sends nothing larger. */
23
+ /** Largest message in kilobytes (1024 bytes) the bank accepts, `0` for no limit; the client sends nothing larger. */
19
24
  readonly maxMessageSize: number | undefined;
20
25
 
21
26
  /**
@@ -26,7 +31,7 @@ export class BankInfo {
26
31
 
27
32
  /**
28
33
  * Time in seconds after which the bank will likely end a dialog without messages, `0` for no limit (HIBPA, Formals
29
- * C.9). The client starts a new dialog for a call after that time.
34
+ * C.9). The client starts a new dialog for a call after that time, or after 5 minutes where the bank states none.
30
35
  */
31
36
  readonly maxTimeout: number | undefined;
32
37
 
@@ -49,9 +54,19 @@ export class BankInfo {
49
54
  this.minTimeout = init.minTimeout;
50
55
  this.maxTimeout = init.maxTimeout;
51
56
  }
57
+
58
+ /** What Node.js's `util.inspect` and `console.log` show: the bank code and the version of the parameter data. */
59
+ [inspectCustom](): string {
60
+ return `BankInfo(${this.bankCode}, BPD version ${this.bpdVersion})`;
61
+ }
52
62
  }
53
63
 
54
- /** Free-text message sent by the bank during login. */
64
+ /**
65
+ * Free-text message sent by the bank during login.
66
+ *
67
+ * Subject and text are the bank's, of any length: show them as text only, never as HTML (`innerHTML`, `v-html`), since
68
+ * they are no markup to trust.
69
+ */
55
70
  export class BankMessage {
56
71
  constructor(
57
72
  /** What the message is about. */
@@ -60,7 +75,16 @@ export class BankMessage {
60
75
  readonly text: string,
61
76
  ) {}
62
77
 
78
+ /**
79
+ * Neither subject nor text, which may address the user, so that logs and crash reports do not collect them; show
80
+ * `subject` and `text` to the user.
81
+ */
63
82
  toString(): string {
64
- return `${this.subject}: ${this.text}`;
83
+ return 'BankMessage(…)';
84
+ }
85
+
86
+ /** What Node.js's `util.inspect` and `console.log` show: `toString`, not the fields. */
87
+ [inspectCustom](): string {
88
+ return this.toString();
65
89
  }
66
90
  }
@@ -3,6 +3,7 @@ import type { TanMedium, TanMethod } from '../tan/tan-models.js';
3
3
  import type { Account } from './account.js';
4
4
  import type { Balance } from './balance.js';
5
5
  import type { BankInfo, BankMessage } from './bank-info.js';
6
+ import type { FinTsLoginRefusedError } from './errors.js';
6
7
  import type { FinTsFeedback } from './feedback.js';
7
8
  import type { Transaction, TransactionFormat } from './transaction.js';
8
9
  import type { FinTsTransport } from './transport.js';
@@ -25,6 +26,11 @@ export type WarningHandler = (warning: FinTsFeedback) => void;
25
26
  * An error thrown by a callback that only informs the app (`onBankMessage`, `onWarning`, `logger`) fails the call once
26
27
  * the call's exchange with the bank is over, so that it cannot break off a dialog half-way; an error of the call itself
27
28
  * takes precedence.
29
+ *
30
+ * The client reads the options once, when it is built: changing them later changes nothing. Its constructor throws
31
+ * `TypeError` for a `tanCallback` that is no function, another callback that is neither a function nor `undefined` or
32
+ * `null`, which mean none, a `state` that is neither a string nor either of these, and a `transport` without the
33
+ * functions `post` and `close`.
28
34
  */
29
35
  export interface FinTsClientOptions {
30
36
  /** Called whenever the bank asks for a TAN or an app approval. */
@@ -36,12 +42,17 @@ export interface FinTsClientOptions {
36
42
  /** Picks the TAN medium when the method needs one and the user has several; the first is used without it. */
37
43
  tanMediumSelector?: TanMediumSelector;
38
44
 
39
- /** Receives the free-text messages the bank sends during login. */
45
+ /**
46
+ * Receives the free-text messages the bank sends during login. How many there are and how long they are is the
47
+ * bank's choice, without a limit, so bound what the app shows of them; show their text as text, never as markup.
48
+ */
40
49
  onBankMessage?: BankMessageHandler;
41
50
 
42
51
  /**
43
52
  * Receives the bank's warnings, such as 3951 (switch to another TAN method), but not those the client acts on itself,
44
- * such as 3040 (more data follow). Show them to the user with code and text, as the specification asks.
53
+ * such as 3040 (more data follow). Show them to the user with code and text, as the specification asks. How many
54
+ * there are and how long they are is the bank's choice, without a limit, so bound what the app shows of them; show
55
+ * their text as text, never as markup.
45
56
  */
46
57
  onWarning?: WarningHandler;
47
58
 
@@ -49,9 +60,16 @@ export interface FinTsClientOptions {
49
60
  logger?: MessageLogger;
50
61
 
51
62
  /**
52
- * The `FinTsClient.state` of a previous session, which skips the bank setup steps.
63
+ * The `FinTsClient.state` of a previous session, which skips the bank setup steps. A refused login in it keeps the
64
+ * client from sending the PIN (`FinTsClient.loginRefusal`).
65
+ *
66
+ * A state belongs to one client at a time: restore the clients of one bank user one after another, each from the
67
+ * state the one before saved, never two at once from the same state. The state saved last wins, and could lift a
68
+ * refused login the other client saved.
53
69
  *
54
- * A state saved for another user or bank is ignored.
70
+ * A state saved for another user or bank is ignored, and of one saved for another server of the bank, such as before
71
+ * the bank moved its FinTS URL, or for FinTS 4.1 by the Dart package, only the refused login and the count of logins
72
+ * whose answer was lost hold.
55
73
  */
56
74
  state?: string | undefined;
57
75
 
@@ -76,40 +94,74 @@ export interface TransactionQuery {
76
94
  /**
77
95
  * Read access to a bank account over FinTS.
78
96
  *
79
- * All calls share one bank dialog, so strong authentication happens once per session. Calls run one after another; call
80
- * `close` when done.
97
+ * A call logs in where it needs to, which sends the PIN and may ask the callbacks for a TAN method, a TAN medium and an
98
+ * approval. Calls share one bank dialog while it is open, so that the approval of its login serves them all. The next
99
+ * call logs in anew, which may ask for another approval, once the dialog has idled longer than `BankInfo.maxTimeout`,
100
+ * after `selectTanMethod` and `clearLoginRefusal`, once the bank has ended the dialog, and after a message failed with
101
+ * another error than the bank's (`FinTsBankError`), such as a cancelled approval or a broken connection. Calls run one
102
+ * after another; call `close` when done.
81
103
  *
82
104
  * A callback in `FinTsClientOptions` must not wait for another call of its client, which would wait for the running
83
- * call. On Node.js, Deno and Bun such a call fails with `FinTsStateError`; elsewhere it would wait forever. `close` works
84
- * from anywhere.
105
+ * call. On Node.js, Deno 2.1 and Bun 1.2.6 or later such a call fails with `FinTsStateError`; elsewhere it waits
106
+ * forever. `close` works from anywhere.
85
107
  */
86
108
  export interface FinTsClient {
87
109
  /**
88
110
  * Serialized session data: bank and user parameters, customer system ID, the TAN methods the user may use, the chosen
89
- * TAN method and medium, and the SEPA account list.
111
+ * TAN method and medium, the SEPA account list, and a refused login (`loginRefusal`) with the count of logins whose
112
+ * answer was lost.
90
113
  *
91
- * Contains no PIN or TAN, but account details, so store it encrypted.
114
+ * Contains no PIN or TAN, but the user ID and customer ID, the customer system ID, the account holders' names and
115
+ * account details, and after a refused login the texts and parameters of the bank's return codes as the bank sent
116
+ * them, which for return code 3072 are the new login data; so store it encrypted. It holds what this client knows,
117
+ * and replaces the state it was restored from: one client at a time per state (`FinTsClientOptions.state`).
92
118
  */
93
119
  readonly state: string;
94
120
 
121
+ /**
122
+ * The refused login that keeps the client from sending the PIN again (`FinTsLoginRefusedError`); `undefined` while
123
+ * there is none. It is kept in `state` until `clearLoginRefusal`.
124
+ */
125
+ readonly loginRefusal: FinTsLoginRefusedError | undefined;
126
+
95
127
  /** Security function code of the TAN method in use, for example `942`; `undefined` before the first login. */
96
128
  readonly selectedTanMethod: string | undefined;
97
129
 
98
- /** Name of the TAN medium sent to the bank, if the method needs one. */
130
+ /**
131
+ * Name of the TAN medium in use, if the method needs one, as the app passed it or the bank listed it; one the app
132
+ * passed goes out without blanks at either end.
133
+ */
99
134
  readonly selectedTanMedium: string | undefined;
100
135
 
101
136
  /**
102
137
  * Switches to another TAN method, and optionally TAN medium, for the following calls.
103
138
  *
104
139
  * Ends the current dialog, so the next call logs in again with the new method. If the bank does not offer
105
- * `securityFunction`, the client picks another method at that login.
140
+ * `securityFunction`, the client picks another method at that login. Without `tanMedium`, also for `null`, that login
141
+ * looks the TAN media up where the method needs one. Rejects with `TypeError`, before it ends the dialog, for a
142
+ * `securityFunction` or `tanMedium` that is no string.
106
143
  */
107
144
  selectTanMethod(securityFunction: string, tanMedium?: string): Promise<void>;
108
145
 
146
+ /**
147
+ * Lifts `loginRefusal` and signs with `pin` from now on. Ends the current dialog, so that the next call logs in with
148
+ * it, and forgets the TAN medium, which the bank may have refused, such as that of a phone the user has replaced: where
149
+ * the TAN method needs one, the next call looks the media up anew and asks the `tanMediumSelector`.
150
+ *
151
+ * Pass a PIN the user has entered after the refusal, never a stored one unasked: banks lock the access after a few
152
+ * failed logins. Rejects with `TypeError` for a PIN the client cannot send, as its constructor throws, and with the
153
+ * refusal where it is a `FinTsLoginDataChangedError`: only a new client with the new login data can log in.
154
+ */
155
+ clearLoginRefusal(pin: string): Promise<void>;
156
+
109
157
  /** General information about the bank, from the bank parameter data. */
110
158
  getBankInfo(): Promise<BankInfo>;
111
159
 
112
- /** TAN procedures the bank allows for this user. */
160
+ /**
161
+ * TAN procedures the bank allows for this user, which the synchronization reports. Where the session has no customer
162
+ * system ID yet, as without a saved state, this call logs in for it: it may ask `tanMethodSelector` where the bank
163
+ * refuses the one-step method, and `tanCallback` for an approval.
164
+ */
113
165
  getTanMethods(): Promise<TanMethod[]>;
114
166
 
115
167
  /** TAN devices registered for this user. */
@@ -124,7 +176,9 @@ export interface FinTsClient {
124
176
  * Where the user parameter data carry the complete balance (account extension), possibly from the saved `state`, it
125
177
  * comes from there without a query; `Balance.bookedAsOf` tells the day it states.
126
178
  *
127
- * Throws `FinTsUnsupportedError`, without asking the bank, where the parameter data rule the query out.
179
+ * Rejects with `FinTsUnsupportedError` where the parameter data rule the query out, without sending it; the login,
180
+ * which delivers them, may have gone out. Rejects with `TypeError`, before it waits for other calls, for an `account`
181
+ * that is no `Account`, such as one written to JSON and read back.
128
182
  */
129
183
  getBalance(account: Account): Promise<Balance>;
130
184
 
@@ -133,17 +187,27 @@ export interface FinTsClient {
133
187
  * the same day in the bank's order.
134
188
  *
135
189
  * Only the local calendar day counts, and a day in the future counts as today. Banks typically ask for a TAN when
136
- * `from` is more than 90 days ago; see also `Account.dataStatus`. Throws `TypeError` if `from` lies on a day after
137
- * `to`, and `FinTsUnsupportedError`, without asking the bank, where the parameter data rule the query out.
190
+ * `from` is more than 90 days ago; see also `Account.dataStatus`. Rejects with `TypeError`, before it waits for other
191
+ * calls, if `account` is no `Account`, `from` or `to` is no valid `Date`, `from` lies on a day after `to` or `format`
192
+ * is no `TransactionFormat`, and with `FinTsUnsupportedError`, without sending the query, where the parameter data
193
+ * rule it out; the login may have gone out.
138
194
  */
139
195
  getTransactions(account: Account, query?: TransactionQuery): Promise<Transaction[]>;
140
196
 
141
197
  /**
142
- * Ends the bank dialog and releases the connection.
198
+ * Ends the bank dialog, releases the connection and forgets the PIN.
143
199
  *
144
200
  * A running call stops at its next step, also while it waits for a TAN, an app approval or a selection, and fails with
145
- * `FinTsStateError`, as do all later calls. A request already on its way is awaited, at most for the transport's
146
- * timeout.
201
+ * `FinTsStateError`, as do all later calls. A request already on its way has five seconds to bring its answer, which
202
+ * counts as any other: the logger gets it, and the client keeps what it brings, such as the parameter data or the
203
+ * customer system ID. The call fails with `FinTsStateError` all the same, also where that answer completes it, unless
204
+ * the answer brings an error, such as a rejected PIN, which the call fails with instead. After those five seconds the
205
+ * client stops waiting for the request, whatever the transport, and aborts it where the transport supports that, as
206
+ * `HttpTransport` does: the call fails with `FinTsTransportError` then, as after a broken connection, and an aborted
207
+ * login counts as one whose answer was lost. After the request, the call sends nothing but the end of its
208
+ * dialogs, that of an aborted message included, unless the message opened it. These dialog ends have five seconds
209
+ * once that request, if any, is over: then they are aborted the same way, and none goes out later, so that a bank
210
+ * that has stopped answering holds up neither `close` nor the call for longer.
147
211
  */
148
212
  close(): Promise<void>;
149
213
  }
@@ -1,38 +1,56 @@
1
+ import { optionalBoolean, optionalString, requireString } from '../util/arguments.js';
1
2
  import { invalidArgument } from '../util/errors.js';
3
+ import { inspectCustom } from '../util/inspect.js';
4
+ import { isLatin1, trimBlanks } from '../util/text.js';
2
5
 
3
- /** Numeric country code of Germany (ISO 3166), where FinTS banks are. */
6
+ /**
7
+ * The country code FinTS gives Germany, where FinTS banks are: the ISO 3166 code of the Federal Republic before 1990,
8
+ * not today's `276`.
9
+ */
4
10
  export const defaultCountryCode = '280';
5
11
 
12
+ /**
13
+ * Longest bank code (Kreditinstitutscode), user or customer ID (data type `id`), product ID (Produktbezeichnung) and
14
+ * product version FinTS 3.0 takes.
15
+ */
16
+ const maxBankCodeLength = 30;
17
+ const maxIdLength = 30;
18
+ const maxProductIdLength = 25;
19
+ const maxProductVersionLength = 5;
20
+
6
21
  /** What `FinTsConfig` takes. */
7
22
  export interface FinTsConfigInit {
8
- /** The bank's FinTS PIN/TAN endpoint; must be https. */
23
+ /** The bank's FinTS PIN/TAN endpoint; must be https, without user name or password. */
9
24
  url: string | URL;
10
- /** German bank code (BLZ). */
25
+ /** Bank code: for a German bank its BLZ, eight digits. */
11
26
  bankCode: string;
12
- /** Login name (Benutzerkennung / Legitimations-ID). */
27
+ /** Login name (Benutzerkennung / Legitimations-ID); at most 30 characters. */
13
28
  userId: string;
14
- /** Customer ID; defaults to `userId`. */
29
+ /** Customer ID, at most 30 characters; defaults to `userId`. */
15
30
  customerId?: string | undefined;
16
31
  /** FinTS product registration number from the Deutsche Kreditwirtschaft; at most 25 characters. */
17
32
  productId: string;
18
33
  /** Version of the app as registered with the product; at most five characters. `1.0` by default. */
19
34
  productVersion?: string;
20
- /** Numeric country code of the bank, `280` (Germany) by default. */
35
+ /** Country code of the bank, three digits: `280`, the default, for Germany. */
21
36
  countryCode?: string;
22
37
  /**
23
38
  * Allows plain http for the loopback addresses `localhost`, `127.x.x.x` and `::1`, for tests against a local server.
24
39
  *
25
- * Any other host must use https, whatever this says: the PIN travels with every message.
40
+ * Any other host must use https, whatever this says: the PIN travels with every message. The default `HttpTransport`
41
+ * sends plain http wherever the global `fetch` sends it, though, as it sends https: a replacement of that `fetch` and
42
+ * a global dispatcher get the PIN, and a proxy that `fetch` takes from the environment sees it unencrypted. See
43
+ * `HttpTransport`.
26
44
  */
27
45
  allowInsecureUrl?: boolean;
28
46
  }
29
47
 
30
- /** Connection and login data for one bank user. */
48
+ /** Connection and login data for one bank user; frozen, so that the client sends what the constructor has checked. */
31
49
  export class FinTsConfig {
32
- /** Numeric country code of Germany (ISO 3166), where FinTS banks are. */
50
+ /** The country code FinTS gives Germany, where FinTS banks are: `280`, not today's ISO 3166 code `276`. */
33
51
  static readonly defaultCountryCode = defaultCountryCode;
34
52
 
35
- readonly url: URL;
53
+ private readonly href: string;
36
54
  readonly bankCode: string;
37
55
  readonly userId: string;
38
56
  private readonly explicitCustomerId: string | undefined;
@@ -42,8 +60,17 @@ export class FinTsConfig {
42
60
  readonly allowInsecureUrl: boolean;
43
61
 
44
62
  /**
45
- * Throws `TypeError` if `url` is not a valid URL, has no host or is not https, unless `allowInsecureUrl` is set and
46
- * `url` is a loopback address, and if a login detail or the product is empty: the bank would refuse the login.
63
+ * Throws `TypeError` if `url` is not a valid URL, has no host, holds a user name or password, or is not https, unless
64
+ * `allowInsecureUrl` is set and `url` is a loopback address. Throws as well where the bank would refuse the login: for
65
+ * a login or product detail that is empty, has characters FinTS 3.0 cannot send (it uses ISO 8859-1, so `€` and most
66
+ * non-Latin letters are not possible) or is longer than its field (30 characters for the bank code, the user ID and
67
+ * the customer ID, 25 for the product ID, 5 for its version), for a country code of other than three digits, and for
68
+ * a German bank code of other than eight. Throws as well for a detail of another type than `FinTsConfigInit` names,
69
+ * such as a number as `bankCode` or the string `'false'` as `allowInsecureUrl`; `null` counts as a detail not given.
70
+ * The message leaves out the value.
71
+ *
72
+ * Drops leading and trailing blanks of the login details and the product, as FinTS sends them without (Formals
73
+ * B.4.1): `'alice '` is the user `alice`, also for a saved state.
47
74
  */
48
75
  constructor(init: FinTsConfigInit) {
49
76
  // `URL` would read `https:bank` and `https:///bank` as the host `bank`; such a URL is a mistake, not a host.
@@ -56,31 +83,61 @@ export class FinTsConfig {
56
83
  } catch {
57
84
  throw invalidArgument('Not a valid URL', 'url');
58
85
  }
59
- this.url = url;
60
- this.bankCode = init.bankCode;
61
- this.userId = init.userId;
62
- this.explicitCustomerId = init.customerId;
63
- this.productId = init.productId;
64
- this.productVersion = init.productVersion ?? '1.0';
65
- this.countryCode = init.countryCode ?? defaultCountryCode;
66
- this.allowInsecureUrl = init.allowInsecureUrl ?? false;
86
+ this.href = url.href;
87
+ this.allowInsecureUrl = optionalBoolean(init.allowInsecureUrl, 'allowInsecureUrl') ?? false;
67
88
  requireSecureUrl(url, this.allowInsecureUrl);
68
- requireNotBlank(this.bankCode, 'bankCode');
69
- requireNotBlank(this.userId, 'userId');
70
- if (this.explicitCustomerId !== undefined) requireNotBlank(this.explicitCustomerId, 'customerId');
71
- requireNotBlank(this.productId, 'productId');
72
- requireNotBlank(this.productVersion, 'productVersion');
73
- requireNotBlank(this.countryCode, 'countryCode');
89
+ this.countryCode = requireValue(init.countryCode ?? defaultCountryCode, 'countryCode');
90
+ if (!/^\d{3}$/.test(this.countryCode)) {
91
+ throw invalidArgument('Must be three digits, 280 for Germany', 'countryCode');
92
+ }
93
+ this.bankCode = requireValue(init.bankCode, 'bankCode', maxBankCodeLength);
94
+ if (this.countryCode === defaultCountryCode && !/^\d{8}$/.test(this.bankCode)) {
95
+ throw invalidArgument('A German bank code (BLZ) has eight digits', 'bankCode');
96
+ }
97
+ this.userId = requireValue(init.userId, 'userId', maxIdLength);
98
+ const customerId = optionalString(init.customerId, 'customerId');
99
+ this.explicitCustomerId =
100
+ customerId === undefined ? undefined : requireValue(customerId, 'customerId', maxIdLength);
101
+ this.productId = requireValue(init.productId, 'productId', maxProductIdLength);
102
+ this.productVersion = requireValue(init.productVersion ?? '1.0', 'productVersion', maxProductVersionLength);
103
+ Object.freeze(this);
104
+ }
105
+
106
+ /**
107
+ * The bank's FinTS PIN/TAN endpoint, as checked by the constructor. A new `URL` on each access, so that changing one
108
+ * cannot change where the client posts the PIN.
109
+ */
110
+ get url(): URL {
111
+ return new URL(this.href);
74
112
  }
75
113
 
76
114
  /** Customer ID; defaults to `userId`. */
77
115
  get customerId(): string {
78
116
  return this.explicitCustomerId ?? this.userId;
79
117
  }
118
+
119
+ /** What Node.js's `util.inspect` and `console.log` show: the bank code, neither the login data nor the product ID. */
120
+ [inspectCustom](): string {
121
+ return `FinTsConfig(${this.bankCode})`;
122
+ }
123
+ }
124
+
125
+ /**
126
+ * `value` without leading and trailing blanks; throws `TypeError` if it is no string, nothing is left, or what is left
127
+ * is not sendable (`requireLatin1`) or longer than `maxLength`: cut short, a value would be one the bank does not know.
128
+ */
129
+ function requireValue(value: unknown, name: string, maxLength = Number.POSITIVE_INFINITY): string {
130
+ const trimmed = trimBlanks(requireString(value, name));
131
+ if (trimmed === '') throw invalidArgument('Must not be empty', name);
132
+ requireLatin1(trimmed, name);
133
+ if (trimmed.length > maxLength) throw invalidArgument(`At most ${maxLength} characters in FinTS 3.0`, name);
134
+ return trimmed;
80
135
  }
81
136
 
82
- function requireNotBlank(value: string, name: string): void {
83
- if (value.trim() === '') throw invalidArgument('Must not be empty', name);
137
+ /** `text`, unless it has characters FinTS 3.0 cannot send (ISO 8859-1 only); the message leaves out the value. */
138
+ export function requireLatin1(text: string, name: string): string {
139
+ if (!isLatin1(text)) throw invalidArgument('Contains characters FinTS 3.0 cannot send (ISO 8859-1 only)', name);
140
+ return text;
84
141
  }
85
142
 
86
143
  /** The host of `url` without the brackets of an IPv6 address, as `URL` writes it. */
@@ -92,24 +149,27 @@ export function hostOf(url: URL): string {
92
149
  /**
93
150
  * Throws `TypeError` unless `url` names a host and uses https: the PIN travels with every message.
94
151
  *
95
- * With `allowInsecure`, http is accepted for loopback hosts only.
152
+ * With `allowInsecure`, http is accepted for loopback hosts only. Throws as well for a user name or password in `url`:
153
+ * `fetch` refuses such a URL, with the whole URL in its error message. The message leaves them out.
96
154
  */
97
- export function requireSecureUrl(url: URL, allowInsecure = false): void {
155
+ function requireSecureUrl(url: URL, allowInsecure = false): void {
98
156
  if (hostOf(url) === '') throw invalidArgument('The URL has no host', 'url');
157
+ if (url.username !== '' || url.password !== '') throw invalidArgument('The URL must not contain credentials', 'url');
99
158
  const loopbackHttp = allowInsecure && url.protocol === 'http:' && isLoopback(hostOf(url));
100
159
  if (url.protocol !== 'https:' && !loopbackHttp) throw invalidArgument('PIN/TAN requires an https URL', 'url');
101
160
  }
102
161
 
103
162
  /**
104
- * Throws `TypeError` for an empty PIN, which the bank would count as a failed attempt, and for one with leading or
105
- * trailing blanks: FinTS values carry none (Formals B.4.1), so such a PIN could only go out changed.
163
+ * `pin`, unless FinTS 3.0 cannot send it: throws `TypeError` for an empty PIN, which the bank would count as a failed
164
+ * attempt, for one with leading or trailing blanks, which would go out without them since FinTS values carry none
165
+ * (Formals B.4.1), and for one with characters outside ISO 8859-1.
106
166
  *
107
167
  * The message leaves out the value.
108
168
  */
109
169
  export function requirePin(pin: string): string {
110
- if (pin.trim() === '') throw invalidArgument('The PIN is empty', 'pin');
111
- if (pin.trim() !== pin) throw invalidArgument('The PIN begins or ends with a blank', 'pin');
112
- return pin;
170
+ if (trimBlanks(pin) === '') throw invalidArgument('The PIN is empty', 'pin');
171
+ if (trimBlanks(pin) !== pin) throw invalidArgument('The PIN begins or ends with a blank', 'pin');
172
+ return requireLatin1(pin, 'pin');
113
173
  }
114
174
 
115
175
  /**