@coderoftime/fints 0.2.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 +377 -0
- package/README.md +31 -23
- package/dist/client.d.ts +3 -2
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +52 -9
- package/dist/client.js.map +1 -1
- package/dist/core/account.d.ts +15 -4
- package/dist/core/account.d.ts.map +1 -1
- package/dist/core/account.js +84 -36
- package/dist/core/account.js.map +1 -1
- package/dist/core/amount.d.ts +13 -3
- package/dist/core/amount.d.ts.map +1 -1
- package/dist/core/amount.js +42 -8
- package/dist/core/amount.js.map +1 -1
- package/dist/core/balance.d.ts +3 -0
- package/dist/core/balance.d.ts.map +1 -1
- package/dist/core/balance.js +5 -0
- package/dist/core/balance.js.map +1 -1
- package/dist/core/bank-info.d.ts +20 -3
- package/dist/core/bank-info.d.ts.map +1 -1
- package/dist/core/bank-info.js +25 -4
- package/dist/core/bank-info.js.map +1 -1
- package/dist/core/client.d.ts +42 -15
- package/dist/core/client.d.ts.map +1 -1
- package/dist/core/config.d.ts +15 -7
- package/dist/core/config.d.ts.map +1 -1
- package/dist/core/config.js +29 -14
- package/dist/core/config.js.map +1 -1
- package/dist/core/errors.d.ts +17 -5
- package/dist/core/errors.d.ts.map +1 -1
- package/dist/core/errors.js +25 -7
- package/dist/core/errors.js.map +1 -1
- package/dist/core/feedback.d.ts +7 -0
- package/dist/core/feedback.d.ts.map +1 -1
- package/dist/core/feedback.js +10 -1
- package/dist/core/feedback.js.map +1 -1
- package/dist/core/transaction.d.ts +5 -0
- package/dist/core/transaction.d.ts.map +1 -1
- package/dist/core/transaction.js +12 -0
- package/dist/core/transaction.js.map +1 -1
- package/dist/core/transport.d.ts +22 -7
- package/dist/core/transport.d.ts.map +1 -1
- package/dist/core/transport.js +51 -11
- package/dist/core/transport.js.map +1 -1
- package/dist/formats/camt.d.ts +7 -4
- package/dist/formats/camt.d.ts.map +1 -1
- package/dist/formats/camt.js +53 -54
- package/dist/formats/camt.js.map +1 -1
- package/dist/formats/mt940.d.ts +18 -6
- package/dist/formats/mt940.d.ts.map +1 -1
- package/dist/formats/mt940.js +83 -66
- package/dist/formats/mt940.js.map +1 -1
- package/dist/formats/readers.d.ts.map +1 -1
- package/dist/formats/readers.js +6 -2
- package/dist/formats/readers.js.map +1 -1
- package/dist/formats/sepa-purpose.d.ts +7 -4
- package/dist/formats/sepa-purpose.d.ts.map +1 -1
- package/dist/formats/sepa-purpose.js +18 -9
- package/dist/formats/sepa-purpose.js.map +1 -1
- package/dist/session/dialog-runner.d.ts +5 -2
- package/dist/session/dialog-runner.d.ts.map +1 -1
- package/dist/session/dialog-runner.js +23 -7
- package/dist/session/dialog-runner.js.map +1 -1
- package/dist/session/login-guard.d.ts +4 -4
- package/dist/session/login-guard.d.ts.map +1 -1
- package/dist/session/login-guard.js +29 -14
- package/dist/session/login-guard.js.map +1 -1
- package/dist/session/session-setup.d.ts +14 -5
- package/dist/session/session-setup.d.ts.map +1 -1
- package/dist/session/session-setup.js +30 -9
- package/dist/session/session-setup.js.map +1 -1
- package/dist/session/session-state.d.ts +4 -3
- package/dist/session/session-state.d.ts.map +1 -1
- package/dist/session/session-state.js +28 -9
- package/dist/session/session-state.js.map +1 -1
- package/dist/session/signed-dialog.d.ts +19 -13
- package/dist/session/signed-dialog.d.ts.map +1 -1
- package/dist/session/signed-dialog.js +45 -30
- package/dist/session/signed-dialog.js.map +1 -1
- package/dist/session/user-prompts.d.ts +6 -1
- package/dist/session/user-prompts.d.ts.map +1 -1
- package/dist/session/user-prompts.js +19 -4
- package/dist/session/user-prompts.js.map +1 -1
- package/dist/tan/challenge.d.ts +15 -13
- package/dist/tan/challenge.d.ts.map +1 -1
- package/dist/tan/challenge.js +32 -26
- package/dist/tan/challenge.js.map +1 -1
- package/dist/tan/flicker.d.ts +5 -1
- package/dist/tan/flicker.d.ts.map +1 -1
- package/dist/tan/flicker.js +42 -30
- package/dist/tan/flicker.js.map +1 -1
- package/dist/tan/tan-image.d.ts +4 -1
- package/dist/tan/tan-image.d.ts.map +1 -1
- package/dist/tan/tan-image.js +8 -2
- package/dist/tan/tan-image.js.map +1 -1
- package/dist/tan/tan-models.d.ts +9 -1
- package/dist/tan/tan-models.d.ts.map +1 -1
- package/dist/tan/tan-models.js +14 -2
- 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 -1
- package/dist/util/async.d.ts.map +1 -1
- package/dist/util/async.js +6 -3
- package/dist/util/async.js.map +1 -1
- package/dist/util/bytes.d.ts +1 -1
- package/dist/util/bytes.d.ts.map +1 -1
- package/dist/util/bytes.js +3 -3
- package/dist/util/bytes.js.map +1 -1
- package/dist/util/dates.d.ts +3 -10
- package/dist/util/dates.d.ts.map +1 -1
- package/dist/util/dates.js +49 -39
- package/dist/util/dates.js.map +1 -1
- package/dist/util/errors.d.ts +5 -3
- package/dist/util/errors.d.ts.map +1 -1
- package/dist/util/errors.js +8 -4
- 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/text.d.ts +18 -2
- package/dist/util/text.d.ts.map +1 -1
- package/dist/util/text.js +50 -5
- package/dist/util/text.js.map +1 -1
- package/dist/util/xml.d.ts +6 -4
- package/dist/util/xml.d.ts.map +1 -1
- package/dist/util/xml.js +106 -44
- package/dist/util/xml.js.map +1 -1
- package/dist/v3/bank-connection.d.ts +23 -3
- package/dist/v3/bank-connection.d.ts.map +1 -1
- package/dist/v3/bank-connection.js +66 -6
- package/dist/v3/bank-connection.js.map +1 -1
- package/dist/v3/dialog.d.ts +6 -2
- package/dist/v3/dialog.d.ts.map +1 -1
- package/dist/v3/dialog.js +20 -9
- package/dist/v3/dialog.js.map +1 -1
- package/dist/v3/message.d.ts +18 -10
- package/dist/v3/message.d.ts.map +1 -1
- package/dist/v3/message.js +92 -75
- package/dist/v3/message.js.map +1 -1
- package/dist/v3/parameter-store.d.ts +43 -7
- package/dist/v3/parameter-store.d.ts.map +1 -1
- package/dist/v3/parameter-store.js +69 -15
- package/dist/v3/parameter-store.js.map +1 -1
- package/dist/v3/parameters.d.ts +12 -11
- package/dist/v3/parameters.d.ts.map +1 -1
- package/dist/v3/parameters.js +57 -35
- package/dist/v3/parameters.js.map +1 -1
- package/dist/v3/protocol.d.ts +16 -9
- package/dist/v3/protocol.d.ts.map +1 -1
- package/dist/v3/protocol.js +57 -26
- package/dist/v3/protocol.js.map +1 -1
- package/dist/v3/queries.d.ts +2 -2
- package/dist/v3/queries.d.ts.map +1 -1
- package/dist/v3/queries.js +24 -16
- package/dist/v3/queries.js.map +1 -1
- package/dist/v3/segments.d.ts +18 -11
- package/dist/v3/segments.d.ts.map +1 -1
- package/dist/v3/segments.js +30 -21
- package/dist/v3/segments.js.map +1 -1
- package/dist/v3/syntax.d.ts +15 -11
- package/dist/v3/syntax.d.ts.map +1 -1
- package/dist/v3/syntax.js +30 -15
- package/dist/v3/syntax.js.map +1 -1
- package/package.json +1 -1
- package/src/client.ts +58 -17
- package/src/core/account.ts +91 -35
- package/src/core/amount.ts +42 -9
- package/src/core/balance.ts +6 -0
- package/src/core/bank-info.ts +28 -4
- package/src/core/client.ts +42 -15
- package/src/core/config.ts +37 -18
- package/src/core/errors.ts +27 -7
- package/src/core/feedback.ts +12 -1
- package/src/core/transaction.ts +13 -0
- package/src/core/transport.ts +62 -16
- package/src/formats/camt.ts +62 -51
- package/src/formats/mt940.ts +114 -78
- package/src/formats/readers.ts +6 -2
- package/src/formats/sepa-purpose.ts +17 -9
- package/src/session/dialog-runner.ts +34 -7
- package/src/session/login-guard.ts +29 -13
- package/src/session/session-setup.ts +27 -9
- package/src/session/session-state.ts +25 -8
- package/src/session/signed-dialog.ts +60 -30
- package/src/session/user-prompts.ts +28 -3
- package/src/tan/challenge.ts +50 -36
- package/src/tan/flicker.ts +40 -29
- package/src/tan/tan-image.ts +9 -2
- package/src/tan/tan-models.ts +16 -2
- package/src/util/arguments.ts +40 -0
- package/src/util/async.ts +10 -4
- package/src/util/bytes.ts +3 -3
- package/src/util/dates.ts +52 -38
- package/src/util/errors.ts +9 -4
- package/src/util/immutable-map.ts +15 -12
- package/src/util/inspect.ts +14 -0
- package/src/util/text.ts +47 -5
- package/src/util/xml.ts +105 -38
- package/src/v3/bank-connection.ts +65 -6
- package/src/v3/dialog.ts +18 -8
- package/src/v3/message.ts +81 -72
- package/src/v3/parameter-store.ts +82 -16
- package/src/v3/parameters.ts +68 -41
- package/src/v3/protocol.ts +61 -28
- package/src/v3/queries.ts +26 -18
- package/src/v3/segments.ts +32 -20
- package/src/v3/syntax.ts +31 -14
|
@@ -9,6 +9,13 @@ import type { SignedDialog } from './signed-dialog.js';
|
|
|
9
9
|
/** Most pages the client fetches for one query, far more than banks send. */
|
|
10
10
|
const maxPages = 1000;
|
|
11
11
|
|
|
12
|
+
/**
|
|
13
|
+
* Most further pages of one query that may ask for an approval, as its first page may: enough for a bank that asks anew
|
|
14
|
+
* for each page of a query of four pages, while the user approves four times per query at most, and loses the pages
|
|
15
|
+
* approved so far with any approval they cancel.
|
|
16
|
+
*/
|
|
17
|
+
const maxPageApprovals = 3;
|
|
18
|
+
|
|
12
19
|
/**
|
|
13
20
|
* Most data one query may hold, summed over its pages as `IncomingMessage3.size` measures it: the transport and the
|
|
14
21
|
* parser bound a single response only. The statements and transactions read from them have a budget of their own
|
|
@@ -70,20 +77,27 @@ export class DialogRunner {
|
|
|
70
77
|
return parseBankData(() => query.read(pages));
|
|
71
78
|
}
|
|
72
79
|
|
|
73
|
-
/** Sends `order` in `dialog` and completes its approval, which
|
|
74
|
-
private async run(
|
|
80
|
+
/** Sends `order` in `dialog` and completes its approval, which `admitApproval` may refuse (`SignedDialog.send`). */
|
|
81
|
+
private async run(
|
|
82
|
+
dialog: SignedDialog,
|
|
83
|
+
order: SegmentSpec,
|
|
84
|
+
admitApproval: (() => void) | undefined,
|
|
85
|
+
): Promise<IncomingMessage3> {
|
|
75
86
|
try {
|
|
76
|
-
return
|
|
87
|
+
return await dialog.send(order, admitApproval);
|
|
77
88
|
} catch (error) {
|
|
78
89
|
// The bank rejected the order and the dialog stays usable, or the bank ended the dialog or rejected the PIN and
|
|
79
|
-
// the dialog is discarded already. A cancelled or
|
|
80
|
-
// unknown state.
|
|
90
|
+
// the dialog is discarded already. A cancelled, failed or refused approval, or a broken connection, leaves the
|
|
91
|
+
// dialog in an unknown state.
|
|
81
92
|
if (!(error instanceof FinTsBankError)) await this.end();
|
|
82
93
|
throw error;
|
|
83
94
|
}
|
|
84
95
|
}
|
|
85
96
|
|
|
86
|
-
/**
|
|
97
|
+
/**
|
|
98
|
+
* Sends the order built by `order` until the bank has delivered all pages (Formals B.6.3, "Aufsetzpunkt"), and
|
|
99
|
+
* completes the approval each asks for, that of `maxPageApprovals` further pages at most.
|
|
100
|
+
*/
|
|
87
101
|
private async fetchPages(
|
|
88
102
|
dialog: SignedDialog,
|
|
89
103
|
order: (continuation: string | undefined) => SegmentSpec,
|
|
@@ -91,9 +105,22 @@ export class DialogRunner {
|
|
|
91
105
|
const pages: IncomingMessage3[] = [];
|
|
92
106
|
const seen = new Set<string>();
|
|
93
107
|
let size = 0;
|
|
108
|
+
let pageApprovals = 0;
|
|
109
|
+
const admitPageApproval = (): void => {
|
|
110
|
+
if (pageApprovals === maxPageApprovals) {
|
|
111
|
+
throw new FinTsProtocolError(
|
|
112
|
+
`The bank asks for an approval of more than ${maxPageApprovals} further pages of one query; ask for a shorter period`,
|
|
113
|
+
);
|
|
114
|
+
}
|
|
115
|
+
pageApprovals++;
|
|
116
|
+
};
|
|
94
117
|
let continuation: string | undefined;
|
|
95
118
|
for (;;) {
|
|
96
|
-
const page = await this.run(
|
|
119
|
+
const page = await this.run(
|
|
120
|
+
dialog,
|
|
121
|
+
order(continuation),
|
|
122
|
+
continuation === undefined ? undefined : admitPageApproval,
|
|
123
|
+
);
|
|
97
124
|
pages.push(page);
|
|
98
125
|
size += page.size;
|
|
99
126
|
if (size > maxQueryMebibytes * 1024 * 1024) {
|
|
@@ -42,9 +42,9 @@ const maxUnreadLogins = 2;
|
|
|
42
42
|
* access. `FinTsLoginRefusedError` tells which logins count as refused; `judge` decides it for each answer.
|
|
43
43
|
*
|
|
44
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
|
|
46
|
-
* logins the bank refuses. A refusal that outlasts the call is final, and the guard forgets
|
|
47
|
-
* user enters anew lifts it (`replacePin`), unless it brings new login data.
|
|
45
|
+
* the session has taken care of a reason it names, or has failed the call trying, each reason once per call, so that a
|
|
46
|
+
* call sends at most three logins the bank refuses. A refusal that outlasts the call is final, and the guard forgets
|
|
47
|
+
* the PIN; only a PIN the user enters anew lifts it (`replacePin`), unless it brings new login data.
|
|
48
48
|
*/
|
|
49
49
|
export class LoginGuard implements PinGuard {
|
|
50
50
|
// ECMAScript private, so that neither `util.inspect` nor `JSON.stringify` of the client reaches it.
|
|
@@ -143,10 +143,17 @@ export class LoginGuard implements PinGuard {
|
|
|
143
143
|
// Only the bank's refusal the guard holds names reasons; any other error, a second refusal too, fails the call.
|
|
144
144
|
if (refusal === undefined || error !== refusal || rejected === undefined) throw error;
|
|
145
145
|
const reason = remediableReasons(refusal, rejected).find((r) => !this.pardonedReasons.has(r));
|
|
146
|
-
if (reason === undefined
|
|
146
|
+
if (reason === undefined) throw error;
|
|
147
147
|
this.pardonedReasons.add(reason);
|
|
148
|
-
//
|
|
149
|
-
|
|
148
|
+
// The refusal named a reason the session takes care of, not the PIN, so it does not outlast a remedy that fails,
|
|
149
|
+
// such as a TAN method prompt the app ends: the call fails with the remedy's own error. On a closed client the
|
|
150
|
+
// next attempt fails before it sends anything.
|
|
151
|
+
const remedied = await remedy(reason, rejected).catch((remedyError: unknown) => {
|
|
152
|
+
this.liftRefusal();
|
|
153
|
+
throw remedyError;
|
|
154
|
+
});
|
|
155
|
+
if (!remedied) throw error;
|
|
156
|
+
this.liftRefusal();
|
|
150
157
|
}
|
|
151
158
|
}
|
|
152
159
|
}
|
|
@@ -172,9 +179,14 @@ export class LoginGuard implements PinGuard {
|
|
|
172
179
|
return { refusal: this.refusalValue?.feedback, unreadLogins: this.unreadLogins };
|
|
173
180
|
}
|
|
174
181
|
|
|
175
|
-
/** Restores what `saveState` returned; a restored refusal is final. */
|
|
182
|
+
/** Restores what `saveState` returned; a restored refusal is final, also one without return codes. */
|
|
176
183
|
restoreState(saved: LoginGuardState): void {
|
|
177
|
-
if (saved.refusal !== undefined)
|
|
184
|
+
if (saved.refusal !== undefined) {
|
|
185
|
+
this.refusalValue =
|
|
186
|
+
saved.refusal.length > 0
|
|
187
|
+
? loginRefusalFor(saved.refusal)
|
|
188
|
+
: new FinTsLoginRefusedError([], 'A login was refused, and the saved state no longer tells why');
|
|
189
|
+
}
|
|
178
190
|
this.unreadLogins = Math.max(0, saved.unreadLogins);
|
|
179
191
|
this.forgetPinIfRefused();
|
|
180
192
|
}
|
|
@@ -201,8 +213,11 @@ function remediableReasons(refusal: FinTsLoginRefusedError, method: TanMethod):
|
|
|
201
213
|
return reasons;
|
|
202
214
|
}
|
|
203
215
|
|
|
204
|
-
/**
|
|
205
|
-
|
|
216
|
+
/**
|
|
217
|
+
* Error codes that cannot be about the PIN: 9050 (the message contains errors) and 9800, which only say that the
|
|
218
|
+
* message failed, and 9955 (the one-step method is not allowed).
|
|
219
|
+
*/
|
|
220
|
+
const errorsNotAboutThePin = new Set(['9050', FeedbackCodes.dialogAborted, '9955']);
|
|
206
221
|
|
|
207
222
|
/**
|
|
208
223
|
* Whether `feedback`, which refuses a login signed with `method`, names the method as the reason: a 3920 that lists the
|
|
@@ -210,12 +225,13 @@ const unspecificErrors = new Set(['9050', FeedbackCodes.dialogAborted]);
|
|
|
210
225
|
*
|
|
211
226
|
* Only the return codes of the refusal itself count: TAN methods listed earlier say nothing about this login. Banks
|
|
212
227
|
* 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
|
|
214
|
-
* as "Anmeldung fehlgeschlagen", may be about
|
|
228
|
+
* the one-step method a 3920 counts only where the bank names no other error than one not about the PIN: any other,
|
|
229
|
+
* such as "Anmeldung fehlgeschlagen", may be about it. PIN/TAN B.8.2 has the bank answer a one-step dialog of a user
|
|
230
|
+
* bound to two-step methods with 9955 and 3920.
|
|
215
231
|
*/
|
|
216
232
|
function refusesMethod(feedback: readonly FinTsFeedback[], method: TanMethod): boolean {
|
|
217
233
|
if (method.isOneStep && hasCode(feedback, FeedbackCodes.scaRequired)) return true;
|
|
218
234
|
const allowed = allowedTanMethodsIn(feedback);
|
|
219
235
|
if (allowed.length === 0 || allowed.includes(method.securityFunction)) return false;
|
|
220
|
-
return !method.isOneStep || feedback.every((f) => !f.isError ||
|
|
236
|
+
return !method.isOneStep || feedback.every((f) => !f.isError || errorsNotAboutThePin.has(f.code));
|
|
221
237
|
}
|
|
@@ -15,7 +15,8 @@ import type { TanSelection } from './tan-selection.js';
|
|
|
15
15
|
* What has to be settled before a login: the bank parameter data, the synchronization, and through `TanSelection` the
|
|
16
16
|
* TAN method and medium. Each of the dialogs this takes serves one purpose only.
|
|
17
17
|
*
|
|
18
|
-
* Where the bank refuses a login for a reason the session can take care of, `remedied` does so and logs in again
|
|
18
|
+
* Where the bank refuses a login for a reason the session can take care of, `remedied` does so and logs in again; so it
|
|
19
|
+
* does where the limits of bank parameter data that may be outdated keep a login from going out.
|
|
19
20
|
*/
|
|
20
21
|
export class SessionSetup {
|
|
21
22
|
constructor(
|
|
@@ -59,9 +60,24 @@ export class SessionSetup {
|
|
|
59
60
|
return new SignedDialog(this.tans.newDialog(), this.session);
|
|
60
61
|
}
|
|
61
62
|
|
|
62
|
-
/**
|
|
63
|
-
|
|
64
|
-
|
|
63
|
+
/**
|
|
64
|
+
* Runs `attempt`, which may log in, once more after each refused login `remedy` takes care of.
|
|
65
|
+
*
|
|
66
|
+
* Where the limits of the bank parameter data kept a login from going out (`FinTs3Protocol.stoppedLogin`), it fetches
|
|
67
|
+
* them anew in an anonymous dialog, once, and runs `attempt` once more: they may be outdated, as saved ones are, and
|
|
68
|
+
* new ones would come only in answer to the login they rule out. The new limits judge the login built anew, not the
|
|
69
|
+
* one stopped: it reports version 0 for the parameter data they stand in for, so it may be shorter.
|
|
70
|
+
*/
|
|
71
|
+
async remedied<T>(attempt: () => Promise<T>): Promise<T> {
|
|
72
|
+
const run = (): Promise<T> =>
|
|
73
|
+
this.session.guard.remedied(attempt, (reason, rejected) => this.remedy(reason, rejected));
|
|
74
|
+
try {
|
|
75
|
+
return await run();
|
|
76
|
+
} catch (error) {
|
|
77
|
+
if (!this.protocol.stoppedLogin(error)) throw error;
|
|
78
|
+
await this.fetchBankParametersAnonymously(true);
|
|
79
|
+
return run();
|
|
80
|
+
}
|
|
65
81
|
}
|
|
66
82
|
|
|
67
83
|
/**
|
|
@@ -97,14 +113,16 @@ export class SessionSetup {
|
|
|
97
113
|
|
|
98
114
|
/**
|
|
99
115
|
* 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`),
|
|
101
|
-
*
|
|
102
|
-
*
|
|
116
|
+
* PIN (`ParameterStore.keepAnonymous`), or with `refresh` their limits only (`ParameterStore.keepRefreshedLimits`),
|
|
117
|
+
* and passes the bank's messages on: all an anonymous dialog tells, since the TAN methods it lists are not those of a
|
|
118
|
+
* user. A bank that does not answer anonymously is no error, but a failed connection is: the synchronization would
|
|
119
|
+
* only wait for the same server again.
|
|
103
120
|
*/
|
|
104
|
-
private async fetchBankParametersAnonymously(): Promise<void> {
|
|
121
|
+
private async fetchBankParametersAnonymously(refresh = false): Promise<void> {
|
|
105
122
|
try {
|
|
106
123
|
const response = await this.requestBankParametersAnonymously();
|
|
107
|
-
this.session.parameters.
|
|
124
|
+
if (refresh) this.session.parameters.keepRefreshedLimits(response);
|
|
125
|
+
else this.session.parameters.keepAnonymous(response);
|
|
108
126
|
for (const message of response.bankMessages) this.session.prompts.showBankMessage(message);
|
|
109
127
|
} catch (error) {
|
|
110
128
|
// Optional: the login delivers the parameters as well.
|
|
@@ -38,23 +38,27 @@ export function saveSession(parameters: ParameterStore, guard: LoginGuard, sessi
|
|
|
38
38
|
* The session saved in `json`, and the parameter data and the guard's refused login restored from it.
|
|
39
39
|
*
|
|
40
40
|
* A missing or damaged state, or one saved for another user, bank, server or protocol version, gives a new session,
|
|
41
|
-
* which only costs a new synchronization. The refused login
|
|
42
|
-
* for another server or protocol version as well, and where the rest is
|
|
43
|
-
*
|
|
41
|
+
* which only costs a new synchronization. The refused login and the count of logins whose answer was lost belong to the
|
|
42
|
+
* user at the bank, so they come first: they hold for another server or protocol version as well, and where the rest is
|
|
43
|
+
* damaged; a state saved by fints-ts 0.1 or the Dart package 0.2.0 has neither. A damaged section of their own fails
|
|
44
|
+
* closed: it restores a refusal without return codes, since it may have held one.
|
|
44
45
|
*/
|
|
45
46
|
export function restoreSession(json: string | undefined, parameters: ParameterStore, guard: LoginGuard): SessionState {
|
|
46
47
|
if (json === undefined) return new SessionState();
|
|
47
48
|
try {
|
|
48
49
|
const saved: unknown = JSON.parse(json);
|
|
49
50
|
if (!isJsonObject(saved) || saved.format !== stateFormat) return new SessionState();
|
|
50
|
-
|
|
51
|
+
// Entries of another type differ like any other value, so that a damaged server entry keeps the refused login.
|
|
52
|
+
const owner: unknown = saved.owner ?? [];
|
|
53
|
+
if (!Array.isArray(owner)) return new SessionState();
|
|
54
|
+
const [server, ...user] = owner as unknown[];
|
|
51
55
|
const expected = userOf(parameters.config);
|
|
52
56
|
if (user.length !== expected.length || user.some((value, i) => value !== expected[i])) return new SessionState();
|
|
53
|
-
const
|
|
54
|
-
if (
|
|
57
|
+
const guardState = savedGuardState(saved);
|
|
58
|
+
if (guardState !== undefined) guard.restoreState(guardState);
|
|
55
59
|
if (server !== hostOf(parameters.config.url) || saved.version !== protocolVersion) return new SessionState();
|
|
56
60
|
const session = SessionState.fromJson(readObject(saved, 'session'));
|
|
57
|
-
parameters.restoreState(readObject(saved, 'protocol'));
|
|
61
|
+
parameters.restoreState(readObject(saved, 'protocol'), session.tanMedium);
|
|
58
62
|
return session;
|
|
59
63
|
} catch (error) {
|
|
60
64
|
// Damaged JSON, base64 or parameter data, and values of the wrong type, only cost a new synchronization.
|
|
@@ -137,7 +141,9 @@ export class SessionState {
|
|
|
137
141
|
const state = new SessionState();
|
|
138
142
|
state.useTanMethod(readOptionalString(json, 'tanMethod'), readOptionalString(json, 'tanMedium'));
|
|
139
143
|
if (allowedMethods !== undefined) state.rememberAllowedMethods(allowedMethods);
|
|
140
|
-
|
|
144
|
+
// fints-ts 0.1 and the Dart package 0.2.0 saved an empty list as well, which completes nothing; without it, the
|
|
145
|
+
// list is asked for again.
|
|
146
|
+
if (sepaAccounts !== undefined && sepaAccounts.length > 0) state.cacheSepaAccounts(sepaAccounts, sepaUpdVersion);
|
|
141
147
|
return state;
|
|
142
148
|
}
|
|
143
149
|
|
|
@@ -181,6 +187,17 @@ function guardStateToJson(state: LoginGuardState): JsonObject {
|
|
|
181
187
|
return { refusal: state.refusal?.map(feedbackToJson) ?? null, unreadLogins: state.unreadLogins };
|
|
182
188
|
}
|
|
183
189
|
|
|
190
|
+
/** The guard's section of `saved`, `undefined` where there is none; a damaged one is a refusal without return codes. */
|
|
191
|
+
function savedGuardState(saved: JsonObject): LoginGuardState | undefined {
|
|
192
|
+
try {
|
|
193
|
+
const json = readOptionalObject(saved, 'loginGuard');
|
|
194
|
+
return json === undefined ? undefined : guardStateFromJson(json);
|
|
195
|
+
} catch (error) {
|
|
196
|
+
if (error instanceof FormatError) return { refusal: [], unreadLogins: 0 };
|
|
197
|
+
throw error;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
184
201
|
function guardStateFromJson(json: JsonObject): LoginGuardState {
|
|
185
202
|
return {
|
|
186
203
|
refusal: readOptionalList(json, 'refusal', isJsonObject)?.map(feedbackFromJson),
|
|
@@ -1,13 +1,23 @@
|
|
|
1
1
|
import { requireLatin1 } from '../core/config.js';
|
|
2
2
|
import {
|
|
3
3
|
bankErrorFor,
|
|
4
|
+
FinTsBankError,
|
|
4
5
|
FinTsLoginDataChangedError,
|
|
6
|
+
FinTsLoginRefusedError,
|
|
5
7
|
FinTsProtocolError,
|
|
6
8
|
FinTsTanCancelledError,
|
|
7
9
|
} from '../core/errors.js';
|
|
8
|
-
import {
|
|
10
|
+
import {
|
|
11
|
+
allowedTanMethodsIn,
|
|
12
|
+
FeedbackCodes,
|
|
13
|
+
hasCode,
|
|
14
|
+
hasErrors,
|
|
15
|
+
userWarnings,
|
|
16
|
+
type FinTsFeedback,
|
|
17
|
+
} from '../core/feedback.js';
|
|
9
18
|
import type { TanMethod } from '../tan/tan-models.js';
|
|
10
19
|
import { invalidArgument } from '../util/errors.js';
|
|
20
|
+
import { trimBlanks, withAsciiForms } from '../util/text.js';
|
|
11
21
|
import type { Approval, FinTs3Dialog } from '../v3/dialog.js';
|
|
12
22
|
import { IncomingMessage3 } from '../v3/message.js';
|
|
13
23
|
import type { ParameterStore } from '../v3/parameter-store.js';
|
|
@@ -43,8 +53,8 @@ export class SignedDialog {
|
|
|
43
53
|
}
|
|
44
54
|
|
|
45
55
|
/** Logs in, which opens the dialog; with `synchronize` the bank also assigns a customer system ID. */
|
|
46
|
-
|
|
47
|
-
return this.
|
|
56
|
+
login(synchronize = false): Promise<IncomingMessage3> {
|
|
57
|
+
return this.settleLogin(() => this.dialog.start(synchronize));
|
|
48
58
|
}
|
|
49
59
|
|
|
50
60
|
/**
|
|
@@ -53,27 +63,16 @@ export class SignedDialog {
|
|
|
53
63
|
*/
|
|
54
64
|
async loginWith(order: SegmentSpec): Promise<IncomingMessage3[]> {
|
|
55
65
|
const start = this.dialog.startWith(order);
|
|
56
|
-
const login = await this.
|
|
66
|
+
const login = await this.settleLogin(start.login);
|
|
57
67
|
return [login, await this.settle(await start.order(), 'order')];
|
|
58
68
|
}
|
|
59
69
|
|
|
60
|
-
/** Sends `order`, the first page of a query, and completes the approval the bank asks for. */
|
|
61
|
-
async send(order: SegmentSpec): Promise<IncomingMessage3> {
|
|
62
|
-
return this.settle(await this.dialog.send(order), 'order');
|
|
63
|
-
}
|
|
64
|
-
|
|
65
70
|
/**
|
|
66
|
-
* Sends `order`
|
|
67
|
-
*
|
|
68
|
-
* page. Throws `FinTsProtocolError` if it asks for one.
|
|
71
|
+
* Sends `order` and completes the approval the bank asks for. `admitApproval` runs before the user is asked, and
|
|
72
|
+
* refuses the approval by throwing.
|
|
69
73
|
*/
|
|
70
|
-
async
|
|
71
|
-
|
|
72
|
-
this.receive(response, 'order', false);
|
|
73
|
-
if (this.dialog.approvalAskedIn(response) !== undefined) {
|
|
74
|
-
throw new FinTsProtocolError('The bank asks for an approval of a further page of the same query');
|
|
75
|
-
}
|
|
76
|
-
return response;
|
|
74
|
+
async send(order: SegmentSpec, admitApproval?: () => void): Promise<IncomingMessage3> {
|
|
75
|
+
return this.settle(await this.dialog.send(order), 'order', admitApproval);
|
|
77
76
|
}
|
|
78
77
|
|
|
79
78
|
/**
|
|
@@ -104,32 +103,63 @@ export class SignedDialog {
|
|
|
104
103
|
}
|
|
105
104
|
|
|
106
105
|
/**
|
|
107
|
-
*
|
|
108
|
-
* the
|
|
109
|
-
*
|
|
110
|
-
|
|
111
|
-
|
|
106
|
+
* Sends the login `send` and settles its answer. New login data next to an error in the answer to its approval are
|
|
107
|
+
* retired all the same (`retireLoginData`), although the guard counts that error as no refusal; next to an error in
|
|
108
|
+
* the answer to the login they are its refusal already (`loginRefusalFor`).
|
|
109
|
+
*/
|
|
110
|
+
private async settleLogin(send: () => Promise<IncomingMessage3>): Promise<IncomingMessage3> {
|
|
111
|
+
try {
|
|
112
|
+
return await this.settle(await send(), 'login');
|
|
113
|
+
} catch (error) {
|
|
114
|
+
if (
|
|
115
|
+
!(error instanceof FinTsBankError) ||
|
|
116
|
+
error instanceof FinTsLoginRefusedError ||
|
|
117
|
+
!hasCode(error.feedback, FeedbackCodes.newLoginData)
|
|
118
|
+
) {
|
|
119
|
+
throw error;
|
|
120
|
+
}
|
|
121
|
+
return this.retireLoginData(error.feedback, 'approval');
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Where `response` answers a login, a request of `kind` other than `order`, or its approval with new login data,
|
|
127
|
+
* retires them (`retireLoginData`). It runs before any approval, which would cost the user a TAN for a session that
|
|
128
|
+
* ends anyway.
|
|
112
129
|
*/
|
|
113
130
|
private async retireOldLoginData(response: IncomingMessage3, kind: RequestKind): Promise<void> {
|
|
114
131
|
if (kind === 'order' || !hasCode(response.feedback, FeedbackCodes.newLoginData)) return;
|
|
132
|
+
await this.retireLoginData(response.feedback, kind);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Ends the dialog, whose answer to a request of `kind` brought new login data with `feedback`, stops the client from
|
|
137
|
+
* logging in with the old ones, and fails the call with `FinTsLoginDataChangedError` (Rückmeldungscodes C.6).
|
|
138
|
+
*/
|
|
139
|
+
private async retireLoginData(feedback: readonly FinTsFeedback[], kind: RequestKind): Promise<never> {
|
|
115
140
|
await this.end();
|
|
116
|
-
const error = new FinTsLoginDataChangedError(
|
|
141
|
+
const error = new FinTsLoginDataChangedError(feedback);
|
|
117
142
|
throw this.session.guard.judge(error, kind, this.dialog.method) ?? error;
|
|
118
143
|
}
|
|
119
144
|
|
|
120
145
|
/**
|
|
121
|
-
* Receives `response`, the answer to a request of `kind`, then completes the approval it asks for
|
|
122
|
-
* with the response that completes the approval.
|
|
146
|
+
* Receives `response`, the answer to a request of `kind`, then completes the approval it asks for, once
|
|
147
|
+
* `admitApproval` has let it; returns it joined with the response that completes the approval.
|
|
123
148
|
*
|
|
124
149
|
* Throws `FinTsProtocolError` if that response asks for an approval once more: the bank has not executed the message
|
|
125
150
|
* then either, and read as done it would report an empty result.
|
|
126
151
|
*/
|
|
127
|
-
private async settle(
|
|
152
|
+
private async settle(
|
|
153
|
+
response: IncomingMessage3,
|
|
154
|
+
kind: RequestKind,
|
|
155
|
+
admitApproval?: () => void,
|
|
156
|
+
): Promise<IncomingMessage3> {
|
|
128
157
|
const opening = kind !== 'order';
|
|
129
158
|
this.receive(response, kind, opening);
|
|
130
159
|
await this.retireOldLoginData(response, kind);
|
|
131
160
|
const approval = this.dialog.approvalAskedIn(response);
|
|
132
161
|
if (approval === undefined) return response;
|
|
162
|
+
admitApproval?.();
|
|
133
163
|
const approved = approval.challenge.isDecoupled
|
|
134
164
|
? await this.awaitAppApproval(approval, opening)
|
|
135
165
|
: await this.enterTan(approval, opening);
|
|
@@ -154,8 +184,8 @@ export class SignedDialog {
|
|
|
154
184
|
if (answer.kind === 'approved') {
|
|
155
185
|
throw invalidArgument('Answered a TAN challenge with an approval in the banking app', 'tanCallback');
|
|
156
186
|
}
|
|
157
|
-
// NFKC,
|
|
158
|
-
const tan = answer.kind === 'tan' ? answer.tan
|
|
187
|
+
// Not NFKC, which also changes characters FinTS 3.0 sends as they are, such as `²`, in a TAN the bank compares.
|
|
188
|
+
const tan = answer.kind === 'tan' ? trimBlanks(withAsciiForms(answer.tan)) : '';
|
|
159
189
|
if (tan === '') throw new FinTsTanCancelledError();
|
|
160
190
|
const response = await approval.submitTan(requireTanOf(approval.challenge.method, tan));
|
|
161
191
|
this.receive(response, 'approval', opening);
|
|
@@ -4,9 +4,16 @@ import { clientClosedError } from '../core/errors.js';
|
|
|
4
4
|
import type { FinTsFeedback } from '../core/feedback.js';
|
|
5
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
|
|
|
11
|
+
/** The callbacks of `FinTsClientOptions`. */
|
|
12
|
+
type Callbacks = Pick<
|
|
13
|
+
FinTsClientOptions,
|
|
14
|
+
'tanCallback' | 'tanMethodSelector' | 'tanMediumSelector' | 'onBankMessage' | 'onWarning' | 'logger'
|
|
15
|
+
>;
|
|
16
|
+
|
|
10
17
|
/**
|
|
11
18
|
* Everything a session tells the app, asks of it or waits for: the bank's messages, warnings and log, the TAN callback,
|
|
12
19
|
* the selectors and the pauses between status requests.
|
|
@@ -23,7 +30,25 @@ export class UserPrompts {
|
|
|
23
30
|
/** Aborts the waits under way; each wait removes its abort when it is over, or when nothing waits for it any more. */
|
|
24
31
|
private readonly aborts = new Set<() => void>();
|
|
25
32
|
|
|
26
|
-
|
|
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
|
+
}
|
|
27
52
|
|
|
28
53
|
/** Ends every wait; the client calls it once. */
|
|
29
54
|
close(): void {
|
|
@@ -121,7 +146,7 @@ export class UserPrompts {
|
|
|
121
146
|
const selector = this.options.tanMethodSelector;
|
|
122
147
|
if (candidates.length === 1 || selector === undefined) return candidates[0]!;
|
|
123
148
|
const chosen = await this.unlessClosed(() => selector(candidates));
|
|
124
|
-
if (chosen === undefined) return candidates[0]!;
|
|
149
|
+
if (chosen === undefined || chosen === null) return candidates[0]!;
|
|
125
150
|
const match = candidates.find((m) => m.securityFunction === chosen.securityFunction);
|
|
126
151
|
if (match === undefined) throw invalidArgument('Returned a TAN method the bank did not offer', 'tanMethodSelector');
|
|
127
152
|
return match;
|
|
@@ -136,7 +161,7 @@ export class UserPrompts {
|
|
|
136
161
|
const selector = this.options.tanMediumSelector;
|
|
137
162
|
if (media.length === 1 || selector === undefined) return media[0]!;
|
|
138
163
|
const chosen = await this.unlessClosed(() => selector(media));
|
|
139
|
-
if (chosen === undefined) return media[0]!;
|
|
164
|
+
if (chosen === undefined || chosen === null) return media[0]!;
|
|
140
165
|
const match = media.find((m) => m.name === chosen.name);
|
|
141
166
|
if (match === undefined) throw invalidArgument('Returned a TAN medium the bank did not list', 'tanMediumSelector');
|
|
142
167
|
return match;
|