@coderoftime/fints 0.0.0-stage → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (249) hide show
  1. package/CHANGELOG.md +152 -0
  2. package/LICENSE +28 -0
  3. package/README.md +246 -2
  4. package/dist/client.d.ts +48 -0
  5. package/dist/client.d.ts.map +1 -0
  6. package/dist/client.js +199 -0
  7. package/dist/client.js.map +1 -0
  8. package/dist/core/account.d.ts +168 -0
  9. package/dist/core/account.d.ts.map +1 -0
  10. package/dist/core/account.js +245 -0
  11. package/dist/core/account.js.map +1 -0
  12. package/dist/core/amount.d.ts +63 -0
  13. package/dist/core/amount.d.ts.map +1 -0
  14. package/dist/core/amount.js +192 -0
  15. package/dist/core/amount.js.map +1 -0
  16. package/dist/core/balance.d.ts +55 -0
  17. package/dist/core/balance.d.ts.map +1 -0
  18. package/dist/core/balance.js +54 -0
  19. package/dist/core/balance.js.map +1 -0
  20. package/dist/core/bank-info.d.ts +49 -0
  21. package/dist/core/bank-info.d.ts.map +1 -0
  22. package/dist/core/bank-info.js +52 -0
  23. package/dist/core/bank-info.js.map +1 -0
  24. package/dist/core/client.d.ts +161 -0
  25. package/dist/core/client.d.ts.map +1 -0
  26. package/dist/core/client.js +2 -0
  27. package/dist/core/client.js.map +1 -0
  28. package/dist/core/config.d.ts +80 -0
  29. package/dist/core/config.d.ts.map +1 -0
  30. package/dist/core/config.js +153 -0
  31. package/dist/core/config.js.map +1 -0
  32. package/dist/core/errors.d.ts +153 -0
  33. package/dist/core/errors.d.ts.map +1 -0
  34. package/dist/core/errors.js +247 -0
  35. package/dist/core/errors.js.map +1 -0
  36. package/dist/core/feedback.d.ts +72 -0
  37. package/dist/core/feedback.d.ts.map +1 -0
  38. package/dist/core/feedback.js +127 -0
  39. package/dist/core/feedback.js.map +1 -0
  40. package/dist/core/transaction.d.ts +103 -0
  41. package/dist/core/transaction.d.ts.map +1 -0
  42. package/dist/core/transaction.js +96 -0
  43. package/dist/core/transaction.js.map +1 -0
  44. package/dist/core/transport.d.ts +64 -0
  45. package/dist/core/transport.d.ts.map +1 -0
  46. package/dist/core/transport.js +149 -0
  47. package/dist/core/transport.js.map +1 -0
  48. package/dist/formats/camt.d.ts +32 -0
  49. package/dist/formats/camt.d.ts.map +1 -0
  50. package/dist/formats/camt.js +242 -0
  51. package/dist/formats/camt.js.map +1 -0
  52. package/dist/formats/mt940.d.ts +54 -0
  53. package/dist/formats/mt940.d.ts.map +1 -0
  54. package/dist/formats/mt940.js +376 -0
  55. package/dist/formats/mt940.js.map +1 -0
  56. package/dist/formats/readers.d.ts +15 -0
  57. package/dist/formats/readers.d.ts.map +1 -0
  58. package/dist/formats/readers.js +42 -0
  59. package/dist/formats/readers.js.map +1 -0
  60. package/dist/formats/record-budget.d.ts +7 -0
  61. package/dist/formats/record-budget.d.ts.map +1 -0
  62. package/dist/formats/record-budget.js +17 -0
  63. package/dist/formats/record-budget.js.map +1 -0
  64. package/dist/formats/sepa-purpose.d.ts +28 -0
  65. package/dist/formats/sepa-purpose.d.ts.map +1 -0
  66. package/dist/formats/sepa-purpose.js +89 -0
  67. package/dist/formats/sepa-purpose.js.map +1 -0
  68. package/dist/index.d.ts +28 -0
  69. package/dist/index.d.ts.map +1 -0
  70. package/dist/index.js +27 -0
  71. package/dist/index.js.map +1 -0
  72. package/dist/session/app-approval.d.ts +66 -0
  73. package/dist/session/app-approval.d.ts.map +1 -0
  74. package/dist/session/app-approval.js +226 -0
  75. package/dist/session/app-approval.js.map +1 -0
  76. package/dist/session/dialog-runner.d.ts +31 -0
  77. package/dist/session/dialog-runner.d.ts.map +1 -0
  78. package/dist/session/dialog-runner.js +111 -0
  79. package/dist/session/dialog-runner.js.map +1 -0
  80. package/dist/session/login-guard.d.ts +78 -0
  81. package/dist/session/login-guard.d.ts.map +1 -0
  82. package/dist/session/login-guard.js +181 -0
  83. package/dist/session/login-guard.js.map +1 -0
  84. package/dist/session/sepa-accounts.d.ts +40 -0
  85. package/dist/session/sepa-accounts.d.ts.map +1 -0
  86. package/dist/session/sepa-accounts.js +112 -0
  87. package/dist/session/sepa-accounts.js.map +1 -0
  88. package/dist/session/session-setup.d.ts +80 -0
  89. package/dist/session/session-setup.d.ts.map +1 -0
  90. package/dist/session/session-setup.js +182 -0
  91. package/dist/session/session-setup.js.map +1 -0
  92. package/dist/session/session-state.d.ts +50 -0
  93. package/dist/session/session-state.d.ts.map +1 -0
  94. package/dist/session/session-state.js +182 -0
  95. package/dist/session/session-state.js.map +1 -0
  96. package/dist/session/signed-dialog.d.ts +77 -0
  97. package/dist/session/signed-dialog.d.ts.map +1 -0
  98. package/dist/session/signed-dialog.js +170 -0
  99. package/dist/session/signed-dialog.js.map +1 -0
  100. package/dist/session/tan-selection.d.ts +96 -0
  101. package/dist/session/tan-selection.d.ts.map +1 -0
  102. package/dist/session/tan-selection.js +189 -0
  103. package/dist/session/tan-selection.js.map +1 -0
  104. package/dist/session/user-prompts.d.ts +65 -0
  105. package/dist/session/user-prompts.d.ts.map +1 -0
  106. package/dist/session/user-prompts.js +184 -0
  107. package/dist/session/user-prompts.js.map +1 -0
  108. package/dist/tan/challenge.d.ts +135 -0
  109. package/dist/tan/challenge.d.ts.map +1 -0
  110. package/dist/tan/challenge.js +184 -0
  111. package/dist/tan/challenge.js.map +1 -0
  112. package/dist/tan/flicker.d.ts +42 -0
  113. package/dist/tan/flicker.d.ts.map +1 -0
  114. package/dist/tan/flicker.js +296 -0
  115. package/dist/tan/flicker.js.map +1 -0
  116. package/dist/tan/tan-image.d.ts +22 -0
  117. package/dist/tan/tan-image.d.ts.map +1 -0
  118. package/dist/tan/tan-image.js +43 -0
  119. package/dist/tan/tan-image.js.map +1 -0
  120. package/dist/tan/tan-models.d.ts +149 -0
  121. package/dist/tan/tan-models.d.ts.map +1 -0
  122. package/dist/tan/tan-models.js +153 -0
  123. package/dist/tan/tan-models.js.map +1 -0
  124. package/dist/util/async.d.ts +22 -0
  125. package/dist/util/async.d.ts.map +1 -0
  126. package/dist/util/async.js +45 -0
  127. package/dist/util/async.js.map +1 -0
  128. package/dist/util/bytes.d.ts +22 -0
  129. package/dist/util/bytes.d.ts.map +1 -0
  130. package/dist/util/bytes.js +128 -0
  131. package/dist/util/bytes.js.map +1 -0
  132. package/dist/util/call-context.d.ts +12 -0
  133. package/dist/util/call-context.d.ts.map +1 -0
  134. package/dist/util/call-context.js +30 -0
  135. package/dist/util/call-context.js.map +1 -0
  136. package/dist/util/dates.d.ts +55 -0
  137. package/dist/util/dates.d.ts.map +1 -0
  138. package/dist/util/dates.js +189 -0
  139. package/dist/util/dates.js.map +1 -0
  140. package/dist/util/errors.d.ts +17 -0
  141. package/dist/util/errors.d.ts.map +1 -0
  142. package/dist/util/errors.js +24 -0
  143. package/dist/util/errors.js.map +1 -0
  144. package/dist/util/immutable-map.d.ts +16 -0
  145. package/dist/util/immutable-map.d.ts.map +1 -0
  146. package/dist/util/immutable-map.js +38 -0
  147. package/dist/util/immutable-map.js.map +1 -0
  148. package/dist/util/json.d.ts +17 -0
  149. package/dist/util/json.d.ts.map +1 -0
  150. package/dist/util/json.js +62 -0
  151. package/dist/util/json.js.map +1 -0
  152. package/dist/util/text.d.ts +37 -0
  153. package/dist/util/text.d.ts.map +1 -0
  154. package/dist/util/text.js +107 -0
  155. package/dist/util/text.js.map +1 -0
  156. package/dist/util/xml.d.ts +36 -0
  157. package/dist/util/xml.d.ts.map +1 -0
  158. package/dist/util/xml.js +194 -0
  159. package/dist/util/xml.js.map +1 -0
  160. package/dist/v3/account-extension.d.ts +25 -0
  161. package/dist/v3/account-extension.d.ts.map +1 -0
  162. package/dist/v3/account-extension.js +173 -0
  163. package/dist/v3/account-extension.js.map +1 -0
  164. package/dist/v3/bank-connection.d.ts +34 -0
  165. package/dist/v3/bank-connection.d.ts.map +1 -0
  166. package/dist/v3/bank-connection.js +58 -0
  167. package/dist/v3/bank-connection.js.map +1 -0
  168. package/dist/v3/dialog.d.ts +112 -0
  169. package/dist/v3/dialog.d.ts.map +1 -0
  170. package/dist/v3/dialog.js +241 -0
  171. package/dist/v3/dialog.js.map +1 -0
  172. package/dist/v3/message.d.ts +85 -0
  173. package/dist/v3/message.d.ts.map +1 -0
  174. package/dist/v3/message.js +303 -0
  175. package/dist/v3/message.js.map +1 -0
  176. package/dist/v3/parameter-store.d.ts +75 -0
  177. package/dist/v3/parameter-store.d.ts.map +1 -0
  178. package/dist/v3/parameter-store.js +143 -0
  179. package/dist/v3/parameter-store.js.map +1 -0
  180. package/dist/v3/parameters.d.ts +105 -0
  181. package/dist/v3/parameters.d.ts.map +1 -0
  182. package/dist/v3/parameters.js +450 -0
  183. package/dist/v3/parameters.js.map +1 -0
  184. package/dist/v3/protocol.d.ts +61 -0
  185. package/dist/v3/protocol.d.ts.map +1 -0
  186. package/dist/v3/protocol.js +113 -0
  187. package/dist/v3/protocol.js.map +1 -0
  188. package/dist/v3/queries.d.ts +51 -0
  189. package/dist/v3/queries.d.ts.map +1 -0
  190. package/dist/v3/queries.js +120 -0
  191. package/dist/v3/queries.js.map +1 -0
  192. package/dist/v3/segments.d.ts +106 -0
  193. package/dist/v3/segments.d.ts.map +1 -0
  194. package/dist/v3/segments.js +325 -0
  195. package/dist/v3/segments.js.map +1 -0
  196. package/dist/v3/syntax.d.ts +127 -0
  197. package/dist/v3/syntax.d.ts.map +1 -0
  198. package/dist/v3/syntax.js +367 -0
  199. package/dist/v3/syntax.js.map +1 -0
  200. package/package.json +67 -4
  201. package/src/client.ts +252 -0
  202. package/src/core/account.ts +323 -0
  203. package/src/core/amount.ts +211 -0
  204. package/src/core/balance.ts +85 -0
  205. package/src/core/bank-info.ts +66 -0
  206. package/src/core/client.ts +186 -0
  207. package/src/core/config.ts +182 -0
  208. package/src/core/errors.ts +277 -0
  209. package/src/core/feedback.ts +140 -0
  210. package/src/core/transaction.ts +161 -0
  211. package/src/core/transport.ts +189 -0
  212. package/src/formats/camt.ts +266 -0
  213. package/src/formats/mt940.ts +419 -0
  214. package/src/formats/readers.ts +49 -0
  215. package/src/formats/record-budget.ts +18 -0
  216. package/src/formats/sepa-purpose.ts +94 -0
  217. package/src/index.ts +70 -0
  218. package/src/session/app-approval.ts +278 -0
  219. package/src/session/dialog-runner.ts +122 -0
  220. package/src/session/login-guard.ts +221 -0
  221. package/src/session/sepa-accounts.ts +115 -0
  222. package/src/session/session-setup.ts +184 -0
  223. package/src/session/session-state.ts +204 -0
  224. package/src/session/signed-dialog.ts +192 -0
  225. package/src/session/tan-selection.ts +203 -0
  226. package/src/session/user-prompts.ts +193 -0
  227. package/src/tan/challenge.ts +261 -0
  228. package/src/tan/flicker.ts +308 -0
  229. package/src/tan/tan-image.ts +38 -0
  230. package/src/tan/tan-models.ts +252 -0
  231. package/src/util/async.ts +48 -0
  232. package/src/util/bytes.ts +129 -0
  233. package/src/util/call-context.ts +41 -0
  234. package/src/util/dates.ts +210 -0
  235. package/src/util/errors.ts +27 -0
  236. package/src/util/immutable-map.ts +47 -0
  237. package/src/util/json.ts +67 -0
  238. package/src/util/text.ts +112 -0
  239. package/src/util/xml.ts +183 -0
  240. package/src/v3/account-extension.ts +185 -0
  241. package/src/v3/bank-connection.ts +67 -0
  242. package/src/v3/dialog.ts +283 -0
  243. package/src/v3/message.ts +373 -0
  244. package/src/v3/parameter-store.ts +160 -0
  245. package/src/v3/parameters.ts +524 -0
  246. package/src/v3/protocol.ts +148 -0
  247. package/src/v3/queries.ts +190 -0
  248. package/src/v3/segments.ts +374 -0
  249. package/src/v3/syntax.ts +369 -0
