@coderoftime/fints 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (272) hide show
  1. package/CHANGELOG.md +142 -1
  2. package/README.md +127 -37
  3. package/dist/client.d.ts +48 -0
  4. package/dist/client.d.ts.map +1 -0
  5. package/dist/client.js +199 -0
  6. package/dist/client.js.map +1 -0
  7. package/dist/core/account.d.ts +35 -7
  8. package/dist/core/account.d.ts.map +1 -1
  9. package/dist/core/account.js +84 -13
  10. package/dist/core/account.js.map +1 -1
  11. package/dist/core/amount.d.ts +2 -1
  12. package/dist/core/amount.d.ts.map +1 -1
  13. package/dist/core/amount.js +4 -1
  14. package/dist/core/amount.js.map +1 -1
  15. package/dist/core/balance.d.ts +1 -0
  16. package/dist/core/balance.d.ts.map +1 -1
  17. package/dist/core/balance.js +6 -5
  18. package/dist/core/balance.js.map +1 -1
  19. package/dist/core/bank-info.d.ts +1 -1
  20. package/dist/core/bank-info.js +1 -1
  21. package/dist/core/client.d.ts +50 -15
  22. package/dist/core/client.d.ts.map +1 -1
  23. package/dist/core/config.d.ts +28 -16
  24. package/dist/core/config.d.ts.map +1 -1
  25. package/dist/core/config.js +66 -26
  26. package/dist/core/config.js.map +1 -1
  27. package/dist/core/errors.d.ts +85 -23
  28. package/dist/core/errors.d.ts.map +1 -1
  29. package/dist/core/errors.js +112 -47
  30. package/dist/core/errors.js.map +1 -1
  31. package/dist/core/feedback.d.ts +4 -1
  32. package/dist/core/feedback.d.ts.map +1 -1
  33. package/dist/core/feedback.js +4 -1
  34. package/dist/core/feedback.js.map +1 -1
  35. package/dist/core/transaction.d.ts +6 -1
  36. package/dist/core/transaction.d.ts.map +1 -1
  37. package/dist/core/transaction.js +9 -4
  38. package/dist/core/transaction.js.map +1 -1
  39. package/dist/core/transport.d.ts +33 -6
  40. package/dist/core/transport.d.ts.map +1 -1
  41. package/dist/core/transport.js +77 -20
  42. package/dist/core/transport.js.map +1 -1
  43. package/dist/formats/camt.d.ts +24 -4
  44. package/dist/formats/camt.d.ts.map +1 -1
  45. package/dist/formats/camt.js +124 -41
  46. package/dist/formats/camt.js.map +1 -1
  47. package/dist/formats/mt940.d.ts +15 -3
  48. package/dist/formats/mt940.d.ts.map +1 -1
  49. package/dist/formats/mt940.js +161 -82
  50. package/dist/formats/mt940.js.map +1 -1
  51. package/dist/formats/readers.d.ts +6 -2
  52. package/dist/formats/readers.d.ts.map +1 -1
  53. package/dist/formats/readers.js +24 -29
  54. package/dist/formats/readers.js.map +1 -1
  55. package/dist/formats/record-budget.d.ts +7 -0
  56. package/dist/formats/record-budget.d.ts.map +1 -0
  57. package/dist/formats/record-budget.js +17 -0
  58. package/dist/formats/record-budget.js.map +1 -0
  59. package/dist/formats/sepa-purpose.d.ts +6 -0
  60. package/dist/formats/sepa-purpose.d.ts.map +1 -1
  61. package/dist/formats/sepa-purpose.js +23 -2
  62. package/dist/formats/sepa-purpose.js.map +1 -1
  63. package/dist/index.d.ts +3 -3
  64. package/dist/index.d.ts.map +1 -1
  65. package/dist/index.js +2 -2
  66. package/dist/index.js.map +1 -1
  67. package/dist/session/app-approval.d.ts +11 -5
  68. package/dist/session/app-approval.d.ts.map +1 -1
  69. package/dist/session/app-approval.js +23 -12
  70. package/dist/session/app-approval.js.map +1 -1
  71. package/dist/session/dialog-runner.d.ts +7 -10
  72. package/dist/session/dialog-runner.d.ts.map +1 -1
  73. package/dist/session/dialog-runner.js +15 -16
  74. package/dist/session/dialog-runner.js.map +1 -1
  75. package/dist/session/login-guard.d.ts +54 -49
  76. package/dist/session/login-guard.d.ts.map +1 -1
  77. package/dist/session/login-guard.js +156 -84
  78. package/dist/session/login-guard.js.map +1 -1
  79. package/dist/session/sepa-accounts.d.ts +20 -14
  80. package/dist/session/sepa-accounts.d.ts.map +1 -1
  81. package/dist/session/sepa-accounts.js +62 -46
  82. package/dist/session/sepa-accounts.js.map +1 -1
  83. package/dist/session/session-setup.d.ts +33 -25
  84. package/dist/session/session-setup.d.ts.map +1 -1
  85. package/dist/session/session-setup.js +87 -65
  86. package/dist/session/session-setup.js.map +1 -1
  87. package/dist/session/session-state.d.ts +9 -6
  88. package/dist/session/session-state.d.ts.map +1 -1
  89. package/dist/session/session-state.js +52 -17
  90. package/dist/session/session-state.js.map +1 -1
  91. package/dist/session/signed-dialog.d.ts +77 -0
  92. package/dist/session/signed-dialog.d.ts.map +1 -0
  93. package/dist/session/signed-dialog.js +170 -0
  94. package/dist/session/signed-dialog.js.map +1 -0
  95. package/dist/session/tan-selection.d.ts +26 -18
  96. package/dist/session/tan-selection.d.ts.map +1 -1
  97. package/dist/session/tan-selection.js +44 -33
  98. package/dist/session/tan-selection.js.map +1 -1
  99. package/dist/session/user-prompts.d.ts +8 -4
  100. package/dist/session/user-prompts.d.ts.map +1 -1
  101. package/dist/session/user-prompts.js +34 -14
  102. package/dist/session/user-prompts.js.map +1 -1
  103. package/dist/tan/challenge.d.ts +34 -13
  104. package/dist/tan/challenge.d.ts.map +1 -1
  105. package/dist/tan/challenge.js +25 -26
  106. package/dist/tan/challenge.js.map +1 -1
  107. package/dist/tan/flicker.d.ts +7 -1
  108. package/dist/tan/flicker.d.ts.map +1 -1
  109. package/dist/tan/flicker.js +40 -6
  110. package/dist/tan/flicker.js.map +1 -1
  111. package/dist/tan/tan-image.d.ts +3 -2
  112. package/dist/tan/tan-image.d.ts.map +1 -1
  113. package/dist/tan/tan-image.js +7 -5
  114. package/dist/tan/tan-image.js.map +1 -1
  115. package/dist/tan/tan-models.d.ts +5 -1
  116. package/dist/tan/tan-models.d.ts.map +1 -1
  117. package/dist/tan/tan-models.js +10 -5
  118. package/dist/tan/tan-models.js.map +1 -1
  119. package/dist/util/async.d.ts +0 -2
  120. package/dist/util/async.d.ts.map +1 -1
  121. package/dist/util/async.js +0 -4
  122. package/dist/util/async.js.map +1 -1
  123. package/dist/util/bytes.d.ts +2 -3
  124. package/dist/util/bytes.d.ts.map +1 -1
  125. package/dist/util/bytes.js +12 -20
  126. package/dist/util/bytes.js.map +1 -1
  127. package/dist/util/call-context.d.ts.map +1 -1
  128. package/dist/util/call-context.js +3 -2
  129. package/dist/util/call-context.js.map +1 -1
  130. package/dist/util/dates.d.ts +9 -16
  131. package/dist/util/dates.d.ts.map +1 -1
  132. package/dist/util/dates.js +39 -19
  133. package/dist/util/dates.js.map +1 -1
  134. package/dist/util/errors.d.ts +9 -1
  135. package/dist/util/errors.d.ts.map +1 -1
  136. package/dist/util/errors.js +12 -3
  137. package/dist/util/errors.js.map +1 -1
  138. package/dist/util/json.d.ts +4 -0
  139. package/dist/util/json.d.ts.map +1 -1
  140. package/dist/util/json.js +18 -0
  141. package/dist/util/json.js.map +1 -1
  142. package/dist/util/text.d.ts +21 -4
  143. package/dist/util/text.d.ts.map +1 -1
  144. package/dist/util/text.js +42 -13
  145. package/dist/util/text.js.map +1 -1
  146. package/dist/util/xml.d.ts +4 -3
  147. package/dist/util/xml.d.ts.map +1 -1
  148. package/dist/util/xml.js +37 -26
  149. package/dist/util/xml.js.map +1 -1
  150. package/dist/v3/account-extension.d.ts.map +1 -1
  151. package/dist/v3/account-extension.js +7 -10
  152. package/dist/v3/account-extension.js.map +1 -1
  153. package/dist/v3/bank-connection.d.ts +34 -0
  154. package/dist/v3/bank-connection.d.ts.map +1 -0
  155. package/dist/v3/bank-connection.js +58 -0
  156. package/dist/v3/bank-connection.js.map +1 -0
  157. package/dist/v3/dialog.d.ts +80 -46
  158. package/dist/v3/dialog.d.ts.map +1 -1
  159. package/dist/v3/dialog.js +162 -59
  160. package/dist/v3/dialog.js.map +1 -1
  161. package/dist/v3/message.d.ts +17 -4
  162. package/dist/v3/message.d.ts.map +1 -1
  163. package/dist/v3/message.js +62 -18
  164. package/dist/v3/message.js.map +1 -1
  165. package/dist/v3/parameter-store.d.ts +75 -0
  166. package/dist/v3/parameter-store.d.ts.map +1 -0
  167. package/dist/v3/parameter-store.js +143 -0
  168. package/dist/v3/parameter-store.js.map +1 -0
  169. package/dist/v3/parameters.d.ts +38 -12
  170. package/dist/v3/parameters.d.ts.map +1 -1
  171. package/dist/v3/parameters.js +91 -37
  172. package/dist/v3/parameters.js.map +1 -1
  173. package/dist/v3/protocol.d.ts +43 -50
  174. package/dist/v3/protocol.d.ts.map +1 -1
  175. package/dist/v3/protocol.js +58 -208
  176. package/dist/v3/protocol.js.map +1 -1
  177. package/dist/v3/queries.d.ts +51 -0
  178. package/dist/v3/queries.d.ts.map +1 -0
  179. package/dist/v3/queries.js +120 -0
  180. package/dist/v3/queries.js.map +1 -0
  181. package/dist/v3/segments.d.ts +29 -12
  182. package/dist/v3/segments.d.ts.map +1 -1
  183. package/dist/v3/segments.js +65 -23
  184. package/dist/v3/segments.js.map +1 -1
  185. package/dist/v3/syntax.d.ts +65 -14
  186. package/dist/v3/syntax.d.ts.map +1 -1
  187. package/dist/v3/syntax.js +167 -101
  188. package/dist/v3/syntax.js.map +1 -1
  189. package/package.json +10 -6
  190. package/src/client.ts +252 -0
  191. package/src/core/account.ts +86 -13
  192. package/src/core/amount.ts +3 -1
  193. package/src/core/balance.ts +6 -5
  194. package/src/core/bank-info.ts +1 -1
  195. package/src/core/client.ts +52 -15
  196. package/src/core/config.ts +69 -28
  197. package/src/core/errors.ts +122 -48
  198. package/src/core/feedback.ts +4 -1
  199. package/src/core/transaction.ts +9 -4
  200. package/src/core/transport.ts +98 -23
  201. package/src/formats/camt.ts +136 -39
  202. package/src/formats/mt940.ts +167 -81
  203. package/src/formats/readers.ts +24 -31
  204. package/src/formats/record-budget.ts +18 -0
  205. package/src/formats/sepa-purpose.ts +21 -2
  206. package/src/index.ts +4 -2
  207. package/src/session/app-approval.ts +51 -38
  208. package/src/session/dialog-runner.ts +31 -28
  209. package/src/session/login-guard.ts +174 -88
  210. package/src/session/sepa-accounts.ts +63 -45
  211. package/src/session/session-setup.ts +90 -71
  212. package/src/session/session-state.ts +56 -22
  213. package/src/session/signed-dialog.ts +192 -0
  214. package/src/session/tan-selection.ts +50 -39
  215. package/src/session/user-prompts.ts +35 -16
  216. package/src/tan/challenge.ts +40 -31
  217. package/src/tan/flicker.ts +44 -6
  218. package/src/tan/tan-image.ts +7 -5
  219. package/src/tan/tan-models.ts +10 -5
  220. package/src/util/async.ts +0 -5
  221. package/src/util/bytes.ts +13 -19
  222. package/src/util/call-context.ts +3 -2
  223. package/src/util/dates.ts +41 -24
  224. package/src/util/errors.ts +14 -3
  225. package/src/util/json.ts +16 -0
  226. package/src/util/text.ts +42 -12
  227. package/src/util/xml.ts +37 -26
  228. package/src/v3/account-extension.ts +7 -12
  229. package/src/v3/bank-connection.ts +67 -0
  230. package/src/v3/dialog.ts +175 -91
  231. package/src/v3/message.ts +69 -17
  232. package/src/v3/parameter-store.ts +160 -0
  233. package/src/v3/parameters.ts +110 -37
  234. package/src/v3/protocol.ts +70 -253
  235. package/src/v3/queries.ts +190 -0
  236. package/src/v3/segments.ts +65 -22
  237. package/src/v3/syntax.ts +153 -91
  238. package/dist/session/bank-connection.d.ts +0 -28
  239. package/dist/session/bank-connection.d.ts.map +0 -1
  240. package/dist/session/bank-connection.js +0 -48
  241. package/dist/session/bank-connection.js.map +0 -1
  242. package/dist/session/parameter-data.d.ts +0 -83
  243. package/dist/session/parameter-data.d.ts.map +0 -1
  244. package/dist/session/parameter-data.js +0 -68
  245. package/dist/session/parameter-data.js.map +0 -1
  246. package/dist/session/pin-tan-client.d.ts +0 -54
  247. package/dist/session/pin-tan-client.d.ts.map +0 -1
  248. package/dist/session/pin-tan-client.js +0 -153
  249. package/dist/session/pin-tan-client.js.map +0 -1
  250. package/dist/session/pin-tan-dialog.d.ts +0 -71
  251. package/dist/session/pin-tan-dialog.d.ts.map +0 -1
  252. package/dist/session/pin-tan-dialog.js +0 -124
  253. package/dist/session/pin-tan-dialog.js.map +0 -1
  254. package/dist/session/protocol.d.ts +0 -209
  255. package/dist/session/protocol.d.ts.map +0 -1
  256. package/dist/session/protocol.js +0 -75
  257. package/dist/session/protocol.js.map +0 -1
  258. package/dist/session/response-handler.d.ts +0 -48
  259. package/dist/session/response-handler.d.ts.map +0 -1
  260. package/dist/session/response-handler.js +0 -121
  261. package/dist/session/response-handler.js.map +0 -1
  262. package/dist/v3/client.d.ts +0 -24
  263. package/dist/v3/client.d.ts.map +0 -1
  264. package/dist/v3/client.js +0 -16
  265. package/dist/v3/client.js.map +0 -1
  266. package/src/session/bank-connection.ts +0 -51
  267. package/src/session/parameter-data.ts +0 -137
  268. package/src/session/pin-tan-client.ts +0 -186
  269. package/src/session/pin-tan-dialog.ts +0 -160
  270. package/src/session/protocol.ts +0 -311
  271. package/src/session/response-handler.ts +0 -130
  272. package/src/v3/client.ts +0 -29
