@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
@@ -1,27 +1,28 @@
1
1
  import type { BankInfo } from '../core/bank-info.js';
2
- import { FinTsBankError, FinTsError, FinTsProtocolError, FinTsTransportError, parseBankData } from '../core/errors.js';
2
+ import { FinTsError, FinTsProtocolError, FinTsTransportError, parseBankData, tryLookUp } from '../core/errors.js';
3
+ import { hasErrors } from '../core/feedback.js';
3
4
  import type { TanMedium, TanMethod } from '../tan/tan-models.js';
4
- import type { LoginGuard, RefusalReason } from './login-guard.js';
5
- import { requireBankParameters } from './parameter-data.js';
6
- import type { BankResponse, PinTanProtocol, ProtocolDialog } from './protocol.js';
7
- import type { ResponseHandler } from './response-handler.js';
5
+ import { FinTs3Dialog } from '../v3/dialog.js';
6
+ import type { IncomingMessage3 } from '../v3/message.js';
7
+ import { requireBankParameters } from '../v3/parameters.js';
8
+ import type { FinTs3Protocol } from '../v3/protocol.js';
9
+ import { tanMediaQuery } from '../v3/queries.js';
10
+ import type { RefusalReason } from './login-guard.js';
11
+ import { SignedDialog, type SessionParts } from './signed-dialog.js';
8
12
  import type { TanSelection } from './tan-selection.js';
9
13
 
10
14
  /**
11
15
  * What has to be settled before a login: the bank parameter data, the synchronization, and through `TanSelection` the
12
16
  * TAN method and medium. Each of the dialogs this takes serves one purpose only.
13
17
  *
14
- * Where the bank refuses a login for a reason the session can take care of, `remedied` does so and logs in again.
18
+ * Where the bank refuses a login for a reason the session can take care of, `remedied` does so and logs in again; so it
19
+ * does where the limits of bank parameter data that may be outdated keep a login from going out.
15
20
  */
