@coderoftime/fints 0.1.0 → 0.2.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 (272) hide show
  1. package/CHANGELOG.md +142 -1
  2. package/README.md +127 -37
  3. package/dist/client.d.ts +48 -0
  4. package/dist/client.d.ts.map +1 -0
  5. package/dist/client.js +199 -0
  6. package/dist/client.js.map +1 -0
  7. package/dist/core/account.d.ts +35 -7
  8. package/dist/core/account.d.ts.map +1 -1
  9. package/dist/core/account.js +84 -13
  10. package/dist/core/account.js.map +1 -1
  11. package/dist/core/amount.d.ts +2 -1
  12. package/dist/core/amount.d.ts.map +1 -1
  13. package/dist/core/amount.js +4 -1
  14. package/dist/core/amount.js.map +1 -1
  15. package/dist/core/balance.d.ts +1 -0
  16. package/dist/core/balance.d.ts.map +1 -1
  17. package/dist/core/balance.js +6 -5
  18. package/dist/core/balance.js.map +1 -1
  19. package/dist/core/bank-info.d.ts +1 -1
  20. package/dist/core/bank-info.js +1 -1
  21. package/dist/core/client.d.ts +50 -15
  22. package/dist/core/client.d.ts.map +1 -1
  23. package/dist/core/config.d.ts +28 -16
  24. package/dist/core/config.d.ts.map +1 -1
  25. package/dist/core/config.js +66 -26
  26. package/dist/core/config.js.map +1 -1
  27. package/dist/core/errors.d.ts +85 -23
  28. package/dist/core/errors.d.ts.map +1 -1
  29. package/dist/core/errors.js +112 -47
  30. package/dist/core/errors.js.map +1 -1
  31. package/dist/core/feedback.d.ts +4 -1
  32. package/dist/core/feedback.d.ts.map +1 -1
  33. package/dist/core/feedback.js +4 -1
  34. package/dist/core/feedback.js.map +1 -1
  35. package/dist/core/transaction.d.ts +6 -1
  36. package/dist/core/transaction.d.ts.map +1 -1
  37. package/dist/core/transaction.js +9 -4
  38. package/dist/core/transaction.js.map +1 -1
  39. package/dist/core/transport.d.ts +33 -6
  40. package/dist/core/transport.d.ts.map +1 -1
  41. package/dist/core/transport.js +77 -20
  42. package/dist/core/transport.js.map +1 -1
  43. package/dist/formats/camt.d.ts +24 -4
  44. package/dist/formats/camt.d.ts.map +1 -1
  45. package/dist/formats/camt.js +124 -41
  46. package/dist/formats/camt.js.map +1 -1
  47. package/dist/formats/mt940.d.ts +15 -3
  48. package/dist/formats/mt940.d.ts.map +1 -1
  49. package/dist/formats/mt940.js +161 -82
  50. package/dist/formats/mt940.js.map +1 -1
  51. package/dist/formats/readers.d.ts +6 -2
  52. package/dist/formats/readers.d.ts.map +1 -1
  53. package/dist/formats/readers.js +24 -29
  54. package/dist/formats/readers.js.map +1 -1
  55. package/dist/formats/record-budget.d.ts +7 -0
  56. package/dist/formats/record-budget.d.ts.map +1 -0
  57. package/dist/formats/record-budget.js +17 -0
  58. package/dist/formats/record-budget.js.map +1 -0
  59. package/dist/formats/sepa-purpose.d.ts +6 -0
  60. package/dist/formats/sepa-purpose.d.ts.map +1 -1
  61. package/dist/formats/sepa-purpose.js +23 -2
  62. package/dist/formats/sepa-purpose.js.map +1 -1
  63. package/dist/index.d.ts +3 -3
  64. package/dist/index.d.ts.map +1 -1
  65. package/dist/index.js +2 -2
  66. package/dist/index.js.map +1 -1
  67. package/dist/session/app-approval.d.ts +11 -5
  68. package/dist/session/app-approval.d.ts.map +1 -1
  69. package/dist/session/app-approval.js +23 -12
  70. package/dist/session/app-approval.js.map +1 -1
  71. package/dist/session/dialog-runner.d.ts +7 -10
  72. package/dist/session/dialog-runner.d.ts.map +1 -1
  73. package/dist/session/dialog-runner.js +15 -16
  74. package/dist/session/dialog-runner.js.map +1 -1
  75. package/dist/session/login-guard.d.ts +54 -49
  76. package/dist/session/login-guard.d.ts.map +1 -1
  77. package/dist/session/login-guard.js +156 -84
  78. package/dist/session/login-guard.js.map +1 -1
  79. package/dist/session/sepa-accounts.d.ts +20 -14
  80. package/dist/session/sepa-accounts.d.ts.map +1 -1
  81. package/dist/session/sepa-accounts.js +62 -46
  82. package/dist/session/sepa-accounts.js.map +1 -1
  83. package/dist/session/session-setup.d.ts +33 -25
  84. package/dist/session/session-setup.d.ts.map +1 -1
  85. package/dist/session/session-setup.js +87 -65
  86. package/dist/session/session-setup.js.map +1 -1
  87. package/dist/session/session-state.d.ts +9 -6
  88. package/dist/session/session-state.d.ts.map +1 -1
  89. package/dist/session/session-state.js +52 -17
  90. package/dist/session/session-state.js.map +1 -1
  91. package/dist/session/signed-dialog.d.ts +77 -0
  92. package/dist/session/signed-dialog.d.ts.map +1 -0
  93. package/dist/session/signed-dialog.js +170 -0
  94. package/dist/session/signed-dialog.js.map +1 -0
  95. package/dist/session/tan-selection.d.ts +26 -18
  96. package/dist/session/tan-selection.d.ts.map +1 -1
  97. package/dist/session/tan-selection.js +44 -33
  98. package/dist/session/tan-selection.js.map +1 -1
  99. package/dist/session/user-prompts.d.ts +8 -4
  100. package/dist/session/user-prompts.d.ts.map +1 -1
  101. package/dist/session/user-prompts.js +34 -14
  102. package/dist/session/user-prompts.js.map +1 -1
  103. package/dist/tan/challenge.d.ts +34 -13
  104. package/dist/tan/challenge.d.ts.map +1 -1
  105. package/dist/tan/challenge.js +25 -26
  106. package/dist/tan/challenge.js.map +1 -1
  107. package/dist/tan/flicker.d.ts +7 -1
  108. package/dist/tan/flicker.d.ts.map +1 -1
  109. package/dist/tan/flicker.js +40 -6
  110. package/dist/tan/flicker.js.map +1 -1
  111. package/dist/tan/tan-image.d.ts +3 -2
  112. package/dist/tan/tan-image.d.ts.map +1 -1
  113. package/dist/tan/tan-image.js +7 -5
  114. package/dist/tan/tan-image.js.map +1 -1
  115. package/dist/tan/tan-models.d.ts +5 -1
  116. package/dist/tan/tan-models.d.ts.map +1 -1
  117. package/dist/tan/tan-models.js +10 -5
  118. package/dist/tan/tan-models.js.map +1 -1
  119. package/dist/util/async.d.ts +0 -2
  120. package/dist/util/async.d.ts.map +1 -1
  121. package/dist/util/async.js +0 -4
  122. package/dist/util/async.js.map +1 -1
  123. package/dist/util/bytes.d.ts +2 -3
  124. package/dist/util/bytes.d.ts.map +1 -1
  125. package/dist/util/bytes.js +12 -20
  126. package/dist/util/bytes.js.map +1 -1
  127. package/dist/util/call-context.d.ts.map +1 -1
  128. package/dist/util/call-context.js +3 -2
  129. package/dist/util/call-context.js.map +1 -1
  130. package/dist/util/dates.d.ts +9 -16
  131. package/dist/util/dates.d.ts.map +1 -1
  132. package/dist/util/dates.js +39 -19
  133. package/dist/util/dates.js.map +1 -1
  134. package/dist/util/errors.d.ts +9 -1
  135. package/dist/util/errors.d.ts.map +1 -1
  136. package/dist/util/errors.js +12 -3
  137. package/dist/util/errors.js.map +1 -1
  138. package/dist/util/json.d.ts +4 -0
  139. package/dist/util/json.d.ts.map +1 -1
  140. package/dist/util/json.js +18 -0
  141. package/dist/util/json.js.map +1 -1
  142. package/dist/util/text.d.ts +21 -4
  143. package/dist/util/text.d.ts.map +1 -1
  144. package/dist/util/text.js +42 -13
  145. package/dist/util/text.js.map +1 -1
  146. package/dist/util/xml.d.ts +4 -3
  147. package/dist/util/xml.d.ts.map +1 -1
  148. package/dist/util/xml.js +37 -26
  149. package/dist/util/xml.js.map +1 -1
  150. package/dist/v3/account-extension.d.ts.map +1 -1
  151. package/dist/v3/account-extension.js +7 -10
  152. package/dist/v3/account-extension.js.map +1 -1
  153. package/dist/v3/bank-connection.d.ts +34 -0
  154. package/dist/v3/bank-connection.d.ts.map +1 -0
  155. package/dist/v3/bank-connection.js +58 -0
  156. package/dist/v3/bank-connection.js.map +1 -0
  157. package/dist/v3/dialog.d.ts +80 -46
  158. package/dist/v3/dialog.d.ts.map +1 -1
  159. package/dist/v3/dialog.js +162 -59
  160. package/dist/v3/dialog.js.map +1 -1
  161. package/dist/v3/message.d.ts +17 -4
  162. package/dist/v3/message.d.ts.map +1 -1
  163. package/dist/v3/message.js +62 -18
  164. package/dist/v3/message.js.map +1 -1
  165. package/dist/v3/parameter-store.d.ts +75 -0
  166. package/dist/v3/parameter-store.d.ts.map +1 -0
  167. package/dist/v3/parameter-store.js +143 -0
  168. package/dist/v3/parameter-store.js.map +1 -0
  169. package/dist/v3/parameters.d.ts +38 -12
  170. package/dist/v3/parameters.d.ts.map +1 -1
  171. package/dist/v3/parameters.js +91 -37
  172. package/dist/v3/parameters.js.map +1 -1
  173. package/dist/v3/protocol.d.ts +43 -50
  174. package/dist/v3/protocol.d.ts.map +1 -1
  175. package/dist/v3/protocol.js +58 -208
  176. package/dist/v3/protocol.js.map +1 -1
  177. package/dist/v3/queries.d.ts +51 -0
  178. package/dist/v3/queries.d.ts.map +1 -0
  179. package/dist/v3/queries.js +120 -0
  180. package/dist/v3/queries.js.map +1 -0
  181. package/dist/v3/segments.d.ts +29 -12
  182. package/dist/v3/segments.d.ts.map +1 -1
  183. package/dist/v3/segments.js +65 -23
  184. package/dist/v3/segments.js.map +1 -1
  185. package/dist/v3/syntax.d.ts +65 -14
  186. package/dist/v3/syntax.d.ts.map +1 -1
  187. package/dist/v3/syntax.js +167 -101
  188. package/dist/v3/syntax.js.map +1 -1
  189. package/package.json +10 -6
  190. package/src/client.ts +252 -0
  191. package/src/core/account.ts +86 -13
  192. package/src/core/amount.ts +3 -1
  193. package/src/core/balance.ts +6 -5
  194. package/src/core/bank-info.ts +1 -1
  195. package/src/core/client.ts +52 -15
  196. package/src/core/config.ts +69 -28
  197. package/src/core/errors.ts +122 -48
  198. package/src/core/feedback.ts +4 -1
  199. package/src/core/transaction.ts +9 -4
  200. package/src/core/transport.ts +98 -23
  201. package/src/formats/camt.ts +136 -39
  202. package/src/formats/mt940.ts +167 -81
  203. package/src/formats/readers.ts +24 -31
  204. package/src/formats/record-budget.ts +18 -0
  205. package/src/formats/sepa-purpose.ts +21 -2
  206. package/src/index.ts +4 -2
  207. package/src/session/app-approval.ts +51 -38
  208. package/src/session/dialog-runner.ts +31 -28
  209. package/src/session/login-guard.ts +174 -88
  210. package/src/session/sepa-accounts.ts +63 -45
  211. package/src/session/session-setup.ts +90 -71
  212. package/src/session/session-state.ts +56 -22
  213. package/src/session/signed-dialog.ts +192 -0
  214. package/src/session/tan-selection.ts +50 -39
  215. package/src/session/user-prompts.ts +35 -16
  216. package/src/tan/challenge.ts +40 -31
  217. package/src/tan/flicker.ts +44 -6
  218. package/src/tan/tan-image.ts +7 -5
  219. package/src/tan/tan-models.ts +10 -5
  220. package/src/util/async.ts +0 -5
  221. package/src/util/bytes.ts +13 -19
  222. package/src/util/call-context.ts +3 -2
  223. package/src/util/dates.ts +41 -24
  224. package/src/util/errors.ts +14 -3
  225. package/src/util/json.ts +16 -0
  226. package/src/util/text.ts +42 -12
  227. package/src/util/xml.ts +37 -26
  228. package/src/v3/account-extension.ts +7 -12
  229. package/src/v3/bank-connection.ts +67 -0
  230. package/src/v3/dialog.ts +175 -91
  231. package/src/v3/message.ts +69 -17
  232. package/src/v3/parameter-store.ts +160 -0
  233. package/src/v3/parameters.ts +110 -37
  234. package/src/v3/protocol.ts +70 -253
  235. package/src/v3/queries.ts +190 -0
  236. package/src/v3/segments.ts +65 -22
  237. package/src/v3/syntax.ts +153 -91
  238. package/dist/session/bank-connection.d.ts +0 -28
  239. package/dist/session/bank-connection.d.ts.map +0 -1
  240. package/dist/session/bank-connection.js +0 -48
  241. package/dist/session/bank-connection.js.map +0 -1
  242. package/dist/session/parameter-data.d.ts +0 -83
  243. package/dist/session/parameter-data.d.ts.map +0 -1
  244. package/dist/session/parameter-data.js +0 -68
  245. package/dist/session/parameter-data.js.map +0 -1
  246. package/dist/session/pin-tan-client.d.ts +0 -54
  247. package/dist/session/pin-tan-client.d.ts.map +0 -1
  248. package/dist/session/pin-tan-client.js +0 -153
  249. package/dist/session/pin-tan-client.js.map +0 -1
  250. package/dist/session/pin-tan-dialog.d.ts +0 -71
  251. package/dist/session/pin-tan-dialog.d.ts.map +0 -1
  252. package/dist/session/pin-tan-dialog.js +0 -124
  253. package/dist/session/pin-tan-dialog.js.map +0 -1
  254. package/dist/session/protocol.d.ts +0 -209
  255. package/dist/session/protocol.d.ts.map +0 -1
  256. package/dist/session/protocol.js +0 -75
  257. package/dist/session/protocol.js.map +0 -1
  258. package/dist/session/response-handler.d.ts +0 -48
  259. package/dist/session/response-handler.d.ts.map +0 -1
  260. package/dist/session/response-handler.js +0 -121
  261. package/dist/session/response-handler.js.map +0 -1
  262. package/dist/v3/client.d.ts +0 -24
  263. package/dist/v3/client.d.ts.map +0 -1
  264. package/dist/v3/client.js +0 -16
  265. package/dist/v3/client.js.map +0 -1
  266. package/src/session/bank-connection.ts +0 -51
  267. package/src/session/parameter-data.ts +0 -137
  268. package/src/session/pin-tan-client.ts +0 -186
  269. package/src/session/pin-tan-dialog.ts +0 -160
  270. package/src/session/protocol.ts +0 -311
  271. package/src/session/response-handler.ts +0 -130
  272. package/src/v3/client.ts +0 -29
