@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,311 @@
1
+ import type { Account } from '../core/account.js';
2
+ import type { Balance } from '../core/balance.js';
3
+ import type { BankMessage } from '../core/bank-info.js';
4
+ import { requirePinLength, type FinTsConfig } from '../core/config.js';
5
+ import { FinTsBankError, FinTsProtocolError, FinTsUnsupportedError } from '../core/errors.js';
6
+ import type { FinTsFeedback } from '../core/feedback.js';
7
+ import type { Transaction, TransactionFormat } from '../core/transaction.js';
8
+ import type { TanChallenge } from '../tan/challenge.js';
9
+ import type { TanMedium, TanMethod } from '../tan/tan-models.js';
10
+ import type { LoginGuard } from './login-guard.js';
11
+ import { knownAccount, lists, type BankParameters, type UserParameters } from './parameter-data.js';
12
+
13
+ /**
14
+ * The filler the specification names for a reference that does not exist.
15
+ *
16
+ * The bank puts it into a dummy answer when no approval is needed. The client sends it as the TAN medium name in the
17
+ * dialog that looks up the TAN media, before it knows a medium; the bank ignores it there (PIN/TAN B.4.3.1.3).
18
+ */
19
+ export const noReference = 'noref';
20
+
21
+ /**
22
+ * A business transaction by its name, such as `HKCAZ`, and the version to send; `undefined` where the bank does not
23
+ * offer it or the parameter data do not permit it.
24
+ */
25
+ export interface OfferedTransaction {
26
+ readonly name: string;
27
+ readonly version: number | undefined;
28
+ }
29
+
30
+ /**
31
+ * What to request for transactions: camt.052 (`camt`) or MT940, the `version` to send, the `camtFormats` to name, and
32
+ * the `currency` of statements that state none.
33
+ */
34
+ export interface TransactionsRequest {
35
+ readonly camt: boolean;
36
+ readonly version: number;
37
+ readonly camtFormats: readonly string[];
38
+ readonly currency: string | undefined;
39
+ }
40
+
41
+ /**
42
+ * What to request for the transactions of `account` in `format`.
43
+ *
44
+ * The client reads camt.052 only, so camt counts as offered only if `camtFormats` include it. With `auto` the client
45
+ * takes camt where the bank offers it, unless the user parameter data `upd` list only MT940 transactions for the
46
+ * account.
47
+ *
48
+ * Throws `FinTsUnsupportedError` if the bank does not offer the chosen format for the account.
49
+ */
50
+ export function chooseTransactions(
51
+ format: TransactionFormat,
52
+ account: Account,
53
+ upd: UserParameters | undefined,
54
+ offered: { camt: OfferedTransaction; camtFormats: readonly string[]; mt940: OfferedTransaction },
55
+ ): TransactionsRequest {
56
+ const readable = offered.camtFormats.filter((f) => f.includes('camt.052'));
57
+ const camtVersion = readable.length === 0 ? undefined : offered.camt.version;
58
+ const known = knownAccount(upd, account);
59
+ const useCamt =
60
+ format === 'camt' ||
61
+ (format === 'auto' &&
62
+ camtVersion !== undefined &&
63
+ (lists(known, offered.camt.name) || !lists(known, offered.mt940.name)));
64
+ const version = useCamt ? camtVersion : offered.mt940.version;
65
+ if (version === undefined) {
66
+ const name = useCamt ? offered.camt.name : offered.mt940.name;
67
+ throw new FinTsUnsupportedError(
68
+ `The bank offers no ${useCamt ? 'camt ' : ''}transactions (${name}) for this account`,
69
+ );
70
+ }
71
+ return { camt: useCamt, version, camtFormats: readable, currency: known.currency ?? account.currency };
72
+ }
73
+
74
+ /**
75
+ * The query for the balance of `account`.
76
+ *
77
+ * `order` builds the request for each page. Once all pages are in, `records` collects the balance records of all pages,
78
+ * `balanceRecord` picks the one for `account` with the help of `accountOf`, and `read` turns it into the `Balance`.
79
+ */
80
+ export function balanceQuery<R extends BankResponse, O, T>(
81
+ account: Account,
82
+ parts: {
83
+ order: (continuation: string | undefined) => O;
84
+ records: (pages: readonly R[]) => T[];
85
+ accountOf: (record: T) => Account;
86
+ read: (record: T, account: Account) => Balance;
87
+ },
88
+ ): Query<R, O, Balance> {
89
+ return {
90
+ order: parts.order,
91
+ read: (pages) =>
92
+ parts.read(
93
+ balanceRecord(
94
+ parts.records(pages),
95
+ account,
96
+ parts.accountOf,
97
+ pages.flatMap((p) => p.feedback),
98
+ ),
99
+ account,
100
+ ),
101
+ };
102
+ }
103
+
104
+ /**
105
+ * Checks that a message signed with `pin` may go out.
106
+ *
107
+ * It may not if the bank refused a login before (`LoginGuard.check`), or if the bank parameters `bpd` rule out the
108
+ * PIN's length: the bank would count such a PIN as a failed attempt.
109
+ */
110
+ export function checkBeforeSigning(guard: LoginGuard, pin: string, bpd: BankParameters | undefined): void {
111
+ guard.check();
112
+ requirePinLength(pin, bpd?.pinLength.min, bpd?.pinLength.max);
113
+ }
114
+
115
+ /**
116
+ * The balance record of `account` in `records`: the one whose account matches. A single record is taken as it is,
117
+ * since the query asked for this one account only.
118
+ *
119
+ * Throws `FinTsBankError` with the bank's `feedback` if there is none, and `FinTsProtocolError` if there are several
120
+ * and none matches.
121
+ */
122
+ export function balanceRecord<T>(
123
+ records: readonly T[],
124
+ account: Account,
125
+ accountOf: (record: T) => Account,
126
+ feedback: readonly FinTsFeedback[],
127
+ ): T {
128
+ if (records.length === 0) throw new FinTsBankError(feedback, 'No balance returned');
129
+ if (records.length === 1) return records[0]!;
130
+ const match = records.find((record) => accountOf(record).matches(account));
131
+ if (match === undefined) {
132
+ throw new FinTsProtocolError('The bank returned balances, but none for the requested account');
133
+ }
134
+ return match;
135
+ }
136
+
137
+ /** A bank response, reduced to what the protocol-independent session logic needs. */
138
+ export interface BankResponse {
139
+ /** Return codes of the whole message and of its parts. */
140
+ readonly feedback: readonly FinTsFeedback[];
141
+
142
+ /** Whether the bank rejected the message as a whole (an error in HIRMG), rather than single orders in it. */
143
+ readonly messageRejected: boolean;
144
+
145
+ /** Free-text messages the bank addresses to the user. */
146
+ readonly bankMessages: readonly BankMessage[];
147
+
148
+ /** The length of the message in bytes, a measure of the memory it holds. */
149
+ readonly size: number;
150
+ }
151
+
152
+ /** The bank's request to approve a message with a TAN or in a banking app. */
153
+ export interface Approval<R> {
154
+ /** What the user is asked. */
155
+ readonly challenge: TanChallenge;
156
+
157
+ /** Sends the TAN the user entered. */
158
+ readonly submitTan: (tan: string) => Promise<R>;
159
+
160
+ /** Asks the bank whether the user has approved in the banking app. */
161
+ readonly checkStatus: () => Promise<R>;
162
+
163
+ /**
164
+ * Whether the bank announces a push message for once the user has approved (return code 3957), before which the
165
+ * status must not be asked. The client has no push channel, so the user reports the approval instead.
166
+ */
167
+ readonly waitForPush: boolean;
168
+
169
+ /** The new instructions a status answer gives the user (return code 3956); `undefined` where it gives none. */
170
+ readonly instructionsIn: (response: R) => string | undefined;
171
+ }
172
+
173
+ /** One message exchange of a dialog, sent when called. */
174
+ export type Exchange<R> = () => Promise<R>;
175
+
176
+ /**
177
+ * The messages that open a dialog and send an order: `login` opens the dialog, and `order` sends the order in a message
178
+ * of its own.
179
+ */
180
+ export interface StartWithOrder<R> {
181
+ readonly login: Exchange<R>;
182
+ readonly order: Exchange<R> | undefined;
183
+ }
184
+
185
+ /** What the handling of responses needs of a dialog, whatever its protocol. */
186
+ export interface DialogHandle<R extends BankResponse> {
187
+ /** The TAN method the dialog's messages are signed with. */
188
+ readonly method: TanMethod;
189
+
190
+ /** Whether the bank knows this dialog and it has not ended. */
191
+ readonly isActive: boolean;
192
+
193
+ /**
194
+ * The approval `response`, the dialog's latest answer, asks for with return code 0030 (TAN) or 3955 (banking app);
195
+ * `undefined` if it asks for none.
196
+ *
197
+ * The bank has not executed the message then, so a request without a challenge is a `FinTsProtocolError`: read as
198
+ * done, it would report an empty result.
199
+ */
200
+ approvalAskedIn(response: R): Approval<R> | undefined;
201
+
202
+ /** Forgets the dialog without contacting the bank, for example after the bank rejected the PIN. */
203
+ discard(): void;
204
+
205
+ /**
206
+ * Ends the dialog at the bank; does nothing if it is not active.
207
+ *
208
+ * Bank and connection errors are ignored, since the bank may have ended the dialog on its side already.
209
+ */
210
+ end(): Promise<void>;
211
+ }
212
+
213
+ /** One dialog with the bank: a numbered sequence of messages under one dialog ID. */
214
+ export interface ProtocolDialog<R extends BankResponse, O> extends DialogHandle<R> {
215
+ /** Opens the dialog; with `synchronize` the bank also assigns a customer system ID. */
216
+ start(synchronize?: boolean): Promise<R>;
217
+
218
+ /** The messages that open the dialog and send `order` while no TAN medium is chosen yet, as for listing the media. */
219
+ startWith(order: O): StartWithOrder<R>;
220
+
221
+ /** Sends `order`, asking for strong customer authentication where the bank requires it. */
222
+ send(order: O): Promise<R>;
223
+ }
224
+
225
+ /** A business transaction: the order to send for each page, and how to read all pages the bank sent. */
226
+ export interface Query<R, O, T> {
227
+ /** The order for the page at `continuation`; `undefined` for the first page. */
228
+ readonly order: (continuation: string | undefined) => O;
229
+
230
+ /** Reads the result; throws `FormatError` for malformed data. */
231
+ readonly read: (pages: readonly R[]) => T;
232
+ }
233
+
234
+ /**
235
+ * What the regular dialogs still need from a synchronization, a login in a dialog of its own, before the TAN method and
236
+ * medium are settled (`TanSelection.synchronizationDialog` tells which method signs it):
237
+ * - `none`: nothing, so no synchronization runs;
238
+ * - `helpful`: nothing they cannot get themselves, but it tells the TAN methods the user may use before the user
239
+ * chooses one, so it runs once per client;
240
+ * - `required`: the customer system ID, without which no regular dialog starts (FinTS 3.0).
241
+ */
242
+ export type Synchronization = 'none' | 'helpful' | 'required';
243
+
244
+ /** What the handling of responses needs of a protocol, whatever the orders it builds. */
245
+ export interface ResponseProtocol<R extends BankResponse> {
246
+ /** Keeps the parameter data and IDs in `response`, the answer to a message of a logged-in dialog. */
247
+ keepParameterData(response: R): void;
248
+
249
+ /** Joins a response with the one completing its approval. */
250
+ combine(first: R, second: R): R;
251
+ }
252
+
253
+ /**
254
+ * The protocol-specific half of a PIN/TAN client: builds and reads the messages.
255
+ *
256
+ * `PinTanClient` holds the other half, the session logic.
257
+ */
258
+ export interface PinTanProtocol<R extends BankResponse, O> extends ResponseProtocol<R> {
259
+ /** Where and as whom the client talks to the bank. */
260
+ readonly config: FinTsConfig;
261
+
262
+ /** The bank parameter data received so far, if any. */
263
+ readonly bankParameters: BankParameters | undefined;
264
+
265
+ /** The user parameter data received so far, if any. */
266
+ readonly userParameters: UserParameters | undefined;
267
+
268
+ /** What regular dialogs still need from a synchronization dialog. */
269
+ readonly synchronization: Synchronization;
270
+
271
+ /**
272
+ * Drops the customer system ID the bank no longer accepts, so that a synchronization is required again; returns
273
+ * whether there was one.
274
+ */
275
+ forgetSystemId(): boolean;
276
+
277
+ /** Keeps the PIN from going out after a refused login: the protocol checks it before every message it signs. */
278
+ readonly loginGuard: LoginGuard;
279
+
280
+ /** Requests the bank parameter data without logging in, and keeps them. */
281
+ fetchBankParametersAnonymously(): Promise<R>;
282
+
283
+ /** A new dialog signed with `method` and, where the method needs one, `tanMedium`. */
284
+ newDialog(method: TanMethod, tanMedium?: string): ProtocolDialog<R, O>;
285
+
286
+ /** Lists the TAN media; `undefined` if the bank does not offer it. */
287
+ tanMedia(): Query<R, O, TanMedium[]> | undefined;
288
+
289
+ /** Lists IBAN and BIC of the accounts; `undefined` if the bank does not offer it. */
290
+ sepaAccounts(): Query<R, O, Account[]> | undefined;
291
+
292
+ /** The balance of `account`; throws `FinTsUnsupportedError` if the bank does not report balances. */
293
+ balance(account: Account): Query<R, O, Balance>;
294
+
295
+ /** The transactions of `account`; throws `FinTsUnsupportedError` if the bank does not offer `format`. */
296
+ transactions(
297
+ account: Account,
298
+ format: TransactionFormat,
299
+ from: Date | undefined,
300
+ to: Date | undefined,
301
+ ): Query<R, O, Transaction[]>;
302
+
303
+ /** The parameter data and IDs the protocol keeps, as JSON data, for the client's saved state. */
304
+ saveState(): Record<string, unknown>;
305
+
306
+ /** Restores what `saveState` saved; throws if `saved` is damaged, leaving the protocol unchanged. */
307
+ restoreState(saved: Record<string, unknown>): void;
308
+
309
+ /** Releases the connection. */
310
+ close(): void;
311
+ }
@@ -0,0 +1,130 @@
1
+ import { bankErrorFor, FinTsLoginDataChangedError, FinTsTanCancelledError } from '../core/errors.js';
2
+ import {
3
+ allowedTanMethodsIn,
4
+ FeedbackCodes,
5
+ hasCode,
6
+ hasErrors,
7
+ userWarnings,
8
+ type FinTsFeedback,
9
+ } from '../core/feedback.js';
10
+ import { invalidArgument } from '../util/errors.js';
11
+ import { appApprovalWait } from './app-approval.js';
12
+ import type { LoginGuard, RequestKind } from './login-guard.js';
13
+ import type { Approval, BankResponse, DialogHandle, Exchange, ResponseProtocol, StartWithOrder } from './protocol.js';
14
+ import type { SessionState } from './session-state.js';
15
+ import type { UserPrompts } from './user-prompts.js';
16
+
17
+ /** Checks what the bank answers and completes the approvals it asks for. The `LoginGuard` judges what errors mean. */
18
+ export class ResponseHandler<R extends BankResponse> {
19
+ constructor(
20
+ private readonly protocol: ResponseProtocol<R>,
21
+ private readonly state: SessionState,
22
+ private readonly prompts: UserPrompts,
23
+ private readonly guard: LoginGuard,
24
+ ) {}
25
+
26
+ /** Sends `login`, the exchange that opens `dialog`, and completes the approval the bank asks for. */
27
+ startDialog(dialog: DialogHandle<R>, login: Exchange<R>): Promise<R> {
28
+ return this.start(dialog, login, 'login');
29
+ }
30
+
31
+ /**
32
+ * Opens `dialog` with `start`, which also sends an order, and returns the answers: the login's, then the order's where
33
+ * the order has a message of its own. Completes the approval of each before the next.
34
+ */
35
+ async startDialogWith(dialog: DialogHandle<R>, start: StartWithOrder<R>): Promise<R[]> {
36
+ const order = start.order;
37
+ if (order === undefined) return [await this.start(dialog, start.login, 'loginWithOrder')];
38
+ const login = await this.startDialog(dialog, start.login);
39
+ return [login, await this.complete(dialog, await order())];
40
+ }
41
+
42
+ /** Receives `response`, the answer to an order in `dialog`, and completes the approval the bank asks for. */
43
+ complete(dialog: DialogHandle<R>, response: R): Promise<R> {
44
+ return this.settle(dialog, response, 'order');
45
+ }
46
+
47
+ /**
48
+ * Receives `response`, the answer to a request of `kind` in `dialog`: keeps its parameter data, even alongside errors,
49
+ * since the remedy for a refused login may need them; passes its notices on; and throws its errors once the guard has
50
+ * judged them (`LoginGuard.judge`).
51
+ */
52
+ receive(dialog: DialogHandle<R>, response: R, kind: RequestKind): void {
53
+ this.protocol.keepParameterData(response);
54
+ this.readNotices(response);
55
+ if (!hasErrors(response.feedback)) return;
56
+ const error = bankErrorFor(response.feedback, kind === 'approval');
57
+ // Every further message of a refused login's dialog would carry the refused PIN.
58
+ if (this.guard.judge(error, kind, dialog.isActive, dialog.method)) dialog.discard();
59
+ throw error;
60
+ }
61
+
62
+ /**
63
+ * Passes the bank's messages in `response` to the user: all an anonymous dialog tells, since the TAN methods it lists
64
+ * are not those of a user.
65
+ */
66
+ showBankMessages(response: R): void {
67
+ for (const message of response.bankMessages) this.prompts.showBankMessage(message);
68
+ }
69
+
70
+ private async start(dialog: DialogHandle<R>, login: Exchange<R>, kind: RequestKind): Promise<R> {
71
+ const response = await this.settle(dialog, await login(), kind);
72
+ if (hasCode(response.feedback, FeedbackCodes.newLoginData)) await this.retireLoginData(dialog, response.feedback);
73
+ return response;
74
+ }
75
+
76
+ /**
77
+ * Ends `dialog`, whose login brought new login data, and stops the client from logging in with the old ones
78
+ * (Rückmeldungscodes C.6).
79
+ */
80
+ private async retireLoginData(dialog: DialogHandle<R>, feedback: readonly FinTsFeedback[]): Promise<never> {
81
+ await dialog.end();
82
+ const error = new FinTsLoginDataChangedError(feedback);
83
+ this.guard.refuse(error);
84
+ throw error;
85
+ }
86
+
87
+ /**
88
+ * Receives `response`, the answer to a request of `kind`, then completes the approval it asks for; returns it joined
89
+ * with the response that completes the approval.
90
+ */
91
+ private async settle(dialog: DialogHandle<R>, response: R, kind: RequestKind): Promise<R> {
92
+ this.receive(dialog, response, kind);
93
+ const approval = dialog.approvalAskedIn(response);
94
+ if (approval === undefined) return response;
95
+ const approved = approval.challenge.isDecoupled
96
+ ? await this.awaitAppApproval(dialog, approval)
97
+ : await this.enterTan(dialog, approval);
98
+ return this.protocol.combine(response, approved);
99
+ }
100
+
101
+ /** Passes on the notices in `response`: the TAN methods the user may use (3920), the bank's messages and warnings. */
102
+ private readNotices(response: R): void {
103
+ const allowed = allowedTanMethodsIn(response.feedback);
104
+ if (allowed.length > 0) this.state.rememberAllowedMethods(allowed);
105
+ this.showBankMessages(response);
106
+ for (const warning of userWarnings(response.feedback)) this.prompts.showWarning(warning);
107
+ }
108
+
109
+ private async enterTan(dialog: DialogHandle<R>, approval: Approval<R>): Promise<R> {
110
+ const answer = await this.prompts.askTan(approval.challenge);
111
+ if (answer.kind === 'approved') {
112
+ throw invalidArgument('Answered a TAN challenge with an approval in the banking app', 'tanCallback');
113
+ }
114
+ const tan = answer.kind === 'tan' ? answer.tan.trim() : '';
115
+ if (tan === '') throw new FinTsTanCancelledError();
116
+ const response = await approval.submitTan(tan);
117
+ this.receive(dialog, response, 'approval');
118
+ return response;
119
+ }
120
+
121
+ private awaitAppApproval(dialog: DialogHandle<R>, approval: Approval<R>): Promise<R> {
122
+ return appApprovalWait(approval, {
123
+ ask: (challenge) => this.prompts.askTan(challenge),
124
+ pause: (delayMs, interrupt) => this.prompts.pause(delayMs, interrupt),
125
+ receive: (response) => {
126
+ this.receive(dialog, response, 'approval');
127
+ },
128
+ }).run();
129
+ }
130
+ }
@@ -0,0 +1,97 @@
1
+ import type { Account } from '../core/account.js';
2
+ import { FinTsDialogAbortedError } from '../core/errors.js';
3
+ import type { DialogRunner } from './dialog-runner.js';
4
+ import type { LoginGuard } from './login-guard.js';
5
+ import type { BankResponse, PinTanProtocol } from './protocol.js';
6
+ import type { SessionState } from './session-state.js';
7
+
8
+ /**
9
+ * IBAN and BIC of the user's accounts, from the bank's SEPA account list where the user parameter data lack them
10
+ * (Formals F.3.4).
11
+ *
12
+ * The list is read once per version of the user parameter data and kept in the saved state. Under version 0, which
13
+ * holds for one dialog only (Formals E.2), without user parameter data, or where the bank rejects the list, it is kept
14
+ * for this client only. Where the bank ends the dialog in answer to the list, the next call asks once more.
15
+ */
16
+ export class SepaAccounts<R extends BankResponse, O> {
17
+ /**
18
+ * The list this client keeps unsaved, with the version of the user parameter data it was read under; empty where the
19
+ * bank rejected it.
20
+ */
21
+ private unsaved: { updVersion: number | undefined; accounts: readonly Account[] } | undefined;
22
+
23
+ /** Whether a call already went on without the list because the bank ended the dialog in answer to it. */
24
+ private abortSkipped = false;
25
+
26
+ constructor(
27
+ private readonly protocol: PinTanProtocol<R, O>,
28
+ private readonly state: SessionState,
29
+ private readonly dialogs: DialogRunner<R, O>,
30
+ private readonly guard: LoginGuard,
31
+ ) {}
32
+
33
+ /** The accounts of the user parameter data completed with IBAN and BIC, then the accounts only the list names. */
34
+ async completedAccounts(): Promise<Account[]> {
35
+ const sepa = await this.list();
36
+ const accounts = this.protocol.userParameters?.accounts ?? [];
37
+ return [
38
+ ...accounts.map((account) => withSepaDetails(account, sepa)),
39
+ ...sepa.filter((s) => !accounts.some((a) => s.matches(a))),
40
+ ];
41
+ }
42
+
43
+ /** `account` with IBAN and BIC filled in from the list. */
44
+ async complete(account: Account): Promise<Account> {
45
+ if (account.iban !== undefined && account.bic !== undefined) return account;
46
+ return withSepaDetails(account, await this.list());
47
+ }
48
+
49
+ /** The list, read in the session's dialog, whose start delivers the user parameter data it is cached for. */
50
+ private async list(): Promise<readonly Account[]> {
51
+ await this.dialogs.open();
52
+ const upd = this.protocol.userParameters;
53
+ if (
54
+ upd !== undefined &&
55
+ upd.accounts.length > 0 &&
56
+ upd.accounts.every((a) => a.iban !== undefined && a.bic !== undefined)
57
+ ) {
58
+ return [];
59
+ }
60
+ const updVersion = upd?.version;
61
+ const saved = this.state.sepaAccounts(updVersion);
62
+ if (saved !== undefined) return saved;
63
+ if (this.unsaved !== undefined && this.unsaved.updVersion === updVersion) return this.unsaved.accounts;
64
+ const query = this.protocol.sepaAccounts();
65
+ if (query === undefined) return [];
66
+ // Where the bank rejects the list, the accounts stay as the user parameter data state them.
67
+ const outcome = { aborted: false };
68
+ const accounts = await this.guard.tryLookUp(async () => {
69
+ try {
70
+ return await this.dialogs.query(() => query);
71
+ } catch (error) {
72
+ if (error instanceof FinTsDialogAbortedError) outcome.aborted = true;
73
+ throw error;
74
+ }
75
+ });
76
+ if (outcome.aborted && !this.abortSkipped) {
77
+ // The bank may have ended the dialog for a reason of its own, such as a timeout, so this call goes on without the
78
+ // list and the next one asks again in its new dialog. Only once: banks also end the dialog when they reject the
79
+ // list, and each time costs the next call a login.
80
+ this.abortSkipped = true;
81
+ return [];
82
+ }
83
+ if (accounts !== undefined && updVersion !== undefined && updVersion > 0) {
84
+ this.state.cacheSepaAccounts(accounts, updVersion);
85
+ } else {
86
+ this.unsaved = { updVersion, accounts: accounts ?? [] };
87
+ }
88
+ return accounts ?? [];
89
+ }
90
+ }
91
+
92
+ function withSepaDetails(account: Account, sepa: readonly Account[]): Account {
93
+ const match = sepa.find((s) => account.matches(s));
94
+ if (match === undefined) return account;
95
+ // A BIC of the list takes precedence over the one of the user parameter data (Formals E.3.1.1, "BIC").
96
+ return account.copyWith({ iban: account.iban ?? match.iban, bic: match.bic ?? account.bic });
97
+ }