@coderoftime/fints 0.1.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (289) hide show
  1. package/CHANGELOG.md +519 -1
  2. package/README.md +138 -40
  3. package/dist/client.d.ts +49 -0
  4. package/dist/client.d.ts.map +1 -0
  5. package/dist/client.js +242 -0
  6. package/dist/client.js.map +1 -0
  7. package/dist/core/account.d.ts +46 -7
  8. package/dist/core/account.d.ts.map +1 -1
  9. package/dist/core/account.js +143 -24
  10. package/dist/core/account.js.map +1 -1
  11. package/dist/core/amount.d.ts +15 -4
  12. package/dist/core/amount.d.ts.map +1 -1
  13. package/dist/core/amount.js +46 -9
  14. package/dist/core/amount.js.map +1 -1
  15. package/dist/core/balance.d.ts +4 -0
  16. package/dist/core/balance.d.ts.map +1 -1
  17. package/dist/core/balance.js +11 -5
  18. package/dist/core/balance.js.map +1 -1
  19. package/dist/core/bank-info.d.ts +21 -4
  20. package/dist/core/bank-info.d.ts.map +1 -1
  21. package/dist/core/bank-info.js +26 -5
  22. package/dist/core/bank-info.js.map +1 -1
  23. package/dist/core/client.d.ts +81 -19
  24. package/dist/core/client.d.ts.map +1 -1
  25. package/dist/core/config.d.ts +40 -20
  26. package/dist/core/config.d.ts.map +1 -1
  27. package/dist/core/config.js +85 -30
  28. package/dist/core/config.js.map +1 -1
  29. package/dist/core/errors.d.ts +100 -26
  30. package/dist/core/errors.d.ts.map +1 -1
  31. package/dist/core/errors.js +136 -53
  32. package/dist/core/errors.js.map +1 -1
  33. package/dist/core/feedback.d.ts +11 -1
  34. package/dist/core/feedback.d.ts.map +1 -1
  35. package/dist/core/feedback.js +14 -2
  36. package/dist/core/feedback.js.map +1 -1
  37. package/dist/core/transaction.d.ts +11 -1
  38. package/dist/core/transaction.d.ts.map +1 -1
  39. package/dist/core/transaction.js +21 -4
  40. package/dist/core/transaction.js.map +1 -1
  41. package/dist/core/transport.d.ts +48 -6
  42. package/dist/core/transport.d.ts.map +1 -1
  43. package/dist/core/transport.js +126 -29
  44. package/dist/core/transport.js.map +1 -1
  45. package/dist/formats/camt.d.ts +28 -5
  46. package/dist/formats/camt.d.ts.map +1 -1
  47. package/dist/formats/camt.js +132 -50
  48. package/dist/formats/camt.js.map +1 -1
  49. package/dist/formats/mt940.d.ts +29 -5
  50. package/dist/formats/mt940.d.ts.map +1 -1
  51. package/dist/formats/mt940.js +209 -113
  52. package/dist/formats/mt940.js.map +1 -1
  53. package/dist/formats/readers.d.ts +6 -2
  54. package/dist/formats/readers.d.ts.map +1 -1
  55. package/dist/formats/readers.js +28 -29
  56. package/dist/formats/readers.js.map +1 -1
  57. package/dist/formats/record-budget.d.ts +7 -0
  58. package/dist/formats/record-budget.d.ts.map +1 -0
  59. package/dist/formats/record-budget.js +17 -0
  60. package/dist/formats/record-budget.js.map +1 -0
  61. package/dist/formats/sepa-purpose.d.ts +9 -0
  62. package/dist/formats/sepa-purpose.d.ts.map +1 -1
  63. package/dist/formats/sepa-purpose.js +32 -2
  64. package/dist/formats/sepa-purpose.js.map +1 -1
  65. package/dist/index.d.ts +3 -3
  66. package/dist/index.d.ts.map +1 -1
  67. package/dist/index.js +2 -2
  68. package/dist/index.js.map +1 -1
  69. package/dist/session/app-approval.d.ts +11 -5
  70. package/dist/session/app-approval.d.ts.map +1 -1
  71. package/dist/session/app-approval.js +23 -12
  72. package/dist/session/app-approval.js.map +1 -1
  73. package/dist/session/dialog-runner.d.ts +11 -11
  74. package/dist/session/dialog-runner.d.ts.map +1 -1
  75. package/dist/session/dialog-runner.js +34 -19
  76. package/dist/session/dialog-runner.js.map +1 -1
  77. package/dist/session/login-guard.d.ts +54 -49
  78. package/dist/session/login-guard.d.ts.map +1 -1
  79. package/dist/session/login-guard.js +170 -83
  80. package/dist/session/login-guard.js.map +1 -1
  81. package/dist/session/sepa-accounts.d.ts +20 -14
  82. package/dist/session/sepa-accounts.d.ts.map +1 -1
  83. package/dist/session/sepa-accounts.js +62 -46
  84. package/dist/session/sepa-accounts.js.map +1 -1
  85. package/dist/session/session-setup.d.ts +41 -24
  86. package/dist/session/session-setup.d.ts.map +1 -1
  87. package/dist/session/session-setup.js +106 -63
  88. package/dist/session/session-setup.js.map +1 -1
  89. package/dist/session/session-state.d.ts +10 -6
  90. package/dist/session/session-state.d.ts.map +1 -1
  91. package/dist/session/session-state.js +72 -18
  92. package/dist/session/session-state.js.map +1 -1
  93. package/dist/session/signed-dialog.d.ts +83 -0
  94. package/dist/session/signed-dialog.d.ts.map +1 -0
  95. package/dist/session/signed-dialog.js +185 -0
  96. package/dist/session/signed-dialog.js.map +1 -0
  97. package/dist/session/tan-selection.d.ts +26 -18
  98. package/dist/session/tan-selection.d.ts.map +1 -1
  99. package/dist/session/tan-selection.js +44 -33
  100. package/dist/session/tan-selection.js.map +1 -1
  101. package/dist/session/user-prompts.d.ts +14 -5
  102. package/dist/session/user-prompts.d.ts.map +1 -1
  103. package/dist/session/user-prompts.js +53 -18
  104. package/dist/session/user-prompts.js.map +1 -1
  105. package/dist/tan/challenge.d.ts +47 -24
  106. package/dist/tan/challenge.d.ts.map +1 -1
  107. package/dist/tan/challenge.js +56 -51
  108. package/dist/tan/challenge.js.map +1 -1
  109. package/dist/tan/flicker.d.ts +11 -1
  110. package/dist/tan/flicker.d.ts.map +1 -1
  111. package/dist/tan/flicker.js +70 -24
  112. package/dist/tan/flicker.js.map +1 -1
  113. package/dist/tan/tan-image.d.ts +6 -2
  114. package/dist/tan/tan-image.d.ts.map +1 -1
  115. package/dist/tan/tan-image.js +14 -6
  116. package/dist/tan/tan-image.js.map +1 -1
  117. package/dist/tan/tan-models.d.ts +14 -2
  118. package/dist/tan/tan-models.d.ts.map +1 -1
  119. package/dist/tan/tan-models.js +24 -7
  120. package/dist/tan/tan-models.js.map +1 -1
  121. package/dist/util/arguments.d.ts +13 -0
  122. package/dist/util/arguments.d.ts.map +1 -0
  123. package/dist/util/arguments.js +40 -0
  124. package/dist/util/arguments.js.map +1 -0
  125. package/dist/util/async.d.ts +4 -3
  126. package/dist/util/async.d.ts.map +1 -1
  127. package/dist/util/async.js +6 -7
  128. package/dist/util/async.js.map +1 -1
  129. package/dist/util/bytes.d.ts +3 -4
  130. package/dist/util/bytes.d.ts.map +1 -1
  131. package/dist/util/bytes.js +13 -21
  132. package/dist/util/bytes.js.map +1 -1
  133. package/dist/util/call-context.d.ts.map +1 -1
  134. package/dist/util/call-context.js +3 -2
  135. package/dist/util/call-context.js.map +1 -1
  136. package/dist/util/dates.d.ts +6 -20
  137. package/dist/util/dates.d.ts.map +1 -1
  138. package/dist/util/dates.js +58 -28
  139. package/dist/util/dates.js.map +1 -1
  140. package/dist/util/errors.d.ts +13 -3
  141. package/dist/util/errors.d.ts.map +1 -1
  142. package/dist/util/errors.js +19 -6
  143. package/dist/util/errors.js.map +1 -1
  144. package/dist/util/immutable-map.d.ts +5 -2
  145. package/dist/util/immutable-map.d.ts.map +1 -1
  146. package/dist/util/immutable-map.js +15 -12
  147. package/dist/util/immutable-map.js.map +1 -1
  148. package/dist/util/inspect.d.ts +11 -0
  149. package/dist/util/inspect.d.ts.map +1 -0
  150. package/dist/util/inspect.js +14 -0
  151. package/dist/util/inspect.js.map +1 -0
  152. package/dist/util/json.d.ts +4 -0
  153. package/dist/util/json.d.ts.map +1 -1
  154. package/dist/util/json.js +18 -0
  155. package/dist/util/json.js.map +1 -1
  156. package/dist/util/text.d.ts +38 -5
  157. package/dist/util/text.d.ts.map +1 -1
  158. package/dist/util/text.js +90 -16
  159. package/dist/util/text.js.map +1 -1
  160. package/dist/util/xml.d.ts +9 -6
  161. package/dist/util/xml.d.ts.map +1 -1
  162. package/dist/util/xml.js +129 -56
  163. package/dist/util/xml.js.map +1 -1
  164. package/dist/v3/account-extension.d.ts.map +1 -1
  165. package/dist/v3/account-extension.js +7 -10
  166. package/dist/v3/account-extension.js.map +1 -1
  167. package/dist/v3/bank-connection.d.ts +54 -0
  168. package/dist/v3/bank-connection.d.ts.map +1 -0
  169. package/dist/v3/bank-connection.js +118 -0
  170. package/dist/v3/bank-connection.js.map +1 -0
  171. package/dist/v3/dialog.d.ts +84 -46
  172. package/dist/v3/dialog.d.ts.map +1 -1
  173. package/dist/v3/dialog.js +174 -60
  174. package/dist/v3/dialog.js.map +1 -1
  175. package/dist/v3/message.d.ts +32 -11
  176. package/dist/v3/message.d.ts.map +1 -1
  177. package/dist/v3/message.js +144 -83
  178. package/dist/v3/message.js.map +1 -1
  179. package/dist/v3/parameter-store.d.ts +111 -0
  180. package/dist/v3/parameter-store.d.ts.map +1 -0
  181. package/dist/v3/parameter-store.js +197 -0
  182. package/dist/v3/parameter-store.js.map +1 -0
  183. package/dist/v3/parameters.d.ts +44 -17
  184. package/dist/v3/parameters.d.ts.map +1 -1
  185. package/dist/v3/parameters.js +136 -60
  186. package/dist/v3/parameters.js.map +1 -1
  187. package/dist/v3/protocol.d.ts +50 -50
  188. package/dist/v3/protocol.d.ts.map +1 -1
  189. package/dist/v3/protocol.js +91 -210
  190. package/dist/v3/protocol.js.map +1 -1
  191. package/dist/v3/queries.d.ts +51 -0
  192. package/dist/v3/queries.d.ts.map +1 -0
  193. package/dist/v3/queries.js +128 -0
  194. package/dist/v3/queries.js.map +1 -0
  195. package/dist/v3/segments.d.ts +43 -19
  196. package/dist/v3/segments.d.ts.map +1 -1
  197. package/dist/v3/segments.js +84 -33
  198. package/dist/v3/segments.js.map +1 -1
  199. package/dist/v3/syntax.d.ts +68 -13
  200. package/dist/v3/syntax.d.ts.map +1 -1
  201. package/dist/v3/syntax.js +183 -102
  202. package/dist/v3/syntax.js.map +1 -1
  203. package/package.json +10 -6
  204. package/src/client.ts +293 -0
  205. package/src/core/account.ts +153 -24
  206. package/src/core/amount.ts +45 -10
  207. package/src/core/balance.ts +12 -5
  208. package/src/core/bank-info.ts +29 -5
  209. package/src/core/client.ts +83 -19
  210. package/src/core/config.ts +95 -35
  211. package/src/core/errors.ts +148 -54
  212. package/src/core/feedback.ts +16 -2
  213. package/src/core/transaction.ts +22 -4
  214. package/src/core/transport.ts +153 -32
  215. package/src/formats/camt.ts +153 -45
  216. package/src/formats/mt940.ts +244 -122
  217. package/src/formats/readers.ts +28 -31
  218. package/src/formats/record-budget.ts +18 -0
  219. package/src/formats/sepa-purpose.ts +29 -2
  220. package/src/index.ts +4 -2
  221. package/src/session/app-approval.ts +51 -38
  222. package/src/session/dialog-runner.ts +61 -31
  223. package/src/session/login-guard.ts +189 -87
  224. package/src/session/sepa-accounts.ts +63 -45
  225. package/src/session/session-setup.ts +105 -68
  226. package/src/session/session-state.ts +74 -23
  227. package/src/session/signed-dialog.ts +222 -0
  228. package/src/session/tan-selection.ts +50 -39
  229. package/src/session/user-prompts.ts +62 -18
  230. package/src/tan/challenge.ts +87 -64
  231. package/src/tan/flicker.ts +72 -23
  232. package/src/tan/tan-image.ts +15 -6
  233. package/src/tan/tan-models.ts +26 -7
  234. package/src/util/arguments.ts +40 -0
  235. package/src/util/async.ts +10 -9
  236. package/src/util/bytes.ts +14 -20
  237. package/src/util/call-context.ts +3 -2
  238. package/src/util/dates.ts +64 -33
  239. package/src/util/errors.ts +22 -6
  240. package/src/util/immutable-map.ts +15 -12
  241. package/src/util/inspect.ts +14 -0
  242. package/src/util/json.ts +16 -0
  243. package/src/util/text.ts +87 -15
  244. package/src/util/xml.ts +130 -52
  245. package/src/v3/account-extension.ts +7 -12
  246. package/src/v3/bank-connection.ts +126 -0
  247. package/src/v3/dialog.ts +186 -92
  248. package/src/v3/message.ts +140 -79
  249. package/src/v3/parameter-store.ts +226 -0
  250. package/src/v3/parameters.ts +166 -66
  251. package/src/v3/protocol.ts +103 -253
  252. package/src/v3/queries.ts +198 -0
  253. package/src/v3/segments.ts +86 -31
  254. package/src/v3/syntax.ts +172 -93
  255. package/dist/session/bank-connection.d.ts +0 -28
  256. package/dist/session/bank-connection.d.ts.map +0 -1
  257. package/dist/session/bank-connection.js +0 -48
  258. package/dist/session/bank-connection.js.map +0 -1
  259. package/dist/session/parameter-data.d.ts +0 -83
  260. package/dist/session/parameter-data.d.ts.map +0 -1
  261. package/dist/session/parameter-data.js +0 -68
  262. package/dist/session/parameter-data.js.map +0 -1
  263. package/dist/session/pin-tan-client.d.ts +0 -54
  264. package/dist/session/pin-tan-client.d.ts.map +0 -1
  265. package/dist/session/pin-tan-client.js +0 -153
  266. package/dist/session/pin-tan-client.js.map +0 -1
  267. package/dist/session/pin-tan-dialog.d.ts +0 -71
  268. package/dist/session/pin-tan-dialog.d.ts.map +0 -1
  269. package/dist/session/pin-tan-dialog.js +0 -124
  270. package/dist/session/pin-tan-dialog.js.map +0 -1
  271. package/dist/session/protocol.d.ts +0 -209
  272. package/dist/session/protocol.d.ts.map +0 -1
  273. package/dist/session/protocol.js +0 -75
  274. package/dist/session/protocol.js.map +0 -1
  275. package/dist/session/response-handler.d.ts +0 -48
  276. package/dist/session/response-handler.d.ts.map +0 -1
  277. package/dist/session/response-handler.js +0 -121
  278. package/dist/session/response-handler.js.map +0 -1
  279. package/dist/v3/client.d.ts +0 -24
  280. package/dist/v3/client.d.ts.map +0 -1
  281. package/dist/v3/client.js +0 -16
  282. package/dist/v3/client.js.map +0 -1
  283. package/src/session/bank-connection.ts +0 -51
  284. package/src/session/parameter-data.ts +0 -137
  285. package/src/session/pin-tan-client.ts +0 -186
  286. package/src/session/pin-tan-dialog.ts +0 -160
  287. package/src/session/protocol.ts +0 -311
  288. package/src/session/response-handler.ts +0 -130
  289. package/src/v3/client.ts +0 -29
