@coderoftime/fints 0.0.0-stage → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (249) hide show
  1. package/CHANGELOG.md +152 -0
  2. package/LICENSE +28 -0
  3. package/README.md +246 -2
  4. package/dist/client.d.ts +48 -0
  5. package/dist/client.d.ts.map +1 -0
  6. package/dist/client.js +199 -0
  7. package/dist/client.js.map +1 -0
  8. package/dist/core/account.d.ts +168 -0
  9. package/dist/core/account.d.ts.map +1 -0
  10. package/dist/core/account.js +245 -0
  11. package/dist/core/account.js.map +1 -0
  12. package/dist/core/amount.d.ts +63 -0
  13. package/dist/core/amount.d.ts.map +1 -0
  14. package/dist/core/amount.js +192 -0
  15. package/dist/core/amount.js.map +1 -0
  16. package/dist/core/balance.d.ts +55 -0
  17. package/dist/core/balance.d.ts.map +1 -0
  18. package/dist/core/balance.js +54 -0
  19. package/dist/core/balance.js.map +1 -0
  20. package/dist/core/bank-info.d.ts +49 -0
  21. package/dist/core/bank-info.d.ts.map +1 -0
  22. package/dist/core/bank-info.js +52 -0
  23. package/dist/core/bank-info.js.map +1 -0
  24. package/dist/core/client.d.ts +161 -0
  25. package/dist/core/client.d.ts.map +1 -0
  26. package/dist/core/client.js +2 -0
  27. package/dist/core/client.js.map +1 -0
  28. package/dist/core/config.d.ts +80 -0
  29. package/dist/core/config.d.ts.map +1 -0
  30. package/dist/core/config.js +153 -0
  31. package/dist/core/config.js.map +1 -0
  32. package/dist/core/errors.d.ts +153 -0
  33. package/dist/core/errors.d.ts.map +1 -0
  34. package/dist/core/errors.js +247 -0
  35. package/dist/core/errors.js.map +1 -0
  36. package/dist/core/feedback.d.ts +72 -0
  37. package/dist/core/feedback.d.ts.map +1 -0
  38. package/dist/core/feedback.js +127 -0
  39. package/dist/core/feedback.js.map +1 -0
  40. package/dist/core/transaction.d.ts +103 -0
  41. package/dist/core/transaction.d.ts.map +1 -0
  42. package/dist/core/transaction.js +96 -0
  43. package/dist/core/transaction.js.map +1 -0
  44. package/dist/core/transport.d.ts +64 -0
  45. package/dist/core/transport.d.ts.map +1 -0
  46. package/dist/core/transport.js +149 -0
  47. package/dist/core/transport.js.map +1 -0
  48. package/dist/formats/camt.d.ts +32 -0
  49. package/dist/formats/camt.d.ts.map +1 -0
  50. package/dist/formats/camt.js +242 -0
  51. package/dist/formats/camt.js.map +1 -0
  52. package/dist/formats/mt940.d.ts +54 -0
  53. package/dist/formats/mt940.d.ts.map +1 -0
  54. package/dist/formats/mt940.js +376 -0
  55. package/dist/formats/mt940.js.map +1 -0
  56. package/dist/formats/readers.d.ts +15 -0
  57. package/dist/formats/readers.d.ts.map +1 -0
  58. package/dist/formats/readers.js +42 -0
  59. package/dist/formats/readers.js.map +1 -0
  60. package/dist/formats/record-budget.d.ts +7 -0
  61. package/dist/formats/record-budget.d.ts.map +1 -0
  62. package/dist/formats/record-budget.js +17 -0
  63. package/dist/formats/record-budget.js.map +1 -0
  64. package/dist/formats/sepa-purpose.d.ts +28 -0
  65. package/dist/formats/sepa-purpose.d.ts.map +1 -0
  66. package/dist/formats/sepa-purpose.js +89 -0
  67. package/dist/formats/sepa-purpose.js.map +1 -0
  68. package/dist/index.d.ts +28 -0
  69. package/dist/index.d.ts.map +1 -0
  70. package/dist/index.js +27 -0
  71. package/dist/index.js.map +1 -0
  72. package/dist/session/app-approval.d.ts +66 -0
  73. package/dist/session/app-approval.d.ts.map +1 -0
  74. package/dist/session/app-approval.js +226 -0
  75. package/dist/session/app-approval.js.map +1 -0
  76. package/dist/session/dialog-runner.d.ts +31 -0
  77. package/dist/session/dialog-runner.d.ts.map +1 -0
  78. package/dist/session/dialog-runner.js +111 -0
  79. package/dist/session/dialog-runner.js.map +1 -0
  80. package/dist/session/login-guard.d.ts +78 -0
  81. package/dist/session/login-guard.d.ts.map +1 -0
  82. package/dist/session/login-guard.js +181 -0
  83. package/dist/session/login-guard.js.map +1 -0
  84. package/dist/session/sepa-accounts.d.ts +40 -0
  85. package/dist/session/sepa-accounts.d.ts.map +1 -0
  86. package/dist/session/sepa-accounts.js +112 -0
  87. package/dist/session/sepa-accounts.js.map +1 -0
  88. package/dist/session/session-setup.d.ts +80 -0
  89. package/dist/session/session-setup.d.ts.map +1 -0
  90. package/dist/session/session-setup.js +182 -0
  91. package/dist/session/session-setup.js.map +1 -0
  92. package/dist/session/session-state.d.ts +50 -0
  93. package/dist/session/session-state.d.ts.map +1 -0
  94. package/dist/session/session-state.js +182 -0
  95. package/dist/session/session-state.js.map +1 -0
  96. package/dist/session/signed-dialog.d.ts +77 -0
  97. package/dist/session/signed-dialog.d.ts.map +1 -0
  98. package/dist/session/signed-dialog.js +170 -0
  99. package/dist/session/signed-dialog.js.map +1 -0
  100. package/dist/session/tan-selection.d.ts +96 -0
  101. package/dist/session/tan-selection.d.ts.map +1 -0
  102. package/dist/session/tan-selection.js +189 -0
  103. package/dist/session/tan-selection.js.map +1 -0
  104. package/dist/session/user-prompts.d.ts +65 -0
  105. package/dist/session/user-prompts.d.ts.map +1 -0
  106. package/dist/session/user-prompts.js +184 -0
  107. package/dist/session/user-prompts.js.map +1 -0
  108. package/dist/tan/challenge.d.ts +135 -0
  109. package/dist/tan/challenge.d.ts.map +1 -0
  110. package/dist/tan/challenge.js +184 -0
  111. package/dist/tan/challenge.js.map +1 -0
  112. package/dist/tan/flicker.d.ts +42 -0
  113. package/dist/tan/flicker.d.ts.map +1 -0
  114. package/dist/tan/flicker.js +296 -0
  115. package/dist/tan/flicker.js.map +1 -0
  116. package/dist/tan/tan-image.d.ts +22 -0
  117. package/dist/tan/tan-image.d.ts.map +1 -0
  118. package/dist/tan/tan-image.js +43 -0
  119. package/dist/tan/tan-image.js.map +1 -0
  120. package/dist/tan/tan-models.d.ts +149 -0
  121. package/dist/tan/tan-models.d.ts.map +1 -0
  122. package/dist/tan/tan-models.js +153 -0
  123. package/dist/tan/tan-models.js.map +1 -0
  124. package/dist/util/async.d.ts +22 -0
  125. package/dist/util/async.d.ts.map +1 -0
  126. package/dist/util/async.js +45 -0
  127. package/dist/util/async.js.map +1 -0
  128. package/dist/util/bytes.d.ts +22 -0
  129. package/dist/util/bytes.d.ts.map +1 -0
  130. package/dist/util/bytes.js +128 -0
  131. package/dist/util/bytes.js.map +1 -0
  132. package/dist/util/call-context.d.ts +12 -0
  133. package/dist/util/call-context.d.ts.map +1 -0
  134. package/dist/util/call-context.js +30 -0
  135. package/dist/util/call-context.js.map +1 -0
  136. package/dist/util/dates.d.ts +55 -0
  137. package/dist/util/dates.d.ts.map +1 -0
  138. package/dist/util/dates.js +189 -0
  139. package/dist/util/dates.js.map +1 -0
  140. package/dist/util/errors.d.ts +17 -0
  141. package/dist/util/errors.d.ts.map +1 -0
  142. package/dist/util/errors.js +24 -0
  143. package/dist/util/errors.js.map +1 -0
  144. package/dist/util/immutable-map.d.ts +16 -0
  145. package/dist/util/immutable-map.d.ts.map +1 -0
  146. package/dist/util/immutable-map.js +38 -0
  147. package/dist/util/immutable-map.js.map +1 -0
  148. package/dist/util/json.d.ts +17 -0
  149. package/dist/util/json.d.ts.map +1 -0
  150. package/dist/util/json.js +62 -0
  151. package/dist/util/json.js.map +1 -0
  152. package/dist/util/text.d.ts +37 -0
  153. package/dist/util/text.d.ts.map +1 -0
  154. package/dist/util/text.js +107 -0
  155. package/dist/util/text.js.map +1 -0
  156. package/dist/util/xml.d.ts +36 -0
  157. package/dist/util/xml.d.ts.map +1 -0
  158. package/dist/util/xml.js +194 -0
  159. package/dist/util/xml.js.map +1 -0
  160. package/dist/v3/account-extension.d.ts +25 -0
  161. package/dist/v3/account-extension.d.ts.map +1 -0
  162. package/dist/v3/account-extension.js +173 -0
  163. package/dist/v3/account-extension.js.map +1 -0
  164. package/dist/v3/bank-connection.d.ts +34 -0
  165. package/dist/v3/bank-connection.d.ts.map +1 -0
  166. package/dist/v3/bank-connection.js +58 -0
  167. package/dist/v3/bank-connection.js.map +1 -0
  168. package/dist/v3/dialog.d.ts +112 -0
  169. package/dist/v3/dialog.d.ts.map +1 -0
  170. package/dist/v3/dialog.js +241 -0
  171. package/dist/v3/dialog.js.map +1 -0
  172. package/dist/v3/message.d.ts +85 -0
  173. package/dist/v3/message.d.ts.map +1 -0
  174. package/dist/v3/message.js +303 -0
  175. package/dist/v3/message.js.map +1 -0
  176. package/dist/v3/parameter-store.d.ts +75 -0
  177. package/dist/v3/parameter-store.d.ts.map +1 -0
  178. package/dist/v3/parameter-store.js +143 -0
  179. package/dist/v3/parameter-store.js.map +1 -0
  180. package/dist/v3/parameters.d.ts +105 -0
  181. package/dist/v3/parameters.d.ts.map +1 -0
  182. package/dist/v3/parameters.js +450 -0
  183. package/dist/v3/parameters.js.map +1 -0
  184. package/dist/v3/protocol.d.ts +61 -0
  185. package/dist/v3/protocol.d.ts.map +1 -0
  186. package/dist/v3/protocol.js +113 -0
  187. package/dist/v3/protocol.js.map +1 -0
  188. package/dist/v3/queries.d.ts +51 -0
  189. package/dist/v3/queries.d.ts.map +1 -0
  190. package/dist/v3/queries.js +120 -0
  191. package/dist/v3/queries.js.map +1 -0
  192. package/dist/v3/segments.d.ts +106 -0
  193. package/dist/v3/segments.d.ts.map +1 -0
  194. package/dist/v3/segments.js +325 -0
  195. package/dist/v3/segments.js.map +1 -0
  196. package/dist/v3/syntax.d.ts +127 -0
  197. package/dist/v3/syntax.d.ts.map +1 -0
  198. package/dist/v3/syntax.js +367 -0
  199. package/dist/v3/syntax.js.map +1 -0
  200. package/package.json +67 -4
  201. package/src/client.ts +252 -0
  202. package/src/core/account.ts +323 -0
  203. package/src/core/amount.ts +211 -0
  204. package/src/core/balance.ts +85 -0
  205. package/src/core/bank-info.ts +66 -0
  206. package/src/core/client.ts +186 -0
  207. package/src/core/config.ts +182 -0
  208. package/src/core/errors.ts +277 -0
  209. package/src/core/feedback.ts +140 -0
  210. package/src/core/transaction.ts +161 -0
  211. package/src/core/transport.ts +189 -0
  212. package/src/formats/camt.ts +266 -0
  213. package/src/formats/mt940.ts +419 -0
  214. package/src/formats/readers.ts +49 -0
  215. package/src/formats/record-budget.ts +18 -0
  216. package/src/formats/sepa-purpose.ts +94 -0
  217. package/src/index.ts +70 -0
  218. package/src/session/app-approval.ts +278 -0
  219. package/src/session/dialog-runner.ts +122 -0
  220. package/src/session/login-guard.ts +221 -0
  221. package/src/session/sepa-accounts.ts +115 -0
  222. package/src/session/session-setup.ts +184 -0
  223. package/src/session/session-state.ts +204 -0
  224. package/src/session/signed-dialog.ts +192 -0
  225. package/src/session/tan-selection.ts +203 -0
  226. package/src/session/user-prompts.ts +193 -0
  227. package/src/tan/challenge.ts +261 -0
  228. package/src/tan/flicker.ts +308 -0
  229. package/src/tan/tan-image.ts +38 -0
  230. package/src/tan/tan-models.ts +252 -0
  231. package/src/util/async.ts +48 -0
  232. package/src/util/bytes.ts +129 -0
  233. package/src/util/call-context.ts +41 -0
  234. package/src/util/dates.ts +210 -0
  235. package/src/util/errors.ts +27 -0
  236. package/src/util/immutable-map.ts +47 -0
  237. package/src/util/json.ts +67 -0
  238. package/src/util/text.ts +112 -0
  239. package/src/util/xml.ts +183 -0
  240. package/src/v3/account-extension.ts +185 -0
  241. package/src/v3/bank-connection.ts +67 -0
  242. package/src/v3/dialog.ts +283 -0
  243. package/src/v3/message.ts +373 -0
  244. package/src/v3/parameter-store.ts +160 -0
  245. package/src/v3/parameters.ts +524 -0
  246. package/src/v3/protocol.ts +148 -0
  247. package/src/v3/queries.ts +190 -0
  248. package/src/v3/segments.ts +374 -0
  249. package/src/v3/syntax.ts +369 -0
