@coderoftime/fints 0.1.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (289) hide show
  1. package/CHANGELOG.md +519 -1
  2. package/README.md +138 -40
  3. package/dist/client.d.ts +49 -0
  4. package/dist/client.d.ts.map +1 -0
  5. package/dist/client.js +242 -0
  6. package/dist/client.js.map +1 -0
  7. package/dist/core/account.d.ts +46 -7
  8. package/dist/core/account.d.ts.map +1 -1
  9. package/dist/core/account.js +143 -24
  10. package/dist/core/account.js.map +1 -1
  11. package/dist/core/amount.d.ts +15 -4
  12. package/dist/core/amount.d.ts.map +1 -1
  13. package/dist/core/amount.js +46 -9
  14. package/dist/core/amount.js.map +1 -1
  15. package/dist/core/balance.d.ts +4 -0
  16. package/dist/core/balance.d.ts.map +1 -1
  17. package/dist/core/balance.js +11 -5
  18. package/dist/core/balance.js.map +1 -1
  19. package/dist/core/bank-info.d.ts +21 -4
  20. package/dist/core/bank-info.d.ts.map +1 -1
  21. package/dist/core/bank-info.js +26 -5
  22. package/dist/core/bank-info.js.map +1 -1
  23. package/dist/core/client.d.ts +81 -19
  24. package/dist/core/client.d.ts.map +1 -1
  25. package/dist/core/config.d.ts +40 -20
  26. package/dist/core/config.d.ts.map +1 -1
  27. package/dist/core/config.js +85 -30
  28. package/dist/core/config.js.map +1 -1
  29. package/dist/core/errors.d.ts +100 -26
  30. package/dist/core/errors.d.ts.map +1 -1
  31. package/dist/core/errors.js +136 -53
  32. package/dist/core/errors.js.map +1 -1
  33. package/dist/core/feedback.d.ts +11 -1
  34. package/dist/core/feedback.d.ts.map +1 -1
  35. package/dist/core/feedback.js +14 -2
  36. package/dist/core/feedback.js.map +1 -1
  37. package/dist/core/transaction.d.ts +11 -1
  38. package/dist/core/transaction.d.ts.map +1 -1
  39. package/dist/core/transaction.js +21 -4
  40. package/dist/core/transaction.js.map +1 -1
  41. package/dist/core/transport.d.ts +48 -6
  42. package/dist/core/transport.d.ts.map +1 -1
  43. package/dist/core/transport.js +126 -29
  44. package/dist/core/transport.js.map +1 -1
  45. package/dist/formats/camt.d.ts +28 -5
  46. package/dist/formats/camt.d.ts.map +1 -1
  47. package/dist/formats/camt.js +132 -50
  48. package/dist/formats/camt.js.map +1 -1
  49. package/dist/formats/mt940.d.ts +29 -5
  50. package/dist/formats/mt940.d.ts.map +1 -1
  51. package/dist/formats/mt940.js +209 -113
  52. package/dist/formats/mt940.js.map +1 -1
  53. package/dist/formats/readers.d.ts +6 -2
  54. package/dist/formats/readers.d.ts.map +1 -1
  55. package/dist/formats/readers.js +28 -29
  56. package/dist/formats/readers.js.map +1 -1
  57. package/dist/formats/record-budget.d.ts +7 -0
  58. package/dist/formats/record-budget.d.ts.map +1 -0
  59. package/dist/formats/record-budget.js +17 -0
  60. package/dist/formats/record-budget.js.map +1 -0
  61. package/dist/formats/sepa-purpose.d.ts +9 -0
  62. package/dist/formats/sepa-purpose.d.ts.map +1 -1
  63. package/dist/formats/sepa-purpose.js +32 -2
  64. package/dist/formats/sepa-purpose.js.map +1 -1
  65. package/dist/index.d.ts +3 -3
  66. package/dist/index.d.ts.map +1 -1
  67. package/dist/index.js +2 -2
  68. package/dist/index.js.map +1 -1
  69. package/dist/session/app-approval.d.ts +11 -5
  70. package/dist/session/app-approval.d.ts.map +1 -1
  71. package/dist/session/app-approval.js +23 -12
  72. package/dist/session/app-approval.js.map +1 -1
  73. package/dist/session/dialog-runner.d.ts +11 -11
  74. package/dist/session/dialog-runner.d.ts.map +1 -1
  75. package/dist/session/dialog-runner.js +34 -19
  76. package/dist/session/dialog-runner.js.map +1 -1
  77. package/dist/session/login-guard.d.ts +54 -49
  78. package/dist/session/login-guard.d.ts.map +1 -1
  79. package/dist/session/login-guard.js +170 -83
  80. package/dist/session/login-guard.js.map +1 -1
  81. package/dist/session/sepa-accounts.d.ts +20 -14
  82. package/dist/session/sepa-accounts.d.ts.map +1 -1
  83. package/dist/session/sepa-accounts.js +62 -46
  84. package/dist/session/sepa-accounts.js.map +1 -1
  85. package/dist/session/session-setup.d.ts +41 -24
  86. package/dist/session/session-setup.d.ts.map +1 -1
  87. package/dist/session/session-setup.js +106 -63
  88. package/dist/session/session-setup.js.map +1 -1
  89. package/dist/session/session-state.d.ts +10 -6
  90. package/dist/session/session-state.d.ts.map +1 -1
  91. package/dist/session/session-state.js +72 -18
  92. package/dist/session/session-state.js.map +1 -1
  93. package/dist/session/signed-dialog.d.ts +83 -0
  94. package/dist/session/signed-dialog.d.ts.map +1 -0
  95. package/dist/session/signed-dialog.js +185 -0
  96. package/dist/session/signed-dialog.js.map +1 -0
  97. package/dist/session/tan-selection.d.ts +26 -18
  98. package/dist/session/tan-selection.d.ts.map +1 -1
  99. package/dist/session/tan-selection.js +44 -33
  100. package/dist/session/tan-selection.js.map +1 -1
  101. package/dist/session/user-prompts.d.ts +14 -5
  102. package/dist/session/user-prompts.d.ts.map +1 -1
  103. package/dist/session/user-prompts.js +53 -18
  104. package/dist/session/user-prompts.js.map +1 -1
  105. package/dist/tan/challenge.d.ts +47 -24
  106. package/dist/tan/challenge.d.ts.map +1 -1
  107. package/dist/tan/challenge.js +56 -51
  108. package/dist/tan/challenge.js.map +1 -1
  109. package/dist/tan/flicker.d.ts +11 -1
  110. package/dist/tan/flicker.d.ts.map +1 -1
  111. package/dist/tan/flicker.js +70 -24
  112. package/dist/tan/flicker.js.map +1 -1
  113. package/dist/tan/tan-image.d.ts +6 -2
  114. package/dist/tan/tan-image.d.ts.map +1 -1
  115. package/dist/tan/tan-image.js +14 -6
  116. package/dist/tan/tan-image.js.map +1 -1
  117. package/dist/tan/tan-models.d.ts +14 -2
  118. package/dist/tan/tan-models.d.ts.map +1 -1
  119. package/dist/tan/tan-models.js +24 -7
  120. package/dist/tan/tan-models.js.map +1 -1
  121. package/dist/util/arguments.d.ts +13 -0
  122. package/dist/util/arguments.d.ts.map +1 -0
  123. package/dist/util/arguments.js +40 -0
  124. package/dist/util/arguments.js.map +1 -0
  125. package/dist/util/async.d.ts +4 -3
  126. package/dist/util/async.d.ts.map +1 -1
  127. package/dist/util/async.js +6 -7
  128. package/dist/util/async.js.map +1 -1
  129. package/dist/util/bytes.d.ts +3 -4
  130. package/dist/util/bytes.d.ts.map +1 -1
  131. package/dist/util/bytes.js +13 -21
  132. package/dist/util/bytes.js.map +1 -1
  133. package/dist/util/call-context.d.ts.map +1 -1
  134. package/dist/util/call-context.js +3 -2
  135. package/dist/util/call-context.js.map +1 -1
  136. package/dist/util/dates.d.ts +6 -20
  137. package/dist/util/dates.d.ts.map +1 -1
  138. package/dist/util/dates.js +58 -28
  139. package/dist/util/dates.js.map +1 -1
  140. package/dist/util/errors.d.ts +13 -3
  141. package/dist/util/errors.d.ts.map +1 -1
  142. package/dist/util/errors.js +19 -6
  143. package/dist/util/errors.js.map +1 -1
  144. package/dist/util/immutable-map.d.ts +5 -2
  145. package/dist/util/immutable-map.d.ts.map +1 -1
  146. package/dist/util/immutable-map.js +15 -12
  147. package/dist/util/immutable-map.js.map +1 -1
  148. package/dist/util/inspect.d.ts +11 -0
  149. package/dist/util/inspect.d.ts.map +1 -0
  150. package/dist/util/inspect.js +14 -0
  151. package/dist/util/inspect.js.map +1 -0
  152. package/dist/util/json.d.ts +4 -0
  153. package/dist/util/json.d.ts.map +1 -1
  154. package/dist/util/json.js +18 -0
  155. package/dist/util/json.js.map +1 -1
  156. package/dist/util/text.d.ts +38 -5
  157. package/dist/util/text.d.ts.map +1 -1
  158. package/dist/util/text.js +90 -16
  159. package/dist/util/text.js.map +1 -1
  160. package/dist/util/xml.d.ts +9 -6
  161. package/dist/util/xml.d.ts.map +1 -1
  162. package/dist/util/xml.js +129 -56
  163. package/dist/util/xml.js.map +1 -1
  164. package/dist/v3/account-extension.d.ts.map +1 -1
  165. package/dist/v3/account-extension.js +7 -10
  166. package/dist/v3/account-extension.js.map +1 -1
  167. package/dist/v3/bank-connection.d.ts +54 -0
  168. package/dist/v3/bank-connection.d.ts.map +1 -0
  169. package/dist/v3/bank-connection.js +118 -0
  170. package/dist/v3/bank-connection.js.map +1 -0
  171. package/dist/v3/dialog.d.ts +84 -46
  172. package/dist/v3/dialog.d.ts.map +1 -1
  173. package/dist/v3/dialog.js +174 -60
  174. package/dist/v3/dialog.js.map +1 -1
  175. package/dist/v3/message.d.ts +32 -11
  176. package/dist/v3/message.d.ts.map +1 -1
  177. package/dist/v3/message.js +144 -83
  178. package/dist/v3/message.js.map +1 -1
  179. package/dist/v3/parameter-store.d.ts +111 -0
  180. package/dist/v3/parameter-store.d.ts.map +1 -0
  181. package/dist/v3/parameter-store.js +197 -0
  182. package/dist/v3/parameter-store.js.map +1 -0
  183. package/dist/v3/parameters.d.ts +44 -17
  184. package/dist/v3/parameters.d.ts.map +1 -1
  185. package/dist/v3/parameters.js +136 -60
  186. package/dist/v3/parameters.js.map +1 -1
  187. package/dist/v3/protocol.d.ts +50 -50
  188. package/dist/v3/protocol.d.ts.map +1 -1
  189. package/dist/v3/protocol.js +91 -210
  190. package/dist/v3/protocol.js.map +1 -1
  191. package/dist/v3/queries.d.ts +51 -0
  192. package/dist/v3/queries.d.ts.map +1 -0
  193. package/dist/v3/queries.js +128 -0
  194. package/dist/v3/queries.js.map +1 -0
  195. package/dist/v3/segments.d.ts +43 -19
  196. package/dist/v3/segments.d.ts.map +1 -1
  197. package/dist/v3/segments.js +84 -33
  198. package/dist/v3/segments.js.map +1 -1
  199. package/dist/v3/syntax.d.ts +68 -13
  200. package/dist/v3/syntax.d.ts.map +1 -1
  201. package/dist/v3/syntax.js +183 -102
  202. package/dist/v3/syntax.js.map +1 -1
  203. package/package.json +10 -6
  204. package/src/client.ts +293 -0
  205. package/src/core/account.ts +153 -24
  206. package/src/core/amount.ts +45 -10
  207. package/src/core/balance.ts +12 -5
  208. package/src/core/bank-info.ts +29 -5
  209. package/src/core/client.ts +83 -19
  210. package/src/core/config.ts +95 -35
  211. package/src/core/errors.ts +148 -54
  212. package/src/core/feedback.ts +16 -2
  213. package/src/core/transaction.ts +22 -4
  214. package/src/core/transport.ts +153 -32
  215. package/src/formats/camt.ts +153 -45
  216. package/src/formats/mt940.ts +244 -122
  217. package/src/formats/readers.ts +28 -31
  218. package/src/formats/record-budget.ts +18 -0
  219. package/src/formats/sepa-purpose.ts +29 -2
  220. package/src/index.ts +4 -2
  221. package/src/session/app-approval.ts +51 -38
  222. package/src/session/dialog-runner.ts +61 -31
  223. package/src/session/login-guard.ts +189 -87
  224. package/src/session/sepa-accounts.ts +63 -45
  225. package/src/session/session-setup.ts +105 -68
  226. package/src/session/session-state.ts +74 -23
  227. package/src/session/signed-dialog.ts +222 -0
  228. package/src/session/tan-selection.ts +50 -39
  229. package/src/session/user-prompts.ts +62 -18
  230. package/src/tan/challenge.ts +87 -64
  231. package/src/tan/flicker.ts +72 -23
  232. package/src/tan/tan-image.ts +15 -6
  233. package/src/tan/tan-models.ts +26 -7
  234. package/src/util/arguments.ts +40 -0
  235. package/src/util/async.ts +10 -9
  236. package/src/util/bytes.ts +14 -20
  237. package/src/util/call-context.ts +3 -2
  238. package/src/util/dates.ts +64 -33
  239. package/src/util/errors.ts +22 -6
  240. package/src/util/immutable-map.ts +15 -12
  241. package/src/util/inspect.ts +14 -0
  242. package/src/util/json.ts +16 -0
  243. package/src/util/text.ts +87 -15
  244. package/src/util/xml.ts +130 -52
  245. package/src/v3/account-extension.ts +7 -12
  246. package/src/v3/bank-connection.ts +126 -0
  247. package/src/v3/dialog.ts +186 -92
  248. package/src/v3/message.ts +140 -79
  249. package/src/v3/parameter-store.ts +226 -0
  250. package/src/v3/parameters.ts +166 -66
  251. package/src/v3/protocol.ts +103 -253
  252. package/src/v3/queries.ts +198 -0
  253. package/src/v3/segments.ts +86 -31
  254. package/src/v3/syntax.ts +172 -93
  255. package/dist/session/bank-connection.d.ts +0 -28
  256. package/dist/session/bank-connection.d.ts.map +0 -1
  257. package/dist/session/bank-connection.js +0 -48
  258. package/dist/session/bank-connection.js.map +0 -1
  259. package/dist/session/parameter-data.d.ts +0 -83
  260. package/dist/session/parameter-data.d.ts.map +0 -1
  261. package/dist/session/parameter-data.js +0 -68
  262. package/dist/session/parameter-data.js.map +0 -1
  263. package/dist/session/pin-tan-client.d.ts +0 -54
  264. package/dist/session/pin-tan-client.d.ts.map +0 -1
  265. package/dist/session/pin-tan-client.js +0 -153
  266. package/dist/session/pin-tan-client.js.map +0 -1
  267. package/dist/session/pin-tan-dialog.d.ts +0 -71
  268. package/dist/session/pin-tan-dialog.d.ts.map +0 -1
  269. package/dist/session/pin-tan-dialog.js +0 -124
  270. package/dist/session/pin-tan-dialog.js.map +0 -1
  271. package/dist/session/protocol.d.ts +0 -209
  272. package/dist/session/protocol.d.ts.map +0 -1
  273. package/dist/session/protocol.js +0 -75
  274. package/dist/session/protocol.js.map +0 -1
  275. package/dist/session/response-handler.d.ts +0 -48
  276. package/dist/session/response-handler.d.ts.map +0 -1
  277. package/dist/session/response-handler.js +0 -121
  278. package/dist/session/response-handler.js.map +0 -1
  279. package/dist/v3/client.d.ts +0 -24
  280. package/dist/v3/client.d.ts.map +0 -1
  281. package/dist/v3/client.js +0 -16
  282. package/dist/v3/client.js.map +0 -1
  283. package/src/session/bank-connection.ts +0 -51
  284. package/src/session/parameter-data.ts +0 -137
  285. package/src/session/pin-tan-client.ts +0 -186
  286. package/src/session/pin-tan-dialog.ts +0 -160
  287. package/src/session/protocol.ts +0 -311
  288. package/src/session/response-handler.ts +0 -130
  289. package/src/v3/client.ts +0 -29
