@coderoftime/fints 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +142 -1
- package/README.md +127 -37
- package/dist/client.d.ts +48 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +199 -0
- package/dist/client.js.map +1 -0
- package/dist/core/account.d.ts +35 -7
- package/dist/core/account.d.ts.map +1 -1
- package/dist/core/account.js +84 -13
- package/dist/core/account.js.map +1 -1
- package/dist/core/amount.d.ts +2 -1
- package/dist/core/amount.d.ts.map +1 -1
- package/dist/core/amount.js +4 -1
- package/dist/core/amount.js.map +1 -1
- package/dist/core/balance.d.ts +1 -0
- package/dist/core/balance.d.ts.map +1 -1
- package/dist/core/balance.js +6 -5
- package/dist/core/balance.js.map +1 -1
- package/dist/core/bank-info.d.ts +1 -1
- package/dist/core/bank-info.js +1 -1
- package/dist/core/client.d.ts +50 -15
- package/dist/core/client.d.ts.map +1 -1
- package/dist/core/config.d.ts +28 -16
- package/dist/core/config.d.ts.map +1 -1
- package/dist/core/config.js +66 -26
- package/dist/core/config.js.map +1 -1
- package/dist/core/errors.d.ts +85 -23
- package/dist/core/errors.d.ts.map +1 -1
- package/dist/core/errors.js +112 -47
- package/dist/core/errors.js.map +1 -1
- package/dist/core/feedback.d.ts +4 -1
- package/dist/core/feedback.d.ts.map +1 -1
- package/dist/core/feedback.js +4 -1
- package/dist/core/feedback.js.map +1 -1
- package/dist/core/transaction.d.ts +6 -1
- package/dist/core/transaction.d.ts.map +1 -1
- package/dist/core/transaction.js +9 -4
- package/dist/core/transaction.js.map +1 -1
- package/dist/core/transport.d.ts +33 -6
- package/dist/core/transport.d.ts.map +1 -1
- package/dist/core/transport.js +77 -20
- package/dist/core/transport.js.map +1 -1
- package/dist/formats/camt.d.ts +24 -4
- package/dist/formats/camt.d.ts.map +1 -1
- package/dist/formats/camt.js +124 -41
- package/dist/formats/camt.js.map +1 -1
- package/dist/formats/mt940.d.ts +15 -3
- package/dist/formats/mt940.d.ts.map +1 -1
- package/dist/formats/mt940.js +161 -82
- package/dist/formats/mt940.js.map +1 -1
- package/dist/formats/readers.d.ts +6 -2
- package/dist/formats/readers.d.ts.map +1 -1
- package/dist/formats/readers.js +24 -29
- package/dist/formats/readers.js.map +1 -1
- package/dist/formats/record-budget.d.ts +7 -0
- package/dist/formats/record-budget.d.ts.map +1 -0
- package/dist/formats/record-budget.js +17 -0
- package/dist/formats/record-budget.js.map +1 -0
- package/dist/formats/sepa-purpose.d.ts +6 -0
- package/dist/formats/sepa-purpose.d.ts.map +1 -1
- package/dist/formats/sepa-purpose.js +23 -2
- package/dist/formats/sepa-purpose.js.map +1 -1
- package/dist/index.d.ts +3 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -2
- package/dist/index.js.map +1 -1
- package/dist/session/app-approval.d.ts +11 -5
- package/dist/session/app-approval.d.ts.map +1 -1
- package/dist/session/app-approval.js +23 -12
- package/dist/session/app-approval.js.map +1 -1
- package/dist/session/dialog-runner.d.ts +7 -10
- package/dist/session/dialog-runner.d.ts.map +1 -1
- package/dist/session/dialog-runner.js +15 -16
- package/dist/session/dialog-runner.js.map +1 -1
- package/dist/session/login-guard.d.ts +54 -49
- package/dist/session/login-guard.d.ts.map +1 -1
- package/dist/session/login-guard.js +156 -84
- package/dist/session/login-guard.js.map +1 -1
- package/dist/session/sepa-accounts.d.ts +20 -14
- package/dist/session/sepa-accounts.d.ts.map +1 -1
- package/dist/session/sepa-accounts.js +62 -46
- package/dist/session/sepa-accounts.js.map +1 -1
- package/dist/session/session-setup.d.ts +33 -25
- package/dist/session/session-setup.d.ts.map +1 -1
- package/dist/session/session-setup.js +87 -65
- package/dist/session/session-setup.js.map +1 -1
- package/dist/session/session-state.d.ts +9 -6
- package/dist/session/session-state.d.ts.map +1 -1
- package/dist/session/session-state.js +52 -17
- package/dist/session/session-state.js.map +1 -1
- package/dist/session/signed-dialog.d.ts +77 -0
- package/dist/session/signed-dialog.d.ts.map +1 -0
- package/dist/session/signed-dialog.js +170 -0
- package/dist/session/signed-dialog.js.map +1 -0
- package/dist/session/tan-selection.d.ts +26 -18
- package/dist/session/tan-selection.d.ts.map +1 -1
- package/dist/session/tan-selection.js +44 -33
- package/dist/session/tan-selection.js.map +1 -1
- package/dist/session/user-prompts.d.ts +8 -4
- package/dist/session/user-prompts.d.ts.map +1 -1
- package/dist/session/user-prompts.js +34 -14
- package/dist/session/user-prompts.js.map +1 -1
- package/dist/tan/challenge.d.ts +34 -13
- package/dist/tan/challenge.d.ts.map +1 -1
- package/dist/tan/challenge.js +25 -26
- package/dist/tan/challenge.js.map +1 -1
- package/dist/tan/flicker.d.ts +7 -1
- package/dist/tan/flicker.d.ts.map +1 -1
- package/dist/tan/flicker.js +40 -6
- package/dist/tan/flicker.js.map +1 -1
- package/dist/tan/tan-image.d.ts +3 -2
- package/dist/tan/tan-image.d.ts.map +1 -1
- package/dist/tan/tan-image.js +7 -5
- package/dist/tan/tan-image.js.map +1 -1
- package/dist/tan/tan-models.d.ts +5 -1
- package/dist/tan/tan-models.d.ts.map +1 -1
- package/dist/tan/tan-models.js +10 -5
- package/dist/tan/tan-models.js.map +1 -1
- package/dist/util/async.d.ts +0 -2
- package/dist/util/async.d.ts.map +1 -1
- package/dist/util/async.js +0 -4
- package/dist/util/async.js.map +1 -1
- package/dist/util/bytes.d.ts +2 -3
- package/dist/util/bytes.d.ts.map +1 -1
- package/dist/util/bytes.js +12 -20
- package/dist/util/bytes.js.map +1 -1
- package/dist/util/call-context.d.ts.map +1 -1
- package/dist/util/call-context.js +3 -2
- package/dist/util/call-context.js.map +1 -1
- package/dist/util/dates.d.ts +9 -16
- package/dist/util/dates.d.ts.map +1 -1
- package/dist/util/dates.js +39 -19
- package/dist/util/dates.js.map +1 -1
- package/dist/util/errors.d.ts +9 -1
- package/dist/util/errors.d.ts.map +1 -1
- package/dist/util/errors.js +12 -3
- package/dist/util/errors.js.map +1 -1
- package/dist/util/json.d.ts +4 -0
- package/dist/util/json.d.ts.map +1 -1
- package/dist/util/json.js +18 -0
- package/dist/util/json.js.map +1 -1
- package/dist/util/text.d.ts +21 -4
- package/dist/util/text.d.ts.map +1 -1
- package/dist/util/text.js +42 -13
- package/dist/util/text.js.map +1 -1
- package/dist/util/xml.d.ts +4 -3
- package/dist/util/xml.d.ts.map +1 -1
- package/dist/util/xml.js +37 -26
- package/dist/util/xml.js.map +1 -1
- package/dist/v3/account-extension.d.ts.map +1 -1
- package/dist/v3/account-extension.js +7 -10
- package/dist/v3/account-extension.js.map +1 -1
- package/dist/v3/bank-connection.d.ts +34 -0
- package/dist/v3/bank-connection.d.ts.map +1 -0
- package/dist/v3/bank-connection.js +58 -0
- package/dist/v3/bank-connection.js.map +1 -0
- package/dist/v3/dialog.d.ts +80 -46
- package/dist/v3/dialog.d.ts.map +1 -1
- package/dist/v3/dialog.js +162 -59
- package/dist/v3/dialog.js.map +1 -1
- package/dist/v3/message.d.ts +17 -4
- package/dist/v3/message.d.ts.map +1 -1
- package/dist/v3/message.js +62 -18
- package/dist/v3/message.js.map +1 -1
- package/dist/v3/parameter-store.d.ts +75 -0
- package/dist/v3/parameter-store.d.ts.map +1 -0
- package/dist/v3/parameter-store.js +143 -0
- package/dist/v3/parameter-store.js.map +1 -0
- package/dist/v3/parameters.d.ts +38 -12
- package/dist/v3/parameters.d.ts.map +1 -1
- package/dist/v3/parameters.js +91 -37
- package/dist/v3/parameters.js.map +1 -1
- package/dist/v3/protocol.d.ts +43 -50
- package/dist/v3/protocol.d.ts.map +1 -1
- package/dist/v3/protocol.js +58 -208
- package/dist/v3/protocol.js.map +1 -1
- package/dist/v3/queries.d.ts +51 -0
- package/dist/v3/queries.d.ts.map +1 -0
- package/dist/v3/queries.js +120 -0
- package/dist/v3/queries.js.map +1 -0
- package/dist/v3/segments.d.ts +29 -12
- package/dist/v3/segments.d.ts.map +1 -1
- package/dist/v3/segments.js +65 -23
- package/dist/v3/segments.js.map +1 -1
- package/dist/v3/syntax.d.ts +65 -14
- package/dist/v3/syntax.d.ts.map +1 -1
- package/dist/v3/syntax.js +167 -101
- package/dist/v3/syntax.js.map +1 -1
- package/package.json +10 -6
- package/src/client.ts +252 -0
- package/src/core/account.ts +86 -13
- package/src/core/amount.ts +3 -1
- package/src/core/balance.ts +6 -5
- package/src/core/bank-info.ts +1 -1
- package/src/core/client.ts +52 -15
- package/src/core/config.ts +69 -28
- package/src/core/errors.ts +122 -48
- package/src/core/feedback.ts +4 -1
- package/src/core/transaction.ts +9 -4
- package/src/core/transport.ts +98 -23
- package/src/formats/camt.ts +136 -39
- package/src/formats/mt940.ts +167 -81
- package/src/formats/readers.ts +24 -31
- package/src/formats/record-budget.ts +18 -0
- package/src/formats/sepa-purpose.ts +21 -2
- package/src/index.ts +4 -2
- package/src/session/app-approval.ts +51 -38
- package/src/session/dialog-runner.ts +31 -28
- package/src/session/login-guard.ts +174 -88
- package/src/session/sepa-accounts.ts +63 -45
- package/src/session/session-setup.ts +90 -71
- package/src/session/session-state.ts +56 -22
- package/src/session/signed-dialog.ts +192 -0
- package/src/session/tan-selection.ts +50 -39
- package/src/session/user-prompts.ts +35 -16
- package/src/tan/challenge.ts +40 -31
- package/src/tan/flicker.ts +44 -6
- package/src/tan/tan-image.ts +7 -5
- package/src/tan/tan-models.ts +10 -5
- package/src/util/async.ts +0 -5
- package/src/util/bytes.ts +13 -19
- package/src/util/call-context.ts +3 -2
- package/src/util/dates.ts +41 -24
- package/src/util/errors.ts +14 -3
- package/src/util/json.ts +16 -0
- package/src/util/text.ts +42 -12
- package/src/util/xml.ts +37 -26
- package/src/v3/account-extension.ts +7 -12
- package/src/v3/bank-connection.ts +67 -0
- package/src/v3/dialog.ts +175 -91
- package/src/v3/message.ts +69 -17
- package/src/v3/parameter-store.ts +160 -0
- package/src/v3/parameters.ts +110 -37
- package/src/v3/protocol.ts +70 -253
- package/src/v3/queries.ts +190 -0
- package/src/v3/segments.ts +65 -22
- package/src/v3/syntax.ts +153 -91
- package/dist/session/bank-connection.d.ts +0 -28
- package/dist/session/bank-connection.d.ts.map +0 -1
- package/dist/session/bank-connection.js +0 -48
- package/dist/session/bank-connection.js.map +0 -1
- package/dist/session/parameter-data.d.ts +0 -83
- package/dist/session/parameter-data.d.ts.map +0 -1
- package/dist/session/parameter-data.js +0 -68
- package/dist/session/parameter-data.js.map +0 -1
- package/dist/session/pin-tan-client.d.ts +0 -54
- package/dist/session/pin-tan-client.d.ts.map +0 -1
- package/dist/session/pin-tan-client.js +0 -153
- package/dist/session/pin-tan-client.js.map +0 -1
- package/dist/session/pin-tan-dialog.d.ts +0 -71
- package/dist/session/pin-tan-dialog.d.ts.map +0 -1
- package/dist/session/pin-tan-dialog.js +0 -124
- package/dist/session/pin-tan-dialog.js.map +0 -1
- package/dist/session/protocol.d.ts +0 -209
- package/dist/session/protocol.d.ts.map +0 -1
- package/dist/session/protocol.js +0 -75
- package/dist/session/protocol.js.map +0 -1
- package/dist/session/response-handler.d.ts +0 -48
- package/dist/session/response-handler.d.ts.map +0 -1
- package/dist/session/response-handler.js +0 -121
- package/dist/session/response-handler.js.map +0 -1
- package/dist/v3/client.d.ts +0 -24
- package/dist/v3/client.d.ts.map +0 -1
- package/dist/v3/client.js +0 -16
- package/dist/v3/client.js.map +0 -1
- package/src/session/bank-connection.ts +0 -51
- package/src/session/parameter-data.ts +0 -137
- package/src/session/pin-tan-client.ts +0 -186
- package/src/session/pin-tan-dialog.ts +0 -160
- package/src/session/protocol.ts +0 -311
- package/src/session/response-handler.ts +0 -130
- package/src/v3/client.ts +0 -29
package/src/util/xml.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { FormatError } from './errors.js';
|
|
2
|
+
import { characterReference, nonBlank } from './text.js';
|
|
2
3
|
|
|
3
4
|
/**
|
|
4
5
|
* An element of a parsed XML document, looked up by local name: camt documents come with and without namespace
|
|
@@ -29,7 +30,7 @@ export class XmlElement {
|
|
|
29
30
|
|
|
30
31
|
/** The first child element called `localName`. */
|
|
31
32
|
child(localName: string): XmlElement | undefined {
|
|
32
|
-
return this.
|
|
33
|
+
return this.children.find((c): c is XmlElement => c instanceof XmlElement && c.localName === localName);
|
|
33
34
|
}
|
|
34
35
|
|
|
35
36
|
/** The element reached by following child names along `path`; this element for an empty path. */
|
|
@@ -39,21 +40,12 @@ export class XmlElement {
|
|
|
39
40
|
|
|
40
41
|
/** The trimmed text of the element at `path`, or `undefined` if it is missing or empty. */
|
|
41
42
|
textAt(path: readonly string[]): string | undefined {
|
|
42
|
-
|
|
43
|
-
return text === undefined || text === '' ? undefined : text;
|
|
43
|
+
return nonBlank(this.at(path)?.innerText);
|
|
44
44
|
}
|
|
45
45
|
|
|
46
|
-
/**
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
const visit = (element: XmlElement): void => {
|
|
50
|
-
for (const c of element.childElements) {
|
|
51
|
-
if (c.localName === localName) found.push(c);
|
|
52
|
-
visit(c);
|
|
53
|
-
}
|
|
54
|
-
};
|
|
55
|
-
visit(this);
|
|
56
|
-
return found;
|
|
46
|
+
/** The child elements called `localName`, in document order. */
|
|
47
|
+
childrenNamed(localName: string): XmlElement[] {
|
|
48
|
+
return this.children.filter((c): c is XmlElement => c instanceof XmlElement && c.localName === localName);
|
|
57
49
|
}
|
|
58
50
|
}
|
|
59
51
|
|
|
@@ -65,7 +57,24 @@ function matchAt(pattern: RegExp, text: string, position: number): RegExpExecArr
|
|
|
65
57
|
pattern.lastIndex = position;
|
|
66
58
|
return pattern.exec(text);
|
|
67
59
|
}
|
|
68
|
-
const predefinedEntities
|
|
60
|
+
const predefinedEntities = new Map([
|
|
61
|
+
['amp', '&'],
|
|
62
|
+
['lt', '<'],
|
|
63
|
+
['gt', '>'],
|
|
64
|
+
['quot', '"'],
|
|
65
|
+
['apos', "'"],
|
|
66
|
+
]);
|
|
67
|
+
|
|
68
|
+
/** Deepest nesting of elements the parser takes: camt needs fewer than 20 levels, and `innerText` recurses. */
|
|
69
|
+
const maxDepth = 256;
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Most elements and attributes one document may hold. Real camt spends about 20 bytes on each, so the largest answer a
|
|
73
|
+
* bank can send holds fewer than 600,000, while a hostile document stays within about 150 MB of memory.
|
|
74
|
+
*/
|
|
75
|
+
const maxNodes = 1_000_000;
|
|
76
|
+
|
|
77
|
+
const noAttributes: ReadonlyMap<string, string> = new Map();
|
|
69
78
|
|
|
70
79
|
function localNameOf(qualified: string): string {
|
|
71
80
|
const colon = qualified.indexOf(':');
|
|
@@ -77,22 +86,19 @@ function decodeEntities(text: string): string {
|
|
|
77
86
|
return text.replace(/&([^;&\s]*);?/g, (match, name: string) => {
|
|
78
87
|
if (!match.endsWith(';')) throw new FormatError('Invalid XML: unterminated entity reference');
|
|
79
88
|
if (name.startsWith('#')) {
|
|
80
|
-
const
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
if (!(code > 0 && code <= 0x10ffff && (code < 0xd800 || code > 0xdfff))) {
|
|
84
|
-
throw new FormatError('Invalid XML character reference', match);
|
|
85
|
-
}
|
|
86
|
-
return String.fromCodePoint(code);
|
|
89
|
+
const character = characterReference(name);
|
|
90
|
+
if (character === undefined) throw new FormatError('Invalid XML character reference', match);
|
|
91
|
+
return character;
|
|
87
92
|
}
|
|
88
|
-
const value = predefinedEntities
|
|
93
|
+
const value = predefinedEntities.get(name);
|
|
89
94
|
if (value === undefined) throw new FormatError('Invalid XML: unknown entity', match);
|
|
90
95
|
return value;
|
|
91
96
|
});
|
|
92
97
|
}
|
|
93
98
|
|
|
94
99
|
/**
|
|
95
|
-
* Parses an XML document and returns its root element; throws `FormatError` if it is not well-formed
|
|
100
|
+
* Parses an XML document and returns its root element; throws `FormatError` if it is not well-formed, nests elements
|
|
101
|
+
* deeper than 256 levels or holds more than a million elements and attributes.
|
|
96
102
|
*
|
|
97
103
|
* A document type declaration is refused: bank data have none, and its entities could expand without limit.
|
|
98
104
|
*/
|
|
@@ -100,6 +106,7 @@ export function parseXml(xml: string): XmlElement {
|
|
|
100
106
|
let position = 0;
|
|
101
107
|
const stack: XmlElement[] = [];
|
|
102
108
|
let root: XmlElement | undefined;
|
|
109
|
+
let nodes = 0;
|
|
103
110
|
|
|
104
111
|
const fail = (reason: string): never => {
|
|
105
112
|
throw new FormatError(`Invalid XML: ${reason} at position ${position}`);
|
|
@@ -142,16 +149,20 @@ export function parseXml(xml: string): XmlElement {
|
|
|
142
149
|
const open = stack.pop();
|
|
143
150
|
if (open === undefined || open.localName !== localNameOf(name)) fail(`unexpected end tag ${name}`);
|
|
144
151
|
} else {
|
|
152
|
+
if (stack.length >= maxDepth) fail(`elements nested deeper than ${maxDepth} levels`);
|
|
153
|
+
if (++nodes > maxNodes) fail(`more than ${maxNodes} elements and attributes`);
|
|
145
154
|
position++;
|
|
146
155
|
const name = matchAt(namePattern, xml, position)?.[0] ?? fail('missing element name');
|
|
147
156
|
position += name.length;
|
|
148
|
-
|
|
157
|
+
let attributes: Map<string, string> | undefined;
|
|
149
158
|
for (;;) {
|
|
150
159
|
const attribute = matchAt(attributePattern, xml, position);
|
|
151
160
|
if (!attribute) break;
|
|
152
161
|
position += attribute[0].length;
|
|
162
|
+
if (++nodes > maxNodes) fail(`more than ${maxNodes} elements and attributes`);
|
|
153
163
|
const attributeName = attribute[1]!;
|
|
154
164
|
if (attributeName !== 'xmlns' && !attributeName.startsWith('xmlns:')) {
|
|
165
|
+
attributes ??= new Map();
|
|
155
166
|
attributes.set(localNameOf(attributeName), decodeEntities(attribute[2] ?? attribute[3] ?? ''));
|
|
156
167
|
}
|
|
157
168
|
}
|
|
@@ -159,7 +170,7 @@ export function parseXml(xml: string): XmlElement {
|
|
|
159
170
|
const selfClosing = xml.startsWith('/>', position);
|
|
160
171
|
if (!selfClosing && xml.charAt(position) !== '>') fail(`malformed start tag ${name}`);
|
|
161
172
|
position += selfClosing ? 2 : 1;
|
|
162
|
-
const element = new XmlElement(localNameOf(name), attributes);
|
|
173
|
+
const element = new XmlElement(localNameOf(name), attributes ?? noAttributes);
|
|
163
174
|
const parent = stack.at(-1);
|
|
164
175
|
if (parent) parent.children.push(element);
|
|
165
176
|
else if (root) fail('a second root element');
|
|
@@ -4,7 +4,9 @@ import { AccountDataStatus, type Account } from '../core/account.js';
|
|
|
4
4
|
import { Amount, withMark } from '../core/amount.js';
|
|
5
5
|
import { Balance } from '../core/balance.js';
|
|
6
6
|
import { parseFintsDate, parseUtcPlusOneDateTime } from '../util/dates.js';
|
|
7
|
-
import { FormatError } from '../util/errors.js';
|
|
7
|
+
import { fail, FormatError } from '../util/errors.js';
|
|
8
|
+
import { isJsonObject } from '../util/json.js';
|
|
9
|
+
import { nonBlank } from '../util/text.js';
|
|
8
10
|
|
|
9
11
|
/** A booked or pending balance of the extension: credit/debit mark, value, date and time. */
|
|
10
12
|
interface Saldo {
|
|
@@ -47,7 +49,7 @@ export class AccountExtension {
|
|
|
47
49
|
static tryParse(json: string): AccountExtension | undefined {
|
|
48
50
|
try {
|
|
49
51
|
const decoded: unknown = JSON.parse(json);
|
|
50
|
-
return
|
|
52
|
+
return isJsonObject(decoded) ? AccountExtension.read(new Json(decoded)) : undefined;
|
|
51
53
|
} catch (error) {
|
|
52
54
|
if (error instanceof FormatError || error instanceof SyntaxError) return undefined;
|
|
53
55
|
throw error;
|
|
@@ -76,6 +78,7 @@ export class AccountExtension {
|
|
|
76
78
|
if (b === undefined || !this.balanceComplete) return undefined;
|
|
77
79
|
return new Balance({
|
|
78
80
|
account,
|
|
81
|
+
productName: account.productName,
|
|
79
82
|
booked: b.booked.amount,
|
|
80
83
|
bookedAsOf: b.booked.date,
|
|
81
84
|
bookedAsOfTime: b.booked.time,
|
|
@@ -129,14 +132,6 @@ function moment(json: Json, defaultTime?: string): Date | undefined {
|
|
|
129
132
|
return parseUtcPlusOneDateTime(json.string('Date'), time) ?? fail('Extension date or time is malformed');
|
|
130
133
|
}
|
|
131
134
|
|
|
132
|
-
function fail(message: string): never {
|
|
133
|
-
throw new FormatError(message);
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
function isObject(value: unknown): value is Record<string, unknown> {
|
|
137
|
-
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
138
|
-
}
|
|
139
|
-
|
|
140
135
|
/**
|
|
141
136
|
* A JSON object of the extension, read by key without regard to case: the specification itself spells some keys
|
|
142
137
|
* differently (`OverDraftLim` and `OverdraftLim`). Values of the wrong type are a `FormatError`.
|
|
@@ -156,7 +151,7 @@ class Json {
|
|
|
156
151
|
string(key: string): string | undefined {
|
|
157
152
|
const value = this.value(key);
|
|
158
153
|
if (value === undefined || value === null) return undefined;
|
|
159
|
-
if (typeof value === 'string') return
|
|
154
|
+
if (typeof value === 'string') return nonBlank(value);
|
|
160
155
|
if (typeof value === 'number') return String(value);
|
|
161
156
|
throw new FormatError(`Extension element "${key}" is not text`);
|
|
162
157
|
}
|
|
@@ -172,7 +167,7 @@ class Json {
|
|
|
172
167
|
object(key: string): Json | undefined {
|
|
173
168
|
const value = this.value(key);
|
|
174
169
|
if (value === undefined || value === null) return undefined;
|
|
175
|
-
if (
|
|
170
|
+
if (isJsonObject(value)) return new Json(value);
|
|
176
171
|
throw new FormatError(`Extension element "${key}" is not an object`);
|
|
177
172
|
}
|
|
178
173
|
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import type { MessageDirection, MessageLogger } from '../core/client.js';
|
|
2
|
+
import { FinTsProtocolError, FinTsTransportError } from '../core/errors.js';
|
|
3
|
+
import { failureMessage, HttpTransport, type FinTsTransport } from '../core/transport.js';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Throws `FinTsProtocolError` if a message of `size` bytes exceeds the bank's `limit` in kilobytes (1024 bytes): the
|
|
7
|
+
* specification does not allow to send such a message.
|
|
8
|
+
*/
|
|
9
|
+
export function checkMessageSize(size: number, limit: number | undefined): void {
|
|
10
|
+
if (limit !== undefined && limit > 0 && size > limit * 1024) {
|
|
11
|
+
throw new FinTsProtocolError(`A message of ${size} bytes exceeds the bank's maximum message size of ${limit} KB`);
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/** The bank's FinTS endpoint: posts messages there and reports them to the logger. */
|
|
16
|
+
export class BankConnection {
|
|
17
|
+
private readonly transport: FinTsTransport;
|
|
18
|
+
|
|
19
|
+
/** Where the messages go, as `FinTsConfig` checked it. */
|
|
20
|
+
private readonly href: string;
|
|
21
|
+
|
|
22
|
+
/** Aborts the requests on their way (`abortRequests`). */
|
|
23
|
+
private requests = new AbortController();
|
|
24
|
+
|
|
25
|
+
/** A connection to `url` through `transport`, an `HttpTransport` by default. */
|
|
26
|
+
constructor(
|
|
27
|
+
url: URL,
|
|
28
|
+
transport: FinTsTransport | undefined,
|
|
29
|
+
private readonly logger: MessageLogger | undefined,
|
|
30
|
+
) {
|
|
31
|
+
this.href = url.href;
|
|
32
|
+
this.transport = transport ?? new HttpTransport();
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Posts `body` to a new copy of the URL, so that a transport that changes the one it is given changes no later
|
|
37
|
+
* request.
|
|
38
|
+
*
|
|
39
|
+
* Reports any failure of the transport as `FinTsTransportError`: a custom transport that calls `fetch` fails with a
|
|
40
|
+
* `TypeError` where the network does, and one that throws another `FinTsError`, such as `FinTsPinError`, would pass
|
|
41
|
+
* for an answer of the bank. Of such an error only the message is kept (`failureMessage`), since it may hold `body`,
|
|
42
|
+
* as the errors of HTTP clients such as axios do.
|
|
43
|
+
*/
|
|
44
|
+
async post(body: Uint8Array, contentType: string): Promise<Uint8Array> {
|
|
45
|
+
try {
|
|
46
|
+
return await this.transport.post(new URL(this.href), body, contentType, this.requests.signal);
|
|
47
|
+
} catch (error) {
|
|
48
|
+
if (error instanceof FinTsTransportError) throw error;
|
|
49
|
+
throw new FinTsTransportError(failureMessage(error));
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** Reports a message to the logger; `message` is only built when a logger is set. */
|
|
54
|
+
log(direction: MessageDirection, message: () => string): void {
|
|
55
|
+
this.logger?.(direction, message());
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** Aborts the requests on their way; those posted afterwards, such as the dialog ends of a closing client, go out. */
|
|
59
|
+
abortRequests(): void {
|
|
60
|
+
this.requests.abort();
|
|
61
|
+
this.requests = new AbortController();
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
close(): void {
|
|
65
|
+
this.transport.close();
|
|
66
|
+
}
|
|
67
|
+
}
|
package/src/v3/dialog.ts
CHANGED
|
@@ -1,13 +1,12 @@
|
|
|
1
1
|
// Internal to the package, and named after the FinTS specification. Documentation is where the meaning is not evident.
|
|
2
2
|
|
|
3
|
-
import { FinTsProtocolError } from '../core/errors.js';
|
|
4
|
-
import { FeedbackCodes, hasCode } from '../core/feedback.js';
|
|
5
|
-
import {
|
|
6
|
-
import { PinTanDialog } from '../session/pin-tan-dialog.js';
|
|
7
|
-
import type { Approval, StartWithOrder } from '../session/protocol.js';
|
|
3
|
+
import { FinTsError, FinTsProtocolError, FinTsTransportError, parseBankData } from '../core/errors.js';
|
|
4
|
+
import { endsDialog, FeedbackCodes, hasCode, hasErrors } from '../core/feedback.js';
|
|
5
|
+
import { challengeText, TanChallenge } from '../tan/challenge.js';
|
|
8
6
|
import { TanMethod } from '../tan/tan-models.js';
|
|
9
7
|
import { IncomingMessage3 } from './message.js';
|
|
10
|
-
import
|
|
8
|
+
import { requireBankParameters } from './parameters.js';
|
|
9
|
+
import type { FinTs3Protocol } from './protocol.js';
|
|
11
10
|
import { Segments, TanResponse } from './segments.js';
|
|
12
11
|
import type { SegmentSpec } from './syntax.js';
|
|
13
12
|
|
|
@@ -20,96 +19,121 @@ const firstScaVersion = 6;
|
|
|
20
19
|
/** The first version of HKTAN that supports app approvals, with the status request (process S). */
|
|
21
20
|
const firstDecoupledVersion = 7;
|
|
22
21
|
|
|
23
|
-
/** How a message is signed: the security function of the TAN method and the customer system ID. */
|
|
24
|
-
export interface Signing3 {
|
|
25
|
-
readonly securityFunction: string;
|
|
26
|
-
readonly systemId: string;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
22
|
/**
|
|
30
|
-
*
|
|
31
|
-
*
|
|
23
|
+
* How long a dialog may stay idle where the bank states no timeout: on the safe side, since a dialog given up too early
|
|
24
|
+
* only costs a new login, while one kept too long fails the next call.
|
|
32
25
|
*/
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
26
|
+
const defaultIdleLimitMs = 5 * 60_000;
|
|
27
|
+
|
|
28
|
+
/** The bank's request to approve a message with a TAN or in a banking app; `R` is the bank's answer. */
|
|
29
|
+
export interface Approval<R> {
|
|
30
|
+
/** What the user is asked. */
|
|
31
|
+
readonly challenge: TanChallenge;
|
|
36
32
|
|
|
37
|
-
/**
|
|
38
|
-
readonly
|
|
33
|
+
/** Sends the TAN the user entered. */
|
|
34
|
+
readonly submitTan: (tan: string) => Promise<R>;
|
|
39
35
|
|
|
40
|
-
/**
|
|
41
|
-
|
|
36
|
+
/** Asks the bank whether the user has approved in the banking app. */
|
|
37
|
+
readonly checkStatus: () => Promise<R>;
|
|
42
38
|
|
|
43
39
|
/**
|
|
44
|
-
*
|
|
45
|
-
*
|
|
46
|
-
* With `signing` the message is signed with the PIN and the `tan`, after `LoginGuard.check`; without, it goes out
|
|
47
|
-
* unsigned, as in an anonymous dialog.
|
|
40
|
+
* Whether the bank announces a push message for once the user has approved (return code 3957), before which the
|
|
41
|
+
* status must not be asked. The client has no push channel, so the user reports the approval instead.
|
|
48
42
|
*/
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
signing: Signing3 | undefined;
|
|
54
|
-
tan: string | undefined;
|
|
55
|
-
}): Promise<Uint8Array>;
|
|
43
|
+
readonly waitForPush: boolean;
|
|
44
|
+
|
|
45
|
+
/** The new instructions a status answer gives the user (return code 3956); `undefined` where it gives none. */
|
|
46
|
+
readonly instructionsIn: (response: R) => string | undefined;
|
|
56
47
|
}
|
|
57
48
|
|
|
58
|
-
/**
|
|
59
|
-
|
|
49
|
+
/**
|
|
50
|
+
* One FinTS 3.0 dialog: numbered messages under the dialog ID the bank assigns in its first answer, signed with a TAN
|
|
51
|
+
* method and medium, and the approvals the bank asks for. The `FinTs3Protocol` builds the messages and signs them.
|
|
52
|
+
*/
|
|
53
|
+
export class FinTs3Dialog {
|
|
60
54
|
private systemId: string;
|
|
61
55
|
private id = unassigned;
|
|
56
|
+
private messageNumber = 1;
|
|
57
|
+
private ended = false;
|
|
58
|
+
|
|
59
|
+
/** When the bank last answered without rejecting the whole message, which shows it still knows the dialog. */
|
|
60
|
+
private lastAnswer: number | undefined;
|
|
62
61
|
|
|
63
62
|
private constructor(
|
|
64
|
-
private readonly
|
|
65
|
-
method: TanMethod,
|
|
66
|
-
|
|
63
|
+
private readonly protocol: FinTs3Protocol,
|
|
64
|
+
readonly method: TanMethod,
|
|
65
|
+
/** The TAN medium the messages name, where the method takes one. */
|
|
66
|
+
readonly tanMedium: string | undefined,
|
|
67
67
|
private readonly anonymous: boolean,
|
|
68
68
|
) {
|
|
69
|
-
|
|
70
|
-
this.systemId = anonymous ? unassigned : (host.systemId ?? unassigned);
|
|
69
|
+
this.systemId = anonymous ? unassigned : (protocol.parameters.systemId ?? unassigned);
|
|
71
70
|
}
|
|
72
71
|
|
|
73
|
-
static signed(
|
|
74
|
-
return new FinTs3Dialog(
|
|
72
|
+
static signed(protocol: FinTs3Protocol, method: TanMethod, tanMedium?: string): FinTs3Dialog {
|
|
73
|
+
return new FinTs3Dialog(protocol, method, tanMedium, false);
|
|
75
74
|
}
|
|
76
75
|
|
|
77
76
|
/** A dialog without login, which only delivers the bank parameter data. */
|
|
78
|
-
static anonymous(
|
|
79
|
-
return new FinTs3Dialog(
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
protected get bankParameters(): Bpd3 | undefined {
|
|
83
|
-
return this.host.bankParameters;
|
|
77
|
+
static anonymous(protocol: FinTs3Protocol): FinTs3Dialog {
|
|
78
|
+
return new FinTs3Dialog(protocol, TanMethod.oneStep, undefined, true);
|
|
84
79
|
}
|
|
85
80
|
|
|
81
|
+
/** Whether the bank knows this dialog and it has not ended. */
|
|
86
82
|
get isActive(): boolean {
|
|
87
83
|
return this.id !== unassigned && !this.hasEnded;
|
|
88
84
|
}
|
|
89
85
|
|
|
86
|
+
/**
|
|
87
|
+
* Whether the dialog has ended, on either side.
|
|
88
|
+
*
|
|
89
|
+
* The bank ends a dialog idle for longer than its maximum timeout (HIBPA, Formals C.9), or five minutes where it
|
|
90
|
+
* states none. Such a dialog is neither used nor ended again: the bank would refuse any message in it.
|
|
91
|
+
*/
|
|
92
|
+
private get hasEnded(): boolean {
|
|
93
|
+
return this.ended || this.idleTooLong;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
private get idleTooLong(): boolean {
|
|
97
|
+
const last = this.lastAnswer;
|
|
98
|
+
const timeout = this.protocol.parameters.bankParameters?.bankInfo.maxTimeout;
|
|
99
|
+
// A timeout of 0 or below: the bank keeps idle dialogs without limit.
|
|
100
|
+
const limit = timeout === undefined ? defaultIdleLimitMs : timeout <= 0 ? undefined : timeout * 1000;
|
|
101
|
+
return last !== undefined && limit !== undefined && Date.now() - last > limit;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** Whether the messages are signed with a two-step TAN method, so that the bank may ask for approvals. */
|
|
105
|
+
private get isTwoStep(): boolean {
|
|
106
|
+
return !this.method.isOneStep;
|
|
107
|
+
}
|
|
108
|
+
|
|
90
109
|
/** The version of the HKTAN segments: that of the HITANS segment the bank describes the method in. */
|
|
91
110
|
private get tanVersion(): number {
|
|
92
|
-
return this.bankParameters?.tanVersion(this.method) ?? firstScaVersion;
|
|
111
|
+
return this.protocol.parameters.bankParameters?.tanVersion(this.method) ?? firstScaVersion;
|
|
93
112
|
}
|
|
94
113
|
|
|
114
|
+
/** Opens the dialog; with `synchronize` the bank also assigns a customer system ID. */
|
|
95
115
|
start(synchronize = false): Promise<IncomingMessage3> {
|
|
96
116
|
if (synchronize) this.systemId = unassigned;
|
|
97
117
|
return this.sendSegments([
|
|
98
|
-
...this.
|
|
118
|
+
...this.protocol.initSegments(this.systemId),
|
|
99
119
|
...(this.isTwoStep ? [this.tanRequest('HKIDN')] : []),
|
|
100
120
|
...(synchronize ? [Segments.synchronization()] : []),
|
|
101
121
|
]);
|
|
102
122
|
}
|
|
103
123
|
|
|
104
124
|
/**
|
|
105
|
-
*
|
|
106
|
-
*
|
|
125
|
+
* The messages that open the dialog and send `order` while no TAN medium is chosen yet, as for listing the media:
|
|
126
|
+
* `login` opens the dialog, asking strong authentication for `order` with a two-step method, and `order` sends the
|
|
127
|
+
* order in a message of its own.
|
|
107
128
|
*/
|
|
108
|
-
startWith(order: SegmentSpec):
|
|
129
|
+
startWith(order: SegmentSpec): {
|
|
130
|
+
readonly login: () => Promise<IncomingMessage3>;
|
|
131
|
+
readonly order: () => Promise<IncomingMessage3>;
|
|
132
|
+
} {
|
|
109
133
|
return {
|
|
110
134
|
login: () =>
|
|
111
135
|
this.sendSegments([
|
|
112
|
-
...this.
|
|
136
|
+
...this.protocol.initSegments(this.systemId),
|
|
113
137
|
...(this.isTwoStep ? [this.tanRequest(order.type)] : []),
|
|
114
138
|
]),
|
|
115
139
|
order: () => this.sendSegments([order]),
|
|
@@ -124,69 +148,129 @@ export class FinTs3Dialog extends PinTanDialog<IncomingMessage3, SegmentSpec> {
|
|
|
124
148
|
*/
|
|
125
149
|
requestBankParameters(announceSca = false): Promise<IncomingMessage3> {
|
|
126
150
|
return this.sendSegments([
|
|
127
|
-
...this.
|
|
151
|
+
...this.protocol.initSegments(unassigned, true),
|
|
128
152
|
...(announceSca ? [Segments.tanProcess4(firstScaVersion, 'HKIDN')] : []),
|
|
129
153
|
]);
|
|
130
154
|
}
|
|
131
155
|
|
|
156
|
+
/** Sends `order`, asking for strong customer authentication where the bank requires it. */
|
|
132
157
|
send(order: SegmentSpec): Promise<IncomingMessage3> {
|
|
133
|
-
const needsTan =
|
|
158
|
+
const needsTan =
|
|
159
|
+
this.isTwoStep && requireBankParameters(this.protocol.parameters.bankParameters).pinTan.requiresTan(order.type);
|
|
134
160
|
return this.sendSegments([order, ...(needsTan ? [this.tanRequest(order.type)] : [])]);
|
|
135
161
|
}
|
|
136
162
|
|
|
137
|
-
|
|
138
|
-
|
|
163
|
+
/**
|
|
164
|
+
* Ends the dialog at the bank and returns the answer; `undefined` if the dialog is not active or the end failed.
|
|
165
|
+
*
|
|
166
|
+
* Bank and connection errors are ignored, since the bank may have ended the dialog on its side already.
|
|
167
|
+
*/
|
|
168
|
+
async end(): Promise<IncomingMessage3 | undefined> {
|
|
169
|
+
if (!this.isActive) return undefined;
|
|
170
|
+
this.ended = true;
|
|
171
|
+
try {
|
|
172
|
+
return await this.sendSegments([Segments.dialogEnd(this.id)]);
|
|
173
|
+
} catch (error) {
|
|
174
|
+
if (!(error instanceof FinTsError)) throw error;
|
|
175
|
+
return undefined;
|
|
176
|
+
}
|
|
139
177
|
}
|
|
140
178
|
|
|
141
|
-
|
|
142
|
-
|
|
179
|
+
/** Forgets the dialog without contacting the bank, for example after the bank rejected the PIN. */
|
|
180
|
+
discard(): void {
|
|
181
|
+
this.ended = true;
|
|
143
182
|
}
|
|
144
183
|
|
|
145
184
|
/**
|
|
146
|
-
* The approval
|
|
185
|
+
* The approval `response`, the dialog's latest answer, asks for with return code 0030 (TAN) or 3955 (banking app):
|
|
186
|
+
* HITAN of process 4, answered with HKTAN process 2 (TAN) or S (status); `undefined` if it asks for none, or carries
|
|
187
|
+
* only the dummy challenge of a message the bank exempts from strong authentication.
|
|
147
188
|
*
|
|
148
|
-
*
|
|
149
|
-
*
|
|
189
|
+
* The bank has not executed the message then, so a request without a challenge is a `FinTsProtocolError`: read as
|
|
190
|
+
* done, it would report an empty result. So is an app approval with a method of HITANS #6: the status request
|
|
191
|
+
* (process S) came with version 7, along with app approvals (PIN/TAN B.5.2).
|
|
150
192
|
*/
|
|
151
|
-
|
|
193
|
+
approvalAskedIn(response: IncomingMessage3): Approval<IncomingMessage3> | undefined {
|
|
194
|
+
const feedback = response.feedback;
|
|
195
|
+
const decoupled = hasCode(feedback, FeedbackCodes.decoupledPending);
|
|
196
|
+
if (hasErrors(feedback) || !(hasCode(feedback, FeedbackCodes.tanRequired) || decoupled)) return undefined;
|
|
152
197
|
const tan = tanResponseOf(response, '4');
|
|
153
|
-
if (tan === undefined || tan.isDummy)
|
|
154
|
-
|
|
198
|
+
if (tan === undefined || tan.isDummy) {
|
|
199
|
+
throw new FinTsProtocolError('The bank asks for an approval, but sends no challenge');
|
|
200
|
+
}
|
|
201
|
+
if (decoupled && this.tanVersion < firstDecoupledVersion) {
|
|
155
202
|
throw new FinTsProtocolError('The bank asks for an app approval, which needs HKTAN version 7');
|
|
156
203
|
}
|
|
157
204
|
const reference = tan.reference!;
|
|
158
|
-
return
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
205
|
+
return {
|
|
206
|
+
challenge: TanChallenge.create({
|
|
207
|
+
method: this.method,
|
|
208
|
+
reference,
|
|
209
|
+
message: tan.challenge,
|
|
210
|
+
hhdUc: tan.hhdUc,
|
|
211
|
+
isDecoupled: decoupled || this.method.decoupled,
|
|
212
|
+
validUntil: tan.validUntil,
|
|
213
|
+
tanMediumName: tan.mediumName ?? this.tanMedium,
|
|
214
|
+
}),
|
|
164
215
|
submitTan: (value) => this.sendSegments([Segments.tanProcess2(this.tanVersion, reference)], value),
|
|
165
216
|
checkStatus: () => this.sendSegments([Segments.tanProcessStatus(this.tanVersion, reference)]),
|
|
217
|
+
waitForPush: hasCode(feedback, FeedbackCodes.decoupledWaitForPush),
|
|
166
218
|
// A status answer that the approval is still pending may carry new instructions (PIN/TAN B.4.2.2.1, step 2b).
|
|
167
|
-
instructionsIn: (status) => tanResponseOf(status, 'S')?.challenge,
|
|
168
|
-
}
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
private
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
segments,
|
|
178
|
-
signing: this.anonymous
|
|
179
|
-
? undefined
|
|
180
|
-
: { securityFunction: this.method.securityFunction, systemId: this.systemId },
|
|
181
|
-
tan,
|
|
182
|
-
}),
|
|
183
|
-
(answer) => IncomingMessage3.parse(answer),
|
|
219
|
+
instructionsIn: (status) => challengeText(tanResponseOf(status, 'S')?.challenge),
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
private tanRequest(segmentType: string): SegmentSpec {
|
|
224
|
+
const medium = this.method.mediumRequirement === 'notAllowed' ? undefined : this.tanMedium;
|
|
225
|
+
return Segments.tanProcess4(
|
|
226
|
+
this.tanVersion,
|
|
227
|
+
segmentType,
|
|
228
|
+
medium === undefined ? undefined : this.protocol.parameters.tanMediumAsSent(medium),
|
|
184
229
|
);
|
|
185
230
|
}
|
|
186
231
|
|
|
187
|
-
/**
|
|
188
|
-
|
|
189
|
-
|
|
232
|
+
/**
|
|
233
|
+
* Sends `segments`, with `tan` where they complete an approval, as the next message, and reads the bank's answer. The
|
|
234
|
+
* message counts once the bank has answered, whatever it answered.
|
|
235
|
+
*
|
|
236
|
+
* The bank assigns the dialog ID in its answer to the first message, so the answer to another user's first message
|
|
237
|
+
* passes for it (`ParameterStore.reportedUpdVersion`). Any other answer to another message fails with
|
|
238
|
+
* `FinTsProtocolError` before anything of it is kept: a relay or proxy that mixes up answers would hand the client
|
|
239
|
+
* another user's data, or a page of another query.
|
|
240
|
+
*
|
|
241
|
+
* The dialog ends on the client's side, without a dialog end, where it is over for the bank: after a return code that
|
|
242
|
+
* ends it, such as 9800, and after a rejected opening message (Formals C.1.2), or a broken connection (C.6).
|
|
243
|
+
*
|
|
244
|
+
* Only an answer that does not reject the whole message starts the idle time anew (`hasEnded`): a rejection may come
|
|
245
|
+
* from a bank that has ended the dialog without saying so.
|
|
246
|
+
*/
|
|
247
|
+
private async sendSegments(segments: readonly SegmentSpec[], tan?: string): Promise<IncomingMessage3> {
|
|
248
|
+
const number = this.messageNumber;
|
|
249
|
+
let answer: Uint8Array;
|
|
250
|
+
try {
|
|
251
|
+
answer = await this.protocol.post({
|
|
252
|
+
dialogId: this.id,
|
|
253
|
+
number,
|
|
254
|
+
segments,
|
|
255
|
+
signing: this.anonymous
|
|
256
|
+
? undefined
|
|
257
|
+
: { securityFunction: this.method.securityFunction, systemId: this.systemId },
|
|
258
|
+
tan,
|
|
259
|
+
});
|
|
260
|
+
} catch (error) {
|
|
261
|
+
if (error instanceof FinTsTransportError) this.discard();
|
|
262
|
+
throw error;
|
|
263
|
+
}
|
|
264
|
+
this.messageNumber++;
|
|
265
|
+
const response = parseBankData(() => IncomingMessage3.parse(answer));
|
|
266
|
+
const assigned = this.id === unassigned ? undefined : this.id;
|
|
267
|
+
if (!response.answers(number, assigned)) {
|
|
268
|
+
throw new FinTsProtocolError('The answer belongs to another message than the one sent');
|
|
269
|
+
}
|
|
270
|
+
if (assigned === undefined) this.id = response.dialogId ?? unassigned;
|
|
271
|
+
if (!response.messageRejected) this.lastAnswer = Date.now();
|
|
272
|
+
if (endsDialog(response.feedback) || (number === 1 && response.messageRejected)) this.discard();
|
|
273
|
+
return response;
|
|
190
274
|
}
|
|
191
275
|
}
|
|
192
276
|
|