@@ -1,112 +1,62 @@
1
1
  // Internal to the package, and named after the FinTS specification. Documentation is where the meaning is not evident.
2
2
 
3
- import type { Account } from '../core/account.js';
4
- import type { Balance } from '../core/balance.js';
5
3
  import type { MessageLogger } from '../core/client.js';
6
- import { requirePin, type FinTsConfig } from '../core/config.js';
7
- import { FinTsError, FinTsTransportError, FinTsUnsupportedError } from '../core/errors.js';
8
- import { hasErrors } from '../core/feedback.js';
9
- import type { Transaction, TransactionFormat } from '../core/transaction.js';
4
+ import { requirePinLength, type FinTsConfig } from '../core/config.js';
5
+ import { clientClosedError } from '../core/errors.js';
10
6
  import type { FinTsTransport } from '../core/transport.js';
11
- import { camtTransactions, mt940Transactions } from '../formats/readers.js';
12
- import { BankConnection, checkMessageSize } from '../session/bank-connection.js';
13
- import { LoginGuard } from '../session/login-guard.js';
14
- import { offeredVersion, requireBankParameters } from '../session/parameter-data.js';
15
- import {
16
- balanceQuery,
17
- checkBeforeSigning,
18
- chooseTransactions,
19
- type PinTanProtocol,
20
- type Query,
21
- type Synchronization,
22
- } from '../session/protocol.js';
23
- import type { TanMedium, TanMethod } from '../tan/tan-models.js';
24
- import { invalidArgument } from '../util/errors.js';
25
- import { readOptionalString, type JsonObject } from '../util/json.js';
26
7
  import { decodeBankText } from '../util/text.js';