@@ -0,0 +1,126 @@
1
+ import type { MessageDirection, MessageLogger } from '../core/client.js';
2
+ import { FinTsProtocolError, FinTsTransportError } from '../core/errors.js';
3
+ import { failureMessage, HttpTransport, type FinTsTransport } from '../core/transport.js';
4
+
5
+ /**
6
+ * Throws `FinTsProtocolError` if a message of `size` bytes exceeds the bank's `limit` in kilobytes (1024 bytes): the
7
+ * specification does not allow to send such a message.
8
+ */
9
+ export function checkMessageSize(size: number, limit: number | undefined): void {
10
+ if (limit !== undefined && limit > 0 && size > limit * 1024) {
11
+ throw new FinTsProtocolError(`A message of ${size} bytes exceeds the bank's maximum message size of ${limit} KB`);
12
+ }
13
+ }
14
+
15
+ /** The bank's FinTS endpoint: posts messages there and reports them to the logger. */
16
+ export class BankConnection {
17
+ private readonly transport: FinTsTransport;
18
+
19
+ /** Where the messages go, as `FinTsConfig` checked it. */
20
+ private readonly href: string;
21
+
22
+ /** Aborts the requests posted since the last `abortRequestsAfter`. */
23
+ private requests = new AbortController();
24
+
25
+ /** How many requests are on their way. */
26
+ private onTheirWay = 0;
27
+
28
+ /** Starts the time of the requests posted after `abortRequestsAfter`, once none posted before is on its way. */
29
+ private startLaterRequests: (() => void) | undefined;
30
+
31
+ /** The timers of `abortRequestsAfter`, which `close` clears. */
32
+ private readonly deadlines: ReturnType<typeof setTimeout>[] = [];
33
+
34
+ /** A connection to `url` through `transport`, an `HttpTransport` by default. */
35
+ constructor(
36
+ url: URL,
37
+ transport: FinTsTransport | undefined,
38
+ private readonly logger: MessageLogger | undefined,
39
+ ) {
40
+ this.href = url.href;
41
+ this.transport = transport ?? new HttpTransport();
42
+ }
43
+
44
+ /**
45
+ * Posts `body` to a new copy of the URL, so that a transport that changes the one it is given changes no later
46
+ * request.
47
+ *
48
+ * Reports any failure of the transport as `FinTsTransportError`: a custom transport that calls `fetch` fails with a
49
+ * `TypeError` where the network does, and one that throws another `FinTsError`, such as `FinTsPinError`, would pass
50
+ * for an answer of the bank. Of such an error only the message is kept (`failureMessage`), since it may hold `body`,
51
+ * as the errors of HTTP clients such as axios do.
52
+ *
53
+ * Once the time `abortRequestsAfter` gives is up, it fails at once, before anything reaches the transport
54
+ * (`checkSendable`), and a request on its way fails then without waiting for the transport, which may ignore the
55
+ * abort.
56
+ */
57
+ async post(body: Uint8Array, contentType: string): Promise<Uint8Array> {
58
+ this.checkSendable();
59
+ const signal = this.requests.signal;
60
+ let stopWaiting!: () => void;
61
+ const aborted = new Promise<never>((_, reject) => {
62
+ stopWaiting = () => {
63
+ reject(new FinTsTransportError('Request aborted'));
64
+ };
65
+ });
66
+ signal.addEventListener('abort', stopWaiting);
67
+ this.onTheirWay++;
68
+ try {
69
+ return await Promise.race([this.transport.post(new URL(this.href), body, contentType, signal), aborted]);
70
+ } catch (error) {
71
+ if (error instanceof FinTsTransportError) throw error;
72
+ throw new FinTsTransportError(failureMessage(error));
73
+ } finally {
74
+ signal.removeEventListener('abort', stopWaiting);
75
+ this.onTheirWay--;
76
+ if (this.onTheirWay === 0) this.startLaterRequests?.();
77
+ }
78
+ }
79
+
80
+ /**
81
+ * Throws the `FinTsTransportError` that `post` fails with once the time `abortRequestsAfter` gives is up; a caller
82
+ * checks it before it reports a message as outgoing, since such a message never goes out.
83
+ */
84
+ checkSendable(): void {
85
+ if (this.requests.signal.aborted) throw new FinTsTransportError('Request aborted');
86
+ }
87
+
88
+ /** Reports a message to the logger; `message` is only built when a logger is set. */
89
+ log(direction: MessageDirection, message: () => string): void {
90
+ this.logger?.(direction, message());
91
+ }
92
+
93
+ /**
94
+ * Gives the requests on their way `graceMs` to finish, and aborts them then. Those posted later, such as the dialog
95
+ * ends of a closing client, have as long again from the moment the earlier ones are over or aborted, and are aborted
96
+ * in turn once that time is up, as is any posted after it: a bank that has stopped answering would hold them up to
97
+ * the transport's timeout.
98
+ */
99
+ abortRequestsAfter(graceMs: number): void {
100
+ const earlier = this.requests;
101
+ const later = new AbortController();
102
+ this.requests = later;
103
+ const afterGrace = (action: () => void): void => {
104
+ this.deadlines.push(setTimeout(action, graceMs));
105
+ };
106
+ this.startLaterRequests = () => {
107
+ this.startLaterRequests = undefined;
108
+ afterGrace(() => {
109
+ later.abort();
110
+ });
111
+ };
112
+ if (this.onTheirWay === 0) {
113
+ this.startLaterRequests();
114
+ } else {
115
+ afterGrace(() => {
116
+ earlier.abort();
117
+ this.startLaterRequests?.();
118
+ });
119
+ }
120
+ }
121
+
122
+ close(): void {
123
+ for (const deadline of this.deadlines) clearTimeout(deadline);
124
+ this.transport.close();
125
+ }
126
+ }
package/src/v3/dialog.ts CHANGED
@@ -1,14 +1,13 @@
1
1
  // Internal to the package, and named after the FinTS specification. Documentation is where the meaning is not evident.
