@coderoftime/fints 0.0.0-stage → 0.1.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 (254) hide show
  1. package/CHANGELOG.md +11 -0
  2. package/LICENSE +28 -0
  3. package/README.md +156 -2
  4. package/dist/core/account.d.ts +140 -0
  5. package/dist/core/account.d.ts.map +1 -0
  6. package/dist/core/account.js +174 -0
  7. package/dist/core/account.js.map +1 -0
  8. package/dist/core/amount.d.ts +62 -0
  9. package/dist/core/amount.d.ts.map +1 -0
  10. package/dist/core/amount.js +189 -0
  11. package/dist/core/amount.js.map +1 -0
  12. package/dist/core/balance.d.ts +54 -0
  13. package/dist/core/balance.d.ts.map +1 -0
  14. package/dist/core/balance.js +53 -0
  15. package/dist/core/balance.js.map +1 -0
  16. package/dist/core/bank-info.d.ts +49 -0
  17. package/dist/core/bank-info.d.ts.map +1 -0
  18. package/dist/core/bank-info.js +52 -0
  19. package/dist/core/bank-info.js.map +1 -0
  20. package/dist/core/client.d.ts +126 -0
  21. package/dist/core/client.d.ts.map +1 -0
  22. package/dist/core/client.js +2 -0
  23. package/dist/core/client.js.map +1 -0
  24. package/dist/core/config.d.ts +68 -0
  25. package/dist/core/config.d.ts.map +1 -0
  26. package/dist/core/config.js +113 -0
  27. package/dist/core/config.js.map +1 -0
  28. package/dist/core/errors.d.ts +91 -0
  29. package/dist/core/errors.d.ts.map +1 -0
  30. package/dist/core/errors.js +182 -0
  31. package/dist/core/errors.js.map +1 -0
  32. package/dist/core/feedback.d.ts +69 -0
  33. package/dist/core/feedback.d.ts.map +1 -0
  34. package/dist/core/feedback.js +124 -0
  35. package/dist/core/feedback.js.map +1 -0
  36. package/dist/core/transaction.d.ts +98 -0
  37. package/dist/core/transaction.d.ts.map +1 -0
  38. package/dist/core/transaction.js +91 -0
  39. package/dist/core/transaction.js.map +1 -0
  40. package/dist/core/transport.d.ts +37 -0
  41. package/dist/core/transport.d.ts.map +1 -0
  42. package/dist/core/transport.js +92 -0
  43. package/dist/core/transport.js.map +1 -0
  44. package/dist/formats/camt.d.ts +12 -0
  45. package/dist/formats/camt.d.ts.map +1 -0
  46. package/dist/formats/camt.js +159 -0
  47. package/dist/formats/camt.js.map +1 -0
  48. package/dist/formats/mt940.d.ts +42 -0
  49. package/dist/formats/mt940.d.ts.map +1 -0
  50. package/dist/formats/mt940.js +297 -0
  51. package/dist/formats/mt940.js.map +1 -0
  52. package/dist/formats/readers.d.ts +11 -0
  53. package/dist/formats/readers.d.ts.map +1 -0
  54. package/dist/formats/readers.js +47 -0
  55. package/dist/formats/readers.js.map +1 -0
  56. package/dist/formats/sepa-purpose.d.ts +22 -0
  57. package/dist/formats/sepa-purpose.d.ts.map +1 -0
  58. package/dist/formats/sepa-purpose.js +68 -0
  59. package/dist/formats/sepa-purpose.js.map +1 -0
  60. package/dist/index.d.ts +28 -0
  61. package/dist/index.d.ts.map +1 -0
  62. package/dist/index.js +27 -0
  63. package/dist/index.js.map +1 -0
  64. package/dist/session/app-approval.d.ts +60 -0
  65. package/dist/session/app-approval.d.ts.map +1 -0
  66. package/dist/session/app-approval.js +215 -0
  67. package/dist/session/app-approval.js.map +1 -0
  68. package/dist/session/bank-connection.d.ts +28 -0
  69. package/dist/session/bank-connection.d.ts.map +1 -0
  70. package/dist/session/bank-connection.js +48 -0
  71. package/dist/session/bank-connection.js.map +1 -0
  72. package/dist/session/dialog-runner.d.ts +34 -0
  73. package/dist/session/dialog-runner.d.ts.map +1 -0
  74. package/dist/session/dialog-runner.js +112 -0
  75. package/dist/session/dialog-runner.js.map +1 -0
  76. package/dist/session/login-guard.d.ts +73 -0
  77. package/dist/session/login-guard.d.ts.map +1 -0
  78. package/dist/session/login-guard.js +109 -0
  79. package/dist/session/login-guard.js.map +1 -0
  80. package/dist/session/parameter-data.d.ts +83 -0
  81. package/dist/session/parameter-data.d.ts.map +1 -0
  82. package/dist/session/parameter-data.js +68 -0
  83. package/dist/session/parameter-data.js.map +1 -0
  84. package/dist/session/pin-tan-client.d.ts +54 -0
  85. package/dist/session/pin-tan-client.d.ts.map +1 -0
  86. package/dist/session/pin-tan-client.js +153 -0
  87. package/dist/session/pin-tan-client.js.map +1 -0
  88. package/dist/session/pin-tan-dialog.d.ts +71 -0
  89. package/dist/session/pin-tan-dialog.d.ts.map +1 -0
  90. package/dist/session/pin-tan-dialog.js +124 -0
  91. package/dist/session/pin-tan-dialog.js.map +1 -0
  92. package/dist/session/protocol.d.ts +209 -0
  93. package/dist/session/protocol.d.ts.map +1 -0
  94. package/dist/session/protocol.js +75 -0
  95. package/dist/session/protocol.js.map +1 -0
  96. package/dist/session/response-handler.d.ts +48 -0
  97. package/dist/session/response-handler.d.ts.map +1 -0
  98. package/dist/session/response-handler.js +121 -0
  99. package/dist/session/response-handler.js.map +1 -0
  100. package/dist/session/sepa-accounts.d.ts +34 -0
  101. package/dist/session/sepa-accounts.d.ts.map +1 -0
  102. package/dist/session/sepa-accounts.js +96 -0
  103. package/dist/session/sepa-accounts.js.map +1 -0
  104. package/dist/session/session-setup.d.ts +72 -0
  105. package/dist/session/session-setup.d.ts.map +1 -0
  106. package/dist/session/session-setup.js +160 -0
  107. package/dist/session/session-setup.js.map +1 -0
  108. package/dist/session/session-state.d.ts +47 -0
  109. package/dist/session/session-state.d.ts.map +1 -0
  110. package/dist/session/session-state.js +147 -0
  111. package/dist/session/session-state.js.map +1 -0
  112. package/dist/session/tan-selection.d.ts +88 -0
  113. package/dist/session/tan-selection.d.ts.map +1 -0
  114. package/dist/session/tan-selection.js +178 -0
  115. package/dist/session/tan-selection.js.map +1 -0
  116. package/dist/session/user-prompts.d.ts +61 -0
  117. package/dist/session/user-prompts.d.ts.map +1 -0
  118. package/dist/session/user-prompts.js +164 -0
  119. package/dist/session/user-prompts.js.map +1 -0
  120. package/dist/tan/challenge.d.ts +114 -0
  121. package/dist/tan/challenge.d.ts.map +1 -0
  122. package/dist/tan/challenge.js +185 -0
  123. package/dist/tan/challenge.js.map +1 -0
  124. package/dist/tan/flicker.d.ts +36 -0
  125. package/dist/tan/flicker.d.ts.map +1 -0
  126. package/dist/tan/flicker.js +262 -0
  127. package/dist/tan/flicker.js.map +1 -0
  128. package/dist/tan/tan-image.d.ts +21 -0
  129. package/dist/tan/tan-image.d.ts.map +1 -0
  130. package/dist/tan/tan-image.js +41 -0
  131. package/dist/tan/tan-image.js.map +1 -0
  132. package/dist/tan/tan-models.d.ts +145 -0
  133. package/dist/tan/tan-models.d.ts.map +1 -0
  134. package/dist/tan/tan-models.js +148 -0
  135. package/dist/tan/tan-models.js.map +1 -0
  136. package/dist/util/async.d.ts +24 -0
  137. package/dist/util/async.d.ts.map +1 -0
  138. package/dist/util/async.js +49 -0
  139. package/dist/util/async.js.map +1 -0
  140. package/dist/util/bytes.d.ts +23 -0
  141. package/dist/util/bytes.d.ts.map +1 -0
  142. package/dist/util/bytes.js +136 -0
  143. package/dist/util/bytes.js.map +1 -0
  144. package/dist/util/call-context.d.ts +12 -0
  145. package/dist/util/call-context.d.ts.map +1 -0
  146. package/dist/util/call-context.js +29 -0
  147. package/dist/util/call-context.js.map +1 -0
  148. package/dist/util/dates.d.ts +62 -0
  149. package/dist/util/dates.d.ts.map +1 -0
  150. package/dist/util/dates.js +169 -0
  151. package/dist/util/dates.js.map +1 -0
  152. package/dist/util/errors.d.ts +9 -0
  153. package/dist/util/errors.d.ts.map +1 -0
  154. package/dist/util/errors.js +15 -0
  155. package/dist/util/errors.js.map +1 -0
  156. package/dist/util/immutable-map.d.ts +16 -0
  157. package/dist/util/immutable-map.d.ts.map +1 -0
  158. package/dist/util/immutable-map.js +38 -0
  159. package/dist/util/immutable-map.js.map +1 -0
  160. package/dist/util/json.d.ts +13 -0
  161. package/dist/util/json.d.ts.map +1 -0
  162. package/dist/util/json.js +44 -0
  163. package/dist/util/json.js.map +1 -0
  164. package/dist/util/text.d.ts +20 -0
  165. package/dist/util/text.d.ts.map +1 -0
  166. package/dist/util/text.js +78 -0
  167. package/dist/util/text.js.map +1 -0
  168. package/dist/util/xml.d.ts +35 -0
  169. package/dist/util/xml.d.ts.map +1 -0
  170. package/dist/util/xml.js +183 -0
  171. package/dist/util/xml.js.map +1 -0
  172. package/dist/v3/account-extension.d.ts +25 -0
  173. package/dist/v3/account-extension.d.ts.map +1 -0
  174. package/dist/v3/account-extension.js +176 -0
  175. package/dist/v3/account-extension.js.map +1 -0
  176. package/dist/v3/client.d.ts +24 -0
  177. package/dist/v3/client.d.ts.map +1 -0
  178. package/dist/v3/client.js +16 -0
  179. package/dist/v3/client.js.map +1 -0
  180. package/dist/v3/dialog.d.ts +78 -0
  181. package/dist/v3/dialog.d.ts.map +1 -0
  182. package/dist/v3/dialog.js +138 -0
  183. package/dist/v3/dialog.js.map +1 -0
  184. package/dist/v3/message.d.ts +72 -0
  185. package/dist/v3/message.d.ts.map +1 -0
  186. package/dist/v3/message.js +259 -0
  187. package/dist/v3/message.js.map +1 -0
  188. package/dist/v3/parameters.d.ts +79 -0
  189. package/dist/v3/parameters.d.ts.map +1 -0
  190. package/dist/v3/parameters.js +396 -0
  191. package/dist/v3/parameters.js.map +1 -0
  192. package/dist/v3/protocol.d.ts +68 -0
  193. package/dist/v3/protocol.d.ts.map +1 -0
  194. package/dist/v3/protocol.js +263 -0
  195. package/dist/v3/protocol.js.map +1 -0
  196. package/dist/v3/segments.d.ts +89 -0
  197. package/dist/v3/segments.d.ts.map +1 -0
  198. package/dist/v3/segments.js +283 -0
  199. package/dist/v3/segments.js.map +1 -0
  200. package/dist/v3/syntax.d.ts +76 -0
  201. package/dist/v3/syntax.d.ts.map +1 -0
  202. package/dist/v3/syntax.js +301 -0
  203. package/dist/v3/syntax.js.map +1 -0
  204. package/package.json +63 -4
  205. package/src/core/account.ts +250 -0
  206. package/src/core/amount.ts +209 -0
  207. package/src/core/balance.ts +84 -0
  208. package/src/core/bank-info.ts +66 -0
  209. package/src/core/client.ts +149 -0
  210. package/src/core/config.ts +141 -0
  211. package/src/core/errors.ts +203 -0
  212. package/src/core/feedback.ts +137 -0
  213. package/src/core/transaction.ts +156 -0
  214. package/src/core/transport.ts +114 -0
  215. package/src/formats/camt.ts +169 -0
  216. package/src/formats/mt940.ts +333 -0
  217. package/src/formats/readers.ts +56 -0
  218. package/src/formats/sepa-purpose.ts +75 -0
  219. package/src/index.ts +68 -0
  220. package/src/session/app-approval.ts +265 -0
  221. package/src/session/bank-connection.ts +51 -0
  222. package/src/session/dialog-runner.ts +119 -0
  223. package/src/session/login-guard.ts +135 -0
  224. package/src/session/parameter-data.ts +137 -0
  225. package/src/session/pin-tan-client.ts +186 -0
  226. package/src/session/pin-tan-dialog.ts +160 -0
  227. package/src/session/protocol.ts +311 -0
  228. package/src/session/response-handler.ts +130 -0
  229. package/src/session/sepa-accounts.ts +97 -0
  230. package/src/session/session-setup.ts +165 -0
  231. package/src/session/session-state.ts +170 -0
  232. package/src/session/tan-selection.ts +192 -0
  233. package/src/session/user-prompts.ts +174 -0
  234. package/src/tan/challenge.ts +252 -0
  235. package/src/tan/flicker.ts +270 -0
  236. package/src/tan/tan-image.ts +36 -0
  237. package/src/tan/tan-models.ts +247 -0
  238. package/src/util/async.ts +53 -0
  239. package/src/util/bytes.ts +135 -0
  240. package/src/util/call-context.ts +40 -0
  241. package/src/util/dates.ts +193 -0
  242. package/src/util/errors.ts +16 -0
  243. package/src/util/immutable-map.ts +47 -0
  244. package/src/util/json.ts +51 -0
  245. package/src/util/text.ts +82 -0
  246. package/src/util/xml.ts +172 -0
  247. package/src/v3/account-extension.ts +190 -0
  248. package/src/v3/client.ts +29 -0
  249. package/src/v3/dialog.ts +199 -0
  250. package/src/v3/message.ts +321 -0
  251. package/src/v3/parameters.ts +451 -0
  252. package/src/v3/protocol.ts +331 -0
  253. package/src/v3/segments.ts +331 -0
  254. package/src/v3/syntax.ts +307 -0