@@ -0,0 +1,252 @@
1
+ import { copyDate } from '../util/dates.js';
2
+ import { maskedId } from '../util/text.js';
3
+
4
+ /** Whether a TAN method needs the name of a TAN medium. */
5
+ export type TanMediumRequirement =
6
+ /** The method takes no medium name. */
7
+ | 'notAllowed'
8
+ /** The method takes a medium name but works without one. */
9
+ | 'optional'
10
+ /** The method needs the name of the medium. */
11
+ | 'required';
12
+
13
+ /** Reads the FinTS code: `0` not allowed, `1` optional, `2` required. */
14
+ export function tanMediumRequirementOf(code: string | undefined): TanMediumRequirement {
15
+ return code === '1' ? 'optional' : code === '2' ? 'required' : 'notAllowed';
16
+ }
17
+
18
+ /** What `TanMethod` takes; the bank states the details it knows. */
19
+ export interface TanMethodInit {
20
+ securityFunction: string;
21
+ name: string;
22
+ technicalId?: string | undefined;
23
+ dkMethod?: string | undefined;
24
+ dkVersion?: string | undefined;
25
+ maxTanLength?: number | undefined;
26
+ numericTan?: boolean;
27
+ returnValueText?: string | undefined;
28
+ maxChallengeLength?: number | undefined;
29
+ mediumRequirement?: TanMediumRequirement;
30
+ activeMediaCount?: number | undefined;
31
+ structuredChallenge?: boolean;
32
+ decoupled?: boolean;
33
+ decoupledMaxPolls?: number | undefined;
34
+ decoupledFirstPollDelaySeconds?: number | undefined;
35
+ decoupledPollIntervalSeconds?: number | undefined;
36
+ decoupledManualConfirmation?: boolean;
37
+ decoupledAutomaticPolling?: boolean;
38
+ }
39
+
40
+ /** A TAN procedure offered by the bank (FinTS "Sicherheitsfunktion"). */
41
+ export class TanMethod {
42
+ /** Bank-specific code of the procedure, for example `942`; `999` is the one-step procedure. */
43
+ readonly securityFunction: string;
44
+
45
+ /** The name the bank gives the procedure, for example `pushTAN`. */
46
+ readonly name: string;
47
+
48
+ /** The bank's technical identification of the procedure. */
49
+ readonly technicalId: string | undefined;
50
+
51
+ /** DK procedure family such as `HHD1.4`, `mobileTAN` or `Decoupled`. */
52
+ readonly dkMethod: string | undefined;
53
+
54
+ /** Version of the `dkMethod`. */
55
+ readonly dkVersion: string | undefined;
56
+
57
+ /** Longest TAN the user may enter. */
58
+ readonly maxTanLength: number | undefined;
59
+
60
+ /** Whether the TAN consists of digits only. */
61
+ readonly numericTan: boolean;
62
+
63
+ /** The bank's label for the value the user sends back, for example `TAN`. */
64
+ readonly returnValueText: string | undefined;
65
+
66
+ /** Longest challenge text the bank sends for this procedure. */
67
+ readonly maxChallengeLength: number | undefined;
68
+
69
+ /** Whether a TAN medium name goes with this procedure. */
70
+ readonly mediumRequirement: TanMediumRequirement;
71
+
72
+ /**
73
+ * Most TAN media a user can have active at once for this procedure, as the bank's parameters state it ("Anzahl
74
+ * unterstützter aktiver TAN-Medien"); not how many the user has.
75
+ *
76
+ * Where it is above 1 and `needsMediumName`, each message names the medium, since the user may have several.
77
+ */
78
+ readonly activeMediaCount: number | undefined;
79
+
80
+ /** Whether the challenge text is structured (marked up) rather than plain. */
81
+ readonly structuredChallenge: boolean;
82
+
83
+ /** Whether approval happens in a separate app instead of entering a TAN. */
84
+ readonly decoupled: boolean;
85
+
86
+ /**
87
+ * Most status requests the bank allows for a decoupled approval, 0 for any number. The client sends 60 where the bank
88
+ * states none, and polls for 10 minutes at most.
89
+ */
90
+ readonly decoupledMaxPolls: number | undefined;
91
+
92
+ /** How long to wait before the first status request of a decoupled approval, in seconds. */
93
+ readonly decoupledFirstPollDelaySeconds: number | undefined;
94
+
95
+ /** How long to wait between two status requests of a decoupled approval, in seconds. */
96
+ readonly decoupledPollIntervalSeconds: number | undefined;
97
+
98
+ /** Whether the user may report the approval, rather than the client asking the bank by itself. */
99
+ readonly decoupledManualConfirmation: boolean;
100
+
101
+ /** Whether the bank allows the client to ask for the approval by itself. */
102
+ readonly decoupledAutomaticPolling: boolean;
103
+
104
+ constructor(init: TanMethodInit) {
105
+ this.securityFunction = init.securityFunction;
106
+ this.name = init.name;
107
+ this.technicalId = init.technicalId;
108
+ this.dkMethod = init.dkMethod;
109
+ this.dkVersion = init.dkVersion;
110
+ this.maxTanLength = init.maxTanLength;
111
+ this.numericTan = init.numericTan ?? true;
112
+ this.returnValueText = init.returnValueText;
113
+ this.maxChallengeLength = init.maxChallengeLength;
114
+ this.mediumRequirement = init.mediumRequirement ?? 'notAllowed';
115
+ this.activeMediaCount = init.activeMediaCount;
116
+ this.structuredChallenge = init.structuredChallenge ?? false;
117
+ this.decoupled = init.decoupled ?? false;
118
+ this.decoupledMaxPolls = init.decoupledMaxPolls;
119
+ this.decoupledFirstPollDelaySeconds = init.decoupledFirstPollDelaySeconds;
120
+ this.decoupledPollIntervalSeconds = init.decoupledPollIntervalSeconds;
121
+ this.decoupledManualConfirmation = init.decoupledManualConfirmation ?? true;
122
+ this.decoupledAutomaticPolling = init.decoupledAutomaticPolling ?? true;
123
+ }
124
+
125
+ /** The one-step procedure (PIN only, no TAN). */
126
+ static readonly oneStep = new TanMethod({ securityFunction: '999', name: 'One-step (PIN only)' });
127
+
128
+ /** Whether this is the one-step procedure. */
129
+ get isOneStep(): boolean {
130
+ return this.securityFunction === TanMethod.oneStep.securityFunction;
131
+ }
132
+
133
+ /** Whether the bank needs the name of a TAN medium for this procedure. */
134
+ get needsMediumName(): boolean {
135
+ return this.mediumRequirement === 'required';
136
+ }
137
+
138
+ toString(): string {
139
+ return `TanMethod(${this.securityFunction}, ${this.name})`;
140
+ }
141
+ }
142
+
143
+ /** State of a TAN medium as reported by the bank. */
144
+ export type TanMediumStatus =
145
+ /** The medium is in use. */
146
+ | 'active'
147
+ /** The medium can be used but is not the active one. */
148
+ | 'available'
149
+ /** The medium is the active follow-up card. */
150
+ | 'activeFollowUpCard'
151
+ /** The medium is an available follow-up card. */
152
+ | 'availableFollowUpCard'
153
+ /** The bank states no status, or one that is not known. */
154
+ | 'unknown';
155
+
156
+ /** Reads the FinTS code `1` to `4`. */
157
+ export function tanMediumStatusOf(code: string | undefined): TanMediumStatus {
158
+ switch (code) {
159
+ case '1':
160
+ return 'active';
161
+ case '2':
162
+ return 'available';
163
+ case '3':
164
+ return 'activeFollowUpCard';
165
+ case '4':
166
+ return 'availableFollowUpCard';
167
+ default:
168
+ return 'unknown';
169
+ }
170
+ }
171
+
172
+ /** What `TanMedium` takes; the bank states the details it knows. */
173
+ export interface TanMediumInit {
174
+ mediumClass: string;
175
+ status: TanMediumStatus;
176
+ name?: string | undefined;
177
+ securityFunction?: string | undefined;
178
+ cardNumber?: string | undefined;
179
+ cardSequenceNumber?: string | undefined;
180
+ mobileNumber?: string | undefined;
181
+ mobileNumberMasked?: string | undefined;
182
+ validFrom?: Date | undefined;
183
+ validUntil?: Date | undefined;
184
+ lastUsed?: Date | undefined;
185
+ activatedOn?: Date | undefined;
186
+ }
187
+
188
+ /** A TAN device registered for the user, such as a phone or TAN generator. */
189
+ export class TanMedium {
190
+ /** `G` TAN generator, `M` mobile phone, `L` TAN list, `S` secoder, `B` bilateral. */
191
+ readonly mediumClass: string;
192
+
193
+ /** Whether the medium is in use. */
194
+ readonly status: TanMediumStatus;
195
+
196
+ /** The name the medium is registered under; this is what the bank wants back to pick the medium. */
197
+ readonly name: string | undefined;
198
+
199
+ /** The TAN procedure the medium belongs to (HITAB version 5). */
200
+ readonly securityFunction: string | undefined;
201
+
202
+ /** Card number of a TAN generator. */
203
+ readonly cardNumber: string | undefined;
204
+
205
+ /** Sequence number of the card of a TAN generator. */
206
+ readonly cardSequenceNumber: string | undefined;
207
+
208
+ /** Phone number of a mobile phone medium, if the bank states it in full. */
209
+ readonly mobileNumber: string | undefined;
210
+
211
+ /** Phone number with the middle digits hidden. */
212
+ readonly mobileNumberMasked: string | undefined;
213
+
214
+ /** The medium is valid from this date. */
215
+ readonly validFrom: Date | undefined;
216
+
217
+ /** The medium is valid until this date. */
218
+ readonly validUntil: Date | undefined;
219
+
220
+ /** When the medium was used last. */
221
+ readonly lastUsed: Date | undefined;
222
+
223
+ /** When the medium was activated. */
224
+ readonly activatedOn: Date | undefined;
225
+
226
+ /** Takes copies of the dates of `init`: each medium has its own, which no other one shares. */
227
+ constructor(init: TanMediumInit) {
228
+ this.mediumClass = init.mediumClass;
229
+ this.status = init.status;
230
+ this.name = init.name;
231
+ this.securityFunction = init.securityFunction;
232
+ this.cardNumber = init.cardNumber;
233
+ this.cardSequenceNumber = init.cardSequenceNumber;
234
+ this.mobileNumber = init.mobileNumber;
235
+ this.mobileNumberMasked = init.mobileNumberMasked;
236
+ this.validFrom = copyDate(init.validFrom);
237
+ this.validUntil = copyDate(init.validUntil);
238
+ this.lastUsed = copyDate(init.lastUsed);
239
+ this.activatedOn = copyDate(init.activatedOn);
240
+ }
241
+
242
+ /** Whether the medium is the one in use. */
243
+ get isActive(): boolean {
244
+ return this.status === 'active' || this.status === 'activeFollowUpCard';
245
+ }
246
+
247
+ /** The name, or the last digits of the card number, so that logs and crash reports do not collect the number. */
248
+ toString(): string {
249
+ const label = this.name ?? (this.cardNumber === undefined ? this.mediumClass : maskedId(this.cardNumber));
250
+ return `TanMedium(${label}, ${this.status})`;
251
+ }
252
+ }
@@ -0,0 +1,48 @@
1
+ /** A promise with its resolve and reject functions, which tells whether it has settled. */
2
+ export class Deferred<T> {
3
+ readonly promise: Promise<T>;
4
+ private resolveFn!: (value: T) => void;
5
+ private rejectFn!: (error: unknown) => void;
6
+ private settled = false;
7
+
8
+ constructor() {
9
+ this.promise = new Promise<T>((resolve, reject) => {
10
+ this.resolveFn = resolve;
11
+ this.rejectFn = reject;
12
+ });
13
+ // A rejection nobody waits for is not an unhandled one: whoever awaits the promise still gets it.
14
+ this.promise.catch(() => undefined);
15
+ }
16
+
17
+ get isCompleted(): boolean {
18
+ return this.settled;
19
+ }
20
+
21
+ resolve(value: T): void {
22
+ if (this.settled) return;
23
+ this.settled = true;
24
+ this.resolveFn(value);
25
+ }
26
+
27
+ reject(error: unknown): void {
28
+ if (this.settled) return;
29
+ this.settled = true;
30
+ this.rejectFn(error);
31
+ }
32
+ }
33
+
34
+ /**
35
+ * Runs asynchronous actions one after another, in the order they were submitted.
36
+ *
37
+ * A failing action does not stop the ones queued after it.
38
+ */
39
+ export class SerialExecutor {
40
+ private last: Promise<unknown> = Promise.resolve();
41
+
42
+ /** Runs `action` once the actions submitted before have finished; the result is that of `action`. */
43
+ run<T>(action: () => Promise<T>): Promise<T> {
44
+ const result = this.last.then(action);
45
+ this.last = result.catch(() => undefined);
46
+ return result;
47
+ }
48
+ }
@@ -0,0 +1,129 @@
1
+ import { FormatError } from './errors.js';
2
+
3
+ /** The `parts` joined into one array. */
4
+ export function concatBytes(parts: readonly Uint8Array[]): Uint8Array {
5
+ let length = 0;
6
+ for (const part of parts) length += part.length;
7
+ const result = new Uint8Array(length);
8
+ let offset = 0;
9
+ for (const part of parts) {
10
+ result.set(part, offset);
11
+ offset += part.length;
12
+ }
13
+ return result;
14
+ }
15
+
16
+ /** Collects bytes, like a growing buffer. */
17
+ export class BytesBuilder {
18
+ private readonly parts: Uint8Array[] = [];
19
+
20
+ add(bytes: Uint8Array | readonly number[]): void {
21
+ this.parts.push(bytes instanceof Uint8Array ? bytes : Uint8Array.from(bytes));
22
+ }
23
+
24
+ addByte(byte: number): void {
25
+ this.add(Uint8Array.of(byte));
26
+ }
27
+
28
+ takeBytes(): Uint8Array {
29
+ const result = concatBytes(this.parts);
30
+ this.parts.length = 0;
31
+ return result;
32
+ }
33
+ }
34
+
35
+ /** ISO 8859-1 bytes of `text`, which must contain no character beyond U+00FF. */
36
+ export function latin1Encode(text: string): Uint8Array {
37
+ const bytes = new Uint8Array(text.length);
38
+ for (let i = 0; i < text.length; i++) bytes[i] = text.charCodeAt(i) & 0xff;
39
+ return bytes;
40
+ }
41
+
42
+ /** The text of ISO 8859-1 `bytes`. */
43
+ export function latin1Decode(bytes: Uint8Array): string {
44
+ return fromCharCodes(bytes);
45
+ }
46
+
47
+ /** The text of the UTF-16 code units `codes`. */
48
+ export function fromCharCodes(codes: Uint8Array | Uint16Array): string {
49
+ let text = '';
50
+ // In chunks, since a large array exceeds the argument limit of some engines. `apply` takes the array as it is, where a
51
+ // spread would iterate over it.
52
+ for (let i = 0; i < codes.length; i += 0x8000) {
53
+ text += Reflect.apply(String.fromCharCode, undefined, codes.subarray(i, i + 0x8000)) as string;
54
+ }
55
+ return text;
56
+ }
57
+
58
+ const utf8Encoder = new TextEncoder();
59
+ const utf8Decoder = new TextDecoder('utf-8', { fatal: true, ignoreBOM: true });
60
+ const lenientUtf8Decoder = new TextDecoder('utf-8', { ignoreBOM: true });
61
+
62
+ export function utf8Encode(text: string): Uint8Array {
63
+ return utf8Encoder.encode(text);
64
+ }
65
+
66
+ /** Decodes UTF-8; throws `FormatError` for malformed data unless `allowMalformed`. */
67
+ export function utf8Decode(bytes: Uint8Array, allowMalformed = false): string {
68
+ try {
69
+ return (allowMalformed ? lenientUtf8Decoder : utf8Decoder).decode(bytes);
70
+ } catch {
71
+ throw new FormatError('Invalid UTF-8');
72
+ }
73
+ }
74
+
75
+ const base64Alphabet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';
76
+ const base64Values = (() => {
77
+ const values = new Int16Array(128).fill(-1);
78
+ for (let i = 0; i < base64Alphabet.length; i++) values[base64Alphabet.charCodeAt(i)] = i;
79
+ // The URL-safe alphabet as well.
80
+ values['-'.charCodeAt(0)] = 62;
81
+ values['_'.charCodeAt(0)] = 63;
82
+ return values;
83
+ })();
84
+
85
+ export function base64Encode(bytes: Uint8Array): string {
86
+ let out = '';
87
+ let i = 0;
88
+ for (; i + 2 < bytes.length; i += 3) {
89
+ const n = (bytes[i]! << 16) | (bytes[i + 1]! << 8) | bytes[i + 2]!;
90
+ out +=
91
+ base64Alphabet[n >> 18]! +
92
+ base64Alphabet[(n >> 12) & 63]! +
93
+ base64Alphabet[(n >> 6) & 63]! +
94
+ base64Alphabet[n & 63]!;
95
+ }
96
+ const rest = bytes.length - i;
97
+ if (rest === 1) {
98
+ const n = bytes[i]! << 16;
99
+ out += base64Alphabet[n >> 18]! + base64Alphabet[(n >> 12) & 63]! + '==';
100
+ } else if (rest === 2) {
101
+ const n = (bytes[i]! << 16) | (bytes[i + 1]! << 8);
102
+ out += base64Alphabet[n >> 18]! + base64Alphabet[(n >> 12) & 63]! + base64Alphabet[(n >> 6) & 63]! + '=';
103
+ }
104
+ return out;
105
+ }
106
+
107
+ /** Decodes base64, in the standard or the URL-safe alphabet; throws `FormatError` if `text` is not base64. */
108
+ export function base64Decode(text: string): Uint8Array {
109
+ if (text.length % 4 !== 0) throw new FormatError('Invalid base64 length, must be a multiple of four');
110
+ let padding = 0;
111
+ if (text.endsWith('==')) padding = 2;
112
+ else if (text.endsWith('=')) padding = 1;
113
+ const out = new Uint8Array((text.length / 4) * 3 - padding);
114
+ let o = 0;
115
+ for (let i = 0; i < text.length; i += 4) {
116
+ let n = 0;
117
+ for (let j = 0; j < 4; j++) {
118
+ const c = text.charCodeAt(i + j);
119
+ const isPadding = c === 0x3d && i + 4 === text.length && j >= 4 - padding;
120
+ const value = isPadding ? 0 : c < 128 ? base64Values[c]! : -1;
121
+ if (value < 0) throw new FormatError('Invalid base64 character', text.charAt(i + j));
122
+ n = (n << 6) | value;
123
+ }
124
+ if (o < out.length) out[o++] = n >> 16;
125
+ if (o < out.length) out[o++] = (n >> 8) & 0xff;
126
+ if (o < out.length) out[o++] = n & 0xff;
127
+ }
128
+ return out;
129
+ }
@@ -0,0 +1,41 @@
1
+ /** The part of Node.js's `AsyncLocalStorage` this package uses. */
2
+ interface AsyncStorage<T> {
3
+ getStore(): T | undefined;
4
+ run<R>(store: T, callback: () => R): R;
5
+ }
6
+
7
+ type AsyncStorageConstructor = new <T>() => AsyncStorage<T>;
8
+
9
+ /**
10
+ * `AsyncLocalStorage` where the runtime has it, found through `process.getBuiltinModule` (Node.js 22.3, Deno 2.1 and
11
+ * Bun 1.2.6 or later) rather than by importing `node:async_hooks`, so that bundlers for the browser have nothing to
12
+ * resolve.
13
+ */
14
+ const AsyncStorageClass: AsyncStorageConstructor | undefined = (() => {
15
+ const runtime = globalThis as {
16
+ process?: { getBuiltinModule?: (id: string) => { AsyncLocalStorage?: AsyncStorageConstructor } | undefined };
17
+ };
18
+ try {
19
+ return runtime.process?.getBuiltinModule?.('node:async_hooks')?.AsyncLocalStorage;
20
+ } catch {
21
+ return undefined;
22
+ }
23
+ })();
24
+
25
+ /**
26
+ * Tells which asynchronous call an operation runs in, through every `await` and callback it starts. Where the runtime
27
+ * cannot track that, as in browsers, `current` is always `undefined`.
28
+ */
29
+ export class CallContext<T extends object> {
30
+ private readonly storage: AsyncStorage<T> | undefined = AsyncStorageClass ? new AsyncStorageClass<T>() : undefined;
31
+
32
+ /** The value of the call that runs now, if the runtime can tell. */
33
+ get current(): T | undefined {
34
+ return this.storage?.getStore();
35
+ }
36
+
37
+ /** Runs `action` as the call `value`. */
38
+ run<R>(value: T, action: () => R): R {
39
+ return this.storage ? this.storage.run(value, action) : action();
40
+ }
41
+ }
@@ -0,0 +1,210 @@
1
+ // A value is either a calendar date, a local `Date` at midnight of which only the day counts, or the exact moment of a
2
+ // date with a time of day, which the bank states in German time (see `germanTime`). The parsers keep the two apart: a
3
+ // date without a valid time of day is not a moment.
4
+
5
+ const hourMs = 3_600_000;
6
+
7
+ function twoDigits(n: number): string {
8
+ return n.toString().padStart(2, '0');
9
+ }
10
+
11
+ function year(d: Date): string {
12
+ return d.getFullYear().toString().padStart(4, '0');
13
+ }
14
+
15
+ /** `YYYYMMDD`, the FinTS 3.0 date format. */
16
+ export function formatFintsDate(d: Date): string {
17
+ return `${year(d)}${twoDigits(d.getMonth() + 1)}${twoDigits(d.getDate())}`;
18
+ }
19
+
20
+ /** `HHMMSS`, the FinTS 3.0 time format. */
21
+ export function formatFintsTime(d: Date): string {
22
+ return `${twoDigits(d.getHours())}${twoDigits(d.getMinutes())}${twoDigits(d.getSeconds())}`;
23
+ }
24
+
25
+ /** `YYYY-MM-DD`, the ISO 8601 date format of camt. */
26
+ export function formatIsoDate(d: Date): string {
27
+ return `${year(d)}-${twoDigits(d.getMonth() + 1)}-${twoDigits(d.getDate())}`;
28
+ }
29
+
30
+ const fintsDate = /^(\d{4})(\d{2})(\d{2})$/;
31
+ const fintsTime = /^(\d{2})(\d{2})(\d{2})/;
32
+ const isoDate = /^(\d{4})-(\d{2})-(\d{2})/;
33
+ const isoTime = /^(\d{2}):(\d{2}):(\d{2})/;
34
+ const isoDateTime = /^(\d{4}-\d{2}-\d{2})T(\d{2}:\d{2}:\d{2})(?:\.\d+)?(Z|[+-]\d{2}:\d{2})?$/;
35
+
36
+ /** Parses a FinTS 3.0 date `YYYYMMDD` as a calendar date; `undefined` if it is missing or invalid. */
37
+ export function parseFintsDate(date: string | undefined): Date | undefined {
38
+ return parseDate(fintsDate, date);
39
+ }
40
+
41
+ /**
42
+ * Parses a FinTS 3.0 date `YYYYMMDD` and time `HHMMSS` as the moment they name; `undefined` if either is missing or
43
+ * invalid.
44
+ */
45
+ export function parseFintsDateTime(date: string | undefined, time: string | undefined): Date | undefined {
46
+ return moment(parseDate(fintsDate, date), fintsTime, time);
47
+ }
48
+
49
+ /**
50
+ * Parses an ISO 8601 date `YYYY-MM-DD` as a calendar date, ignoring extra characters such as a time part; `undefined`
51
+ * if it is missing or invalid.
52
+ */
53
+ export function parseIsoDate(date: string | undefined): Date | undefined {
54
+ return parseDate(isoDate, date);
55
+ }
56
+
57
+ /**
58
+ * Parses an ISO 8601 date and time as XML Schema writes it, `YYYY-MM-DDThh:mm:ss` with optional fractional seconds and
59
+ * UTC offset (`Z` or `±hh:mm`), as the moment it names, to the second: in German time where it states no offset.
60
+ * `undefined` if it is missing or invalid.
61
+ */
62
+ export function parseIsoMoment(dateTime: string | undefined): Date | undefined {
63
+ const m = dateTime === undefined ? null : isoDateTime.exec(dateTime);
64
+ if (!m) return undefined;
65
+ const [, date, time, offset] = m;
66
+ const german = moment(parseIsoDate(date), isoTime, time);
67
+ if (german === undefined || offset === undefined) return german;
68
+ // Every engine reads a valid date and time with an offset alike (ECMAScript Date Time String Format), but some roll
69
+ // an invalid date, such as 30 February, over into the next month: `german` has checked them.
70
+ const exact = new Date(`${date}T${time}${offset}`);
71
+ return Number.isNaN(exact.getTime()) ? undefined : exact;
72
+ }
73
+
74
+ function parseDate(pattern: RegExp, date: string | undefined): Date | undefined {
75
+ const d = date === undefined ? null : pattern.exec(date);
76
+ return d ? validDate(Number(d[1]), Number(d[2]), Number(d[3])) : undefined;
77
+ }
78
+
79
+ /** Hour, minute and second of the time matched by `pattern` in `time`; `undefined` if it is missing or invalid. */
80
+ function parseTime(pattern: RegExp, time: string | undefined): [number, number, number] | undefined {
81
+ const t = time === undefined ? null : pattern.exec(time);
82
+ if (!t) return undefined;
83
+ const [hour, minute, second] = [Number(t[1]), Number(t[2]), Number(t[3])];
84
+ return hour > 23 || minute > 59 || second > 59 ? undefined : [hour, minute, second];
85
+ }
86
+
87
+ /** The moment of the German time matched by `timePattern` in `time` on `day`. */
88
+ function moment(day: Date | undefined, timePattern: RegExp, time: string | undefined): Date | undefined {
89
+ const t = parseTime(timePattern, time);
90
+ if (day === undefined || t === undefined) return undefined;
91
+ return germanTime(day.getFullYear(), day.getMonth() + 1, day.getDate(), ...t);
92
+ }
93
+
94
+ /**
95
+ * Parses a FinTS 3.0 date `YYYYMMDD` and time `HHMMSS` given in UTC+01:00 all year round, as the account extension of
96
+ * the user parameter data states its times (Formals E.3.1); `undefined` if either is missing or invalid. A time with
97
+ * fewer than six digits lost its leading zeros as a JSON number.
98
+ */
99
+ export function parseUtcPlusOneDateTime(date: string | undefined, time: string | undefined): Date | undefined {
100
+ const day = parseDate(fintsDate, date);
101
+ const t = parseTime(fintsTime, time?.padStart(6, '0'));
102
+ if (day === undefined || t === undefined) return undefined;
103
+ return new Date(utc(day.getFullYear(), day.getMonth() + 1, day.getDate(), ...t) - hourMs);
104
+ }
105
+
106
+ /**
107
+ * The moment a German clock shows the given time.
108
+ *
109
+ * German time is CET (UTC+1), and CEST (UTC+2) from 01:00 UTC on the last Sunday of March to 01:00 UTC on the last
110
+ * Sunday of October (the EU rule). The hour the clocks show twice in October counts as CEST; a time in the hour that is
111
+ * skipped in March counts as CET.
112
+ */
113
+ function germanTime(year: number, month: number, day: number, hour: number, minute: number, second: number): Date {
114
+ const clock = utc(year, month, day, hour, minute, second);
115
+ const summer = clock - 2 * hourMs;
116
+ return new Date(isSummerTime(summer) ? summer : clock - hourMs);
117
+ }
118
+
119
+ /** The calendar day a German clock shows at `moment`, as a local `Date` at midnight like every calendar date. */
120
+ export function germanDay(moment: Date): Date {
121
+ const utcMs = moment.getTime();
122
+ const clock = new Date(utcMs + (isSummerTime(utcMs) ? 2 : 1) * hourMs);
123
+ return localDate(clock.getUTCFullYear(), clock.getUTCMonth() + 1, clock.getUTCDate());
124
+ }
125
+
126
+ function isSummerTime(utcMs: number): boolean {
127
+ const y = new Date(utcMs).getUTCFullYear();
128
+ return utcMs >= clockChange(y, 3) && utcMs < clockChange(y, 10);
129
+ }
130
+
131
+ /** 01:00 UTC on the last Sunday of `month`, when the EU changes the clocks. */
132
+ function clockChange(year: number, month: number): number {
133
+ const lastDay = daysInMonth(year, month);
134
+ const weekday = new Date(utc(year, month, lastDay, 0, 0, 0)).getUTCDay();
135
+ return utc(year, month, lastDay - weekday, 1, 0, 0);
136
+ }
137
+
138
+ /** Milliseconds since the epoch of the UTC time; `month` counts from 1, and years below 100 are not moved to 19xx. */
139
+ function utc(year: number, month: number, day: number, hour: number, minute: number, second: number): number {
140
+ const d = new Date(0);
141
+ d.setUTCFullYear(year, month - 1, day);
142
+ d.setUTCHours(hour, minute, second, 0);
143
+ return d.getTime();
144
+ }
145
+
146
+ /** Local midnight of the day; `month` counts from 1, and years below 100 are not moved to 19xx. */
147
+ function localDate(year: number, month: number, day: number): Date {
148
+ const d = new Date(0);
149
+ d.setFullYear(year, month - 1, day);
150
+ d.setHours(0, 0, 0, 0);
151
+ return d;
152
+ }
153
+
154
+ /** The date, or `undefined` if `month` or `day` is out of range; `Date` would roll it over into another date. */
155
+ function validDate(year: number, month: number, day: number): Date | undefined {
156
+ if (month < 1 || month > 12 || day < 1 || day > daysInMonth(year, month)) return undefined;
157
+ return localDate(year, month, day);
158
+ }
159
+
160
+ /**
161
+ * The date, with a day beyond the end of the month moved to its last day: banks write 30 February or 31 April for
162
+ * month-end bookings. `undefined` for a month or day that no month has.
163
+ */
164
+ export function clampedDate(year: number, month: number, day: number): Date | undefined {
165
+ if (month < 1 || month > 12 || day < 1 || day > 31) return undefined;
166
+ return validDate(year, month, day) ?? localDate(year, month, daysInMonth(year, month));
167
+ }
168
+
169
+ const swiftDate = /^(\d{2})(\d{2})(\d{2})$/;
170
+
171
+ /**
172
+ * Parses a SWIFT date `YYMMDD`, as MT940 writes it, with month ends as `clampedDate` reads them; years from 80 on count
173
+ * as 19xx. `undefined` if the date is invalid.
174
+ */
175
+ export function parseSwiftDate(yymmdd: string): Date | undefined {
176
+ const m = swiftDate.exec(yymmdd);
177
+ if (!m) return undefined;
178
+ const yy = Number(m[1]);
179
+ return clampedDate(yy >= 80 ? 1900 + yy : 2000 + yy, Number(m[2]), Number(m[3]));
180
+ }
181
+
182
+ /** Number of days the `month` (1 to 12) of `year` has. */
183
+ function daysInMonth(year: number, month: number): number {
184
+ if (month === 2) return (year % 4 === 0 && year % 100 !== 0) || year % 400 === 0 ? 29 : 28;
185
+ return [4, 6, 9, 11].includes(month) ? 30 : 31;
186
+ }
187
+
188
+ /** A copy of `d`, so that a model keeps dates of its own: a `Date` can be changed in place. */
189
+ export function copyDate(d: Date): Date;
190
+ export function copyDate(d: Date | undefined): Date | undefined;
191
+ export function copyDate(d: Date | undefined): Date | undefined {
192
+ return d && new Date(d.getTime());
193
+ }
194
+
195
+ /** The calendar day `moment` falls on in local time, as a local `Date` at midnight. */
196
+ export function calendarDay(moment: Date): Date {
197
+ return localDate(moment.getFullYear(), moment.getMonth() + 1, moment.getDate());
198
+ }
199
+
200
+ /**
201
+ * The calendar `day`, moved back to the bank's today if it lies after it: banks reject future dates in queries.
202
+ *
203
+ * The bank's today is the German day at `now`, the current time by default; east of Germany the local day starts
204
+ * earlier.
205
+ */
206
+ export function notAfterToday(day: Date | undefined, now: Date = new Date()): Date | undefined {
207
+ if (day === undefined) return undefined;
208
+ const today = germanDay(now);
209
+ return day.getTime() > today.getTime() ? today : day;
210
+ }