@coderoftime/fints 0.0.0-stage → 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 +152 -0
- package/LICENSE +28 -0
- package/README.md +246 -2
- 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 +168 -0
- package/dist/core/account.d.ts.map +1 -0
- package/dist/core/account.js +245 -0
- package/dist/core/account.js.map +1 -0
- package/dist/core/amount.d.ts +63 -0
- package/dist/core/amount.d.ts.map +1 -0
- package/dist/core/amount.js +192 -0
- package/dist/core/amount.js.map +1 -0
- package/dist/core/balance.d.ts +55 -0
- package/dist/core/balance.d.ts.map +1 -0
- package/dist/core/balance.js +54 -0
- package/dist/core/balance.js.map +1 -0
- package/dist/core/bank-info.d.ts +49 -0
- package/dist/core/bank-info.d.ts.map +1 -0
- package/dist/core/bank-info.js +52 -0
- package/dist/core/bank-info.js.map +1 -0
- package/dist/core/client.d.ts +161 -0
- package/dist/core/client.d.ts.map +1 -0
- package/dist/core/client.js +2 -0
- package/dist/core/client.js.map +1 -0
- package/dist/core/config.d.ts +80 -0
- package/dist/core/config.d.ts.map +1 -0
- package/dist/core/config.js +153 -0
- package/dist/core/config.js.map +1 -0
- package/dist/core/errors.d.ts +153 -0
- package/dist/core/errors.d.ts.map +1 -0
- package/dist/core/errors.js +247 -0
- package/dist/core/errors.js.map +1 -0
- package/dist/core/feedback.d.ts +72 -0
- package/dist/core/feedback.d.ts.map +1 -0
- package/dist/core/feedback.js +127 -0
- package/dist/core/feedback.js.map +1 -0
- package/dist/core/transaction.d.ts +103 -0
- package/dist/core/transaction.d.ts.map +1 -0
- package/dist/core/transaction.js +96 -0
- package/dist/core/transaction.js.map +1 -0
- package/dist/core/transport.d.ts +64 -0
- package/dist/core/transport.d.ts.map +1 -0
- package/dist/core/transport.js +149 -0
- package/dist/core/transport.js.map +1 -0
- package/dist/formats/camt.d.ts +32 -0
- package/dist/formats/camt.d.ts.map +1 -0
- package/dist/formats/camt.js +242 -0
- package/dist/formats/camt.js.map +1 -0
- package/dist/formats/mt940.d.ts +54 -0
- package/dist/formats/mt940.d.ts.map +1 -0
- package/dist/formats/mt940.js +376 -0
- package/dist/formats/mt940.js.map +1 -0
- package/dist/formats/readers.d.ts +15 -0
- package/dist/formats/readers.d.ts.map +1 -0
- package/dist/formats/readers.js +42 -0
- package/dist/formats/readers.js.map +1 -0
- 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 +28 -0
- package/dist/formats/sepa-purpose.d.ts.map +1 -0
- package/dist/formats/sepa-purpose.js +89 -0
- package/dist/formats/sepa-purpose.js.map +1 -0
- package/dist/index.d.ts +28 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +27 -0
- package/dist/index.js.map +1 -0
- package/dist/session/app-approval.d.ts +66 -0
- package/dist/session/app-approval.d.ts.map +1 -0
- package/dist/session/app-approval.js +226 -0
- package/dist/session/app-approval.js.map +1 -0
- package/dist/session/dialog-runner.d.ts +31 -0
- package/dist/session/dialog-runner.d.ts.map +1 -0
- package/dist/session/dialog-runner.js +111 -0
- package/dist/session/dialog-runner.js.map +1 -0
- package/dist/session/login-guard.d.ts +78 -0
- package/dist/session/login-guard.d.ts.map +1 -0
- package/dist/session/login-guard.js +181 -0
- package/dist/session/login-guard.js.map +1 -0
- package/dist/session/sepa-accounts.d.ts +40 -0
- package/dist/session/sepa-accounts.d.ts.map +1 -0
- package/dist/session/sepa-accounts.js +112 -0
- package/dist/session/sepa-accounts.js.map +1 -0
- package/dist/session/session-setup.d.ts +80 -0
- package/dist/session/session-setup.d.ts.map +1 -0
- package/dist/session/session-setup.js +182 -0
- package/dist/session/session-setup.js.map +1 -0
- package/dist/session/session-state.d.ts +50 -0
- package/dist/session/session-state.d.ts.map +1 -0
- package/dist/session/session-state.js +182 -0
- package/dist/session/session-state.js.map +1 -0
- 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 +96 -0
- package/dist/session/tan-selection.d.ts.map +1 -0
- package/dist/session/tan-selection.js +189 -0
- package/dist/session/tan-selection.js.map +1 -0
- package/dist/session/user-prompts.d.ts +65 -0
- package/dist/session/user-prompts.d.ts.map +1 -0
- package/dist/session/user-prompts.js +184 -0
- package/dist/session/user-prompts.js.map +1 -0
- package/dist/tan/challenge.d.ts +135 -0
- package/dist/tan/challenge.d.ts.map +1 -0
- package/dist/tan/challenge.js +184 -0
- package/dist/tan/challenge.js.map +1 -0
- package/dist/tan/flicker.d.ts +42 -0
- package/dist/tan/flicker.d.ts.map +1 -0
- package/dist/tan/flicker.js +296 -0
- package/dist/tan/flicker.js.map +1 -0
- package/dist/tan/tan-image.d.ts +22 -0
- package/dist/tan/tan-image.d.ts.map +1 -0
- package/dist/tan/tan-image.js +43 -0
- package/dist/tan/tan-image.js.map +1 -0
- package/dist/tan/tan-models.d.ts +149 -0
- package/dist/tan/tan-models.d.ts.map +1 -0
- package/dist/tan/tan-models.js +153 -0
- package/dist/tan/tan-models.js.map +1 -0
- package/dist/util/async.d.ts +22 -0
- package/dist/util/async.d.ts.map +1 -0
- package/dist/util/async.js +45 -0
- package/dist/util/async.js.map +1 -0
- package/dist/util/bytes.d.ts +22 -0
- package/dist/util/bytes.d.ts.map +1 -0
- package/dist/util/bytes.js +128 -0
- package/dist/util/bytes.js.map +1 -0
- package/dist/util/call-context.d.ts +12 -0
- package/dist/util/call-context.d.ts.map +1 -0
- package/dist/util/call-context.js +30 -0
- package/dist/util/call-context.js.map +1 -0
- package/dist/util/dates.d.ts +55 -0
- package/dist/util/dates.d.ts.map +1 -0
- package/dist/util/dates.js +189 -0
- package/dist/util/dates.js.map +1 -0
- package/dist/util/errors.d.ts +17 -0
- package/dist/util/errors.d.ts.map +1 -0
- package/dist/util/errors.js +24 -0
- package/dist/util/errors.js.map +1 -0
- package/dist/util/immutable-map.d.ts +16 -0
- package/dist/util/immutable-map.d.ts.map +1 -0
- package/dist/util/immutable-map.js +38 -0
- package/dist/util/immutable-map.js.map +1 -0
- package/dist/util/json.d.ts +17 -0
- package/dist/util/json.d.ts.map +1 -0
- package/dist/util/json.js +62 -0
- package/dist/util/json.js.map +1 -0
- package/dist/util/text.d.ts +37 -0
- package/dist/util/text.d.ts.map +1 -0
- package/dist/util/text.js +107 -0
- package/dist/util/text.js.map +1 -0
- package/dist/util/xml.d.ts +36 -0
- package/dist/util/xml.d.ts.map +1 -0
- package/dist/util/xml.js +194 -0
- package/dist/util/xml.js.map +1 -0
- package/dist/v3/account-extension.d.ts +25 -0
- package/dist/v3/account-extension.d.ts.map +1 -0
- package/dist/v3/account-extension.js +173 -0
- package/dist/v3/account-extension.js.map +1 -0
- 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 +112 -0
- package/dist/v3/dialog.d.ts.map +1 -0
- package/dist/v3/dialog.js +241 -0
- package/dist/v3/dialog.js.map +1 -0
- package/dist/v3/message.d.ts +85 -0
- package/dist/v3/message.d.ts.map +1 -0
- package/dist/v3/message.js +303 -0
- package/dist/v3/message.js.map +1 -0
- 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 +105 -0
- package/dist/v3/parameters.d.ts.map +1 -0
- package/dist/v3/parameters.js +450 -0
- package/dist/v3/parameters.js.map +1 -0
- package/dist/v3/protocol.d.ts +61 -0
- package/dist/v3/protocol.d.ts.map +1 -0
- package/dist/v3/protocol.js +113 -0
- package/dist/v3/protocol.js.map +1 -0
- 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 +106 -0
- package/dist/v3/segments.d.ts.map +1 -0
- package/dist/v3/segments.js +325 -0
- package/dist/v3/segments.js.map +1 -0
- package/dist/v3/syntax.d.ts +127 -0
- package/dist/v3/syntax.d.ts.map +1 -0
- package/dist/v3/syntax.js +367 -0
- package/dist/v3/syntax.js.map +1 -0
- package/package.json +67 -4
- package/src/client.ts +252 -0
- package/src/core/account.ts +323 -0
- package/src/core/amount.ts +211 -0
- package/src/core/balance.ts +85 -0
- package/src/core/bank-info.ts +66 -0
- package/src/core/client.ts +186 -0
- package/src/core/config.ts +182 -0
- package/src/core/errors.ts +277 -0
- package/src/core/feedback.ts +140 -0
- package/src/core/transaction.ts +161 -0
- package/src/core/transport.ts +189 -0
- package/src/formats/camt.ts +266 -0
- package/src/formats/mt940.ts +419 -0
- package/src/formats/readers.ts +49 -0
- package/src/formats/record-budget.ts +18 -0
- package/src/formats/sepa-purpose.ts +94 -0
- package/src/index.ts +70 -0
- package/src/session/app-approval.ts +278 -0
- package/src/session/dialog-runner.ts +122 -0
- package/src/session/login-guard.ts +221 -0
- package/src/session/sepa-accounts.ts +115 -0
- package/src/session/session-setup.ts +184 -0
- package/src/session/session-state.ts +204 -0
- package/src/session/signed-dialog.ts +192 -0
- package/src/session/tan-selection.ts +203 -0
- package/src/session/user-prompts.ts +193 -0
- package/src/tan/challenge.ts +261 -0
- package/src/tan/flicker.ts +308 -0
- package/src/tan/tan-image.ts +38 -0
- package/src/tan/tan-models.ts +252 -0
- package/src/util/async.ts +48 -0
- package/src/util/bytes.ts +129 -0
- package/src/util/call-context.ts +41 -0
- package/src/util/dates.ts +210 -0
- package/src/util/errors.ts +27 -0
- package/src/util/immutable-map.ts +47 -0
- package/src/util/json.ts +67 -0
- package/src/util/text.ts +112 -0
- package/src/util/xml.ts +183 -0
- package/src/v3/account-extension.ts +185 -0
- package/src/v3/bank-connection.ts +67 -0
- package/src/v3/dialog.ts +283 -0
- package/src/v3/message.ts +373 -0
- package/src/v3/parameter-store.ts +160 -0
- package/src/v3/parameters.ts +524 -0
- package/src/v3/protocol.ts +148 -0
- package/src/v3/queries.ts +190 -0
- package/src/v3/segments.ts +374 -0
- package/src/v3/syntax.ts +369 -0
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
import { invalidArgument } from '../util/errors.js';
|
|
2
|
+
import { isLatin1 } from '../util/text.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* The country code FinTS gives Germany, where FinTS banks are: the ISO 3166 code of the Federal Republic before 1990,
|
|
6
|
+
* not today's `276`.
|
|
7
|
+
*/
|
|
8
|
+
export const defaultCountryCode = '280';
|
|
9
|
+
|
|
10
|
+
/** Longest bank code (Kreditinstitutscode), product ID (Produktbezeichnung) and product version FinTS 3.0 takes. */
|
|
11
|
+
const maxBankCodeLength = 30;
|
|
12
|
+
const maxProductIdLength = 25;
|
|
13
|
+
const maxProductVersionLength = 5;
|
|
14
|
+
|
|
15
|
+
/** What `FinTsConfig` takes. */
|
|
16
|
+
export interface FinTsConfigInit {
|
|
17
|
+
/** The bank's FinTS PIN/TAN endpoint; must be https, without user name or password. */
|
|
18
|
+
url: string | URL;
|
|
19
|
+
/** Bank code: for a German bank its BLZ, eight digits. */
|
|
20
|
+
bankCode: string;
|
|
21
|
+
/** Login name (Benutzerkennung / Legitimations-ID). */
|
|
22
|
+
userId: string;
|
|
23
|
+
/** Customer ID; defaults to `userId`. */
|
|
24
|
+
customerId?: string | undefined;
|
|
25
|
+
/** FinTS product registration number from the Deutsche Kreditwirtschaft; at most 25 characters. */
|
|
26
|
+
productId: string;
|
|
27
|
+
/** Version of the app as registered with the product; at most five characters. `1.0` by default. */
|
|
28
|
+
productVersion?: string;
|
|
29
|
+
/** Country code of the bank, three digits: `280`, the default, for Germany. */
|
|
30
|
+
countryCode?: string;
|
|
31
|
+
/**
|
|
32
|
+
* Allows plain http for the loopback addresses `localhost`, `127.x.x.x` and `::1`, for tests against a local server.
|
|
33
|
+
*
|
|
34
|
+
* Any other host must use https, whatever this says: the PIN travels with every message.
|
|
35
|
+
*/
|
|
36
|
+
allowInsecureUrl?: boolean;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** Connection and login data for one bank user. */
|
|
40
|
+
export class FinTsConfig {
|
|
41
|
+
/** The country code FinTS gives Germany, where FinTS banks are: `280`, not today's ISO 3166 code `276`. */
|
|
42
|
+
static readonly defaultCountryCode = defaultCountryCode;
|
|
43
|
+
|
|
44
|
+
private readonly href: string;
|
|
45
|
+
readonly bankCode: string;
|
|
46
|
+
readonly userId: string;
|
|
47
|
+
private readonly explicitCustomerId: string | undefined;
|
|
48
|
+
readonly productId: string;
|
|
49
|
+
readonly productVersion: string;
|
|
50
|
+
readonly countryCode: string;
|
|
51
|
+
readonly allowInsecureUrl: boolean;
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Throws `TypeError` if `url` is not a valid URL, has no host, holds a user name or password, or is not https, unless
|
|
55
|
+
* `allowInsecureUrl` is set and `url` is a loopback address. Throws as well where the bank would refuse the login: for
|
|
56
|
+
* a login or product detail that is empty, has characters FinTS 3.0 cannot send (it uses ISO 8859-1, so `€` and most
|
|
57
|
+
* non-Latin letters are not possible) or is longer than its field (30 characters for the bank code, 25 for the
|
|
58
|
+
* product ID, 5 for its version), for a country code of other than three digits, and for a German bank code of other
|
|
59
|
+
* than eight. The message leaves out the value.
|
|
60
|
+
*
|
|
61
|
+
* Drops leading and trailing blanks of the login details and the product, as FinTS sends them without (Formals
|
|
62
|
+
* B.4.1): `'alice '` is the user `alice`, also for a saved state.
|
|
63
|
+
*/
|
|
64
|
+
constructor(init: FinTsConfigInit) {
|
|
65
|
+
// `URL` would read `https:bank` and `https:///bank` as the host `bank`; such a URL is a mistake, not a host.
|
|
66
|
+
if (typeof init.url === 'string' && !/^[a-z][a-z0-9+.-]*:\/\/[^/]/i.test(init.url)) {
|
|
67
|
+
throw invalidArgument('The URL has no host', 'url');
|
|
68
|
+
}
|
|
69
|
+
let url: URL;
|
|
70
|
+
try {
|
|
71
|
+
url = new URL(init.url);
|
|
72
|
+
} catch {
|
|
73
|
+
throw invalidArgument('Not a valid URL', 'url');
|
|
74
|
+
}
|
|
75
|
+
this.href = url.href;
|
|
76
|
+
this.allowInsecureUrl = init.allowInsecureUrl ?? false;
|
|
77
|
+
requireSecureUrl(url, this.allowInsecureUrl);
|
|
78
|
+
this.countryCode = requireValue(init.countryCode ?? defaultCountryCode, 'countryCode');
|
|
79
|
+
if (!/^\d{3}$/.test(this.countryCode)) {
|
|
80
|
+
throw invalidArgument('Must be three digits, 280 for Germany', 'countryCode');
|
|
81
|
+
}
|
|
82
|
+
this.bankCode = requireValue(init.bankCode, 'bankCode', maxBankCodeLength);
|
|
83
|
+
if (this.countryCode === defaultCountryCode && !/^\d{8}$/.test(this.bankCode)) {
|
|
84
|
+
throw invalidArgument('A German bank code (BLZ) has eight digits', 'bankCode');
|
|
85
|
+
}
|
|
86
|
+
this.userId = requireValue(init.userId, 'userId');
|
|
87
|
+
this.explicitCustomerId = init.customerId === undefined ? undefined : requireValue(init.customerId, 'customerId');
|
|
88
|
+
this.productId = requireValue(init.productId, 'productId', maxProductIdLength);
|
|
89
|
+
this.productVersion = requireValue(init.productVersion ?? '1.0', 'productVersion', maxProductVersionLength);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* The bank's FinTS PIN/TAN endpoint, as checked by the constructor. A new `URL` on each access, so that changing one
|
|
94
|
+
* cannot change where the client posts the PIN.
|
|
95
|
+
*/
|
|
96
|
+
get url(): URL {
|
|
97
|
+
return new URL(this.href);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/** Customer ID; defaults to `userId`. */
|
|
101
|
+
get customerId(): string {
|
|
102
|
+
return this.explicitCustomerId ?? this.userId;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* `value` without leading and trailing blanks; throws `TypeError` if nothing is left, or what is left is not sendable
|
|
108
|
+
* (`requireLatin1`) or longer than `maxLength`: cut short, a value would be one the bank does not know.
|
|
109
|
+
*/
|
|
110
|
+
function requireValue(value: string, name: string, maxLength = Number.POSITIVE_INFINITY): string {
|
|
111
|
+
const trimmed = value.trim();
|
|
112
|
+
if (trimmed === '') throw invalidArgument('Must not be empty', name);
|
|
113
|
+
requireLatin1(trimmed, name);
|
|
114
|
+
if (trimmed.length > maxLength) throw invalidArgument(`At most ${maxLength} characters in FinTS 3.0`, name);
|
|
115
|
+
return trimmed;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/** `text`, unless it has characters FinTS 3.0 cannot send (ISO 8859-1 only); the message leaves out the value. */
|
|
119
|
+
export function requireLatin1(text: string, name: string): string {
|
|
120
|
+
if (!isLatin1(text)) throw invalidArgument('Contains characters FinTS 3.0 cannot send (ISO 8859-1 only)', name);
|
|
121
|
+
return text;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/** The host of `url` without the brackets of an IPv6 address, as `URL` writes it. */
|
|
125
|
+
export function hostOf(url: URL): string {
|
|
126
|
+
const host = url.hostname;
|
|
127
|
+
return host.startsWith('[') && host.endsWith(']') ? host.substring(1, host.length - 1) : host;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Throws `TypeError` unless `url` names a host and uses https: the PIN travels with every message.
|
|
132
|
+
*
|
|
133
|
+
* With `allowInsecure`, http is accepted for loopback hosts only. Throws as well for a user name or password in `url`:
|
|
134
|
+
* `fetch` refuses such a URL, with the whole URL in its error message. The message leaves them out.
|
|
135
|
+
*/
|
|
136
|
+
function requireSecureUrl(url: URL, allowInsecure = false): void {
|
|
137
|
+
if (hostOf(url) === '') throw invalidArgument('The URL has no host', 'url');
|
|
138
|
+
if (url.username !== '' || url.password !== '') throw invalidArgument('The URL must not contain credentials', 'url');
|
|
139
|
+
const loopbackHttp = allowInsecure && url.protocol === 'http:' && isLoopback(hostOf(url));
|
|
140
|
+
if (url.protocol !== 'https:' && !loopbackHttp) throw invalidArgument('PIN/TAN requires an https URL', 'url');
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* `pin`, unless FinTS 3.0 cannot send it: throws `TypeError` for an empty PIN, which the bank would count as a failed
|
|
145
|
+
* attempt, for one with leading or trailing blanks, which would go out without them since FinTS values carry none
|
|
146
|
+
* (Formals B.4.1), and for one with characters outside ISO 8859-1.
|
|
147
|
+
*
|
|
148
|
+
* The message leaves out the value.
|
|
149
|
+
*/
|
|
150
|
+
export function requirePin(pin: string): string {
|
|
151
|
+
if (pin.trim() === '') throw invalidArgument('The PIN is empty', 'pin');
|
|
152
|
+
if (pin.trim() !== pin) throw invalidArgument('The PIN begins or ends with a blank', 'pin');
|
|
153
|
+
return requireLatin1(pin, 'pin');
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Throws `TypeError` if `pin` is shorter than `min` or longer than `max` characters, the bank's limits: the bank would
|
|
158
|
+
* count such a PIN as a failed attempt. A limit that is `undefined` or below 1 does not apply.
|
|
159
|
+
*
|
|
160
|
+
* The message leaves out the PIN and its length.
|
|
161
|
+
*/
|
|
162
|
+
export function requirePinLength(pin: string, min: number | undefined, max: number | undefined): void {
|
|
163
|
+
const tooShort = min !== undefined && min > 0 && pin.length < min;
|
|
164
|
+
const tooLong = max !== undefined && max > 0 && pin.length > max;
|
|
165
|
+
if (tooShort || tooLong) {
|
|
166
|
+
const range = [
|
|
167
|
+
...(min !== undefined && min > 0 ? [`at least ${min}`] : []),
|
|
168
|
+
...(max !== undefined && max > 0 ? [`at most ${max}`] : []),
|
|
169
|
+
].join(' and ');
|
|
170
|
+
throw invalidArgument(`The bank takes PINs of ${range} characters`, 'pin');
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const loopbackAddress = /^127(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}$/;
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Only addresses and names that cannot leave the machine: a resolver may send a subdomain such as `bank.localhost`, or
|
|
178
|
+
* a string that is not an address, such as `127.999.1.1`, to DNS.
|
|
179
|
+
*/
|
|
180
|
+
function isLoopback(host: string): boolean {
|
|
181
|
+
return host === 'localhost' || host === '::1' || loopbackAddress.test(host);
|
|
182
|
+
}
|
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
import { FormatError } from '../util/errors.js';
|
|
2
|
+
import { nonBlank } from '../util/text.js';
|
|
3
|
+
import { endsDialog, FeedbackCodes, hasCode, type FinTsFeedback } from './feedback.js';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Base class of all errors the client raises about the bank and its data.
|
|
7
|
+
*
|
|
8
|
+
* Each error class states its `name`: a minifier renames classes, and `constructor.name` would show a meaningless one.
|
|
9
|
+
*/
|
|
10
|
+
export abstract class FinTsError extends Error {
|
|
11
|
+
abstract override name: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* The connection to the bank failed or returned an HTTP error.
|
|
16
|
+
*
|
|
17
|
+
* It carries no `cause`: the transport's own error may hold the message it was sending, PIN and TAN included.
|
|
18
|
+
*/
|
|
19
|
+
export class FinTsTransportError extends FinTsError {
|
|
20
|
+
override name = 'FinTsTransportError';
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* The HTTP status code of the bank's answer, `undefined` where none came, for example after a timeout; the client
|
|
24
|
+
* acts on it (`FinTsTransport.post`).
|
|
25
|
+
*/
|
|
26
|
+
readonly statusCode: number | undefined;
|
|
27
|
+
|
|
28
|
+
constructor(message: string, options?: { statusCode?: number }) {
|
|
29
|
+
super(message);
|
|
30
|
+
this.statusCode = options?.statusCode;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** The bank's response could not be understood. */
|
|
35
|
+
export class FinTsProtocolError extends FinTsError {
|
|
36
|
+
override name = 'FinTsProtocolError';
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** The bank does not offer the requested business transaction. */
|
|
40
|
+
export class FinTsUnsupportedError extends FinTsError {
|
|
41
|
+
override name = 'FinTsUnsupportedError';
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* The client has no TAN method or medium to log in with: the method in use needs a TAN medium the client could not
|
|
46
|
+
* learn, or the bank offers the user only TAN methods the client cannot run. Pass a medium to
|
|
47
|
+
* `FinTsClient.selectTanMethod`, or choose another method.
|
|
48
|
+
*
|
|
49
|
+
* The client does not send a login the bank would refuse for it, but the steps before, such as the synchronization or
|
|
50
|
+
* the lookup of the TAN media, may have gone out.
|
|
51
|
+
*/
|
|
52
|
+
export class FinTsTanSetupError extends FinTsError {
|
|
53
|
+
override name = 'FinTsTanSetupError';
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** The bank rejected the request; `feedback` holds all return codes. A refused login is a `FinTsLoginRefusedError`. */
|
|
57
|
+
export class FinTsBankError extends FinTsError {
|
|
58
|
+
override name = 'FinTsBankError';
|
|
59
|
+
|
|
60
|
+
/** All return codes of the response, warnings and successes included. */
|
|
61
|
+
readonly feedback: readonly FinTsFeedback[];
|
|
62
|
+
|
|
63
|
+
/** A rejection with the return codes in `feedback`; `message` defaults to the errors among them. */
|
|
64
|
+
constructor(feedback: readonly FinTsFeedback[], message?: string) {
|
|
65
|
+
super(
|
|
66
|
+
message ??
|
|
67
|
+
feedback
|
|
68
|
+
.filter((f) => f.isError)
|
|
69
|
+
.map((f) => f.toString())
|
|
70
|
+
.join('; '),
|
|
71
|
+
);
|
|
72
|
+
this.feedback = Object.freeze([...feedback]);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** The return codes of `feedback`. */
|
|
76
|
+
get codes(): string[] {
|
|
77
|
+
return this.feedback.map((f) => f.code);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* The bank refused the login, so the client sends the PIN no more: every later call that would send it fails with this
|
|
83
|
+
* error without contacting the bank, also in a client restored from the saved state, until
|
|
84
|
+
* `FinTsClient.clearLoginRefusal` takes a PIN the user has entered anew; for good where the bank has changed the login
|
|
85
|
+
* data (`FinTsLoginDataChangedError`).
|
|
86
|
+
*
|
|
87
|
+
* Banks lock the access after a few failed logins, and a return code does not always tell whether the PIN was the
|
|
88
|
+
* reason, so any error in the answer to a login refuses it, also one that ends the dialog, and so does a wrong PIN
|
|
89
|
+
* wherever the bank reports it. Only where the bank names a reason the client can take care of, another TAN method or a
|
|
90
|
+
* new customer system ID, does the same call log in once more, for each reason once. The client refuses the login
|
|
91
|
+
* itself where it could not read the answers to two logins in a row, which the bank may have counted as failed
|
|
92
|
+
* attempts; `feedback` is empty then.
|
|
93
|
+
*/
|
|
94
|
+
export class FinTsLoginRefusedError extends FinTsBankError {
|
|
95
|
+
override name = 'FinTsLoginRefusedError';
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** Wrong or blocked PIN. */
|
|
99
|
+
export class FinTsPinError extends FinTsLoginRefusedError {
|
|
100
|
+
override name = 'FinTsPinError';
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** Invalid, expired or rejected TAN, as the bank reports it in answer to a TAN or to an approval status request. */
|
|
104
|
+
export class FinTsTanError extends FinTsBankError {
|
|
105
|
+
override name = 'FinTsTanError';
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* The bank has changed the user's login data and expects the new ones from now on (return code 3072).
|
|
110
|
+
*
|
|
111
|
+
* The client has ended the dialog, where the bank opened one, and logs in with the old data no more. Create a new
|
|
112
|
+
* client with the new data; a `loginName` replaces user ID and customer ID: pass it as `userId`, without a customer ID.
|
|
113
|
+
*/
|
|
114
|
+
export class FinTsLoginDataChangedError extends FinTsLoginRefusedError {
|
|
115
|
+
override name = 'FinTsLoginDataChangedError';
|
|
116
|
+
|
|
117
|
+
constructor(feedback: readonly FinTsFeedback[]) {
|
|
118
|
+
super(feedback, 'The bank has changed the login data');
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/** The new user ID (Benutzerkennung); `undefined` where the bank names none. */
|
|
122
|
+
get userId(): string | undefined {
|
|
123
|
+
return this.parameter(0);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/** The new customer ID (Kunden-ID); `undefined` where the bank names none. */
|
|
127
|
+
get customerId(): string | undefined {
|
|
128
|
+
return this.parameter(1);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/** The new login name (Anmeldename); `undefined` where the bank names none. */
|
|
132
|
+
get loginName(): string | undefined {
|
|
133
|
+
return this.parameter(2);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
private parameter(index: number): string | undefined {
|
|
137
|
+
const change = this.feedback.find((f) => f.code === FeedbackCodes.newLoginData);
|
|
138
|
+
return nonBlank(change?.parameters[index]);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* The bank ended the dialog, for example after a timeout; the next call logs in anew. Where the bank ends the dialog
|
|
144
|
+
* in answer to a login, the call fails with `FinTsLoginRefusedError` instead.
|
|
145
|
+
*/
|
|
146
|
+
export class FinTsDialogAbortedError extends FinTsBankError {
|
|
147
|
+
override name = 'FinTsDialogAbortedError';
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/** The user cancelled the TAN entry or the approval in the banking app. */
|
|
151
|
+
export class FinTsTanCancelledError extends FinTsError {
|
|
152
|
+
override name = 'FinTsTanCancelledError';
|
|
153
|
+
|
|
154
|
+
constructor() {
|
|
155
|
+
super('TAN entry cancelled');
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* The user did not approve in the banking app in time: within the status requests the bank allows, 60 where it states
|
|
161
|
+
* none, and 10 minutes of polling at most.
|
|
162
|
+
*/
|
|
163
|
+
export class FinTsDecoupledTimeoutError extends FinTsError {
|
|
164
|
+
override name = 'FinTsDecoupledTimeoutError';
|
|
165
|
+
|
|
166
|
+
constructor() {
|
|
167
|
+
super('Approval on the separate device was not completed in time');
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* The client was closed, or called from inside one of its own callbacks, which would wait for itself.
|
|
173
|
+
*
|
|
174
|
+
* It does not extend `FinTsError`: it tells about the app's use of the client, not about the bank.
|
|
175
|
+
*/
|
|
176
|
+
export class FinTsStateError extends Error {
|
|
177
|
+
override name = 'FinTsStateError';
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/** The error of whatever a closed client is asked to do. */
|
|
181
|
+
export function clientClosedError(): FinTsStateError {
|
|
182
|
+
return new FinTsStateError('The FinTS client is closed');
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Return codes for a wrong or blocked PIN, from the FinTS return code list (Rückmeldungscodes): 9340 signature (PIN)
|
|
187
|
+
* wrong, 9910 PIN invalid or blocked, 9930 PIN blocked, 9931 blocked after failed attempts, 9942 PIN invalid.
|
|
188
|
+
*/
|
|
189
|
+
const pinErrors = new Set(['9340', '9910', '9930', '9931', '9942']);
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Return codes for a rejected TAN: 9941 TAN invalid, 9943 and 9991 TAN already used (the return code list and the
|
|
193
|
+
* PIN/TAN specification give different numbers), 9951 two-step timeout.
|
|
194
|
+
*
|
|
195
|
+
* The return code list gives 9941 and 9951 a second meaning, "Signatur ungültig" (invalid signature), which in PIN/TAN
|
|
196
|
+
* is the PIN; see `bankErrorFor`.
|
|
197
|
+
*/
|
|
198
|
+
const tanErrors = new Set(['9941', '9943', '9951', '9991']);
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Whether `f` reports a wrong or blocked PIN. Two of the codes stand for other faults as well, which the text tells:
|
|
202
|
+
* 9340 "Sicherheitsprofil unbekannt", 9910 "TAN-Reihe gesperrt" and "Chipkarte gesperrt".
|
|
203
|
+
*/
|
|
204
|
+
function isPinError(f: FinTsFeedback): boolean {
|
|
205
|
+
if (!f.isError || !pinErrors.has(f.code)) return false;
|
|
206
|
+
const text = f.text.toLowerCase();
|
|
207
|
+
switch (f.code) {
|
|
208
|
+
case '9340':
|
|
209
|
+
return !text.includes('profil');
|
|
210
|
+
case '9910':
|
|
211
|
+
return !text.includes('tan-reihe') && !text.includes('chipkarte');
|
|
212
|
+
default:
|
|
213
|
+
return true;
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* The most specific error for the errors in `feedback`, the answer to a message that `completesApproval` or not.
|
|
219
|
+
*
|
|
220
|
+
* Only the answer to a TAN or an approval status request can reject a TAN; anywhere else a TAN return code is a plain
|
|
221
|
+
* `FinTsBankError`. In answer to a login, 9941 or 9951 may reject the PIN, and the client must not take a refused login
|
|
222
|
+
* for a wrong TAN, after which it would send the PIN again.
|
|
223
|
+
*/
|
|
224
|
+
export function bankErrorFor(feedback: readonly FinTsFeedback[], completesApproval = false): FinTsBankError {
|
|
225
|
+
if (feedback.some(isPinError)) return new FinTsPinError(feedback);
|
|
226
|
+
if (completesApproval && feedback.some((f) => f.isError && tanErrors.has(f.code))) {
|
|
227
|
+
return new FinTsTanError(feedback);
|
|
228
|
+
}
|
|
229
|
+
if (endsDialog(feedback)) return new FinTsDialogAbortedError(feedback);
|
|
230
|
+
return new FinTsBankError(feedback);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* The refusal of a login whose answer carried `feedback`: `FinTsPinError` for a rejected PIN, else
|
|
235
|
+
* `FinTsLoginDataChangedError` for new login data, also next to other errors, and `FinTsLoginRefusedError` for anything
|
|
236
|
+
* else. The same for a refusal restored from the saved state, which keeps only `feedback`.
|
|
237
|
+
*
|
|
238
|
+
* New login data end the old ones for good, so the session takes care of no other reason the answer names.
|
|
239
|
+
*/
|
|
240
|
+
export function loginRefusalFor(feedback: readonly FinTsFeedback[]): FinTsLoginRefusedError {
|
|
241
|
+
if (feedback.some(isPinError)) return new FinTsPinError(feedback);
|
|
242
|
+
if (hasCode(feedback, FeedbackCodes.newLoginData)) return new FinTsLoginDataChangedError(feedback);
|
|
243
|
+
return new FinTsLoginRefusedError(feedback);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* Runs `parse` on data from the bank and reports malformed data as `FinTsProtocolError`, whose message leaves the data
|
|
248
|
+
* out (`FormatError`).
|
|
249
|
+
*/
|
|
250
|
+
export function parseBankData<T>(parse: () => T): T {
|
|
251
|
+
try {
|
|
252
|
+
return parse();
|
|
253
|
+
} catch (error) {
|
|
254
|
+
if (error instanceof FormatError) throw new FinTsProtocolError(`Malformed bank data: ${error.message}`);
|
|
255
|
+
throw error;
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* Runs `lookup`, which only completes data, such as the SEPA account list; `undefined` where the bank rejects it, so
|
|
261
|
+
* that the caller carries on without it. An aborted dialog counts as a rejection: banks end the dialog along with many.
|
|
262
|
+
* A refused login and a rejected TAN fail the call: the user must hear of them.
|
|
263
|
+
*/
|
|
264
|
+
export async function tryLookUp<T>(lookup: () => Promise<T>): Promise<T | undefined> {
|
|
265
|
+
try {
|
|
266
|
+
return await lookup();
|
|
267
|
+
} catch (error) {
|
|
268
|
+
if (
|
|
269
|
+
!(error instanceof FinTsBankError) ||
|
|
270
|
+
error instanceof FinTsLoginRefusedError ||
|
|
271
|
+
error instanceof FinTsTanError
|
|
272
|
+
) {
|
|
273
|
+
throw error;
|
|
274
|
+
}
|
|
275
|
+
return undefined;
|
|
276
|
+
}
|
|
277
|
+
}
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
/** A return code with text from the bank, for example `0020: Auftrag ausgeführt`. */
|
|
2
|
+
export class FinTsFeedback {
|
|
3
|
+
/** The four-digit return code; `0…` reports success, `3…` a warning and `9…` an error. */
|
|
4
|
+
readonly code: string;
|
|
5
|
+
|
|
6
|
+
/** The element of the request the code refers to, if the bank names one. */
|
|
7
|
+
readonly reference: string | undefined;
|
|
8
|
+
|
|
9
|
+
/** The bank's explanation of the code. */
|
|
10
|
+
readonly text: string;
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Values the bank adds to the code, such as the position to continue at: one character per byte the bank sent (ISO
|
|
14
|
+
* 8859-1, the FinTS character set), since the client and the app send them back as they came.
|
|
15
|
+
*/
|
|
16
|
+
readonly parameters: readonly string[];
|
|
17
|
+
|
|
18
|
+
constructor(init: { code: string; reference?: string | undefined; text: string; parameters?: readonly string[] }) {
|
|
19
|
+
this.code = init.code;
|
|
20
|
+
this.reference = init.reference;
|
|
21
|
+
this.text = init.text;
|
|
22
|
+
this.parameters = Object.freeze([...(init.parameters ?? [])]);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** Whether the code reports success. */
|
|
26
|
+
get isSuccess(): boolean {
|
|
27
|
+
return this.code.startsWith('0');
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** Whether the code is a warning. */
|
|
31
|
+
get isWarning(): boolean {
|
|
32
|
+
return this.code.startsWith('3');
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** Whether the code is an error. */
|
|
36
|
+
get isError(): boolean {
|
|
37
|
+
return this.code.startsWith('9');
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
toString(): string {
|
|
41
|
+
return `${this.code}: ${this.text}${this.parameters.length === 0 ? '' : ` [${this.parameters.join(', ')}]`}`;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** Return codes the client reacts to, as named in the FinTS return code list (Rückmeldungscodes). */
|
|
46
|
+
export const FeedbackCodes = Object.freeze({
|
|
47
|
+
/** Order received, strong authentication required (Auftrag empfangen – Sicherheitsfreigabe erforderlich). */
|
|
48
|
+
tanRequired: '0030',
|
|
49
|
+
/** More information available; the first parameter is where to continue (Es liegen weitere Informationen vor). */
|
|
50
|
+
moreData: '3040',
|
|
51
|
+
/**
|
|
52
|
+
* The bank has new login data for the user; the parameters are the user ID, the customer ID and the login name
|
|
53
|
+
* (Neue Anmeldedaten).
|
|
54
|
+
*/
|
|
55
|
+
newLoginData: '3072',
|
|
56
|
+
/** The one- and two-step methods the user may use, as parameters. */
|
|
57
|
+
allowedTanMethods: '3920',
|
|
58
|
+
/** Approval happens on another channel, such as a banking app (Sicherheitsfreigabe erfolgt über anderen Kanal). */
|
|
59
|
+
decoupledPending: '3955',
|
|
60
|
+
/** Strong authentication still pending (Starke Kundenauthentifizierung noch ausstehend). */
|
|
61
|
+
decoupledStillPending: '3956',
|
|
62
|
+
/** Wait for the push message (Auf Push-Nachricht warten). */
|
|
63
|
+
decoupledWaitForPush: '3957',
|
|
64
|
+
/** The bank ended the dialog (Dialog abgebrochen). */
|
|
65
|
+
dialogAborted: '9800',
|
|
66
|
+
/**
|
|
67
|
+
* The bank ended the dialog because it requires strong authentication (Dialog abgebrochen – starke Authentifizierung
|
|
68
|
+
* erforderlich).
|
|
69
|
+
*/
|
|
70
|
+
scaRequired: '9075',
|
|
71
|
+
/** The customer system ID is not accepted any more; a synchronization delivers a new one (Neue Kundensystem-ID holen). */
|
|
72
|
+
newSystemIdRequired: '9391',
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* The return codes with which the bank ends the dialog ("Dialog abgebrochen" in the return code list): 9800 and 9075,
|
|
77
|
+
* and 9077 (hardened browser required), 9078 (product not registered) and 9361 (security procedure no longer allowed).
|
|
78
|
+
*/
|
|
79
|
+
const dialogEnding = new Set<string>([FeedbackCodes.dialogAborted, FeedbackCodes.scaRequired, '9077', '9078', '9361']);
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* The warnings about the protocol, which the client acts on itself and the user has no use for: paging, the allowed TAN
|
|
83
|
+
* methods, the steps of an approval and 3076 (no strong authentication needed), new login data, which fail the call,
|
|
84
|
+
* and 3060 (some warnings), which only announces the others.
|
|
85
|
+
*/
|
|
86
|
+
const protocolWarnings = new Set<string>([
|
|
87
|
+
FeedbackCodes.moreData,
|
|
88
|
+
'3060',
|
|
89
|
+
FeedbackCodes.newLoginData,
|
|
90
|
+
'3076',
|
|
91
|
+
FeedbackCodes.allowedTanMethods,
|
|
92
|
+
FeedbackCodes.decoupledPending,
|
|
93
|
+
FeedbackCodes.decoupledStillPending,
|
|
94
|
+
FeedbackCodes.decoupledWaitForPush,
|
|
95
|
+
]);
|
|
96
|
+
|
|
97
|
+
// Reading return codes the way the session logic needs; not part of the public API.
|
|
98
|
+
|
|
99
|
+
/** Whether a return code `code` is among the `feedback`. */
|
|
100
|
+
export function hasCode(feedback: readonly FinTsFeedback[], code: string): boolean {
|
|
101
|
+
return feedback.some((f) => f.code === code);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** Whether any return code is an error. */
|
|
105
|
+
export function hasErrors(feedback: readonly FinTsFeedback[]): boolean {
|
|
106
|
+
return feedback.some((f) => f.isError);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/** The positions to continue at that the bank names with return code 3040; empty on the last page. */
|
|
110
|
+
export function continuations(feedback: readonly FinTsFeedback[]): Set<string> {
|
|
111
|
+
const positions = new Set<string>();
|
|
112
|
+
for (const f of feedback) {
|
|
113
|
+
const position = f.parameters[0];
|
|
114
|
+
if (f.code === FeedbackCodes.moreData && position !== undefined && position !== '') positions.add(position);
|
|
115
|
+
}
|
|
116
|
+
return positions;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* The security functions of the TAN methods the user may use, as the bank lists them with return code 3920, in all such
|
|
121
|
+
* return codes; empty if it lists none.
|
|
122
|
+
*/
|
|
123
|
+
export function allowedTanMethodsIn(feedback: readonly FinTsFeedback[]): string[] {
|
|
124
|
+
const codes = new Set<string>();
|
|
125
|
+
for (const f of feedback) {
|
|
126
|
+
if (f.code !== FeedbackCodes.allowedTanMethods) continue;
|
|
127
|
+
for (const code of f.parameters) if (code !== '') codes.add(code);
|
|
128
|
+
}
|
|
129
|
+
return [...codes];
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/** Whether the bank ended the dialog with one of its return codes for that. */
|
|
133
|
+
export function endsDialog(feedback: readonly FinTsFeedback[]): boolean {
|
|
134
|
+
return feedback.some((f) => dialogEnding.has(f.code));
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/** The warnings for the user: all but those about the protocol. */
|
|
138
|
+
export function userWarnings(feedback: readonly FinTsFeedback[]): FinTsFeedback[] {
|
|
139
|
+
return feedback.filter((f) => f.isWarning && !protocolWarnings.has(f.code));
|
|
140
|
+
}
|