16
- export class SessionSetup<R extends BankResponse, O> {
17
- /** Whether a synchronization dialog ran, so that a helpful one runs once only. */
18
- private synchronizationTried = false;
19
-
21
+ export class SessionSetup {
20
22
  constructor(
21
- private readonly protocol: PinTanProtocol<R, O>,
22
- private readonly responses: ResponseHandler<R>,
23
- private readonly tans: TanSelection<R, O>,
24
- private readonly guard: LoginGuard,
23
+ private readonly session: SessionParts,
24
+ private readonly protocol: FinTs3Protocol,
25
+ private readonly tans: TanSelection,
25
26
  ) {}
26
27
 
27
28
  /**
@@ -29,9 +30,9 @@ export class SessionSetup<R extends BankResponse, O> {
29
30
  * No TAN method or medium is chosen for it.
30
31
  */
31
32
  async bankInfo(): Promise<BankInfo> {
32
- if (this.protocol.bankParameters === undefined) await this.fetchBankParametersAnonymously();
33
- if (this.protocol.bankParameters === undefined) await this.synchronizeIfDue();
34
- return requireBankParameters(this.protocol.bankParameters).bankInfo;
33
+ if (this.session.parameters.bankParameters === undefined) await this.fetchBankParametersAnonymously();
34
+ if (this.session.parameters.bankParameters === undefined) await this.synchronizeIfDue();
35
+ return requireBankParameters(this.session.parameters.bankParameters).bankInfo;
35
36
  }
36
37
 
37
38
  /** The TAN methods the bank allows for this user, which the synchronization reports. */
@@ -40,56 +41,58 @@ export class SessionSetup<R extends BankResponse, O> {
40
41
  return this.tans.available();
41
42
  }
42
43
 
43
- /** Settles what is still open: the bank parameter data, the synchronization, the TAN method and medium. */
44
- async prepare(): Promise<void> {
44
+ /**
45
+ * Settles what is still open: the bank parameter data, the synchronization, the TAN method and medium. Returns the TAN
46
+ * media it looked up to choose the medium; `undefined` where it looked up none, or the bank listed none.
47
+ */
48
+ async prepare(): Promise<TanMedium[] | undefined> {
45
49
  await this.loadParameters();
46
50
  await this.tans.ensureMethodChosen();
47
- if (this.tans.mediumToLookUp) await this.tans.chooseMedium(await this.lookUpTanMedia());
51
+ if (!this.tans.mediumToLookUp) return undefined;
52
+ const media = await this.lookUpTanMedia();
53
+ await this.tans.chooseMedium(media);
54
+ return media;
48
55
  }
49
56
 
50
57
  /** A new dialog signed with the TAN method and medium in use, once `prepare` has settled them. */
51
- async newDialog(): Promise<ProtocolDialog<R, O>> {
58
+ async newDialog(): Promise<SignedDialog> {
52
59
  await this.prepare();
53
- return this.tans.newDialog();
60
+ return new SignedDialog(this.tans.newDialog(), this.session);
54
61
  }
55
62
 
56
63
  /**
57
- * Runs `attempt`, which may log in, and runs it once more for each reason of a refused login the session can take
58
- * care of, so that a bank that keeps refusing fails with its own error:
64
+ * Runs `attempt`, which may log in, once more after each refused login `remedy` takes care of.
59
65
  *
60
- * | Refusal (`LoginGuard.remediableReasons`) | Remedy |
61
- * | --- | --- |
62
- * | 3920 without the method, or 9075 after the one-step method | another allowed method (`TanSelection.replace`) |
63
- * | 9391 | a new synchronization, which issues a customer system ID |
66
+ * Where the limits of the bank parameter data kept a login from going out (`FinTs3Protocol.stoppedLogin`), it fetches
67
+ * them anew in an anonymous dialog, once, and runs `attempt` once more: they may be outdated, as saved ones are, and
68
+ * new ones would come only in answer to the login they rule out. The new limits judge the login built anew, not the
69
+ * one stopped: it reports version 0 for the parameter data they stand in for, so it may be shorter.
64
70
  */
65
71
  async remedied<T>(attempt: () => Promise<T>): Promise<T> {
66
- // A refusal of an earlier call stops the attempt before it sends the PIN; there is nothing to remedy.
67
- if (this.guard.hasRefusal) return attempt();
68
- const remedied = new Set<RefusalReason>();
69
- for (;;) {
70
- try {
71
- return await attempt();
72
- } catch (error) {
73
- if (!(error instanceof FinTsBankError)) throw error;
74
- const reason = this.guard.remediableReasons(error).find((r) => !remedied.has(r));
75
- const rejected = this.guard.refusedMethod;
76
- if (reason === undefined || rejected === undefined || !(await this.remedy(reason, rejected))) throw error;
77
- remedied.add(reason);
78
- this.guard.pardon(error);
79
- }
72
+ const run = (): Promise<T> =>
73
+ this.session.guard.remedied(attempt, (reason, rejected) => this.remedy(reason, rejected));
74
+ try {
75
+ return await run();
76
+ } catch (error) {
77
+ if (!this.protocol.stoppedLogin(error)) throw error;
78
+ await this.fetchBankParametersAnonymously(true);
79
+ return run();
80
80
  }
81
81
  }
82
82
 
83
- /** Takes care of `reason`, given for refusing a login signed with `rejected`; returns whether it could. */
83
+ /**
84
+ * Takes care of `reason`, given for refusing a login signed with `rejected`, with another TAN method or a new
85
+ * synchronization; returns whether it could.
86
+ */
84
87
  private async remedy(reason: RefusalReason, rejected: TanMethod): Promise<boolean> {
85
88
  switch (reason) {
86
89
  case 'tanMethodNotAllowed':
87
90
  // The parameter data name the methods to choose from; the refusal may be the first to deliver them.
88
- if (this.protocol.bankParameters === undefined) await this.fetchBankParametersAnonymously();
91
+ if (this.session.parameters.bankParameters === undefined) await this.fetchBankParametersAnonymously();
89
92
  return this.tans.replace(rejected);
90
93
  case 'systemIdRejected':
91
94
  // Without a customer system ID there is none to lose, and logging in once more would only send the PIN again.
92
- return this.protocol.forgetSystemId();
95
+ return this.session.parameters.forgetSystemId();
93
96
  }
94
97
  }
95
98
 
@@ -103,35 +106,68 @@ export class SessionSetup<R extends BankResponse, O> {
103
106
  * After a refused login it throws the refusal right away, before the user is asked for a TAN method or medium.
104
107
  */
105
108
  private async loadParameters(): Promise<void> {
106
- this.guard.check();
107
- if (this.protocol.bankParameters === undefined) await this.fetchBankParametersAnonymously();
109
+ this.session.guard.check();
110
+ if (this.session.parameters.bankParameters === undefined) await this.fetchBankParametersAnonymously();
108
111
  await this.synchronizeIfDue();
109
112
  }
110
113
 
111
114
  /**
112
- * Asks for the bank parameter data without logging in. A bank that does not answer anonymously is no error, but a
113
- * failed connection is: the synchronization would only wait for the same server again.
115
+ * Asks for the bank parameter data without logging in, keeps them where there are none from a dialog signed with the
116
+ * PIN (`ParameterStore.keepAnonymous`), or with `refresh` their limits only (`ParameterStore.keepRefreshedLimits`),
117
+ * and passes the bank's messages on: all an anonymous dialog tells, since the TAN methods it lists are not those of a
118
+ * user. A bank that does not answer anonymously is no error, but a failed connection is: the synchronization would
119
+ * only wait for the same server again.
114
120
  */
115
- private async fetchBankParametersAnonymously(): Promise<void> {
121
+ private async fetchBankParametersAnonymously(refresh = false): Promise<void> {
116
122
  try {
117
- this.responses.showBankMessages(await this.protocol.fetchBankParametersAnonymously());
123
+ const response = await this.requestBankParametersAnonymously();
124
+ if (refresh) this.session.parameters.keepRefreshedLimits(response);
125
+ else this.session.parameters.keepAnonymous(response);
126
+ for (const message of response.bankMessages) this.session.prompts.showBankMessage(message);
118
127
  } catch (error) {
119
128
  // Optional: the login delivers the parameters as well.
120
129
  if (error instanceof FinTsTransportError || !(error instanceof FinTsError)) throw error;
121
130
  }
122
131
  }
123
132
 
124
- /** Runs the synchronization dialog where it is due (`TanSelection.synchronizationDialog`). */
133
+ /**
134
+ * Asks for the bank parameter data in an anonymous dialog that announces strong customer authentication; a bank that
135
+ * rejects the announcement, with a return code or by failing on it, is asked again without it.
136
+ *
137
+ * A timeout or a broken connection is not a reason to ask again: it would only double the wait.
138
+ */
139
+ private async requestBankParametersAnonymously(): Promise<IncomingMessage3> {
140
+ let announced: IncomingMessage3;
141
+ try {
142
+ announced = await this.anonymousDialog(true);
143
+ } catch (error) {
144
+ if (error instanceof FinTsTransportError && error.statusCode === undefined) throw error;
145
+ if (!(error instanceof FinTsError)) throw error;
146
+ return this.anonymousDialog(false);
147
+ }
148
+ if (!hasErrors(announced.feedback) || announced.find('HIBPA') !== undefined) return announced;
149
+ return this.anonymousDialog(false);
150
+ }
151
+
152
+ private async anonymousDialog(announceSca: boolean): Promise<IncomingMessage3> {
153
+ const dialog = FinTs3Dialog.anonymous(this.protocol);
154
+ try {
155
+ return await dialog.requestBankParameters(announceSca);
156
+ } finally {
157
+ await dialog.end();
158
+ }
159
+ }
160
+
161
+ /**
162
+ * Runs the synchronization, a login in a dialog of its own, while there is no customer system ID, without which no
163
+ * regular dialog starts. It runs before the TAN method and medium are settled (`TanSelection.synchronizationDialog`
164
+ * tells which method signs it).
165
+ */
125
166
  private async synchronizeIfDue(): Promise<void> {
126
- const due =
127
- this.protocol.synchronization === 'required' ||
128
- (this.protocol.synchronization === 'helpful' && !this.synchronizationTried);
129
- if (!due) return;
130
- this.synchronizationTried = true;
131
- await this.once(this.tans.synchronizationDialog(), (dialog) =>
132
- this.responses.startDialog(dialog, () => dialog.start(true)),
133
- );
134
- if (this.protocol.synchronization === 'required') {
167
+ if (this.session.parameters.systemId !== undefined) return;
168
+ await this.once(this.tans.synchronizationDialog(), (dialog) => dialog.login(true));
169
+ // Set by the synchronization, which the compiler cannot see.
170
+ if ((this.session.parameters.systemId as string | undefined) === undefined) {
135
171
  throw new FinTsProtocolError('The bank did not complete the synchronization');
136
172
  }
137
173
  }
@@ -144,22 +180,23 @@ export class SessionSetup<R extends BankResponse, O> {
144
180
  * medium the list tells (PIN/TAN B.4.3.1.3).
145
181
  */
146
182
  private async lookUpTanMedia(): Promise<TanMedium[] | undefined> {
147
- const query = this.protocol.tanMedia();
183
+ const query = tanMediaQuery(this.session.parameters);
148
184
  if (query === undefined) return undefined;
149
- return this.guard.tryLookUp(() =>
185
+ return tryLookUp(() =>
150
186
  this.once(this.tans.lookupDialog(), async (dialog) => {
151
- const pages = await this.responses.startDialogWith(dialog, dialog.startWith(query.order(undefined)));
152
- return parseBankData(() => query.read(pages));
187
+ const answers = await dialog.loginWith(query.order(undefined));
188
+ return parseBankData(() => query.read(answers));
153
189
  }),
154
190
  );
155
191
  }
156
192
 
157
193
  /** Runs `use` in `dialog`, a dialog of its own, and ends it afterwards, however `use` ends. */
158
- private async once<T>(dialog: ProtocolDialog<R, O>, use: (dialog: ProtocolDialog<R, O>) => Promise<T>): Promise<T> {
194
+ private async once<T>(dialog: FinTs3Dialog, use: (dialog: SignedDialog) => Promise<T>): Promise<T> {
195
+ const signed = new SignedDialog(dialog, this.session);
159
196
  try {
160
- return await use(dialog);
197
+ return await use(signed);
161
198
  } finally {
162
- await dialog.end();
199
+ await signed.end();
163
200
  }
164
201
  }
165
202
  }
@@ -1,16 +1,19 @@
1
1
  import { Account } from '../core/account.js';
2
2
  import { hostOf, type FinTsConfig } from '../core/config.js';
3
+ import { FinTsFeedback } from '../core/feedback.js';
3
4
  import { FormatError } from '../util/errors.js';
4
5
  import {
5
6
  isJsonObject,
6
7
  isString,
7
8
  readObject,
8
9
  readOptionalInt,
10
+ readOptionalObject,
9
11
  readOptionalList,
10
12
  readOptionalString,
11
13
  type JsonObject,
12
14
  } from '../util/json.js';
13
- import type { BankResponse, PinTanProtocol } from './protocol.js';
15
+ import type { ParameterStore } from '../v3/parameter-store.js';
16
+ import type { LoginGuard, LoginGuardState } from './login-guard.js';
14
17
 
15
18
  /** Layout of the saved session; a state of another layout is ignored. */
16
19
  const stateFormat = 1;
@@ -18,40 +21,44 @@ const stateFormat = 1;
18
21
  /** The protocol version the state is for, named as the Dart package names it, so that both read each other's states. */
19
22
  const protocolVersion = 'v300';
20
23
 
21
- /** The session and the protocol's parameter data as one JSON string, which `restoreSession` reads. */
22
- export function saveSession<R extends BankResponse, O>(protocol: PinTanProtocol<R, O>, session: SessionState): string {
24
+ /** The session, the parameter data and the guard's refusal as JSON, which `restoreSession` reads. */
25
+ export function saveSession(parameters: ParameterStore, guard: LoginGuard, session: SessionState): string {
23
26
  return JSON.stringify({
24
27
  format: stateFormat,
25
28
  version: protocolVersion,
26
- owner: ownerOf(protocol.config),
29
+ owner: ownerOf(parameters.config),
27
30
  session: session.toJson(),
28
- protocol: protocol.saveState(),
31
+ // Under the key the Dart package reads as well.
32
+ protocol: parameters.saveState(),
33
+ loginGuard: guardStateToJson(guard.saveState()),
29
34
  });
30
35
  }
31
36
 
32
37
  /**
33
- * The session saved in `json`, and the protocol's parameter data restored from it.
38
+ * The session saved in `json`, and the parameter data and the guard's refused login restored from it.
34
39
  *
35
40
  * A missing or damaged state, or one saved for another user, bank, server or protocol version, gives a new session,
36
- * which only costs a new synchronization.
41
+ * which only costs a new synchronization. The refused login and the count of logins whose answer was lost belong to the
42
+ * user at the bank, so they come first: they hold for another server or protocol version as well, and where the rest is
43
+ * damaged; a state saved by fints-ts 0.1 or the Dart package 0.2.0 has neither. A damaged section of their own fails
44
+ * closed: it restores a refusal without return codes, since it may have held one.
37
45
  */
38
- export function restoreSession<R extends BankResponse, O>(
39
- json: string | undefined,
40
- protocol: PinTanProtocol<R, O>,
41
- ): SessionState {
46
+ export function restoreSession(json: string | undefined, parameters: ParameterStore, guard: LoginGuard): SessionState {
42
47
  if (json === undefined) return new SessionState();
43
48
  try {
44
49
  const saved: unknown = JSON.parse(json);
45
- if (!isJsonObject(saved) || saved.format !== stateFormat || saved.version !== protocolVersion) {
46
- return new SessionState();
47
- }
48
- const owner = readOptionalList(saved, 'owner', isString);
49
- const expected = ownerOf(protocol.config);
50
- if (owner?.length !== expected.length || owner.some((value, i) => value !== expected[i])) {
51
- return new SessionState();
52
- }
50
+ if (!isJsonObject(saved) || saved.format !== stateFormat) return new SessionState();
51
+ // Entries of another type differ like any other value, so that a damaged server entry keeps the refused login.
52
+ const owner: unknown = saved.owner ?? [];
53
+ if (!Array.isArray(owner)) return new SessionState();
54
+ const [server, ...user] = owner as unknown[];
55
+ const expected = userOf(parameters.config);
56
+ if (user.length !== expected.length || user.some((value, i) => value !== expected[i])) return new SessionState();
57
+ const guardState = savedGuardState(saved);
58
+ if (guardState !== undefined) guard.restoreState(guardState);
59
+ if (server !== hostOf(parameters.config.url) || saved.version !== protocolVersion) return new SessionState();
53
60
  const session = SessionState.fromJson(readObject(saved, 'session'));
54
- protocol.restoreState(readObject(saved, 'protocol'));
61
+ parameters.restoreState(readObject(saved, 'protocol'), session.tanMedium);
55
62
  return session;
56
63
  } catch (error) {
57
64
  // Damaged JSON, base64 or parameter data, and values of the wrong type, only cost a new synchronization.
@@ -60,9 +67,14 @@ export function restoreSession<R extends BankResponse, O>(
60
67
  }
61
68
  }
62
69
 
63
- /** Whose data a state holds, so that the state of another user or bank is never used. */
70
+ /** Whose data a state holds, so that the state of another user or bank is never used: the server, then `userOf`. */
64
71
  function ownerOf(config: FinTsConfig): string[] {
65
- return [hostOf(config.url), config.bankCode, config.userId, config.customerId];
72
+ return [hostOf(config.url), ...userOf(config)];
73
+ }
74
+
75
+ /** The user at the bank, whom a refused login belongs to. */
76
+ function userOf(config: FinTsConfig): string[] {
77
+ return [config.bankCode, config.userId, config.customerId];
66
78
  }
67
79
 
68
80
  /**
@@ -129,7 +141,9 @@ export class SessionState {
129
141
  const state = new SessionState();
130
142
  state.useTanMethod(readOptionalString(json, 'tanMethod'), readOptionalString(json, 'tanMedium'));
131
143
  if (allowedMethods !== undefined) state.rememberAllowedMethods(allowedMethods);
132
- if (sepaAccounts !== undefined) state.cacheSepaAccounts(sepaAccounts, sepaUpdVersion);
144
+ // fints-ts 0.1 and the Dart package 0.2.0 saved an empty list as well, which completes nothing; without it, the
145
+ // list is asked for again.
146
+ if (sepaAccounts !== undefined && sepaAccounts.length > 0) state.cacheSepaAccounts(sepaAccounts, sepaUpdVersion);
133
147
  return state;
134
148
  }
135
149
 
@@ -168,3 +182,40 @@ function accountFromJson(json: JsonObject): Account {
168
182
  bankCode,
169
183
  });
170
184
  }
185
+
186
+ function guardStateToJson(state: LoginGuardState): JsonObject {
187
+ return { refusal: state.refusal?.map(feedbackToJson) ?? null, unreadLogins: state.unreadLogins };
188
+ }
189
+
190
+ /** The guard's section of `saved`, `undefined` where there is none; a damaged one is a refusal without return codes. */
191
+ function savedGuardState(saved: JsonObject): LoginGuardState | undefined {
192
+ try {
193
+ const json = readOptionalObject(saved, 'loginGuard');
194
+ return json === undefined ? undefined : guardStateFromJson(json);
195
+ } catch (error) {
196
+ if (error instanceof FormatError) return { refusal: [], unreadLogins: 0 };
197
+ throw error;
198
+ }
199
+ }
200
+
201
+ function guardStateFromJson(json: JsonObject): LoginGuardState {
202
+ return {
203
+ refusal: readOptionalList(json, 'refusal', isJsonObject)?.map(feedbackFromJson),
204
+ unreadLogins: readOptionalInt(json, 'unreadLogins') ?? 0,
205
+ };
206
+ }
207
+
208
+ function feedbackToJson(f: FinTsFeedback): JsonObject {
209
+ return { code: f.code, reference: f.reference ?? null, text: f.text, parameters: [...f.parameters] };
210
+ }
211
+
212
+ function feedbackFromJson(json: JsonObject): FinTsFeedback {
213
+ const code = readOptionalString(json, 'code');
214
+ if (code === undefined) throw new FormatError('Saved state: return code without code');
215
+ return new FinTsFeedback({
216
+ code,
217
+ reference: readOptionalString(json, 'reference'),
218
+ text: readOptionalString(json, 'text') ?? '',
219
+ parameters: readOptionalList(json, 'parameters', isString) ?? [],
220
+ });
221
+ }
@@ -0,0 +1,222 @@
1
+ import { requireLatin1 } from '../core/config.js';
2
+ import {
3
+ bankErrorFor,
4
+ FinTsBankError,
5
+ FinTsLoginDataChangedError,
6
+ FinTsLoginRefusedError,
7
+ FinTsProtocolError,
8
+ FinTsTanCancelledError,
9
+ } from '../core/errors.js';
10
+ import {
11
+ allowedTanMethodsIn,
12
+ FeedbackCodes,
13
+ hasCode,
14
+ hasErrors,
15
+ userWarnings,
16
+ type FinTsFeedback,
17
+ } from '../core/feedback.js';
18
+ import type { TanMethod } from '../tan/tan-models.js';
19
+ import { invalidArgument } from '../util/errors.js';
20
+ import { trimBlanks, withAsciiForms } from '../util/text.js';
21
+ import type { Approval, FinTs3Dialog } from '../v3/dialog.js';
22
+ import { IncomingMessage3 } from '../v3/message.js';
23
+ import type { ParameterStore } from '../v3/parameter-store.js';
24
+ import type { SegmentSpec } from '../v3/syntax.js';
25
+ import { appApprovalWait } from './app-approval.js';
26
+ import type { LoginGuard, RequestKind } from './login-guard.js';
27
+ import type { SessionState } from './session-state.js';
28
+ import type { UserPrompts } from './user-prompts.js';
29
+
30
+ /** The parts of the session that every `SignedDialog` reports to. */
31
+ export interface SessionParts {
32
+ readonly parameters: ParameterStore;
33
+ readonly state: SessionState;
34
+ readonly prompts: UserPrompts;
35
+ readonly guard: LoginGuard;
36
+ }
37
+
38
+ /**
39
+ * A dialog signed with the PIN, which settles each answer before it hands it out: it keeps the parameter data of the
40
+ * dialog's opening, passes the notices on, has the `LoginGuard` judge the answer and throws its errors, and completes
41
+ * the approval the bank asks for. The session sends every message signed with the PIN through it, so that the guard
42
+ * judges every answer, that to the dialog's end included.
43
+ */
44
+ export class SignedDialog {
45
+ constructor(
46
+ private readonly dialog: FinTs3Dialog,
47
+ private readonly session: SessionParts,
48
+ ) {}
49
+
50
+ /** Whether the bank knows this dialog and it has not ended. */
51
+ get isActive(): boolean {
52
+ return this.dialog.isActive;
53
+ }
54
+
55
+ /** Logs in, which opens the dialog; with `synchronize` the bank also assigns a customer system ID. */
56
+ login(synchronize = false): Promise<IncomingMessage3> {
57
+ return this.settleLogin(() => this.dialog.start(synchronize));
58
+ }
59
+
60
+ /**
61
+ * Logs in, asking strong authentication for `order`, then sends `order` in a message of its own, as for listing the
62
+ * TAN media while no medium is chosen yet (`FinTs3Dialog.startWith`); returns both answers.
63
+ */
64
+ async loginWith(order: SegmentSpec): Promise<IncomingMessage3[]> {
65
+ const start = this.dialog.startWith(order);
66
+ const login = await this.settleLogin(start.login);
67
+ return [login, await this.settle(await start.order(), 'order')];
68
+ }
69
+
70
+ /**
71
+ * Sends `order` and completes the approval the bank asks for. `admitApproval` runs before the user is asked, and
72
+ * refuses the approval by throwing.
73
+ */
74
+ async send(order: SegmentSpec, admitApproval?: () => void): Promise<IncomingMessage3> {
75
+ return this.settle(await this.dialog.send(order), 'order', admitApproval);
76
+ }
77
+
78
+ /**
79
+ * Ends the dialog at the bank; does nothing if it is not active. Of the answer only a rejected PIN counts, which the
80
+ * guard judges: the dialog is over either way.
81
+ */
82
+ async end(): Promise<void> {
83
+ const response = await this.dialog.end();
84
+ if (response !== undefined && hasErrors(response.feedback)) {
85
+ this.session.guard.judge(bankErrorFor(response.feedback), 'order', this.dialog.method);
86
+ }
87
+ }
88
+
89
+ /**
90
+ * Receives `response`, the answer to a request of `kind`: keeps its parameter data where it answers the `opening` of
91
+ * the dialog, the login or its approval, even alongside errors, since the remedy for a refused login may need them;
92
+ * passes its notices on; and has the guard judge it, then throws its errors, as the refusal of the login where they
93
+ * amount to one.
94
+ */
95
+ private receive(response: IncomingMessage3, kind: RequestKind, opening: boolean): void {
96
+ if (opening) this.session.parameters.keep(response);
97
+ this.readNotices(response);
98
+ const error = hasErrors(response.feedback) ? bankErrorFor(response.feedback, kind === 'approval') : undefined;
99
+ const refusal = this.session.guard.judge(error, kind, this.dialog.method);
100
+ // Every further message of a refused login's dialog would carry the refused PIN.
101
+ if (refusal !== undefined) this.dialog.discard();
102
+ if (error !== undefined) throw refusal ?? error;
103
+ }
104
+
105
+ /**
106
+ * Sends the login `send` and settles its answer. New login data next to an error in the answer to its approval are
107
+ * retired all the same (`retireLoginData`), although the guard counts that error as no refusal; next to an error in
108
+ * the answer to the login they are its refusal already (`loginRefusalFor`).
109
+ */
110
+ private async settleLogin(send: () => Promise<IncomingMessage3>): Promise<IncomingMessage3> {
111
+ try {
112
+ return await this.settle(await send(), 'login');
113
+ } catch (error) {
114
+ if (
115
+ !(error instanceof FinTsBankError) ||
116
+ error instanceof FinTsLoginRefusedError ||
117
+ !hasCode(error.feedback, FeedbackCodes.newLoginData)
118
+ ) {
119
+ throw error;
120
+ }
121
+ return this.retireLoginData(error.feedback, 'approval');
122
+ }
123
+ }
124
+
125
+ /**
126
+ * Where `response` answers a login, a request of `kind` other than `order`, or its approval with new login data,
127
+ * retires them (`retireLoginData`). It runs before any approval, which would cost the user a TAN for a session that
128
+ * ends anyway.
129
+ */
130
+ private async retireOldLoginData(response: IncomingMessage3, kind: RequestKind): Promise<void> {
131
+ if (kind === 'order' || !hasCode(response.feedback, FeedbackCodes.newLoginData)) return;
132
+ await this.retireLoginData(response.feedback, kind);
133
+ }
134
+
135
+ /**
136
+ * Ends the dialog, whose answer to a request of `kind` brought new login data with `feedback`, stops the client from
137
+ * logging in with the old ones, and fails the call with `FinTsLoginDataChangedError` (Rückmeldungscodes C.6).
138
+ */
139
+ private async retireLoginData(feedback: readonly FinTsFeedback[], kind: RequestKind): Promise<never> {
140
+ await this.end();
141
+ const error = new FinTsLoginDataChangedError(feedback);
142
+ throw this.session.guard.judge(error, kind, this.dialog.method) ?? error;
143
+ }
144
+
145
+ /**
146
+ * Receives `response`, the answer to a request of `kind`, then completes the approval it asks for, once
147
+ * `admitApproval` has let it; returns it joined with the response that completes the approval.
148
+ *
149
+ * Throws `FinTsProtocolError` if that response asks for an approval once more: the bank has not executed the message
150
+ * then either, and read as done it would report an empty result.
151
+ */
152
+ private async settle(
153
+ response: IncomingMessage3,
154
+ kind: RequestKind,
155
+ admitApproval?: () => void,
156
+ ): Promise<IncomingMessage3> {
157
+ const opening = kind !== 'order';
158
+ this.receive(response, kind, opening);
159
+ await this.retireOldLoginData(response, kind);
160
+ const approval = this.dialog.approvalAskedIn(response);
161
+ if (approval === undefined) return response;
162
+ admitApproval?.();
163
+ const approved = approval.challenge.isDecoupled
164
+ ? await this.awaitAppApproval(approval, opening)
165
+ : await this.enterTan(approval, opening);
166
+ await this.retireOldLoginData(approved, kind);
167
+ if (this.dialog.approvalAskedIn(approved) !== undefined) {
168
+ throw new FinTsProtocolError('The bank asks for another approval in answer to the approval');
169
+ }
170
+ return IncomingMessage3.combine(response, approved);
171
+ }
172
+
173
+ /** Passes on the notices in `response`: the TAN methods the user may use (3920), the bank's messages and warnings. */
174
+ private readNotices(response: IncomingMessage3): void {
175
+ const allowed = allowedTanMethodsIn(response.feedback);
176
+ if (allowed.length > 0) this.session.state.rememberAllowedMethods(allowed);
177
+ for (const message of response.bankMessages) this.session.prompts.showBankMessage(message);
178
+ for (const warning of userWarnings(response.feedback)) this.session.prompts.showWarning(warning);
179
+ }
180
+
181
+ /** Asks the user for the TAN `approval` takes and sends it; `opening` tells whether it approves the login. */
182
+ private async enterTan(approval: Approval<IncomingMessage3>, opening: boolean): Promise<IncomingMessage3> {
183
+ const answer = await this.session.prompts.askTan(approval.challenge);
184
+ if (answer.kind === 'approved') {
185
+ throw invalidArgument('Answered a TAN challenge with an approval in the banking app', 'tanCallback');
186
+ }
187
+ // Not NFKC, which also changes characters FinTS 3.0 sends as they are, such as `²`, in a TAN the bank compares.
188
+ const tan = answer.kind === 'tan' ? trimBlanks(withAsciiForms(answer.tan)) : '';
189
+ if (tan === '') throw new FinTsTanCancelledError();
190
+ const response = await approval.submitTan(requireTanOf(approval.challenge.method, tan));
191
+ this.receive(response, 'approval', opening);
192
+ return response;
193
+ }
194
+
195
+ /** Waits for the user to approve in the banking app; `opening` tells whether the approval is that of the login. */
196
+ private awaitAppApproval(approval: Approval<IncomingMessage3>, opening: boolean): Promise<IncomingMessage3> {
197
+ const prompts = this.session.prompts;
198
+ return appApprovalWait(approval, {
199
+ ask: (challenge) => prompts.askTan(challenge),
200
+ pause: (delayMs, interrupt) => prompts.pause(delayMs, interrupt),
201
+ receive: (response) => {
202
+ this.receive(response, 'approval', opening);
203
+ },
204
+ }).run();
205
+ }
206
+ }
207
+
208
+ /**
209
+ * Throws `TypeError` for a TAN that `method` rules out, which the bank would count as a failed attempt: one longer than
210
+ * `TanMethod.maxTanLength`, or with other characters than digits for a `TanMethod.numericTan` method; and for one that
211
+ * FinTS 3.0 cannot send. The message leaves out the TAN.
212
+ */
213
+ function requireTanOf(method: TanMethod, tan: string): string {
214
+ const max = method.maxTanLength;
215
+ if (max !== undefined && max > 0 && tan.length > max) {
216
+ throw invalidArgument(`The TAN method ${method.name} takes TANs of at most ${max} characters`, 'tan');
217
+ }
218
+ if (method.numericTan && !/^[0-9]+$/.test(tan)) {
219
+ throw invalidArgument(`The TAN method ${method.name} takes digits only`, 'tan');
220
+ }
221
+ return requireLatin1(tan, 'tan');
222
+ }