@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,10 +1,15 @@
|
|
|
1
1
|
import { FinTsDecoupledTimeoutError, FinTsTanCancelledError } from '../core/errors.js';
|
|
2
|
-
import { FeedbackCodes, hasCode } from '../core/feedback.js';
|
|
2
|
+
import { FeedbackCodes, hasCode, type FinTsFeedback } from '../core/feedback.js';
|
|
3
3
|
import { nextAttempt, withResolved, type TanAnswer, type TanChallenge } from '../tan/challenge.js';
|
|
4
4
|
import type { TanMethod } from '../tan/tan-models.js';
|
|
5
5
|
import { Deferred } from '../util/async.js';
|
|
6
6
|
import { invalidArgument } from '../util/errors.js';
|
|
7
|
-
import type { Approval
|
|
7
|
+
import type { Approval } from '../v3/dialog.js';
|
|
8
|
+
|
|
9
|
+
/** What a wait reads of the bank's answer to a status request: its return codes. */
|
|
10
|
+
interface StatusAnswer {
|
|
11
|
+
readonly feedback: readonly FinTsFeedback[];
|
|
12
|
+
}
|
|
8
13
|
|
|
9
14
|
/** How long to wait for an app approval when the bank does not say. */
|
|
10
15
|
const defaultMaxStatusPolls = 60;
|
|
@@ -17,10 +22,10 @@ const maxPollDelayMs = 60_000;
|
|
|
17
22
|
const maxApprovalWaitMs = 10 * 60_000;
|
|
18
23
|
|
|
19
24
|
/** Shows a challenge and returns the user's answer. */
|
|
20
|
-
|
|
25
|
+
type AskUser = (challenge: TanChallenge) => Promise<TanAnswer>;
|
|
21
26
|
|
|
22
27
|
/** Waits for `delayMs`, or until `interrupt` settles. */
|
|
23
|
-
|
|
28
|
+
type Pause = (delayMs: number, interrupt?: Promise<unknown>) => Promise<void>;
|
|
24
29
|
|
|
25
30
|
/** What an `AppApprovalWait` needs: `ask` shows a challenge, `pause` waits, `receive` takes in each status response. */
|
|
26
31
|
export interface AppApprovalHooks<R> {
|
|
@@ -44,7 +49,7 @@ export interface AppApprovalHooks<R> {
|
|
|
44
49
|
* handles one `Approval`; `run` may be called once. A failure of `ask` or `pause`, such as a closed client, ends the
|
|
45
50
|
* wait.
|
|
46
51
|
*/
|
|
47
|
-
export function appApprovalWait<R extends
|
|
52
|
+
export function appApprovalWait<R extends StatusAnswer>(
|
|
48
53
|
approval: Approval<R>,
|
|
49
54
|
hooks: AppApprovalHooks<R>,
|
|
50
55
|
): AppApprovalWait<R> {
|
|
@@ -53,7 +58,7 @@ export function appApprovalWait<R extends BankResponse>(
|
|
|
53
58
|
return automatic ? new PollingWhileShown(approval, hooks) : new PollingAfterReports(approval, hooks);
|
|
54
59
|
}
|
|
55
60
|
|
|
56
|
-
export abstract class AppApprovalWait<R extends
|
|
61
|
+
export abstract class AppApprovalWait<R extends StatusAnswer> {
|
|
57
62
|
private requests = 0;
|
|
58
63
|
|
|
59
64
|
/** The time the client waited by itself, which counts towards the longest wait. */
|
|
@@ -122,19 +127,20 @@ export abstract class AppApprovalWait<R extends BankResponse> {
|
|
|
122
127
|
}
|
|
123
128
|
|
|
124
129
|
/**
|
|
125
|
-
* The client polls by itself while the callback shows the instructions, and settles `TanChallenge.resolved`
|
|
126
|
-
*
|
|
130
|
+
* The client polls by itself while the callback shows the instructions, and settles the `TanChallenge.resolved` of a
|
|
131
|
+
* prompt when it needs no more answer to it.
|
|
127
132
|
*
|
|
128
133
|
* `run` shows the prompt, waits the bank's first delay, then asks the bank until it confirms, waiting the interval in
|
|
129
|
-
* between.
|
|
134
|
+
* between. A status answer with new instructions replaces the prompt with one that shows them. The callback's answer
|
|
135
|
+
* only records what the user did, and counts while its prompt is shown:
|
|
130
136
|
* - `stopped` settles on a cancel, a TAN or a failing callback; the loop stops before its next request.
|
|
131
137
|
* - `report` settles on a report of the approval, which cuts the current wait short once, though not below a second.
|
|
132
138
|
* The report closed the prompt, so it is shown anew while the bank has not confirmed yet.
|
|
133
139
|
* - A user who cannot report the approval has every answer but `TanAnswer.cancelled` ignored.
|
|
134
140
|
*/
|
|
135
|
-
class PollingWhileShown<R extends
|
|
136
|
-
/** Settles when the
|
|
137
|
-
private
|
|
141
|
+
class PollingWhileShown<R extends StatusAnswer> extends AppApprovalWait<R> {
|
|
142
|
+
/** Settles when the prompt shown now needs no more answer, which its `TanChallenge.resolved` tells the callback. */
|
|
143
|
+
private shown: Deferred<void> | undefined;
|
|
138
144
|
|
|
139
145
|
/** Settles when the user cancels or the callback fails; `stopReason` holds the error that ends the wait. */
|
|
140
146
|
private readonly stopped = new Deferred<void>();
|
|
@@ -154,38 +160,45 @@ class PollingWhileShown<R extends BankResponse> extends AppApprovalWait<R> {
|
|
|
154
160
|
const response = await this.requestStatus();
|
|
155
161
|
if (!isPending(response)) return response;
|
|
156
162
|
this.throwIfStopped();
|
|
157
|
-
//
|
|
158
|
-
|
|
159
|
-
|
|
163
|
+
// A report closed the prompt, but the bank has not confirmed the approval yet; or the bank's instructions changed.
|
|
164
|
+
const reported = this.userConfirms && this.report === undefined;
|
|
165
|
+
const next = this.nextPrompt(challenge, response);
|
|
166
|
+
if (reported || next.message !== challenge.message) this.show((challenge = next));
|
|
160
167
|
await this.sleep(this.pollIntervalMs, minPollIntervalMs);
|
|
161
168
|
}
|
|
162
169
|
} finally {
|
|
163
|
-
this.
|
|
170
|
+
this.shown?.resolve();
|
|
164
171
|
}
|
|
165
172
|
}
|
|
166
173
|
|
|
167
|
-
/** Shows `challenge` without waiting for the answer. */
|
|
174
|
+
/** Shows `challenge` in place of the prompt before, without waiting for the answer. */
|
|
168
175
|
private show(challenge: TanChallenge): void {
|
|
176
|
+
this.shown?.resolve();
|
|
177
|
+
const shown = (this.shown = new Deferred<void>());
|
|
169
178
|
const report = (this.report = this.userConfirms ? new Deferred<void>() : undefined);
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
179
|
+
// Asked a microtask later: the app closes the prompt before on its `resolved` first, and then opens this one.
|
|
180
|
+
void Promise.resolve()
|
|
181
|
+
.then(() => this.hooks.ask(withResolved(challenge, shown.promise)))
|
|
182
|
+
.then(
|
|
183
|
+
(answer) => {
|
|
184
|
+
if (shown.isCompleted) return;
|
|
185
|
+
switch (answer.kind) {
|
|
186
|
+
case 'cancelled':
|
|
187
|
+
this.stop(new FinTsTanCancelledError());
|
|
188
|
+
break;
|
|
189
|
+
case 'approved':
|
|
190
|
+
report?.resolve();
|
|
191
|
+
break;
|
|
192
|
+
case 'tan':
|
|
193
|
+
if (report !== undefined) this.stop(tanForApproval());
|
|
194
|
+
break;
|
|
195
|
+
// The user cannot report the approval: the client waits for the bank.
|
|
196
|
+
}
|
|
197
|
+
},
|
|
198
|
+
(error: unknown) => {
|
|
199
|
+
if (!shown.isCompleted) this.stop(error);
|
|
200
|
+
},
|
|
201
|
+
);
|
|
189
202
|
}
|
|
190
203
|
|
|
191
204
|
private stop(error: unknown): void {
|
|
@@ -219,7 +232,7 @@ class PollingWhileShown<R extends BankResponse> extends AppApprovalWait<R> {
|
|
|
219
232
|
* The client asks the bank after each report of the user; while the approval is pending, the user is asked again with
|
|
220
233
|
* the bank's latest instructions.
|
|
221
234
|
*/
|
|
222
|
-
class PollingAfterReports<R extends
|
|
235
|
+
class PollingAfterReports<R extends StatusAnswer> extends AppApprovalWait<R> {
|
|
223
236
|
async run(): Promise<R> {
|
|
224
237
|
let challenge = this.approval.challenge;
|
|
225
238
|
await this.confirm(challenge);
|
|
@@ -248,7 +261,7 @@ class PollingAfterReports<R extends BankResponse> extends AppApprovalWait<R> {
|
|
|
248
261
|
}
|
|
249
262
|
|
|
250
263
|
/** Whether `response` tells that the approval is still pending (return code 3956). */
|
|
251
|
-
function isPending(response:
|
|
264
|
+
function isPending(response: StatusAnswer): boolean {
|
|
252
265
|
return hasCode(response.feedback, FeedbackCodes.decoupledStillPending);
|
|
253
266
|
}
|
|
254
267
|
|
|
@@ -1,14 +1,19 @@
|
|
|
1
1
|
import { FinTsBankError, FinTsProtocolError, parseBankData } from '../core/errors.js';
|
|
2
2
|
import { continuations, FeedbackCodes, hasCode } from '../core/feedback.js';
|
|
3
|
-
import type {
|
|
4
|
-
import type {
|
|
3
|
+
import type { IncomingMessage3 } from '../v3/message.js';
|
|
4
|
+
import type { Query } from '../v3/queries.js';
|
|
5
|
+
import type { SegmentSpec } from '../v3/syntax.js';
|
|
5
6
|
import type { SessionSetup } from './session-setup.js';
|
|
6
|
-
import type {
|
|
7
|
+
import type { SignedDialog } from './signed-dialog.js';
|
|
7
8
|
|
|
8
9
|
/** Most pages the client fetches for one query, far more than banks send. */
|
|
9
10
|
const maxPages = 1000;
|
|
10
11
|
|
|
11
|
-
/**
|
|
12
|
+
/**
|
|
13
|
+
* Most data one query may hold, summed over its pages as `IncomingMessage3.size` measures it: the transport and the
|
|
14
|
+
* parser bound a single response only. The statements and transactions read from them have a budget of their own
|
|
15
|
+
* (`RecordBudget`).
|
|
16
|
+
*/
|
|
12
17
|
const maxQueryMebibytes = 128;
|
|
13
18
|
|
|
14
19
|
/**
|
|
@@ -18,17 +23,13 @@ const maxQueryMebibytes = 128;
|
|
|
18
23
|
* client does not repeat the call by itself, since the bank's answer cannot tell a dialog that had ended before from an
|
|
19
24
|
* order it refuses along with ending the dialog, and a new login may cost the user another approval.
|
|
20
25
|
*/
|
|
21
|
-
export class DialogRunner
|
|
22
|
-
private dialog:
|
|
26
|
+
export class DialogRunner {
|
|
27
|
+
private dialog: SignedDialog | undefined;
|
|
23
28
|
|
|
24
|
-
constructor(
|
|
25
|
-
private readonly setup: SessionSetup<R, O>,
|
|
26
|
-
private readonly responses: ResponseHandler<R>,
|
|
27
|
-
private readonly prompts: UserPrompts,
|
|
28
|
-
) {}
|
|
29
|
+
constructor(private readonly setup: SessionSetup) {}
|
|
29
30
|
|
|
30
31
|
/** The open dialog, or a new one once the setup is complete and the user has passed strong authentication. */
|
|
31
|
-
async open(): Promise<
|
|
32
|
+
async open(): Promise<SignedDialog> {
|
|
32
33
|
const current = this.dialog;
|
|
33
34
|
if (current?.isActive === true) return current;
|
|
34
35
|
this.dialog = undefined;
|
|
@@ -37,10 +38,10 @@ export class DialogRunner<R extends BankResponse, O> {
|
|
|
37
38
|
return dialog;
|
|
38
39
|
}
|
|
39
40
|
|
|
40
|
-
private async login(): Promise<
|
|
41
|
+
private async login(): Promise<SignedDialog> {
|
|
41
42
|
const dialog = await this.setup.newDialog();
|
|
42
43
|
try {
|
|
43
|
-
await
|
|
44
|
+
await dialog.login();
|
|
44
45
|
return dialog;
|
|
45
46
|
} catch (error) {
|
|
46
47
|
// After a refused login the dialog is discarded already, and ending it sends nothing. After any other failure,
|
|
@@ -62,18 +63,17 @@ export class DialogRunner<R extends BankResponse, O> {
|
|
|
62
63
|
*
|
|
63
64
|
* The query is built once the dialog is open, since building it takes the parameter data.
|
|
64
65
|
*/
|
|
65
|
-
async query<T>(build: () => Query<
|
|
66
|
+
async query<T>(build: () => Query<T>): Promise<T> {
|
|
66
67
|
const dialog = await this.open();
|
|
67
68
|
const query = build();
|
|
68
69
|
const pages = await this.fetchPages(dialog, query.order);
|
|
69
70
|
return parseBankData(() => query.read(pages));
|
|
70
71
|
}
|
|
71
72
|
|
|
72
|
-
/** Sends `order` in `dialog` and completes its approval. */
|
|
73
|
-
private async run(dialog:
|
|
74
|
-
this.prompts.checkOpen();
|
|
73
|
+
/** Sends `order` in `dialog` and completes its approval, which only the `firstPage` of a query may ask for. */
|
|
74
|
+
private async run(dialog: SignedDialog, order: SegmentSpec, firstPage: boolean): Promise<IncomingMessage3> {
|
|
75
75
|
try {
|
|
76
|
-
return await
|
|
76
|
+
return firstPage ? await dialog.send(order) : await dialog.sendFurtherPage(order);
|
|
77
77
|
} catch (error) {
|
|
78
78
|
// The bank rejected the order and the dialog stays usable, or the bank ended the dialog or rejected the PIN and
|
|
79
79
|
// the dialog is discarded already. A cancelled or failed approval, or a broken connection, leaves the dialog in an
|
|
@@ -84,15 +84,23 @@ export class DialogRunner<R extends BankResponse, O> {
|
|
|
84
84
|
}
|
|
85
85
|
|
|
86
86
|
/** Sends the order built by `order` until the bank has delivered all pages (Formals B.6.3, "Aufsetzpunkt"). */
|
|
87
|
-
private async fetchPages(
|
|
88
|
-
|
|
87
|
+
private async fetchPages(
|
|
88
|
+
dialog: SignedDialog,
|
|
89
|
+
order: (continuation: string | undefined) => SegmentSpec,
|
|
90
|
+
): Promise<IncomingMessage3[]> {
|
|
91
|
+
const pages: IncomingMessage3[] = [];
|
|
89
92
|
const seen = new Set<string>();
|
|
90
93
|
let size = 0;
|
|
91
94
|
let continuation: string | undefined;
|
|
92
95
|
for (;;) {
|
|
93
|
-
const page = await this.run(dialog, order(continuation));
|
|
96
|
+
const page = await this.run(dialog, order(continuation), continuation === undefined);
|
|
94
97
|
pages.push(page);
|
|
95
98
|
size += page.size;
|
|
99
|
+
if (size > maxQueryMebibytes * 1024 * 1024) {
|
|
100
|
+
throw new FinTsProtocolError(
|
|
101
|
+
`The bank sends more than ${maxQueryMebibytes} MiB for one query; ask for a shorter period`,
|
|
102
|
+
);
|
|
103
|
+
}
|
|
96
104
|
const positions = [...continuations(page.feedback)];
|
|
97
105
|
// A message carries one order, so the bank names one position at most.
|
|
98
106
|
if (positions.length > 1) throw new FinTsProtocolError('The bank names several positions to continue at');
|
|
@@ -104,16 +112,11 @@ export class DialogRunner<R extends BankResponse, O> {
|
|
|
104
112
|
}
|
|
105
113
|
return pages;
|
|
106
114
|
}
|
|
107
|
-
// A repeated position or endless pages would keep the client busy forever
|
|
115
|
+
// A repeated position or endless pages would keep the client busy forever.
|
|
108
116
|
if (seen.has(continuation) || pages.length === maxPages) {
|
|
109
117
|
throw new FinTsProtocolError('The bank does not stop sending pages');
|
|
110
118
|
}
|
|
111
119
|
seen.add(continuation);
|
|
112
|
-
if (size > maxQueryMebibytes * 1024 * 1024) {
|
|
113
|
-
throw new FinTsProtocolError(
|
|
114
|
-
`The bank sends more than ${maxQueryMebibytes} MiB for one query; ask for a shorter period`,
|
|
115
|
-
);
|
|
116
|
-
}
|
|
117
120
|
}
|
|
118
121
|
}
|
|
119
122
|
}
|
|
@@ -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,197 @@ 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, each reason once per call, so that a call sends at most three
|
|
46
|
+
* logins the bank refuses. A refusal that outlasts the call is final, and the guard forgets the PIN; only a PIN the
|
|
47
|
+
* 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;
|
|
40
57
|
|
|
41
|
-
/**
|
|
42
|
-
private
|
|
58
|
+
/** Logins sent since the bank last took the PIN whose answer the client has not read. */
|
|
59
|
+
private unreadLogins = 0;
|
|
43
60
|
|
|
44
|
-
/**
|
|
61
|
+
/** The refusal of `maxUnreadLogins` unread logins, kept so that `check` throws the same error each time. */
|
|
62
|
+
private unreadLoginsRefusal: FinTsLoginRefusedError | undefined;
|
|
63
|
+
|
|
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 || !(await remedy(reason, rejected))) throw error;
|
|
147
|
+
this.pardonedReasons.add(reason);
|
|
148
|
+
// A closed client keeps the refusal.
|
|
149
|
+
if (this.#pin !== undefined) this.liftRefusal();
|
|
150
|
+
}
|
|
92
151
|
}
|
|
93
152
|
}
|
|
94
153
|
|
|
95
154
|
/**
|
|
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.
|
|
155
|
+
* Signs with `pin` from now on, a PIN the user has entered anew and the protocol can send, and lifts the refusal.
|
|
156
|
+
* Throws the refusal instead where it brings new login data, which end the old ones for good.
|
|
101
157
|
*/
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
error instanceof FinTsLoginDataChangedError
|
|
109
|
-
) {
|
|
110
|
-
return [];
|
|
111
|
-
}
|
|
112
|
-
const feedback = error.feedback;
|
|
113
|
-
const allowed = allowedTanMethodsIn(feedback);
|
|
114
|
-
const reasons: RefusalReason[] = [];
|
|
115
|
-
if (
|
|
116
|
-
(allowed.length > 0 && !allowed.includes(method.securityFunction)) ||
|
|
117
|
-
(method.isOneStep && hasCode(feedback, FeedbackCodes.scaRequired))
|
|
118
|
-
) {
|
|
119
|
-
reasons.push('tanMethodNotAllowed');
|
|
120
|
-
}
|
|
121
|
-
if (hasCode(feedback, FeedbackCodes.newSystemIdRequired)) reasons.push('systemIdRejected');
|
|
122
|
-
return reasons;
|
|
158
|
+
replacePin(pin: string): void {
|
|
159
|
+
if (this.refusalValue instanceof FinTsLoginDataChangedError) throw this.refusalValue;
|
|
160
|
+
this.#pin = pin;
|
|
161
|
+
this.liftRefusal();
|
|
162
|
+
this.unreadLogins = 0;
|
|
163
|
+
this.unreadLoginsRefusal = undefined;
|
|
123
164
|
}
|
|
124
165
|
|
|
125
|
-
/**
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
this.refusal = undefined;
|
|
129
|
-
this.refusedMethodValue = undefined;
|
|
166
|
+
/** Forgets the PIN; the client calls it once it is closed. */
|
|
167
|
+
close(): void {
|
|
168
|
+
this.#pin = undefined;
|
|
130
169
|
}
|
|
131
170
|
|
|
132
|
-
|
|
133
|
-
return
|
|
171
|
+
saveState(): LoginGuardState {
|
|
172
|
+
return { refusal: this.refusalValue?.feedback, unreadLogins: this.unreadLogins };
|
|
134
173
|
}
|
|
174
|
+
|
|
175
|
+
/** Restores what `saveState` returned; a restored refusal is final. */
|
|
176
|
+
restoreState(saved: LoginGuardState): void {
|
|
177
|
+
if (saved.refusal !== undefined) this.refusalValue = loginRefusalFor(saved.refusal);
|
|
178
|
+
this.unreadLogins = Math.max(0, saved.unreadLogins);
|
|
179
|
+
this.forgetPinIfRefused();
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
private liftRefusal(): void {
|
|
183
|
+
this.refusalValue = undefined;
|
|
184
|
+
this.refusedMethod = undefined;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
private forgetPinIfRefused(): void {
|
|
188
|
+
if (this.refusal !== undefined) this.#pin = undefined;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* The reasons `refusal`, of a login signed with `method`, names that the session can take care of: a TAN method the
|
|
194
|
+
* refusal names as not allowed (`refusesMethod`), and 9391. None for a rejected PIN or new login data.
|
|
195
|
+
*/
|
|
196
|
+
function remediableReasons(refusal: FinTsLoginRefusedError, method: TanMethod): RefusalReason[] {
|
|
197
|
+
if (refusal instanceof FinTsPinError || refusal instanceof FinTsLoginDataChangedError) return [];
|
|
198
|
+
const reasons: RefusalReason[] = [];
|
|
199
|
+
if (refusesMethod(refusal.feedback, method)) reasons.push('tanMethodNotAllowed');
|
|
200
|
+
if (hasCode(refusal.feedback, FeedbackCodes.newSystemIdRequired)) reasons.push('systemIdRejected');
|
|
201
|
+
return reasons;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/** Error codes that only say that the message failed, not why: 9050 (the message contains errors) and 9800. */
|
|
205
|
+
const unspecificErrors = new Set(['9050', FeedbackCodes.dialogAborted]);
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Whether `feedback`, which refuses a login signed with `method`, names the method as the reason: a 3920 that lists the
|
|
209
|
+
* methods the user may use without it, or 9075 after the one-step method.
|
|
210
|
+
*
|
|
211
|
+
* Only the return codes of the refusal itself count: TAN methods listed earlier say nothing about this login. Banks
|
|
212
|
+
* list two-step methods only, so a 3920 always leaves the one-step method out, also where the login succeeds. After
|
|
213
|
+
* the one-step method a 3920 counts only where the bank names no other error than an unspecific one: any other, such
|
|
214
|
+
* as "Anmeldung fehlgeschlagen", may be about the PIN.
|
|
215
|
+
*/
|
|
216
|
+
function refusesMethod(feedback: readonly FinTsFeedback[], method: TanMethod): boolean {
|
|
217
|
+
if (method.isOneStep && hasCode(feedback, FeedbackCodes.scaRequired)) return true;
|
|
218
|
+
const allowed = allowedTanMethodsIn(feedback);
|
|
219
|
+
if (allowed.length === 0 || allowed.includes(method.securityFunction)) return false;
|
|
220
|
+
return !method.isOneStep || feedback.every((f) => !f.isError || unspecificErrors.has(f.code));
|
|
135
221
|
}
|