@@ -0,0 +1,451 @@
1
+ // Internal to the package, and named after the FinTS specification. Documentation is where the meaning is not evident.
2
+
3
+ import { Account } from '../core/account.js';
4
+ import type { Balance } from '../core/balance.js';
5
+ import { BankInfo } from '../core/bank-info.js';
6
+ import { highestCommonVersion, type BankParameters, type UserParameters } from '../session/parameter-data.js';
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';
10
+ import { AccountExtension } from './account-extension.js';
11
+ import { parseSegments, type Segment } from './syntax.js';
12
+
13
+ /**
14
+ * A two-step TAN method as HITANS describes it: whether the client can run it, and the HITANS version, which is the
15
+ * version of the HKTAN segments the method is used with.
16
+ */
17
+ interface OfferedMethod {
18
+ readonly method: TanMethod;
19
+ readonly runnable: boolean;
20
+ readonly version: number;
21
+ }
22
+
23
+ /** Positions of the data elements in the header of the bank parameter data (HIBPA), after the segment header. */
24
+ const Hibpa = {
25
+ version: 1,
26
+ /** Country code, then bank code. */
27
+ bankId: 2,
28
+ bankName: 3,
29
+ languages: 5,
30
+ fintsVersions: 6,
31
+ maxMessageSize: 7,
32
+ minTimeout: 8,
33
+ maxTimeout: 9,
34
+ } as const;
35
+
36
+ /**
37
+ * The data element of every parameter segment (`HI…S`) that holds its specific parameters, after the maximum number of
38
+ * orders, the minimum number of signatures and the security class.
39
+ */
40
+ const parameterGroup = 4;
41
+
42
+ /** The standard language in the communication access (HIKOM). */
43
+ const hikomStandardLanguage = 2;
44
+
45
+ function defined<T>(values: readonly (T | undefined)[]): T[] {
46
+ return values.filter((v): v is T => v !== undefined);
47
+ }
48
+
49
+ /** FinTS 3.0 bank parameter data (BPD): HIBPA and the parameter segments (`HI…S`), parsed once. */
50
+ export class Bpd3 implements BankParameters {
51
+ readonly bankInfo: BankInfo;
52
+
53
+ /** The two-step TAN methods of HITANS versions 6 and 7, by security function. */
54
+ private readonly offeredMethods: ReadonlyMap<string, OfferedMethod>;
55
+
56
+ readonly tanMethods: readonly TanMethod[];
57
+ readonly unsupportedTanMethods: readonly string[];
58
+ readonly pinTan: PinTanParameters;
59
+
60
+ /** camt formats the bank offers for HKCAZ. */
61
+ readonly camtFormats: readonly string[];
62
+
63
+ /** Versions of each parameter segment, by segment type. */
64
+ private readonly versions: ReadonlyMap<string, ReadonlySet<number>>;
65
+
66
+ /**
67
+ * The dialog language HKVVB names: the bank's standard language (HIKOM), else the first it supports (HIBPA). Only
68
+ * these may be sent once the parameter data are known; `0`, the default, only before (Formals C.3.1.3).
69
+ */
70
+ readonly dialogLanguage: number;
71
+
72
+ private constructor(
73
+ readonly segments: readonly Segment[],
74
+ hibpa: Segment,
75
+ fallbackBankCode: string,
76
+ ) {
77
+ this.bankInfo = new BankInfo({
78
+ bankCode: hibpa.text(Hibpa.bankId, 1) ?? fallbackBankCode,
79
+ name: hibpa.text(Hibpa.bankName),
80
+ bpdVersion: hibpa.integer(Hibpa.version) ?? 0,
81
+ languages: defined(hibpa.texts(Hibpa.languages)),
82
+ supportedVersions: defined(hibpa.texts(Hibpa.fintsVersions)),
83
+ maxMessageSize: hibpa.integer(Hibpa.maxMessageSize),
84
+ minTimeout: hibpa.integer(Hibpa.minTimeout),
85
+ maxTimeout: hibpa.integer(Hibpa.maxTimeout),
86
+ });
87
+ this.offeredMethods = parseTanMethods(segments);
88
+ const offered = [...this.offeredMethods.values()];
89
+ this.tanMethods = Object.freeze(offered.filter((o) => o.runnable).map((o) => o.method));
90
+ this.unsupportedTanMethods = Object.freeze(
91
+ offered.filter((o) => !o.runnable).map((o) => o.method.securityFunction),
92
+ );
93
+ this.pinTan = PinTanParameters.parse(segments);
94
+ this.camtFormats = segments
95
+ .filter((s) => s.type === 'HICAZS')
96
+ .flatMap((s) => defined(s.texts(parameterGroup)))
97
+ .filter((format) => format.includes('camt'));
98
+ this.versions = versionsByType(segments);
99
+ this.dialogLanguage = dialogLanguageOf(segments, this.bankInfo);
100
+ }
101
+
102
+ get version(): number {
103
+ return this.bankInfo.bpdVersion;
104
+ }
105
+
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
+ /** The BPD in `all`, or `undefined` if they contain no HIBPA segment. */
115
+ static fromSegments(all: readonly Segment[], fallbackBankCode: string): Bpd3 | undefined {
116
+ const segments = all.filter(isParameterSegment);
117
+ const hibpa = segments.find((s) => s.type === 'HIBPA');
118
+ return hibpa === undefined ? undefined : new Bpd3(segments, hibpa, fallbackBankCode);
119
+ }
120
+
121
+ /** The parameter segment type of a business transaction: `HKSAL` → `HISALS`. */
122
+ static parameterName(transaction: string): string {
123
+ return `HI${transaction.substring(2)}S`;
124
+ }
125
+
126
+ bestVersion(transaction: string, implemented: ReadonlySet<number>): number | undefined {
127
+ return highestCommonVersion(this.versions.get(Bpd3.parameterName(transaction)), implemented);
128
+ }
129
+
130
+ /** The version of the HKTAN segments that go with `method`; `undefined` for a method the bank does not offer. */
131
+ tanVersion(method: TanMethod): number | undefined {
132
+ return this.offeredMethods.get(method.securityFunction)?.version;
133
+ }
134
+ }
135
+
136
+ function dialogLanguageOf(segments: readonly Segment[], bankInfo: BankInfo): number {
137
+ const languages = new Set([1, 2, 3]);
138
+ const standard = segments.find((s) => s.type === 'HIKOM')?.integer(hikomStandardLanguage);
139
+ if (standard !== undefined && languages.has(standard)) return standard;
140
+ return bankInfo.languages.map((l) => tryParseInt(l)).find((l) => l !== undefined && languages.has(l)) ?? 0;
141
+ }
142
+
143
+ function isParameterSegment(s: Segment): boolean {
144
+ const type = s.type;
145
+ if (['HIBPA', 'HIKOM', 'HISHV', 'HIKPV'].includes(type)) return true;
146
+ if (type === 'HIRMS' || type === 'HIRMG') return false;
147
+ return type.length >= 5 && (type.startsWith('HI') || type.startsWith('DI')) && type.endsWith('S');
148
+ }
149
+
150
+ function versionsByType(segments: readonly Segment[]): Map<string, Set<number>> {
151
+ const versions = new Map<string, Set<number>>();
152
+ for (const s of segments) {
153
+ let set = versions.get(s.type);
154
+ if (set === undefined) versions.set(s.type, (set = new Set()));
155
+ set.add(s.version);
156
+ }
157
+ return versions;
158
+ }
159
+
160
+ /** Parses the two-step TAN methods from HITANS versions 6 and 7; for a method offered in several, the highest wins. */
161
+ function parseTanMethods(segments: readonly Segment[]): Map<string, OfferedMethod> {
162
+ const byCode = new Map<string, OfferedMethod>();
163
+ for (const s of segments.filter((s) => s.type === 'HITANS')) {
164
+ for (const offered of parseHitans(s)) {
165
+ const existing = byCode.get(offered.method.securityFunction);
166
+ if (existing === undefined || offered.version > existing.version) {
167
+ byCode.set(offered.method.securityFunction, offered);
168
+ }
169
+ }
170
+ }
171
+ return byCode;
172
+ }
173
+
174
+ /** The fields describing one TAN method in HITANS, in order; version 6 ends with `activeMediaCount`. */
175
+ const HitansField = {
176
+ securityFunction: 0,
177
+ processVariant: 1,
178
+ technicalId: 2,
179
+ dkMethod: 3,
180
+ dkVersion: 4,
181
+ name: 5,
182
+ maxTanLength: 6,
183
+ tanFormat: 7,
184
+ returnValueText: 8,
185
+ /** "Maximale Länge des Rückgabewertes": the return value is the challenge. */
186
+ maxChallengeLength: 9,
187
+ multipleTans: 10,
188
+ tanTimeAndDialogRelation: 11,
189
+ orderCancellation: 12,
190
+ smsChargeAccountRequired: 13,
191
+ orderingAccountRequired: 14,
192
+ challengeClassRequired: 15,
193
+ structuredChallenge: 16,
194
+ initializationMode: 17,
195
+ mediumNameRequired: 18,
196
+ hhdUcRequired: 19,
197
+ activeMediaCount: 20,
198
+ maxStatusPolls: 21,
199
+ firstPollDelay: 22,
200
+ pollInterval: 23,
201
+ manualConfirmation: 24,
202
+ automaticPolling: 25,
203
+ } as const;
204
+
205
+ function parseHitans(s: Segment): OfferedMethod[] {
206
+ const version = s.version;
207
+ if (version !== 6 && version !== 7) return [];
208
+ // Version 7 adds the fields for decoupled approval.
209
+ const fieldsPerMethod = version === 6 ? HitansField.activeMediaCount + 1 : HitansField.automaticPolling + 1;
210
+ const all = s.texts(parameterGroup);
211
+ const methods: OfferedMethod[] = [];
212
+ // The group starts with three general fields, then lists the methods.
213
+ for (let start = 3; start < all.length; start += fieldsPerMethod) {
214
+ const fields = all.slice(start, start + fieldsPerMethod);
215
+ const field = (f: number): string | undefined => fields[f];
216
+ const number = (f: number): number | undefined => tryParseInt(field(f));
217
+ const code = field(HitansField.securityFunction);
218
+ if (code === undefined) continue;
219
+ // Process variant 1 needs the order hash and another message flow; the client runs variant 2, the one of PSD2
220
+ // strong customer authentication. An SMS charge account or an HHD_UC answer, where required, it cannot send.
221
+ const runnable =
222
+ number(HitansField.processVariant) !== 1 &&
223
+ field(HitansField.smsChargeAccountRequired) !== '2' &&
224
+ field(HitansField.hhdUcRequired) !== 'J';
225
+ const dkMethod = field(HitansField.dkMethod);
226
+ const decoupledFields = version === 7;
227
+ const method = new TanMethod({
228
+ securityFunction: code,
229
+ technicalId: field(HitansField.technicalId),
230
+ dkMethod,
231
+ dkVersion: field(HitansField.dkVersion),
232
+ name: field(HitansField.name) ?? code,
233
+ maxTanLength: number(HitansField.maxTanLength),
234
+ numericTan: field(HitansField.tanFormat) !== '2',
235
+ returnValueText: field(HitansField.returnValueText),
236
+ maxChallengeLength: number(HitansField.maxChallengeLength),
237
+ structuredChallenge: field(HitansField.structuredChallenge) === 'J',
238
+ mediumRequirement: tanMediumRequirementOf(field(HitansField.mediumNameRequired)),
239
+ activeMediaCount: number(HitansField.activeMediaCount),
240
+ decoupled: decoupledFields && dkMethod !== undefined && dkMethod.toLowerCase().startsWith('decoupled'),
241
+ decoupledMaxPolls: decoupledFields ? number(HitansField.maxStatusPolls) : undefined,
242
+ decoupledFirstPollDelaySeconds: decoupledFields ? number(HitansField.firstPollDelay) : undefined,
243
+ decoupledPollIntervalSeconds: decoupledFields ? number(HitansField.pollInterval) : undefined,
244
+ decoupledManualConfirmation: !decoupledFields || field(HitansField.manualConfirmation) !== 'N',
245
+ decoupledAutomaticPolling: !decoupledFields || field(HitansField.automaticPolling) !== 'N',
246
+ });
247
+ methods.push({ method, runnable, version });
248
+ }
249
+ return methods;
250
+ }
251
+
252
+ /**
253
+ * PIN/TAN parameters (HIPINS): the PIN length, which business transactions PIN/TAN allows, and which of them need a
254
+ * TAN.
255
+ */
256
+ export class PinTanParameters {
257
+ private constructor(
258
+ private readonly tanRequired: ReadonlyMap<string, boolean>,
259
+ /** The shortest and longest PIN the bank takes. */
260
+ readonly pinLength: { min: number | undefined; max: number | undefined },
261
+ ) {}
262
+
263
+ /** Reads the HIPINS segment of the highest version. */
264
+ 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 });
270
+ const fields = hipins.texts(parameterGroup);
271
+ const tanRequired = new Map<string, boolean>();
272
+ // Five general fields, the PIN lengths first; then pairs of segment type and TAN flag.
273
+ for (let i = 5; i + 1 < fields.length; i += 2) {
274
+ const type = fields[i];
275
+ if (type !== undefined) tanRequired.set(type, fields[i + 1] === 'J');
276
+ }
277
+ return new PinTanParameters(tanRequired, { min: tryParseInt(fields[0]), max: tryParseInt(fields[1]) });
278
+ }
279
+
280
+ /** Whether the bank wants a TAN for the business transaction of `segmentType`, such as `HKKAZ`. */
281
+ requiresTan(segmentType: string): boolean {
282
+ return this.tanRequired.get(segmentType) ?? false;
283
+ }
284
+
285
+ /** Whether PIN/TAN allows `segmentType` at all: HIPINS lists it, or lists no business transactions (PIN/TAN B.8.1). */
286
+ allowsWithPinTan(segmentType: string): boolean {
287
+ return this.tanRequired.size === 0 || this.tanRequired.has(segmentType);
288
+ }
289
+ }
290
+
291
+ interface UpdEntry {
292
+ readonly account: Account;
293
+ readonly extension: AccountExtension | undefined;
294
+ }
295
+
296
+ /** The version and the usage ("UPD-Verwendung") of the user parameter data in HIUPA. */
297
+ const hiupaVersion = 2;
298
+ const hiupaUsage = 3;
299
+
300
+ /** Business transaction codes such as `HKSAL` or `DKPAE`. */
301
+ const transactionCode = /^[A-Z]{4,6}$/;
302
+
303
+ /** FinTS 3.0 user parameter data (UPD): HIUPA and the accounts (HIUPD), parsed once. */
304
+ export class Upd3 implements UserParameters {
305
+ readonly version: number;
306
+ readonly listsAllTransactions: boolean;
307
+ readonly listedTransactions: ReadonlySet<string>;
308
+
309
+ /** The accounts with the extension each carries, where it does. */
310
+ private readonly entries: readonly UpdEntry[];
311
+ readonly accounts: readonly Account[];
312
+
313
+ /** `listsAll` tells whether HIUPA declares the lists of permitted transactions complete ("UPD-Verwendung" 0). */
314
+ private constructor(
315
+ readonly segments: readonly Segment[],
316
+ hiupa: Segment,
317
+ listsAll: boolean,
318
+ defaultBankCode: string,
319
+ defaultCountry: string,
320
+ ) {
321
+ const hiupds = segments.filter((s) => s.type === 'HIUPD');
322
+ this.version = hiupa.integer(hiupaVersion) ?? 0;
323
+ this.listsAllTransactions = listsAll;
324
+ this.listedTransactions = new Set(hiupds.flatMap(transactionsOf));
325
+ this.entries = Object.freeze(
326
+ hiupds.map((s) => entryOf(s, defaultBankCode, defaultCountry, listsAll)).filter((e) => e !== undefined),
327
+ );
328
+ this.accounts = Object.freeze(this.entries.map((e) => e.account));
329
+ }
330
+
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
+
336
+ completeBalance(account: Account): Balance | undefined {
337
+ return this.entries.find((e) => e.account.matches(account))?.extension?.completeBalance(account);
338
+ }
339
+
340
+ /** The UPD in `all`, or `undefined` if they contain no HIUPA segment. */
341
+ static fromSegments(all: readonly Segment[], defaultBankCode: string, defaultCountry: string): Upd3 | undefined {
342
+ const segments = all.filter((s) => s.type === 'HIUPA' || s.type === 'HIUPD');
343
+ const hiupa = segments.find((s) => s.type === 'HIUPA');
344
+ if (hiupa === undefined) return undefined;
345
+ return new Upd3(segments, hiupa, hiupa.text(hiupaUsage) === '0', defaultBankCode, defaultCountry);
346
+ }
347
+ }
348
+
349
+ /**
350
+ * The data element where the permitted transactions start, by HIUPD version: version 6 inserts the IBAN, version 5 the
351
+ * account type.
352
+ */
353
+ function allowedFrom(version: number): number {
354
+ return version >= 6 ? 10 : version === 5 ? 9 : 8;
355
+ }
356
+
357
+ /** The permitted transactions of the HIUPD `s`. */
358
+ function transactionsOf(s: Segment): string[] {
359
+ const codes: string[] = [];
360
+ for (let i = allowedFrom(s.version); i < s.length; i++) {
361
+ const code = s.text(i, 0);
362
+ if (code !== undefined && transactionCode.test(code)) codes.push(code);
363
+ }
364
+ return codes;
365
+ }
366
+
367
+ /**
368
+ * The account extension of `s` (version 6, "Erweiterung, kontobezogen"): the JSON object behind the permitted
369
+ * transactions, whose count varies. A bank that leaves its colons unescaped splits it into group elements, which are
370
+ * joined again.
371
+ */
372
+ function extensionOf(s: Segment): AccountExtension | undefined {
373
+ if (s.version < 6) return undefined;
374
+ 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);
380
+ }
381
+ return undefined;
382
+ }
383
+
384
+ function entryOf(s: Segment, defaultBankCode: string, defaultCountry: string, listsAll: boolean): UpdEntry | undefined {
385
+ const account = accountOf(s, defaultBankCode, defaultCountry, listsAll);
386
+ if (account === undefined) return undefined;
387
+ const extension = extensionOf(s);
388
+ return {
389
+ account: extension === undefined ? account : account.copyWith({ bic: extension.bic, dataStatus: extension.status }),
390
+ extension,
391
+ };
392
+ }
393
+
394
+ function accountOf(
395
+ s: Segment,
396
+ defaultBankCode: string,
397
+ defaultCountry: string,
398
+ listsAll: boolean,
399
+ ): Account | undefined {
400
+ 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
+ }
414
+ const iban = version >= 6 ? s.text(2) : undefined;
415
+ if (number === undefined && iban === undefined) return undefined;
416
+ // Field positions by HIUPD version: version 6 inserts the IBAN, version 5 the account type.
417
+ const fields =
418
+ version >= 6
419
+ ? { customer: 3, type: 4, currency: 5, name1: 6, name2: 7, product: 8 }
420
+ : version === 5
421
+ ? { customer: 2, type: 3, currency: 4, name1: 5, name2: 6, product: 7 }
422
+ : { 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
+ return new Account({
427
+ accountNumber: number,
428
+ subAccount,
429
+ countryCode: country ?? defaultCountry,
430
+ bankCode: bank ?? defaultBankCode,
431
+ iban,
432
+ customerId: s.text(fields.customer),
433
+ typeCode: fields.type === undefined ? undefined : s.integer(fields.type),
434
+ currency: s.text(fields.currency),
435
+ ownerName: owner === '' ? undefined : owner,
436
+ productName: s.text(fields.product),
437
+ allowedTransactions: transactionsOf(s),
438
+ allowedTransactionsComplete: listsAll,
439
+ });
440
+ }
441
+
442
+ /** 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());
447
+ }
448
+
449
+ export function decodeSegments(data: string): Segment[] {
450
+ return parseSegments(base64Decode(data));
451
+ }