@@ -0,0 +1,67 @@
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 on their way (`abortRequests`). */
23
+ private requests = new AbortController();
24
+
25
+ /** A connection to `url` through `transport`, an `HttpTransport` by default. */
26
+ constructor(
27
+ url: URL,
28
+ transport: FinTsTransport | undefined,
29
+ private readonly logger: MessageLogger | undefined,
30
+ ) {
31
+ this.href = url.href;
32
+ this.transport = transport ?? new HttpTransport();
33
+ }
34
+
35
+ /**
36
+ * Posts `body` to a new copy of the URL, so that a transport that changes the one it is given changes no later
37
+ * request.
38
+ *
39
+ * Reports any failure of the transport as `FinTsTransportError`: a custom transport that calls `fetch` fails with a
40
+ * `TypeError` where the network does, and one that throws another `FinTsError`, such as `FinTsPinError`, would pass
41
+ * for an answer of the bank. Of such an error only the message is kept (`failureMessage`), since it may hold `body`,
42
+ * as the errors of HTTP clients such as axios do.
43
+ */
44
+ async post(body: Uint8Array, contentType: string): Promise<Uint8Array> {
45
+ try {
46
+ return await this.transport.post(new URL(this.href), body, contentType, this.requests.signal);
47
+ } catch (error) {
48
+ if (error instanceof FinTsTransportError) throw error;
49
+ throw new FinTsTransportError(failureMessage(error));
50
+ }
51
+ }
52
+
53
+ /** Reports a message to the logger; `message` is only built when a logger is set. */
54
+ log(direction: MessageDirection, message: () => string): void {
55
+ this.logger?.(direction, message());
56
+ }
57
+
58
+ /** Aborts the requests on their way; those posted afterwards, such as the dialog ends of a closing client, go out. */
59
+ abortRequests(): void {
60
+ this.requests.abort();
61
+ this.requests = new AbortController();
62
+ }
63
+
64
+ close(): void {
65
+ this.transport.close();
66
+ }
67
+ }
@@ -0,0 +1,283 @@
1
+ // Internal to the package, and named after the FinTS specification. Documentation is where the meaning is not evident.
2
+
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';
6
+ import { TanMethod } from '../tan/tan-models.js';
7
+ import { IncomingMessage3 } from './message.js';
8
+ import { requireBankParameters } from './parameters.js';
9
+ import type { FinTs3Protocol } from './protocol.js';
10
+ import { Segments, TanResponse } from './segments.js';
11
+ import type { SegmentSpec } from './syntax.js';
12
+
13
+ /** Dialog ID of a dialog the bank has not answered yet, and customer system ID before synchronization. */
14
+ const unassigned = '0';
15
+
16
+ /** The first version of HKTAN that supports strong customer authentication. */
17
+ const firstScaVersion = 6;
18
+
19
+ /** The first version of HKTAN that supports app approvals, with the status request (process S). */
20
+ const firstDecoupledVersion = 7;
21
+
22
+ /**
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.
25
+ */
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;
32
+
33
+ /** Sends the TAN the user entered. */
34
+ readonly submitTan: (tan: string) => Promise<R>;
35
+
36
+ /** Asks the bank whether the user has approved in the banking app. */
37
+ readonly checkStatus: () => Promise<R>;
38
+
39
+ /**
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.
42
+ */
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;
47
+ }
48
+
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 {
54
+ private systemId: string;
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;
61
+
62
+ private constructor(
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
+ private readonly anonymous: boolean,
68
+ ) {
69
+ this.systemId = anonymous ? unassigned : (protocol.parameters.systemId ?? unassigned);
70
+ }
71
+
72
+ static signed(protocol: FinTs3Protocol, method: TanMethod, tanMedium?: string): FinTs3Dialog {
73
+ return new FinTs3Dialog(protocol, method, tanMedium, false);
74
+ }
75
+
76
+ /** A dialog without login, which only delivers the bank parameter data. */
77
+ static anonymous(protocol: FinTs3Protocol): FinTs3Dialog {
78
+ return new FinTs3Dialog(protocol, TanMethod.oneStep, undefined, true);
79
+ }
80
+
81
+ /** Whether the bank knows this dialog and it has not ended. */
82
+ get isActive(): boolean {
83
+ return this.id !== unassigned && !this.hasEnded;
84
+ }
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
+
109
+ /** The version of the HKTAN segments: that of the HITANS segment the bank describes the method in. */
110
+ private get tanVersion(): number {
111
+ return this.protocol.parameters.bankParameters?.tanVersion(this.method) ?? firstScaVersion;
112
+ }
113
+
114
+ /** Opens the dialog; with `synchronize` the bank also assigns a customer system ID. */
115
+ start(synchronize = false): Promise<IncomingMessage3> {
116
+ if (synchronize) this.systemId = unassigned;
117
+ return this.sendSegments([
118
+ ...this.protocol.initSegments(this.systemId),
119
+ ...(this.isTwoStep ? [this.tanRequest('HKIDN')] : []),
120
+ ...(synchronize ? [Segments.synchronization()] : []),
121
+ ]);
122
+ }
123
+
124
+ /**
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.
128
+ */
129
+ startWith(order: SegmentSpec): {
130
+ readonly login: () => Promise<IncomingMessage3>;
131
+ readonly order: () => Promise<IncomingMessage3>;
132
+ } {
133
+ return {
134
+ login: () =>
135
+ this.sendSegments([
136
+ ...this.protocol.initSegments(this.systemId),
137
+ ...(this.isTwoStep ? [this.tanRequest(order.type)] : []),
138
+ ]),
139
+ order: () => this.sendSegments([order]),
140
+ };
141
+ }
142
+
143
+ /**
144
+ * Opens the anonymous dialog; the answer carries the bank parameter data.
145
+ *
146
+ * With `announceSca` the message also carries HKTAN, the way a client that can do strong customer authentication
147
+ * tells the bank so, whereupon the bank may send parameter data suited to it (PIN/TAN, B.4.3.1).
148
+ */
149
+ requestBankParameters(announceSca = false): Promise<IncomingMessage3> {
150
+ return this.sendSegments([
151
+ ...this.protocol.initSegments(unassigned, true),
152
+ ...(announceSca ? [Segments.tanProcess4(firstScaVersion, 'HKIDN')] : []),
153
+ ]);
154
+ }
155
+
156
+ /** Sends `order`, asking for strong customer authentication where the bank requires it. */
157
+ send(order: SegmentSpec): Promise<IncomingMessage3> {
158
+ const needsTan =
159
+ this.isTwoStep && requireBankParameters(this.protocol.parameters.bankParameters).pinTan.requiresTan(order.type);
160
+ return this.sendSegments([order, ...(needsTan ? [this.tanRequest(order.type)] : [])]);
161
+ }
162
+
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
+ }
177
+ }
178
+
179
+ /** Forgets the dialog without contacting the bank, for example after the bank rejected the PIN. */
180
+ discard(): void {
181
+ this.ended = true;
182
+ }
183
+
184
+ /**
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.
188
+ *
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).
192
+ */
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;
197
+ const tan = tanResponseOf(response, '4');
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) {
202
+ throw new FinTsProtocolError('The bank asks for an app approval, which needs HKTAN version 7');
203
+ }
204
+ const reference = tan.reference!;
205
+ return {
206
+ challenge: TanChallenge.create({
207
+ method: this.method,
208
+ reference,
209
+ message: tan.challenge,
210
+ hhdUc: tan.hhdUc,
211
+ isDecoupled: decoupled || this.method.decoupled,
212
+ validUntil: tan.validUntil,
213
+ tanMediumName: tan.mediumName ?? this.tanMedium,
214
+ }),
215
+ submitTan: (value) => this.sendSegments([Segments.tanProcess2(this.tanVersion, reference)], value),
216
+ checkStatus: () => this.sendSegments([Segments.tanProcessStatus(this.tanVersion, reference)]),
217
+ waitForPush: hasCode(feedback, FeedbackCodes.decoupledWaitForPush),
218
+ // A status answer that the approval is still pending may carry new instructions (PIN/TAN B.4.2.2.1, step 2b).
219
+ instructionsIn: (status) => challengeText(tanResponseOf(status, 'S')?.challenge),
220
+ };
221
+ }
222
+
223
+ private tanRequest(segmentType: string): SegmentSpec {
224
+ const medium = this.method.mediumRequirement === 'notAllowed' ? undefined : this.tanMedium;
225
+ return Segments.tanProcess4(
226
+ this.tanVersion,
227
+ segmentType,
228
+ medium === undefined ? undefined : this.protocol.parameters.tanMediumAsSent(medium),
229
+ );
230
+ }
231
+
232
+ /**
233
+ * Sends `segments`, with `tan` where they complete an approval, as the next message, and reads the bank's answer. The
234
+ * message counts once the bank has answered, whatever it answered.
235
+ *
236
+ * The bank assigns the dialog ID in its answer to the first message, so the answer to another user's first message
237
+ * passes for it (`ParameterStore.reportedUpdVersion`). Any other answer to another message fails with
238
+ * `FinTsProtocolError` before anything of it is kept: a relay or proxy that mixes up answers would hand the client
239
+ * another user's data, or a page of another query.
240
+ *
241
+ * The dialog ends on the client's side, without a dialog end, where it is over for the bank: after a return code that
242
+ * ends it, such as 9800, and after a rejected opening message (Formals C.1.2), or a broken connection (C.6).
243
+ *
244
+ * Only an answer that does not reject the whole message starts the idle time anew (`hasEnded`): a rejection may come
245
+ * from a bank that has ended the dialog without saying so.
246
+ */
247
+ private async sendSegments(segments: readonly SegmentSpec[], tan?: string): Promise<IncomingMessage3> {
248
+ const number = this.messageNumber;
249
+ let answer: Uint8Array;
250
+ try {
251
+ answer = await this.protocol.post({
252
+ dialogId: this.id,
253
+ number,
254
+ segments,
255
+ signing: this.anonymous
256
+ ? undefined
257
+ : { securityFunction: this.method.securityFunction, systemId: this.systemId },
258
+ tan,
259
+ });
260
+ } catch (error) {
261
+ if (error instanceof FinTsTransportError) this.discard();
262
+ throw error;
263
+ }
264
+ this.messageNumber++;
265
+ const response = parseBankData(() => IncomingMessage3.parse(answer));
266
+ const assigned = this.id === unassigned ? undefined : this.id;
267
+ if (!response.answers(number, assigned)) {
268
+ throw new FinTsProtocolError('The answer belongs to another message than the one sent');
269
+ }
270
+ if (assigned === undefined) this.id = response.dialogId ?? unassigned;
271
+ if (!response.messageRejected) this.lastAnswer = Date.now();
272
+ if (endsDialog(response.feedback) || (number === 1 && response.messageRejected)) this.discard();
273
+ return response;
274
+ }
275
+ }
276
+
277
+ /** The TAN part of `response` that answers TAN `process`. */
278
+ function tanResponseOf(response: IncomingMessage3, process: string): TanResponse | undefined {
279
+ return response
280
+ .findAll('HITAN')
281
+ .map((s) => TanResponse.parse(s))
282
+ .find((t) => t.process === process);
283
+ }