@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
package/README.md CHANGED
@@ -2,25 +2,25 @@
2
2
 
3
3
  A FinTS 3.0 client for German banks, written in TypeScript, without runtime dependencies. It reads accounts, balances and transactions with PIN/TAN, including PSD2 strong customer authentication and app approval (pushTAN, SecureGo plus and the like).
4
4
 
5
- It is a port of the Dart package [`fints`](https://pub.dev/packages/fints) and behaves the same: same session logic, same safeguards, same tests. FinTS 4.1, which hardly any bank offers, is not part of the port.
5
+ It is a port of the Dart package [`fints`](https://pub.dev/packages/fints) (see [Differences from the Dart package](#differences-from-the-dart-package)). FinTS 4.1, which hardly any bank offers, is not part of the port.
6
6
 
7
7
  ## Features
8
8
 
9
9
  - **PIN/TAN** with the one-step method (`999`) and two-step methods of process variant 2 (`HKTAN` process 4, then 2 or `S`).
10
10
  - **Strong customer authentication** at login and per transaction where the bank asks for it, including exemptions (`3076`).
11
- - **App approval** (decoupled), polled by the bank's own parameters; the client stops when the app is closed or the user cancels.
12
- - **TAN media** lookup and selection, **chipTAN flicker codes** (HHD 1.3 and 1.4, as frames to animate) and **photoTAN/QR images** (as `data:` URLs).
11
+ - **App approval** (decoupled), polled as the bank's parameters say, for 10 minutes at most; the client stops when the app is closed or the user cancels.
12
+ - **TAN media** lookup and selection, **chipTAN flicker codes** (HHD 1.3 and 1.4, as frames to animate) and **photoTAN/QR images** (PNG, JPEG, GIF, WebP or BMP, as `data:` URLs for an `<img>`).
13
13
  - **Balances** (`HKSAL` 6–8) and **transactions** as camt.052 (`HKCAZ`) or MT940/MT942 (`HKKAZ` 6–7), with automatic paging.
14
14
  - **IBAN and BIC** from the SEPA account list (`HKSPA`), and the account extension of the user parameter data.
15
- - **Safe by default**: the PIN is never sent again after the bank refused a login, so a retrying app cannot lock the access; only what the bank's parameter data permit is sent; PIN and TAN are masked in every log.
16
- - **Saved sessions**: bank and user parameters, customer system ID and the chosen TAN method in one state string, which spares the setup steps at the next start.
17
- - Parsers for MT940 (German structured `:86:` fields, SEPA keywords) and camt.052/053.
15
+ - **Safe by default**: after a refused login the PIN is not sent again until the user enters it anew, also by a client restored from the saved state (see [Refused logins](#refused-logins)); only what the bank's parameter data permit is sent; PIN and TAN are masked in every log.
16
+ - **Saved sessions**: bank and user parameters, customer system ID, the chosen TAN method and a refused login in one state string, which spares the setup steps at the next start.
17
+ - Parsers for MT940 (German structured `:86:` fields, SEPA keywords) and camt.052/053/054, statement by statement with the account each is for (`Mt940.parse`, `Camt.parse`), or as one list of transactions (`parseTransactions`).
18
18
 
19
19
  ## Requirements
20
20
 
21
21
  1. **A FinTS product registration.** German banks only accept registered products. Register for free with the Deutsche Kreditwirtschaft at <https://www.fints.org/de/hersteller/produktregistrierung> and pass the ID as `productId`. Keep it out of your repository.
22
22
  2. **Your bank's FinTS URL and bank code (BLZ)**, for example from the DK institute list.
23
- 3. **A runtime with `fetch`, Web Crypto and `TextDecoder`**: Node.js 22.12 or later, Deno, Bun, or a browser behind a server of your own (see [Using it with Vue](#using-it-with-vue)). The type declarations need TypeScript 5.6 or later.
23
+ 3. **A runtime with `fetch`, Web Crypto and `TextDecoder`**: Node.js from 22.12, Deno from 2.1, Bun from 1.2.6, or a browser behind a server of your own (see [Using it with Vue](#using-it-with-vue)). The type declarations use the global `URL`, `AbortSignal` and `fetch` types: include the TypeScript lib `DOM` or `@types/node`.
24
24
 
25
25
  ## Usage
26
26
 
@@ -44,11 +44,13 @@ const client = new FinTs3Client({
44
44
  onWarning: (warning) => showNotice(warning.code, warning.text), // banks want code and text shown
45
45
  tanCallback: async (challenge) => {
46
46
  if (challenge.isDecoupled) {
47
- // Close the prompt when challenge.resolved settles; offer "approved" only if challenge.manualConfirmation.
47
+ // Close the prompt when challenge.resolved settles; the bank's new instructions come with the next call.
48
+ // Offer "approved" only if challenge.manualConfirmation.
48
49
  const confirmed = await showApprovalPrompt(challenge);
49
50
  return confirmed ? TanAnswer.approved : TanAnswer.cancelled;
50
51
  }
51
- // Show challenge.plainMessage, challenge.image?.toDataUrl() or challenge.flickerCode?.frames().
52
+ // Show challenge.plainMessage as text (textContent, never innerHTML), challenge.image?.toDataUrl() in an
53
+ // <img>, or challenge.flickerCode?.frames().
52
54
  const tan = await askForTan(challenge);
53
55
  return tan === undefined ? TanAnswer.cancelled : TanAnswer.tan(tan);
54
56
  },
@@ -60,12 +62,25 @@ try {
60
62
  const balance = await client.getBalance(accounts[0]!);
61
63
  const transactions = await client.getTransactions(accounts[0]!, { from: new Date(2024, 0, 1) });
62
64
  } finally {
63
- saveEncrypted(client.state); // no PIN or TAN, but account data
64
- await client.close();
65
+ try {
66
+ await client.close();
67
+ } finally {
68
+ saveEncrypted(client.state); // after close, whose answers may change it; no PIN or TAN, but login and account data
69
+ }
65
70
  }
66
71
  ```
67
72
 
68
- All calls share one bank dialog, so the user approves once per session. Calls run one after another; a call from inside a callback would wait for itself, so on Node.js, Deno and Bun it fails with `FinTsStateError` (in browsers it is not detected).
73
+ 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 approval; so does `getTanMethods` of a client without saved state. Calls share one bank dialog while it is open, so the approval of its login serves them all. The next call logs in anew, which may ask for another approval, once the dialog has idled longer than the bank allows (`BankInfo.maxTimeout`, 5 minutes where it states none), after `selectTanMethod` and `clearLoginRefusal`, once the bank has ended the dialog, and after a message failed with another error than a `FinTsBankError`, such as a cancelled or timed-out approval, a failing `tanCallback`, or a lost or unreadable answer. A query may need an approval of its own, such as one for transactions older than 90 days, and so may up to three of its further pages; where a fourth asks for one, the call fails with `FinTsProtocolError` before the user is asked.
74
+
75
+ Calls run one after another. A callback must not wait for another call of its client, which would wait for the running call: on Node.js, Deno and Bun such a call fails with `FinTsStateError`, in browsers it waits forever.
76
+
77
+ A saved state belongs to one client at a time. Run the clients of one bank user one after another, each restored from the state the one before saved, for example behind a lock or a queue per user, never two at once from the same state, such as for two browser tabs: the state saved last wins, and could lift a refused login the other client saved.
78
+
79
+ Never log the client: `JSON.stringify` and Node.js's `console.log` leave out the PIN, which the client keeps in a private field until it is closed or the login refused, but a debugger and the browser's DevTools show it.
80
+
81
+ `console.log` and `util.inspect` on Node.js, Deno and Bun show the objects of this package only by what identifies no person, account or TAN: codes, kinds, counts, dates and the last digits of an IBAN or card number, such as `Account(…3000, Girokonto)`, `TanChallenge(TanMethod(942, pushTAN), decoupled, attempt 1)` or `FinTsFeedback(9010)`. They leave out the bank's texts, references and parameters, TAN media names, TANs, challenge data and images, login data, and the amounts of balances and transactions; an `Amount` on its own shows as its `toString`, such as `-123.45 EUR`. `toString` of a `FinTsFeedback` and of a `BankMessage` leaves out the bank's texts as well: show `code` and `text`, or `subject` and `text`, to the user. An error shows its message and stack, and a `FinTsTransportError` its `statusCode`, also where Node.js reports it uncaught; the message of a `FinTsBankError`, and so its stack, holds the bank's texts and parameters of its errors, such as an IBAN, so log its `codes` instead. `JSON.stringify` and the browser console show every field, IBANs, names, amounts, purposes and the TAN of a `TanAnswer` included, so that a server can pass the data to its own front end: neither belongs in logs. Only `JSON.stringify` of an error leaves out the `feedback` of a `FinTsBankError` and the `source` of a `FormatError`.
82
+
83
+ An account the app builds itself, such as `new Account({ bankCode, iban })`, stands for the account of `getAccounts` it matches by IBAN or account number, whose details and permitted transactions the queries take. One the bank names nowhere goes out with what identifies it only, and fails with `FinTsUnsupportedError` where the bank's lists of permitted transactions are complete.
69
84
 
70
85
  Calendar dates, such as `Transaction.bookingDate`, are `Date`s at local midnight, of which only the day counts. Moments, such as `Balance.bookedAsOfTime`, are exact; the bank states them in German time.
71
86
 
@@ -75,29 +90,63 @@ Calendar dates, such as `Transaction.bookingDate`, are `Date`s at local midnight
75
90
 
76
91
  There are three ways to set it up:
77
92
 
78
- 1. **The client runs on your server** (Nuxt server routes, Express, …). The Vue app calls your own API; the server keeps one `FinTs3Client` per session and passes the TAN challenges to the browser, for example with a pending request the browser answers. This keeps the bank traffic and the product ID on the server.
93
+ 1. **The client runs on your server** (Nuxt server routes, Express, …). The Vue app calls your own API; the server keeps one `FinTs3Client` per session and passes the TAN challenges to the browser, for example with a pending request the browser answers. Two sessions of one bank user share the saved state, so run their clients one after another (see [Usage](#usage)). This keeps the bank traffic and the product ID on the server.
79
94
  2. **The client runs in the browser, through a relay.** The library runs in the Vue app, so TAN prompts are plain local UI, and a small endpoint of yours forwards each message to the bank. Pass a transport that posts to it:
80
95
 
81
96
  ```ts
82
- import type { FinTsTransport } from '@coderoftime/fints';
97
+ import { FinTsTransportError, type FinTsTransport } from '@coderoftime/fints';
83
98
 
84
99
  const relay: FinTsTransport = {
85
- async post(url, body, contentType) {
100
+ async post(url, body, contentType, signal) {
86
101
  const response = await fetch('/api/fints-relay', {
87
102
  method: 'POST',
88
103
  headers: { 'Content-Type': contentType, 'X-FinTS-URL': url.href },
89
- body,
104
+ body: body.slice(), // a copy on an ArrayBuffer of its own, the only kind TypeScript's fetch types take
105
+ signal, // aborted five seconds after the client is closed
106
+ redirect: 'error', // never send the PIN on to another address
107
+ cache: 'no-store',
90
108
  });
91
- if (!response.ok) throw new Error(`Relay: HTTP ${response.status}`);
109
+ if (!response.ok) {
110
+ // The bank's HTTP status, which the relay passes on and the client acts on (FinTsTransport.post).
111
+ throw new FinTsTransportError(`Relay: HTTP ${response.status}`, { statusCode: response.status });
112
+ }
92
113
  return new Uint8Array(await response.arrayBuffer());
93
114
  },
94
115
  close() {},
95
116
  };
96
117
  ```
97
118
 
98
- The relay must forward only to the bank URLs of the DK institute list, never to any URL a caller names, or it is an open proxy. The product ID ends up in the page; anyone can read it there.
119
+ Until it is closed, the client limits neither the time of an exchange nor the size of an answer, so a transport of your own has to, unless it goes through a relay that does, as this one does. The relay forwards with `HttpTransport`, which times out, limits the size of the answer and follows no redirect, and passes the bank's HTTP status on. A sketch with the web-standard `Request` and `Response`:
120
+
121
+ ```ts
122
+ import { FinTsTransportError, HttpTransport } from '@coderoftime/fints';
123
+
124
+ const bank = new HttpTransport();
125
+ const maxBodyBytes = 1024 * 1024; // far more than a message of the client takes
126
+
127
+ // Behind the app's login and a rate limit per session; bankUrl is the bank the user chose, as URL.href writes it.
128
+ export async function relay(request: Request, session: { bankUrl: string }): Promise<Response> {
129
+ if (request.headers.get('X-FinTS-URL') !== session.bankUrl) return new Response(null, { status: 403 });
130
+ // Checked before the body is read, of which the server reads no more than the length states.
131
+ const length = Number(request.headers.get('Content-Length') ?? NaN);
132
+ if (!(length <= maxBodyBytes)) return new Response(null, { status: 413 });
133
+ try {
134
+ const body = new Uint8Array(await request.arrayBuffer());
135
+ const answer = await bank.post(new URL(session.bankUrl), body, 'text/plain', request.signal);
136
+ return new Response(answer.slice(), { headers: { 'Content-Type': 'text/plain', 'Cache-Control': 'no-store' } });
137
+ } catch (error) {
138
+ // The bank's HTTP status, which the client acts on; 502 where the bank did not answer.
139
+ const status = error instanceof FinTsTransportError ? error.statusCode : undefined;
140
+ return new Response(null, { status: status ?? 502 });
141
+ }
142
+ }
143
+ ```
144
+
145
+ Anyone who can call the relay can send logins to the banks it forwards to, from your server's address, which a bank may then block for all your users. So serve it to the signed-in sessions of your app only, limit the requests of each session, and forward only to the bank the user chose for the session, from a list of known https URLs such as the DK institute list. The body holds PIN and TAN: never log it, on either side, nor put it in an error message, which the client passes on. And never mix up the answers of two sessions: the client cannot tell the answer to the first message of a dialog from that of another user. The product ID ends up in the page; anyone can read it there.
99
146
 
100
- 3. **The app is not a web page**: Electron, Tauri or Capacitor can send requests without CORS. Pass a `FinTsTransport` that uses the platform's HTTP client, or `HttpTransport` where `fetch` is not bound by CORS.
147
+ 3. **The app is not a web page**: Electron, Tauri or Capacitor can send requests without CORS. Pass a `FinTsTransport` that uses the platform's HTTP client without following redirects, with a timeout and a limit on the size of the answer, or `HttpTransport` where `fetch` is not bound by CORS.
148
+
149
+ Keep a client in the Vue app out of reactive state (`ref`, `reactive`, a Pinia store): Vue wraps it in a proxy there, which cannot reach the client's private fields, so every call fails with a `TypeError`. A plain variable, a `shallowRef` or `markRaw(client)` keeps the client as it is.
101
150
 
102
151
  A TAN prompt in Vue fits the callback as a promise the dialog resolves:
103
152
 
@@ -110,48 +159,97 @@ export const pending = ref<{ challenge: TanChallenge; answer: (a: TanAnswer) =>
110
159
  export function tanCallback(challenge: TanChallenge): Promise<TanAnswer> {
111
160
  return new Promise((answer) => {
112
161
  pending.value = { challenge, answer };
113
- void challenge.resolved?.then(() => (pending.value = undefined)); // the bank confirmed the app approval
162
+ void challenge.resolved?.then(() => (pending.value = undefined)); // the client needs no answer any more
114
163
  });
115
164
  }
116
165
  // The dialog component shows pending.value.challenge and calls pending.value.answer(TanAnswer.tan(input)),
117
166
  // TanAnswer.approved or TanAnswer.cancelled, then clears pending.value.
118
167
  ```
119
168
 
169
+ The dialog shows the challenge's text with `{{ }}`, which Vue escapes, never with `v-html`: `plainMessage` and `message`, like any text of the bank, are no markup to trust, and as HTML they could run scripts in the page where the user enters the PIN. The same holds for the bank messages and warnings of `onBankMessage` and `onWarning`, whose number and length the bank chooses without a limit, so bound what the app shows of them. `white-space: pre-line` keeps the line breaks of `plainMessage`:
170
+
171
+ ```vue
172
+ <div v-if="pending">
173
+ <p style="white-space: pre-line">{{ pending.challenge.plainMessage }}</p>
174
+ <img v-if="pending.challenge.image" :src="pending.challenge.image.toDataUrl()" alt="TAN image" />
175
+ </div>
176
+ ```
177
+
120
178
  ## Errors
121
179
 
122
- | Error | Meaning |
123
- | ---------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------ |
124
- | `FinTsPinError` | Wrong or blocked PIN. The client sends no PIN any more; create a new client to try again. |
125
- | `FinTsTanError` | Wrong, expired or used TAN. The session stays usable. |
126
- | `FinTsTanCancelledError` | The user cancelled the TAN or the app approval. |
127
- | `FinTsDecoupledTimeoutError` | The app approval did not come within the bank's limit. |
128
- | `FinTsLoginDataChangedError` | The bank issued new login data (`3072`); `loginName` tells it. |
129
- | `FinTsDialogAbortedError` | The bank ended the dialog; the next call logs in anew. |
130
- | `FinTsBankError` | Any other rejection; `feedback` holds the return codes. After a refused login, later calls fail with the same error without contacting the bank. |
131
- | `FinTsUnsupportedError` | The bank or the user's parameter data do not offer the transaction; nothing was sent. |
132
- | `FinTsProtocolError` | The bank's answer could not be read. |
133
- | `FinTsTransportError` | No connection, an HTTP error or a timeout. |
134
- | `FinTsStateError` | The client was closed, or called from inside one of its callbacks. |
135
- | `TypeError` | An invalid argument, such as an empty PIN or a period that ends before it starts; the message never contains the PIN. |
136
-
137
- The client never sends a message larger than the bank's maximum (`BankInfo.maxMessageSize`, in kilobytes), and never repeats a call by itself: the bank's answer cannot tell whether an order went through.
180
+ | Error | Meaning |
181
+ | ---------------------------- | --------------------------------------------------------------------------------------------------------------------- |
182
+ | `FinTsLoginRefusedError` | The bank refused the login. The client sends the PIN no more, see [Refused logins](#refused-logins). |
183
+ | `FinTsPinError` | Wrong or blocked PIN, a `FinTsLoginRefusedError`. |
184
+ | `FinTsTanError` | Wrong, expired or used TAN. The session stays usable. |
185
+ | `FinTsTanCancelledError` | The user cancelled the TAN or the app approval. |
186
+ | `FinTsDecoupledTimeoutError` | The app approval did not come within the requests the bank allows (60 where it states none) or 10 minutes. |
187
+ | `FinTsLoginDataChangedError` | The bank issued new login data (`3072`); `loginName` tells it. A `FinTsLoginRefusedError` for the old data. |
188
+ | `FinTsDialogAbortedError` | The bank ended the dialog; the next call logs in anew. |
189
+ | `FinTsBankError` | Any other rejection; `feedback` holds the return codes. |
190
+ | `FinTsUnsupportedError` | The parameter data do not offer the transaction; the client did not send it, though it may have logged in. |
191
+ | `FinTsTanSetupError` | No TAN method or medium to log in with; pass a medium to `selectTanMethod`, or choose another method. |
192
+ | `FinTsProtocolError` | The bank's answer could not be read. The message leaves out the data, which may be account data. |
193
+ | `FinTsTransportError` | No connection, an HTTP error (`statusCode`), a timeout or an abort by `close`. The message may have reached the bank. |
194
+ | `FinTsStateError` | The client was closed, or called from inside one of its callbacks. |
195
+ | `TypeError` | An invalid argument, such as an empty PIN or a TAN that the TAN method rules out; the message never shows PIN or TAN. |
196
+
197
+ The client never repeats a call by itself: the bank's answer cannot tell whether an order went through.
198
+
199
+ It reads only complete answers to the message it sent: one cut short, broken, or with the number or dialog ID of another message fails the call with `FinTsProtocolError`. Only the bank's dialog abort message, which names neither (dialog ID `unbekannt`, message number 9999), answers any message: it fails the call with a `FinTsBankError` that holds the bank's text, and ends the dialog. The first message of every dialog goes out with the same number and dialog ID, though, so the answer to another user's first message passes: a relay must never mix up answers. A query whose pages hold more than 128 MiB, each data element counted as 512 bytes, or more than 200,000 statements and transactions, fails with `FinTsProtocolError` as well: ask for a shorter period then. So that a broken or hostile server cannot block the app or exhaust its memory, the client also refuses an answer with more than 1,000,000 data elements and segments in its plain or in its encrypted part, or with more than one encrypted part; XML nested more than 100 levels deep, or with more than 500,000 nodes besides text or one per 16 characters, whichever is more; an MT940 field or camt remittance information (`Ustrd`) of more than 65,536 characters; and an amount written with more than 64 characters. `Mt940.parse`, `Camt.parse` and `Amount.parseDecimal` throw `FormatError` for these.
200
+
201
+ MT940 statements must balance. Where a bank wraps a purpose at a fixed width, a payer could start a line of it like a field, such as `:61:`, which would add a transaction; `getTransactions` fails with `FinTsProtocolError` instead, and `Mt940.parse` with `FormatError`. The check misses such lines that add up to nothing, and statements without balances, such as pending transactions (MT942).
202
+
203
+ ## Refused logins
204
+
205
+ Banks lock the online access after a few failed logins. So once the bank has refused a login, the client does not send the PIN again: every call that would fails with the refusal, a `FinTsLoginRefusedError`, without contacting the bank. Any error in the answer to a login refuses it, since a return code does not always tell whether the PIN was the reason, and so does a wrong PIN the bank reports later in the dialog.
206
+
207
+ Only where the bank names a reason the client can take care of does the same call log in once more, once per reason: with another TAN method where the bank lists those the user may use without the one the login was signed with (`3920`, after the one-step method only next to no error but `9050`, `9800` or `9955`) or demands strong authentication of a one-step login (`9075`), and with a new customer system ID where the bank rejects the old one (`9391`). None of them follows a wrong PIN, and a call sends at most three logins the bank refuses.
208
+
209
+ A login whose answer is lost, times out, is aborted by `close` or cannot be read may have counted as a failed attempt all the same. After two such logins in a row the client refuses the login itself, with a `FinTsLoginRefusedError` whose `feedback` is empty; it cannot tell a login that never reached the bank, so a connection that fails twice in a row stops it as well. A login the bank takes clears the count.
210
+
211
+ The refusal and that count are part of `client.state`, so a client restored from it does not send the PIN either, as long as the clients of the user take turns with the state (see [Usage](#usage)); where their part of the state is damaged, the client restores a refusal whose `feedback` is empty. `client.loginRefusal` tells it before any call. Lift it with a PIN the user has entered anew, unless the bank has changed the login data:
212
+
213
+ ```ts
214
+ const refusal = client.loginRefusal;
215
+ if (refusal instanceof FinTsLoginDataChangedError) {
216
+ // The old login data are retired: build a new client with refusal.loginName as userId, or its userId and customerId.
217
+ } else if (refusal !== undefined) {
218
+ showNotice(refusal.message); // the bank's errors, or why the client stopped by itself
219
+ await client.clearLoginRefusal(await askForPin());
220
+ }
221
+ ```
222
+
223
+ Never pass `clearLoginRefusal` a stored PIN without asking the user: that is how a retrying app locks the access. It also forgets the TAN medium, which the bank may have refused instead, such as that of a replaced phone, so that the next call looks the media up anew.
138
224
 
139
225
  ## Differences from the Dart package
140
226
 
141
- - Errors are named `…Error` instead of `…Exception`; invalid arguments are `TypeError`s, malformed data `FormatError`s.
227
+ This section compares with the Dart package as its source stands after its release 0.2.0, which differs in more ways. Both keep the saved state in the same layout, a refused login included, so each restores the other's. The Dart package 0.2.0 keeps no refused login, though: restored from a state of this package, it sends the PIN the bank refused. They differ in:
228
+
229
+ - Errors are named `…Error` instead of `…Exception`; invalid arguments are `TypeError`s, malformed data `FormatError`s, and a closed client or a call from inside a callback a `FinTsStateError`. Some messages are worded differently.
142
230
  - `getTransactions(account, { from, to, format })` takes an options object; enums are string unions such as `'mt940'`.
143
231
  - The polling parameters of `TanMethod` are in seconds: `decoupledFirstPollDelaySeconds`, `decoupledPollIntervalSeconds`.
144
- - `HttpTransport` takes a `fetch` and `timeoutMs`; `TanImage.toDataUrl()` is new.
145
- - The saved state has the same layout, so a state saved by one package can be restored by the other.
232
+ - No FinTS 4.1, so no `FinTs4Client`, `FinTsClient.version` or `BankInfo.bic`. `FinTsConfig` checks the login data against what FinTS 3.0 can send, which the Dart package does in `FinTs3Client`, and also refuses a user ID or customer ID of more than 30 characters, which the Dart package sends.
233
+ - `HttpTransport` takes a `fetch` and `timeoutMs` instead of an HTTP client and a `Duration`. `FinTsTransport.post` takes an `AbortSignal`, with which `close` aborts a request on its way after five seconds, also through a custom transport, and then stops waiting for it whatever the transport does; the Dart package aborts only its own `HttpTransport` (`abortRequests`) and waits for any other transport up to that transport's timeout.
234
+ - HTTP goes through the runtime's `fetch`, with its proxies, timeouts and trusted certificates. Plain http to a local server, which `allowInsecureUrl` allows, goes wherever the global `fetch` sends it, as https does: to whatever replaces `globalThis.fetch`, such as a test's stub, to a global dispatcher on Node.js, and through any proxy that `fetch` takes from the environment (Node.js with `NODE_USE_ENV_PROXY=1`, Deno and Bun with `HTTP_PROXY`), which sees the PIN. Exempt the host with `NO_PROXY=localhost,127.0.0.1,::1` or pass a `fetch` that connects directly. The Dart package connects to the loopback host directly, also bypassing a client set with `runWithClient`, and on native platforms follows `HTTPS_PROXY` and `NO_PROXY` for https, which Node.js does only with `NODE_USE_ENV_PROXY=1`.
235
+ - A call from inside a callback fails on Node.js, Deno and Bun only; in browsers it waits forever, where the Dart package fails on every platform.
236
+ - `console.log` and `util.inspect` show the objects of this package as [Usage](#usage) describes, where the Dart package prints most of them as `Instance of '…'`, and `toString` of a `FinTsFeedback` or a `BankMessage` leaves out the bank's texts, which the Dart package prints. The browser console shows every field, where the Dart package on the web prints `toString`.
237
+ - `JSON.stringify` gives every field of a model, the TAN of a `TanAnswer` included, and of an error its `name`, and the `statusCode` of a `FinTsTransportError`; the Dart package's models and errors have no JSON form.
238
+ - Some bank data are read and sent otherwise. In MT940, the counterparty's IBAN and BIC come from the subfields `?31` and `?30` only, not from `IBAN+` and `BIC+` in the payer's reference, a bank code in `?30` next to an IBAN is the bank code, the customer reference `KREF+` stands for the one of `:86:`, and the balance date `0` or `000000` for none. A camt purpose falls back to the structured creditor reference, and remittance information of more than 65,536 characters is refused. Challenge text counts as a combined chipTAN challenge only where it starts with `CHLGUC` and states the length of its code. The bank's dialog abort message fails the call with a `FinTsBankError`. A balance or transaction query that names the account by its international account (KTI) leaves out the national account where the bank parameter data rule it out, unless the account has no BIC, and sends no IBAN without its BIC, unless the account has no account number. A one-step login that the bank answers with `9955` and `3920` is repeated with a two-step method.
146
239
 
147
240
  ## Development
148
241
 
149
242
  ```sh
150
243
  npm install
151
- npm run check # type check, lint, format check and tests
244
+ npm run check # type check, lint, unused code, format check and tests with coverage
152
245
  npm run build
153
246
  ```
154
247
 
248
+ The check fails for Node.js APIs in `src/` that browsers lack, for files, exports and dependencies nothing uses, or only
249
+ the tests ([knip](https://knip.dev)), and for coverage below the thresholds in `vitest.config.ts`, which each module
250
+ that guards what goes to and comes from the bank has to reach on its own. The coverage report is in
251
+ `coverage/index.html`.
252
+
155
253
  ## License
156
254
 
157
255
  BSD 3-Clause, see [LICENSE](LICENSE). Commercial use is allowed; a product that ships the library must reproduce its copyright notice and license in its documentation or notices.
@@ -0,0 +1,49 @@
1
+ import { Account } from './core/account.js';
2
+ import type { Balance } from './core/balance.js';
3
+ import type { BankInfo } from './core/bank-info.js';
4
+ import type { FinTsClient, FinTsClientOptions, TransactionQuery } from './core/client.js';
5
+ import { FinTsConfig, type FinTsConfigInit } from './core/config.js';
6
+ import { type FinTsLoginRefusedError } from './core/errors.js';
7
+ import type { Transaction } from './core/transaction.js';
8
+ import { TanMedium, type TanMethod } from './tan/tan-models.js';
9
+ /** What `FinTs3Client` takes. */
10
+ export interface FinTs3ClientInit {
11
+ /** Where and as whom to log in: a `FinTsConfig`, or what its constructor takes. */
12
+ config: FinTsConfig | FinTsConfigInit;
13
+ /**
14
+ * The user's PIN. The client keeps it in memory only, never logs it, and forgets it once it is closed or the bank has
15
+ * refused the login. It is out of reach of `JSON.stringify` and Node.js's `util.inspect` of the client, but not of a
16
+ * debugger or the browser's DevTools: never log the client. A client whose `options.state` holds a refused login
17
+ * does not send it (`FinTsClient.loginRefusal`).
18
+ */
19
+ pin: string;
20
+ /** How the client reaches the user and the bank. */
21
+ options: FinTsClientOptions;
22
+ }
23
+ /** FinTS 3.0 PIN/TAN client, the version German banks use. */
24
+ export declare class FinTs3Client implements FinTsClient {
25
+ #private;
26
+ /**
27
+ * A client with the session restored from `FinTsClientOptions.state` where it was saved for this user and bank.
28
+ *
29
+ * Throws `TypeError` if the configuration is invalid, as the constructor of `FinTsConfig` tells, or the PIN is empty,
30
+ * begins or ends with a blank, or has characters FinTS 3.0 cannot send: it uses ISO 8859-1, so `€` and most
31
+ * non-Latin letters are not possible. Throws `TypeError` as well for an option of another type than
32
+ * `FinTsClientOptions` names, as `FinTsClientOptions` tells.
33
+ */
34
+ constructor(init: FinTs3ClientInit);
35
+ get state(): string;
36
+ get loginRefusal(): FinTsLoginRefusedError | undefined;
37
+ get selectedTanMethod(): string | undefined;
38
+ get selectedTanMedium(): string | undefined;
39
+ selectTanMethod(securityFunction: string, tanMedium?: string): Promise<void>;
40
+ clearLoginRefusal(pin: string): Promise<void>;
41
+ getBankInfo(): Promise<BankInfo>;
42
+ getTanMethods(): Promise<TanMethod[]>;
43
+ getTanMedia(): Promise<TanMedium[]>;
44
+ getAccounts(): Promise<Account[]>;
45
+ getBalance(account: Account): Promise<Balance>;
46
+ getTransactions(account: Account, query?: TransactionQuery): Promise<Transaction[]>;
47
+ close(): Promise<void>;
48
+ }
49
+ //# sourceMappingURL=client.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,mBAAmB,CAAC;AAC5C,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,mBAAmB,CAAC;AACjD,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,qBAAqB,CAAC;AACpD,OAAO,KAAK,EAAE,WAAW,EAAE,kBAAkB,EAAE,gBAAgB,EAAE,MAAM,kBAAkB,CAAC;AAC1F,OAAO,EAAE,WAAW,EAAc,KAAK,eAAe,EAAE,MAAM,kBAAkB,CAAC;AACjF,OAAO,EAA0C,KAAK,sBAAsB,EAAE,MAAM,kBAAkB,CAAC;AACvG,OAAO,KAAK,EAAE,WAAW,EAAqB,MAAM,uBAAuB,CAAC;AAS5E,OAAO,EAAE,SAAS,EAAE,KAAK,SAAS,EAAE,MAAM,qBAAqB,CAAC;AAUhE,iCAAiC;AACjC,MAAM,WAAW,gBAAgB;IAC/B,mFAAmF;IACnF,MAAM,EAAE,WAAW,GAAG,eAAe,CAAC;IACtC;;;;;OAKG;IACH,GAAG,EAAE,MAAM,CAAC;IACZ,oDAAoD;IACpD,OAAO,EAAE,kBAAkB,CAAC;CAC7B;AA2CD,8DAA8D;AAC9D,qBAAa,YAAa,YAAW,WAAW;;IAkC9C;;;;;;;OAOG;gBACS,IAAI,EAAE,gBAAgB;IAyBlC,IAAI,KAAK,IAAI,MAAM,CAElB;IAED,IAAI,YAAY,IAAI,sBAAsB,GAAG,SAAS,CAErD;IAED,IAAI,iBAAiB,IAAI,MAAM,GAAG,SAAS,CAE1C;IAED,IAAI,iBAAiB,IAAI,MAAM,GAAG,SAAS,CAE1C;IAEK,eAAe,CAAC,gBAAgB,EAAE,MAAM,EAAE,SAAS,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAUlF,iBAAiB,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAU7C,WAAW,IAAI,OAAO,CAAC,QAAQ,CAAC;IAIhC,aAAa,IAAI,OAAO,CAAC,SAAS,EAAE,CAAC;IAIrC,WAAW,IAAI,OAAO,CAAC,SAAS,EAAE,CAAC;IAenC,WAAW,IAAI,OAAO,CAAC,OAAO,EAAE,CAAC;IAI3B,UAAU,CAAC,OAAO,EAAE,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC;IAe9C,eAAe,CAAC,OAAO,EAAE,OAAO,EAAE,KAAK,GAAE,gBAAqB,GAAG,OAAO,CAAC,WAAW,EAAE,CAAC;IAqB7F,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;CA4CvB"}
package/dist/client.js ADDED
@@ -0,0 +1,242 @@
1
+ import { Account } from './core/account.js';
2
+ import { FinTsConfig, requirePin } from './core/config.js';
3
+ import { FinTsStateError, FinTsUnsupportedError } from './core/errors.js';
4
+ import { DialogRunner } from './session/dialog-runner.js';
5
+ import { LoginGuard } from './session/login-guard.js';
6
+ import { SepaAccounts } from './session/sepa-accounts.js';
7
+ import { SessionSetup } from './session/session-setup.js';
8
+ import { restoreSession, saveSession } from './session/session-state.js';
9
+ import { TanSelection } from './session/tan-selection.js';
10
+ import { UserPrompts } from './session/user-prompts.js';
11
+ import { TanMedium } from './tan/tan-models.js';
12
+ import { optionalString, requireString } from './util/arguments.js';
13
+ import { SerialExecutor } from './util/async.js';
14
+ import { CallContext } from './util/call-context.js';
15
+ import { calendarDay, notAfterToday } from './util/dates.js';
16
+ import { invalidArgument } from './util/errors.js';
17
+ import { ParameterStore } from './v3/parameter-store.js';
18
+ import { FinTs3Protocol } from './v3/protocol.js';
19
+ import { balanceQuery, tanMediaQuery, transactionsQuery } from './v3/queries.js';
20
+ /** A token of the client call running now, so that a call from inside its callbacks is recognized. */
21
+ const runningCalls = new CallContext();
22
+ /**
23
+ * `transport`, `undefined` for `null`; throws `TypeError` unless it is neither or has the functions of a
24
+ * `FinTsTransport`.
25
+ */
26
+ function optionalTransport(transport) {
27
+ if (transport === undefined || transport === null)
28
+ return undefined;
29
+ const candidate = transport;
30
+ if (typeof candidate.post !== 'function' || typeof candidate.close !== 'function') {
31
+ throw invalidArgument('Must have the functions post and close', 'transport');
32
+ }
33
+ return transport;
34
+ }
35
+ /** Throws `TypeError` for anything but an `Account`, such as one written to JSON and read back. */
36
+ function requireAccount(account) {
37
+ if (!(account instanceof Account))
38
+ throw invalidArgument('Must be an Account', 'account');
39
+ }
40
+ /**
41
+ * The calendar day of `bound`, the `name` bound of a `TransactionQuery`: the bank takes dates without time of day.
42
+ * Throws `TypeError` for anything but a valid `Date`, such as an Invalid Date, which would go out as `0NaNNaNNaN`.
43
+ */
44
+ function requestedDay(bound, name) {
45
+ if (bound === undefined)
46
+ return undefined;
47
+ if (!(bound instanceof Date) || Number.isNaN(bound.getTime()))
48
+ throw invalidArgument('Not a valid Date', name);
49
+ return calendarDay(bound);
50
+ }
51
+ /**
52
+ * The format a `TransactionQuery` asks for with `format`: `auto` where it is `undefined`. Throws `TypeError` for anything
53
+ * but a `TransactionFormat`, such as `'CAMT'` or `null`.
54
+ */
55
+ function requestedFormat(format) {
56
+ if (format === undefined)
57
+ return 'auto';
58
+ if (format === 'auto' || format === 'mt940' || format === 'camt')
59
+ return format;
60
+ throw invalidArgument("Must be 'auto', 'mt940' or 'camt'", 'format');
61
+ }
62
+ /** FinTS 3.0 PIN/TAN client, the version German banks use. */
63
+ export class FinTs3Client {
64
+ // A call such as `getBalance` runs through these parts:
65
+ // 1. `DialogRunner.open` reuses the session's dialog or logs in, through `SessionSetup.remedied`, which logs in once
66
+ // more for each refusal it can take care of.
67
+ // 2. `SessionSetup.prepare` first settles what is open: the bank parameter data, the synchronization, and through
68
+ // `TanSelection` the TAN method and medium.
69
+ // 3. `SignedDialog.login` sends the login and completes the strong authentication the bank asks for, a TAN or an app
70
+ // approval (`AppApprovalWait`).
71
+ // 4. `DialogRunner.query` sends the order in that dialog, completes its approval the same way, and fetches all pages.
72
+ //
73
+ // `FinTs3Protocol` builds, signs and posts the messages of each `FinTs3Dialog`, the queries build the orders and read
74
+ // the answers, and the `ParameterStore` keeps the bank's parameter data and IDs. `SepaAccounts` completes IBAN and
75
+ // BIC, and `UserPrompts` is everything the app is told or asked. The `LoginGuard` holds the PIN: the protocol takes it
76
+ // from the guard for every message it signs, and `SignedDialog` has the guard judge every answer, so that the PIN does
77
+ // not go out after a refusal.
78
+ //
79
+ // The fields are ECMAScript private, so that neither `util.inspect` nor `JSON.stringify` of a client reaches the PIN
80
+ // or the account data behind them.
81
+ #prompts;
82
+ #guard;
83
+ #parameters;
84
+ #protocol;
85
+ #sessionState;
86
+ #tans;
87
+ #setup;
88
+ #dialogs;
89
+ #sepa;
90
+ #queue = new SerialExecutor();
91
+ #closed;
92
+ /** The call running now, which a call from inside its callbacks would wait for. */
93
+ #runningCall;
94
+ /**
95
+ * A client with the session restored from `FinTsClientOptions.state` where it was saved for this user and bank.
96
+ *
97
+ * Throws `TypeError` if the configuration is invalid, as the constructor of `FinTsConfig` tells, or the PIN is empty,
98
+ * begins or ends with a blank, or has characters FinTS 3.0 cannot send: it uses ISO 8859-1, so `€` and most
99
+ * non-Latin letters are not possible. Throws `TypeError` as well for an option of another type than
100
+ * `FinTsClientOptions` names, as `FinTsClientOptions` tells.
101
+ */
102
+ constructor(init) {
103
+ const config = init.config instanceof FinTsConfig ? init.config : new FinTsConfig(init.config);
104
+ const { state, transport } = init.options;
105
+ this.#prompts = new UserPrompts(init.options);
106
+ this.#guard = new LoginGuard(requirePin(init.pin));
107
+ this.#parameters = new ParameterStore(config);
108
+ this.#protocol = new FinTs3Protocol(this.#parameters, this.#guard, optionalTransport(transport), this.#prompts.logger);
109
+ this.#sessionState = restoreSession(optionalString(state, 'state'), this.#parameters, this.#guard);
110
+ this.#tans = new TanSelection(this.#protocol, this.#sessionState, this.#prompts);
111
+ const session = {
112
+ parameters: this.#parameters,
113
+ state: this.#sessionState,
114
+ prompts: this.#prompts,
115
+ guard: this.#guard,
116
+ };
117
+ this.#setup = new SessionSetup(session, this.#protocol, this.#tans);
118
+ this.#dialogs = new DialogRunner(this.#setup);
119
+ this.#sepa = new SepaAccounts(this.#parameters, this.#sessionState, this.#dialogs);
120
+ }
121
+ get state() {
122
+ return saveSession(this.#parameters, this.#guard, this.#sessionState);
123
+ }
124
+ get loginRefusal() {
125
+ return this.#guard.refusal;
126
+ }
127
+ get selectedTanMethod() {
128
+ return this.#sessionState.tanMethod;
129
+ }
130
+ get selectedTanMedium() {
131
+ return this.#sessionState.tanMedium;
132
+ }
133
+ async selectTanMethod(securityFunction, tanMedium) {
134
+ // Checked before the dialog ends; an async method, so that every argument error is a rejection.
135
+ const method = requireString(securityFunction, 'securityFunction');
136
+ const medium = optionalString(tanMedium, 'tanMedium');
137
+ return this.#serialized(async () => {
138
+ await this.#dialogs.end();
139
+ this.#tans.select(method, medium);
140
+ });
141
+ }
142
+ clearLoginRefusal(pin) {
143
+ return this.#serialized(async () => {
144
+ const sendable = requirePin(pin);
145
+ // The dialog ends with the PIN it was opened with.
146
+ await this.#dialogs.end();
147
+ this.#guard.replacePin(sendable);
148
+ this.#tans.forgetMedium();
149
+ });
150
+ }
151
+ getBankInfo() {
152
+ return this.#serialized(() => this.#setup.remedied(() => this.#setup.bankInfo()));
153
+ }
154
+ getTanMethods() {
155
+ return this.#serialized(() => this.#setup.remedied(() => this.#setup.methods()));
156
+ }
157
+ getTanMedia() {
158
+ return this.#serialized(async () => {
159
+ // The session keeps the list of a lookup that found no medium; the app gets media of its own.
160
+ const lookedUp = (await this.#setup.remedied(() => this.#setup.prepare())) ?? this.#tans.lookedUpMedia;
161
+ return (lookedUp?.map((m) => new TanMedium(m)) ??
162
+ (await this.#dialogs.query(() => {
163
+ const query = tanMediaQuery(this.#parameters);
164
+ if (query === undefined)
165
+ throw new FinTsUnsupportedError('The bank does not list TAN media');
166
+ return query;
167
+ })));
168
+ });
169
+ }
170
+ getAccounts() {
171
+ return this.#serialized(() => this.#sepa.completedAccounts());
172
+ }
173
+ async getBalance(account) {
174
+ // Checked before the call waits for others; an async method, so that every argument error is a rejection.
175
+ requireAccount(account);
176
+ return this.#serialized(async () => {
177
+ const completed = await this.#sepa.complete(account);
178
+ // The dialog start delivers the user parameter data, which may carry the complete balance: then the client must
179
+ // not ask for it separately (Formals E.3.1.1, F.3.2).
180
+ await this.#dialogs.open();
181
+ return (this.#parameters.userParameters?.completeBalance(completed) ??
182
+ (await this.#dialogs.query(() => balanceQuery(this.#parameters, completed))));
183
+ });
184
+ }
185
+ async getTransactions(account, query = {}) {
186
+ // Checked before the call waits for others; an async method, so that every argument error is a rejection.
187
+ requireAccount(account);
188
+ const firstDay = requestedDay(query.from, 'from');
189
+ const lastDay = requestedDay(query.to, 'to');
190
+ if (firstDay !== undefined && lastDay !== undefined && firstDay.getTime() > lastDay.getTime()) {
191
+ throw invalidArgument('`from` lies on a day after `to`', 'from');
192
+ }
193
+ const format = requestedFormat(query.format);
194
+ return this.#serialized(async () => {
195
+ const completed = await this.#sepa.complete(account);
196
+ const transactions = await this.#dialogs.query(() => transactionsQuery(this.#parameters, completed, format, notAfterToday(firstDay), notAfterToday(lastDay)));
197
+ // A stable sort, so that transactions of the same day keep the bank's order.
198
+ return transactions.sort((a, b) => (a.bookingDate ?? a.valueDate).getTime() - (b.bookingDate ?? b.valueDate).getTime());
199
+ });
200
+ }
201
+ close() {
202
+ this.#closed ??= this.#closeNow();
203
+ return this.#closed;
204
+ }
205
+ #closeNow() {
206
+ this.#prompts.close();
207
+ this.#protocol.beginClose();
208
+ return this.#queue.run(() => this.#prompts.reportingCallbackErrors(async () => {
209
+ try {
210
+ await this.#dialogs.end();
211
+ }
212
+ finally {
213
+ this.#guard.close();
214
+ this.#protocol.close();
215
+ }
216
+ }));
217
+ }
218
+ #serialized(action) {
219
+ const caller = runningCalls.current;
220
+ if (caller !== undefined && caller === this.#runningCall) {
221
+ // The call would wait behind the running one, which waits for the callback: neither would ever end.
222
+ return Promise.reject(new FinTsStateError('The FinTS client was called from inside one of its callbacks'));
223
+ }
224
+ return this.#queue.run(async () => {
225
+ const call = {};
226
+ this.#runningCall = call;
227
+ try {
228
+ return await runningCalls.run(call, () => this.#guard.runCall(async () => {
229
+ this.#prompts.checkOpen();
230
+ const result = await this.#prompts.reportingCallbackErrors(action);
231
+ // Also where the last answer arrived after `close`: a closed client hands out no account data.
232
+ this.#prompts.checkOpen();
233
+ return result;
234
+ }));
235
+ }
236
+ finally {
237
+ this.#runningCall = undefined;
238
+ }
239
+ });
240
+ }
241
+ }
242
+ //# sourceMappingURL=client.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"client.js","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"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"}