@coderoftime/fints 0.0.0-stage → 0.1.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 (254) hide show
  1. package/CHANGELOG.md +11 -0
  2. package/LICENSE +28 -0
  3. package/README.md +156 -2
  4. package/dist/core/account.d.ts +140 -0
  5. package/dist/core/account.d.ts.map +1 -0
  6. package/dist/core/account.js +174 -0
  7. package/dist/core/account.js.map +1 -0
  8. package/dist/core/amount.d.ts +62 -0
  9. package/dist/core/amount.d.ts.map +1 -0
  10. package/dist/core/amount.js +189 -0
  11. package/dist/core/amount.js.map +1 -0
  12. package/dist/core/balance.d.ts +54 -0
  13. package/dist/core/balance.d.ts.map +1 -0
  14. package/dist/core/balance.js +53 -0
  15. package/dist/core/balance.js.map +1 -0
  16. package/dist/core/bank-info.d.ts +49 -0
  17. package/dist/core/bank-info.d.ts.map +1 -0
  18. package/dist/core/bank-info.js +52 -0
  19. package/dist/core/bank-info.js.map +1 -0
  20. package/dist/core/client.d.ts +126 -0
  21. package/dist/core/client.d.ts.map +1 -0
  22. package/dist/core/client.js +2 -0
  23. package/dist/core/client.js.map +1 -0
  24. package/dist/core/config.d.ts +68 -0
  25. package/dist/core/config.d.ts.map +1 -0
  26. package/dist/core/config.js +113 -0
  27. package/dist/core/config.js.map +1 -0
  28. package/dist/core/errors.d.ts +91 -0
  29. package/dist/core/errors.d.ts.map +1 -0
  30. package/dist/core/errors.js +182 -0
  31. package/dist/core/errors.js.map +1 -0
  32. package/dist/core/feedback.d.ts +69 -0
  33. package/dist/core/feedback.d.ts.map +1 -0
  34. package/dist/core/feedback.js +124 -0
  35. package/dist/core/feedback.js.map +1 -0
  36. package/dist/core/transaction.d.ts +98 -0
  37. package/dist/core/transaction.d.ts.map +1 -0
  38. package/dist/core/transaction.js +91 -0
  39. package/dist/core/transaction.js.map +1 -0
  40. package/dist/core/transport.d.ts +37 -0
  41. package/dist/core/transport.d.ts.map +1 -0
  42. package/dist/core/transport.js +92 -0
  43. package/dist/core/transport.js.map +1 -0
  44. package/dist/formats/camt.d.ts +12 -0
  45. package/dist/formats/camt.d.ts.map +1 -0
  46. package/dist/formats/camt.js +159 -0
  47. package/dist/formats/camt.js.map +1 -0
  48. package/dist/formats/mt940.d.ts +42 -0
  49. package/dist/formats/mt940.d.ts.map +1 -0
  50. package/dist/formats/mt940.js +297 -0
  51. package/dist/formats/mt940.js.map +1 -0
  52. package/dist/formats/readers.d.ts +11 -0
  53. package/dist/formats/readers.d.ts.map +1 -0
  54. package/dist/formats/readers.js +47 -0
  55. package/dist/formats/readers.js.map +1 -0
  56. package/dist/formats/sepa-purpose.d.ts +22 -0
  57. package/dist/formats/sepa-purpose.d.ts.map +1 -0
  58. package/dist/formats/sepa-purpose.js +68 -0
  59. package/dist/formats/sepa-purpose.js.map +1 -0
  60. package/dist/index.d.ts +28 -0
  61. package/dist/index.d.ts.map +1 -0
  62. package/dist/index.js +27 -0
  63. package/dist/index.js.map +1 -0
  64. package/dist/session/app-approval.d.ts +60 -0
  65. package/dist/session/app-approval.d.ts.map +1 -0
  66. package/dist/session/app-approval.js +215 -0
  67. package/dist/session/app-approval.js.map +1 -0
  68. package/dist/session/bank-connection.d.ts +28 -0
  69. package/dist/session/bank-connection.d.ts.map +1 -0
  70. package/dist/session/bank-connection.js +48 -0
  71. package/dist/session/bank-connection.js.map +1 -0
  72. package/dist/session/dialog-runner.d.ts +34 -0
  73. package/dist/session/dialog-runner.d.ts.map +1 -0
  74. package/dist/session/dialog-runner.js +112 -0
  75. package/dist/session/dialog-runner.js.map +1 -0
  76. package/dist/session/login-guard.d.ts +73 -0
  77. package/dist/session/login-guard.d.ts.map +1 -0
  78. package/dist/session/login-guard.js +109 -0
  79. package/dist/session/login-guard.js.map +1 -0
  80. package/dist/session/parameter-data.d.ts +83 -0
  81. package/dist/session/parameter-data.d.ts.map +1 -0
  82. package/dist/session/parameter-data.js +68 -0
  83. package/dist/session/parameter-data.js.map +1 -0
  84. package/dist/session/pin-tan-client.d.ts +54 -0
  85. package/dist/session/pin-tan-client.d.ts.map +1 -0
  86. package/dist/session/pin-tan-client.js +153 -0
  87. package/dist/session/pin-tan-client.js.map +1 -0
  88. package/dist/session/pin-tan-dialog.d.ts +71 -0
  89. package/dist/session/pin-tan-dialog.d.ts.map +1 -0
  90. package/dist/session/pin-tan-dialog.js +124 -0
  91. package/dist/session/pin-tan-dialog.js.map +1 -0
  92. package/dist/session/protocol.d.ts +209 -0
  93. package/dist/session/protocol.d.ts.map +1 -0
  94. package/dist/session/protocol.js +75 -0
  95. package/dist/session/protocol.js.map +1 -0
  96. package/dist/session/response-handler.d.ts +48 -0
  97. package/dist/session/response-handler.d.ts.map +1 -0
  98. package/dist/session/response-handler.js +121 -0
  99. package/dist/session/response-handler.js.map +1 -0
  100. package/dist/session/sepa-accounts.d.ts +34 -0
  101. package/dist/session/sepa-accounts.d.ts.map +1 -0
  102. package/dist/session/sepa-accounts.js +96 -0
  103. package/dist/session/sepa-accounts.js.map +1 -0
  104. package/dist/session/session-setup.d.ts +72 -0
  105. package/dist/session/session-setup.d.ts.map +1 -0
  106. package/dist/session/session-setup.js +160 -0
  107. package/dist/session/session-setup.js.map +1 -0
  108. package/dist/session/session-state.d.ts +47 -0
  109. package/dist/session/session-state.d.ts.map +1 -0
  110. package/dist/session/session-state.js +147 -0
  111. package/dist/session/session-state.js.map +1 -0
  112. package/dist/session/tan-selection.d.ts +88 -0
  113. package/dist/session/tan-selection.d.ts.map +1 -0
  114. package/dist/session/tan-selection.js +178 -0
  115. package/dist/session/tan-selection.js.map +1 -0
  116. package/dist/session/user-prompts.d.ts +61 -0
  117. package/dist/session/user-prompts.d.ts.map +1 -0
  118. package/dist/session/user-prompts.js +164 -0
  119. package/dist/session/user-prompts.js.map +1 -0
  120. package/dist/tan/challenge.d.ts +114 -0
  121. package/dist/tan/challenge.d.ts.map +1 -0
  122. package/dist/tan/challenge.js +185 -0
  123. package/dist/tan/challenge.js.map +1 -0
  124. package/dist/tan/flicker.d.ts +36 -0
  125. package/dist/tan/flicker.d.ts.map +1 -0
  126. package/dist/tan/flicker.js +262 -0
  127. package/dist/tan/flicker.js.map +1 -0
  128. package/dist/tan/tan-image.d.ts +21 -0
  129. package/dist/tan/tan-image.d.ts.map +1 -0
  130. package/dist/tan/tan-image.js +41 -0
  131. package/dist/tan/tan-image.js.map +1 -0
  132. package/dist/tan/tan-models.d.ts +145 -0
  133. package/dist/tan/tan-models.d.ts.map +1 -0
  134. package/dist/tan/tan-models.js +148 -0
  135. package/dist/tan/tan-models.js.map +1 -0
  136. package/dist/util/async.d.ts +24 -0
  137. package/dist/util/async.d.ts.map +1 -0
  138. package/dist/util/async.js +49 -0
  139. package/dist/util/async.js.map +1 -0
  140. package/dist/util/bytes.d.ts +23 -0
  141. package/dist/util/bytes.d.ts.map +1 -0
  142. package/dist/util/bytes.js +136 -0
  143. package/dist/util/bytes.js.map +1 -0
  144. package/dist/util/call-context.d.ts +12 -0
  145. package/dist/util/call-context.d.ts.map +1 -0
  146. package/dist/util/call-context.js +29 -0
  147. package/dist/util/call-context.js.map +1 -0
  148. package/dist/util/dates.d.ts +62 -0
  149. package/dist/util/dates.d.ts.map +1 -0
  150. package/dist/util/dates.js +169 -0
  151. package/dist/util/dates.js.map +1 -0
  152. package/dist/util/errors.d.ts +9 -0
  153. package/dist/util/errors.d.ts.map +1 -0
  154. package/dist/util/errors.js +15 -0
  155. package/dist/util/errors.js.map +1 -0
  156. package/dist/util/immutable-map.d.ts +16 -0
  157. package/dist/util/immutable-map.d.ts.map +1 -0
  158. package/dist/util/immutable-map.js +38 -0
  159. package/dist/util/immutable-map.js.map +1 -0
  160. package/dist/util/json.d.ts +13 -0
  161. package/dist/util/json.d.ts.map +1 -0
  162. package/dist/util/json.js +44 -0
  163. package/dist/util/json.js.map +1 -0
  164. package/dist/util/text.d.ts +20 -0
  165. package/dist/util/text.d.ts.map +1 -0
  166. package/dist/util/text.js +78 -0
  167. package/dist/util/text.js.map +1 -0
  168. package/dist/util/xml.d.ts +35 -0
  169. package/dist/util/xml.d.ts.map +1 -0
  170. package/dist/util/xml.js +183 -0
  171. package/dist/util/xml.js.map +1 -0
  172. package/dist/v3/account-extension.d.ts +25 -0
  173. package/dist/v3/account-extension.d.ts.map +1 -0
  174. package/dist/v3/account-extension.js +176 -0
  175. package/dist/v3/account-extension.js.map +1 -0
  176. package/dist/v3/client.d.ts +24 -0
  177. package/dist/v3/client.d.ts.map +1 -0
  178. package/dist/v3/client.js +16 -0
  179. package/dist/v3/client.js.map +1 -0
  180. package/dist/v3/dialog.d.ts +78 -0
  181. package/dist/v3/dialog.d.ts.map +1 -0
  182. package/dist/v3/dialog.js +138 -0
  183. package/dist/v3/dialog.js.map +1 -0
  184. package/dist/v3/message.d.ts +72 -0
  185. package/dist/v3/message.d.ts.map +1 -0
  186. package/dist/v3/message.js +259 -0
  187. package/dist/v3/message.js.map +1 -0
  188. package/dist/v3/parameters.d.ts +79 -0
  189. package/dist/v3/parameters.d.ts.map +1 -0
  190. package/dist/v3/parameters.js +396 -0
  191. package/dist/v3/parameters.js.map +1 -0
  192. package/dist/v3/protocol.d.ts +68 -0
  193. package/dist/v3/protocol.d.ts.map +1 -0
  194. package/dist/v3/protocol.js +263 -0
  195. package/dist/v3/protocol.js.map +1 -0
  196. package/dist/v3/segments.d.ts +89 -0
  197. package/dist/v3/segments.d.ts.map +1 -0
  198. package/dist/v3/segments.js +283 -0
  199. package/dist/v3/segments.js.map +1 -0
  200. package/dist/v3/syntax.d.ts +76 -0
  201. package/dist/v3/syntax.d.ts.map +1 -0
  202. package/dist/v3/syntax.js +301 -0
  203. package/dist/v3/syntax.js.map +1 -0
  204. package/package.json +63 -4
  205. package/src/core/account.ts +250 -0
  206. package/src/core/amount.ts +209 -0
  207. package/src/core/balance.ts +84 -0
  208. package/src/core/bank-info.ts +66 -0
  209. package/src/core/client.ts +149 -0
  210. package/src/core/config.ts +141 -0
  211. package/src/core/errors.ts +203 -0
  212. package/src/core/feedback.ts +137 -0
  213. package/src/core/transaction.ts +156 -0
  214. package/src/core/transport.ts +114 -0
  215. package/src/formats/camt.ts +169 -0
  216. package/src/formats/mt940.ts +333 -0
  217. package/src/formats/readers.ts +56 -0
  218. package/src/formats/sepa-purpose.ts +75 -0
  219. package/src/index.ts +68 -0
  220. package/src/session/app-approval.ts +265 -0
  221. package/src/session/bank-connection.ts +51 -0
  222. package/src/session/dialog-runner.ts +119 -0
  223. package/src/session/login-guard.ts +135 -0
  224. package/src/session/parameter-data.ts +137 -0
  225. package/src/session/pin-tan-client.ts +186 -0
  226. package/src/session/pin-tan-dialog.ts +160 -0
  227. package/src/session/protocol.ts +311 -0
  228. package/src/session/response-handler.ts +130 -0
  229. package/src/session/sepa-accounts.ts +97 -0
  230. package/src/session/session-setup.ts +165 -0
  231. package/src/session/session-state.ts +170 -0
  232. package/src/session/tan-selection.ts +192 -0
  233. package/src/session/user-prompts.ts +174 -0
  234. package/src/tan/challenge.ts +252 -0
  235. package/src/tan/flicker.ts +270 -0
  236. package/src/tan/tan-image.ts +36 -0
  237. package/src/tan/tan-models.ts +247 -0
  238. package/src/util/async.ts +53 -0
  239. package/src/util/bytes.ts +135 -0
  240. package/src/util/call-context.ts +40 -0
  241. package/src/util/dates.ts +193 -0
  242. package/src/util/errors.ts +16 -0
  243. package/src/util/immutable-map.ts +47 -0
  244. package/src/util/json.ts +51 -0
  245. package/src/util/text.ts +82 -0
  246. package/src/util/xml.ts +172 -0
  247. package/src/v3/account-extension.ts +190 -0
  248. package/src/v3/client.ts +29 -0
  249. package/src/v3/dialog.ts +199 -0
  250. package/src/v3/message.ts +321 -0
  251. package/src/v3/parameters.ts +451 -0
  252. package/src/v3/protocol.ts +331 -0
  253. package/src/v3/segments.ts +331 -0
  254. package/src/v3/syntax.ts +307 -0
