@coderoftime/fints 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +142 -1
- package/README.md +127 -37
- package/dist/client.d.ts +48 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +199 -0
- package/dist/client.js.map +1 -0
- package/dist/core/account.d.ts +35 -7
- package/dist/core/account.d.ts.map +1 -1
- package/dist/core/account.js +84 -13
- package/dist/core/account.js.map +1 -1
- package/dist/core/amount.d.ts +2 -1
- package/dist/core/amount.d.ts.map +1 -1
- package/dist/core/amount.js +4 -1
- package/dist/core/amount.js.map +1 -1
- package/dist/core/balance.d.ts +1 -0
- package/dist/core/balance.d.ts.map +1 -1
- package/dist/core/balance.js +6 -5
- package/dist/core/balance.js.map +1 -1
- package/dist/core/bank-info.d.ts +1 -1
- package/dist/core/bank-info.js +1 -1
- package/dist/core/client.d.ts +50 -15
- package/dist/core/client.d.ts.map +1 -1
- package/dist/core/config.d.ts +28 -16
- package/dist/core/config.d.ts.map +1 -1
- package/dist/core/config.js +66 -26
- package/dist/core/config.js.map +1 -1
- package/dist/core/errors.d.ts +85 -23
- package/dist/core/errors.d.ts.map +1 -1
- package/dist/core/errors.js +112 -47
- package/dist/core/errors.js.map +1 -1
- package/dist/core/feedback.d.ts +4 -1
- package/dist/core/feedback.d.ts.map +1 -1
- package/dist/core/feedback.js +4 -1
- package/dist/core/feedback.js.map +1 -1
- package/dist/core/transaction.d.ts +6 -1
- package/dist/core/transaction.d.ts.map +1 -1
- package/dist/core/transaction.js +9 -4
- package/dist/core/transaction.js.map +1 -1
- package/dist/core/transport.d.ts +33 -6
- package/dist/core/transport.d.ts.map +1 -1
- package/dist/core/transport.js +77 -20
- package/dist/core/transport.js.map +1 -1
- package/dist/formats/camt.d.ts +24 -4
- package/dist/formats/camt.d.ts.map +1 -1
- package/dist/formats/camt.js +124 -41
- package/dist/formats/camt.js.map +1 -1
- package/dist/formats/mt940.d.ts +15 -3
- package/dist/formats/mt940.d.ts.map +1 -1
- package/dist/formats/mt940.js +161 -82
- 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 +24 -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 +6 -0
- package/dist/formats/sepa-purpose.d.ts.map +1 -1
- package/dist/formats/sepa-purpose.js +23 -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 +7 -10
- package/dist/session/dialog-runner.d.ts.map +1 -1
- package/dist/session/dialog-runner.js +15 -16
- 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 +156 -84
- 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 +33 -25
- package/dist/session/session-setup.d.ts.map +1 -1
- package/dist/session/session-setup.js +87 -65
- package/dist/session/session-setup.js.map +1 -1
- package/dist/session/session-state.d.ts +9 -6
- package/dist/session/session-state.d.ts.map +1 -1
- package/dist/session/session-state.js +52 -17
- package/dist/session/session-state.js.map +1 -1
- package/dist/session/signed-dialog.d.ts +77 -0
- package/dist/session/signed-dialog.d.ts.map +1 -0
- package/dist/session/signed-dialog.js +170 -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 +8 -4
- package/dist/session/user-prompts.d.ts.map +1 -1
- package/dist/session/user-prompts.js +34 -14
- package/dist/session/user-prompts.js.map +1 -1
- package/dist/tan/challenge.d.ts +34 -13
- package/dist/tan/challenge.d.ts.map +1 -1
- package/dist/tan/challenge.js +25 -26
- package/dist/tan/challenge.js.map +1 -1
- package/dist/tan/flicker.d.ts +7 -1
- package/dist/tan/flicker.d.ts.map +1 -1
- package/dist/tan/flicker.js +40 -6
- package/dist/tan/flicker.js.map +1 -1
- package/dist/tan/tan-image.d.ts +3 -2
- package/dist/tan/tan-image.d.ts.map +1 -1
- package/dist/tan/tan-image.js +7 -5
- package/dist/tan/tan-image.js.map +1 -1
- package/dist/tan/tan-models.d.ts +5 -1
- package/dist/tan/tan-models.d.ts.map +1 -1
- package/dist/tan/tan-models.js +10 -5
- package/dist/tan/tan-models.js.map +1 -1
- package/dist/util/async.d.ts +0 -2
- package/dist/util/async.d.ts.map +1 -1
- package/dist/util/async.js +0 -4
- package/dist/util/async.js.map +1 -1
- package/dist/util/bytes.d.ts +2 -3
- package/dist/util/bytes.d.ts.map +1 -1
- package/dist/util/bytes.js +12 -20
- 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 +9 -16
- package/dist/util/dates.d.ts.map +1 -1
- package/dist/util/dates.js +39 -19
- package/dist/util/dates.js.map +1 -1
- package/dist/util/errors.d.ts +9 -1
- package/dist/util/errors.d.ts.map +1 -1
- package/dist/util/errors.js +12 -3
- package/dist/util/errors.js.map +1 -1
- 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 +21 -4
- package/dist/util/text.d.ts.map +1 -1
- package/dist/util/text.js +42 -13
- package/dist/util/text.js.map +1 -1
- package/dist/util/xml.d.ts +4 -3
- package/dist/util/xml.d.ts.map +1 -1
- package/dist/util/xml.js +37 -26
- 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 +34 -0
- package/dist/v3/bank-connection.d.ts.map +1 -0
- package/dist/v3/bank-connection.js +58 -0
- package/dist/v3/bank-connection.js.map +1 -0
- package/dist/v3/dialog.d.ts +80 -46
- package/dist/v3/dialog.d.ts.map +1 -1
- package/dist/v3/dialog.js +162 -59
- package/dist/v3/dialog.js.map +1 -1
- package/dist/v3/message.d.ts +17 -4
- package/dist/v3/message.d.ts.map +1 -1
- package/dist/v3/message.js +62 -18
- package/dist/v3/message.js.map +1 -1
- package/dist/v3/parameter-store.d.ts +75 -0
- package/dist/v3/parameter-store.d.ts.map +1 -0
- package/dist/v3/parameter-store.js +143 -0
- package/dist/v3/parameter-store.js.map +1 -0
- package/dist/v3/parameters.d.ts +38 -12
- package/dist/v3/parameters.d.ts.map +1 -1
- package/dist/v3/parameters.js +91 -37
- package/dist/v3/parameters.js.map +1 -1
- package/dist/v3/protocol.d.ts +43 -50
- package/dist/v3/protocol.d.ts.map +1 -1
- package/dist/v3/protocol.js +58 -208
- 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 +120 -0
- package/dist/v3/queries.js.map +1 -0
- package/dist/v3/segments.d.ts +29 -12
- package/dist/v3/segments.d.ts.map +1 -1
- package/dist/v3/segments.js +65 -23
- package/dist/v3/segments.js.map +1 -1
- package/dist/v3/syntax.d.ts +65 -14
- package/dist/v3/syntax.d.ts.map +1 -1
- package/dist/v3/syntax.js +167 -101
- package/dist/v3/syntax.js.map +1 -1
- package/package.json +10 -6
- package/src/client.ts +252 -0
- package/src/core/account.ts +86 -13
- package/src/core/amount.ts +3 -1
- package/src/core/balance.ts +6 -5
- package/src/core/bank-info.ts +1 -1
- package/src/core/client.ts +52 -15
- package/src/core/config.ts +69 -28
- package/src/core/errors.ts +122 -48
- package/src/core/feedback.ts +4 -1
- package/src/core/transaction.ts +9 -4
- package/src/core/transport.ts +98 -23
- package/src/formats/camt.ts +136 -39
- package/src/formats/mt940.ts +167 -81
- package/src/formats/readers.ts +24 -31
- package/src/formats/record-budget.ts +18 -0
- package/src/formats/sepa-purpose.ts +21 -2
- package/src/index.ts +4 -2
- package/src/session/app-approval.ts +51 -38
- package/src/session/dialog-runner.ts +31 -28
- package/src/session/login-guard.ts +174 -88
- package/src/session/sepa-accounts.ts +63 -45
- package/src/session/session-setup.ts +90 -71
- package/src/session/session-state.ts +56 -22
- package/src/session/signed-dialog.ts +192 -0
- package/src/session/tan-selection.ts +50 -39
- package/src/session/user-prompts.ts +35 -16
- package/src/tan/challenge.ts +40 -31
- package/src/tan/flicker.ts +44 -6
- package/src/tan/tan-image.ts +7 -5
- package/src/tan/tan-models.ts +10 -5
- package/src/util/async.ts +0 -5
- package/src/util/bytes.ts +13 -19
- package/src/util/call-context.ts +3 -2
- package/src/util/dates.ts +41 -24
- package/src/util/errors.ts +14 -3
- package/src/util/json.ts +16 -0
- package/src/util/text.ts +42 -12
- package/src/util/xml.ts +37 -26
- package/src/v3/account-extension.ts +7 -12
- package/src/v3/bank-connection.ts +67 -0
- package/src/v3/dialog.ts +175 -91
- package/src/v3/message.ts +69 -17
- package/src/v3/parameter-store.ts +160 -0
- package/src/v3/parameters.ts +110 -37
- package/src/v3/protocol.ts +70 -253
- package/src/v3/queries.ts +190 -0
- package/src/v3/segments.ts +65 -22
- package/src/v3/syntax.ts +153 -91
- 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,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
|
}
|
|
@@ -1,10 +1,14 @@
|
|
|
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
|
/**
|
|
@@ -13,15 +17,11 @@ import type { TanSelection } from './tan-selection.js';
|
|
|
13
17
|
*
|
|
14
18
|
* Where the bank refuses a login for a reason the session can take care of, `remedied` does so and logs in again.
|
|
15
19
|
*/
|
|
16
|
-
export class SessionSetup
|
|
17
|
-
/** Whether a synchronization dialog ran, so that a helpful one runs once only. */
|
|
18
|
-
private synchronizationTried = false;
|
|
19
|
-
|
|
20
|
+
export class SessionSetup {
|
|
20
21
|
constructor(
|
|
21
|
-
private readonly
|
|
22
|
-
private readonly
|
|
23
|
-
private readonly tans: TanSelection
|
|
24
|
-
private readonly guard: LoginGuard,
|
|
22
|
+
private readonly session: SessionParts,
|
|
23
|
+
private readonly protocol: FinTs3Protocol,
|
|
24
|
+
private readonly tans: TanSelection,
|
|
25
25
|
) {}
|
|
26
26
|
|
|
27
27
|
/**
|
|
@@ -29,9 +29,9 @@ export class SessionSetup<R extends BankResponse, O> {
|
|
|
29
29
|
* No TAN method or medium is chosen for it.
|
|
30
30
|
*/
|
|
31
31
|
async bankInfo(): Promise<BankInfo> {
|
|
32
|
-
if (this.
|
|
33
|
-
if (this.
|
|
34
|
-
return requireBankParameters(this.
|
|
32
|
+
if (this.session.parameters.bankParameters === undefined) await this.fetchBankParametersAnonymously();
|
|
33
|
+
if (this.session.parameters.bankParameters === undefined) await this.synchronizeIfDue();
|
|
34
|
+
return requireBankParameters(this.session.parameters.bankParameters).bankInfo;
|
|
35
35
|
}
|
|
36
36
|
|
|
37
37
|
/** The TAN methods the bank allows for this user, which the synchronization reports. */
|
|
@@ -40,56 +40,43 @@ export class SessionSetup<R extends BankResponse, O> {
|
|
|
40
40
|
return this.tans.available();
|
|
41
41
|
}
|
|
42
42
|
|
|
43
|
-
/**
|
|
44
|
-
|
|
43
|
+
/**
|
|
44
|
+
* Settles what is still open: the bank parameter data, the synchronization, the TAN method and medium. Returns the TAN
|
|
45
|
+
* media it looked up to choose the medium; `undefined` where it looked up none, or the bank listed none.
|
|
46
|
+
*/
|
|
47
|
+
async prepare(): Promise<TanMedium[] | undefined> {
|
|
45
48
|
await this.loadParameters();
|
|
46
49
|
await this.tans.ensureMethodChosen();
|
|
47
|
-
if (this.tans.mediumToLookUp)
|
|
50
|
+
if (!this.tans.mediumToLookUp) return undefined;
|
|
51
|
+
const media = await this.lookUpTanMedia();
|
|
52
|
+
await this.tans.chooseMedium(media);
|
|
53
|
+
return media;
|
|
48
54
|
}
|
|
49
55
|
|
|
50
56
|
/** A new dialog signed with the TAN method and medium in use, once `prepare` has settled them. */
|
|
51
|
-
async newDialog(): Promise<
|
|
57
|
+
async newDialog(): Promise<SignedDialog> {
|
|
52
58
|
await this.prepare();
|
|
53
|
-
return this.tans.newDialog();
|
|
59
|
+
return new SignedDialog(this.tans.newDialog(), this.session);
|
|
54
60
|
}
|
|
55
61
|
|
|
56
|
-
/**
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
*
|
|
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 |
|
|
64
|
-
*/
|
|
65
|
-
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
|
-
}
|
|
80
|
-
}
|
|
62
|
+
/** Runs `attempt`, which may log in, once more after each refused login `remedy` takes care of. */
|
|
63
|
+
remedied<T>(attempt: () => Promise<T>): Promise<T> {
|
|
64
|
+
return this.session.guard.remedied(attempt, (reason, rejected) => this.remedy(reason, rejected));
|
|
81
65
|
}
|
|
82
66
|
|
|
83
|
-
/**
|
|
67
|
+
/**
|
|
68
|
+
* Takes care of `reason`, given for refusing a login signed with `rejected`, with another TAN method or a new
|
|
69
|
+
* synchronization; returns whether it could.
|
|
70
|
+
*/
|
|
84
71
|
private async remedy(reason: RefusalReason, rejected: TanMethod): Promise<boolean> {
|
|
85
72
|
switch (reason) {
|
|
86
73
|
case 'tanMethodNotAllowed':
|
|
87
74
|
// The parameter data name the methods to choose from; the refusal may be the first to deliver them.
|
|
88
|
-
if (this.
|
|
75
|
+
if (this.session.parameters.bankParameters === undefined) await this.fetchBankParametersAnonymously();
|
|
89
76
|
return this.tans.replace(rejected);
|
|
90
77
|
case 'systemIdRejected':
|
|
91
78
|
// Without a customer system ID there is none to lose, and logging in once more would only send the PIN again.
|
|
92
|
-
return this.
|
|
79
|
+
return this.session.parameters.forgetSystemId();
|
|
93
80
|
}
|
|
94
81
|
}
|
|
95
82
|
|
|
@@ -103,35 +90,66 @@ export class SessionSetup<R extends BankResponse, O> {
|
|
|
103
90
|
* After a refused login it throws the refusal right away, before the user is asked for a TAN method or medium.
|
|
104
91
|
*/
|
|
105
92
|
private async loadParameters(): Promise<void> {
|
|
106
|
-
this.guard.check();
|
|
107
|
-
if (this.
|
|
93
|
+
this.session.guard.check();
|
|
94
|
+
if (this.session.parameters.bankParameters === undefined) await this.fetchBankParametersAnonymously();
|
|
108
95
|
await this.synchronizeIfDue();
|
|
109
96
|
}
|
|
110
97
|
|
|
111
98
|
/**
|
|
112
|
-
* Asks for the bank parameter data without logging in
|
|
113
|
-
*
|
|
99
|
+
* Asks for the bank parameter data without logging in, keeps them where there are none from a dialog signed with the
|
|
100
|
+
* PIN (`ParameterStore.keepAnonymous`), and passes the bank's messages on: all an anonymous dialog tells, since the
|
|
101
|
+
* TAN methods it lists are not those of a user. A bank that does not answer anonymously is no error, but a failed
|
|
102
|
+
* connection is: the synchronization would only wait for the same server again.
|
|
114
103
|
*/
|
|
115
104
|
private async fetchBankParametersAnonymously(): Promise<void> {
|
|
116
105
|
try {
|
|
117
|
-
|
|
106
|
+
const response = await this.requestBankParametersAnonymously();
|
|
107
|
+
this.session.parameters.keepAnonymous(response);
|
|
108
|
+
for (const message of response.bankMessages) this.session.prompts.showBankMessage(message);
|
|
118
109
|
} catch (error) {
|
|
119
110
|
// Optional: the login delivers the parameters as well.
|
|
120
111
|
if (error instanceof FinTsTransportError || !(error instanceof FinTsError)) throw error;
|
|
121
112
|
}
|
|
122
113
|
}
|
|
123
114
|
|
|
124
|
-
/**
|
|
115
|
+
/**
|
|
116
|
+
* Asks for the bank parameter data in an anonymous dialog that announces strong customer authentication; a bank that
|
|
117
|
+
* rejects the announcement, with a return code or by failing on it, is asked again without it.
|
|
118
|
+
*
|
|
119
|
+
* A timeout or a broken connection is not a reason to ask again: it would only double the wait.
|
|
120
|
+
*/
|
|
121
|
+
private async requestBankParametersAnonymously(): Promise<IncomingMessage3> {
|
|
122
|
+
let announced: IncomingMessage3;
|
|
123
|
+
try {
|
|
124
|
+
announced = await this.anonymousDialog(true);
|
|
125
|
+
} catch (error) {
|
|
126
|
+
if (error instanceof FinTsTransportError && error.statusCode === undefined) throw error;
|
|
127
|
+
if (!(error instanceof FinTsError)) throw error;
|
|
128
|
+
return this.anonymousDialog(false);
|
|
129
|
+
}
|
|
130
|
+
if (!hasErrors(announced.feedback) || announced.find('HIBPA') !== undefined) return announced;
|
|
131
|
+
return this.anonymousDialog(false);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
private async anonymousDialog(announceSca: boolean): Promise<IncomingMessage3> {
|
|
135
|
+
const dialog = FinTs3Dialog.anonymous(this.protocol);
|
|
136
|
+
try {
|
|
137
|
+
return await dialog.requestBankParameters(announceSca);
|
|
138
|
+
} finally {
|
|
139
|
+
await dialog.end();
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Runs the synchronization, a login in a dialog of its own, while there is no customer system ID, without which no
|
|
145
|
+
* regular dialog starts. It runs before the TAN method and medium are settled (`TanSelection.synchronizationDialog`
|
|
146
|
+
* tells which method signs it).
|
|
147
|
+
*/
|
|
125
148
|
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') {
|
|
149
|
+
if (this.session.parameters.systemId !== undefined) return;
|
|
150
|
+
await this.once(this.tans.synchronizationDialog(), (dialog) => dialog.login(true));
|
|
151
|
+
// Set by the synchronization, which the compiler cannot see.
|
|
152
|
+
if ((this.session.parameters.systemId as string | undefined) === undefined) {
|
|
135
153
|
throw new FinTsProtocolError('The bank did not complete the synchronization');
|
|
136
154
|
}
|
|
137
155
|
}
|
|
@@ -144,22 +162,23 @@ export class SessionSetup<R extends BankResponse, O> {
|
|
|
144
162
|
* medium the list tells (PIN/TAN B.4.3.1.3).
|
|
145
163
|
*/
|
|
146
164
|
private async lookUpTanMedia(): Promise<TanMedium[] | undefined> {
|
|
147
|
-
const query = this.
|
|
165
|
+
const query = tanMediaQuery(this.session.parameters);
|
|
148
166
|
if (query === undefined) return undefined;
|
|
149
|
-
return
|
|
167
|
+
return tryLookUp(() =>
|
|
150
168
|
this.once(this.tans.lookupDialog(), async (dialog) => {
|
|
151
|
-
const
|
|
152
|
-
return parseBankData(() => query.read(
|
|
169
|
+
const answers = await dialog.loginWith(query.order(undefined));
|
|
170
|
+
return parseBankData(() => query.read(answers));
|
|
153
171
|
}),
|
|
154
172
|
);
|
|
155
173
|
}
|
|
156
174
|
|
|
157
175
|
/** Runs `use` in `dialog`, a dialog of its own, and ends it afterwards, however `use` ends. */
|
|
158
|
-
private async once<T>(dialog:
|
|
176
|
+
private async once<T>(dialog: FinTs3Dialog, use: (dialog: SignedDialog) => Promise<T>): Promise<T> {
|
|
177
|
+
const signed = new SignedDialog(dialog, this.session);
|
|
159
178
|
try {
|
|
160
|
-
return await use(
|
|
179
|
+
return await use(signed);
|
|
161
180
|
} finally {
|
|
162
|
-
await
|
|
181
|
+
await signed.end();
|
|
163
182
|
}
|
|
164
183
|
}
|
|
165
184
|
}
|
|
@@ -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,40 @@ 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 belongs to the user at the bank, so it comes first: it holds
|
|
42
|
+
* for another server or protocol version as well, and where the rest is damaged; a state saved by the Dart package or
|
|
43
|
+
* an earlier version has none.
|
|
37
44
|
*/
|
|
38
|
-
export function restoreSession
|
|
39
|
-
json: string | undefined,
|
|
40
|
-
protocol: PinTanProtocol<R, O>,
|
|
41
|
-
): SessionState {
|
|
45
|
+
export function restoreSession(json: string | undefined, parameters: ParameterStore, guard: LoginGuard): SessionState {
|
|
42
46
|
if (json === undefined) return new SessionState();
|
|
43
47
|
try {
|
|
44
48
|
const saved: unknown = JSON.parse(json);
|
|
45
|
-
if (!isJsonObject(saved) || saved.format !== stateFormat
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
const
|
|
50
|
-
if (
|
|
51
|
-
|
|
52
|
-
}
|
|
49
|
+
if (!isJsonObject(saved) || saved.format !== stateFormat) return new SessionState();
|
|
50
|
+
const [server, ...user] = readOptionalList(saved, 'owner', isString) ?? [];
|
|
51
|
+
const expected = userOf(parameters.config);
|
|
52
|
+
if (user.length !== expected.length || user.some((value, i) => value !== expected[i])) return new SessionState();
|
|
53
|
+
const loginGuard = readOptionalObject(saved, 'loginGuard');
|
|
54
|
+
if (loginGuard !== undefined) guard.restoreState(guardStateFromJson(loginGuard));
|
|
55
|
+
if (server !== hostOf(parameters.config.url) || saved.version !== protocolVersion) return new SessionState();
|
|
53
56
|
const session = SessionState.fromJson(readObject(saved, 'session'));
|
|
54
|
-
|
|
57
|
+
parameters.restoreState(readObject(saved, 'protocol'));
|
|
55
58
|
return session;
|
|
56
59
|
} catch (error) {
|
|
57
60
|
// Damaged JSON, base64 or parameter data, and values of the wrong type, only cost a new synchronization.
|
|
@@ -60,9 +63,14 @@ export function restoreSession<R extends BankResponse, O>(
|
|
|
60
63
|
}
|
|
61
64
|
}
|
|
62
65
|
|
|
63
|
-
/** Whose data a state holds, so that the state of another user or bank is never used
|
|
66
|
+
/** Whose data a state holds, so that the state of another user or bank is never used: the server, then `userOf`. */
|
|
64
67
|
function ownerOf(config: FinTsConfig): string[] {
|
|
65
|
-
return [hostOf(config.url), config
|
|
68
|
+
return [hostOf(config.url), ...userOf(config)];
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** The user at the bank, whom a refused login belongs to. */
|
|
72
|
+
function userOf(config: FinTsConfig): string[] {
|
|
73
|
+
return [config.bankCode, config.userId, config.customerId];
|
|
66
74
|
}
|
|
67
75
|
|
|
68
76
|
/**
|
|
@@ -168,3 +176,29 @@ function accountFromJson(json: JsonObject): Account {
|
|
|
168
176
|
bankCode,
|
|
169
177
|
});
|
|
170
178
|
}
|
|
179
|
+
|
|
180
|
+
function guardStateToJson(state: LoginGuardState): JsonObject {
|
|
181
|
+
return { refusal: state.refusal?.map(feedbackToJson) ?? null, unreadLogins: state.unreadLogins };
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function guardStateFromJson(json: JsonObject): LoginGuardState {
|
|
185
|
+
return {
|
|
186
|
+
refusal: readOptionalList(json, 'refusal', isJsonObject)?.map(feedbackFromJson),
|
|
187
|
+
unreadLogins: readOptionalInt(json, 'unreadLogins') ?? 0,
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function feedbackToJson(f: FinTsFeedback): JsonObject {
|
|
192
|
+
return { code: f.code, reference: f.reference ?? null, text: f.text, parameters: [...f.parameters] };
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function feedbackFromJson(json: JsonObject): FinTsFeedback {
|
|
196
|
+
const code = readOptionalString(json, 'code');
|
|
197
|
+
if (code === undefined) throw new FormatError('Saved state: return code without code');
|
|
198
|
+
return new FinTsFeedback({
|
|
199
|
+
code,
|
|
200
|
+
reference: readOptionalString(json, 'reference'),
|
|
201
|
+
text: readOptionalString(json, 'text') ?? '',
|
|
202
|
+
parameters: readOptionalList(json, 'parameters', isString) ?? [],
|
|
203
|
+
});
|
|
204
|
+
}
|