@@ -1,13 +1,22 @@
1
1
  import { invalidArgument } from '../util/errors.js';
2
+ import { isLatin1 } from '../util/text.js';
2
3
 
3
- /** Numeric country code of Germany (ISO 3166), where FinTS banks are. */
4
+ /**
5
+ * The country code FinTS gives Germany, where FinTS banks are: the ISO 3166 code of the Federal Republic before 1990,
6
+ * not today's `276`.
7
+ */
4
8
  export const defaultCountryCode = '280';
5
9
 
10
+ /** Longest bank code (Kreditinstitutscode), product ID (Produktbezeichnung) and product version FinTS 3.0 takes. */
11
+ const maxBankCodeLength = 30;
12
+ const maxProductIdLength = 25;
13
+ const maxProductVersionLength = 5;
14
+
6
15
  /** What `FinTsConfig` takes. */
7
16
  export interface FinTsConfigInit {
8
- /** The bank's FinTS PIN/TAN endpoint; must be https. */
17
+ /** The bank's FinTS PIN/TAN endpoint; must be https, without user name or password. */
9
18
  url: string | URL;
10
- /** German bank code (BLZ). */
19
+ /** Bank code: for a German bank its BLZ, eight digits. */
11
20
  bankCode: string;
12
21
  /** Login name (Benutzerkennung / Legitimations-ID). */
13
22
  userId: string;
@@ -17,7 +26,7 @@ export interface FinTsConfigInit {
17
26
  productId: string;
18
27
  /** Version of the app as registered with the product; at most five characters. `1.0` by default. */
19
28
  productVersion?: string;
20
- /** Numeric country code of the bank, `280` (Germany) by default. */
29
+ /** Country code of the bank, three digits: `280`, the default, for Germany. */
21
30
  countryCode?: string;
22
31
  /**
23
32
  * Allows plain http for the loopback addresses `localhost`, `127.x.x.x` and `::1`, for tests against a local server.
@@ -29,10 +38,10 @@ export interface FinTsConfigInit {
29
38
 
30
39
  /** Connection and login data for one bank user. */
31
40
  export class FinTsConfig {
32
- /** Numeric country code of Germany (ISO 3166), where FinTS banks are. */
41
+ /** The country code FinTS gives Germany, where FinTS banks are: `280`, not today's ISO 3166 code `276`. */
33
42
  static readonly defaultCountryCode = defaultCountryCode;
34
43
 
35
- readonly url: URL;
44
+ private readonly href: string;
36
45
  readonly bankCode: string;
37
46
  readonly userId: string;
38
47
  private readonly explicitCustomerId: string | undefined;
@@ -42,8 +51,15 @@ export class FinTsConfig {
42
51
  readonly allowInsecureUrl: boolean;
43
52
 
44
53
  /**
45
- * Throws `TypeError` if `url` is not a valid URL, has no host or is not https, unless `allowInsecureUrl` is set and
46
- * `url` is a loopback address, and if a login detail or the product is empty: the bank would refuse the login.
54
+ * Throws `TypeError` if `url` is not a valid URL, has no host, holds a user name or password, or is not https, unless
55
+ * `allowInsecureUrl` is set and `url` is a loopback address. Throws as well where the bank would refuse the login: for
56
+ * 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.
60
+ *
61
+ * Drops leading and trailing blanks of the login details and the product, as FinTS sends them without (Formals
62
+ * B.4.1): `'alice '` is the user `alice`, also for a saved state.
47
63
  */
48
64
  constructor(init: FinTsConfigInit) {
49
65
  // `URL` would read `https:bank` and `https:///bank` as the host `bank`; such a URL is a mistake, not a host.
@@ -56,21 +72,29 @@ export class FinTsConfig {
56
72
  } catch {
57
73
  throw invalidArgument('Not a valid URL', 'url');
58
74
  }
59
- this.url = url;
60
- this.bankCode = init.bankCode;
61
- this.userId = init.userId;
62
- this.explicitCustomerId = init.customerId;
63
- this.productId = init.productId;
64
- this.productVersion = init.productVersion ?? '1.0';
65
- this.countryCode = init.countryCode ?? defaultCountryCode;
75
+ this.href = url.href;
66
76
  this.allowInsecureUrl = init.allowInsecureUrl ?? false;
67
77
  requireSecureUrl(url, this.allowInsecureUrl);
68
- requireNotBlank(this.bankCode, 'bankCode');
69
- requireNotBlank(this.userId, 'userId');
70
- if (this.explicitCustomerId !== undefined) requireNotBlank(this.explicitCustomerId, 'customerId');
71
- requireNotBlank(this.productId, 'productId');
72
- requireNotBlank(this.productVersion, 'productVersion');
73
- requireNotBlank(this.countryCode, 'countryCode');
78
+ this.countryCode = requireValue(init.countryCode ?? defaultCountryCode, 'countryCode');
79
+ if (!/^\d{3}$/.test(this.countryCode)) {
80
+ throw invalidArgument('Must be three digits, 280 for Germany', 'countryCode');
81
+ }
82
+ this.bankCode = requireValue(init.bankCode, 'bankCode', maxBankCodeLength);
83
+ if (this.countryCode === defaultCountryCode && !/^\d{8}$/.test(this.bankCode)) {
84
+ throw invalidArgument('A German bank code (BLZ) has eight digits', 'bankCode');
85
+ }
86
+ this.userId = requireValue(init.userId, 'userId');
87
+ this.explicitCustomerId = init.customerId === undefined ? undefined : requireValue(init.customerId, 'customerId');
88
+ this.productId = requireValue(init.productId, 'productId', maxProductIdLength);
89
+ this.productVersion = requireValue(init.productVersion ?? '1.0', 'productVersion', maxProductVersionLength);
90
+ }
91
+
92
+ /**
93
+ * The bank's FinTS PIN/TAN endpoint, as checked by the constructor. A new `URL` on each access, so that changing one
94
+ * cannot change where the client posts the PIN.
95
+ */
96
+ get url(): URL {
97
+ return new URL(this.href);
74
98
  }
75
99
 
76
100
  /** Customer ID; defaults to `userId`. */
@@ -79,8 +103,22 @@ export class FinTsConfig {
79
103
  }
80
104
  }
81
105
 
82
- function requireNotBlank(value: string, name: string): void {
83
- if (value.trim() === '') throw invalidArgument('Must not be empty', name);
106
+ /**
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.
109
+ */
110
+ function requireValue(value: string, name: string, maxLength = Number.POSITIVE_INFINITY): string {
111
+ const trimmed = value.trim();
112
+ if (trimmed === '') throw invalidArgument('Must not be empty', name);
113
+ requireLatin1(trimmed, name);
114
+ if (trimmed.length > maxLength) throw invalidArgument(`At most ${maxLength} characters in FinTS 3.0`, name);
115
+ return trimmed;
116
+ }
117
+
118
+ /** `text`, unless it has characters FinTS 3.0 cannot send (ISO 8859-1 only); the message leaves out the value. */
119
+ export function requireLatin1(text: string, name: string): string {
120
+ if (!isLatin1(text)) throw invalidArgument('Contains characters FinTS 3.0 cannot send (ISO 8859-1 only)', name);
121
+ return text;
84
122
  }
85
123
 
86
124
  /** The host of `url` without the brackets of an IPv6 address, as `URL` writes it. */
@@ -92,24 +130,27 @@ export function hostOf(url: URL): string {
92
130
  /**
93
131
  * Throws `TypeError` unless `url` names a host and uses https: the PIN travels with every message.
94
132
  *
95
- * With `allowInsecure`, http is accepted for loopback hosts only.
133
+ * With `allowInsecure`, http is accepted for loopback hosts only. Throws as well for a user name or password in `url`:
134
+ * `fetch` refuses such a URL, with the whole URL in its error message. The message leaves them out.
96
135
  */
97
- export function requireSecureUrl(url: URL, allowInsecure = false): void {
136
+ function requireSecureUrl(url: URL, allowInsecure = false): void {
98
137
  if (hostOf(url) === '') throw invalidArgument('The URL has no host', 'url');
138
+ if (url.username !== '' || url.password !== '') throw invalidArgument('The URL must not contain credentials', 'url');
99
139
  const loopbackHttp = allowInsecure && url.protocol === 'http:' && isLoopback(hostOf(url));
100
140
  if (url.protocol !== 'https:' && !loopbackHttp) throw invalidArgument('PIN/TAN requires an https URL', 'url');
101
141
  }
102
142
 
103
143
  /**
104
- * Throws `TypeError` for an empty PIN, which the bank would count as a failed attempt, and for one with leading or
105
- * trailing blanks: FinTS values carry none (Formals B.4.1), so such a PIN could only go out changed.
144
+ * `pin`, unless FinTS 3.0 cannot send it: throws `TypeError` for an empty PIN, which the bank would count as a failed
145
+ * attempt, for one with leading or trailing blanks, which would go out without them since FinTS values carry none
146
+ * (Formals B.4.1), and for one with characters outside ISO 8859-1.
106
147
  *
107
148
  * The message leaves out the value.
108
149
  */
109
150
  export function requirePin(pin: string): string {
110
151
  if (pin.trim() === '') throw invalidArgument('The PIN is empty', 'pin');
111
152
  if (pin.trim() !== pin) throw invalidArgument('The PIN begins or ends with a blank', 'pin');
112
- return pin;
153
+ return requireLatin1(pin, 'pin');
113
154
  }
114
155
 
115
156
  /**
@@ -1,54 +1,68 @@
1
1
  import { FormatError } from '../util/errors.js';
2
- import { endsDialog, FeedbackCodes, type FinTsFeedback } from './feedback.js';
2
+ import { nonBlank } from '../util/text.js';
3
+ import { endsDialog, FeedbackCodes, hasCode, type FinTsFeedback } from './feedback.js';
3
4
 
4
- /** Base class of all errors the client raises about the bank and its data. */
5
+ /**
6
+ * Base class of all errors the client raises about the bank and its data.
7
+ *
8
+ * Each error class states its `name`: a minifier renames classes, and `constructor.name` would show a meaningless one.
9
+ */
5
10
  export abstract class FinTsError extends Error {
6
- protected constructor(name: string, message: string, cause?: unknown) {
7
- super(message, cause === undefined ? undefined : { cause });
8
- // Set explicitly: a minifier renames classes, and `constructor.name` would show a meaningless name.
9
- this.name = name;
10
- }
11
+ abstract override name: string;
11
12
  }
12
13
 
13
- /** The connection to the bank failed or returned an HTTP error. */
14
+ /**
15
+ * The connection to the bank failed or returned an HTTP error.
16
+ *
17
+ * It carries no `cause`: the transport's own error may hold the message it was sending, PIN and TAN included.
18
+ */
14
19
  export class FinTsTransportError extends FinTsError {
15
- /** The HTTP status code of the response, `undefined` if there was none, for example after a timeout. */
20
+ override name = 'FinTsTransportError';
21
+
22
+ /**
23
+ * The HTTP status code of the bank's answer, `undefined` where none came, for example after a timeout; the client
24
+ * acts on it (`FinTsTransport.post`).
25
+ */
16
26
  readonly statusCode: number | undefined;
17
27
 
18
- constructor(message: string, options?: { statusCode?: number; cause?: unknown }) {
19
- super('FinTsTransportError', message, options?.cause);
28
+ constructor(message: string, options?: { statusCode?: number }) {
29
+ super(message);
20
30
  this.statusCode = options?.statusCode;
21
31
  }
22
32
  }
23
33
 
24
34
  /** The bank's response could not be understood. */
25
35
  export class FinTsProtocolError extends FinTsError {
26
- constructor(message: string) {
27
- super('FinTsProtocolError', message);
28
- }
36
+ override name = 'FinTsProtocolError';
29
37
  }
30
38
 
31
39
  /** The bank does not offer the requested business transaction. */
32
40
  export class FinTsUnsupportedError extends FinTsError {
33
- constructor(message: string) {
34
- super('FinTsUnsupportedError', message);
35
- }
41
+ override name = 'FinTsUnsupportedError';
36
42
  }
37
43
 
38
44
  /**
39
- * The bank rejected the request; `feedback` holds all return codes.
45
+ * The client has no TAN method or medium to log in with: the method in use needs a TAN medium the client could not
46
+ * learn, or the bank offers the user only TAN methods the client cannot run. Pass a medium to
47
+ * `FinTsClient.selectTanMethod`, or choose another method.
40
48
  *
41
- * After a refused login every later call that would send the PIN fails with the same error, without contacting the
42
- * bank; create a new client to try again. The README tells which errors refuse a login.
49
+ * The client does not send a login the bank would refuse for it, but the steps before, such as the synchronization or
50
+ * the lookup of the TAN media, may have gone out.
43
51
  */
52
+ export class FinTsTanSetupError extends FinTsError {
53
+ override name = 'FinTsTanSetupError';
54
+ }
55
+
56
+ /** The bank rejected the request; `feedback` holds all return codes. A refused login is a `FinTsLoginRefusedError`. */
44
57
  export class FinTsBankError extends FinTsError {
58
+ override name = 'FinTsBankError';
59
+
45
60
  /** All return codes of the response, warnings and successes included. */
46
61
  readonly feedback: readonly FinTsFeedback[];
47
62
 
48
63
  /** A rejection with the return codes in `feedback`; `message` defaults to the errors among them. */
49
- constructor(feedback: readonly FinTsFeedback[], message?: string, name = 'FinTsBankError') {
64
+ constructor(feedback: readonly FinTsFeedback[], message?: string) {
50
65
  super(
51
- name,
52
66
  message ??
53
67
  feedback
54
68
  .filter((f) => f.isError)
@@ -64,29 +78,44 @@ export class FinTsBankError extends FinTsError {
64
78
  }
65
79
  }
66
80
 
67
- /** Wrong or blocked PIN, a refused login as `FinTsBankError` describes it. */
68
- export class FinTsPinError extends FinTsBankError {
69
- constructor(feedback: readonly FinTsFeedback[]) {
70
- super(feedback, undefined, 'FinTsPinError');
71
- }
81
+ /**
82
+ * The bank refused the login, so the client sends the PIN no more: every later call that would send it fails with this
83
+ * error without contacting the bank, also in a client restored from the saved state, until
84
+ * `FinTsClient.clearLoginRefusal` takes a PIN the user has entered anew; for good where the bank has changed the login
85
+ * data (`FinTsLoginDataChangedError`).
86
+ *
87
+ * Banks lock the access after a few failed logins, and a return code does not always tell whether the PIN was the
88
+ * reason, so any error in the answer to a login refuses it, also one that ends the dialog, and so does a wrong PIN
89
+ * wherever the bank reports it. Only where the bank names a reason the client can take care of, another TAN method or a
90
+ * new customer system ID, does the same call log in once more, for each reason once. The client refuses the login
91
+ * itself where it could not read the answers to two logins in a row, which the bank may have counted as failed
92
+ * attempts; `feedback` is empty then.
93
+ */
94
+ export class FinTsLoginRefusedError extends FinTsBankError {
95
+ override name = 'FinTsLoginRefusedError';
96
+ }
97
+
98
+ /** Wrong or blocked PIN. */
99
+ export class FinTsPinError extends FinTsLoginRefusedError {
100
+ override name = 'FinTsPinError';
72
101
  }
73
102
 
74
103
  /** Invalid, expired or rejected TAN, as the bank reports it in answer to a TAN or to an approval status request. */
75
104
  export class FinTsTanError extends FinTsBankError {
76
- constructor(feedback: readonly FinTsFeedback[]) {
77
- super(feedback, undefined, 'FinTsTanError');
78
- }
105
+ override name = 'FinTsTanError';
79
106
  }
80
107
 
81
108
  /**
82
109
  * The bank has changed the user's login data and expects the new ones from now on (return code 3072).
83
110
  *
84
- * The client has ended the dialog, and every later call fails with this error. Create a new client with the new data; a
85
- * `loginName` replaces user ID and customer ID: pass it as `userId`, without a customer ID.
111
+ * The client has ended the dialog, where the bank opened one, and logs in with the old data no more. Create a new
112
+ * client with the new data; a `loginName` replaces user ID and customer ID: pass it as `userId`, without a customer ID.
86
113
  */
87
- export class FinTsLoginDataChangedError extends FinTsBankError {
114
+ export class FinTsLoginDataChangedError extends FinTsLoginRefusedError {
115
+ override name = 'FinTsLoginDataChangedError';
116
+
88
117
  constructor(feedback: readonly FinTsFeedback[]) {
89
- super(feedback, 'The bank has changed the login data', 'FinTsLoginDataChangedError');
118
+ super(feedback, 'The bank has changed the login data');
90
119
  }
91
120
 
92
121
  /** The new user ID (Benutzerkennung); `undefined` where the bank names none. */
@@ -106,29 +135,36 @@ export class FinTsLoginDataChangedError extends FinTsBankError {
106
135
 
107
136
  private parameter(index: number): string | undefined {
108
137
  const change = this.feedback.find((f) => f.code === FeedbackCodes.newLoginData);
109
- const value = change?.parameters[index]?.trim();
110
- return value === undefined || value === '' ? undefined : value;
138
+ return nonBlank(change?.parameters[index]);
111
139
  }
112
140
  }
113
141
 
114
- /** The bank ended the dialog, for example after a timeout. */
142
+ /**
143
+ * The bank ended the dialog, for example after a timeout; the next call logs in anew. Where the bank ends the dialog
144
+ * in answer to a login, the call fails with `FinTsLoginRefusedError` instead.
145
+ */
115
146
  export class FinTsDialogAbortedError extends FinTsBankError {
116
- constructor(feedback: readonly FinTsFeedback[]) {
117
- super(feedback, undefined, 'FinTsDialogAbortedError');
118
- }
147
+ override name = 'FinTsDialogAbortedError';
119
148
  }
120
149
 
121
150
  /** The user cancelled the TAN entry or the approval in the banking app. */
122
151
  export class FinTsTanCancelledError extends FinTsError {
152
+ override name = 'FinTsTanCancelledError';
153
+
123
154
  constructor() {
124
- super('FinTsTanCancelledError', 'TAN entry cancelled');
155
+ super('TAN entry cancelled');
125
156
  }
126
157
  }
127
158
 
128
- /** The user did not approve in the banking app within the bank's polling limit. */
159
+ /**
160
+ * The user did not approve in the banking app in time: within the status requests the bank allows, 60 where it states
161
+ * none, and 10 minutes of polling at most.
162
+ */
129
163
  export class FinTsDecoupledTimeoutError extends FinTsError {
164
+ override name = 'FinTsDecoupledTimeoutError';
165
+
130
166
  constructor() {
131
- super('FinTsDecoupledTimeoutError', 'Approval on the separate device was not completed in time');
167
+ super('Approval on the separate device was not completed in time');
132
168
  }
133
169
  }
134
170
 
@@ -138,10 +174,12 @@ export class FinTsDecoupledTimeoutError extends FinTsError {
138
174
  * It does not extend `FinTsError`: it tells about the app's use of the client, not about the bank.
139
175
  */
140
176
  export class FinTsStateError extends Error {
141
- constructor(message: string) {
142
- super(message);
143
- this.name = 'FinTsStateError';
144
- }
177
+ override name = 'FinTsStateError';
178
+ }
179
+
180
+ /** The error of whatever a closed client is asked to do. */
181
+ export function clientClosedError(): FinTsStateError {
182
+ return new FinTsStateError('The FinTS client is closed');
145
183
  }
146
184
 
147
185
  /**
@@ -192,7 +230,23 @@ export function bankErrorFor(feedback: readonly FinTsFeedback[], completesApprov
192
230
  return new FinTsBankError(feedback);
193
231
  }
194
232
 
195
- /** Runs `parse` on data from the bank and reports malformed data as `FinTsProtocolError`. */
233
+ /**
234
+ * The refusal of a login whose answer carried `feedback`: `FinTsPinError` for a rejected PIN, else
235
+ * `FinTsLoginDataChangedError` for new login data, also next to other errors, and `FinTsLoginRefusedError` for anything
236
+ * else. The same for a refusal restored from the saved state, which keeps only `feedback`.
237
+ *
238
+ * New login data end the old ones for good, so the session takes care of no other reason the answer names.
239
+ */
240
+ export function loginRefusalFor(feedback: readonly FinTsFeedback[]): FinTsLoginRefusedError {
241
+ if (feedback.some(isPinError)) return new FinTsPinError(feedback);
242
+ if (hasCode(feedback, FeedbackCodes.newLoginData)) return new FinTsLoginDataChangedError(feedback);
243
+ return new FinTsLoginRefusedError(feedback);
244
+ }
245
+
246
+ /**
247
+ * Runs `parse` on data from the bank and reports malformed data as `FinTsProtocolError`, whose message leaves the data
248
+ * out (`FormatError`).
249
+ */
196
250
  export function parseBankData<T>(parse: () => T): T {
197
251
  try {
198
252
  return parse();
@@ -201,3 +255,23 @@ export function parseBankData<T>(parse: () => T): T {
201
255
  throw error;
202
256
  }
203
257
  }
258
+
259
+ /**
260
+ * Runs `lookup`, which only completes data, such as the SEPA account list; `undefined` where the bank rejects it, so
261
+ * that the caller carries on without it. An aborted dialog counts as a rejection: banks end the dialog along with many.
262
+ * A refused login and a rejected TAN fail the call: the user must hear of them.
263
+ */
264
+ export async function tryLookUp<T>(lookup: () => Promise<T>): Promise<T | undefined> {
265
+ try {
266
+ return await lookup();
267
+ } catch (error) {
268
+ if (
269
+ !(error instanceof FinTsBankError) ||
270
+ error instanceof FinTsLoginRefusedError ||
271
+ error instanceof FinTsTanError
272
+ ) {
273
+ throw error;
274
+ }
275
+ return undefined;
276
+ }
277
+ }
@@ -9,7 +9,10 @@ export class FinTsFeedback {
9
9
  /** The bank's explanation of the code. */
10
10
  readonly text: string;
11
11
 
12
- /** Values the bank adds to the code, such as the position to continue at. */
12
+ /**
13
+ * Values the bank adds to the code, such as the position to continue at: one character per byte the bank sent (ISO
14
+ * 8859-1, the FinTS character set), since the client and the app send them back as they came.
15
+ */
13
16
  readonly parameters: readonly string[];
14
17
 
15
18
  constructor(init: { code: string; reference?: string | undefined; text: string; parameters?: readonly string[] }) {
@@ -1,4 +1,4 @@
1
- import { formatIsoDate } from '../util/dates.js';
1
+ import { copyDate, formatIsoDate } from '../util/dates.js';
2
2
  import { ImmutableMap } from '../util/immutable-map.js';
3
3
  import type { Amount } from './amount.js';
4
4
 
@@ -112,13 +112,18 @@ export class Transaction {
112
112
  /** The party the payer acts for, where that is not the payer itself. */
113
113
  readonly ultimateDebtor: string | undefined;
114
114
 
115
- /** All SEPA keywords found in the purpose, keyed by keyword (EREF, KREF, MREF, CRED, SVWZ, …). */
115
+ /**
116
+ * The SEPA keywords of the purpose, keyed by keyword (EREF, KREF, MREF, CRED, SVWZ, …): those `SepaPurpose` reads. In
117
+ * camt, EREF, MREF and CRED are those of `endToEndId`, `mandateId` and `creditorId`, which the bank states apart,
118
+ * never keywords of the remittance text the payer or the payee writes.
119
+ */
116
120
  readonly purposeCodes: ReadonlyMap<string, string>;
117
121
 
122
+ /** Takes copies of the dates and purpose codes of `init`: each transaction has its own, which no other one shares. */
118
123
  constructor(init: TransactionInit) {
119
124
  this.status = init.status ?? 'booked';
120
- this.bookingDate = init.bookingDate;
121
- this.valueDate = init.valueDate;
125
+ this.bookingDate = copyDate(init.bookingDate);
126
+ this.valueDate = copyDate(init.valueDate);
122
127
  this.amount = init.amount;
123
128
  this.isReversal = init.isReversal ?? false;
124
129
  this.bookingText = init.bookingText;