@@ -0,0 +1,149 @@
1
+ import type { TanCallback, TanMediumSelector, TanMethodSelector } from '../tan/challenge.js';
2
+ import type { TanMedium, TanMethod } from '../tan/tan-models.js';
3
+ import type { Account } from './account.js';
4
+ import type { Balance } from './balance.js';
5
+ import type { BankInfo, BankMessage } from './bank-info.js';
6
+ import type { FinTsFeedback } from './feedback.js';
7
+ import type { Transaction, TransactionFormat } from './transaction.js';
8
+ import type { FinTsTransport } from './transport.js';
9
+
10
+ /** Which way a FinTS message travels: from the client to the bank, or back. */
11
+ export type MessageDirection = 'outgoing' | 'incoming';
12
+
13
+ /** Receives every FinTS message with PIN and TAN masked. */
14
+ export type MessageLogger = (direction: MessageDirection, message: string) => void;
15
+
16
+ /** Receives free-text messages the bank sends during login. */
17
+ export type BankMessageHandler = (message: BankMessage) => void;
18
+
19
+ /** Receives a warning the bank sends, a return code from 3000 to 3999. */
20
+ export type WarningHandler = (warning: FinTsFeedback) => void;
21
+
22
+ /**
23
+ * How a client reaches the user and the bank, apart from the login data in `FinTsConfig`.
24
+ *
25
+ * An error thrown by a callback that only informs the app (`onBankMessage`, `onWarning`, `logger`) fails the call once
26
+ * the call's exchange with the bank is over, so that it cannot break off a dialog half-way; an error of the call itself
27
+ * takes precedence.
28
+ */
29
+ export interface FinTsClientOptions {
30
+ /** Called whenever the bank asks for a TAN or an app approval. */
31
+ tanCallback: TanCallback;
32
+
33
+ /** Picks the TAN method when the bank offers several; the first is used without it. */
34
+ tanMethodSelector?: TanMethodSelector;
35
+
36
+ /** Picks the TAN medium when the method needs one and the user has several; the first is used without it. */
37
+ tanMediumSelector?: TanMediumSelector;
38
+
39
+ /** Receives the free-text messages the bank sends during login. */
40
+ onBankMessage?: BankMessageHandler;
41
+
42
+ /**
43
+ * Receives the bank's warnings, such as 3951 (switch to another TAN method), but not those the client acts on itself,
44
+ * such as 3040 (more data follow). Show them to the user with code and text, as the specification asks.
45
+ */
46
+ onWarning?: WarningHandler;
47
+
48
+ /** Receives every FinTS message with PIN and TAN masked, for diagnosis; the messages contain account data. */
49
+ logger?: MessageLogger;
50
+
51
+ /**
52
+ * The `FinTsClient.state` of a previous session, which skips the bank setup steps.
53
+ *
54
+ * A state saved for another user or bank is ignored.
55
+ */
56
+ state?: string | undefined;
57
+
58
+ /**
59
+ * Sends the messages; an `HttpTransport` by default.
60
+ *
61
+ * The client closes it on `FinTsClient.close`, so hand each client a transport of its own.
62
+ */
63
+ transport?: FinTsTransport;
64
+ }
65
+
66
+ /** What `getTransactions` takes besides the account. */
67
+ export interface TransactionQuery {
68
+ /** The first day, a day in the future counting as today; banks typically ask for a TAN beyond 90 days ago. */
69
+ from?: Date | undefined;
70
+ /** The last day, a day in the future counting as today. */
71
+ to?: Date | undefined;
72
+ /** The format to ask for; `auto` by default. */
73
+ format?: TransactionFormat;
74
+ }
75
+
76
+ /**
77
+ * Read access to a bank account over FinTS.
78
+ *
79
+ * All calls share one bank dialog, so strong authentication happens once per session. Calls run one after another; call
80
+ * `close` when done.
81
+ *
82
+ * A callback in `FinTsClientOptions` must not wait for another call of its client, which would wait for the running
83
+ * call. On Node.js, Deno and Bun such a call fails with `FinTsStateError`; elsewhere it would wait forever. `close` works
84
+ * from anywhere.
85
+ */
86
+ export interface FinTsClient {
87
+ /**
88
+ * Serialized session data: bank and user parameters, customer system ID, the TAN methods the user may use, the chosen
89
+ * TAN method and medium, and the SEPA account list.
90
+ *
91
+ * Contains no PIN or TAN, but account details, so store it encrypted.
92
+ */
93
+ readonly state: string;
94
+
95
+ /** Security function code of the TAN method in use, for example `942`; `undefined` before the first login. */
96
+ readonly selectedTanMethod: string | undefined;
97
+
98
+ /** Name of the TAN medium sent to the bank, if the method needs one. */
99
+ readonly selectedTanMedium: string | undefined;
100
+
101
+ /**
102
+ * Switches to another TAN method, and optionally TAN medium, for the following calls.
103
+ *
104
+ * Ends the current dialog, so the next call logs in again with the new method. If the bank does not offer
105
+ * `securityFunction`, the client picks another method at that login.
106
+ */
107
+ selectTanMethod(securityFunction: string, tanMedium?: string): Promise<void>;
108
+
109
+ /** General information about the bank, from the bank parameter data. */
110
+ getBankInfo(): Promise<BankInfo>;
111
+
112
+ /** TAN procedures the bank allows for this user. */
113
+ getTanMethods(): Promise<TanMethod[]>;
114
+
115
+ /** TAN devices registered for this user. */
116
+ getTanMedia(): Promise<TanMedium[]>;
117
+
118
+ /** Accounts of the user, completed with IBAN and BIC where the bank provides them. */
119
+ getAccounts(): Promise<Account[]>;
120
+
121
+ /**
122
+ * The balance of `account`.
123
+ *
124
+ * Where the user parameter data carry the complete balance (account extension), possibly from the saved `state`, it
125
+ * comes from there without a query; `Balance.bookedAsOf` tells the day it states.
126
+ *
127
+ * Throws `FinTsUnsupportedError`, without asking the bank, where the parameter data rule the query out.
128
+ */
129
+ getBalance(account: Account): Promise<Balance>;
130
+
131
+ /**
132
+ * Transactions from the day of `from` to the day of `to`, both included, booked and pending, sorted by date; those of
133
+ * the same day in the bank's order.
134
+ *
135
+ * Only the local calendar day counts, and a day in the future counts as today. Banks typically ask for a TAN when
136
+ * `from` is more than 90 days ago; see also `Account.dataStatus`. Throws `TypeError` if `from` lies on a day after
137
+ * `to`, and `FinTsUnsupportedError`, without asking the bank, where the parameter data rule the query out.
138
+ */
139
+ getTransactions(account: Account, query?: TransactionQuery): Promise<Transaction[]>;
140
+
141
+ /**
142
+ * Ends the bank dialog and releases the connection.
143
+ *
144
+ * A running call stops at its next step, also while it waits for a TAN, an app approval or a selection, and fails with
145
+ * `FinTsStateError`, as do all later calls. A request already on its way is awaited, at most for the transport's
146
+ * timeout.
147
+ */
148
+ close(): Promise<void>;
149
+ }
@@ -0,0 +1,141 @@
1
+ import { invalidArgument } from '../util/errors.js';
2
+
3
+ /** Numeric country code of Germany (ISO 3166), where FinTS banks are. */
4
+ export const defaultCountryCode = '280';
5
+
6
+ /** What `FinTsConfig` takes. */
7
+ export interface FinTsConfigInit {
8
+ /** The bank's FinTS PIN/TAN endpoint; must be https. */
9
+ url: string | URL;
10
+ /** German bank code (BLZ). */
11
+ bankCode: string;
12
+ /** Login name (Benutzerkennung / Legitimations-ID). */
13
+ userId: string;
14
+ /** Customer ID; defaults to `userId`. */
15
+ customerId?: string | undefined;
16
+ /** FinTS product registration number from the Deutsche Kreditwirtschaft; at most 25 characters. */
17
+ productId: string;
18
+ /** Version of the app as registered with the product; at most five characters. `1.0` by default. */
19
+ productVersion?: string;
20
+ /** Numeric country code of the bank, `280` (Germany) by default. */
21
+ countryCode?: string;
22
+ /**
23
+ * Allows plain http for the loopback addresses `localhost`, `127.x.x.x` and `::1`, for tests against a local server.
24
+ *
25
+ * Any other host must use https, whatever this says: the PIN travels with every message.
26
+ */
27
+ allowInsecureUrl?: boolean;
28
+ }
29
+
30
+ /** Connection and login data for one bank user. */
31
+ export class FinTsConfig {
32
+ /** Numeric country code of Germany (ISO 3166), where FinTS banks are. */
33
+ static readonly defaultCountryCode = defaultCountryCode;
34
+
35
+ readonly url: URL;
36
+ readonly bankCode: string;
37
+ readonly userId: string;
38
+ private readonly explicitCustomerId: string | undefined;
39
+ readonly productId: string;
40
+ readonly productVersion: string;
41
+ readonly countryCode: string;
42
+ readonly allowInsecureUrl: boolean;
43
+
44
+ /**
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.
47
+ */
48
+ constructor(init: FinTsConfigInit) {
49
+ // `URL` would read `https:bank` and `https:///bank` as the host `bank`; such a URL is a mistake, not a host.
50
+ if (typeof init.url === 'string' && !/^[a-z][a-z0-9+.-]*:\/\/[^/]/i.test(init.url)) {
51
+ throw invalidArgument('The URL has no host', 'url');
52
+ }
53
+ let url: URL;
54
+ try {
55
+ url = new URL(init.url);
56
+ } catch {
57
+ throw invalidArgument('Not a valid URL', 'url');
58
+ }
59
+ this.url = url;
60
+ this.bankCode = init.bankCode;
61
+ this.userId = init.userId;
62
+ this.explicitCustomerId = init.customerId;
63
+ this.productId = init.productId;
64
+ this.productVersion = init.productVersion ?? '1.0';
65
+ this.countryCode = init.countryCode ?? defaultCountryCode;
66
+ this.allowInsecureUrl = init.allowInsecureUrl ?? false;
67
+ 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');
74
+ }
75
+
76
+ /** Customer ID; defaults to `userId`. */
77
+ get customerId(): string {
78
+ return this.explicitCustomerId ?? this.userId;
79
+ }
80
+ }
81
+
82
+ function requireNotBlank(value: string, name: string): void {
83
+ if (value.trim() === '') throw invalidArgument('Must not be empty', name);
84
+ }
85
+
86
+ /** The host of `url` without the brackets of an IPv6 address, as `URL` writes it. */
87
+ export function hostOf(url: URL): string {
88
+ const host = url.hostname;
89
+ return host.startsWith('[') && host.endsWith(']') ? host.substring(1, host.length - 1) : host;
90
+ }
91
+
92
+ /**
93
+ * Throws `TypeError` unless `url` names a host and uses https: the PIN travels with every message.
94
+ *
95
+ * With `allowInsecure`, http is accepted for loopback hosts only.
96
+ */
97
+ export function requireSecureUrl(url: URL, allowInsecure = false): void {
98
+ if (hostOf(url) === '') throw invalidArgument('The URL has no host', 'url');
99
+ const loopbackHttp = allowInsecure && url.protocol === 'http:' && isLoopback(hostOf(url));
100
+ if (url.protocol !== 'https:' && !loopbackHttp) throw invalidArgument('PIN/TAN requires an https URL', 'url');
101
+ }
102
+
103
+ /**
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.
106
+ *
107
+ * The message leaves out the value.
108
+ */
109
+ export function requirePin(pin: string): string {
110
+ if (pin.trim() === '') throw invalidArgument('The PIN is empty', 'pin');
111
+ if (pin.trim() !== pin) throw invalidArgument('The PIN begins or ends with a blank', 'pin');
112
+ return pin;
113
+ }
114
+
115
+ /**
116
+ * Throws `TypeError` if `pin` is shorter than `min` or longer than `max` characters, the bank's limits: the bank would
117
+ * count such a PIN as a failed attempt. A limit that is `undefined` or below 1 does not apply.
118
+ *
119
+ * The message leaves out the PIN and its length.
120
+ */
121
+ export function requirePinLength(pin: string, min: number | undefined, max: number | undefined): void {
122
+ const tooShort = min !== undefined && min > 0 && pin.length < min;
123
+ const tooLong = max !== undefined && max > 0 && pin.length > max;
124
+ if (tooShort || tooLong) {
125
+ const range = [
126
+ ...(min !== undefined && min > 0 ? [`at least ${min}`] : []),
127
+ ...(max !== undefined && max > 0 ? [`at most ${max}`] : []),
128
+ ].join(' and ');
129
+ throw invalidArgument(`The bank takes PINs of ${range} characters`, 'pin');
130
+ }
131
+ }
132
+
133
+ const loopbackAddress = /^127(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}$/;
134
+
135
+ /**
136
+ * Only addresses and names that cannot leave the machine: a resolver may send a subdomain such as `bank.localhost`, or
137
+ * a string that is not an address, such as `127.999.1.1`, to DNS.
138
+ */
139
+ function isLoopback(host: string): boolean {
140
+ return host === 'localhost' || host === '::1' || loopbackAddress.test(host);
141
+ }
@@ -0,0 +1,203 @@
1
+ import { FormatError } from '../util/errors.js';
2
+ import { endsDialog, FeedbackCodes, type FinTsFeedback } from './feedback.js';
3
+
4
+ /** Base class of all errors the client raises about the bank and its data. */
5
+ 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
+ }
12
+
13
+ /** The connection to the bank failed or returned an HTTP error. */
14
+ export class FinTsTransportError extends FinTsError {
15
+ /** The HTTP status code of the response, `undefined` if there was none, for example after a timeout. */
16
+ readonly statusCode: number | undefined;
17
+
18
+ constructor(message: string, options?: { statusCode?: number; cause?: unknown }) {
19
+ super('FinTsTransportError', message, options?.cause);
20
+ this.statusCode = options?.statusCode;
21
+ }
22
+ }
23
+
24
+ /** The bank's response could not be understood. */
25
+ export class FinTsProtocolError extends FinTsError {
26
+ constructor(message: string) {
27
+ super('FinTsProtocolError', message);
28
+ }
29
+ }
30
+
31
+ /** The bank does not offer the requested business transaction. */
32
+ export class FinTsUnsupportedError extends FinTsError {
33
+ constructor(message: string) {
34
+ super('FinTsUnsupportedError', message);
35
+ }
36
+ }
37
+
38
+ /**
39
+ * The bank rejected the request; `feedback` holds all return codes.
40
+ *
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.
43
+ */
44
+ export class FinTsBankError extends FinTsError {
45
+ /** All return codes of the response, warnings and successes included. */
46
+ readonly feedback: readonly FinTsFeedback[];
47
+
48
+ /** A rejection with the return codes in `feedback`; `message` defaults to the errors among them. */
49
+ constructor(feedback: readonly FinTsFeedback[], message?: string, name = 'FinTsBankError') {
50
+ super(
51
+ name,
52
+ message ??
53
+ feedback
54
+ .filter((f) => f.isError)
55
+ .map((f) => f.toString())
56
+ .join('; '),
57
+ );
58
+ this.feedback = Object.freeze([...feedback]);
59
+ }
60
+
61
+ /** The return codes of `feedback`. */
62
+ get codes(): string[] {
63
+ return this.feedback.map((f) => f.code);
64
+ }
65
+ }
66
+
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
+ }
72
+ }
73
+
74
+ /** Invalid, expired or rejected TAN, as the bank reports it in answer to a TAN or to an approval status request. */
75
+ export class FinTsTanError extends FinTsBankError {
76
+ constructor(feedback: readonly FinTsFeedback[]) {
77
+ super(feedback, undefined, 'FinTsTanError');
78
+ }
79
+ }
80
+
81
+ /**
82
+ * The bank has changed the user's login data and expects the new ones from now on (return code 3072).
83
+ *
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.
86
+ */
87
+ export class FinTsLoginDataChangedError extends FinTsBankError {
88
+ constructor(feedback: readonly FinTsFeedback[]) {
89
+ super(feedback, 'The bank has changed the login data', 'FinTsLoginDataChangedError');
90
+ }
91
+
92
+ /** The new user ID (Benutzerkennung); `undefined` where the bank names none. */
93
+ get userId(): string | undefined {
94
+ return this.parameter(0);
95
+ }
96
+
97
+ /** The new customer ID (Kunden-ID); `undefined` where the bank names none. */
98
+ get customerId(): string | undefined {
99
+ return this.parameter(1);
100
+ }
101
+
102
+ /** The new login name (Anmeldename); `undefined` where the bank names none. */
103
+ get loginName(): string | undefined {
104
+ return this.parameter(2);
105
+ }
106
+
107
+ private parameter(index: number): string | undefined {
108
+ const change = this.feedback.find((f) => f.code === FeedbackCodes.newLoginData);
109
+ const value = change?.parameters[index]?.trim();
110
+ return value === undefined || value === '' ? undefined : value;
111
+ }
112
+ }
113
+
114
+ /** The bank ended the dialog, for example after a timeout. */
115
+ export class FinTsDialogAbortedError extends FinTsBankError {
116
+ constructor(feedback: readonly FinTsFeedback[]) {
117
+ super(feedback, undefined, 'FinTsDialogAbortedError');
118
+ }
119
+ }
120
+
121
+ /** The user cancelled the TAN entry or the approval in the banking app. */
122
+ export class FinTsTanCancelledError extends FinTsError {
123
+ constructor() {
124
+ super('FinTsTanCancelledError', 'TAN entry cancelled');
125
+ }
126
+ }
127
+
128
+ /** The user did not approve in the banking app within the bank's polling limit. */
129
+ export class FinTsDecoupledTimeoutError extends FinTsError {
130
+ constructor() {
131
+ super('FinTsDecoupledTimeoutError', 'Approval on the separate device was not completed in time');
132
+ }
133
+ }
134
+
135
+ /**
136
+ * The client was closed, or called from inside one of its own callbacks, which would wait for itself.
137
+ *
138
+ * It does not extend `FinTsError`: it tells about the app's use of the client, not about the bank.
139
+ */
140
+ export class FinTsStateError extends Error {
141
+ constructor(message: string) {
142
+ super(message);
143
+ this.name = 'FinTsStateError';
144
+ }
145
+ }
146
+
147
+ /**
148
+ * Return codes for a wrong or blocked PIN, from the FinTS return code list (Rückmeldungscodes): 9340 signature (PIN)
149
+ * wrong, 9910 PIN invalid or blocked, 9930 PIN blocked, 9931 blocked after failed attempts, 9942 PIN invalid.
150
+ */
151
+ const pinErrors = new Set(['9340', '9910', '9930', '9931', '9942']);
152
+
153
+ /**
154
+ * Return codes for a rejected TAN: 9941 TAN invalid, 9943 and 9991 TAN already used (the return code list and the
155
+ * PIN/TAN specification give different numbers), 9951 two-step timeout.
156
+ *
157
+ * The return code list gives 9941 and 9951 a second meaning, "Signatur ungültig" (invalid signature), which in PIN/TAN
158
+ * is the PIN; see `bankErrorFor`.
159
+ */
160
+ const tanErrors = new Set(['9941', '9943', '9951', '9991']);
161
+
162
+ /**
163
+ * Whether `f` reports a wrong or blocked PIN. Two of the codes stand for other faults as well, which the text tells:
164
+ * 9340 "Sicherheitsprofil unbekannt", 9910 "TAN-Reihe gesperrt" and "Chipkarte gesperrt".
165
+ */
166
+ function isPinError(f: FinTsFeedback): boolean {
167
+ if (!f.isError || !pinErrors.has(f.code)) return false;
168
+ const text = f.text.toLowerCase();
169
+ switch (f.code) {
170
+ case '9340':
171
+ return !text.includes('profil');
172
+ case '9910':
173
+ return !text.includes('tan-reihe') && !text.includes('chipkarte');
174
+ default:
175
+ return true;
176
+ }
177
+ }
178
+
179
+ /**
180
+ * The most specific error for the errors in `feedback`, the answer to a message that `completesApproval` or not.
181
+ *
182
+ * Only the answer to a TAN or an approval status request can reject a TAN; anywhere else a TAN return code is a plain
183
+ * `FinTsBankError`. In answer to a login, 9941 or 9951 may reject the PIN, and the client must not take a refused login
184
+ * for a wrong TAN, after which it would send the PIN again.
185
+ */
186
+ export function bankErrorFor(feedback: readonly FinTsFeedback[], completesApproval = false): FinTsBankError {
187
+ if (feedback.some(isPinError)) return new FinTsPinError(feedback);
188
+ if (completesApproval && feedback.some((f) => f.isError && tanErrors.has(f.code))) {
189
+ return new FinTsTanError(feedback);
190
+ }
191
+ if (endsDialog(feedback)) return new FinTsDialogAbortedError(feedback);
192
+ return new FinTsBankError(feedback);
193
+ }
194
+
195
+ /** Runs `parse` on data from the bank and reports malformed data as `FinTsProtocolError`. */
196
+ export function parseBankData<T>(parse: () => T): T {
197
+ try {
198
+ return parse();
199
+ } catch (error) {
200
+ if (error instanceof FormatError) throw new FinTsProtocolError(`Malformed bank data: ${error.message}`);
201
+ throw error;
202
+ }
203
+ }
@@ -0,0 +1,137 @@
1
+ /** A return code with text from the bank, for example `0020: Auftrag ausgeführt`. */
2
+ export class FinTsFeedback {
3
+ /** The four-digit return code; `0…` reports success, `3…` a warning and `9…` an error. */
4
+ readonly code: string;
5
+
6
+ /** The element of the request the code refers to, if the bank names one. */
7
+ readonly reference: string | undefined;
8
+
9
+ /** The bank's explanation of the code. */
10
+ readonly text: string;
11
+
12
+ /** Values the bank adds to the code, such as the position to continue at. */
13
+ readonly parameters: readonly string[];
14
+
15
+ constructor(init: { code: string; reference?: string | undefined; text: string; parameters?: readonly string[] }) {
16
+ this.code = init.code;
17
+ this.reference = init.reference;
18
+ this.text = init.text;
19
+ this.parameters = Object.freeze([...(init.parameters ?? [])]);
20
+ }
21
+
22
+ /** Whether the code reports success. */
23
+ get isSuccess(): boolean {
24
+ return this.code.startsWith('0');
25
+ }
26
+
27
+ /** Whether the code is a warning. */
28
+ get isWarning(): boolean {
29
+ return this.code.startsWith('3');
30
+ }
31
+
32
+ /** Whether the code is an error. */
33
+ get isError(): boolean {
34
+ return this.code.startsWith('9');
35
+ }
36
+
37
+ toString(): string {
38
+ return `${this.code}: ${this.text}${this.parameters.length === 0 ? '' : ` [${this.parameters.join(', ')}]`}`;
39
+ }
40
+ }
41
+
42
+ /** Return codes the client reacts to, as named in the FinTS return code list (Rückmeldungscodes). */
43
+ export const FeedbackCodes = Object.freeze({
44
+ /** Order received, strong authentication required (Auftrag empfangen – Sicherheitsfreigabe erforderlich). */
45
+ tanRequired: '0030',
46
+ /** More information available; the first parameter is where to continue (Es liegen weitere Informationen vor). */
47
+ moreData: '3040',
48
+ /**
49
+ * The bank has new login data for the user; the parameters are the user ID, the customer ID and the login name
50
+ * (Neue Anmeldedaten).
51
+ */
52
+ newLoginData: '3072',
53
+ /** The one- and two-step methods the user may use, as parameters. */
54
+ allowedTanMethods: '3920',
55
+ /** Approval happens on another channel, such as a banking app (Sicherheitsfreigabe erfolgt über anderen Kanal). */
56
+ decoupledPending: '3955',
57
+ /** Strong authentication still pending (Starke Kundenauthentifizierung noch ausstehend). */
58
+ decoupledStillPending: '3956',
59
+ /** Wait for the push message (Auf Push-Nachricht warten). */
60
+ decoupledWaitForPush: '3957',
61
+ /** The bank ended the dialog (Dialog abgebrochen). */
62
+ dialogAborted: '9800',
63
+ /**
64
+ * The bank ended the dialog because it requires strong authentication (Dialog abgebrochen – starke Authentifizierung
65
+ * erforderlich).
66
+ */
67
+ scaRequired: '9075',
68
+ /** The customer system ID is not accepted any more; a synchronization delivers a new one (Neue Kundensystem-ID holen). */
69
+ newSystemIdRequired: '9391',
70
+ });
71
+
72
+ /**
73
+ * The return codes with which the bank ends the dialog ("Dialog abgebrochen" in the return code list): 9800 and 9075,
74
+ * and 9077 (hardened browser required), 9078 (product not registered) and 9361 (security procedure no longer allowed).
75
+ */
76
+ const dialogEnding = new Set<string>([FeedbackCodes.dialogAborted, FeedbackCodes.scaRequired, '9077', '9078', '9361']);
77
+
78
+ /**
79
+ * The warnings about the protocol, which the client acts on itself and the user has no use for: paging, the allowed TAN
80
+ * methods, the steps of an approval and 3076 (no strong authentication needed), new login data, which fail the call,
81
+ * and 3060 (some warnings), which only announces the others.
82
+ */
83
+ const protocolWarnings = new Set<string>([
84
+ FeedbackCodes.moreData,
85
+ '3060',
86
+ FeedbackCodes.newLoginData,
87
+ '3076',
88
+ FeedbackCodes.allowedTanMethods,
89
+ FeedbackCodes.decoupledPending,
90
+ FeedbackCodes.decoupledStillPending,
91
+ FeedbackCodes.decoupledWaitForPush,
92
+ ]);
93
+
94
+ // Reading return codes the way the session logic needs; not part of the public API.
95
+
96
+ /** Whether a return code `code` is among the `feedback`. */
97
+ export function hasCode(feedback: readonly FinTsFeedback[], code: string): boolean {
98
+ return feedback.some((f) => f.code === code);
99
+ }
100
+
101
+ /** Whether any return code is an error. */
102
+ export function hasErrors(feedback: readonly FinTsFeedback[]): boolean {
103
+ return feedback.some((f) => f.isError);
104
+ }
105
+
106
+ /** The positions to continue at that the bank names with return code 3040; empty on the last page. */
107
+ export function continuations(feedback: readonly FinTsFeedback[]): Set<string> {
108
+ const positions = new Set<string>();
109
+ for (const f of feedback) {
110
+ const position = f.parameters[0];
111
+ if (f.code === FeedbackCodes.moreData && position !== undefined && position !== '') positions.add(position);
112
+ }
113
+ return positions;
114
+ }
115
+
116
+ /**
117
+ * The security functions of the TAN methods the user may use, as the bank lists them with return code 3920, in all such
118
+ * return codes; empty if it lists none.
119
+ */
120
+ export function allowedTanMethodsIn(feedback: readonly FinTsFeedback[]): string[] {
121
+ const codes = new Set<string>();
122
+ for (const f of feedback) {
123
+ if (f.code !== FeedbackCodes.allowedTanMethods) continue;
124
+ for (const code of f.parameters) if (code !== '') codes.add(code);
125
+ }
126
+ return [...codes];
127
+ }
128
+
129
+ /** Whether the bank ended the dialog with one of its return codes for that. */
130
+ export function endsDialog(feedback: readonly FinTsFeedback[]): boolean {
131
+ return feedback.some((f) => dialogEnding.has(f.code));
132
+ }
133
+
134
+ /** The warnings for the user: all but those about the protocol. */
135
+ export function userWarnings(feedback: readonly FinTsFeedback[]): FinTsFeedback[] {
136
+ return feedback.filter((f) => f.isWarning && !protocolWarnings.has(f.code));
137
+ }