2
2
 
3
- import { FinTsProtocolError } from '../core/errors.js';
4
- import { FeedbackCodes, hasCode } from '../core/feedback.js';
5
- import { requireBankParameters } from '../session/parameter-data.js';
6
- import { PinTanDialog } from '../session/pin-tan-dialog.js';
7
- import type { Approval, StartWithOrder } from '../session/protocol.js';
3
+ import { FinTsError, FinTsProtocolError, FinTsTransportError, parseBankData } from '../core/errors.js';
4
+ import { endsDialog, FeedbackCodes, hasCode, hasErrors } from '../core/feedback.js';
5
+ import { challengeText, TanChallenge } from '../tan/challenge.js';
8
6
  import { TanMethod } from '../tan/tan-models.js';
9
7
  import { IncomingMessage3 } from './message.js';
10
- import type { Bpd3 } from './parameters.js';
11
- import { Segments, TanResponse } from './segments.js';
8
+ import { requireBankParameters } from './parameters.js';
9
+ import type { FinTs3Protocol } from './protocol.js';
10
+ import { noReference, Segments, TanResponse } from './segments.js';
12
11
  import type { SegmentSpec } from './syntax.js';
13
12
 
14
13
  /** Dialog ID of a dialog the bank has not answered yet, and customer system ID before synchronization. */
