@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.
- package/CHANGELOG.md +519 -1
- package/README.md +138 -40
- package/dist/client.d.ts +49 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +242 -0
- package/dist/client.js.map +1 -0
- package/dist/core/account.d.ts +46 -7
- package/dist/core/account.d.ts.map +1 -1
- package/dist/core/account.js +143 -24
- package/dist/core/account.js.map +1 -1
- package/dist/core/amount.d.ts +15 -4
- package/dist/core/amount.d.ts.map +1 -1
- package/dist/core/amount.js +46 -9
- package/dist/core/amount.js.map +1 -1
- package/dist/core/balance.d.ts +4 -0
- package/dist/core/balance.d.ts.map +1 -1
- package/dist/core/balance.js +11 -5
- package/dist/core/balance.js.map +1 -1
- package/dist/core/bank-info.d.ts +21 -4
- package/dist/core/bank-info.d.ts.map +1 -1
- package/dist/core/bank-info.js +26 -5
- package/dist/core/bank-info.js.map +1 -1
- package/dist/core/client.d.ts +81 -19
- package/dist/core/client.d.ts.map +1 -1
- package/dist/core/config.d.ts +40 -20
- package/dist/core/config.d.ts.map +1 -1
- package/dist/core/config.js +85 -30
- package/dist/core/config.js.map +1 -1
- package/dist/core/errors.d.ts +100 -26
- package/dist/core/errors.d.ts.map +1 -1
- package/dist/core/errors.js +136 -53
- package/dist/core/errors.js.map +1 -1
- package/dist/core/feedback.d.ts +11 -1
- package/dist/core/feedback.d.ts.map +1 -1
- package/dist/core/feedback.js +14 -2
- package/dist/core/feedback.js.map +1 -1
- package/dist/core/transaction.d.ts +11 -1
- package/dist/core/transaction.d.ts.map +1 -1
- package/dist/core/transaction.js +21 -4
- package/dist/core/transaction.js.map +1 -1
- package/dist/core/transport.d.ts +48 -6
- package/dist/core/transport.d.ts.map +1 -1
- package/dist/core/transport.js +126 -29
- package/dist/core/transport.js.map +1 -1
- package/dist/formats/camt.d.ts +28 -5
- package/dist/formats/camt.d.ts.map +1 -1
- package/dist/formats/camt.js +132 -50
- package/dist/formats/camt.js.map +1 -1
- package/dist/formats/mt940.d.ts +29 -5
- package/dist/formats/mt940.d.ts.map +1 -1
- package/dist/formats/mt940.js +209 -113
- package/dist/formats/mt940.js.map +1 -1
- package/dist/formats/readers.d.ts +6 -2
- package/dist/formats/readers.d.ts.map +1 -1
- package/dist/formats/readers.js +28 -29
- package/dist/formats/readers.js.map +1 -1
- package/dist/formats/record-budget.d.ts +7 -0
- package/dist/formats/record-budget.d.ts.map +1 -0
- package/dist/formats/record-budget.js +17 -0
- package/dist/formats/record-budget.js.map +1 -0
- package/dist/formats/sepa-purpose.d.ts +9 -0
- package/dist/formats/sepa-purpose.d.ts.map +1 -1
- package/dist/formats/sepa-purpose.js +32 -2
- package/dist/formats/sepa-purpose.js.map +1 -1
- package/dist/index.d.ts +3 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -2
- package/dist/index.js.map +1 -1
- package/dist/session/app-approval.d.ts +11 -5
- package/dist/session/app-approval.d.ts.map +1 -1
- package/dist/session/app-approval.js +23 -12
- package/dist/session/app-approval.js.map +1 -1
- package/dist/session/dialog-runner.d.ts +11 -11
- package/dist/session/dialog-runner.d.ts.map +1 -1
- package/dist/session/dialog-runner.js +34 -19
- package/dist/session/dialog-runner.js.map +1 -1
- package/dist/session/login-guard.d.ts +54 -49
- package/dist/session/login-guard.d.ts.map +1 -1
- package/dist/session/login-guard.js +170 -83
- package/dist/session/login-guard.js.map +1 -1
- package/dist/session/sepa-accounts.d.ts +20 -14
- package/dist/session/sepa-accounts.d.ts.map +1 -1
- package/dist/session/sepa-accounts.js +62 -46
- package/dist/session/sepa-accounts.js.map +1 -1
- package/dist/session/session-setup.d.ts +41 -24
- package/dist/session/session-setup.d.ts.map +1 -1
- package/dist/session/session-setup.js +106 -63
- package/dist/session/session-setup.js.map +1 -1
- package/dist/session/session-state.d.ts +10 -6
- package/dist/session/session-state.d.ts.map +1 -1
- package/dist/session/session-state.js +72 -18
- package/dist/session/session-state.js.map +1 -1
- package/dist/session/signed-dialog.d.ts +83 -0
- package/dist/session/signed-dialog.d.ts.map +1 -0
- package/dist/session/signed-dialog.js +185 -0
- package/dist/session/signed-dialog.js.map +1 -0
- package/dist/session/tan-selection.d.ts +26 -18
- package/dist/session/tan-selection.d.ts.map +1 -1
- package/dist/session/tan-selection.js +44 -33
- package/dist/session/tan-selection.js.map +1 -1
- package/dist/session/user-prompts.d.ts +14 -5
- package/dist/session/user-prompts.d.ts.map +1 -1
- package/dist/session/user-prompts.js +53 -18
- package/dist/session/user-prompts.js.map +1 -1
- package/dist/tan/challenge.d.ts +47 -24
- package/dist/tan/challenge.d.ts.map +1 -1
- package/dist/tan/challenge.js +56 -51
- package/dist/tan/challenge.js.map +1 -1
- package/dist/tan/flicker.d.ts +11 -1
- package/dist/tan/flicker.d.ts.map +1 -1
- package/dist/tan/flicker.js +70 -24
- package/dist/tan/flicker.js.map +1 -1
- package/dist/tan/tan-image.d.ts +6 -2
- package/dist/tan/tan-image.d.ts.map +1 -1
- package/dist/tan/tan-image.js +14 -6
- package/dist/tan/tan-image.js.map +1 -1
- package/dist/tan/tan-models.d.ts +14 -2
- package/dist/tan/tan-models.d.ts.map +1 -1
- package/dist/tan/tan-models.js +24 -7
- package/dist/tan/tan-models.js.map +1 -1
- package/dist/util/arguments.d.ts +13 -0
- package/dist/util/arguments.d.ts.map +1 -0
- package/dist/util/arguments.js +40 -0
- package/dist/util/arguments.js.map +1 -0
- package/dist/util/async.d.ts +4 -3
- package/dist/util/async.d.ts.map +1 -1
- package/dist/util/async.js +6 -7
- package/dist/util/async.js.map +1 -1
- package/dist/util/bytes.d.ts +3 -4
- package/dist/util/bytes.d.ts.map +1 -1
- package/dist/util/bytes.js +13 -21
- package/dist/util/bytes.js.map +1 -1
- package/dist/util/call-context.d.ts.map +1 -1
- package/dist/util/call-context.js +3 -2
- package/dist/util/call-context.js.map +1 -1
- package/dist/util/dates.d.ts +6 -20
- package/dist/util/dates.d.ts.map +1 -1
- package/dist/util/dates.js +58 -28
- package/dist/util/dates.js.map +1 -1
- package/dist/util/errors.d.ts +13 -3
- package/dist/util/errors.d.ts.map +1 -1
- package/dist/util/errors.js +19 -6
- package/dist/util/errors.js.map +1 -1
- package/dist/util/immutable-map.d.ts +5 -2
- package/dist/util/immutable-map.d.ts.map +1 -1
- package/dist/util/immutable-map.js +15 -12
- package/dist/util/immutable-map.js.map +1 -1
- package/dist/util/inspect.d.ts +11 -0
- package/dist/util/inspect.d.ts.map +1 -0
- package/dist/util/inspect.js +14 -0
- package/dist/util/inspect.js.map +1 -0
- package/dist/util/json.d.ts +4 -0
- package/dist/util/json.d.ts.map +1 -1
- package/dist/util/json.js +18 -0
- package/dist/util/json.js.map +1 -1
- package/dist/util/text.d.ts +38 -5
- package/dist/util/text.d.ts.map +1 -1
- package/dist/util/text.js +90 -16
- package/dist/util/text.js.map +1 -1
- package/dist/util/xml.d.ts +9 -6
- package/dist/util/xml.d.ts.map +1 -1
- package/dist/util/xml.js +129 -56
- package/dist/util/xml.js.map +1 -1
- package/dist/v3/account-extension.d.ts.map +1 -1
- package/dist/v3/account-extension.js +7 -10
- package/dist/v3/account-extension.js.map +1 -1
- package/dist/v3/bank-connection.d.ts +54 -0
- package/dist/v3/bank-connection.d.ts.map +1 -0
- package/dist/v3/bank-connection.js +118 -0
- package/dist/v3/bank-connection.js.map +1 -0
- package/dist/v3/dialog.d.ts +84 -46
- package/dist/v3/dialog.d.ts.map +1 -1
- package/dist/v3/dialog.js +174 -60
- package/dist/v3/dialog.js.map +1 -1
- package/dist/v3/message.d.ts +32 -11
- package/dist/v3/message.d.ts.map +1 -1
- package/dist/v3/message.js +144 -83
- package/dist/v3/message.js.map +1 -1
- package/dist/v3/parameter-store.d.ts +111 -0
- package/dist/v3/parameter-store.d.ts.map +1 -0
- package/dist/v3/parameter-store.js +197 -0
- package/dist/v3/parameter-store.js.map +1 -0
- package/dist/v3/parameters.d.ts +44 -17
- package/dist/v3/parameters.d.ts.map +1 -1
- package/dist/v3/parameters.js +136 -60
- package/dist/v3/parameters.js.map +1 -1
- package/dist/v3/protocol.d.ts +50 -50
- package/dist/v3/protocol.d.ts.map +1 -1
- package/dist/v3/protocol.js +91 -210
- package/dist/v3/protocol.js.map +1 -1
- package/dist/v3/queries.d.ts +51 -0
- package/dist/v3/queries.d.ts.map +1 -0
- package/dist/v3/queries.js +128 -0
- package/dist/v3/queries.js.map +1 -0
- package/dist/v3/segments.d.ts +43 -19
- package/dist/v3/segments.d.ts.map +1 -1
- package/dist/v3/segments.js +84 -33
- package/dist/v3/segments.js.map +1 -1
- package/dist/v3/syntax.d.ts +68 -13
- package/dist/v3/syntax.d.ts.map +1 -1
- package/dist/v3/syntax.js +183 -102
- package/dist/v3/syntax.js.map +1 -1
- package/package.json +10 -6
- package/src/client.ts +293 -0
- package/src/core/account.ts +153 -24
- package/src/core/amount.ts +45 -10
- package/src/core/balance.ts +12 -5
- package/src/core/bank-info.ts +29 -5
- package/src/core/client.ts +83 -19
- package/src/core/config.ts +95 -35
- package/src/core/errors.ts +148 -54
- package/src/core/feedback.ts +16 -2
- package/src/core/transaction.ts +22 -4
- package/src/core/transport.ts +153 -32
- package/src/formats/camt.ts +153 -45
- package/src/formats/mt940.ts +244 -122
- package/src/formats/readers.ts +28 -31
- package/src/formats/record-budget.ts +18 -0
- package/src/formats/sepa-purpose.ts +29 -2
- package/src/index.ts +4 -2
- package/src/session/app-approval.ts +51 -38
- package/src/session/dialog-runner.ts +61 -31
- package/src/session/login-guard.ts +189 -87
- package/src/session/sepa-accounts.ts +63 -45
- package/src/session/session-setup.ts +105 -68
- package/src/session/session-state.ts +74 -23
- package/src/session/signed-dialog.ts +222 -0
- package/src/session/tan-selection.ts +50 -39
- package/src/session/user-prompts.ts +62 -18
- package/src/tan/challenge.ts +87 -64
- package/src/tan/flicker.ts +72 -23
- package/src/tan/tan-image.ts +15 -6
- package/src/tan/tan-models.ts +26 -7
- package/src/util/arguments.ts +40 -0
- package/src/util/async.ts +10 -9
- package/src/util/bytes.ts +14 -20
- package/src/util/call-context.ts +3 -2
- package/src/util/dates.ts +64 -33
- package/src/util/errors.ts +22 -6
- package/src/util/immutable-map.ts +15 -12
- package/src/util/inspect.ts +14 -0
- package/src/util/json.ts +16 -0
- package/src/util/text.ts +87 -15
- package/src/util/xml.ts +130 -52
- package/src/v3/account-extension.ts +7 -12
- package/src/v3/bank-connection.ts +126 -0
- package/src/v3/dialog.ts +186 -92
- package/src/v3/message.ts +140 -79
- package/src/v3/parameter-store.ts +226 -0
- package/src/v3/parameters.ts +166 -66
- package/src/v3/protocol.ts +103 -253
- package/src/v3/queries.ts +198 -0
- package/src/v3/segments.ts +86 -31
- package/src/v3/syntax.ts +172 -93
- package/dist/session/bank-connection.d.ts +0 -28
- package/dist/session/bank-connection.d.ts.map +0 -1
- package/dist/session/bank-connection.js +0 -48
- package/dist/session/bank-connection.js.map +0 -1
- package/dist/session/parameter-data.d.ts +0 -83
- package/dist/session/parameter-data.d.ts.map +0 -1
- package/dist/session/parameter-data.js +0 -68
- package/dist/session/parameter-data.js.map +0 -1
- package/dist/session/pin-tan-client.d.ts +0 -54
- package/dist/session/pin-tan-client.d.ts.map +0 -1
- package/dist/session/pin-tan-client.js +0 -153
- package/dist/session/pin-tan-client.js.map +0 -1
- package/dist/session/pin-tan-dialog.d.ts +0 -71
- package/dist/session/pin-tan-dialog.d.ts.map +0 -1
- package/dist/session/pin-tan-dialog.js +0 -124
- package/dist/session/pin-tan-dialog.js.map +0 -1
- package/dist/session/protocol.d.ts +0 -209
- package/dist/session/protocol.d.ts.map +0 -1
- package/dist/session/protocol.js +0 -75
- package/dist/session/protocol.js.map +0 -1
- package/dist/session/response-handler.d.ts +0 -48
- package/dist/session/response-handler.d.ts.map +0 -1
- package/dist/session/response-handler.js +0 -121
- package/dist/session/response-handler.js.map +0 -1
- package/dist/v3/client.d.ts +0 -24
- package/dist/v3/client.d.ts.map +0 -1
- package/dist/v3/client.js +0 -16
- package/dist/v3/client.js.map +0 -1
- package/src/session/bank-connection.ts +0 -51
- package/src/session/parameter-data.ts +0 -137
- package/src/session/pin-tan-client.ts +0 -186
- package/src/session/pin-tan-dialog.ts +0 -160
- package/src/session/protocol.ts +0 -311
- package/src/session/response-handler.ts +0 -130
- package/src/v3/client.ts +0 -29
|
@@ -1,27 +1,28 @@
|
|
|
1
1
|
import type { BankInfo } from '../core/bank-info.js';
|
|
2
|
-
import {
|
|
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
|
|
5
|
-
import {
|
|
6
|
-
import
|
|
7
|
-
import type {
|
|
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
|
|
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
|
|
22
|
-
private readonly
|
|
23
|
-
private readonly tans: TanSelection
|
|
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.
|
|
33
|
-
if (this.
|
|
34
|
-
return requireBankParameters(this.
|
|
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
|
-
/**
|
|
44
|
-
|
|
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)
|
|
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<
|
|
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,
|
|
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
|
-
*
|
|
61
|
-
*
|
|
62
|
-
*
|
|
63
|
-
*
|
|
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
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
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
|
-
/**
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
|
113
|
-
*
|
|
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
|
-
|
|
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
|
-
/**
|
|
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
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
if (
|
|
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.
|
|
183
|
+
const query = tanMediaQuery(this.session.parameters);
|
|
148
184
|
if (query === undefined) return undefined;
|
|
149
|
-
return
|
|
185
|
+
return tryLookUp(() =>
|
|
150
186
|
this.once(this.tans.lookupDialog(), async (dialog) => {
|
|
151
|
-
const
|
|
152
|
-
return parseBankData(() => query.read(
|
|
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:
|
|
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(
|
|
197
|
+
return await use(signed);
|
|
161
198
|
} finally {
|
|
162
|
-
await
|
|
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 {
|
|
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
|
|
22
|
-
export function saveSession
|
|
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(
|
|
29
|
+
owner: ownerOf(parameters.config),
|
|
27
30
|
session: session.toJson(),
|
|
28
|
-
|
|
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
|
|
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
|
|
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
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
const
|
|
50
|
-
|
|
51
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
+
}
|