@@ -3,10 +3,10 @@
3
3
  import { Account } from '../core/account.js';
4
4
  import type { Balance } from '../core/balance.js';
5
5
  import { BankInfo } from '../core/bank-info.js';
6
- import { highestCommonVersion, type BankParameters, type UserParameters } from '../session/parameter-data.js';
6
+ import { FinTsProtocolError } from '../core/errors.js';
7
7
  import { TanMethod, tanMediumRequirementOf } from '../tan/tan-models.js';
8
- import { base64Decode, base64Encode, BytesBuilder } from '../util/bytes.js';
9
- import { tryParseInt } from '../util/text.js';
8
+ import { base64Decode, base64Encode, concatBytes } from '../util/bytes.js';
9
+ import { nonBlank, trimBlanks, tryParseInt } from '../util/text.js';
10
10
  import { AccountExtension } from './account-extension.js';
11
11
  import { parseSegments, type Segment } from './syntax.js';
12
12
 
@@ -33,6 +33,12 @@ const Hibpa = {
33
33
  maxTimeout: 9,
34
34
  } as const;
35
35
 
36
+ /** The size or time at `index` of `hibpa`, a field of at most four digits; `undefined` for a larger value, as for none. */
37
+ function hibpaLimit(hibpa: Segment, index: number): number | undefined {
38
+ const value = hibpa.integer(index);
39
+ return value !== undefined && value <= 9999 ? value : undefined;
40
+ }
41
+
36
42
  /**
37
43
  * The data element of every parameter segment (`HI…S`) that holds its specific parameters, after the maximum number of
38
44
  * orders, the minimum number of signatures and the security class.
@@ -46,20 +52,35 @@ function defined<T>(values: readonly (T | undefined)[]): T[] {
46
52
  return values.filter((v): v is T => v !== undefined);
47
53
  }
48
54
 
55
+ /** `bpd`, or `FinTsProtocolError` if the bank has not sent its parameter data yet. */
56
+ export function requireBankParameters(bpd: Bpd3 | undefined): Bpd3 {
57
+ if (bpd === undefined) throw new FinTsProtocolError('No bank parameter data available');
58
+ return bpd;
59
+ }
60
+
49
61
  /** FinTS 3.0 bank parameter data (BPD): HIBPA and the parameter segments (`HI…S`), parsed once. */
