@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,6 +1,8 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { FinTsTanSetupError } from '../core/errors.js';
|
|
2
2
|
import { TanMethod, type TanMedium } from '../tan/tan-models.js';
|
|
3
|
-
import {
|
|
3
|
+
import { FinTs3Dialog } from '../v3/dialog.js';
|
|
4
|
+
import type { FinTs3Protocol } from '../v3/protocol.js';
|
|
5
|
+
import { noReference } from '../v3/segments.js';
|
|
4
6
|
import type { SessionState } from './session-state.js';
|
|
5
7
|
import type { UserPrompts } from './user-prompts.js';
|
|
6
8
|
|
|
@@ -8,18 +10,18 @@ import type { UserPrompts } from './user-prompts.js';
|
|
|
8
10
|
* The TAN method and medium the session's messages are signed with, chosen by the user among what the bank offers and
|
|
9
11
|
* the user may use, and kept in the session state.
|
|
10
12
|
*/
|
|
11
|
-
export class TanSelection
|
|
12
|
-
/**
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
private
|
|
13
|
+
export class TanSelection {
|
|
14
|
+
/**
|
|
15
|
+
* The lookups of this session that found no medium to choose, by TAN method, with the list each gave; `undefined`
|
|
16
|
+
* where the bank gave none. Such a lookup is not repeated for its method until `select` or `forgetMedium` drops it.
|
|
17
|
+
*/
|
|
18
|
+
private readonly lookupsWithoutMedium = new Map<string, TanMedium[] | undefined>();
|
|
17
19
|
|
|
18
20
|
/** Whether the bank refused a login signed with the one-step method in this session. */
|
|
19
21
|
private oneStepRefused = false;
|
|
20
22
|
|
|
21
23
|
constructor(
|
|
22
|
-
private readonly protocol:
|
|
24
|
+
private readonly protocol: FinTs3Protocol,
|
|
23
25
|
private readonly state: SessionState,
|
|
24
26
|
private readonly prompts: UserPrompts,
|
|
25
27
|
) {}
|
|
@@ -28,31 +30,41 @@ export class TanSelection<R extends BankResponse, O> {
|
|
|
28
30
|
get method(): TanMethod {
|
|
29
31
|
const code = this.state.tanMethod;
|
|
30
32
|
if (code === undefined || code === TanMethod.oneStep.securityFunction) return TanMethod.oneStep;
|
|
31
|
-
|
|
33
|
+
const methods = this.protocol.parameters.bankParameters?.tanMethods ?? [];
|
|
34
|
+
return methods.find((m) => m.securityFunction === code) ?? TanMethod.oneStep;
|
|
32
35
|
}
|
|
33
36
|
|
|
34
37
|
/** Switches to `securityFunction` and `tanMedium` for the dialogs to come. */
|
|
35
38
|
select(securityFunction: string, tanMedium: string | undefined): void {
|
|
36
39
|
this.state.useTanMethod(securityFunction, tanMedium);
|
|
37
|
-
this.
|
|
40
|
+
this.lookupsWithoutMedium.delete(securityFunction);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Forgets the medium in use and the lookups of this session, so that the next login looks the media up anew where the
|
|
45
|
+
* method needs one: the bank may have refused the medium, such as that of a phone the user has replaced.
|
|
46
|
+
*/
|
|
47
|
+
forgetMedium(): void {
|
|
48
|
+
this.state.useTanMedium(undefined);
|
|
49
|
+
this.lookupsWithoutMedium.clear();
|
|
38
50
|
}
|
|
39
51
|
|
|
40
52
|
/**
|
|
41
53
|
* A dialog signed with the TAN method in use and its medium.
|
|
42
54
|
*
|
|
43
|
-
* Throws `
|
|
55
|
+
* Throws `FinTsTanSetupError` if the method needs a medium the user has not chosen: the bank would refuse the
|
|
44
56
|
* login, and the client would not send the PIN again.
|
|
45
57
|
*/
|
|
46
|
-
newDialog():
|
|
58
|
+
newDialog(): FinTs3Dialog {
|
|
47
59
|
const method = this.method;
|
|
48
60
|
if (this.lacksMedium(method)) {
|
|
49
|
-
throw new
|
|
61
|
+
throw new FinTsTanSetupError(
|
|
50
62
|
`The TAN method ${method.name} needs a TAN medium, and the client could not learn one: the bank lists no ` +
|
|
51
63
|
'active medium for it, or does not offer or permit the list of TAN media, or rejected it. Pass a medium to ' +
|
|
52
64
|
'selectTanMethod, or choose another method',
|
|
53
65
|
);
|
|
54
66
|
}
|
|
55
|
-
return this.protocol
|
|
67
|
+
return FinTs3Dialog.signed(this.protocol, method, this.state.tanMedium);
|
|
56
68
|
}
|
|
57
69
|
|
|
58
70
|
/**
|
|
@@ -61,9 +73,9 @@ export class TanSelection<R extends BankResponse, O> {
|
|
|
61
73
|
* It runs before the medium is known, so it names a placeholder where the method needs one; the bank ignores the
|
|
62
74
|
* medium in this dialog (PIN/TAN B.4.3.1.3).
|
|
63
75
|
*/
|
|
64
|
-
lookupDialog():
|
|
76
|
+
lookupDialog(): FinTs3Dialog {
|
|
65
77
|
const method = this.method;
|
|
66
|
-
return this.protocol
|
|
78
|
+
return FinTs3Dialog.signed(this.protocol, method, this.lacksMedium(method) ? noReference : this.state.tanMedium);
|
|
67
79
|
}
|
|
68
80
|
|
|
69
81
|
/**
|
|
@@ -75,15 +87,15 @@ export class TanSelection<R extends BankResponse, O> {
|
|
|
75
87
|
* a one-step login;
|
|
76
88
|
* 3. another two-step method the user may use.
|
|
77
89
|
*
|
|
78
|
-
* Throws `
|
|
90
|
+
* Throws `FinTsTanSetupError` if none does, before the bank could refuse the login.
|
|
79
91
|
*/
|
|
80
|
-
synchronizationDialog():
|
|
92
|
+
synchronizationDialog(): FinTs3Dialog {
|
|
81
93
|
const method = this.method;
|
|
82
|
-
if (!this.lacksMedium(method)) return this.protocol
|
|
83
|
-
if (!this.oneStepRefused) return this.protocol
|
|
94
|
+
if (!this.lacksMedium(method)) return FinTs3Dialog.signed(this.protocol, method, this.state.tanMedium);
|
|
95
|
+
if (!this.oneStepRefused) return FinTs3Dialog.signed(this.protocol, TanMethod.oneStep);
|
|
84
96
|
const other = this.available().find((m) => !m.isOneStep && !this.lacksMedium(m));
|
|
85
|
-
if (other !== undefined) return this.protocol
|
|
86
|
-
throw new
|
|
97
|
+
if (other !== undefined) return FinTs3Dialog.signed(this.protocol, other);
|
|
98
|
+
throw new FinTsTanSetupError(
|
|
87
99
|
`The bank refuses the one-step method, and the TAN method ${method.name} needs a TAN medium, which the client ` +
|
|
88
100
|
'can look up only after the synchronization. Pass the medium to selectTanMethod, or choose another method',
|
|
89
101
|
);
|
|
@@ -98,17 +110,17 @@ export class TanSelection<R extends BankResponse, O> {
|
|
|
98
110
|
* The two-step TAN methods the bank offers, the user may use and this client can run; the one-step method if there
|
|
99
111
|
* are none.
|
|
100
112
|
*
|
|
101
|
-
* Throws `
|
|
113
|
+
* Throws `FinTsTanSetupError` if the bank offers the user nothing but methods the client cannot run.
|
|
102
114
|
*/
|
|
103
115
|
available(): TanMethod[] {
|
|
104
|
-
const bpd = this.protocol.bankParameters;
|
|
116
|
+
const bpd = this.protocol.parameters.bankParameters;
|
|
105
117
|
if (bpd === undefined) return [TanMethod.oneStep];
|
|
106
|
-
const allowed = this.state.allowedMethods
|
|
118
|
+
const allowed = this.state.allowedMethods;
|
|
107
119
|
const isAllowed = (securityFunction: string): boolean =>
|
|
108
120
|
allowed === undefined || allowed.length === 0 || allowed.includes(securityFunction);
|
|
109
121
|
const runnable = bpd.tanMethods.filter((m) => isAllowed(m.securityFunction));
|
|
110
122
|
if (runnable.length === 0 && bpd.unsupportedTanMethods.some(isAllowed)) {
|
|
111
|
-
throw new
|
|
123
|
+
throw new FinTsTanSetupError(
|
|
112
124
|
'The bank offers only TAN methods this client cannot run: process variant 1, or methods that need an SMS ' +
|
|
113
125
|
'charge account or an HHD_UC answer',
|
|
114
126
|
);
|
|
@@ -142,11 +154,11 @@ export class TanSelection<R extends BankResponse, O> {
|
|
|
142
154
|
|
|
143
155
|
/**
|
|
144
156
|
* Whether the media of the method in use have to be looked up: it needs a medium the user has not chosen, and they
|
|
145
|
-
* were not looked up for it in this session.
|
|
157
|
+
* were not looked up in vain for it in this session.
|
|
146
158
|
*/
|
|
147
159
|
get mediumToLookUp(): boolean {
|
|
148
160
|
const method = this.method;
|
|
149
|
-
return this.lacksMedium(method) && !this.
|
|
161
|
+
return this.lacksMedium(method) && !this.lookupsWithoutMedium.has(method.securityFunction);
|
|
150
162
|
}
|
|
151
163
|
|
|
152
164
|
/**
|
|
@@ -155,21 +167,20 @@ export class TanSelection<R extends BankResponse, O> {
|
|
|
155
167
|
*/
|
|
156
168
|
async chooseMedium(media: TanMedium[] | undefined): Promise<void> {
|
|
157
169
|
const method = this.method;
|
|
158
|
-
|
|
159
|
-
this.state.useTanMedium(
|
|
160
|
-
this.
|
|
170
|
+
const medium = media === undefined ? undefined : await this.chooseMediumOf(method, media);
|
|
171
|
+
this.state.useTanMedium(medium);
|
|
172
|
+
if (medium === undefined) this.lookupsWithoutMedium.set(method.securityFunction, media);
|
|
161
173
|
}
|
|
162
174
|
|
|
163
175
|
/**
|
|
164
|
-
* The list of the
|
|
176
|
+
* The list of the lookup that found no medium for the method in use; `undefined` where there was none, or the bank
|
|
177
|
+
* gave no list.
|
|
165
178
|
*
|
|
166
|
-
*
|
|
167
|
-
*
|
|
179
|
+
* It outlasts the call that failed for want of the medium, so that `getTanMedia` can show the user the list that tells
|
|
180
|
+
* why: the client neither looks the media up again for the method nor can list them in a dialog without a medium.
|
|
168
181
|
*/
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
this.lookedUpMedia = undefined;
|
|
172
|
-
return media;
|
|
182
|
+
get lookedUpMedia(): TanMedium[] | undefined {
|
|
183
|
+
return this.lookupsWithoutMedium.get(this.method.securityFunction);
|
|
173
184
|
}
|
|
174
185
|
|
|
175
186
|
/** The name of the medium the user chooses among the active ones in `media`; `undefined` if none is active. */
|
|
@@ -1,15 +1,18 @@
|
|
|
1
1
|
import type { BankMessage } from '../core/bank-info.js';
|
|
2
2
|
import type { FinTsClientOptions, MessageLogger } from '../core/client.js';
|
|
3
|
-
import {
|
|
3
|
+
import { clientClosedError } from '../core/errors.js';
|
|
4
4
|
import type { FinTsFeedback } from '../core/feedback.js';
|
|
5
|
-
import
|
|
5
|
+
import { TanAnswer, type TanChallenge } from '../tan/challenge.js';
|
|
6
6
|
import type { TanMedium, TanMethod } from '../tan/tan-models.js';
|
|
7
|
+
import { optionalFunction, requireFunction } from '../util/arguments.js';
|
|
7
8
|
import { Deferred } from '../util/async.js';
|
|
8
9
|
import { invalidArgument } from '../util/errors.js';
|
|
9
10
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
11
|
+
/** The callbacks of `FinTsClientOptions`. */
|
|
12
|
+
type Callbacks = Pick<
|
|
13
|
+
FinTsClientOptions,
|
|
14
|
+
'tanCallback' | 'tanMethodSelector' | 'tanMediumSelector' | 'onBankMessage' | 'onWarning' | 'logger'
|
|
15
|
+
>;
|
|
13
16
|
|
|
14
17
|
/**
|
|
15
18
|
* Everything a session tells the app, asks of it or waits for: the bank's messages, warnings and log, the TAN callback,
|
|
@@ -24,10 +27,28 @@ export class UserPrompts {
|
|
|
24
27
|
/** The first error an informing callback threw during the running call. */
|
|
25
28
|
private callbackError: { error: unknown } | undefined;
|
|
26
29
|
|
|
27
|
-
/** Aborts the waits under way; each wait removes its abort when it is over. */
|
|
30
|
+
/** Aborts the waits under way; each wait removes its abort when it is over, or when nothing waits for it any more. */
|
|
28
31
|
private readonly aborts = new Set<() => void>();
|
|
29
32
|
|
|
30
|
-
|
|
33
|
+
/** The callbacks as the client was built with them, as in the Dart package, whose options are final. */
|
|
34
|
+
private readonly options: Callbacks;
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Throws `TypeError` for a `tanCallback` that is no function, and for another callback that is neither a function
|
|
38
|
+
* nor `undefined` or `null`, which mean none.
|
|
39
|
+
*/
|
|
40
|
+
constructor(options: FinTsClientOptions) {
|
|
41
|
+
const tanCallback = requireFunction(options.tanCallback, 'tanCallback');
|
|
42
|
+
this.options = {
|
|
43
|
+
// Called on the app's options, which a callback written as a method of them may refer to as `this`.
|
|
44
|
+
tanCallback: (challenge) => tanCallback.call(options, challenge),
|
|
45
|
+
tanMethodSelector: optionalFunction(options.tanMethodSelector, 'tanMethodSelector'),
|
|
46
|
+
tanMediumSelector: optionalFunction(options.tanMediumSelector, 'tanMediumSelector'),
|
|
47
|
+
onBankMessage: optionalFunction(options.onBankMessage, 'onBankMessage'),
|
|
48
|
+
onWarning: optionalFunction(options.onWarning, 'onWarning'),
|
|
49
|
+
logger: optionalFunction(options.logger, 'logger'),
|
|
50
|
+
};
|
|
51
|
+
}
|
|
31
52
|
|
|
32
53
|
/** Ends every wait; the client calls it once. */
|
|
33
54
|
close(): void {
|
|
@@ -38,7 +59,7 @@ export class UserPrompts {
|
|
|
38
59
|
|
|
39
60
|
/** Throws `FinTsStateError` once `close` was called. */
|
|
40
61
|
checkOpen(): void {
|
|
41
|
-
if (this.closed) throw
|
|
62
|
+
if (this.closed) throw clientClosedError();
|
|
42
63
|
}
|
|
43
64
|
|
|
44
65
|
/**
|
|
@@ -93,14 +114,17 @@ export class UserPrompts {
|
|
|
93
114
|
}
|
|
94
115
|
}
|
|
95
116
|
|
|
96
|
-
/**
|
|
117
|
+
/**
|
|
118
|
+
* Shows `challenge` through the TAN callback and returns its answer; rejects with `TypeError` for anything but a
|
|
119
|
+
* `TanAnswer`. Once `TanChallenge.resolved` settles, nothing waits for the answer any more.
|
|
120
|
+
*/
|
|
97
121
|
askTan(challenge: TanChallenge): Promise<TanAnswer> {
|
|
98
|
-
return this.unlessClosed(() => this.options.tanCallback(challenge));
|
|
122
|
+
return this.unlessClosed(async () => tanAnswerOf(await this.options.tanCallback(challenge)), challenge.resolved);
|
|
99
123
|
}
|
|
100
124
|
|
|
101
125
|
/** Waits for `delayMs` milliseconds, or until `interrupt` settles. */
|
|
102
126
|
pause(delayMs: number, interrupt?: Promise<unknown>): Promise<void> {
|
|
103
|
-
if (this.closed) return Promise.reject(
|
|
127
|
+
if (this.closed) return Promise.reject(clientClosedError());
|
|
104
128
|
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
105
129
|
const elapsed = new Promise<void>((resolve) => {
|
|
106
130
|
timer = setTimeout(resolve, Math.max(0, delayMs));
|
|
@@ -122,7 +146,7 @@ export class UserPrompts {
|
|
|
122
146
|
const selector = this.options.tanMethodSelector;
|
|
123
147
|
if (candidates.length === 1 || selector === undefined) return candidates[0]!;
|
|
124
148
|
const chosen = await this.unlessClosed(() => selector(candidates));
|
|
125
|
-
if (chosen === undefined) return candidates[0]!;
|
|
149
|
+
if (chosen === undefined || chosen === null) return candidates[0]!;
|
|
126
150
|
const match = candidates.find((m) => m.securityFunction === chosen.securityFunction);
|
|
127
151
|
if (match === undefined) throw invalidArgument('Returned a TAN method the bank did not offer', 'tanMethodSelector');
|
|
128
152
|
return match;
|
|
@@ -137,23 +161,27 @@ export class UserPrompts {
|
|
|
137
161
|
const selector = this.options.tanMediumSelector;
|
|
138
162
|
if (media.length === 1 || selector === undefined) return media[0]!;
|
|
139
163
|
const chosen = await this.unlessClosed(() => selector(media));
|
|
140
|
-
if (chosen === undefined) return media[0]!;
|
|
164
|
+
if (chosen === undefined || chosen === null) return media[0]!;
|
|
141
165
|
const match = media.find((m) => m.name === chosen.name);
|
|
142
166
|
if (match === undefined) throw invalidArgument('Returned a TAN medium the bank did not list', 'tanMediumSelector');
|
|
143
167
|
return match;
|
|
144
168
|
}
|
|
145
169
|
|
|
146
170
|
/**
|
|
147
|
-
* Starts `wait` and ends it with `FinTsStateError` if `close` is called before it is over.
|
|
171
|
+
* Starts `wait` and ends it with `FinTsStateError` if `close` is called before it is over. Once `unwanted` settles,
|
|
172
|
+
* nothing waits for it any more: it is forgotten then, rather than kept until a callback that never answers does.
|
|
148
173
|
*
|
|
149
174
|
* Once the client is closed, the returned promise rejects rather than this function throwing: a caller that starts
|
|
150
175
|
* several waits at once would otherwise leave the failure of those started first unheard.
|
|
151
176
|
*/
|
|
152
|
-
private unlessClosed<T>(wait: () => T | Promise<T>): Promise<T> {
|
|
153
|
-
if (this.closed) return Promise.reject(
|
|
177
|
+
private unlessClosed<T>(wait: () => T | Promise<T>, unwanted?: Promise<unknown>): Promise<T> {
|
|
178
|
+
if (this.closed) return Promise.reject(clientClosedError());
|
|
154
179
|
const result = new Deferred<T>();
|
|
155
180
|
const abort = (): void => {
|
|
156
|
-
result.reject(
|
|
181
|
+
result.reject(clientClosedError());
|
|
182
|
+
};
|
|
183
|
+
const forget = (): void => {
|
|
184
|
+
this.aborts.delete(abort);
|
|
157
185
|
};
|
|
158
186
|
this.aborts.add(abort);
|
|
159
187
|
// Runs `wait` at once, as far as its first `await`, and turns an error it throws into a rejection.
|
|
@@ -167,8 +195,24 @@ export class UserPrompts {
|
|
|
167
195
|
result.reject(error);
|
|
168
196
|
},
|
|
169
197
|
)
|
|
170
|
-
.finally(
|
|
198
|
+
.finally(forget)
|
|
171
199
|
.catch(() => undefined);
|
|
200
|
+
void unwanted?.then(forget, forget);
|
|
172
201
|
return result.promise;
|
|
173
202
|
}
|
|
174
203
|
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* `answer`, a result of the TAN callback, as a `TanAnswer`; throws `TypeError` for anything else, such as the
|
|
207
|
+
* `undefined` of a callback that returns nothing. The answer is read once, so that what was checked is what counts.
|
|
208
|
+
*/
|
|
209
|
+
function tanAnswerOf(answer: unknown): TanAnswer {
|
|
210
|
+
const { kind, tan } = (typeof answer === 'object' && answer !== null ? answer : {}) as {
|
|
211
|
+
kind?: unknown;
|
|
212
|
+
tan?: unknown;
|
|
213
|
+
};
|
|
214
|
+
if (kind === 'tan' && typeof tan === 'string') return TanAnswer.tan(tan);
|
|
215
|
+
if (kind === 'approved') return TanAnswer.approved;
|
|
216
|
+
if (kind === 'cancelled') return TanAnswer.cancelled;
|
|
217
|
+
throw invalidArgument('Returned no TanAnswer', 'tanCallback');
|
|
218
|
+
}
|
package/src/tan/challenge.ts
CHANGED
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
import { latin1Decode } from '../util/bytes.js';
|
|
2
|
+
import { copyDate } from '../util/dates.js';
|
|
3
|
+
import { frozenWithInspect, inspectCustom } from '../util/inspect.js';
|
|
4
|
+
import { characterReference, trimBlanks } from '../util/text.js';
|
|
2
5
|
import { challengeInstruction, FlickerCode, hasEmbeddedFlickerCode } from './flicker.js';
|
|
3
6
|
import { TanImage } from './tan-image.js';
|
|
4
7
|
import type { TanMedium, TanMethod } from './tan-models.js';
|
|
@@ -11,7 +14,6 @@ export function challengeText(message: string | undefined): string | undefined {
|
|
|
11
14
|
return message === undefined || message === '' || message === noChallenge ? undefined : message;
|
|
12
15
|
}
|
|
13
16
|
|
|
14
|
-
const printableAscii = /^[\x20-\x7E]+$/;
|
|
15
17
|
const lineBreak = /<br\b[^<>]*>/gi;
|
|
16
18
|
const blockTag = /<\/?(?:p|li)\b[^<>]*>/gi;
|
|
17
19
|
|
|
@@ -24,6 +26,9 @@ const formattingTag = /<\/?(?:b|i|u|ul|ol)\b[^<>]*>/gi;
|
|
|
24
26
|
const entityReference = /&(#x?[0-9a-fA-F]+|[a-zA-Z]+);/g;
|
|
25
27
|
const blankLines = /\n{3,}/g;
|
|
26
28
|
|
|
29
|
+
/** Longest challenge `plainMessage` formats: far longer than the specification allows (an..2048). */
|
|
30
|
+
const maxFormattedLength = 65_536;
|
|
31
|
+
|
|
27
32
|
const namedEntities: Record<string, string> = {
|
|
28
33
|
amp: '&',
|
|
29
34
|
lt: '<',
|
|
@@ -41,19 +46,9 @@ const namedEntities: Record<string, string> = {
|
|
|
41
46
|
euro: '€',
|
|
42
47
|
};
|
|
43
48
|
|
|
44
|
-
/**
|
|
45
|
-
* The character `name` refers to; `undefined` for an unknown name and for a number that is not a character, as XML
|
|
46
|
-
* defines them: `NUL`, half a surrogate pair, or beyond Unicode.
|
|
47
|
-
*/
|
|
49
|
+
/** The character `name` refers to; `undefined` for an unknown name and for a number that is not a character. */
|
|
48
50
|
function decodeEntity(name: string): string | undefined {
|
|
49
|
-
|
|
50
|
-
const isHex = name.startsWith('#x') || name.startsWith('#X');
|
|
51
|
-
const digits = name.substring(isHex ? 2 : 1);
|
|
52
|
-
const code = (isHex ? /^[0-9a-fA-F]+$/ : /^[0-9]+$/).test(digits) ? parseInt(digits, isHex ? 16 : 10) : NaN;
|
|
53
|
-
const isCharacter = code > 0 && (code < 0xd800 || code > 0xdfff) && code <= 0x10ffff;
|
|
54
|
-
return isCharacter ? String.fromCodePoint(code) : undefined;
|
|
55
|
-
}
|
|
56
|
-
return Object.hasOwn(namedEntities, name) ? namedEntities[name] : undefined;
|
|
51
|
+
return characterReference(name) ?? (Object.hasOwn(namedEntities, name) ? namedEntities[name] : undefined);
|
|
57
52
|
}
|
|
58
53
|
|
|
59
54
|
/** What `TanChallenge` takes. */
|
|
@@ -76,10 +71,14 @@ export class TanChallenge {
|
|
|
76
71
|
/** The TAN method the challenge is for. */
|
|
77
72
|
readonly method: TanMethod;
|
|
78
73
|
|
|
79
|
-
/** Bank reference of the pending action. */
|
|
74
|
+
/** Bank reference of the pending action, one character per byte the bank sent (ISO 8859-1). */
|
|
80
75
|
readonly reference: string;
|
|
81
76
|
|
|
82
|
-
/**
|
|
77
|
+
/**
|
|
78
|
+
* Challenge text as the bank sent it, including the formatting tags of a structured challenge
|
|
79
|
+
* (`TanMethod.structuredChallenge`); `plainMessage` is the text to show. Show either as text only, never as HTML
|
|
80
|
+
* (`innerHTML`, `v-html`): it is the bank's text, not markup to trust.
|
|
81
|
+
*/
|
|
83
82
|
readonly message: string | undefined;
|
|
84
83
|
|
|
85
84
|
/** Raw challenge data for TAN generators (HHD_UC). */
|
|
@@ -104,13 +103,16 @@ export class TanChallenge {
|
|
|
104
103
|
readonly attempt: number;
|
|
105
104
|
|
|
106
105
|
/**
|
|
107
|
-
* Settles when the client needs no more answer to this challenge:
|
|
108
|
-
* failed, timed out or was cancelled. Set where the client asks the bank
|
|
109
|
-
* challenge (`TanCallback`), `undefined` otherwise. It never rejects.
|
|
106
|
+
* Settles when the client needs no more answer to this challenge: a later prompt for the approval replaces it, or the
|
|
107
|
+
* bank confirmed the approval, or the approval failed, timed out or was cancelled. Set where the client asks the bank
|
|
108
|
+
* by itself while the callback shows the challenge (`TanCallback`), `undefined` otherwise. It never rejects.
|
|
110
109
|
*/
|
|
111
110
|
readonly resolved: Promise<void> | undefined;
|
|
112
111
|
|
|
113
|
-
/**
|
|
112
|
+
/**
|
|
113
|
+
* A challenge as the bank sent it, with a copy of `validUntil`, which no other challenge shares. Use
|
|
114
|
+
* `TanChallenge.create` to have a flicker code or image detected in the data.
|
|
115
|
+
*/
|
|
114
116
|
constructor(init: TanChallengeInit) {
|
|
115
117
|
this.method = init.method;
|
|
116
118
|
this.reference = init.reference;
|
|
@@ -119,7 +121,7 @@ export class TanChallenge {
|
|
|
119
121
|
this.flickerCode = init.flickerCode;
|
|
120
122
|
this.image = init.image;
|
|
121
123
|
this.isDecoupled = init.isDecoupled ?? false;
|
|
122
|
-
this.validUntil = init.validUntil;
|
|
124
|
+
this.validUntil = copyDate(init.validUntil);
|
|
123
125
|
this.tanMediumName = init.tanMediumName;
|
|
124
126
|
this.attempt = init.attempt ?? 1;
|
|
125
127
|
this.resolved = init.resolved;
|
|
@@ -132,12 +134,10 @@ export class TanChallenge {
|
|
|
132
134
|
const data = init.hhdUc !== undefined && init.hhdUc.length > 0 ? init.hhdUc : undefined;
|
|
133
135
|
if (data !== undefined) {
|
|
134
136
|
image = TanImage.tryParse(data);
|
|
135
|
-
if (image === undefined)
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
} else if (init.message !== undefined && hasEmbeddedFlickerCode(init.message)) {
|
|
140
|
-
flicker = FlickerCode.tryParse(init.message);
|
|
137
|
+
if (image === undefined) flicker = FlickerCode.tryParse(latin1Decode(data));
|
|
138
|
+
}
|
|
139
|
+
if (image === undefined && init.message !== undefined && hasEmbeddedFlickerCode(init.message)) {
|
|
140
|
+
flicker ??= FlickerCode.tryParse(init.message);
|
|
141
141
|
}
|
|
142
142
|
return new TanChallenge({
|
|
143
143
|
...init,
|
|
@@ -162,25 +162,43 @@ export class TanChallenge {
|
|
|
162
162
|
* A method with structured challenges (`TanMethod.structuredChallenge`) may use the formatting tags the specification
|
|
163
163
|
* defines. Line breaks, paragraphs and list items become new lines. Bold, italics, underlining and list tags are
|
|
164
164
|
* removed, and the entity references of such markup are resolved. Any other challenge stays as the bank sent it,
|
|
165
|
-
* which is how the specification wants it shown.
|
|
165
|
+
* which is how the specification wants it shown. So does a structured challenge of more than 65,536 characters.
|
|
166
|
+
*
|
|
167
|
+
* Show it as text, such as with `textContent`, or `{{ }}` in Vue, and `white-space: pre-line` for its line breaks;
|
|
168
|
+
* never as HTML (`innerHTML`, `v-html`). It keeps the tags the specification does not define, and `<` becomes
|
|
169
|
+
* `<`, so that as HTML the bank's text could run scripts in the app.
|
|
166
170
|
*/
|
|
167
171
|
get plainMessage(): string | undefined {
|
|
168
172
|
const text = this.message;
|
|
169
173
|
if (text === undefined) return undefined;
|
|
170
174
|
const instruction = challengeInstruction(text);
|
|
171
|
-
if (!this.method.structuredChallenge) return instruction;
|
|
172
|
-
return
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
175
|
+
if (!this.method.structuredChallenge || instruction.length > maxFormattedLength) return instruction;
|
|
176
|
+
return trimBlanks(
|
|
177
|
+
instruction
|
|
178
|
+
.replace(lineBreak, '\n')
|
|
179
|
+
.replace(blockTag, '\n')
|
|
180
|
+
.replace(formattingTag, '')
|
|
181
|
+
.replace(entityReference, (match, name: string) => decodeEntity(name) ?? match)
|
|
182
|
+
.replace(blankLines, '\n\n'),
|
|
183
|
+
);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* What Node.js's `util.inspect` and `console.log` show: the method, the kind of challenge and the attempt; not the
|
|
188
|
+
* text, the reference, the challenge data, the image or the medium, which may name the user, the account or the order.
|
|
189
|
+
*/
|
|
190
|
+
[inspectCustom](): string {
|
|
191
|
+
const kinds = [
|
|
192
|
+
...(this.isDecoupled ? ['decoupled'] : []),
|
|
193
|
+
...(this.flickerCode === undefined ? [] : ['flicker code']),
|
|
194
|
+
...(this.image === undefined ? [] : ['image']),
|
|
195
|
+
];
|
|
196
|
+
return `TanChallenge(${[this.method.toString(), ...kinds, `attempt ${this.attempt}`].join(', ')})`;
|
|
179
197
|
}
|
|
180
198
|
}
|
|
181
199
|
|
|
182
200
|
// Copies of a challenge for the prompts of one approval. Not methods of `TanChallenge`, so that they are not part of its
|
|
183
|
-
// API
|
|
201
|
+
// API. The own fields of a challenge are those of `TanChallengeInit`, so a copy spreads it.
|
|
184
202
|
|
|
185
203
|
/** `challenge` again, counted as the next prompt for the same approval, with the bank's new instructions `message`. */
|
|
186
204
|
export function nextAttempt(challenge: TanChallenge, message?: string): TanChallenge {
|
|
@@ -193,38 +211,34 @@ export function withResolved(challenge: TanChallenge, resolved: Promise<void>):
|
|
|
193
211
|
}
|
|
194
212
|
|
|
195
213
|
function copyChallenge(challenge: TanChallenge, changes: Partial<TanChallengeInit>): TanChallenge {
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
reference: challenge.reference,
|
|
199
|
-
message: challenge.message,
|
|
200
|
-
hhdUc: challenge.hhdUc,
|
|
201
|
-
flickerCode: challenge.flickerCode,
|
|
202
|
-
image: challenge.image,
|
|
203
|
-
isDecoupled: challenge.isDecoupled,
|
|
204
|
-
validUntil: challenge.validUntil,
|
|
205
|
-
tanMediumName: challenge.tanMediumName,
|
|
206
|
-
attempt: challenge.attempt,
|
|
207
|
-
resolved: challenge.resolved,
|
|
208
|
-
...changes,
|
|
209
|
-
});
|
|
214
|
+
const init: TanChallengeInit = challenge;
|
|
215
|
+
return new TanChallenge({ ...init, ...changes });
|
|
210
216
|
}
|
|
211
217
|
|
|
212
218
|
/** The user's answer to a `TanChallenge`: `TanAnswer.tan(…)`, `TanAnswer.approved` or `TanAnswer.cancelled`. */
|
|
213
219
|
export type TanAnswer =
|
|
214
|
-
/**
|
|
220
|
+
/**
|
|
221
|
+
* The TAN the user entered; an empty one cancels a TAN challenge. Blanks around it do not count, and full-width
|
|
222
|
+
* characters, as East Asian input methods type them, count as their ASCII counterparts.
|
|
223
|
+
*/
|
|
215
224
|
| { readonly kind: 'tan'; readonly tan: string }
|
|
216
225
|
/** The user reports the approval in the banking app (`TanChallenge.isDecoupled`); the client then asks the bank. */
|
|
217
226
|
| { readonly kind: 'approved' }
|
|
218
227
|
/** The user cancels: the client ends the bank dialog, and the call fails with `FinTsTanCancelledError`. */
|
|
219
228
|
| { readonly kind: 'cancelled' };
|
|
220
229
|
|
|
221
|
-
|
|
222
|
-
|
|
230
|
+
/** `answer` frozen, which `util.inspect` shows by its kind, never with the TAN. */
|
|
231
|
+
function frozenAnswer(answer: TanAnswer): TanAnswer {
|
|
232
|
+
return frozenWithInspect(answer, (a) => `TanAnswer(${a.kind})`);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
const approvedAnswer = frozenAnswer({ kind: 'approved' });
|
|
236
|
+
const cancelledAnswer = frozenAnswer({ kind: 'cancelled' });
|
|
223
237
|
|
|
224
238
|
/** The answers a `TanCallback` returns. */
|
|
225
239
|
export const TanAnswer = Object.freeze({
|
|
226
|
-
/** The TAN the user entered; an empty one cancels. */
|
|
227
|
-
tan: (tan: string): TanAnswer =>
|
|
240
|
+
/** The TAN the user entered; an empty one cancels a TAN challenge (`TanAnswer`). */
|
|
241
|
+
tan: (tan: string): TanAnswer => frozenAnswer({ kind: 'tan', tan }),
|
|
228
242
|
/** The user reports the approval in the banking app (`TanChallenge.isDecoupled`); the client then asks the bank. */
|
|
229
243
|
approved: approvedAnswer,
|
|
230
244
|
/** The user cancels: the client ends the bank dialog, and the call fails with `FinTsTanCancelledError`. */
|
|
@@ -234,19 +248,28 @@ export const TanAnswer = Object.freeze({
|
|
|
234
248
|
/**
|
|
235
249
|
* Shows the `TanChallenge` to the user and returns the `TanAnswer`: `TanAnswer.tan(…)` for a TAN, `TanAnswer.approved`
|
|
236
250
|
* once the user reports an approval in the banking app (`TanChallenge.isDecoupled`), `TanAnswer.cancelled` to cancel
|
|
237
|
-
* either.
|
|
251
|
+
* either.
|
|
252
|
+
*
|
|
253
|
+
* The call fails with a `TypeError`, without reaching the bank, for anything but a `TanAnswer`, for `approved` to a TAN
|
|
254
|
+
* challenge, for a TAN to an app approval the user can report, and for a TAN that the method rules out: longer than
|
|
255
|
+
* `TanMethod.maxTanLength`, with anything but the digits 0 to 9 where `TanMethod.numericTan`, or that FinTS 3.0 cannot
|
|
256
|
+
* send (ISO 8859-1 only). Where the user cannot report the approval (`TanChallenge.manualConfirmation` is `false`),
|
|
257
|
+
* offer no confirm button: any `TanAnswer` but `cancelled` is ignored.
|
|
238
258
|
*
|
|
239
|
-
*
|
|
240
|
-
*
|
|
241
|
-
*
|
|
242
|
-
*
|
|
259
|
+
* If the bank has not confirmed a reported approval yet, the callback is called again with the next
|
|
260
|
+
* `TanChallenge.attempt` and the bank's latest instructions. Where the client polls the bank by itself, it sets
|
|
261
|
+
* `TanChallenge.resolved`: close the prompt when it settles, the callback's result is no longer waited for then. New
|
|
262
|
+
* instructions of the bank then replace the open prompt with the next attempt, which the callback gets after the
|
|
263
|
+
* handlers of the earlier prompt's `resolved` have run.
|
|
243
264
|
*/
|
|
244
265
|
export type TanCallback = (challenge: TanChallenge) => TanAnswer | Promise<TanAnswer>;
|
|
245
266
|
|
|
246
|
-
/** Picks one of several allowed TAN methods; the first is used when this returns `undefined`. */
|
|
267
|
+
/** Picks one of several allowed TAN methods; the first is used when this returns `undefined` or `null`. */
|
|
247
268
|
export type TanMethodSelector = (
|
|
248
269
|
methods: readonly TanMethod[],
|
|
249
|
-
) => TanMethod | undefined | Promise<TanMethod | undefined>;
|
|
270
|
+
) => TanMethod | null | undefined | Promise<TanMethod | null | undefined>;
|
|
250
271
|
|
|
251
|
-
/** Picks one of several active TAN media; the first is used when this returns `undefined`. */
|
|
252
|
-
export type TanMediumSelector = (
|
|
272
|
+
/** Picks one of several active TAN media; the first is used when this returns `undefined` or `null`. */
|
|
273
|
+
export type TanMediumSelector = (
|
|
274
|
+
media: readonly TanMedium[],
|
|
275
|
+
) => TanMedium | null | undefined | Promise<TanMedium | null | undefined>;
|