@coderoftime/fints 0.2.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 (215) hide show
  1. package/CHANGELOG.md +377 -0
  2. package/README.md +31 -23
  3. package/dist/client.d.ts +3 -2
  4. package/dist/client.d.ts.map +1 -1
  5. package/dist/client.js +52 -9
  6. package/dist/client.js.map +1 -1
  7. package/dist/core/account.d.ts +15 -4
  8. package/dist/core/account.d.ts.map +1 -1
  9. package/dist/core/account.js +84 -36
  10. package/dist/core/account.js.map +1 -1
  11. package/dist/core/amount.d.ts +13 -3
  12. package/dist/core/amount.d.ts.map +1 -1
  13. package/dist/core/amount.js +42 -8
  14. package/dist/core/amount.js.map +1 -1
  15. package/dist/core/balance.d.ts +3 -0
  16. package/dist/core/balance.d.ts.map +1 -1
  17. package/dist/core/balance.js +5 -0
  18. package/dist/core/balance.js.map +1 -1
  19. package/dist/core/bank-info.d.ts +20 -3
  20. package/dist/core/bank-info.d.ts.map +1 -1
  21. package/dist/core/bank-info.js +25 -4
  22. package/dist/core/bank-info.js.map +1 -1
  23. package/dist/core/client.d.ts +42 -15
  24. package/dist/core/client.d.ts.map +1 -1
  25. package/dist/core/config.d.ts +15 -7
  26. package/dist/core/config.d.ts.map +1 -1
  27. package/dist/core/config.js +29 -14
  28. package/dist/core/config.js.map +1 -1
  29. package/dist/core/errors.d.ts +17 -5
  30. package/dist/core/errors.d.ts.map +1 -1
  31. package/dist/core/errors.js +25 -7
  32. package/dist/core/errors.js.map +1 -1
  33. package/dist/core/feedback.d.ts +7 -0
  34. package/dist/core/feedback.d.ts.map +1 -1
  35. package/dist/core/feedback.js +10 -1
  36. package/dist/core/feedback.js.map +1 -1
  37. package/dist/core/transaction.d.ts +5 -0
  38. package/dist/core/transaction.d.ts.map +1 -1
  39. package/dist/core/transaction.js +12 -0
  40. package/dist/core/transaction.js.map +1 -1
  41. package/dist/core/transport.d.ts +22 -7
  42. package/dist/core/transport.d.ts.map +1 -1
  43. package/dist/core/transport.js +51 -11
  44. package/dist/core/transport.js.map +1 -1
  45. package/dist/formats/camt.d.ts +7 -4
  46. package/dist/formats/camt.d.ts.map +1 -1
  47. package/dist/formats/camt.js +53 -54
  48. package/dist/formats/camt.js.map +1 -1
  49. package/dist/formats/mt940.d.ts +18 -6
  50. package/dist/formats/mt940.d.ts.map +1 -1
  51. package/dist/formats/mt940.js +83 -66
  52. package/dist/formats/mt940.js.map +1 -1
  53. package/dist/formats/readers.d.ts.map +1 -1
  54. package/dist/formats/readers.js +6 -2
  55. package/dist/formats/readers.js.map +1 -1
  56. package/dist/formats/sepa-purpose.d.ts +7 -4
  57. package/dist/formats/sepa-purpose.d.ts.map +1 -1
  58. package/dist/formats/sepa-purpose.js +18 -9
  59. package/dist/formats/sepa-purpose.js.map +1 -1
  60. package/dist/session/dialog-runner.d.ts +5 -2
  61. package/dist/session/dialog-runner.d.ts.map +1 -1
  62. package/dist/session/dialog-runner.js +23 -7
  63. package/dist/session/dialog-runner.js.map +1 -1
  64. package/dist/session/login-guard.d.ts +4 -4
  65. package/dist/session/login-guard.d.ts.map +1 -1
  66. package/dist/session/login-guard.js +29 -14
  67. package/dist/session/login-guard.js.map +1 -1
  68. package/dist/session/session-setup.d.ts +14 -5
  69. package/dist/session/session-setup.d.ts.map +1 -1
  70. package/dist/session/session-setup.js +30 -9
  71. package/dist/session/session-setup.js.map +1 -1
  72. package/dist/session/session-state.d.ts +4 -3
  73. package/dist/session/session-state.d.ts.map +1 -1
  74. package/dist/session/session-state.js +28 -9
  75. package/dist/session/session-state.js.map +1 -1
  76. package/dist/session/signed-dialog.d.ts +19 -13
  77. package/dist/session/signed-dialog.d.ts.map +1 -1
  78. package/dist/session/signed-dialog.js +45 -30
  79. package/dist/session/signed-dialog.js.map +1 -1
  80. package/dist/session/user-prompts.d.ts +6 -1
  81. package/dist/session/user-prompts.d.ts.map +1 -1
  82. package/dist/session/user-prompts.js +19 -4
  83. package/dist/session/user-prompts.js.map +1 -1
  84. package/dist/tan/challenge.d.ts +15 -13
  85. package/dist/tan/challenge.d.ts.map +1 -1
  86. package/dist/tan/challenge.js +32 -26
  87. package/dist/tan/challenge.js.map +1 -1
  88. package/dist/tan/flicker.d.ts +5 -1
  89. package/dist/tan/flicker.d.ts.map +1 -1
  90. package/dist/tan/flicker.js +42 -30
  91. package/dist/tan/flicker.js.map +1 -1
  92. package/dist/tan/tan-image.d.ts +4 -1
  93. package/dist/tan/tan-image.d.ts.map +1 -1
  94. package/dist/tan/tan-image.js +8 -2
  95. package/dist/tan/tan-image.js.map +1 -1
  96. package/dist/tan/tan-models.d.ts +9 -1
  97. package/dist/tan/tan-models.d.ts.map +1 -1
  98. package/dist/tan/tan-models.js +14 -2
  99. package/dist/tan/tan-models.js.map +1 -1
  100. package/dist/util/arguments.d.ts +13 -0
  101. package/dist/util/arguments.d.ts.map +1 -0
  102. package/dist/util/arguments.js +40 -0
  103. package/dist/util/arguments.js.map +1 -0
  104. package/dist/util/async.d.ts +4 -1
  105. package/dist/util/async.d.ts.map +1 -1
  106. package/dist/util/async.js +6 -3
  107. package/dist/util/async.js.map +1 -1
  108. package/dist/util/bytes.d.ts +1 -1
  109. package/dist/util/bytes.d.ts.map +1 -1
  110. package/dist/util/bytes.js +3 -3
  111. package/dist/util/bytes.js.map +1 -1
  112. package/dist/util/dates.d.ts +3 -10
  113. package/dist/util/dates.d.ts.map +1 -1
  114. package/dist/util/dates.js +49 -39
  115. package/dist/util/dates.js.map +1 -1
  116. package/dist/util/errors.d.ts +5 -3
  117. package/dist/util/errors.d.ts.map +1 -1
  118. package/dist/util/errors.js +8 -4
  119. package/dist/util/errors.js.map +1 -1
  120. package/dist/util/immutable-map.d.ts +5 -2
  121. package/dist/util/immutable-map.d.ts.map +1 -1
  122. package/dist/util/immutable-map.js +15 -12
  123. package/dist/util/immutable-map.js.map +1 -1
  124. package/dist/util/inspect.d.ts +11 -0
  125. package/dist/util/inspect.d.ts.map +1 -0
  126. package/dist/util/inspect.js +14 -0
  127. package/dist/util/inspect.js.map +1 -0
  128. package/dist/util/text.d.ts +18 -2
  129. package/dist/util/text.d.ts.map +1 -1
  130. package/dist/util/text.js +50 -5
  131. package/dist/util/text.js.map +1 -1
  132. package/dist/util/xml.d.ts +6 -4
  133. package/dist/util/xml.d.ts.map +1 -1
  134. package/dist/util/xml.js +106 -44
  135. package/dist/util/xml.js.map +1 -1
  136. package/dist/v3/bank-connection.d.ts +23 -3
  137. package/dist/v3/bank-connection.d.ts.map +1 -1
  138. package/dist/v3/bank-connection.js +66 -6
  139. package/dist/v3/bank-connection.js.map +1 -1
  140. package/dist/v3/dialog.d.ts +6 -2
  141. package/dist/v3/dialog.d.ts.map +1 -1
  142. package/dist/v3/dialog.js +20 -9
  143. package/dist/v3/dialog.js.map +1 -1
  144. package/dist/v3/message.d.ts +18 -10
  145. package/dist/v3/message.d.ts.map +1 -1
  146. package/dist/v3/message.js +92 -75
  147. package/dist/v3/message.js.map +1 -1
  148. package/dist/v3/parameter-store.d.ts +43 -7
  149. package/dist/v3/parameter-store.d.ts.map +1 -1
  150. package/dist/v3/parameter-store.js +69 -15
  151. package/dist/v3/parameter-store.js.map +1 -1
  152. package/dist/v3/parameters.d.ts +12 -11
  153. package/dist/v3/parameters.d.ts.map +1 -1
  154. package/dist/v3/parameters.js +57 -35
  155. package/dist/v3/parameters.js.map +1 -1
  156. package/dist/v3/protocol.d.ts +16 -9
  157. package/dist/v3/protocol.d.ts.map +1 -1
  158. package/dist/v3/protocol.js +57 -26
  159. package/dist/v3/protocol.js.map +1 -1
  160. package/dist/v3/queries.d.ts +2 -2
  161. package/dist/v3/queries.d.ts.map +1 -1
  162. package/dist/v3/queries.js +24 -16
  163. package/dist/v3/queries.js.map +1 -1
  164. package/dist/v3/segments.d.ts +18 -11
  165. package/dist/v3/segments.d.ts.map +1 -1
  166. package/dist/v3/segments.js +30 -21
  167. package/dist/v3/segments.js.map +1 -1
  168. package/dist/v3/syntax.d.ts +15 -11
  169. package/dist/v3/syntax.d.ts.map +1 -1
  170. package/dist/v3/syntax.js +30 -15
  171. package/dist/v3/syntax.js.map +1 -1
  172. package/package.json +1 -1
  173. package/src/client.ts +58 -17
  174. package/src/core/account.ts +91 -35
  175. package/src/core/amount.ts +42 -9
  176. package/src/core/balance.ts +6 -0
  177. package/src/core/bank-info.ts +28 -4
  178. package/src/core/client.ts +42 -15
  179. package/src/core/config.ts +37 -18
  180. package/src/core/errors.ts +27 -7
  181. package/src/core/feedback.ts +12 -1
  182. package/src/core/transaction.ts +13 -0
  183. package/src/core/transport.ts +62 -16
  184. package/src/formats/camt.ts +62 -51
  185. package/src/formats/mt940.ts +114 -78
  186. package/src/formats/readers.ts +6 -2
  187. package/src/formats/sepa-purpose.ts +17 -9
  188. package/src/session/dialog-runner.ts +34 -7
  189. package/src/session/login-guard.ts +29 -13
  190. package/src/session/session-setup.ts +27 -9
  191. package/src/session/session-state.ts +25 -8
  192. package/src/session/signed-dialog.ts +60 -30
  193. package/src/session/user-prompts.ts +28 -3
  194. package/src/tan/challenge.ts +50 -36
  195. package/src/tan/flicker.ts +40 -29
  196. package/src/tan/tan-image.ts +9 -2
  197. package/src/tan/tan-models.ts +16 -2
  198. package/src/util/arguments.ts +40 -0
  199. package/src/util/async.ts +10 -4
  200. package/src/util/bytes.ts +3 -3
  201. package/src/util/dates.ts +52 -38
  202. package/src/util/errors.ts +9 -4
  203. package/src/util/immutable-map.ts +15 -12
  204. package/src/util/inspect.ts +14 -0
  205. package/src/util/text.ts +47 -5
  206. package/src/util/xml.ts +105 -38
  207. package/src/v3/bank-connection.ts +65 -6
  208. package/src/v3/dialog.ts +18 -8
  209. package/src/v3/message.ts +81 -72
  210. package/src/v3/parameter-store.ts +82 -16
  211. package/src/v3/parameters.ts +68 -41
  212. package/src/v3/protocol.ts +61 -28
  213. package/src/v3/queries.ts +26 -18
  214. package/src/v3/segments.ts +32 -20
  215. package/src/v3/syntax.ts +31 -14
