@coderoftime/fints 0.1.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +519 -1
- package/README.md +138 -40
- package/dist/client.d.ts +49 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +242 -0
- package/dist/client.js.map +1 -0
- package/dist/core/account.d.ts +46 -7
- package/dist/core/account.d.ts.map +1 -1
- package/dist/core/account.js +143 -24
- package/dist/core/account.js.map +1 -1
- package/dist/core/amount.d.ts +15 -4
- package/dist/core/amount.d.ts.map +1 -1
- package/dist/core/amount.js +46 -9
- package/dist/core/amount.js.map +1 -1
- package/dist/core/balance.d.ts +4 -0
- package/dist/core/balance.d.ts.map +1 -1
- package/dist/core/balance.js +11 -5
- package/dist/core/balance.js.map +1 -1
- package/dist/core/bank-info.d.ts +21 -4
- package/dist/core/bank-info.d.ts.map +1 -1
- package/dist/core/bank-info.js +26 -5
- package/dist/core/bank-info.js.map +1 -1
- package/dist/core/client.d.ts +81 -19
- package/dist/core/client.d.ts.map +1 -1
- package/dist/core/config.d.ts +40 -20
- package/dist/core/config.d.ts.map +1 -1
- package/dist/core/config.js +85 -30
- package/dist/core/config.js.map +1 -1
- package/dist/core/errors.d.ts +100 -26
- package/dist/core/errors.d.ts.map +1 -1
- package/dist/core/errors.js +136 -53
- package/dist/core/errors.js.map +1 -1
- package/dist/core/feedback.d.ts +11 -1
- package/dist/core/feedback.d.ts.map +1 -1
- package/dist/core/feedback.js +14 -2
- package/dist/core/feedback.js.map +1 -1
- package/dist/core/transaction.d.ts +11 -1
- package/dist/core/transaction.d.ts.map +1 -1
- package/dist/core/transaction.js +21 -4
- package/dist/core/transaction.js.map +1 -1
- package/dist/core/transport.d.ts +48 -6
- package/dist/core/transport.d.ts.map +1 -1
- package/dist/core/transport.js +126 -29
- package/dist/core/transport.js.map +1 -1
- package/dist/formats/camt.d.ts +28 -5
- package/dist/formats/camt.d.ts.map +1 -1
- package/dist/formats/camt.js +132 -50
- package/dist/formats/camt.js.map +1 -1
- package/dist/formats/mt940.d.ts +29 -5
- package/dist/formats/mt940.d.ts.map +1 -1
- package/dist/formats/mt940.js +209 -113
- package/dist/formats/mt940.js.map +1 -1
- package/dist/formats/readers.d.ts +6 -2
- package/dist/formats/readers.d.ts.map +1 -1
- package/dist/formats/readers.js +28 -29
- package/dist/formats/readers.js.map +1 -1
- package/dist/formats/record-budget.d.ts +7 -0
- package/dist/formats/record-budget.d.ts.map +1 -0
- package/dist/formats/record-budget.js +17 -0
- package/dist/formats/record-budget.js.map +1 -0
- package/dist/formats/sepa-purpose.d.ts +9 -0
- package/dist/formats/sepa-purpose.d.ts.map +1 -1
- package/dist/formats/sepa-purpose.js +32 -2
- package/dist/formats/sepa-purpose.js.map +1 -1
- package/dist/index.d.ts +3 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -2
- package/dist/index.js.map +1 -1
- package/dist/session/app-approval.d.ts +11 -5
- package/dist/session/app-approval.d.ts.map +1 -1
- package/dist/session/app-approval.js +23 -12
- package/dist/session/app-approval.js.map +1 -1
- package/dist/session/dialog-runner.d.ts +11 -11
- package/dist/session/dialog-runner.d.ts.map +1 -1
- package/dist/session/dialog-runner.js +34 -19
- package/dist/session/dialog-runner.js.map +1 -1
- package/dist/session/login-guard.d.ts +54 -49
- package/dist/session/login-guard.d.ts.map +1 -1
- package/dist/session/login-guard.js +170 -83
- package/dist/session/login-guard.js.map +1 -1
- package/dist/session/sepa-accounts.d.ts +20 -14
- package/dist/session/sepa-accounts.d.ts.map +1 -1
- package/dist/session/sepa-accounts.js +62 -46
- package/dist/session/sepa-accounts.js.map +1 -1
- package/dist/session/session-setup.d.ts +41 -24
- package/dist/session/session-setup.d.ts.map +1 -1
- package/dist/session/session-setup.js +106 -63
- package/dist/session/session-setup.js.map +1 -1
- package/dist/session/session-state.d.ts +10 -6
- package/dist/session/session-state.d.ts.map +1 -1
- package/dist/session/session-state.js +72 -18
- package/dist/session/session-state.js.map +1 -1
- package/dist/session/signed-dialog.d.ts +83 -0
- package/dist/session/signed-dialog.d.ts.map +1 -0
- package/dist/session/signed-dialog.js +185 -0
- package/dist/session/signed-dialog.js.map +1 -0
- package/dist/session/tan-selection.d.ts +26 -18
- package/dist/session/tan-selection.d.ts.map +1 -1
- package/dist/session/tan-selection.js +44 -33
- package/dist/session/tan-selection.js.map +1 -1
- package/dist/session/user-prompts.d.ts +14 -5
- package/dist/session/user-prompts.d.ts.map +1 -1
- package/dist/session/user-prompts.js +53 -18
- package/dist/session/user-prompts.js.map +1 -1
- package/dist/tan/challenge.d.ts +47 -24
- package/dist/tan/challenge.d.ts.map +1 -1
- package/dist/tan/challenge.js +56 -51
- package/dist/tan/challenge.js.map +1 -1
- package/dist/tan/flicker.d.ts +11 -1
- package/dist/tan/flicker.d.ts.map +1 -1
- package/dist/tan/flicker.js +70 -24
- package/dist/tan/flicker.js.map +1 -1
- package/dist/tan/tan-image.d.ts +6 -2
- package/dist/tan/tan-image.d.ts.map +1 -1
- package/dist/tan/tan-image.js +14 -6
- package/dist/tan/tan-image.js.map +1 -1
- package/dist/tan/tan-models.d.ts +14 -2
- package/dist/tan/tan-models.d.ts.map +1 -1
- package/dist/tan/tan-models.js +24 -7
- package/dist/tan/tan-models.js.map +1 -1
- package/dist/util/arguments.d.ts +13 -0
- package/dist/util/arguments.d.ts.map +1 -0
- package/dist/util/arguments.js +40 -0
- package/dist/util/arguments.js.map +1 -0
- package/dist/util/async.d.ts +4 -3
- package/dist/util/async.d.ts.map +1 -1
- package/dist/util/async.js +6 -7
- package/dist/util/async.js.map +1 -1
- package/dist/util/bytes.d.ts +3 -4
- package/dist/util/bytes.d.ts.map +1 -1
- package/dist/util/bytes.js +13 -21
- package/dist/util/bytes.js.map +1 -1
- package/dist/util/call-context.d.ts.map +1 -1
- package/dist/util/call-context.js +3 -2
- package/dist/util/call-context.js.map +1 -1
- package/dist/util/dates.d.ts +6 -20
- package/dist/util/dates.d.ts.map +1 -1
- package/dist/util/dates.js +58 -28
- package/dist/util/dates.js.map +1 -1
- package/dist/util/errors.d.ts +13 -3
- package/dist/util/errors.d.ts.map +1 -1
- package/dist/util/errors.js +19 -6
- package/dist/util/errors.js.map +1 -1
- package/dist/util/immutable-map.d.ts +5 -2
- package/dist/util/immutable-map.d.ts.map +1 -1
- package/dist/util/immutable-map.js +15 -12
- package/dist/util/immutable-map.js.map +1 -1
- package/dist/util/inspect.d.ts +11 -0
- package/dist/util/inspect.d.ts.map +1 -0
- package/dist/util/inspect.js +14 -0
- package/dist/util/inspect.js.map +1 -0
- package/dist/util/json.d.ts +4 -0
- package/dist/util/json.d.ts.map +1 -1
- package/dist/util/json.js +18 -0
- package/dist/util/json.js.map +1 -1
- package/dist/util/text.d.ts +38 -5
- package/dist/util/text.d.ts.map +1 -1
- package/dist/util/text.js +90 -16
- package/dist/util/text.js.map +1 -1
- package/dist/util/xml.d.ts +9 -6
- package/dist/util/xml.d.ts.map +1 -1
- package/dist/util/xml.js +129 -56
- package/dist/util/xml.js.map +1 -1
- package/dist/v3/account-extension.d.ts.map +1 -1
- package/dist/v3/account-extension.js +7 -10
- package/dist/v3/account-extension.js.map +1 -1
- package/dist/v3/bank-connection.d.ts +54 -0
- package/dist/v3/bank-connection.d.ts.map +1 -0
- package/dist/v3/bank-connection.js +118 -0
- package/dist/v3/bank-connection.js.map +1 -0
- package/dist/v3/dialog.d.ts +84 -46
- package/dist/v3/dialog.d.ts.map +1 -1
- package/dist/v3/dialog.js +174 -60
- package/dist/v3/dialog.js.map +1 -1
- package/dist/v3/message.d.ts +32 -11
- package/dist/v3/message.d.ts.map +1 -1
- package/dist/v3/message.js +144 -83
- package/dist/v3/message.js.map +1 -1
- package/dist/v3/parameter-store.d.ts +111 -0
- package/dist/v3/parameter-store.d.ts.map +1 -0
- package/dist/v3/parameter-store.js +197 -0
- package/dist/v3/parameter-store.js.map +1 -0
- package/dist/v3/parameters.d.ts +44 -17
- package/dist/v3/parameters.d.ts.map +1 -1
- package/dist/v3/parameters.js +136 -60
- package/dist/v3/parameters.js.map +1 -1
- package/dist/v3/protocol.d.ts +50 -50
- package/dist/v3/protocol.d.ts.map +1 -1
- package/dist/v3/protocol.js +91 -210
- package/dist/v3/protocol.js.map +1 -1
- package/dist/v3/queries.d.ts +51 -0
- package/dist/v3/queries.d.ts.map +1 -0
- package/dist/v3/queries.js +128 -0
- package/dist/v3/queries.js.map +1 -0
- package/dist/v3/segments.d.ts +43 -19
- package/dist/v3/segments.d.ts.map +1 -1
- package/dist/v3/segments.js +84 -33
- package/dist/v3/segments.js.map +1 -1
- package/dist/v3/syntax.d.ts +68 -13
- package/dist/v3/syntax.d.ts.map +1 -1
- package/dist/v3/syntax.js +183 -102
- package/dist/v3/syntax.js.map +1 -1
- package/package.json +10 -6
- package/src/client.ts +293 -0
- package/src/core/account.ts +153 -24
- package/src/core/amount.ts +45 -10
- package/src/core/balance.ts +12 -5
- package/src/core/bank-info.ts +29 -5
- package/src/core/client.ts +83 -19
- package/src/core/config.ts +95 -35
- package/src/core/errors.ts +148 -54
- package/src/core/feedback.ts +16 -2
- package/src/core/transaction.ts +22 -4
- package/src/core/transport.ts +153 -32
- package/src/formats/camt.ts +153 -45
- package/src/formats/mt940.ts +244 -122
- package/src/formats/readers.ts +28 -31
- package/src/formats/record-budget.ts +18 -0
- package/src/formats/sepa-purpose.ts +29 -2
- package/src/index.ts +4 -2
- package/src/session/app-approval.ts +51 -38
- package/src/session/dialog-runner.ts +61 -31
- package/src/session/login-guard.ts +189 -87
- package/src/session/sepa-accounts.ts +63 -45
- package/src/session/session-setup.ts +105 -68
- package/src/session/session-state.ts +74 -23
- package/src/session/signed-dialog.ts +222 -0
- package/src/session/tan-selection.ts +50 -39
- package/src/session/user-prompts.ts +62 -18
- package/src/tan/challenge.ts +87 -64
- package/src/tan/flicker.ts +72 -23
- package/src/tan/tan-image.ts +15 -6
- package/src/tan/tan-models.ts +26 -7
- package/src/util/arguments.ts +40 -0
- package/src/util/async.ts +10 -9
- package/src/util/bytes.ts +14 -20
- package/src/util/call-context.ts +3 -2
- package/src/util/dates.ts +64 -33
- package/src/util/errors.ts +22 -6
- package/src/util/immutable-map.ts +15 -12
- package/src/util/inspect.ts +14 -0
- package/src/util/json.ts +16 -0
- package/src/util/text.ts +87 -15
- package/src/util/xml.ts +130 -52
- package/src/v3/account-extension.ts +7 -12
- package/src/v3/bank-connection.ts +126 -0
- package/src/v3/dialog.ts +186 -92
- package/src/v3/message.ts +140 -79
- package/src/v3/parameter-store.ts +226 -0
- package/src/v3/parameters.ts +166 -66
- package/src/v3/protocol.ts +103 -253
- package/src/v3/queries.ts +198 -0
- package/src/v3/segments.ts +86 -31
- package/src/v3/syntax.ts +172 -93
- package/dist/session/bank-connection.d.ts +0 -28
- package/dist/session/bank-connection.d.ts.map +0 -1
- package/dist/session/bank-connection.js +0 -48
- package/dist/session/bank-connection.js.map +0 -1
- package/dist/session/parameter-data.d.ts +0 -83
- package/dist/session/parameter-data.d.ts.map +0 -1
- package/dist/session/parameter-data.js +0 -68
- package/dist/session/parameter-data.js.map +0 -1
- package/dist/session/pin-tan-client.d.ts +0 -54
- package/dist/session/pin-tan-client.d.ts.map +0 -1
- package/dist/session/pin-tan-client.js +0 -153
- package/dist/session/pin-tan-client.js.map +0 -1
- package/dist/session/pin-tan-dialog.d.ts +0 -71
- package/dist/session/pin-tan-dialog.d.ts.map +0 -1
- package/dist/session/pin-tan-dialog.js +0 -124
- package/dist/session/pin-tan-dialog.js.map +0 -1
- package/dist/session/protocol.d.ts +0 -209
- package/dist/session/protocol.d.ts.map +0 -1
- package/dist/session/protocol.js +0 -75
- package/dist/session/protocol.js.map +0 -1
- package/dist/session/response-handler.d.ts +0 -48
- package/dist/session/response-handler.d.ts.map +0 -1
- package/dist/session/response-handler.js +0 -121
- package/dist/session/response-handler.js.map +0 -1
- package/dist/v3/client.d.ts +0 -24
- package/dist/v3/client.d.ts.map +0 -1
- package/dist/v3/client.js +0 -16
- package/dist/v3/client.js.map +0 -1
- package/src/session/bank-connection.ts +0 -51
- package/src/session/parameter-data.ts +0 -137
- package/src/session/pin-tan-client.ts +0 -186
- package/src/session/pin-tan-dialog.ts +0 -160
- package/src/session/protocol.ts +0 -311
- package/src/session/response-handler.ts +0 -130
- package/src/v3/client.ts +0 -29
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
import { invalidArgument } from '../util/errors.js';
|
|
2
|
+
import { trimBlanks } from '../util/text.js';
|
|
3
|
+
|
|
1
4
|
/** SEPA keywords German banks put into the purpose text, each followed by `+`. */
|
|
2
5
|
const keywords = [
|
|
3
6
|
'EREF',
|
|
@@ -26,6 +29,9 @@ export function providedReference(reference: string | undefined): string | undef
|
|
|
26
29
|
|
|
27
30
|
const keywordPattern = new RegExp(`(${keywords.join('|')})\\+`, 'g');
|
|
28
31
|
|
|
32
|
+
/** The keywords of the text the payer or the payee writes, which German banks put after every other keyword. */
|
|
33
|
+
const payerText = new Set(['SVWZ', 'ABWA', 'ABWE']);
|
|
34
|
+
|
|
29
35
|
/** Parser for the SEPA keywords in a purpose text. */
|
|
30
36
|
export const SepaPurpose = Object.freeze({
|
|
31
37
|
/**
|
|
@@ -34,6 +40,13 @@ export const SepaPurpose = Object.freeze({
|
|
|
34
40
|
* A keyword counts unless a capital letter comes right before it, so `XEREF+` does not open an EREF value. After a
|
|
35
41
|
* digit or a small letter it does: a purpose line cut at 35 characters is joined to the next one without a space, so
|
|
36
42
|
* `…4711EREF+` may be where one line ends and the next begins.
|
|
43
|
+
*
|
|
44
|
+
* The remittance information (`SVWZ+`) and the ultimate debtor and creditor (`ABWA+`, `ABWE+`) are text the payer or
|
|
45
|
+
* the payee writes, which banks put after every other keyword, some of them `ABWA+` or `ABWE+` first. So from the
|
|
46
|
+
* first of them on only these three start a value: any other keyword in that text, such as `IBAN+`, is part of it. A
|
|
47
|
+
* keyword that occurs twice has no value: which of the two the bank wrote cannot be told, since a reference the payee
|
|
48
|
+
* chose, such as `EREF+`, comes before the bank's `CRED+` and may hold `CRED+…` and `SVWZ+`, which turns the bank's
|
|
49
|
+
* value into remittance information.
|
|
37
50
|
*/
|
|
38
51
|
parse(purpose: string): Map<string, string> {
|
|
39
52
|
return split(purpose, new Set());
|
|
@@ -45,11 +58,15 @@ export const SepaPurpose = Object.freeze({
|
|
|
45
58
|
*
|
|
46
59
|
* A keyword also counts at the start of a part, where the previous value may end in a capital letter, as in
|
|
47
60
|
* `EREF+NOTPROVIDED` followed by a part `MREF+…`.
|
|
61
|
+
*
|
|
62
|
+
* Throws `TypeError` for a string, whose characters would count as parts, and for parts that are no strings.
|
|
48
63
|
*/
|
|
49
64
|
parseParts(parts: Iterable<string>): Map<string, string> {
|
|
65
|
+
if (typeof parts === 'string') throw invalidArgument('Must be the parts, not one string', 'parts');
|
|
50
66
|
let text = '';
|
|
51
67
|
const partStarts = new Set<number>();
|
|
52
68
|
for (const part of parts) {
|
|
69
|
+
if (typeof part !== 'string') throw invalidArgument('Must be strings', 'parts');
|
|
53
70
|
partStarts.add(text.length);
|
|
54
71
|
text += part;
|
|
55
72
|
}
|
|
@@ -60,11 +77,21 @@ export const SepaPurpose = Object.freeze({
|
|
|
60
77
|
function split(purpose: string, partStarts: ReadonlySet<number>): Map<string, string> {
|
|
61
78
|
const atWordStart = (index: number): boolean =>
|
|
62
79
|
index === 0 || partStarts.has(index) || !isCapitalLetter(purpose, index - 1);
|
|
63
|
-
const
|
|
80
|
+
const occurrences = new Map<string, number>();
|
|
81
|
+
const matches: RegExpExecArray[] = [];
|
|
82
|
+
let inPayerText = false;
|
|
83
|
+
for (const m of purpose.matchAll(keywordPattern)) {
|
|
84
|
+
if (!atWordStart(m.index)) continue;
|
|
85
|
+
const keyword = m[1]!;
|
|
86
|
+
occurrences.set(keyword, (occurrences.get(keyword) ?? 0) + 1);
|
|
87
|
+
if (inPayerText && !payerText.has(keyword)) continue;
|
|
88
|
+
inPayerText ||= payerText.has(keyword);
|
|
89
|
+
matches.push(m);
|
|
90
|
+
}
|
|
64
91
|
const result = new Map<string, string>();
|
|
65
92
|
matches.forEach((m, i) => {
|
|
66
93
|
const end = i + 1 < matches.length ? matches[i + 1]!.index : purpose.length;
|
|
67
|
-
result.set(m[1]!, purpose.substring(m.index + m[0].length, end)
|
|
94
|
+
if (occurrences.get(m[1]!) === 1) result.set(m[1]!, trimBlanks(purpose.substring(m.index + m[0].length, end)));
|
|
68
95
|
});
|
|
69
96
|
return result;
|
|
70
97
|
}
|
package/src/index.ts
CHANGED
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
* @packageDocumentation
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
+
export { FinTs3Client, type FinTs3ClientInit } from './client.js';
|
|
10
11
|
export { Account, AccountDataStatus, type AccountCategory, type AccountInit } from './core/account.js';
|
|
11
12
|
export { Amount } from './core/amount.js';
|
|
12
13
|
export { Balance, type BalanceInit } from './core/balance.js';
|
|
@@ -27,11 +28,13 @@ export {
|
|
|
27
28
|
FinTsDialogAbortedError,
|
|
28
29
|
FinTsError,
|
|
29
30
|
FinTsLoginDataChangedError,
|
|
31
|
+
FinTsLoginRefusedError,
|
|
30
32
|
FinTsPinError,
|
|
31
33
|
FinTsProtocolError,
|
|
32
34
|
FinTsStateError,
|
|
33
35
|
FinTsTanCancelledError,
|
|
34
36
|
FinTsTanError,
|
|
37
|
+
FinTsTanSetupError,
|
|
35
38
|
FinTsTransportError,
|
|
36
39
|
FinTsUnsupportedError,
|
|
37
40
|
} from './core/errors.js';
|
|
@@ -43,7 +46,7 @@ export {
|
|
|
43
46
|
type TransactionStatus,
|
|
44
47
|
} from './core/transaction.js';
|
|
45
48
|
export { HttpTransport, type FinTsTransport, type HttpTransportOptions } from './core/transport.js';
|
|
46
|
-
export { Camt } from './formats/camt.js';
|
|
49
|
+
export { Camt, type CamtStatement } from './formats/camt.js';
|
|
47
50
|
export { Mt940, type Mt940Options, type Mt940Statement } from './formats/mt940.js';
|
|
48
51
|
export { SepaPurpose } from './formats/sepa-purpose.js';
|
|
49
52
|
export {
|
|
@@ -65,4 +68,3 @@ export {
|
|
|
65
68
|
type TanMethodInit,
|
|
66
69
|
} from './tan/tan-models.js';
|
|
67
70
|
export { FormatError } from './util/errors.js';
|
|
68
|
-
export { FinTs3Client, type FinTs3ClientInit } from './v3/client.js';
|
|
@@ -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,26 @@
|
|
|
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 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
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Most data one query may hold, summed over its pages as `IncomingMessage3.size` measures it: the transport and the
|
|
21
|
+
* parser bound a single response only. The statements and transactions read from them have a budget of their own
|
|
22
|
+
* (`RecordBudget`).
|
|
23
|
+
*/
|
|
12
24
|
const maxQueryMebibytes = 128;
|
|
13
25
|
|
|
14
26
|
/**
|
|
@@ -18,17 +30,13 @@ const maxQueryMebibytes = 128;
|
|
|
18
30
|
* client does not repeat the call by itself, since the bank's answer cannot tell a dialog that had ended before from an
|
|
19
31
|
* order it refuses along with ending the dialog, and a new login may cost the user another approval.
|
|
20
32
|
*/
|
|
21
|
-
export class DialogRunner
|
|
22
|
-
private dialog:
|
|
33
|
+
export class DialogRunner {
|
|
34
|
+
private dialog: SignedDialog | undefined;
|
|
23
35
|
|
|
24
|
-
constructor(
|
|
25
|
-
private readonly setup: SessionSetup<R, O>,
|
|
26
|
-
private readonly responses: ResponseHandler<R>,
|
|
27
|
-
private readonly prompts: UserPrompts,
|
|
28
|
-
) {}
|
|
36
|
+
constructor(private readonly setup: SessionSetup) {}
|
|
29
37
|
|
|
30
38
|
/** The open dialog, or a new one once the setup is complete and the user has passed strong authentication. */
|
|
31
|
-
async open(): Promise<
|
|
39
|
+
async open(): Promise<SignedDialog> {
|
|
32
40
|
const current = this.dialog;
|
|
33
41
|
if (current?.isActive === true) return current;
|
|
34
42
|
this.dialog = undefined;
|
|
@@ -37,10 +45,10 @@ export class DialogRunner<R extends BankResponse, O> {
|
|
|
37
45
|
return dialog;
|
|
38
46
|
}
|
|
39
47
|
|
|
40
|
-
private async login(): Promise<
|
|
48
|
+
private async login(): Promise<SignedDialog> {
|
|
41
49
|
const dialog = await this.setup.newDialog();
|
|
42
50
|
try {
|
|
43
|
-
await
|
|
51
|
+
await dialog.login();
|
|
44
52
|
return dialog;
|
|
45
53
|
} catch (error) {
|
|
46
54
|
// After a refused login the dialog is discarded already, and ending it sends nothing. After any other failure,
|
|
@@ -62,37 +70,64 @@ export class DialogRunner<R extends BankResponse, O> {
|
|
|
62
70
|
*
|
|
63
71
|
* The query is built once the dialog is open, since building it takes the parameter data.
|
|
64
72
|
*/
|
|
65
|
-
async query<T>(build: () => Query<
|
|
73
|
+
async query<T>(build: () => Query<T>): Promise<T> {
|
|
66
74
|
const dialog = await this.open();
|
|
67
75
|
const query = build();
|
|
68
76
|
const pages = await this.fetchPages(dialog, query.order);
|
|
69
77
|
return parseBankData(() => query.read(pages));
|
|
70
78
|
}
|
|
71
79
|
|
|
72
|
-
/** Sends `order` in `dialog` and completes its approval. */
|
|
73
|
-
private async run(
|
|
74
|
-
|
|
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 await
|
|
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
|
-
/**
|
|
87
|
-
|
|
88
|
-
|
|
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
|
+
*/
|
|
101
|
+
private async fetchPages(
|
|
102
|
+
dialog: SignedDialog,
|
|
103
|
+
order: (continuation: string | undefined) => SegmentSpec,
|
|
104
|
+
): Promise<IncomingMessage3[]> {
|
|
105
|
+
const pages: IncomingMessage3[] = [];
|
|
89
106
|
const seen = new Set<string>();
|
|
90
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
|
+
};
|
|
91
117
|
let continuation: string | undefined;
|
|
92
118
|
for (;;) {
|
|
93
|
-
const page = await this.run(
|
|
119
|
+
const page = await this.run(
|
|
120
|
+
dialog,
|
|
121
|
+
order(continuation),
|
|
122
|
+
continuation === undefined ? undefined : admitPageApproval,
|
|
123
|
+
);
|
|
94
124
|
pages.push(page);
|
|
95
125
|
size += page.size;
|
|
126
|
+
if (size > maxQueryMebibytes * 1024 * 1024) {
|
|
127
|
+
throw new FinTsProtocolError(
|
|
128
|
+
`The bank sends more than ${maxQueryMebibytes} MiB for one query; ask for a shorter period`,
|
|
129
|
+
);
|
|
130
|
+
}
|
|
96
131
|
const positions = [...continuations(page.feedback)];
|
|
97
132
|
// A message carries one order, so the bank names one position at most.
|
|
98
133
|
if (positions.length > 1) throw new FinTsProtocolError('The bank names several positions to continue at');
|
|
@@ -104,16 +139,11 @@ export class DialogRunner<R extends BankResponse, O> {
|
|
|
104
139
|
}
|
|
105
140
|
return pages;
|
|
106
141
|
}
|
|
107
|
-
// A repeated position or endless pages would keep the client busy forever
|
|
142
|
+
// A repeated position or endless pages would keep the client busy forever.
|
|
108
143
|
if (seen.has(continuation) || pages.length === maxPages) {
|
|
109
144
|
throw new FinTsProtocolError('The bank does not stop sending pages');
|
|
110
145
|
}
|
|
111
146
|
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
147
|
}
|
|
118
148
|
}
|
|
119
149
|
}
|