27
- import { FinTs3Dialog, type FinTs3DialogHost, type Signing3 } from './dialog.js';
28
- import { buildMessage, decodeTransport, encodeTransport, IncomingMessage3 } from './message.js';
29
- import { Bpd3, decodeSegments, encodeSegments, Upd3 } from './parameters.js';
30
- import { balanceAccount, parseBalance, parseSepaAccounts, parseTanMedia, Segments } from './segments.js';
31
- import { isLatin1, type Segment, type SegmentSpec } from './syntax.js';
8
+ import { BankConnection, checkMessageSize } from './bank-connection.js';
9
+ import { buildMessage, decodeTransport, encodeTransport } from './message.js';
10
+ import type { ParameterStore } from './parameter-store.js';
11
+ import { Segments } from './segments.js';
12
+ import type { SegmentSpec } from './syntax.js';
32
13
 
33
14
  /** Customer ID the specification reserves for anonymous dialogs. */
34
15
  const anonymousCustomerId = '9999999999';
35
16
 
36
- /** Longest product ID (Produktbezeichnung) and product version HKVVB takes. */
37
- const maxProductIdLength = 25;
38
- const maxProductVersionLength = 5;
39
-
40
- /** A business transaction in FinTS 3.0. */
41
- export type FinTs3Query<T> = Query<IncomingMessage3, SegmentSpec, T>;
42
-
43
- function requireLatin1(text: string, name: string): string {
44
- // The message leaves out the value, which may be the PIN.
45
- if (!isLatin1(text)) throw invalidArgument(`${name} contains characters FinTS 3.0 cannot send (ISO 8859-1 only)`);
46
- return text;
17
+ /** How a message is signed: the security function of the TAN method and the customer system ID. */
18
+ export interface Signing3 {
19
+ readonly securityFunction: string;
20
+ readonly systemId: string;
47
21
  }
48
22
 
49
- function requireMaxLength(text: string, maxLength: number, name: string): void {
50
- if (text.length > maxLength) throw invalidArgument(`At most ${maxLength} characters in FinTS 3.0`, name);
51
- }
23
+ /** Hands out the PIN for each message the protocol signs, and so decides whether one may go out. */
24
+ export interface PinGuard {
25
+ /** The PIN to sign a message with; throws instead where none may go out. */
26
+ readonly pin: string;
52
27
 
53
- function requireSendable(config: FinTsConfig): FinTsConfig {
54
- requireLatin1(config.userId, 'userId');
55
- requireLatin1(config.customerId, 'customerId');
56
- requireLatin1(config.productId, 'productId');
57
- requireLatin1(config.productVersion, 'productVersion');
58
- // Cut short, the product would be one the bank does not know.
59
- requireMaxLength(config.productId, maxProductIdLength, 'productId');
60
- requireMaxLength(config.productVersion, maxProductVersionLength, 'productVersion');
61
- return config;
28
+ /** Counts a login signed with the PIN, the first message of a dialog, before it goes out. */
29
+ loginSent(): void;
62
30
  }
63
31
 
64
- /** The segments of `type` in all `pages`. */
65
- function all(pages: readonly IncomingMessage3[], type: string): Segment[] {
66
- return pages.flatMap((p) => p.findAll(type));
32
+ /** Whether `segments` are those of a dialog end, the one message a closing client still sends. */
33
+ function isDialogEnd(segments: readonly SegmentSpec[]): boolean {
34
+ return segments.length === 1 && segments[0]!.type === 'HKEND';
67
35
  }
68
36
 
69
- /** FinTS 3.0 messages and business transactions (HBCI segment syntax, PIN/TAN security). */
70
- export class FinTs3Protocol implements PinTanProtocol<IncomingMessage3, SegmentSpec>, FinTs3DialogHost {
71
- readonly config: FinTsConfig;
72
- private readonly pin: string;
37
+ /**
38
+ * Builds, signs and posts FinTS 3.0 messages (HBCI segment syntax, PIN/TAN security) for the user `parameters.config`
39
+ * names, with the parameter data and IDs `parameters` keeps. Every message signed with the PIN takes it from the
40
+ * `PinGuard` (`post`), which keeps it from going out after a refused login.
41
+ */
42
+ export class FinTs3Protocol {
43
+ private readonly config: FinTsConfig;
73
44
  private readonly connection: BankConnection;
74
- readonly loginGuard = new LoginGuard();
75
-
76
- /** Customer system ID the bank assigned in the synchronization. */
77
- systemId: string | undefined;
78
- private bpd: Bpd3 | undefined;
79
- private upd: Upd3 | undefined;
80
-
81
- /**
82
- * Throws `TypeError` if the PIN is empty, if the PIN or login data contain characters FinTS 3.0 cannot send (ISO
83
- * 8859-1 only), or the product ID or version is longer than its field in HKVVB.
84
- */
85
- constructor(config: FinTsConfig, pin: string, transport?: FinTsTransport, logger?: MessageLogger) {
86
- this.config = requireSendable(config);
87
- this.pin = requireLatin1(requirePin(pin), 'pin');
88
- this.connection = new BankConnection(config.url, transport, logger);
89
- }
90
-
91
- get bankParameters(): Bpd3 | undefined {
92
- return this.bpd;
93
- }
94
45
 
95
- get userParameters(): Upd3 | undefined {
96
- return this.upd;
97
- }
46
+ /** Whether the client is closing, so that only dialog ends go out (`beginClose`). */
47
+ private closing = false;
98
48
 
99
- /** Dialogs cannot start without the customer system ID. */
100
- get synchronization(): Synchronization {
101
- return this.systemId === undefined ? 'required' : 'none';
102
- }
103
-
104
- forgetSystemId(): boolean {
105
- const had = this.systemId !== undefined;
106
- this.systemId = undefined;
107
- return had;
49
+ constructor(
50
+ readonly parameters: ParameterStore,
51
+ private readonly guard: PinGuard,
52
+ transport?: FinTsTransport,
53
+ logger?: MessageLogger,
54
+ ) {
55
+ this.config = parameters.config;
56
+ this.connection = new BankConnection(this.config.url, transport, logger);
108
57
  }
109
58
 
59
+ /** Identification (HKIDN) and processing preparation (HKVVB), which open every dialog. */
110
60
  initSegments(systemId: string, anonymous = false): SegmentSpec[] {
111
61
  return [
112
62
  Segments.identification({
@@ -117,15 +67,21 @@ export class FinTs3Protocol implements PinTanProtocol<IncomingMessage3, SegmentS
117
67
  systemIdRequired: !anonymous,
118
68
  }),
119
69
  Segments.processingPreparation({
120
- bpdVersion: this.bpd?.version ?? 0,
121
- updVersion: anonymous ? 0 : (this.upd?.version ?? 0),
122
- language: this.bpd?.dialogLanguage ?? 0,
70
+ bpdVersion: this.parameters.reportedBpdVersion,
71
+ updVersion: anonymous ? 0 : this.parameters.reportedUpdVersion,
72
+ language: this.parameters.bankParameters?.dialogLanguage ?? 0,
123
73
  productId: this.config.productId,
124
74
  productVersion: this.config.productVersion,
125
75
  }),
126
76
  ];
127
77
  }
128
78
 
79
+ /**
80
+ * Sends `segments` as message `number` of the dialog `dialogId` and returns the bank's answer, not yet parsed.
81
+ *
82
+ * With `signing` the message is signed with the PIN, which `PinGuard.pin` hands out, and the `tan`; without, it
83
+ * goes out unsigned, as in an anonymous dialog.
84
+ */
129
85
  async post(message: {
130
86
  dialogId: string;
131
87
  number: number;
@@ -133,8 +89,10 @@ export class FinTs3Protocol implements PinTanProtocol<IncomingMessage3, SegmentS
133
89
  signing: Signing3 | undefined;
134
90
  tan: string | undefined;
135
91
  }): Promise<Uint8Array> {
92
+ if (this.closing && !isDialogEnd(message.segments)) throw clientClosedError();
136
93
  const signing = message.signing;
137
- if (signing !== undefined) checkBeforeSigning(this.loginGuard, this.pin, this.bpd);
94
+ // The first message of a signed dialog logs in: from here on the bank may count the PIN.
95
+ const login = signing !== undefined && message.number === 1;
138
96
  const outgoing = buildMessage({
139
97
  dialogId: message.dialogId,
140
98
  messageNumber: message.number,
@@ -147,182 +105,41 @@ export class FinTs3Protocol implements PinTanProtocol<IncomingMessage3, SegmentS
147
105
  bankCode: this.config.bankCode,
148
106
  userId: this.config.userId,
149
107
  systemId: signing.systemId,
150
- pin: this.pin,
108
+ pin: login ? this.pinForLogin() : this.guard.pin,
151
109
  securityFunction: signing.securityFunction,
152
110
  },
153
111
  tan: message.tan,
154
112
  });
155
- checkMessageSize(outgoing.bytes.length, this.bpd?.bankInfo.maxMessageSize);
113
+ checkMessageSize(outgoing.bytes.length, this.parameters.bankParameters?.bankInfo.maxMessageSize);
156
114
  this.connection.log('outgoing', () => outgoing.masked);
115
+ if (login) this.guard.loginSent();
157
116
  const bytes = decodeTransport(await this.connection.post(encodeTransport(outgoing.bytes), 'text/plain'));
158
117
  this.connection.log('incoming', () => decodeBankText(bytes));
159
118
  return bytes;
160
119
  }
161
120
 
162
- keepParameterData(response: IncomingMessage3): void {
163
- this.keepBankParameters(response);
164
- this.upd = Upd3.fromSegments(response.segments, this.config.bankCode, this.config.countryCode) ?? this.upd;
165
- this.systemId = response.find('HISYN')?.text(1) ?? this.systemId;
166
- }
167
-
168
- private keepBankParameters(response: IncomingMessage3): void {
169
- this.bpd = Bpd3.fromSegments(response.segments, this.config.bankCode) ?? this.bpd;
170
- }
171
-
172
- /** Keeps the bank parameter data of an anonymous dialog, and nothing that belongs to a user. */
173
- async fetchBankParametersAnonymously(): Promise<IncomingMessage3> {
174
- const response = await this.requestBankParametersAnonymously();
175
- this.keepBankParameters(response);
176
- return response;
177
- }
178
-
179
121
  /**
180
- * Asks for the bank parameter data in an anonymous dialog that announces strong customer authentication; a bank that
181
- * rejects the announcement, with a return code or by failing on it, is asked again without it.
122
+ * The PIN to sign a login with, the first message of a dialog.
182
123
  *
183
- * A timeout or a broken connection is not a reason to ask again: it would only double the wait.
124
+ * Throws instead where the guard holds the PIN back (`PinGuard.pin`), or if the bank parameters rule out the PIN's
125
+ * length: the bank would count such a PIN as a failed attempt. The later messages of a dialog, its end included, take
126
+ * the PIN the bank has accepted, whatever the parameter data it delivered since say.
184
127
  */
185
- private async requestBankParametersAnonymously(): Promise<IncomingMessage3> {
186
- let announced: IncomingMessage3;
187
- try {
188
- announced = await this.anonymousDialog(true);
189
- } catch (error) {
190
- if (error instanceof FinTsTransportError && error.statusCode === undefined) throw error;
191
- if (!(error instanceof FinTsError)) throw error;
192
- return this.anonymousDialog(false);
193
- }
194
- if (!hasErrors(announced.feedback) || announced.find('HIBPA') !== undefined) return announced;
195
- return this.anonymousDialog(false);
196
- }
197
-
198
- private async anonymousDialog(announceSca: boolean): Promise<IncomingMessage3> {
199
- const dialog = FinTs3Dialog.anonymous(this);
200
- try {
201
- return await dialog.requestBankParameters(announceSca);
202
- } finally {
203
- await dialog.end();
204
- }
205
- }
206
-
207
- newDialog(method: TanMethod, tanMedium?: string): FinTs3Dialog {
208
- return FinTs3Dialog.signed(this, method, tanMedium);
209
- }
210
-
211
- combine(first: IncomingMessage3, second: IncomingMessage3): IncomingMessage3 {
212
- return IncomingMessage3.combine(first, second);
213
- }
214
-
215
- // --- Business transactions ---
216
-
217
- private version(
218
- transaction: string,
219
- implemented: readonly number[],
220
- options: { account?: Account; requiresEntry?: boolean } = {},
221
- ): number | undefined {
222
- return offeredVersion(this.bpd, this.upd, transaction, new Set(implemented), options);
223
- }
224
-
225
- tanMedia(): FinTs3Query<TanMedium[]> | undefined {
226
- const version = this.version('HKTAB', [4, 5]);
227
- if (version === undefined) return undefined;
228
- return {
229
- order: () => Segments.tanMedia(version),
230
- read: (pages) => all(pages, 'HITAB').flatMap(parseTanMedia),
231
- };
232
- }
233
-
234
- sepaAccounts(): FinTs3Query<Account[]> | undefined {
235
- const version = this.version('HKSPA', [1, 2, 3], { requiresEntry: true });
236
- if (version === undefined) return undefined;
237
- return {
238
- order: (continuation) => Segments.sepaAccounts(version, continuation),
239
- read: (pages) =>
240
- all(pages, 'HISPA').flatMap((s) => parseSepaAccounts(s, this.config.countryCode, this.config.bankCode)),
241
- };
128
+ private pinForLogin(): string {
129
+ const pin = this.guard.pin;
130
+ const length = this.parameters.bankParameters?.pinTan.pinLength;
131
+ requirePinLength(pin, length?.min, length?.max);
132
+ return pin;
242
133
  }
243
134
 
244
- balance(account: Account): FinTs3Query<Balance> {
245
- const version = this.version('HKSAL', [6, 7, 8], { account });
246
- if (version === undefined) {
247
- throw new FinTsUnsupportedError('The bank reports no balance (HKSAL) for this account');
248
- }
249
- return balanceQuery<IncomingMessage3, SegmentSpec, Segment>(account, {
250
- order: (continuation) => Segments.balance(version, account, continuation),
251
- records: (pages) => all(pages, 'HISAL'),
252
- accountOf: balanceAccount,
253
- read: parseBalance,
254
- });
255
- }
256
-
257
- transactions(
258
- account: Account,
259
- format: TransactionFormat,
260
- from: Date | undefined,
261
- to: Date | undefined,
262
- ): FinTs3Query<Transaction[]> {
263
- const request = chooseTransactions(format, account, this.upd, {
264
- camt: { name: 'HKCAZ', version: this.version('HKCAZ', [1], { account }) },
265
- camtFormats: requireBankParameters(this.bpd).camtFormats,
266
- mt940: { name: 'HKKAZ', version: this.version('HKKAZ', [6, 7], { account }) },
267
- });
268
- const version = request.version;
269
- if (request.camt) {
270
- return {
271
- order: (continuation) =>
272
- Segments.transactionsCamt(version, account, request.camtFormats, { from, to, continuation }),
273
- // The third element group holds the booked documents, the fourth element the pending one.
274
- read: (pages) =>
275
- camtTransactions(
276
- all(pages, 'HICAZ').flatMap((s) =>
277
- s
278
- .group(3)
279
- .filter((d) => !d.isEmpty)
280
- .map((d) => d.bytes),
281
- ),
282
- all(pages, 'HICAZ')
283
- .map((s) => s.binary(4))
284
- .filter((d) => d !== undefined),
285
- ),
286
- };
287
- }
288
- return {
289
- order: (continuation) => Segments.transactionsMt940(version, account, { from, to, continuation }),
290
- // The first element holds the booked transactions, the second the pending ones.
291
- read: (pages) =>
292
- mt940Transactions(
293
- all(pages, 'HIKAZ')
294
- .map((s) => s.binary(1))
295
- .filter((d) => d !== undefined),
296
- all(pages, 'HIKAZ')
297
- .map((s) => s.binary(2))
298
- .filter((d) => d !== undefined),
299
- request.currency,
300
- ),
301
- };
302
- }
303
-
304
- // --- Saved state ---
305
-
306
- saveState(): JsonObject {
307
- return {
308
- systemId: this.systemId ?? null,
309
- bpd: this.bpd === undefined ? null : encodeSegments(this.bpd.segments),
310
- upd: this.upd === undefined ? null : encodeSegments(this.upd.segments),
311
- };
312
- }
313
-
314
- restoreState(saved: JsonObject): void {
315
- const bpd = readOptionalString(saved, 'bpd');
316
- const upd = readOptionalString(saved, 'upd');
317
- const restoredSystemId = readOptionalString(saved, 'systemId');
318
- const restoredBpd = bpd === undefined ? undefined : Bpd3.fromSegments(decodeSegments(bpd), this.config.bankCode);
319
- const restoredUpd =
320
- upd === undefined
321
- ? undefined
322
- : Upd3.fromSegments(decodeSegments(upd), this.config.bankCode, this.config.countryCode);
323
- this.systemId = restoredSystemId;
324
- this.bpd = restoredBpd;
325
- this.upd = restoredUpd;
135
+ /**
136
+ * Aborts the request on its way, and sends nothing but dialog ends from now on: any other message fails with
137
+ * `FinTsStateError` before it goes out, so that a call under way stops at its next one. The client calls it as soon as
138
+ * it is closed, and `close` once it has ended its dialog.
139
+ */
140
+ beginClose(): void {
141
+ this.closing = true;
142
+ this.connection.abortRequests();
326
143
  }
327
144
 
328
145
  close(): void {
@@ -0,0 +1,190 @@
1
+ // Internal to the package, and named after the FinTS specification. Documentation is where the meaning is not evident.
2
+
3
+ import type { Account } from '../core/account.js';
4
+ import type { Balance } from '../core/balance.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 { camtTransactions, mt940Transactions } from '../formats/readers.js';
9
+ import type { TanMedium } from '../tan/tan-models.js';
10
+ import type { IncomingMessage3 } from './message.js';
11
+ import type { ParameterStore } from './parameter-store.js';
12
+ import { lists, offeredVersion, requireBankParameters } from './parameters.js';
13
+ import { balanceAccount, camtAccount, parseBalance, parseSepaAccounts, parseTanMedia, Segments } from './segments.js';
14
+ import type { Segment, SegmentSpec } from './syntax.js';
15
+
16
+ /**
17
+ * A business transaction: the order to send for each page, and how to read all pages the bank sent.
18
+ *
19
+ * Where the bank does not offer the transaction, the builders of the lookups the session makes for itself, of the TAN
20
+ * media and the SEPA accounts, return `undefined`, and those of the app's queries, of a balance and of transactions,
21
+ * throw `FinTsUnsupportedError`.
22
+ */
23
+ export interface Query<T> {
24
+ /** The order for the page at `continuation`; `undefined` for the first page. */
25
+ readonly order: (continuation: string | undefined) => SegmentSpec;
26
+
27
+ /** Reads the result; throws `FormatError` for malformed data. */
28
+ readonly read: (pages: readonly IncomingMessage3[]) => T;
29
+ }
30
+
31
+ /** The segments of `type` in all `pages`. */
32
+ function all(pages: readonly IncomingMessage3[], type: string): Segment[] {
33
+ return pages.flatMap((p) => p.findAll(type));
34
+ }
35
+
36
+ function version(
37
+ parameters: ParameterStore,
38
+ transaction: string,
39
+ implemented: readonly number[],
40
+ options: { account?: Account; requiresEntry?: boolean } = {},
41
+ ): number | undefined {
42
+ return offeredVersion(
43
+ parameters.bankParameters,
44
+ parameters.userParameters,
45
+ transaction,
46
+ new Set(implemented),
47
+ options,
48
+ );
49
+ }
50
+
51
+ /** Lists the TAN media, and keeps the names as the bank sent them in `parameters`; `undefined` if the bank does not. */
52
+ export function tanMediaQuery(parameters: ParameterStore): Query<TanMedium[]> | undefined {
53
+ const offered = version(parameters, 'HKTAB', [4, 5]);
54
+ if (offered === undefined) return undefined;
55
+ return {
56
+ order: () => Segments.tanMedia(offered),
57
+ read: (pages) => {
58
+ const names = new Map<string, string>();
59
+ const media = all(pages, 'HITAB').flatMap((s) => parseTanMedia(s, names));
60
+ parameters.keepTanMediumNames(names);
61
+ return media;
62
+ },
63
+ };
64
+ }
65
+
66
+ /** Lists IBAN and BIC of the accounts; `undefined` if the bank does not offer it. */
67
+ export function sepaAccountsQuery(parameters: ParameterStore): Query<Account[]> | undefined {
68
+ const offered = version(parameters, 'HKSPA', [1, 2, 3], { requiresEntry: true });
69
+ if (offered === undefined) return undefined;
70
+ const { countryCode, bankCode } = parameters.config;
71
+ return {
72
+ order: (continuation) => Segments.sepaAccounts(offered, continuation),
73
+ read: (pages) => all(pages, 'HISPA').flatMap((s) => parseSepaAccounts(s, countryCode, bankCode)),
74
+ };
75
+ }
76
+
77
+ /**
78
+ * The balance (HISAL) of `account` in `records`: the one whose account matches. A single record must match as well,
79
+ * though the query asked for this one account only: it names the account it is for.
80
+ *
81
+ * Throws `FinTsBankError` with the bank's `feedback` if there is none, and `FinTsProtocolError` if none matches.
82
+ *
83
+ * @internal Exported for the tests.
84
+ */
85
+ export function balanceRecord(
86
+ records: readonly Segment[],
87
+ account: Account,
88
+ feedback: readonly FinTsFeedback[],
89
+ ): Segment {
90
+ if (records.length === 0) throw new FinTsBankError(feedback, 'No balance returned');
91
+ const match = records.find((record) => balanceAccount(record).matches(account));
92
+ if (match === undefined) {
93
+ throw new FinTsProtocolError('The bank returned balances, but none for the requested account');
94
+ }
95
+ return match;
96
+ }
97
+
98
+ /**
99
+ * The balance of `account`, which the session has taken as the bank knows it, with the transactions the bank permits
100
+ * for it (`SepaAccounts.complete`); throws `FinTsUnsupportedError` if the bank does not report balances.
101
+ */
102
+ export function balanceQuery(parameters: ParameterStore, account: Account): Query<Balance> {
103
+ const offered = version(parameters, 'HKSAL', [6, 7, 8], { account });
104
+ if (offered === undefined) {
105
+ throw new FinTsUnsupportedError('The bank reports no balance (HKSAL) for this account');
106
+ }
107
+ return {
108
+ order: (continuation) => Segments.balance(offered, account, continuation),
109
+ read: (pages) =>
110
+ parseBalance(
111
+ balanceRecord(
112
+ all(pages, 'HISAL'),
113
+ account,
114
+ pages.flatMap((p) => p.feedback),
115
+ ),
116
+ account,
117
+ ),
118
+ };
119
+ }
120
+
121
+ /**
122
+ * The transactions of `account` in `format`, from the day of `from` to that of `to`. The session has taken `account` as
123
+ * the bank knows it, with the transactions the bank permits for it and its currency (`SepaAccounts.complete`).
124
+ *
125
+ * The client reads camt.052 only, so camt counts as offered only where the bank offers a camt.052 format. With `auto`
126
+ * the client takes camt where the bank offers it, unless the user parameter data list only MT940 transactions for the
127
+ * account.
128
+ *
129
+ * Throws `FinTsUnsupportedError` if the bank does not offer the chosen format for the account.
130
+ */
131
+ export function transactionsQuery(
132
+ parameters: ParameterStore,
133
+ account: Account,
134
+ format: TransactionFormat,
135
+ from: Date | undefined,
136
+ to: Date | undefined,
137
+ ): Query<Transaction[]> {
138
+ const camtFormats = requireBankParameters(parameters.bankParameters).camtFormats.filter((f) =>
139
+ f.includes('camt.052'),
140
+ );
141
+ const camtVersion = camtFormats.length === 0 ? undefined : version(parameters, 'HKCAZ', [1], { account });
142
+ const camt =
143
+ format === 'camt' ||
144
+ (format === 'auto' && camtVersion !== undefined && (lists(account, 'HKCAZ') || !lists(account, 'HKKAZ')));
145
+ const offered = camt ? camtVersion : version(parameters, 'HKKAZ', [6, 7], { account });
146
+ if (offered === undefined) {
147
+ throw new FinTsUnsupportedError(
148
+ `The bank offers no ${camt ? 'camt ' : ''}transactions (${camt ? 'HKCAZ' : 'HKKAZ'}) for this account`,
149
+ );
150
+ }
151
+ if (camt) {
152
+ return {
153
+ order: (continuation) => Segments.transactionsCamt(offered, account, camtFormats, { from, to, continuation }),
154
+ // The first element names the account, the third element group holds the booked documents, the fourth
155
+ // element the pending one.
156
+ read: (pages) => {
157
+ const segments = all(pages, 'HICAZ');
158
+ if (!segments.every((s) => camtAccount(s).matches(account))) {
159
+ throw new FinTsProtocolError('The bank returned the transactions of another account than the one asked for');
160
+ }
161
+ return camtTransactions(
162
+ segments.flatMap((s) =>
163
+ s
164
+ .group(3)
165
+ .filter((d) => !d.isEmpty)
166
+ .map((d) => d.bytes),
167
+ ),
168
+ segments.map((s) => s.binary(4)).filter((d) => d !== undefined),
169
+ );
170
+ },
171
+ };
172
+ }
173
+ return {
174
+ order: (continuation) => Segments.transactionsMt940(offered, account, { from, to, continuation }),
175
+ // The first element holds the booked transactions, the second the pending ones. Unlike HICAZ, HIKAZ names no
176
+ // account, and banks write the account line of a statement (`:25:`) in many ways, so it is not compared with
177
+ // `account`: a strict comparison would fail every query of such a bank.
178
+ read: (pages) =>
179
+ mt940Transactions(
180
+ all(pages, 'HIKAZ')
181
+ .map((s) => s.binary(1))
182
+ .filter((d) => d !== undefined),
183
+ all(pages, 'HIKAZ')
184
+ .map((s) => s.binary(2))
185
+ .filter((d) => d !== undefined),
186
+ // The currency of statements that state none.
187
+ account.currency,
188
+ ),
189
+ };
190
+ }