@@ -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
  /**
@@ -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
  }
@@ -26,6 +26,11 @@ export type WarningHandler = (warning: FinTsFeedback) => void;
26
26
  * An error thrown by a callback that only informs the app (`onBankMessage`, `onWarning`, `logger`) fails the call once
27
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
28
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`.
29
34
  */
30
35
  export interface FinTsClientOptions {
31
36
  /** Called whenever the bank asks for a TAN or an app approval. */
@@ -37,12 +42,17 @@ export interface FinTsClientOptions {
37
42
  /** Picks the TAN medium when the method needs one and the user has several; the first is used without it. */
38
43
  tanMediumSelector?: TanMediumSelector;
39
44
 
40
- /** 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
+ */
41
49
  onBankMessage?: BankMessageHandler;
42
50
 
43
51
  /**
44
52
  * Receives the bank's warnings, such as 3951 (switch to another TAN method), but not those the client acts on itself,
45
- * 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.
46
56
  */
47
57
  onWarning?: WarningHandler;
48
58
 
@@ -58,7 +68,8 @@ export interface FinTsClientOptions {
58
68
  * refused login the other client saved.
59
69
  *
60
70
  * A state saved for another user or bank is ignored, and of one saved for another server of the bank, such as before
61
- * the bank moved its FinTS URL, only the refused login holds.
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.
62
73
  */
63
74
  state?: string | undefined;
64
75
 
@@ -97,10 +108,13 @@ export interface TransactionQuery {
97
108
  export interface FinTsClient {
98
109
  /**
99
110
  * Serialized session data: bank and user parameters, customer system ID, the TAN methods the user may use, the chosen
100
- * TAN method and medium, the SEPA account list, and a refused login (`loginRefusal`).
111
+ * TAN method and medium, the SEPA account list, and a refused login (`loginRefusal`) with the count of logins whose
112
+ * answer was lost.
101
113
  *
102
- * Contains no PIN or TAN, but account details, so store it encrypted. It holds what this client knows, and replaces
103
- * the state it was restored from: one client at a time per state (`FinTsClientOptions.state`).
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`).
104
118
  */
105
119
  readonly state: string;
106
120
 
@@ -113,14 +127,19 @@ export interface FinTsClient {
113
127
  /** Security function code of the TAN method in use, for example `942`; `undefined` before the first login. */
114
128
  readonly selectedTanMethod: string | undefined;
115
129
 
116
- /** 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
+ */
117
134
  readonly selectedTanMedium: string | undefined;
118
135
 
119
136
  /**
120
137
  * Switches to another TAN method, and optionally TAN medium, for the following calls.
121
138
  *
122
139
  * Ends the current dialog, so the next call logs in again with the new method. If the bank does not offer
123
- * `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.
124
143
  */
125
144
  selectTanMethod(securityFunction: string, tanMedium?: string): Promise<void>;
126
145
 
@@ -158,7 +177,8 @@ export interface FinTsClient {
158
177
  * comes from there without a query; `Balance.bookedAsOf` tells the day it states.
159
178
  *
160
179
  * Rejects with `FinTsUnsupportedError` where the parameter data rule the query out, without sending it; the login,
161
- * which delivers them, may have gone out.
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.
162
182
  */
163
183
  getBalance(account: Account): Promise<Balance>;
164
184
 
@@ -168,8 +188,9 @@ export interface FinTsClient {
168
188
  *
169
189
  * Only the local calendar day counts, and a day in the future counts as today. Banks typically ask for a TAN when
170
190
  * `from` is more than 90 days ago; see also `Account.dataStatus`. Rejects with `TypeError`, before it waits for other
171
- * calls, if `from` or `to` is no valid `Date` or `from` lies on a day after `to`, and with `FinTsUnsupportedError`,
172
- * without sending the query, where the parameter data rule it out; the login may have gone out.
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.
173
194
  */
174
195
  getTransactions(account: Account, query?: TransactionQuery): Promise<Transaction[]>;
175
196
 
@@ -177,10 +198,16 @@ export interface FinTsClient {
177
198
  * Ends the bank dialog, releases the connection and forgets the PIN.
178
199
  *
179
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
180
- * `FinTsStateError`, as do all later calls. A request already on its way is aborted where the transport supports it,
181
- * as `HttpTransport` does: the call fails with `FinTsTransportError` then, as after a broken connection, and an aborted
182
- * login counts as one whose answer was lost. Another transport is awaited, at most for its timeout; after the
183
- * request, the call sends nothing but the end of its dialogs.
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.
184
211
  */
185
212
  close(): Promise<void>;
186
213
  }
@@ -1,5 +1,7 @@
1
+ import { optionalBoolean, optionalString, requireString } from '../util/arguments.js';
1
2
  import { invalidArgument } from '../util/errors.js';
2
- import { isLatin1 } from '../util/text.js';
3
+ import { inspectCustom } from '../util/inspect.js';
4
+ import { isLatin1, trimBlanks } from '../util/text.js';
3
5
 
4
6
  /**
5
7
  * The country code FinTS gives Germany, where FinTS banks are: the ISO 3166 code of the Federal Republic before 1990,
@@ -7,8 +9,12 @@ import { isLatin1 } from '../util/text.js';
7
9
  */
8
10
  export const defaultCountryCode = '280';
9
11
 
10
- /** Longest bank code (Kreditinstitutscode), product ID (Produktbezeichnung) and product version FinTS 3.0 takes. */
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
+ */
11
16
  const maxBankCodeLength = 30;
17
+ const maxIdLength = 30;
12
18
  const maxProductIdLength = 25;
13
19
  const maxProductVersionLength = 5;
14
20
 
@@ -18,9 +24,9 @@ export interface FinTsConfigInit {
18
24
  url: string | URL;
19
25
  /** Bank code: for a German bank its BLZ, eight digits. */
20
26
  bankCode: string;
21
- /** Login name (Benutzerkennung / Legitimations-ID). */
27
+ /** Login name (Benutzerkennung / Legitimations-ID); at most 30 characters. */
22
28
  userId: string;
23
- /** Customer ID; defaults to `userId`. */
29
+ /** Customer ID, at most 30 characters; defaults to `userId`. */
24
30
  customerId?: string | undefined;
25
31
  /** FinTS product registration number from the Deutsche Kreditwirtschaft; at most 25 characters. */
26
32
  productId: string;
@@ -31,12 +37,15 @@ export interface FinTsConfigInit {
31
37
  /**
32
38
  * Allows plain http for the loopback addresses `localhost`, `127.x.x.x` and `::1`, for tests against a local server.
33
39
  *
34
- * 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`.
35
44
  */
36
45
  allowInsecureUrl?: boolean;
37
46
  }
38
47
 
39
- /** 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. */
40
49
  export class FinTsConfig {
41
50
  /** The country code FinTS gives Germany, where FinTS banks are: `280`, not today's ISO 3166 code `276`. */
42
51
  static readonly defaultCountryCode = defaultCountryCode;
@@ -54,9 +63,11 @@ export class FinTsConfig {
54
63
  * Throws `TypeError` if `url` is not a valid URL, has no host, holds a user name or password, or is not https, unless
55
64
  * `allowInsecureUrl` is set and `url` is a loopback address. Throws as well where the bank would refuse the login: for
56
65
  * a login or product detail that is empty, has characters FinTS 3.0 cannot send (it uses ISO 8859-1, so `€` and most
57
- * non-Latin letters are not possible) or is longer than its field (30 characters for the bank code, 25 for the
58
- * product ID, 5 for its version), for a country code of other than three digits, and for a German bank code of other
59
- * than eight. The message leaves out the value.
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.
60
71
  *
61
72
  * Drops leading and trailing blanks of the login details and the product, as FinTS sends them without (Formals
62
73
  * B.4.1): `'alice '` is the user `alice`, also for a saved state.
@@ -73,7 +84,7 @@ export class FinTsConfig {
73
84
  throw invalidArgument('Not a valid URL', 'url');
74
85
  }
75
86
  this.href = url.href;
76
- this.allowInsecureUrl = init.allowInsecureUrl ?? false;
87
+ this.allowInsecureUrl = optionalBoolean(init.allowInsecureUrl, 'allowInsecureUrl') ?? false;
77
88
  requireSecureUrl(url, this.allowInsecureUrl);
78
89
  this.countryCode = requireValue(init.countryCode ?? defaultCountryCode, 'countryCode');
79
90
  if (!/^\d{3}$/.test(this.countryCode)) {
@@ -83,10 +94,13 @@ export class FinTsConfig {
83
94
  if (this.countryCode === defaultCountryCode && !/^\d{8}$/.test(this.bankCode)) {
84
95
  throw invalidArgument('A German bank code (BLZ) has eight digits', 'bankCode');
85
96
  }
86
- this.userId = requireValue(init.userId, 'userId');
87
- this.explicitCustomerId = init.customerId === undefined ? undefined : requireValue(init.customerId, 'customerId');
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);
88
101
  this.productId = requireValue(init.productId, 'productId', maxProductIdLength);
89
102
  this.productVersion = requireValue(init.productVersion ?? '1.0', 'productVersion', maxProductVersionLength);
103
+ Object.freeze(this);
90
104
  }
91
105
 
92
106
  /**
@@ -101,14 +115,19 @@ export class FinTsConfig {
101
115
  get customerId(): string {
102
116
  return this.explicitCustomerId ?? this.userId;
103
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
+ }
104
123
  }
105
124
 
106
125
  /**
107
- * `value` without leading and trailing blanks; throws `TypeError` if nothing is left, or what is left is not sendable
108
- * (`requireLatin1`) or longer than `maxLength`: cut short, a value would be one the bank does not know.
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.
109
128
  */
110
- function requireValue(value: string, name: string, maxLength = Number.POSITIVE_INFINITY): string {
111
- const trimmed = value.trim();
129
+ function requireValue(value: unknown, name: string, maxLength = Number.POSITIVE_INFINITY): string {
130
+ const trimmed = trimBlanks(requireString(value, name));
112
131
  if (trimmed === '') throw invalidArgument('Must not be empty', name);
113
132
  requireLatin1(trimmed, name);
114
133
  if (trimmed.length > maxLength) throw invalidArgument(`At most ${maxLength} characters in FinTS 3.0`, name);
@@ -148,8 +167,8 @@ function requireSecureUrl(url: URL, allowInsecure = false): void {
148
167
  * The message leaves out the value.
149
168
  */
150
169
  export function requirePin(pin: string): string {
151
- if (pin.trim() === '') throw invalidArgument('The PIN is empty', 'pin');
152
- if (pin.trim() !== pin) throw invalidArgument('The PIN begins or ends with a blank', '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');
153
172
  return requireLatin1(pin, 'pin');
154
173
  }
155
174
 
@@ -53,23 +53,37 @@ export class FinTsTanSetupError extends FinTsError {
53
53
  override name = 'FinTsTanSetupError';
54
54
  }
55
55
 
56
- /** The bank rejected the request; `feedback` holds all return codes. A refused login is a `FinTsLoginRefusedError`. */
56
+ /**
57
+ * The bank rejected the request; `feedback` holds all return codes. A refused login is a `FinTsLoginRefusedError`.
58
+ *
59
+ * The message is not safe to log: by default it holds the code, text and parameters of each error the bank names, such
60
+ * as an IBAN or a login name, and so does `stack`. Log `codes` instead. `feedback` is a getter, not a field, so that
61
+ * `JSON.stringify` and Node.js's report of an uncaught error, which show an error's fields as they are, leave out at
62
+ * least the warnings and successes, such as the new login data of return code 3072.
63
+ */
57
64
  export class FinTsBankError extends FinTsError {
58
65
  override name = 'FinTsBankError';
59
66
 
60
- /** All return codes of the response, warnings and successes included. */
61
- readonly feedback: readonly FinTsFeedback[];
67
+ readonly #feedback: readonly FinTsFeedback[];
62
68
 
63
- /** A rejection with the return codes in `feedback`; `message` defaults to the errors among them. */
69
+ /**
70
+ * A rejection with the return codes in `feedback`; `message` defaults to the errors among them, with the bank's texts
71
+ * and parameters, such as `9010: Konto gesperrt [DE89370400440532013000]`, so it is not safe to log.
72
+ */
64
73
  constructor(feedback: readonly FinTsFeedback[], message?: string) {
65
74
  super(
66
75
  message ??
67
76
  feedback
68
77
  .filter((f) => f.isError)
69
- .map((f) => f.toString())
78
+ .map(withText)
70
79
  .join('; '),
71
80
  );
72
- this.feedback = Object.freeze([...feedback]);
81
+ this.#feedback = Object.freeze([...feedback]);
82
+ }
83
+
84
+ /** All return codes of the response, warnings and successes included. */
85
+ get feedback(): readonly FinTsFeedback[] {
86
+ return this.#feedback;
73
87
  }
74
88
 
75
89
  /** The return codes of `feedback`. */
@@ -106,7 +120,8 @@ export class FinTsTanError extends FinTsBankError {
106
120
  }
107
121
 
108
122
  /**
109
- * The bank has changed the user's login data and expects the new ones from now on (return code 3072).
123
+ * The bank has changed the user's login data and expects the new ones from now on (return code 3072), also where it
124
+ * names an error next to them, but for a rejected PIN, which is a `FinTsPinError`.
110
125
  *
111
126
  * The client has ended the dialog, where the bank opened one, and logs in with the old data no more. Create a new
112
127
  * client with the new data; a `loginName` replaces user ID and customer ID: pass it as `userId`, without a customer ID.
@@ -214,6 +229,11 @@ function isPinError(f: FinTsFeedback): boolean {
214
229
  }
215
230
  }
216
231
 
232
+ /** `f` with the bank's text and parameters, which `FinTsFeedback.toString` leaves out. */
233
+ function withText(f: FinTsFeedback): string {
234
+ return `${f.code}: ${f.text}${f.parameters.length === 0 ? '' : ` [${f.parameters.join(', ')}]`}`;
235
+ }
236
+
217
237
  /**
218
238
  * The most specific error for the errors in `feedback`, the answer to a message that `completesApproval` or not.
219
239
  *
@@ -1,3 +1,5 @@
1
+ import { inspectCustom } from '../util/inspect.js';
2
+
1
3
  /** A return code with text from the bank, for example `0020: Auftrag ausgeführt`. */
2
4
  export class FinTsFeedback {
3
5
  /** The four-digit return code; `0…` reports success, `3…` a warning and `9…` an error. */
@@ -37,8 +39,17 @@ export class FinTsFeedback {
37
39
  return this.code.startsWith('9');
38
40
  }
39
41
 
42
+ /**
43
+ * The code only, not the text and parameters, which may name the account or hold login data, such as those of return
44
+ * code 3072, so that logs and crash reports do not collect them; show `text` to the user.
45
+ */
40
46
  toString(): string {
41
- return `${this.code}: ${this.text}${this.parameters.length === 0 ? '' : ` [${this.parameters.join(', ')}]`}`;
47
+ return `FinTsFeedback(${this.code})`;
48
+ }
49
+
50
+ /** What Node.js's `util.inspect` and `console.log` show: `toString`, not the fields. */
51
+ [inspectCustom](): string {
52
+ return this.toString();
42
53
  }
43
54
  }
44
55
 
@@ -1,10 +1,18 @@
1
1
  import { copyDate, formatIsoDate } from '../util/dates.js';
2
+ import { invalidArgument } from '../util/errors.js';
2
3
  import { ImmutableMap } from '../util/immutable-map.js';
4
+ import { inspectCustom } from '../util/inspect.js';
3
5
  import type { Amount } from './amount.js';
4
6
 
5
7
  /** Whether a transaction is booked or still pending. */
6
8
  export type TransactionStatus = 'booked' | 'pending';
7
9
 
10
+ /** `status`; throws `TypeError` unless it is a `TransactionStatus`. */
11
+ export function requireStatus(status: unknown): TransactionStatus {
12
+ if (status === 'booked' || status === 'pending') return status;
13
+ throw invalidArgument("Must be 'booked' or 'pending'", 'status');
14
+ }
15
+
8
16
  /** Transaction format to request; `auto` prefers camt.052 when the bank supports it. */
9
17
  export type TransactionFormat =
10
18
  /** camt.052 where the bank offers it, otherwise MT940. */
@@ -158,4 +166,9 @@ export class Transaction {
158
166
  toString(): string {
159
167
  return `Transaction(${formatIsoDate(this.bookingDate ?? this.valueDate)}, ${this.status}${this.isReversal ? ', reversal' : ''})`;
160
168
  }
169
+
170
+ /** What Node.js's `util.inspect` and `console.log` show: `toString`, not the fields. */
171
+ [inspectCustom](): string {
172
+ return this.toString();
173
+ }
161
174
  }
@@ -1,5 +1,5 @@
1
- import { concatBytes } from '../util/bytes.js';
2
1
  import { invalidArgument } from '../util/errors.js';
2
+ import { trimBlanks } from '../util/text.js';
3
3
  import { FinTsTransportError } from './errors.js';
4
4
 
5
5
  /** Sends one FinTS message and returns the bank's reply; replace it for a relay, proxies, pinning or tests. */
@@ -7,14 +7,20 @@ export interface FinTsTransport {
7
7
  /**
8
8
  * Posts `body` to `url` and returns the response body.
9
9
  *
10
- * The client aborts `signal` once it is closed: a transport that stops the request then keeps `close` from waiting
11
- * for the answer.
10
+ * Once the client is closed, it aborts `signal` five seconds later, and that of the dialog ends it then sends five
11
+ * seconds after the request on its way, if any, is over: the client no longer waits for the answer then, and the
12
+ * transport should stop the request, since the dialog end may go out while it still runs.
13
+ *
14
+ * The client bounds neither the time of the exchange nor the size of the answer: do so in the transport, as
15
+ * `HttpTransport` does, so that a server that stops answering cannot hold up a call and one that sends without end
16
+ * cannot exhaust the memory.
12
17
  *
13
18
  * Implementations report failures as `FinTsTransportError`, with the HTTP status as `statusCode` where the bank
14
19
  * answered with an HTTP error: an anonymous request for the bank parameter data that fails so goes out once more,
15
20
  * without the announcement of strong authentication, which some banks fail on. Of any other error, another
16
- * `FinTsError` included, the client keeps only its message and that of its cause, as a `FinTsTransportError` without
17
- * `statusCode`. `body` holds PIN and TAN in plain text: keep it out of error messages and logs.
21
+ * `FinTsError` included, the client keeps only its message and the reason its cause gives (its message, or else the
22
+ * messages of its errors or its code), as a `FinTsTransportError` without `statusCode`. `body` holds PIN and TAN in
23
+ * plain text: keep it out of error messages and logs.
18
24
  */
19
25
  post(url: URL, body: Uint8Array, contentType: string, signal: AbortSignal): Promise<Uint8Array>;
20
26
 
@@ -25,7 +31,8 @@ export interface FinTsTransport {
25
31
  /** What `HttpTransport` takes; all optional. */
26
32
  export interface HttpTransportOptions {
27
33
  /**
28
- * The `fetch` to send with, for example one with a proxy agent; the global `fetch` by default.
34
+ * The `fetch` to send with, for example one with a proxy agent; the global `fetch` by default, as it is at each
35
+ * request.
29
36
  *
30
37
  * It must post to the URL it is given: an answer from another URL counts as a redirect, which fails the request. Of
31
38
  * an answer whose `body` is no `ReadableStream`, as with node-fetch, the size can only be checked once it is read
@@ -47,6 +54,14 @@ const maxTimeoutMs = 2 ** 31 - 1;
47
54
  /**
48
55
  * Default `FinTsTransport`: HTTP POST with `fetch`.
49
56
  *
57
+ * Without the `fetch` option it sends with the global `fetch` as it is at each request, and cannot bypass what that
58
+ * does, for plain http to a local server (`FinTsConfig.allowInsecureUrl`) as for https: whatever replaces that `fetch`
59
+ * or handles its requests, such as a test's stub of `globalThis.fetch` or on Node.js a global dispatcher (undici's
60
+ * `setGlobalDispatcher`), gets every message, PIN and TAN included, and a proxy that the runtime takes from the
61
+ * environment, as Node.js does with `NODE_USE_ENV_PROXY=1`, and Deno and Bun do from `HTTP_PROXY`, sees the PIN of
62
+ * plain http unencrypted. Exempt the host with `NO_PROXY`, such as `localhost,127.0.0.1,::1`, or pass a `fetch` that
63
+ * connects directly.
64
+ *
50
65
  * In a browser it only reaches servers that allow the page's origin (CORS), which bank servers do not: a web app needs
51
66
  * a server of its own between browser and bank, reached with a `FinTsTransport` of its own.
52
67
  */
@@ -106,6 +121,10 @@ export class HttpTransport implements FinTsTransport {
106
121
  headers: { 'Content-Type': contentType, 'Cache-Control': 'no-cache', Pragma: 'no-cache' },
107
122
  // The PIN travels in the body: never let the request be repeated at another address.
108
123
  redirect: 'manual',
124
+ // In a browser: no cookies of the page and no referrer go with the PIN, and no cache keeps the exchange.
125
+ credentials: 'omit',
126
+ referrerPolicy: 'no-referrer',
127
+ cache: 'no-store',
109
128
  signal,
110
129
  });
111
130
  if (response.type === 'opaqueredirect') {
@@ -118,10 +137,15 @@ export class HttpTransport implements FinTsTransport {
118
137
  }
119
138
  if (response.status < 200 || response.status > 299) {
120
139
  await discard(response);
121
- throw new FinTsTransportError(`HTTP ${response.status} ${response.statusText}`.trim(), {
140
+ throw new FinTsTransportError(trimBlanks(`HTTP ${response.status} ${response.statusText}`), {
122
141
  statusCode: response.status,
123
142
  });
124
143
  }
144
+ const announced = response.headers.get('Content-Length');
145
+ if (announced !== null && /^\d+$/.test(announced) && Number(announced) > this.maxResponseBytes) {
146
+ await discard(response);
147
+ throw this.tooLarge();
148
+ }
125
149
  return this.readBody(response);
126
150
  }
127
151
 
@@ -134,19 +158,27 @@ export class HttpTransport implements FinTsTransport {
134
158
  return bytes;
135
159
  }
136
160
  const reader = body.getReader();
137
- const chunks: Uint8Array[] = [];
161
+ // Copied as they arrive: many tiny chunks kept as they are would take many times the bytes they hold, and a `fetch`
162
+ // may refill the chunk it handed out.
163
+ let bytes = new Uint8Array(0);
138
164
  let size = 0;
139
165
  for (;;) {
140
166
  const { done, value } = await reader.read();
141
167
  if (done) break;
142
- chunks.push(value);
143
- size += value.length;
144
- if (size > this.maxResponseBytes) {
168
+ const end = size + value.length;
169
+ if (end > this.maxResponseBytes) {
145
170
  await reader.cancel().catch(() => undefined);
146
171
  throw this.tooLarge();
147
172
  }
173
+ if (end > bytes.length) {
174
+ const grown = new Uint8Array(Math.min(Math.max(2 * bytes.length, end), this.maxResponseBytes));
175
+ grown.set(bytes.subarray(0, size));
176
+ bytes = grown;
177
+ }
178
+ bytes.set(value, size);
179
+ size = end;
148
180
  }
149
- return concatBytes(chunks);
181
+ return bytes.slice(0, size);
150
182
  }
151
183
 
152
184
  private tooLarge(): FinTsTransportError {
@@ -179,11 +211,25 @@ function isPositive(value: number): boolean {
179
211
  }
180
212
 
181
213
  /**
182
- * The message of a failed request, with the message of its cause: Node.js reports `fetch failed` and the reason in the
183
- * cause. Only the messages, since the error may hold the request, PIN and TAN included.
214
+ * The message of a failed request, with the reason its cause gives: Node.js reports `fetch failed` and the reason in the
215
+ * cause. Only messages and codes, since the error may hold the request, PIN and TAN included.
184
216
  */
185
217
  export function failureMessage(error: unknown): string {
186
218
  if (!(error instanceof Error)) return String(error);
187
- const cause: unknown = error.cause;
188
- return cause instanceof Error ? `${error.message}: ${cause.message}` : error.message;
219
+ const reason = error.cause instanceof Error ? reasonOf(error.cause) : '';
220
+ return reason ? `${error.message}: ${reason}` : error.message;
221
+ }
222
+
223
+ /**
224
+ * The message of `cause`, or where it has none, the distinct messages of its `errors` or its `code`: Node.js reports a
225
+ * failed connection to a host with IPv6 and IPv4 addresses as an `AggregateError` without a message.
226
+ */
227
+ function reasonOf(cause: Error): string {
228
+ if (cause.message) return cause.message;
229
+ if (cause instanceof AggregateError) {
230
+ const messages = cause.errors.flatMap((error) => (error instanceof Error && error.message ? [error.message] : []));
231
+ if (messages.length > 0) return [...new Set(messages)].join('; ');
232
+ }
233
+ const code: unknown = (cause as { code?: unknown }).code;
234
+ return typeof code === 'string' ? code : '';
189
235
  }