@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,16 +1,23 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
1
|
+
import {
|
|
2
|
+
clientClosedError,
|
|
3
|
+
FinTsLoginDataChangedError,
|
|
4
|
+
FinTsLoginRefusedError,
|
|
5
|
+
FinTsPinError,
|
|
6
|
+
loginRefusalFor,
|
|
7
|
+
type FinTsBankError,
|
|
8
|
+
} from '../core/errors.js';
|
|
9
|
+
import { allowedTanMethodsIn, FeedbackCodes, hasCode, type FinTsFeedback } from '../core/feedback.js';
|
|
3
10
|
import type { TanMethod } from '../tan/tan-models.js';
|
|
11
|
+
import type { PinGuard } from '../v3/protocol.js';
|
|
4
12
|
|
|
5
13
|
/**
|
|
6
14
|
* The kind of request a bank response answers, which decides what its errors mean for the login (`LoginGuard.judge`)
|
|
7
15
|
* and whether they can reject a TAN:
|
|
8
|
-
* - `login`: the login that opens a dialog;
|
|
9
|
-
* - `loginWithOrder`: a login that carries an order in the same message, whose errors may concern the order alone;
|
|
16
|
+
* - `login`: the login that opens a dialog, which carries no order;
|
|
10
17
|
* - `order`: an order in a dialog the bank has opened;
|
|
11
18
|
* - `approval`: a message that completes an approval, the TAN or the question whether the user approved in the app.
|
|
12
19
|
*/
|
|
13
|
-
export type RequestKind = 'login' | '
|
|
20
|
+
export type RequestKind = 'login' | 'order' | 'approval';
|
|
14
21
|
|
|
15
22
|
/**
|
|
16
23
|
* A reason for a refused login that the session can take care of by itself, after which it logs in once more: the user
|
|
@@ -18,118 +25,213 @@ export type RequestKind = 'login' | 'loginWithOrder' | 'order' | 'approval';
|
|
|
18
25
|
*/
|
|
19
26
|
export type RefusalReason = 'tanMethodNotAllowed' | 'systemIdRejected';
|
|
20
27
|
|
|
28
|
+
/** What the saved state keeps of the guard: the return codes of the bank's refusal, and `unreadLogins`. */
|
|
29
|
+
export interface LoginGuardState {
|
|
30
|
+
readonly refusal: readonly FinTsFeedback[] | undefined;
|
|
31
|
+
readonly unreadLogins: number;
|
|
32
|
+
}
|
|
33
|
+
|
|
21
34
|
/**
|
|
22
|
-
*
|
|
23
|
-
* PIN
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
* Errors in the answer to an order or an approval are no refusal, such as a wrong TAN at login: the bank checks the PIN
|
|
32
|
-
* before it asks for the approval. New login data (return code 3072) end the old ones for good (`refuse`).
|
|
35
|
+
* Logins in a row whose answer the client could not read, after which it sends the PIN no more: one may be a glitch of
|
|
36
|
+
* the connection, but with two the client would not learn of a wrong PIN before the bank locks the access.
|
|
37
|
+
*/
|
|
38
|
+
const maxUnreadLogins = 2;
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Holds the PIN and hands it out only while no login is refused (`pin`), so that a retrying app cannot lock the user's
|
|
42
|
+
* access. `FinTsLoginRefusedError` tells which logins count as refused; `judge` decides it for each answer.
|
|
33
43
|
*
|
|
34
|
-
*
|
|
35
|
-
* session has taken care of a
|
|
36
|
-
*
|
|
44
|
+
* A refusal counts at once and fails closed: only `remedied` lifts it, within the call that met it (`runCall`), once
|
|
45
|
+
* the session has taken care of a reason it names, or has failed the call trying, each reason once per call, so that a
|
|
46
|
+
* call sends at most three logins the bank refuses. A refusal that outlasts the call is final, and the guard forgets
|
|
47
|
+
* the PIN; only a PIN the user enters anew lifts it (`replacePin`), unless it brings new login data.
|
|
37
48
|
*/
|
|
38
|
-
export class LoginGuard {
|
|
39
|
-
private
|
|
49
|
+
export class LoginGuard implements PinGuard {
|
|
50
|
+
// ECMAScript private, so that neither `util.inspect` nor `JSON.stringify` of the client reaches it.
|
|
51
|
+
#pin: string | undefined;
|
|
52
|
+
|
|
53
|
+
private refusalValue: FinTsLoginRefusedError | undefined;
|
|
54
|
+
|
|
55
|
+
/** The TAN method of the login `refusalValue` refused, which a restored refusal does not tell. */
|
|
56
|
+
private refusedMethod: TanMethod | undefined;
|
|
57
|
+
|
|
58
|
+
/** Logins sent since the bank last took the PIN whose answer the client has not read. */
|
|
59
|
+
private unreadLogins = 0;
|
|
40
60
|
|
|
41
|
-
/** The
|
|
42
|
-
private
|
|
61
|
+
/** The refusal of `maxUnreadLogins` unread logins, kept so that `check` throws the same error each time. */
|
|
62
|
+
private unreadLoginsRefusal: FinTsLoginRefusedError | undefined;
|
|
43
63
|
|
|
44
|
-
/**
|
|
64
|
+
/** The reasons `remedied` has lifted a refusal for in the running call. */
|
|
65
|
+
private readonly pardonedReasons = new Set<RefusalReason>();
|
|
66
|
+
|
|
67
|
+
constructor(pin: string) {
|
|
68
|
+
this.#pin = pin;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** The PIN to sign a message with; throws the refusal instead (`check`), and `FinTsStateError` once closed. */
|
|
72
|
+
get pin(): string {
|
|
73
|
+
this.check();
|
|
74
|
+
if (this.#pin === undefined) throw clientClosedError();
|
|
75
|
+
return this.#pin;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** Throws the refusal, if there is one. */
|
|
45
79
|
check(): void {
|
|
46
|
-
|
|
80
|
+
const refusal = this.refusal;
|
|
81
|
+
if (refusal !== undefined) throw refusal;
|
|
47
82
|
}
|
|
48
83
|
|
|
49
|
-
/**
|
|
50
|
-
get
|
|
51
|
-
|
|
84
|
+
/** The bank's refusal, else that of `maxUnreadLogins` unread logins; `undefined` while there is none. */
|
|
85
|
+
get refusal(): FinTsLoginRefusedError | undefined {
|
|
86
|
+
if (this.refusalValue !== undefined || this.unreadLogins < maxUnreadLogins) return this.refusalValue;
|
|
87
|
+
this.unreadLoginsRefusal ??= new FinTsLoginRefusedError(
|
|
88
|
+
[],
|
|
89
|
+
`The answers to the last ${maxUnreadLogins} logins were lost or unreadable, and the bank may have counted them ` +
|
|
90
|
+
'as failed attempts',
|
|
91
|
+
);
|
|
92
|
+
return this.unreadLoginsRefusal;
|
|
52
93
|
}
|
|
53
94
|
|
|
54
|
-
/**
|
|
55
|
-
|
|
56
|
-
|
|
95
|
+
/** Counts a login signed with the PIN as unread until `judge` reads its answer. */
|
|
96
|
+
loginSent(): void {
|
|
97
|
+
this.unreadLogins++;
|
|
57
98
|
}
|
|
58
99
|
|
|
59
100
|
/**
|
|
60
|
-
*
|
|
61
|
-
*
|
|
62
|
-
*
|
|
63
|
-
* `dialogOpen` tells whether the bank keeps the dialog open after its answer.
|
|
101
|
+
* Judges `error`, if the answer to a request of `kind` in a dialog signed with `method` has one: returns the refusal
|
|
102
|
+
* of the login it amounts to, and records it unless the guard holds one already. Any error refuses a login; after the
|
|
103
|
+
* login only one that refuses it itself, a rejected PIN or new login data, since the bank checked the PIN before.
|
|
64
104
|
*/
|
|
65
|
-
judge(error: FinTsBankError, kind: RequestKind,
|
|
66
|
-
const refused = error instanceof
|
|
67
|
-
if (refused
|
|
68
|
-
|
|
69
|
-
|
|
105
|
+
judge(error: FinTsBankError | undefined, kind: RequestKind, method: TanMethod): FinTsLoginRefusedError | undefined {
|
|
106
|
+
const refused = error instanceof FinTsLoginRefusedError || (error !== undefined && kind === 'login');
|
|
107
|
+
if (kind === 'login') this.unreadLogins = refused ? Math.max(0, this.unreadLogins - 1) : 0;
|
|
108
|
+
if (!refused) return undefined;
|
|
109
|
+
const refusal = loginRefusalFor(error.feedback);
|
|
110
|
+
if (this.refusalValue === undefined) {
|
|
111
|
+
this.refusalValue = refusal;
|
|
112
|
+
this.refusedMethod = method;
|
|
70
113
|
}
|
|
71
|
-
return
|
|
114
|
+
return refusal;
|
|
72
115
|
}
|
|
73
116
|
|
|
74
|
-
/**
|
|
75
|
-
|
|
76
|
-
|
|
117
|
+
/** Runs `call`, one call of the client; a refusal that outlasts it is final, and the next call remedies anew. */
|
|
118
|
+
async runCall<T>(call: () => Promise<T>): Promise<T> {
|
|
119
|
+
try {
|
|
120
|
+
return await call();
|
|
121
|
+
} finally {
|
|
122
|
+
this.pardonedReasons.clear();
|
|
123
|
+
this.forgetPinIfRefused();
|
|
124
|
+
}
|
|
77
125
|
}
|
|
78
126
|
|
|
79
127
|
/**
|
|
80
|
-
* Runs `
|
|
81
|
-
*
|
|
82
|
-
*
|
|
83
|
-
* A refused login is thrown, and so is a wrong TAN, which the user must hear of. An aborted dialog counts as a
|
|
84
|
-
* rejection: banks end the dialog along with many.
|
|
128
|
+
* Runs `attempt`, which may log in, and runs it once more each time `remedy` takes care of a reason the refusal it
|
|
129
|
+
* failed with names; `remedy` gets the reason and the TAN method of the refused login, and returns whether it could.
|
|
85
130
|
*/
|
|
86
|
-
async
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
131
|
+
async remedied<T>(
|
|
132
|
+
attempt: () => Promise<T>,
|
|
133
|
+
remedy: (reason: RefusalReason, rejected: TanMethod) => Promise<boolean>,
|
|
134
|
+
): Promise<T> {
|
|
135
|
+
// A refusal of an earlier call stops the attempt before it sends the PIN; there is nothing to remedy.
|
|
136
|
+
if (this.refusal !== undefined) return attempt();
|
|
137
|
+
for (;;) {
|
|
138
|
+
try {
|
|
139
|
+
return await attempt();
|
|
140
|
+
} catch (error) {
|
|
141
|
+
const refusal = this.refusalValue;
|
|
142
|
+
const rejected = this.refusedMethod;
|
|
143
|
+
// Only the bank's refusal the guard holds names reasons; any other error, a second refusal too, fails the call.
|
|
144
|
+
if (refusal === undefined || error !== refusal || rejected === undefined) throw error;
|
|
145
|
+
const reason = remediableReasons(refusal, rejected).find((r) => !this.pardonedReasons.has(r));
|
|
146
|
+
if (reason === undefined) throw error;
|
|
147
|
+
this.pardonedReasons.add(reason);
|
|
148
|
+
// The refusal named a reason the session takes care of, not the PIN, so it does not outlast a remedy that fails,
|
|
149
|
+
// such as a TAN method prompt the app ends: the call fails with the remedy's own error. On a closed client the
|
|
150
|
+
// next attempt fails before it sends anything.
|
|
151
|
+
const remedied = await remedy(reason, rejected).catch((remedyError: unknown) => {
|
|
152
|
+
this.liftRefusal();
|
|
153
|
+
throw remedyError;
|
|
154
|
+
});
|
|
155
|
+
if (!remedied) throw error;
|
|
156
|
+
this.liftRefusal();
|
|
157
|
+
}
|
|
92
158
|
}
|
|
93
159
|
}
|
|
94
160
|
|
|
95
161
|
/**
|
|
96
|
-
*
|
|
97
|
-
* the
|
|
98
|
-
* login data.
|
|
99
|
-
*
|
|
100
|
-
* Only the return codes of the refusal itself count: TAN methods listed earlier say nothing about this login.
|
|
162
|
+
* Signs with `pin` from now on, a PIN the user has entered anew and the protocol can send, and lifts the refusal.
|
|
163
|
+
* Throws the refusal instead where it brings new login data, which end the old ones for good.
|
|
101
164
|
*/
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
165
|
+
replacePin(pin: string): void {
|
|
166
|
+
if (this.refusalValue instanceof FinTsLoginDataChangedError) throw this.refusalValue;
|
|
167
|
+
this.#pin = pin;
|
|
168
|
+
this.liftRefusal();
|
|
169
|
+
this.unreadLogins = 0;
|
|
170
|
+
this.unreadLoginsRefusal = undefined;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/** Forgets the PIN; the client calls it once it is closed. */
|
|
174
|
+
close(): void {
|
|
175
|
+
this.#pin = undefined;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
saveState(): LoginGuardState {
|
|
179
|
+
return { refusal: this.refusalValue?.feedback, unreadLogins: this.unreadLogins };
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/** Restores what `saveState` returned; a restored refusal is final, also one without return codes. */
|
|
183
|
+
restoreState(saved: LoginGuardState): void {
|
|
184
|
+
if (saved.refusal !== undefined) {
|
|
185
|
+
this.refusalValue =
|
|
186
|
+
saved.refusal.length > 0
|
|
187
|
+
? loginRefusalFor(saved.refusal)
|
|
188
|
+
: new FinTsLoginRefusedError([], 'A login was refused, and the saved state no longer tells why');
|
|
120
189
|
}
|
|
121
|
-
|
|
122
|
-
|
|
190
|
+
this.unreadLogins = Math.max(0, saved.unreadLogins);
|
|
191
|
+
this.forgetPinIfRefused();
|
|
123
192
|
}
|
|
124
193
|
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
this.refusal = undefined;
|
|
129
|
-
this.refusedMethodValue = undefined;
|
|
194
|
+
private liftRefusal(): void {
|
|
195
|
+
this.refusalValue = undefined;
|
|
196
|
+
this.refusedMethod = undefined;
|
|
130
197
|
}
|
|
131
198
|
|
|
132
|
-
private
|
|
133
|
-
|
|
199
|
+
private forgetPinIfRefused(): void {
|
|
200
|
+
if (this.refusal !== undefined) this.#pin = undefined;
|
|
134
201
|
}
|
|
135
202
|
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* The reasons `refusal`, of a login signed with `method`, names that the session can take care of: a TAN method the
|
|
206
|
+
* refusal names as not allowed (`refusesMethod`), and 9391. None for a rejected PIN or new login data.
|
|
207
|
+
*/
|
|
208
|
+
function remediableReasons(refusal: FinTsLoginRefusedError, method: TanMethod): RefusalReason[] {
|
|
209
|
+
if (refusal instanceof FinTsPinError || refusal instanceof FinTsLoginDataChangedError) return [];
|
|
210
|
+
const reasons: RefusalReason[] = [];
|
|
211
|
+
if (refusesMethod(refusal.feedback, method)) reasons.push('tanMethodNotAllowed');
|
|
212
|
+
if (hasCode(refusal.feedback, FeedbackCodes.newSystemIdRequired)) reasons.push('systemIdRejected');
|
|
213
|
+
return reasons;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Error codes that cannot be about the PIN: 9050 (the message contains errors) and 9800, which only say that the
|
|
218
|
+
* message failed, and 9955 (the one-step method is not allowed).
|
|
219
|
+
*/
|
|
220
|
+
const errorsNotAboutThePin = new Set(['9050', FeedbackCodes.dialogAborted, '9955']);
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* Whether `feedback`, which refuses a login signed with `method`, names the method as the reason: a 3920 that lists the
|
|
224
|
+
* methods the user may use without it, or 9075 after the one-step method.
|
|
225
|
+
*
|
|
226
|
+
* Only the return codes of the refusal itself count: TAN methods listed earlier say nothing about this login. Banks
|
|
227
|
+
* list two-step methods only, so a 3920 always leaves the one-step method out, also where the login succeeds. After
|
|
228
|
+
* the one-step method a 3920 counts only where the bank names no other error than one not about the PIN: any other,
|
|
229
|
+
* such as "Anmeldung fehlgeschlagen", may be about it. PIN/TAN B.8.2 has the bank answer a one-step dialog of a user
|
|
230
|
+
* bound to two-step methods with 9955 and 3920.
|
|
231
|
+
*/
|
|
232
|
+
function refusesMethod(feedback: readonly FinTsFeedback[], method: TanMethod): boolean {
|
|
233
|
+
if (method.isOneStep && hasCode(feedback, FeedbackCodes.scaRequired)) return true;
|
|
234
|
+
const allowed = allowedTanMethodsIn(feedback);
|
|
235
|
+
if (allowed.length === 0 || allowed.includes(method.securityFunction)) return false;
|
|
236
|
+
return !method.isOneStep || feedback.every((f) => !f.isError || errorsNotAboutThePin.has(f.code));
|
|
237
|
+
}
|
|
@@ -1,55 +1,85 @@
|
|
|
1
|
-
import
|
|
2
|
-
import {
|
|
1
|
+
import { Account, AccountIndex } from '../core/account.js';
|
|
2
|
+
import { FinTsUnsupportedError, tryLookUp } from '../core/errors.js';
|
|
3
|
+
import type { ParameterStore } from '../v3/parameter-store.js';
|
|
4
|
+
import { sepaAccountsQuery } from '../v3/queries.js';
|
|
3
5
|
import type { DialogRunner } from './dialog-runner.js';
|
|
4
|
-
import type { LoginGuard } from './login-guard.js';
|
|
5
|
-
import type { BankResponse, PinTanProtocol } from './protocol.js';
|
|
6
6
|
import type { SessionState } from './session-state.js';
|
|
7
7
|
|
|
8
8
|
/**
|
|
9
9
|
* IBAN and BIC of the user's accounts, from the bank's SEPA account list where the user parameter data lack them
|
|
10
|
-
* (Formals F.3.4).
|
|
10
|
+
* (Formals F.3.4), and the accounts the app passes completed from both.
|
|
11
11
|
*
|
|
12
12
|
* The list is read once per version of the user parameter data and kept in the saved state. Under version 0, which
|
|
13
|
-
* holds for one dialog only (Formals E.2), without user parameter data, or where the bank rejects the list,
|
|
14
|
-
*
|
|
13
|
+
* holds for one dialog only (Formals E.2), without user parameter data, or where the bank rejects the list, also by
|
|
14
|
+
* ending the dialog, or names no account in it, it is kept for this client only: an empty list completes nothing, and
|
|
15
|
+
* saved it would outlast a fault of a single answer until the bank changes its user parameter data.
|
|
15
16
|
*/
|
|
16
|
-
export class SepaAccounts
|
|
17
|
+
export class SepaAccounts {
|
|
17
18
|
/**
|
|
18
19
|
* The list this client keeps unsaved, with the version of the user parameter data it was read under; empty where the
|
|
19
|
-
* bank rejected it.
|
|
20
|
+
* bank rejected it or named no account.
|
|
20
21
|
*/
|
|
21
22
|
private unsaved: { updVersion: number | undefined; accounts: readonly Account[] } | undefined;
|
|
22
23
|
|
|
23
|
-
/** Whether a call already went on without the list because the bank ended the dialog in answer to it. */
|
|
24
|
-
private abortSkipped = false;
|
|
25
|
-
|
|
26
24
|
constructor(
|
|
27
|
-
private readonly
|
|
25
|
+
private readonly parameters: ParameterStore,
|
|
28
26
|
private readonly state: SessionState,
|
|
29
|
-
private readonly dialogs: DialogRunner
|
|
30
|
-
private readonly guard: LoginGuard,
|
|
27
|
+
private readonly dialogs: DialogRunner,
|
|
31
28
|
) {}
|
|
32
29
|
|
|
33
30
|
/** The accounts of the user parameter data completed with IBAN and BIC, then the accounts only the list names. */
|
|
34
31
|
async completedAccounts(): Promise<Account[]> {
|
|
35
32
|
const sepa = await this.list();
|
|
36
|
-
const accounts = this.
|
|
33
|
+
const accounts = this.parameters.userParameters?.accounts ?? [];
|
|
34
|
+
const listed = new AccountIndex(sepa);
|
|
35
|
+
const known = new AccountIndex(accounts);
|
|
37
36
|
return [
|
|
38
|
-
...accounts.map((account) => withSepaDetails(account,
|
|
39
|
-
...sepa.filter((s) =>
|
|
37
|
+
...accounts.map((account) => withSepaDetails(account, listed.find(account))),
|
|
38
|
+
...sepa.filter((s) => known.find(s) === undefined),
|
|
40
39
|
];
|
|
41
40
|
}
|
|
42
41
|
|
|
43
|
-
/**
|
|
42
|
+
/**
|
|
43
|
+
* `account` as the bank knows it: the one of `completedAccounts` it matches, with what the bank states of it first,
|
|
44
|
+
* so that an account the app has built itself, such as from an IBAN alone, goes out with the account number, the
|
|
45
|
+
* sub-account and the BIC the bank's orders take, and the currency of its statements. Where the bank names none it
|
|
46
|
+
* matches, only what identifies `account`, and its currency: the rest, such as the transactions it permits, only the
|
|
47
|
+
* bank can state.
|
|
48
|
+
*
|
|
49
|
+
* Throws `FinTsUnsupportedError` for such an account where the user parameter data list all permitted transactions:
|
|
50
|
+
* they permit nothing for an account they leave out, and the client cannot tell it from a mistyped one.
|
|
51
|
+
*/
|
|
44
52
|
async complete(account: Account): Promise<Account> {
|
|
45
|
-
|
|
46
|
-
|
|
53
|
+
const known = (await this.completedAccounts()).find((a) => account.matches(a));
|
|
54
|
+
if (known !== undefined) {
|
|
55
|
+
// A sub-account belongs to the account number it comes with.
|
|
56
|
+
const national = known.accountNumber === undefined ? account : known;
|
|
57
|
+
return known.copyWith({
|
|
58
|
+
accountNumber: national.accountNumber,
|
|
59
|
+
subAccount: national.subAccount,
|
|
60
|
+
iban: known.iban ?? account.iban,
|
|
61
|
+
bic: known.bic ?? account.bic,
|
|
62
|
+
currency: known.currency ?? account.currency,
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
if (this.parameters.userParameters?.listsAllTransactions === true) {
|
|
66
|
+
throw new FinTsUnsupportedError("The bank does not name this account among the user's, and so permits nothing");
|
|
67
|
+
}
|
|
68
|
+
return new Account({
|
|
69
|
+
accountNumber: account.accountNumber,
|
|
70
|
+
subAccount: account.subAccount,
|
|
71
|
+
bankCode: account.bankCode,
|
|
72
|
+
countryCode: account.countryCode,
|
|
73
|
+
iban: account.iban,
|
|
74
|
+
bic: account.bic,
|
|
75
|
+
currency: account.currency,
|
|
76
|
+
});
|
|
47
77
|
}
|
|
48
78
|
|
|
49
79
|
/** The list, read in the session's dialog, whose start delivers the user parameter data it is cached for. */
|
|
50
80
|
private async list(): Promise<readonly Account[]> {
|
|
51
81
|
await this.dialogs.open();
|
|
52
|
-
const upd = this.
|
|
82
|
+
const upd = this.parameters.userParameters;
|
|
53
83
|
if (
|
|
54
84
|
upd !== undefined &&
|
|
55
85
|
upd.accounts.length > 0 &&
|
|
@@ -61,26 +91,12 @@ export class SepaAccounts<R extends BankResponse, O> {
|
|
|
61
91
|
const saved = this.state.sepaAccounts(updVersion);
|
|
62
92
|
if (saved !== undefined) return saved;
|
|
63
93
|
if (this.unsaved !== undefined && this.unsaved.updVersion === updVersion) return this.unsaved.accounts;
|
|
64
|
-
const query = this.
|
|
94
|
+
const query = sepaAccountsQuery(this.parameters);
|
|
65
95
|
if (query === undefined) return [];
|
|
66
|
-
// Where the bank rejects the list, the accounts stay as the user parameter data state
|
|
67
|
-
|
|
68
|
-
const accounts = await this.
|
|
69
|
-
|
|
70
|
-
return await this.dialogs.query(() => query);
|
|
71
|
-
} catch (error) {
|
|
72
|
-
if (error instanceof FinTsDialogAbortedError) outcome.aborted = true;
|
|
73
|
-
throw error;
|
|
74
|
-
}
|
|
75
|
-
});
|
|
76
|
-
if (outcome.aborted && !this.abortSkipped) {
|
|
77
|
-
// The bank may have ended the dialog for a reason of its own, such as a timeout, so this call goes on without the
|
|
78
|
-
// list and the next one asks again in its new dialog. Only once: banks also end the dialog when they reject the
|
|
79
|
-
// list, and each time costs the next call a login.
|
|
80
|
-
this.abortSkipped = true;
|
|
81
|
-
return [];
|
|
82
|
-
}
|
|
83
|
-
if (accounts !== undefined && updVersion !== undefined && updVersion > 0) {
|
|
96
|
+
// Where the bank rejects the list, also by ending the dialog, the accounts stay as the user parameter data state
|
|
97
|
+
// them: asked again, a bank that ends the dialog in answer would cost each call a login.
|
|
98
|
+
const accounts = await tryLookUp(() => this.dialogs.query(() => query));
|
|
99
|
+
if (accounts !== undefined && accounts.length > 0 && updVersion !== undefined && updVersion > 0) {
|
|
84
100
|
this.state.cacheSepaAccounts(accounts, updVersion);
|
|
85
101
|
} else {
|
|
86
102
|
this.unsaved = { updVersion, accounts: accounts ?? [] };
|
|
@@ -89,9 +105,11 @@ export class SepaAccounts<R extends BankResponse, O> {
|
|
|
89
105
|
}
|
|
90
106
|
}
|
|
91
107
|
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
108
|
+
/**
|
|
109
|
+
* A copy of `account`, which shares no date with the user parameter data, with the IBAN and BIC of `match`, its entry
|
|
110
|
+
* in the SEPA account list, if any.
|
|
111
|
+
*/
|
|
112
|
+
function withSepaDetails(account: Account, match: Account | undefined): Account {
|
|
95
113
|
// A BIC of the list takes precedence over the one of the user parameter data (Formals E.3.1.1, "BIC").
|
|
96
|
-
return account.copyWith({ iban: account.iban ?? match
|
|
114
|
+
return account.copyWith({ iban: account.iban ?? match?.iban, bic: match?.bic ?? account.bic });
|
|
97
115
|
}
|