50
- export class Bpd3 implements BankParameters {
62
+ export class Bpd3 {
51
63
  readonly bankInfo: BankInfo;
52
64
 
53
65
  /** The two-step TAN methods of HITANS versions 6 and 7, by security function. */
54
66
  private readonly offeredMethods: ReadonlyMap<string, OfferedMethod>;
55
67
 
68
+ /** Two-step TAN methods the bank offers and the client can run. */
56
69
  readonly tanMethods: readonly TanMethod[];
70
+
71
+ /** Security functions of the two-step methods the bank offers in a form the client cannot run. */
57
72
  readonly unsupportedTanMethods: readonly string[];
58
73
  readonly pinTan: PinTanParameters;
59
74
 
60
- /** camt formats the bank offers for HKCAZ. */
75
+ /** The camt.052 formats the bank offers for HKCAZ, the one camt message the client reads. */
61
76
  readonly camtFormats: readonly string[];
62
77
 
78
+ /**
79
+ * Whether an international account (KTI) may name the national account as well: HISPAS does not rule it out
80
+ * ("Nationale Kontoverbindung erlaubt").
81
+ */
82
+ readonly nationalAccountAllowed: boolean;
83
+
63
84
  /** Versions of each parameter segment, by segment type. */
64
85
  private readonly versions: ReadonlyMap<string, ReadonlySet<number>>;
65
86
 
@@ -69,20 +90,19 @@ export class Bpd3 implements BankParameters {
69
90
  */
70
91
  readonly dialogLanguage: number;
71
92
 
72
- private constructor(
73
- readonly segments: readonly Segment[],
74
- hibpa: Segment,
75
- fallbackBankCode: string,
76
- ) {
93
+ /** The segments as the saved state keeps them. */
94
+ readonly encoded: string;
95
+
96
+ private constructor(segments: readonly Segment[], hibpa: Segment, fallbackBankCode: string) {
77
97
  this.bankInfo = new BankInfo({
78
98
  bankCode: hibpa.text(Hibpa.bankId, 1) ?? fallbackBankCode,
79
99
  name: hibpa.text(Hibpa.bankName),
80
100
  bpdVersion: hibpa.integer(Hibpa.version) ?? 0,
81
101
  languages: defined(hibpa.texts(Hibpa.languages)),
82
102
  supportedVersions: defined(hibpa.texts(Hibpa.fintsVersions)),
83
- maxMessageSize: hibpa.integer(Hibpa.maxMessageSize),
84
- minTimeout: hibpa.integer(Hibpa.minTimeout),
85
- maxTimeout: hibpa.integer(Hibpa.maxTimeout),
103
+ maxMessageSize: hibpaLimit(hibpa, Hibpa.maxMessageSize),
104
+ minTimeout: hibpaLimit(hibpa, Hibpa.minTimeout),
105
+ maxTimeout: hibpaLimit(hibpa, Hibpa.maxTimeout),
86
106
  });
87
107
  this.offeredMethods = parseTanMethods(segments);
88
108
  const offered = [...this.offeredMethods.values()];
@@ -94,23 +114,18 @@ export class Bpd3 implements BankParameters {
94
114
  this.camtFormats = segments
95
115
  .filter((s) => s.type === 'HICAZS')
96
116
  .flatMap((s) => defined(s.texts(parameterGroup)))
97
- .filter((format) => format.includes('camt'));
117
+ .filter((format) => format.includes('camt.052'));
118
+ this.nationalAccountAllowed = highest(segments, 'HISPAS')?.text(parameterGroup, 1) !== 'N';
98
119
  this.versions = versionsByType(segments);
99
120
  this.dialogLanguage = dialogLanguageOf(segments, this.bankInfo);
121
+ this.encoded = encodeSegments(segments);
100
122
  }
101
123
 
124
+ /** The version of the parameter data, which the client reports back so that the bank sends newer ones only. */
102
125
  get version(): number {
103
126
  return this.bankInfo.bpdVersion;
104
127
  }
105
128
 
106
- get pinLength(): { min: number | undefined; max: number | undefined } {
107
- return this.pinTan.pinLength;
108
- }
109
-
110
- allowsWithPinTan(transaction: string): boolean {
111
- return this.pinTan.allowsWithPinTan(transaction);
112
- }
113
-
114
129
  /** The BPD in `all`, or `undefined` if they contain no HIBPA segment. */
115
130
  static fromSegments(all: readonly Segment[], fallbackBankCode: string): Bpd3 | undefined {
116
131
  const segments = all.filter(isParameterSegment);
@@ -123,8 +138,13 @@ export class Bpd3 implements BankParameters {
123
138
  return `HI${transaction.substring(2)}S`;
124
139
  }
125
140
 
141
+ /** The highest version of `transaction` that the bank offers and the client `implemented`. */
126
142
  bestVersion(transaction: string, implemented: ReadonlySet<number>): number | undefined {
127
- return highestCommonVersion(this.versions.get(Bpd3.parameterName(transaction)), implemented);
143
+ let best: number | undefined;
144
+ for (const version of this.versions.get(Bpd3.parameterName(transaction)) ?? []) {
145
+ if (implemented.has(version) && (best === undefined || version > best)) best = version;
146
+ }
147
+ return best;
128
148
  }
129
149
 
130
150
  /** The version of the HKTAN segments that go with `method`; `undefined` for a method the bank does not offer. */
@@ -140,13 +160,23 @@ function dialogLanguageOf(segments: readonly Segment[], bankInfo: BankInfo): num
140
160
  return bankInfo.languages.map((l) => tryParseInt(l)).find((l) => l !== undefined && languages.has(l)) ?? 0;
141
161
  }
142
162
 
163
+ const generalParameterSegments = new Set(['HIBPA', 'HIKOM', 'HISHV', 'HIKPV']);
164
+
143
165
  function isParameterSegment(s: Segment): boolean {
144
166
  const type = s.type;
145
- if (['HIBPA', 'HIKOM', 'HISHV', 'HIKPV'].includes(type)) return true;
146
- if (type === 'HIRMS' || type === 'HIRMG') return false;
167
+ if (generalParameterSegments.has(type)) return true;
168
+ // Return codes for single segments end with an S, as parameter segments do.
169
+ if (type === 'HIRMS') return false;
147
170
  return type.length >= 5 && (type.startsWith('HI') || type.startsWith('DI')) && type.endsWith('S');
148
171
  }
149
172
 
173
+ /** The segment of `type` with the highest version in `segments`. */
174
+ function highest(segments: readonly Segment[], type: string): Segment | undefined {
175
+ let found: Segment | undefined;
176
+ for (const s of segments) if (s.type === type && (found === undefined || s.version > found.version)) found = s;
177
+ return found;
178
+ }
179
+
150
180
  function versionsByType(segments: readonly Segment[]): Map<string, Set<number>> {
151
181
  const versions = new Map<string, Set<number>>();
152
182
  for (const s of segments) {
@@ -256,17 +286,14 @@ function parseHitans(s: Segment): OfferedMethod[] {
256
286
  export class PinTanParameters {
257
287
  private constructor(
258
288
  private readonly tanRequired: ReadonlyMap<string, boolean>,
259
- /** The shortest and longest PIN the bank takes. */
260
- readonly pinLength: { min: number | undefined; max: number | undefined },
289
+ /** The shortest and longest PIN the bank takes; `undefined` where the bank parameter data have no HIPINS. */
290
+ readonly pinLength: { min: number | undefined; max: number | undefined } | undefined,
261
291
  ) {}
262
292
 
263
293
  /** Reads the HIPINS segment of the highest version. */
264
294
  static parse(segments: readonly Segment[]): PinTanParameters {
265
- let hipins: Segment | undefined;
266
- for (const s of segments.filter((s) => s.type === 'HIPINS')) {
267
- if (hipins === undefined || s.version > hipins.version) hipins = s;
268
- }
269
- if (hipins === undefined) return new PinTanParameters(new Map(), { min: undefined, max: undefined });
295
+ const hipins = highest(segments, 'HIPINS');
296
+ if (hipins === undefined) return new PinTanParameters(new Map(), undefined);
270
297
  const fields = hipins.texts(parameterGroup);
271
298
  const tanRequired = new Map<string, boolean>();
272
299
  // Five general fields, the PIN lengths first; then pairs of segment type and TAN flag.
@@ -293,7 +320,8 @@ interface UpdEntry {
293
320
  readonly extension: AccountExtension | undefined;
294
321
  }
295
322
 
296
- /** The version and the usage ("UPD-Verwendung") of the user parameter data in HIUPA. */
323
+ /** The user ID (Benutzerkennung), the version and the usage ("UPD-Verwendung") of the user parameter data in HIUPA. */
324
+ const hiupaUserId = 1;
297
325
  const hiupaVersion = 2;
298
326
  const hiupaUsage = 3;
299
327
 
@@ -301,24 +329,41 @@ const hiupaUsage = 3;
301
329
  const transactionCode = /^[A-Z]{4,6}$/;
302
330
 
303
331
  /** FinTS 3.0 user parameter data (UPD): HIUPA and the accounts (HIUPD), parsed once. */
304
- export class Upd3 implements UserParameters {
332
+ export class Upd3 {
333
+ /** The user the bank sent them for; `undefined` where it names none. */
334
+ readonly userId: string | undefined;
335
+
336
+ /** The version of the parameter data, which the client reports back so that the bank sends newer ones only. */
305
337
  readonly version: number;
338
+
339
+ /**
340
+ * Whether the lists of permitted transactions are complete: the bank permits nothing they leave out ("UPD-Verwendung"
341
+ * 0).
342
+ */
306
343
  readonly listsAllTransactions: boolean;
344
+
345
+ /** The transactions any entry lists as permitted, with or without an account. */
307
346
  readonly listedTransactions: ReadonlySet<string>;
308
347
 
309
348
  /** The accounts with the extension each carries, where it does. */
310
349
  private readonly entries: readonly UpdEntry[];
350
+
351
+ /** The accounts the user may access. */
311
352
  readonly accounts: readonly Account[];
312
353
 
354
+ /** The segments as the saved state keeps them. */
355
+ readonly encoded: string;
356
+
313
357
  /** `listsAll` tells whether HIUPA declares the lists of permitted transactions complete ("UPD-Verwendung" 0). */
314
358
  private constructor(
315
- readonly segments: readonly Segment[],
359
+ segments: readonly Segment[],
316
360
  hiupa: Segment,
317
361
  listsAll: boolean,
318
362
  defaultBankCode: string,
319
363
  defaultCountry: string,
320
364
  ) {
321
365
  const hiupds = segments.filter((s) => s.type === 'HIUPD');
366
+ this.userId = nonBlank(hiupa.text(hiupaUserId));
322
367
  this.version = hiupa.integer(hiupaVersion) ?? 0;
323
368
  this.listsAllTransactions = listsAll;
324
369
  this.listedTransactions = new Set(hiupds.flatMap(transactionsOf));
@@ -326,13 +371,13 @@ export class Upd3 implements UserParameters {
326
371
  hiupds.map((s) => entryOf(s, defaultBankCode, defaultCountry, listsAll)).filter((e) => e !== undefined),
327
372
  );
328
373
  this.accounts = Object.freeze(this.entries.map((e) => e.account));
374
+ this.encoded = encodeSegments(segments);
329
375
  }
330
376
 
331
- /** FinTS 3.0 UPD do not list the allowed TAN methods; the bank reports them with return code 3920. */
332
- get allowedTanMethods(): readonly string[] {
333
- return [];
334
- }
335
-
377
+ /**
378
+ * The balance of `account` the user parameter data carry, where it holds all a balance query would deliver, so that
379
+ * the client asks for none (Formals E.3.1, F.3.2); `undefined` otherwise.
380
+ */
336
381
  completeBalance(account: Account): Balance | undefined {
337
382
  return this.entries.find((e) => e.account.matches(account))?.extension?.completeBalance(account);
338
383
  }
@@ -372,11 +417,13 @@ function transactionsOf(s: Segment): string[] {
372
417
  function extensionOf(s: Segment): AccountExtension | undefined {
373
418
  if (s.version < 6) return undefined;
374
419
  for (let i = allowedFrom(s.version); i < s.length; i++) {
375
- const text = s
376
- .group(i)
377
- .map((e) => e.text)
378
- .join(':');
379
- if (text.trimStart().startsWith('{')) return AccountExtension.tryParse(text);
420
+ const text = trimBlanks(
421
+ s
422
+ .group(i)
423
+ .map((e) => e.text)
424
+ .join(':'),
425
+ );
426
+ if (text.startsWith('{')) return AccountExtension.tryParse(text);
380
427
  }
381
428
  return undefined;
382
429
  }
@@ -391,6 +438,22 @@ function entryOf(s: Segment, defaultBankCode: string, defaultCountry: string, li
391
438
  };
392
439
  }
393
440
 
441
+ /** Whether `code` is a country code, three digits, such as `280`; a German bank code has eight. */
442
+ function isCountryCode(code: string | undefined): boolean {
443
+ return code !== undefined && /^[0-9]{3}$/.test(code);
444
+ }
445
+
446
+ /**
447
+ * The national account of the HIUPD `s`: number, sub-account, country and bank code (KTV2), by position in every
448
+ * version, so that a sub-account or bank code the bank leaves out is empty. Three elements whose second is a country
449
+ * code and whose third is not are the older layout without sub-account: number, country and bank code.
450
+ */
451
+ function nationalAccountOf(s: Segment): (string | undefined)[] {
452
+ const ktv = s.texts(1);
453
+ const [number, second, third] = ktv;
454
+ return ktv.length === 3 && isCountryCode(second) && !isCountryCode(third) ? [number, undefined, second, third] : ktv;
455
+ }
456
+
394
457
  function accountOf(
395
458
  s: Segment,
396
459
  defaultBankCode: string,
@@ -398,19 +461,7 @@ function accountOf(
398
461
  listsAll: boolean,
399
462
  ): Account | undefined {
400
463
  const version = s.version;
401
- const nationalAccount = s.texts(1);
402
- let number: string | undefined;
403
- let subAccount: string | undefined;
404
- let country: string | undefined;
405
- let bank: string | undefined;
406
- if (nationalAccount.length >= 4) {
407
- [number, subAccount, country, bank] = nationalAccount;
408
- } else if (nationalAccount.length === 3) {
409
- // Older banks leave out the sub-account.
410
- [number, country, bank] = nationalAccount;
411
- } else if (nationalAccount.length > 0) {
412
- number = nationalAccount[0];
413
- }
464
+ const [number, subAccount, country, bank] = nationalAccountOf(s);
414
465
  const iban = version >= 6 ? s.text(2) : undefined;
415
466
  if (number === undefined && iban === undefined) return undefined;
416
467
  // Field positions by HIUPD version: version 6 inserts the IBAN, version 5 the account type.
@@ -420,9 +471,6 @@ function accountOf(
420
471
  : version === 5
421
472
  ? { customer: 2, type: 3, currency: 4, name1: 5, name2: 6, product: 7 }
422
473
  : { customer: 2, type: undefined, currency: 3, name1: 4, name2: 5, product: 6 };
423
- const owner = defined([s.text(fields.name1), s.text(fields.name2)])
424
- .join(' ')
425
- .trim();
426
474
  return new Account({
427
475
  accountNumber: number,
428
476
  subAccount,
@@ -432,18 +480,70 @@ function accountOf(
432
480
  customerId: s.text(fields.customer),
433
481
  typeCode: fields.type === undefined ? undefined : s.integer(fields.type),
434
482
  currency: s.text(fields.currency),
435
- ownerName: owner === '' ? undefined : owner,
483
+ ownerName: nonBlank(defined([s.text(fields.name1), s.text(fields.name2)]).join(' ')),
436
484
  productName: s.text(fields.product),
437
485
  allowedTransactions: transactionsOf(s),
438
486
  allowedTransactionsComplete: listsAll,
439
487
  });
440
488
  }
441
489
 
490
+ /**
491
+ * The version of `transaction` to send, for `account` where it concerns one: the highest the bank offers and the client
492
+ * `implemented`. `undefined` where there is none, or the parameter data do not permit the transaction
493
+ * (`permitsTransaction`).
494
+ */
495
+ export function offeredVersion(
496
+ bpd: Bpd3 | undefined,
497
+ upd: Upd3 | undefined,
498
+ transaction: string,
499
+ implemented: ReadonlySet<number>,
500
+ options: { account?: Account; requiresEntry?: boolean },
501
+ ): number | undefined {
502
+ return permitsTransaction(bpd, upd, transaction, options) ? bpd?.bestVersion(transaction, implemented) : undefined;
503
+ }
504
+
505
+ /**
506
+ * Whether the parameter data let the user send `transaction`, for `account` where it concerns one: the bank allows it
507
+ * with PIN/TAN, and the user parameter data permit it (Formals E.1, F.3.4; PIN/TAN B.1, B.8.1).
508
+ *
509
+ * - `account`, which the session has taken as the bank knows it (`SepaAccounts.complete`), permits what it lists, and
510
+ * anything else only where its list is incomplete ("UPD-Verwendung" 1). One only the SEPA account list names lists
511
+ * nothing, and so permits anything; where the lists are complete, the session refuses one the bank names nowhere.
512
+ * - A transaction without account, such as the TAN media list, is permitted where some entry lists it or the lists are
513
+ * incomplete.
514
+ * - One that `requiresEntry`, the SEPA account list, must be listed even where the lists are incomplete (Formals F.3.4),
515
+ * unless they list nothing at all, which tells nothing.
516
+ */
517
+ function permitsTransaction(
518
+ bpd: Bpd3 | undefined,
519
+ upd: Upd3 | undefined,
520
+ transaction: string,
521
+ options: { account?: Account; requiresEntry?: boolean },
522
+ ): boolean {
523
+ if (bpd !== undefined && !bpd.pinTan.allowsWithPinTan(transaction)) return false;
524
+ if (upd === undefined) return true;
525
+ if (options.account !== undefined) return allows(options.account, transaction);
526
+ const listed = upd.listedTransactions;
527
+ if (listed.has(transaction)) return true;
528
+ return !upd.listsAllTransactions && (!(options.requiresEntry ?? false) || listed.size === 0);
529
+ }
530
+
531
+ /** Whether the bank lists `transaction`, such as `HKCAZ`, as permitted for `account`. */
532
+ export function lists(account: Account, transaction: string): boolean {
533
+ return account.allowedTransactions.includes(transaction);
534
+ }
535
+
536
+ /**
537
+ * Whether the bank may permit `transaction` on `account`: it lists it, or its list is not complete
538
+ * (`Account.allowedTransactionsComplete`), so that only the bank can tell.
539
+ */
540
+ function allows(account: Account, transaction: string): boolean {
541
+ return lists(account, transaction) || !account.allowedTransactionsComplete;
542
+ }
543
+
442
544
  /** Serializes parameter segments for the saved state. */
443
- export function encodeSegments(segments: readonly Segment[]): string {
444
- const out = new BytesBuilder();
445
- for (const s of segments) out.add(s.encode());
446
- return base64Encode(out.takeBytes());
545
+ function encodeSegments(segments: readonly Segment[]): string {
546
+ return base64Encode(concatBytes(segments.map((s) => s.encode())));
447
547
  }
448
548
 
449
549
  export function decodeSegments(data: string): Segment[] {