@@ -20,96 +19,121 @@ const firstScaVersion = 6;
20
19
  /** The first version of HKTAN that supports app approvals, with the status request (process S). */
21
20
  const firstDecoupledVersion = 7;
22
21
 
23
- /** How a message is signed: the security function of the TAN method and the customer system ID. */
24
- export interface Signing3 {
25
- readonly securityFunction: string;
26
- readonly systemId: string;
27
- }
28
-
29
22
  /**
30
- * What a dialog needs from the session: the parameter data received so far, the segments that open a dialog, and the
31
- * way to the bank. The `FinTs3Protocol` provides it, and keeps the login data and the PIN to itself.
23
+ * How long a dialog may stay idle where the bank states no timeout: on the safe side, since a dialog given up too early
24
+ * only costs a new login, while one kept too long fails the next call.
32
25
  */
33
- export interface FinTs3DialogHost {
34
- /** The bank parameter data received so far, if any. */
35
- readonly bankParameters: Bpd3 | undefined;
26
+ const defaultIdleLimitMs = 5 * 60_000;
27
+
28
+ /** The bank's request to approve a message with a TAN or in a banking app; `R` is the bank's answer. */
29
+ export interface Approval<R> {
30
+ /** What the user is asked. */
31
+ readonly challenge: TanChallenge;
36
32
 
37
- /** The customer system ID the bank assigned; `undefined` before the synchronization. */
38
- readonly systemId: string | undefined;
33
+ /** Sends the TAN the user entered. */
34
+ readonly submitTan: (tan: string) => Promise<R>;
39
35
 
40
- /** Identification (HKIDN) and processing preparation (HKVVB), which open every dialog. */
41
- initSegments(systemId: string, anonymous?: boolean): SegmentSpec[];
36
+ /** Asks the bank whether the user has approved in the banking app. */
37
+ readonly checkStatus: () => Promise<R>;
42
38
 
43
39
  /**
44
- * Sends `segments` as message `number` of the dialog `dialogId` and returns the bank's answer, not yet parsed.
45
- *
46
- * With `signing` the message is signed with the PIN and the `tan`, after `LoginGuard.check`; without, it goes out
47
- * unsigned, as in an anonymous dialog.
40
+ * Whether the bank announces a push message for once the user has approved (return code 3957), before which the
41
+ * status must not be asked. The client has no push channel, so the user reports the approval instead.
48
42
  */
49
- post(message: {
50
- dialogId: string;
51
- number: number;
52
- segments: readonly SegmentSpec[];
53
- signing: Signing3 | undefined;
54
- tan: string | undefined;
55
- }): Promise<Uint8Array>;
43
+ readonly waitForPush: boolean;
44
+
45
+ /** The new instructions a status answer gives the user (return code 3956); `undefined` where it gives none. */
46
+ readonly instructionsIn: (response: R) => string | undefined;
56
47
  }
57
48
 
58
- /** One FinTS 3.0 dialog: numbered messages under the dialog ID the bank assigns in its first answer. */
59
- export class FinTs3Dialog extends PinTanDialog<IncomingMessage3, SegmentSpec> {
49
+ /**
50
+ * One FinTS 3.0 dialog: numbered messages under the dialog ID the bank assigns in its first answer, signed with a TAN
51
+ * method and medium, and the approvals the bank asks for. The `FinTs3Protocol` builds the messages and signs them.
52
+ */
53
+ export class FinTs3Dialog {
60
54
  private systemId: string;
61
55
  private id = unassigned;
56
+ private messageNumber = 1;
57
+ private ended = false;
58
+
59
+ /** When the bank last answered without rejecting the whole message, which shows it still knows the dialog. */
60
+ private lastAnswer: number | undefined;
62
61
 
63
62
  private constructor(
64
- private readonly host: FinTs3DialogHost,
65
- method: TanMethod,
66
- tanMedium: string | undefined,
63
+ private readonly protocol: FinTs3Protocol,
64
+ readonly method: TanMethod,
65
+ /** The TAN medium the messages name, where the method takes one. */
66
+ readonly tanMedium: string | undefined,
67
67
  private readonly anonymous: boolean,
68
68
  ) {
69
- super(method, tanMedium);
70
- this.systemId = anonymous ? unassigned : (host.systemId ?? unassigned);
69
+ this.systemId = anonymous ? unassigned : (protocol.parameters.systemId ?? unassigned);
71
70
  }
72
71
 
73
- static signed(host: FinTs3DialogHost, method: TanMethod, tanMedium?: string): FinTs3Dialog {
74
- return new FinTs3Dialog(host, method, tanMedium, false);
72
+ static signed(protocol: FinTs3Protocol, method: TanMethod, tanMedium?: string): FinTs3Dialog {
73
+ return new FinTs3Dialog(protocol, method, tanMedium, false);
75
74
  }
76
75
 
77
76
  /** A dialog without login, which only delivers the bank parameter data. */
78
- static anonymous(host: FinTs3DialogHost): FinTs3Dialog {
79
- return new FinTs3Dialog(host, TanMethod.oneStep, undefined, true);
80
- }
81
-
82
- protected get bankParameters(): Bpd3 | undefined {
83
- return this.host.bankParameters;
77
+ static anonymous(protocol: FinTs3Protocol): FinTs3Dialog {
78
+ return new FinTs3Dialog(protocol, TanMethod.oneStep, undefined, true);
84
79
  }
85
80
 
81
+ /** Whether the bank knows this dialog and it has not ended. */
86
82
  get isActive(): boolean {
87
83
  return this.id !== unassigned && !this.hasEnded;
88
84
  }
89
85
 
86
+ /**
87
+ * Whether the dialog has ended, on either side.
88
+ *
89
+ * The bank ends a dialog idle for longer than its maximum timeout (HIBPA, Formals C.9), or five minutes where it
90
+ * states none. Such a dialog is neither used nor ended again: the bank would refuse any message in it.
91
+ */
92
+ private get hasEnded(): boolean {
93
+ return this.ended || this.idleTooLong;
94
+ }
95
+
96
+ private get idleTooLong(): boolean {
97
+ const last = this.lastAnswer;
98
+ const timeout = this.protocol.parameters.bankParameters?.bankInfo.maxTimeout;
99
+ // A timeout of 0 or below: the bank keeps idle dialogs without limit.
100
+ const limit = timeout === undefined ? defaultIdleLimitMs : timeout <= 0 ? undefined : timeout * 1000;
101
+ return last !== undefined && limit !== undefined && Date.now() - last > limit;
102
+ }
103
+
104
+ /** Whether the messages are signed with a two-step TAN method, so that the bank may ask for approvals. */
105
+ private get isTwoStep(): boolean {
106
+ return !this.method.isOneStep;
107
+ }
108
+
90
109
  /** The version of the HKTAN segments: that of the HITANS segment the bank describes the method in. */
91
110
  private get tanVersion(): number {
92
- return this.bankParameters?.tanVersion(this.method) ?? firstScaVersion;
111
+ return this.protocol.parameters.bankParameters?.tanVersion(this.method) ?? firstScaVersion;
93
112
  }
94
113
 
114
+ /** Opens the dialog; with `synchronize` the bank also assigns a customer system ID. */
95
115
  start(synchronize = false): Promise<IncomingMessage3> {
96
116
  if (synchronize) this.systemId = unassigned;
97
117
  return this.sendSegments([
98
- ...this.host.initSegments(this.systemId),
118
+ ...this.protocol.initSegments(this.systemId),
99
119
  ...(this.isTwoStep ? [this.tanRequest('HKIDN')] : []),
100
120
  ...(synchronize ? [Segments.synchronization()] : []),
101
121
  ]);
102
122
  }
103
123
 
104
124
  /**
105
- * Opens the dialog, asking strong authentication for `order` with a two-step method, then sends `order` in the next
106
- * message.
125
+ * The messages that open the dialog and send `order` while no TAN medium is chosen yet, as for listing the media:
126
+ * `login` opens the dialog, asking strong authentication for `order` with a two-step method, and `order` sends the
127
+ * order in a message of its own.
107
128
  */
108
- startWith(order: SegmentSpec): StartWithOrder<IncomingMessage3> {
129
+ startWith(order: SegmentSpec): {
130
+ readonly login: () => Promise<IncomingMessage3>;
131
+ readonly order: () => Promise<IncomingMessage3>;
132
+ } {
109
133
  return {
110
134
  login: () =>
111
135
  this.sendSegments([
112
- ...this.host.initSegments(this.systemId),
136
+ ...this.protocol.initSegments(this.systemId),
113
137
  ...(this.isTwoStep ? [this.tanRequest(order.type)] : []),
114
138
  ]),
115
139
  order: () => this.sendSegments([order]),
@@ -124,69 +148,139 @@ export class FinTs3Dialog extends PinTanDialog<IncomingMessage3, SegmentSpec> {
124
148
  */
125
149
  requestBankParameters(announceSca = false): Promise<IncomingMessage3> {
126
150
  return this.sendSegments([
127
- ...this.host.initSegments(unassigned, true),
151
+ ...this.protocol.initSegments(unassigned, true),
128
152
  ...(announceSca ? [Segments.tanProcess4(firstScaVersion, 'HKIDN')] : []),
129
153
  ]);
130
154
  }
131
155
 
156
+ /** Sends `order`, asking for strong customer authentication where the bank requires it. */
132
157
  send(order: SegmentSpec): Promise<IncomingMessage3> {
133
- const needsTan = this.isTwoStep && requireBankParameters(this.bankParameters).pinTan.requiresTan(order.type);
158
+ const needsTan =
159
+ this.isTwoStep && requireBankParameters(this.protocol.parameters.bankParameters).pinTan.requiresTan(order.type);
134
160
  return this.sendSegments([order, ...(needsTan ? [this.tanRequest(order.type)] : [])]);
135
161
  }
136
162
 
137
- protected async sendEnd(): Promise<void> {
138
- await this.sendSegments([Segments.dialogEnd(this.id)]);
163
+ /**
164
+ * Ends the dialog at the bank and returns the answer; `undefined` if the dialog is not active or the end failed.
165
+ *
166
+ * Bank and connection errors are ignored, since the bank may have ended the dialog on its side already.
167
+ */
168
+ async end(): Promise<IncomingMessage3 | undefined> {
169
+ if (!this.isActive) return undefined;
170
+ this.ended = true;
171
+ try {
172
+ return await this.sendSegments([Segments.dialogEnd(this.id)]);
173
+ } catch (error) {
174
+ if (!(error instanceof FinTsError)) throw error;
175
+ return undefined;
176
+ }
139
177
  }
140
178
 
141
- private tanRequest(segmentType: string): SegmentSpec {
142
- return Segments.tanProcess4(this.tanVersion, segmentType, this.mediumName);
179
+ /** Forgets the dialog without contacting the bank, for example after the bank rejected the PIN. */
180
+ discard(): void {
181
+ this.ended = true;
143
182
  }
144
183
 
145
184
  /**
146
- * The approval a HITAN segment of process 4 asks for, answered with HKTAN process 2 (TAN) or S (status).
185
+ * The approval `response`, the dialog's latest answer, asks for with return code 0030 (TAN) or 3955 (banking app):
186
+ * HITAN of process 4, answered with HKTAN process 2 (TAN) or S (status); `undefined` if it asks for none, or carries
187
+ * only the dummy challenge of a message the bank exempts from strong authentication.
147
188
  *
148
- * Throws `FinTsProtocolError` for an app approval with a method of HITANS #6: the status request (process S) came with
149
- * version 7, along with app approvals (PIN/TAN B.5.2).
189
+ * The bank has not executed the message then, so a request without a challenge is a `FinTsProtocolError`: read as
190
+ * done, it would report an empty result. So is an app approval with a method of HITANS #6: the status request
191
+ * (process S) came with version 7, along with app approvals (PIN/TAN B.5.2).
150
192
  */
151
- protected approvalIn(response: IncomingMessage3): Approval<IncomingMessage3> | undefined {
193
+ approvalAskedIn(response: IncomingMessage3): Approval<IncomingMessage3> | undefined {
194
+ const feedback = response.feedback;
195
+ const decoupled = hasCode(feedback, FeedbackCodes.decoupledPending);
196
+ if (hasErrors(feedback) || !(hasCode(feedback, FeedbackCodes.tanRequired) || decoupled)) return undefined;
152
197
  const tan = tanResponseOf(response, '4');
153
- if (tan === undefined || tan.isDummy) return undefined;
154
- if (hasCode(response.feedback, FeedbackCodes.decoupledPending) && this.tanVersion < firstDecoupledVersion) {
198
+ if (tan === undefined || tan.isDummy) {
199
+ throw new FinTsProtocolError('The bank asks for an approval, but sends no challenge');
200
+ }
201
+ if (decoupled && this.tanVersion < firstDecoupledVersion) {
155
202
  throw new FinTsProtocolError('The bank asks for an app approval, which needs HKTAN version 7');
156
203
  }
157
204
  const reference = tan.reference!;
158
- return this.approvalOf(response, {
159
- reference,
160
- message: tan.challenge,
161
- hhdUc: tan.hhdUc,
162
- validUntil: tan.validUntil,
163
- tanMediumName: tan.mediumName,
205
+ // The placeholder the dialog that looks up the TAN media sends, and the bank may echo, is no medium.
206
+ const medium = tan.mediumName ?? this.tanMedium;
207
+ return {
208
+ challenge: TanChallenge.create({
209
+ method: this.method,
210
+ reference,
211
+ message: tan.challenge,
212
+ hhdUc: tan.hhdUc,
213
+ isDecoupled: decoupled || this.method.decoupled,
214
+ validUntil: tan.validUntil,
215
+ tanMediumName: medium === noReference ? undefined : medium,
216
+ }),
164
217
  submitTan: (value) => this.sendSegments([Segments.tanProcess2(this.tanVersion, reference)], value),
165
218
  checkStatus: () => this.sendSegments([Segments.tanProcessStatus(this.tanVersion, reference)]),
219
+ waitForPush: hasCode(feedback, FeedbackCodes.decoupledWaitForPush),
166
220
  // A status answer that the approval is still pending may carry new instructions (PIN/TAN B.4.2.2.1, step 2b).
167
- instructionsIn: (status) => tanResponseOf(status, 'S')?.challenge,
168
- });
169
- }
170
-
171
- private sendSegments(segments: readonly SegmentSpec[], tan?: string): Promise<IncomingMessage3> {
172
- return this.transmit(
173
- (number) =>
174
- this.host.post({
175
- dialogId: this.id,
176
- number,
177
- segments,
178
- signing: this.anonymous
179
- ? undefined
180
- : { securityFunction: this.method.securityFunction, systemId: this.systemId },
181
- tan,
182
- }),
183
- (answer) => IncomingMessage3.parse(answer),
221
+ instructionsIn: (status) => challengeText(tanResponseOf(status, 'S')?.challenge),
222
+ };
223
+ }
224
+
225
+ private tanRequest(segmentType: string): SegmentSpec {
226
+ const medium = this.method.mediumRequirement === 'notAllowed' ? undefined : this.tanMedium;
227
+ return Segments.tanProcess4(
228
+ this.tanVersion,
229
+ segmentType,
230
+ medium === undefined ? undefined : this.protocol.parameters.tanMediumToSend(medium),
184
231
  );
185
232
  }
186
233
 
187
- /** The bank assigns the dialog ID in its answer to the first message. */
188
- protected readDialogData(response: IncomingMessage3): void {
189
- if (this.id === unassigned) this.id = response.dialogId ?? unassigned;
234
+ /**
235
+ * Sends `segments`, with `tan` where they complete an approval, as the next message, and reads the bank's answer. The
236
+ * message counts once the bank has answered, whatever it answered.
237
+ *
238
+ * The bank assigns the dialog ID in its answer to the first message, so the answer to another user's first message
239
+ * passes for it (`ParameterStore.reportedUpdVersion`). Any other answer to another message fails with
240
+ * `FinTsProtocolError` before anything of it is kept: a relay or proxy that mixes up answers would hand the client
241
+ * another user's data, or a page of another query. Only the bank's dialog abort message may name the dialog or the
242
+ * message as unknown instead (`IncomingMessage3.aborts`), so that its text reaches the user.
243
+ *
244
+ * The dialog ends on the client's side, without a dialog end, where it is over for the bank: after a return code that
245
+ * ends it, such as 9800, a dialog abort message, a rejected opening message (Formals C.1.2), or a broken connection
246
+ * (C.6). A request that fails while the client is closing, as one it aborts (`FinTs3Protocol.beginClose`), leaves the
247
+ * dialog active instead, since the bank may have received it: its end follows under the next number. Not so where
248
+ * the request opened the dialog, whose ID only the answer tells.
249
+ *
250
+ * Only an answer that does not reject the whole message starts the idle time anew (`hasEnded`): a rejection may come
251
+ * from a bank that has ended the dialog without saying so.
252
+ */
253
+ private async sendSegments(segments: readonly SegmentSpec[], tan?: string): Promise<IncomingMessage3> {
254
+ const number = this.messageNumber;
255
+ let answer: Uint8Array;
256
+ try {
257
+ answer = await this.protocol.post({
258
+ dialogId: this.id,
259
+ number,
260
+ segments,
261
+ signing: this.anonymous
262
+ ? undefined
263
+ : { securityFunction: this.method.securityFunction, systemId: this.systemId },
264
+ tan,
265
+ });
266
+ } catch (error) {
267
+ if (error instanceof FinTsTransportError) {
268
+ if (this.protocol.isClosing && this.id !== unassigned) this.messageNumber++;
269
+ else this.discard();
270
+ }
271
+ throw error;
272
+ }
273
+ this.messageNumber++;
274
+ const response = parseBankData(() => IncomingMessage3.parse(answer));
275
+ const assigned = this.id === unassigned ? undefined : this.id;
276
+ const aborted = response.aborts(number, assigned);
277
+ if (!aborted && !response.answers(number, assigned)) {
278
+ throw new FinTsProtocolError('The answer belongs to another message than the one sent');
279
+ }
280
+ if (assigned === undefined) this.id = response.dialogId ?? unassigned;
281
+ if (!response.messageRejected) this.lastAnswer = Date.now();
282
+ if (aborted || endsDialog(response.feedback) || (number === 1 && response.messageRejected)) this.discard();
283
+ return response;
190
284
  }
191
285
  }
192
286