@coderoftime/fints 0.2.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 (215) hide show
  1. package/CHANGELOG.md +377 -0
  2. package/README.md +31 -23
  3. package/dist/client.d.ts +3 -2
  4. package/dist/client.d.ts.map +1 -1
  5. package/dist/client.js +52 -9
  6. package/dist/client.js.map +1 -1
  7. package/dist/core/account.d.ts +15 -4
  8. package/dist/core/account.d.ts.map +1 -1
  9. package/dist/core/account.js +84 -36
  10. package/dist/core/account.js.map +1 -1
  11. package/dist/core/amount.d.ts +13 -3
  12. package/dist/core/amount.d.ts.map +1 -1
  13. package/dist/core/amount.js +42 -8
  14. package/dist/core/amount.js.map +1 -1
  15. package/dist/core/balance.d.ts +3 -0
  16. package/dist/core/balance.d.ts.map +1 -1
  17. package/dist/core/balance.js +5 -0
  18. package/dist/core/balance.js.map +1 -1
  19. package/dist/core/bank-info.d.ts +20 -3
  20. package/dist/core/bank-info.d.ts.map +1 -1
  21. package/dist/core/bank-info.js +25 -4
  22. package/dist/core/bank-info.js.map +1 -1
  23. package/dist/core/client.d.ts +42 -15
  24. package/dist/core/client.d.ts.map +1 -1
  25. package/dist/core/config.d.ts +15 -7
  26. package/dist/core/config.d.ts.map +1 -1
  27. package/dist/core/config.js +29 -14
  28. package/dist/core/config.js.map +1 -1
  29. package/dist/core/errors.d.ts +17 -5
  30. package/dist/core/errors.d.ts.map +1 -1
  31. package/dist/core/errors.js +25 -7
  32. package/dist/core/errors.js.map +1 -1
  33. package/dist/core/feedback.d.ts +7 -0
  34. package/dist/core/feedback.d.ts.map +1 -1
  35. package/dist/core/feedback.js +10 -1
  36. package/dist/core/feedback.js.map +1 -1
  37. package/dist/core/transaction.d.ts +5 -0
  38. package/dist/core/transaction.d.ts.map +1 -1
  39. package/dist/core/transaction.js +12 -0
  40. package/dist/core/transaction.js.map +1 -1
  41. package/dist/core/transport.d.ts +22 -7
  42. package/dist/core/transport.d.ts.map +1 -1
  43. package/dist/core/transport.js +51 -11
  44. package/dist/core/transport.js.map +1 -1
  45. package/dist/formats/camt.d.ts +7 -4
  46. package/dist/formats/camt.d.ts.map +1 -1
  47. package/dist/formats/camt.js +53 -54
  48. package/dist/formats/camt.js.map +1 -1
  49. package/dist/formats/mt940.d.ts +18 -6
  50. package/dist/formats/mt940.d.ts.map +1 -1
  51. package/dist/formats/mt940.js +83 -66
  52. package/dist/formats/mt940.js.map +1 -1
  53. package/dist/formats/readers.d.ts.map +1 -1
  54. package/dist/formats/readers.js +6 -2
  55. package/dist/formats/readers.js.map +1 -1
  56. package/dist/formats/sepa-purpose.d.ts +7 -4
  57. package/dist/formats/sepa-purpose.d.ts.map +1 -1
  58. package/dist/formats/sepa-purpose.js +18 -9
  59. package/dist/formats/sepa-purpose.js.map +1 -1
  60. package/dist/session/dialog-runner.d.ts +5 -2
  61. package/dist/session/dialog-runner.d.ts.map +1 -1
  62. package/dist/session/dialog-runner.js +23 -7
  63. package/dist/session/dialog-runner.js.map +1 -1
  64. package/dist/session/login-guard.d.ts +4 -4
  65. package/dist/session/login-guard.d.ts.map +1 -1
  66. package/dist/session/login-guard.js +29 -14
  67. package/dist/session/login-guard.js.map +1 -1
  68. package/dist/session/session-setup.d.ts +14 -5
  69. package/dist/session/session-setup.d.ts.map +1 -1
  70. package/dist/session/session-setup.js +30 -9
  71. package/dist/session/session-setup.js.map +1 -1
  72. package/dist/session/session-state.d.ts +4 -3
  73. package/dist/session/session-state.d.ts.map +1 -1
  74. package/dist/session/session-state.js +28 -9
  75. package/dist/session/session-state.js.map +1 -1
  76. package/dist/session/signed-dialog.d.ts +19 -13
  77. package/dist/session/signed-dialog.d.ts.map +1 -1
  78. package/dist/session/signed-dialog.js +45 -30
  79. package/dist/session/signed-dialog.js.map +1 -1
  80. package/dist/session/user-prompts.d.ts +6 -1
  81. package/dist/session/user-prompts.d.ts.map +1 -1
  82. package/dist/session/user-prompts.js +19 -4
  83. package/dist/session/user-prompts.js.map +1 -1
  84. package/dist/tan/challenge.d.ts +15 -13
  85. package/dist/tan/challenge.d.ts.map +1 -1
  86. package/dist/tan/challenge.js +32 -26
  87. package/dist/tan/challenge.js.map +1 -1
  88. package/dist/tan/flicker.d.ts +5 -1
  89. package/dist/tan/flicker.d.ts.map +1 -1
  90. package/dist/tan/flicker.js +42 -30
  91. package/dist/tan/flicker.js.map +1 -1
  92. package/dist/tan/tan-image.d.ts +4 -1
  93. package/dist/tan/tan-image.d.ts.map +1 -1
  94. package/dist/tan/tan-image.js +8 -2
  95. package/dist/tan/tan-image.js.map +1 -1
  96. package/dist/tan/tan-models.d.ts +9 -1
  97. package/dist/tan/tan-models.d.ts.map +1 -1
  98. package/dist/tan/tan-models.js +14 -2
  99. package/dist/tan/tan-models.js.map +1 -1
  100. package/dist/util/arguments.d.ts +13 -0
  101. package/dist/util/arguments.d.ts.map +1 -0
  102. package/dist/util/arguments.js +40 -0
  103. package/dist/util/arguments.js.map +1 -0
  104. package/dist/util/async.d.ts +4 -1
  105. package/dist/util/async.d.ts.map +1 -1
  106. package/dist/util/async.js +6 -3
  107. package/dist/util/async.js.map +1 -1
  108. package/dist/util/bytes.d.ts +1 -1
  109. package/dist/util/bytes.d.ts.map +1 -1
  110. package/dist/util/bytes.js +3 -3
  111. package/dist/util/bytes.js.map +1 -1
  112. package/dist/util/dates.d.ts +3 -10
  113. package/dist/util/dates.d.ts.map +1 -1
  114. package/dist/util/dates.js +49 -39
  115. package/dist/util/dates.js.map +1 -1
  116. package/dist/util/errors.d.ts +5 -3
  117. package/dist/util/errors.d.ts.map +1 -1
  118. package/dist/util/errors.js +8 -4
  119. package/dist/util/errors.js.map +1 -1
  120. package/dist/util/immutable-map.d.ts +5 -2
  121. package/dist/util/immutable-map.d.ts.map +1 -1
  122. package/dist/util/immutable-map.js +15 -12
  123. package/dist/util/immutable-map.js.map +1 -1
  124. package/dist/util/inspect.d.ts +11 -0
  125. package/dist/util/inspect.d.ts.map +1 -0
  126. package/dist/util/inspect.js +14 -0
  127. package/dist/util/inspect.js.map +1 -0
  128. package/dist/util/text.d.ts +18 -2
  129. package/dist/util/text.d.ts.map +1 -1
  130. package/dist/util/text.js +50 -5
  131. package/dist/util/text.js.map +1 -1
  132. package/dist/util/xml.d.ts +6 -4
  133. package/dist/util/xml.d.ts.map +1 -1
  134. package/dist/util/xml.js +106 -44
  135. package/dist/util/xml.js.map +1 -1
  136. package/dist/v3/bank-connection.d.ts +23 -3
  137. package/dist/v3/bank-connection.d.ts.map +1 -1
  138. package/dist/v3/bank-connection.js +66 -6
  139. package/dist/v3/bank-connection.js.map +1 -1
  140. package/dist/v3/dialog.d.ts +6 -2
  141. package/dist/v3/dialog.d.ts.map +1 -1
  142. package/dist/v3/dialog.js +20 -9
  143. package/dist/v3/dialog.js.map +1 -1
  144. package/dist/v3/message.d.ts +18 -10
  145. package/dist/v3/message.d.ts.map +1 -1
  146. package/dist/v3/message.js +92 -75
  147. package/dist/v3/message.js.map +1 -1
  148. package/dist/v3/parameter-store.d.ts +43 -7
  149. package/dist/v3/parameter-store.d.ts.map +1 -1
  150. package/dist/v3/parameter-store.js +69 -15
  151. package/dist/v3/parameter-store.js.map +1 -1
  152. package/dist/v3/parameters.d.ts +12 -11
  153. package/dist/v3/parameters.d.ts.map +1 -1
  154. package/dist/v3/parameters.js +57 -35
  155. package/dist/v3/parameters.js.map +1 -1
  156. package/dist/v3/protocol.d.ts +16 -9
  157. package/dist/v3/protocol.d.ts.map +1 -1
  158. package/dist/v3/protocol.js +57 -26
  159. package/dist/v3/protocol.js.map +1 -1
  160. package/dist/v3/queries.d.ts +2 -2
  161. package/dist/v3/queries.d.ts.map +1 -1
  162. package/dist/v3/queries.js +24 -16
  163. package/dist/v3/queries.js.map +1 -1
  164. package/dist/v3/segments.d.ts +18 -11
  165. package/dist/v3/segments.d.ts.map +1 -1
  166. package/dist/v3/segments.js +30 -21
  167. package/dist/v3/segments.js.map +1 -1
  168. package/dist/v3/syntax.d.ts +15 -11
  169. package/dist/v3/syntax.d.ts.map +1 -1
  170. package/dist/v3/syntax.js +30 -15
  171. package/dist/v3/syntax.js.map +1 -1
  172. package/package.json +1 -1
  173. package/src/client.ts +58 -17
  174. package/src/core/account.ts +91 -35
  175. package/src/core/amount.ts +42 -9
  176. package/src/core/balance.ts +6 -0
  177. package/src/core/bank-info.ts +28 -4
  178. package/src/core/client.ts +42 -15
  179. package/src/core/config.ts +37 -18
  180. package/src/core/errors.ts +27 -7
  181. package/src/core/feedback.ts +12 -1
  182. package/src/core/transaction.ts +13 -0
  183. package/src/core/transport.ts +62 -16
  184. package/src/formats/camt.ts +62 -51
  185. package/src/formats/mt940.ts +114 -78
  186. package/src/formats/readers.ts +6 -2
  187. package/src/formats/sepa-purpose.ts +17 -9
  188. package/src/session/dialog-runner.ts +34 -7
  189. package/src/session/login-guard.ts +29 -13
  190. package/src/session/session-setup.ts +27 -9
  191. package/src/session/session-state.ts +25 -8
  192. package/src/session/signed-dialog.ts +60 -30
  193. package/src/session/user-prompts.ts +28 -3
  194. package/src/tan/challenge.ts +50 -36
  195. package/src/tan/flicker.ts +40 -29
  196. package/src/tan/tan-image.ts +9 -2
  197. package/src/tan/tan-models.ts +16 -2
  198. package/src/util/arguments.ts +40 -0
  199. package/src/util/async.ts +10 -4
  200. package/src/util/bytes.ts +3 -3
  201. package/src/util/dates.ts +52 -38
  202. package/src/util/errors.ts +9 -4
  203. package/src/util/immutable-map.ts +15 -12
  204. package/src/util/inspect.ts +14 -0
  205. package/src/util/text.ts +47 -5
  206. package/src/util/xml.ts +105 -38
  207. package/src/v3/bank-connection.ts +65 -6
  208. package/src/v3/dialog.ts +18 -8
  209. package/src/v3/message.ts +81 -72
  210. package/src/v3/parameter-store.ts +82 -16
  211. package/src/v3/parameters.ts +68 -41
  212. package/src/v3/protocol.ts +61 -28
  213. package/src/v3/queries.ts +26 -18
  214. package/src/v3/segments.ts +32 -20
  215. package/src/v3/syntax.ts +31 -14
