@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
|
@@ -0,0 +1,126 @@
|
|
|
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 posted since the last `abortRequestsAfter`. */
|
|
23
|
+
private requests = new AbortController();
|
|
24
|
+
|
|
25
|
+
/** How many requests are on their way. */
|
|
26
|
+
private onTheirWay = 0;
|
|
27
|
+
|
|
28
|
+
/** Starts the time of the requests posted after `abortRequestsAfter`, once none posted before is on its way. */
|
|
29
|
+
private startLaterRequests: (() => void) | undefined;
|
|
30
|
+
|
|
31
|
+
/** The timers of `abortRequestsAfter`, which `close` clears. */
|
|
32
|
+
private readonly deadlines: ReturnType<typeof setTimeout>[] = [];
|
|
33
|
+
|
|
34
|
+
/** A connection to `url` through `transport`, an `HttpTransport` by default. */
|
|
35
|
+
constructor(
|
|
36
|
+
url: URL,
|
|
37
|
+
transport: FinTsTransport | undefined,
|
|
38
|
+
private readonly logger: MessageLogger | undefined,
|
|
39
|
+
) {
|
|
40
|
+
this.href = url.href;
|
|
41
|
+
this.transport = transport ?? new HttpTransport();
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Posts `body` to a new copy of the URL, so that a transport that changes the one it is given changes no later
|
|
46
|
+
* request.
|
|
47
|
+
*
|
|
48
|
+
* Reports any failure of the transport as `FinTsTransportError`: a custom transport that calls `fetch` fails with a
|
|
49
|
+
* `TypeError` where the network does, and one that throws another `FinTsError`, such as `FinTsPinError`, would pass
|
|
50
|
+
* for an answer of the bank. Of such an error only the message is kept (`failureMessage`), since it may hold `body`,
|
|
51
|
+
* as the errors of HTTP clients such as axios do.
|
|
52
|
+
*
|
|
53
|
+
* Once the time `abortRequestsAfter` gives is up, it fails at once, before anything reaches the transport
|
|
54
|
+
* (`checkSendable`), and a request on its way fails then without waiting for the transport, which may ignore the
|
|
55
|
+
* abort.
|
|
56
|
+
*/
|
|
57
|
+
async post(body: Uint8Array, contentType: string): Promise<Uint8Array> {
|
|
58
|
+
this.checkSendable();
|
|
59
|
+
const signal = this.requests.signal;
|
|
60
|
+
let stopWaiting!: () => void;
|
|
61
|
+
const aborted = new Promise<never>((_, reject) => {
|
|
62
|
+
stopWaiting = () => {
|
|
63
|
+
reject(new FinTsTransportError('Request aborted'));
|
|
64
|
+
};
|
|
65
|
+
});
|
|
66
|
+
signal.addEventListener('abort', stopWaiting);
|
|
67
|
+
this.onTheirWay++;
|
|
68
|
+
try {
|
|
69
|
+
return await Promise.race([this.transport.post(new URL(this.href), body, contentType, signal), aborted]);
|
|
70
|
+
} catch (error) {
|
|
71
|
+
if (error instanceof FinTsTransportError) throw error;
|
|
72
|
+
throw new FinTsTransportError(failureMessage(error));
|
|
73
|
+
} finally {
|
|
74
|
+
signal.removeEventListener('abort', stopWaiting);
|
|
75
|
+
this.onTheirWay--;
|
|
76
|
+
if (this.onTheirWay === 0) this.startLaterRequests?.();
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Throws the `FinTsTransportError` that `post` fails with once the time `abortRequestsAfter` gives is up; a caller
|
|
82
|
+
* checks it before it reports a message as outgoing, since such a message never goes out.
|
|
83
|
+
*/
|
|
84
|
+
checkSendable(): void {
|
|
85
|
+
if (this.requests.signal.aborted) throw new FinTsTransportError('Request aborted');
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** Reports a message to the logger; `message` is only built when a logger is set. */
|
|
89
|
+
log(direction: MessageDirection, message: () => string): void {
|
|
90
|
+
this.logger?.(direction, message());
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Gives the requests on their way `graceMs` to finish, and aborts them then. Those posted later, such as the dialog
|
|
95
|
+
* ends of a closing client, have as long again from the moment the earlier ones are over or aborted, and are aborted
|
|
96
|
+
* in turn once that time is up, as is any posted after it: a bank that has stopped answering would hold them up to
|
|
97
|
+
* the transport's timeout.
|
|
98
|
+
*/
|
|
99
|
+
abortRequestsAfter(graceMs: number): void {
|
|
100
|
+
const earlier = this.requests;
|
|
101
|
+
const later = new AbortController();
|
|
102
|
+
this.requests = later;
|
|
103
|
+
const afterGrace = (action: () => void): void => {
|
|
104
|
+
this.deadlines.push(setTimeout(action, graceMs));
|
|
105
|
+
};
|
|
106
|
+
this.startLaterRequests = () => {
|
|
107
|
+
this.startLaterRequests = undefined;
|
|
108
|
+
afterGrace(() => {
|
|
109
|
+
later.abort();
|
|
110
|
+
});
|
|
111
|
+
};
|
|
112
|
+
if (this.onTheirWay === 0) {
|
|
113
|
+
this.startLaterRequests();
|
|
114
|
+
} else {
|
|
115
|
+
afterGrace(() => {
|
|
116
|
+
earlier.abort();
|
|
117
|
+
this.startLaterRequests?.();
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
close(): void {
|
|
123
|
+
for (const deadline of this.deadlines) clearTimeout(deadline);
|
|
124
|
+
this.transport.close();
|
|
125
|
+
}
|
|
126
|
+
}
|
package/src/v3/dialog.ts
CHANGED
|
@@ -1,14 +1,13 @@
|
|
|
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
|
|
11
|
-
import {
|
|
8
|
+
import { requireBankParameters } from './parameters.js';
|
|
9
|
+
import type { FinTs3Protocol } from './protocol.js';
|
|
10
|
+
import { noReference, Segments, TanResponse } from './segments.js';
|
|
12
11
|
import type { SegmentSpec } from './syntax.js';
|
|
13
12
|
|
|
14
13
|
/** Dialog ID of a dialog the bank has not answered yet, and customer system ID before synchronization. */
|
|
@@ -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,139 @@ 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
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
205
|
+
// The placeholder the dialog that looks up the TAN media sends, and the bank may echo, is no medium.
|
|
206
|
+
const medium = tan.mediumName ?? this.tanMedium;
|
|
207
|
+
return {
|
|
208
|
+
challenge: TanChallenge.create({
|
|
209
|
+
method: this.method,
|
|
210
|
+
reference,
|
|
211
|
+
message: tan.challenge,
|
|
212
|
+
hhdUc: tan.hhdUc,
|
|
213
|
+
isDecoupled: decoupled || this.method.decoupled,
|
|
214
|
+
validUntil: tan.validUntil,
|
|
215
|
+
tanMediumName: medium === noReference ? undefined : medium,
|
|
216
|
+
}),
|
|
164
217
|
submitTan: (value) => this.sendSegments([Segments.tanProcess2(this.tanVersion, reference)], value),
|
|
165
218
|
checkStatus: () => this.sendSegments([Segments.tanProcessStatus(this.tanVersion, reference)]),
|
|
219
|
+
waitForPush: hasCode(feedback, FeedbackCodes.decoupledWaitForPush),
|
|
166
220
|
// 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),
|
|
221
|
+
instructionsIn: (status) => challengeText(tanResponseOf(status, 'S')?.challenge),
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
private tanRequest(segmentType: string): SegmentSpec {
|
|
226
|
+
const medium = this.method.mediumRequirement === 'notAllowed' ? undefined : this.tanMedium;
|
|
227
|
+
return Segments.tanProcess4(
|
|
228
|
+
this.tanVersion,
|
|
229
|
+
segmentType,
|
|
230
|
+
medium === undefined ? undefined : this.protocol.parameters.tanMediumToSend(medium),
|
|
184
231
|
);
|
|
185
232
|
}
|
|
186
233
|
|
|
187
|
-
/**
|
|
188
|
-
|
|
189
|
-
|
|
234
|
+
/**
|
|
235
|
+
* Sends `segments`, with `tan` where they complete an approval, as the next message, and reads the bank's answer. The
|
|
236
|
+
* message counts once the bank has answered, whatever it answered.
|
|
237
|
+
*
|
|
238
|
+
* The bank assigns the dialog ID in its answer to the first message, so the answer to another user's first message
|
|
239
|
+
* passes for it (`ParameterStore.reportedUpdVersion`). Any other answer to another message fails with
|
|
240
|
+
* `FinTsProtocolError` before anything of it is kept: a relay or proxy that mixes up answers would hand the client
|
|
241
|
+
* another user's data, or a page of another query. Only the bank's dialog abort message may name the dialog or the
|
|
242
|
+
* message as unknown instead (`IncomingMessage3.aborts`), so that its text reaches the user.
|
|
243
|
+
*
|
|
244
|
+
* The dialog ends on the client's side, without a dialog end, where it is over for the bank: after a return code that
|
|
245
|
+
* ends it, such as 9800, a dialog abort message, a rejected opening message (Formals C.1.2), or a broken connection
|
|
246
|
+
* (C.6). A request that fails while the client is closing, as one it aborts (`FinTs3Protocol.beginClose`), leaves the
|
|
247
|
+
* dialog active instead, since the bank may have received it: its end follows under the next number. Not so where
|
|
248
|
+
* the request opened the dialog, whose ID only the answer tells.
|
|
249
|
+
*
|
|
250
|
+
* Only an answer that does not reject the whole message starts the idle time anew (`hasEnded`): a rejection may come
|
|
251
|
+
* from a bank that has ended the dialog without saying so.
|
|
252
|
+
*/
|
|
253
|
+
private async sendSegments(segments: readonly SegmentSpec[], tan?: string): Promise<IncomingMessage3> {
|
|
254
|
+
const number = this.messageNumber;
|
|
255
|
+
let answer: Uint8Array;
|
|
256
|
+
try {
|
|
257
|
+
answer = await this.protocol.post({
|
|
258
|
+
dialogId: this.id,
|
|
259
|
+
number,
|
|
260
|
+
segments,
|
|
261
|
+
signing: this.anonymous
|
|
262
|
+
? undefined
|
|
263
|
+
: { securityFunction: this.method.securityFunction, systemId: this.systemId },
|
|
264
|
+
tan,
|
|
265
|
+
});
|
|
266
|
+
} catch (error) {
|
|
267
|
+
if (error instanceof FinTsTransportError) {
|
|
268
|
+
if (this.protocol.isClosing && this.id !== unassigned) this.messageNumber++;
|
|
269
|
+
else this.discard();
|
|
270
|
+
}
|
|
271
|
+
throw error;
|
|
272
|
+
}
|
|
273
|
+
this.messageNumber++;
|
|
274
|
+
const response = parseBankData(() => IncomingMessage3.parse(answer));
|
|
275
|
+
const assigned = this.id === unassigned ? undefined : this.id;
|
|
276
|
+
const aborted = response.aborts(number, assigned);
|
|
277
|
+
if (!aborted && !response.answers(number, assigned)) {
|
|
278
|
+
throw new FinTsProtocolError('The answer belongs to another message than the one sent');
|
|
279
|
+
}
|
|
280
|
+
if (assigned === undefined) this.id = response.dialogId ?? unassigned;
|
|
281
|
+
if (!response.messageRejected) this.lastAnswer = Date.now();
|
|
282
|
+
if (aborted || endsDialog(response.feedback) || (number === 1 && response.messageRejected)) this.discard();
|
|
283
|
+
return response;
|
|
190
284
|
}
|
|
191
285
|
}
|
|
192
286
|
|