@@ -7,13 +7,19 @@ import type { FinTsTransport } from '../core/transport.js';
7
7
  import { decodeBankText } from '../util/text.js';
8
8
  import { BankConnection, checkMessageSize } from './bank-connection.js';
9
9
  import { buildMessage, decodeTransport, encodeTransport } from './message.js';
10
- import type { ParameterStore } from './parameter-store.js';
10
+ import type { MessageLimits, ParameterStore } from './parameter-store.js';
11
11
  import { Segments } from './segments.js';
12
12
  import type { SegmentSpec } from './syntax.js';
13
13
 
14
14
  /** Customer ID the specification reserves for anonymous dialogs. */
15
15
  const anonymousCustomerId = '9999999999';
16
16
 
17
+ /**
18
+ * How long the request on its way when the client is closed may take, and then its dialog ends, where the transport
19
+ * takes the abort (`beginClose`).
20
+ */
21
+ const closingGraceMs = 5000;
22
+
17
23
  /** How a message is signed: the security function of the TAN method and the customer system ID. */
18
24
  export interface Signing3 {
19
25
  readonly securityFunction: string;
@@ -29,6 +35,15 @@ export interface PinGuard {
29
35
  loginSent(): void;
30
36
  }
31
37
 
38
+ /**
39
+ * Throws if `limits` rule out a message of `size` bytes, or the length of `loginPin`, the PIN of a login: `TypeError`
40
+ * for the PIN, which the bank would count as a failed attempt, and `FinTsProtocolError` for the size.
41
+ */
42
+ function checkLimits(limits: MessageLimits, size: number, loginPin: string | undefined): void {
43
+ if (loginPin !== undefined) requirePinLength(loginPin, limits.pinLength?.min, limits.pinLength?.max);
44
+ checkMessageSize(size, limits.maxMessageSize);
45
+ }
46
+
32
47
  /** Whether `segments` are those of a dialog end, the one message a closing client still sends. */
33
48
  function isDialogEnd(segments: readonly SegmentSpec[]): boolean {
34
49
  return segments.length === 1 && segments[0]!.type === 'HKEND';
@@ -43,9 +58,11 @@ export class FinTs3Protocol {
43
58
  private readonly config: FinTsConfig;
44
59
  private readonly connection: BankConnection;
45
60
 
46
- /** Whether the client is closing, so that only dialog ends go out (`beginClose`). */
47
61
  private closing = false;
48
62
 
63
+ /** The error with which the limits last stopped a login (`stoppedLogin`). */
64
+ private stoppedLoginError: unknown;
65
+
49
66
  constructor(
50
67
  readonly parameters: ParameterStore,
51
68
  private readonly guard: PinGuard,
@@ -91,26 +108,28 @@ export class FinTs3Protocol {
91
108
  }): Promise<Uint8Array> {
92
109
  if (this.closing && !isDialogEnd(message.segments)) throw clientClosedError();
93
110
  const signing = message.signing;
111
+ const security =
112
+ signing === undefined
113
+ ? undefined
114
+ : {
115
+ countryCode: this.config.countryCode,
116
+ bankCode: this.config.bankCode,
117
+ userId: this.config.userId,
118
+ systemId: signing.systemId,
119
+ pin: this.guard.pin,
120
+ securityFunction: signing.securityFunction,
121
+ };
94
122
  // 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;
123
+ const login = security !== undefined && message.number === 1;
96
124
  const outgoing = buildMessage({
97
125
  dialogId: message.dialogId,
98
126
  messageNumber: message.number,
99
127
  segments: message.segments,
100
- security:
101
- signing === undefined
102
- ? undefined
103
- : {
104
- countryCode: this.config.countryCode,
105
- bankCode: this.config.bankCode,
106
- userId: this.config.userId,
107
- systemId: signing.systemId,
108
- pin: login ? this.pinForLogin() : this.guard.pin,
109
- securityFunction: signing.securityFunction,
110
- },
128
+ security,
111
129
  tan: message.tan,
112
130
  });
113
- checkMessageSize(outgoing.bytes.length, this.parameters.bankParameters?.bankInfo.maxMessageSize);
131
+ this.checkOutgoing(outgoing.bytes.length, login ? security.pin : undefined);
132
+ this.connection.checkSendable();
114
133
  this.connection.log('outgoing', () => outgoing.masked);
115
134
  if (login) this.guard.loginSent();
116
135
  const bytes = decodeTransport(await this.connection.post(encodeTransport(outgoing.bytes), 'text/plain'));
@@ -119,27 +138,41 @@ export class FinTs3Protocol {
119
138
  }
120
139
 
121
140
  /**
122
- * The PIN to sign a login with, the first message of a dialog.
141
+ * Throws if the limits of the parameter data rule out a message of `size` bytes, or the length of `loginPin`, the PIN
142
+ * of a login (`checkLimits`). The later messages of a dialog, its end included, take the PIN the bank has accepted,
143
+ * whatever the parameter data it delivered since say.
123
144
  *
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.
145
+ * Remembers the error that stops a login (`stoppedLogin`): the parameter data may be outdated, as saved ones are,
146
+ * and new ones would come only in answer to the login they rule out.
127
147
  */
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;
148
+ private checkOutgoing(size: number, loginPin: string | undefined): void {
149
+ try {
150
+ checkLimits(this.parameters.limits, size, loginPin);
151
+ } catch (error) {
152
+ if (loginPin !== undefined) this.stoppedLoginError = error;
153
+ throw error;
154
+ }
155
+ }
156
+
157
+ /** Whether `error` is the one with which the limits of the parameter data last stopped a login. */
158
+ stoppedLogin(error: unknown): boolean {
159
+ return this.stoppedLoginError !== undefined && this.stoppedLoginError === error;
160
+ }
161
+
162
+ /** Whether the client is closing, so that only dialog ends go out (`beginClose`). */
163
+ get isClosing(): boolean {
164
+ return this.closing;
133
165
  }
134
166
 
135
167
  /**
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.
168
+ * Sends nothing but dialog ends from now on: any other message fails with `FinTsStateError` before it goes out, so
169
+ * that a call under way stops at its next one. The request on its way has `closingGraceMs` to bring its answer, which
170
+ * counts as any other, and is aborted then; the dialog ends have as long again once it is over, and none goes out
171
+ * later. The client calls it as soon as it is closed, and `close` once it has ended its dialog.
139
172
  */
140
173
  beginClose(): void {
141
174
  this.closing = true;
142
- this.connection.abortRequests();
175
+ this.connection.abortRequestsAfter(closingGraceMs);
143
176
  }
144
177
 
145
178
  close(): void {
package/src/v3/queries.ts CHANGED
@@ -104,8 +104,9 @@ export function balanceQuery(parameters: ParameterStore, account: Account): Quer
104
104
  if (offered === undefined) {
105
105
  throw new FinTsUnsupportedError('The bank reports no balance (HKSAL) for this account');
106
106
  }
107
+ const nationalAllowed = requireBankParameters(parameters.bankParameters).nationalAccountAllowed;
107
108
  return {
108
- order: (continuation) => Segments.balance(offered, account, continuation),
109
+ order: (continuation) => Segments.balance(offered, account, nationalAllowed, continuation),
109
110
  read: (pages) =>
110
111
  parseBalance(
111
112
  balanceRecord(
@@ -123,8 +124,8 @@ export function balanceQuery(parameters: ParameterStore, account: Account): Quer
123
124
  * the bank knows it, with the transactions the bank permits for it and its currency (`SepaAccounts.complete`).
124
125
  *
125
126
  * 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.
127
+ * the client takes camt where the bank offers it, unless MT940 can be requested for the account and the user parameter
128
+ * data list only MT940 transactions for it.
128
129
  *
129
130
  * Throws `FinTsUnsupportedError` if the bank does not offer the chosen format for the account.
130
131
  */
@@ -135,14 +136,15 @@ export function transactionsQuery(
135
136
  from: Date | undefined,
136
137
  to: Date | undefined,
137
138
  ): 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 });
139
+ const bpd = requireBankParameters(parameters.bankParameters);
140
+ const camtVersion = bpd.camtFormats.length === 0 ? undefined : version(parameters, 'HKCAZ', [1], { account });
141
+ const mt940Version = version(parameters, 'HKKAZ', [6, 7], { account });
142
142
  const camt =
143
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 });
144
+ (format === 'auto' &&
145
+ camtVersion !== undefined &&
146
+ (mt940Version === undefined || lists(account, 'HKCAZ') || !lists(account, 'HKKAZ')));
147
+ const offered = camt ? camtVersion : mt940Version;
146
148
  if (offered === undefined) {
147
149
  throw new FinTsUnsupportedError(
148
150
  `The bank offers no ${camt ? 'camt ' : ''}transactions (${camt ? 'HKCAZ' : 'HKKAZ'}) for this account`,
@@ -150,9 +152,14 @@ export function transactionsQuery(
150
152
  }
151
153
  if (camt) {
152
154
  return {
153
- order: (continuation) => Segments.transactionsCamt(offered, account, camtFormats, { from, to, continuation }),
155
+ order: (continuation) =>
156
+ Segments.transactionsCamt(offered, account, bpd.nationalAccountAllowed, bpd.camtFormats, {
157
+ from,
158
+ to,
159
+ continuation,
160
+ }),
154
161
  // The first element names the account, the third element group holds the booked documents, the fourth
155
- // element the pending one.
162
+ // element the pending one: binary data, which a bank may send as text instead.
156
163
  read: (pages) => {
157
164
  const segments = all(pages, 'HICAZ');
158
165
  if (!segments.every((s) => camtAccount(s).matches(account))) {
@@ -165,23 +172,24 @@ export function transactionsQuery(
165
172
  .filter((d) => !d.isEmpty)
166
173
  .map((d) => d.bytes),
167
174
  ),
168
- segments.map((s) => s.binary(4)).filter((d) => d !== undefined),
175
+ segments.map((s) => s.bytes(4)).filter((d) => d !== undefined),
169
176
  );
170
177
  },
171
178
  };
172
179
  }
173
180
  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.
181
+ order: (continuation) =>
182
+ Segments.transactionsMt940(offered, account, bpd.nationalAccountAllowed, { from, to, continuation }),
183
+ // The first element holds the booked transactions, the second the pending ones: binary data, which a bank may send
184
+ // as text instead. Unlike HICAZ, HIKAZ names no account, and banks write the account line of a statement (`:25:`)
185
+ // in many ways, so it is not compared with `account`: a strict comparison would fail every query of such a bank.
178
186
  read: (pages) =>
179
187
  mt940Transactions(
180
188
  all(pages, 'HIKAZ')
181
- .map((s) => s.binary(1))
189
+ .map((s) => s.bytes(1))
182
190
  .filter((d) => d !== undefined),
183
191
  all(pages, 'HIKAZ')
184
- .map((s) => s.binary(2))
192
+ .map((s) => s.bytes(2))
185
193
  .filter((d) => d !== undefined),
186
194
  // The currency of statements that state none.
187
195
  account.currency,
@@ -6,6 +6,7 @@ import { Balance } from '../core/balance.js';
6
6
  import { TanMedium, tanMediumStatusOf } from '../tan/tan-models.js';
7
7
  import { formatFintsDate, parseFintsDate, parseFintsDateTime } from '../util/dates.js';
8
8
  import { FormatError } from '../util/errors.js';
9
+ import { trimBlanks } from '../util/text.js';
9
10
  import { type Segment, SegmentSpec, type SegmentValue, Verbatim } from './syntax.js';
10
11
 
11
12
  /**
@@ -26,9 +27,13 @@ function nationalAccount(a: Account): SegmentValue[] {
26
27
  return [a.accountNumber, a.subAccount, a.countryCode, a.bankCode];
27
28
  }
28
29
 
29
- /** International account (KTI): IBAN, BIC, then the national account. */
30
- function internationalAccount(a: Account): SegmentValue[] {
31
- return [a.iban, a.bic, ...nationalAccount(a)];
30
+ /**
31
+ * International account (KTI): IBAN and BIC, then the national account where the bank allows it (HISPAS). An IBAN goes
32
+ * only with its BIC (Messages B.3.2): without one the national account names the account, where it has a number.
33
+ */
34
+ function internationalAccount(a: Account, nationalAllowed: boolean): SegmentValue[] {
35
+ const iban = a.bic !== undefined || a.accountNumber === undefined ? a.iban : undefined;
36
+ return [iban, a.bic, ...(nationalAllowed || iban === undefined ? nationalAccount(a) : [])];
32
37
  }
33
38
 
34
39
  function dateOrEmpty(date: Date | undefined): string | undefined {
@@ -89,14 +94,10 @@ export const Segments = Object.freeze({
89
94
 
90
95
  /**
91
96
  * HKTAN process 4: asks for strong customer authentication of the `segmentType` sent in the same message, with the TAN
92
- * medium `mediumName`, as the bank sent it.
97
+ * medium `mediumName`: a `Verbatim` where it has to go back as the bank sent it (`ParameterStore.tanMediumToSend`).
93
98
  */
94
- tanProcess4(version: number, segmentType: string, mediumName?: string): SegmentSpec {
95
- return SegmentSpec.numbered('HKTAN', version, {
96
- 1: '4',
97
- 2: segmentType,
98
- [tanMediumField]: Verbatim.of(mediumName),
99
- });
99
+ tanProcess4(version: number, segmentType: string, mediumName?: string | Verbatim): SegmentSpec {
100
+ return SegmentSpec.numbered('HKTAN', version, { 1: '4', 2: segmentType, [tanMediumField]: mediumName });
100
101
  },
101
102
 
102
103
  /** HKTAN process 2: submits the TAN (carried in the signature) for the order with `reference`. */
@@ -117,24 +118,28 @@ export const Segments = Object.freeze({
117
118
  });
118
119
  },
119
120
 
120
- /** HKSAL: the balance of one account (not of all accounts). */
121
- balance(version: number, account: Account, continuation?: string): SegmentSpec {
121
+ /**
122
+ * HKSAL: the balance of one account (not of all accounts), named by its national account as well where
123
+ * `nationalAllowed` (HISPAS).
124
+ */
125
+ balance(version: number, account: Account, nationalAllowed: boolean, continuation?: string): SegmentSpec {
122
126
  return new SegmentSpec('HKSAL', version, [
123
- version >= 7 ? internationalAccount(account) : nationalAccount(account),
127
+ version >= 7 ? internationalAccount(account, nationalAllowed) : nationalAccount(account),
124
128
  false,
125
129
  null,
126
130
  Verbatim.of(continuation),
127
131
  ]);
128
132
  },
129
133
 
130
- /** HKKAZ: MT940 transactions of one account. */
134
+ /** HKKAZ: MT940 transactions of one account, named by its national account as well where `nationalAllowed`. */
131
135
  transactionsMt940(
132
136
  version: number,
133
137
  account: Account,
138
+ nationalAllowed: boolean,
134
139
  period: { from?: Date | undefined; to?: Date | undefined; continuation?: string | undefined },
135
140
  ): SegmentSpec {
136
141
  return new SegmentSpec('HKKAZ', version, [
137
- version >= 7 ? internationalAccount(account) : nationalAccount(account),
142
+ version >= 7 ? internationalAccount(account, nationalAllowed) : nationalAccount(account),
138
143
  false,
139
144
  dateOrEmpty(period.from),
140
145
  dateOrEmpty(period.to),
@@ -143,15 +148,19 @@ export const Segments = Object.freeze({
143
148
  ]);
144
149
  },
145
150
 
146
- /** HKCAZ: camt transactions of one account in one of `formats`. */
151
+ /**
152
+ * HKCAZ: camt transactions of one account in one of `formats`, named by its national account as well where
153
+ * `nationalAllowed`.
154
+ */
147
155
  transactionsCamt(
148
156
  version: number,
149
157
  account: Account,
158
+ nationalAllowed: boolean,
150
159
  formats: readonly string[],
151
160
  period: { from?: Date | undefined; to?: Date | undefined; continuation?: string | undefined },
152
161
  ): SegmentSpec {
153
162
  return new SegmentSpec('HKCAZ', version, [
154
- internationalAccount(account),
163
+ internationalAccount(account, nationalAllowed),
155
164
  formats,
156
165
  false,
157
166
  dateOrEmpty(period.from),
@@ -333,8 +342,9 @@ const accountWidth = 4;
333
342
  /**
334
343
  * Parses the TAN media list (HITAB versions 4 and 5, PIN/TAN "TAN-Medium-Liste").
335
344
  *
336
- * Each name whose text is not what `Segment.verbatim` reads, such as a name in UTF-8, goes into `namesAsSent`, keyed
337
- * by its text: it has to go back as the bank sent it, which its text does not tell where it fits ISO 8859-1.
345
+ * Each name whose text would not go back as the bank sent it goes into `namesAsSent` as `Segment.verbatim` reads it,
346
+ * keyed by its text: a name in UTF-8, which its text does not tell where it fits ISO 8859-1, and one with blanks at
347
+ * either end, which text goes out without (Formals B.4.1).
338
348
  */
339
349
  export function parseTanMedia(s: Segment, namesAsSent?: Map<string, string>): TanMedium[] {
340
350
  // Version 5 adds the security function in front of the card number.
@@ -352,7 +362,9 @@ export function parseTanMedia(s: Segment, namesAsSent?: Map<string, string>): Ta
352
362
  if (mediumClass === undefined) continue;
353
363
  const name = fields[after + 3];
354
364
  const nameAsSent = s.verbatim(i, after + 3);
355
- if (name !== undefined && nameAsSent !== undefined && nameAsSent !== name) namesAsSent?.set(name, nameAsSent);
365
+ if (name !== undefined && nameAsSent !== undefined && nameAsSent !== trimBlanks(name)) {
366
+ namesAsSent?.set(name, nameAsSent);
367
+ }
356
368
  media.push(
357
369
  new TanMedium({
358
370
  mediumClass,
package/src/v3/syntax.ts CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  import { BytesBuilder, latin1Decode, latin1Encode, utf8Encode } from '../util/bytes.js';
4
4
  import { FormatError } from '../util/errors.js';
5
- import { decodeBankText, isLatin1, tryParseInt } from '../util/text.js';
5
+ import { decodeBankText, isLatin1, trimBlanks, tryParseInt } from '../util/text.js';
6
6
 
7
7
  // FinTS 3.0 syntax characters.
8
8
  const dataElementSeparator = 0x2b; // '+'
@@ -59,6 +59,8 @@ export class DataElement {
59
59
  * Index 0 is the segment header (type, number, version, reference); the data elements follow from index 1.
60
60
  */
61
61
  export class Segment {
62
+ private typeValue: string | undefined;
63
+
62
64
  constructor(
63
65
  readonly elements: readonly (readonly DataElement[])[],
64
66
  /** The bytes the segment was read from: `start` to `end` in `source`, terminator included. */
@@ -68,7 +70,8 @@ export class Segment {
68
70
  ) {}
69
71
 
70
72
  get type(): string {
71
- return this.text(0)?.trim() ?? '';
73
+ this.typeValue ??= trimBlanks(this.text(0) ?? '');
74
+ return this.typeValue;
72
75
  }
73
76
 
74
77
  get version(): number {
@@ -99,6 +102,12 @@ export class Segment {
99
102
  return e?.isBinary === true ? e.bytes : undefined;
100
103
  }
101
104
 
105
+ /** The binary data or the text without escape characters at `index`; `undefined` for an empty element. */
106
+ bytes(index: number, sub = 0): Uint8Array | undefined {
107
+ const e = this.element(index, sub);
108
+ return e === undefined || e.isEmpty ? undefined : e.bytes;
109
+ }
110
+
102
111
  /**
103
112
  * The element at `index` as the bank sent it, for a value the client sends back, such as the dialog ID: untrimmed,
104
113
  * and one character per byte (ISO 8859-1, the FinTS 3.0 character set), so that `Verbatim` gives the bank back the
@@ -111,7 +120,7 @@ export class Segment {
111
120
  }
112
121
 
113
122
  integer(index: number, sub = 0): number | undefined {
114
- return tryParseInt(this.text(index, sub)?.trim());
123
+ return tryParseInt(this.text(index, sub));
115
124
  }
116
125
 
117
126
  /** The texts of the group at `index`; `undefined` for empty elements. */
@@ -151,17 +160,20 @@ export function encodeText(value: string): Uint8Array {
151
160
  }
152
161
 
153
162
  /**
154
- * The data elements one message may hold, segment headers and group elements included: several times what the
155
- * parameter data of a user with hundreds of accounts take, and few enough that a hostile message cannot exhaust the
156
- * memory, at about `dataElementMemory` bytes each.
163
+ * The data elements and segments one parse may produce together, segment headers and group elements included: what
164
+ * the user parameter data of 6,000 accounts take, at about 155 data elements each, which is about what 9,999 KB, the
165
+ * largest message size HIBPA can state, hold of real data. Without a limit, a message of nothing but separators would
166
+ * take a hundred times its size in memory.
167
+ *
168
+ * @internal Exported for the tests.
157
169
  */
158
- export const maxDataElements = 200_000;
170
+ export const maxParts = 1_000_000;
159
171
 
160
172
  /**
161
- * The memory a data element of a received message takes, in bytes, with its share of the segment and of a return code:
162
- * about this much in V8, however few bytes of the message it is made of.
173
+ * What `IncomingMessage3.size` counts for each data element it keeps: more than any of them takes once parsed, so that
174
+ * a message of many short elements counts as the memory it holds rather than its bytes.
163
175
  */
164
- export const dataElementMemory = 256;
176
+ export const parsedElementBytes = 512;
165
177
 
166
178
  /** Every empty data element of a received segment; it has no bytes to change. */
167
179
  const emptyElement = new DataElement(new Uint8Array(0), 0, 0, false);
@@ -184,10 +196,14 @@ function isDigit(byte: number | undefined): boolean {
184
196
  *
185
197
  * The elements share the memory of `data`. Throws `FormatError` for anything a complete message cannot hold: a
186
198
  * malformed binary length, binary data beyond the end of `data` or not followed by a separator, a last segment without
187
- * terminator, and more elements than `budget` has left, which parses of the same message share.
199
+ * terminator, and more than `maxParts` data elements and segments, an empty one included.
188
200
  */
189
- export function parseSegments(data: Uint8Array, budget = { elements: maxDataElements }): Segment[] {
201
+ export function parseSegments(data: Uint8Array): Segment[] {
190
202
  const segments: Segment[] = [];
203
+ let parts = 0;
204
+ const count = (): void => {
205
+ if (++parts > maxParts) throw new FormatError(`More than ${maxParts} data elements and segments`);
206
+ };
191
207
  // Created with their first element: an array that starts empty reserves room for more than most of them hold.
192
208
  let elements: DataElement[][] | undefined;
193
209
  let group: DataElement[] | undefined;
@@ -228,7 +244,7 @@ export function parseSegments(data: Uint8Array, budget = { elements: maxDataElem
228
244
  i++;
229
245
  continue;
230
246
  }
231
- if (--budget.elements < 0) throw new FormatError('Too many data elements');
247
+ count();
232
248
  if (elements === undefined && group === undefined) segmentStart = i;
233
249
  const element = data[i] === binaryMarker ? readBinary() : readText();
234
250
  if (group === undefined) group = [element];
@@ -239,6 +255,7 @@ export function parseSegments(data: Uint8Array, budget = { elements: maxDataElem
239
255
  else elements.push(group);
240
256
  group = undefined;
241
257
  if (separator === segmentTerminator) {
258
+ count();
242
259
  const segment = new Segment(elements, data, segmentStart, i);
243
260
  // Skip empty segments, such as a stray double terminator.
244
261
  if (segment.type !== '') segments.push(segment);
@@ -363,7 +380,7 @@ function encodeValue(value: SingleValue): Uint8Array {
363
380
  } else {
364
381
  // All but binary data go without leading and trailing blanks (Formals B.4.1). Encode first, then escape the bytes,
365
382
  // so that nothing unescaped slips into the syntax.
366
- writeEscaped(out, encodeText(String(value).trim()));
383
+ writeEscaped(out, encodeText(trimBlanks(String(value))));
367
384
  }
368
385
  return out.takeBytes();
369
386
  }