@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
package/src/v3/message.ts
CHANGED
|
@@ -2,13 +2,20 @@
|
|
|
2
2
|
|
|
3
3
|
import { BankMessage } from '../core/bank-info.js';
|
|
4
4
|
import { FinTsFeedback } from '../core/feedback.js';
|
|
5
|
-
import type { BankResponse } from '../session/protocol.js';
|
|
6
5
|
import { TanMethod } from '../tan/tan-models.js';
|
|
7
|
-
import { base64Encode,
|
|
6
|
+
import { base64Encode, concatBytes, latin1Encode, utf8Decode } from '../util/bytes.js';
|
|
8
7
|
import { formatFintsDate, formatFintsTime } from '../util/dates.js';
|
|
9
8
|
import { FormatError } from '../util/errors.js';
|
|
10
9
|
import { decodeBankText, decodeBase64Lines } from '../util/text.js';
|
|
11
|
-
import {
|
|
10
|
+
import {
|
|
11
|
+
Binary,
|
|
12
|
+
parsedElementBytes,
|
|
13
|
+
parseSegments,
|
|
14
|
+
type Segment,
|
|
15
|
+
SegmentSpec,
|
|
16
|
+
type SegmentValue,
|
|
17
|
+
Verbatim,
|
|
18
|
+
} from './syntax.js';
|
|
12
19
|
|
|
13
20
|
/** Signature data of a PIN/TAN message. */
|
|
14
21
|
export interface PinTanCredentials {
|
|
@@ -35,7 +42,7 @@ const issuer = 1;
|
|
|
35
42
|
|
|
36
43
|
/** Security identification: message sender (MS), empty CID, then the customer system ID. */
|
|
37
44
|
function sender(systemId: string): SegmentValue {
|
|
38
|
-
return [1, null, systemId];
|
|
45
|
+
return [1, null, new Verbatim(systemId)];
|
|
39
46
|
}
|
|
40
47
|
|
|
41
48
|
/** Security reference number, the same in every message. */
|
|
@@ -69,10 +76,14 @@ function keyName(c: PinTanCredentials, keyType: string): SegmentValue {
|
|
|
69
76
|
return [c.countryCode, c.bankCode, c.userId, keyType, 0, 0];
|
|
70
77
|
}
|
|
71
78
|
|
|
72
|
-
/** A random integer from 0 below `bound`, from a cryptographically secure source. */
|
|
79
|
+
/** A random integer from 0 below `bound`, each equally likely, from a cryptographically secure source. */
|
|
73
80
|
function secureRandom(bound: number): number {
|
|
81
|
+
// Values from the last multiple of `bound` on cover only the low remainders, which they would make more likely.
|
|
82
|
+
const limit = 2 ** 32 - (2 ** 32 % bound);
|
|
74
83
|
const values = new Uint32Array(1);
|
|
75
|
-
|
|
84
|
+
do {
|
|
85
|
+
crypto.getRandomValues(values);
|
|
86
|
+
} while (values[0]! >= limit);
|
|
76
87
|
return values[0]! % bound;
|
|
77
88
|
}
|
|
78
89
|
|
|
@@ -85,20 +96,21 @@ function securityControlReference(): string {
|
|
|
85
96
|
export class OutgoingMessage {
|
|
86
97
|
constructor(
|
|
87
98
|
readonly bytes: Uint8Array,
|
|
88
|
-
|
|
89
|
-
private readonly maskedParts: readonly Uint8Array[],
|
|
99
|
+
private readonly buildMasked: () => Uint8Array,
|
|
90
100
|
) {}
|
|
91
101
|
|
|
92
|
-
/**
|
|
102
|
+
/**
|
|
103
|
+
* The message with PIN and TAN masked; built only when a logger asks for it. It is decoded as a whole, as the bank's
|
|
104
|
+
* text is, so a message that mixes ISO 8859-1 text with UTF-8 text (`encodeText`) shows all of it as ISO 8859-1.
|
|
105
|
+
*/
|
|
93
106
|
get masked(): string {
|
|
94
|
-
return this.
|
|
107
|
+
return decodeBankText(this.buildMasked());
|
|
95
108
|
}
|
|
96
109
|
}
|
|
97
110
|
|
|
98
|
-
/** The segments between header and trailer,
|
|
111
|
+
/** The segments between header and trailer, and the number of the trailer. */
|
|
99
112
|
interface Body {
|
|
100
|
-
readonly
|
|
101
|
-
readonly masked: readonly Uint8Array[];
|
|
113
|
+
readonly segments: readonly Uint8Array[];
|
|
102
114
|
readonly nextNumber: number;
|
|
103
115
|
}
|
|
104
116
|
|
|
@@ -108,7 +120,8 @@ interface Body {
|
|
|
108
120
|
* With `security` the segments are signed with PIN and optional `tan` (HNSHK … HNSHA) and wrapped in the encryption
|
|
109
121
|
* envelope (HNVSK, HNVSD) that PIN/TAN uses without actual encryption.
|
|
110
122
|
*
|
|
111
|
-
* The masked message
|
|
123
|
+
* The masked message is built as the message is, from the same time stamp and security control reference, with `***`
|
|
124
|
+
* for PIN and TAN, so that the log does not tell how long they are.
|
|
112
125
|
*/
|
|
113
126
|
export function buildMessage(parts: {
|
|
114
127
|
dialogId: string;
|
|
@@ -118,54 +131,49 @@ export function buildMessage(parts: {
|
|
|
118
131
|
tan?: string | undefined;
|
|
119
132
|
now?: Date;
|
|
120
133
|
}): OutgoingMessage {
|
|
121
|
-
const
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
const
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
result.add(header(sizeOf([body.bytes])));
|
|
132
|
-
result.add(body.bytes);
|
|
133
|
-
result.add(trailer);
|
|
134
|
-
return new OutgoingMessage(result.takeBytes(), [header(sizeOf(body.masked)), ...body.masked, trailer]);
|
|
134
|
+
const { security, tan } = parts;
|
|
135
|
+
if (security === undefined) {
|
|
136
|
+
const bytes = withHeaderAndTrailer(parts, plainBody(parts.segments));
|
|
137
|
+
return new OutgoingMessage(bytes, () => bytes);
|
|
138
|
+
}
|
|
139
|
+
const time = parts.now ?? new Date();
|
|
140
|
+
const controlReference = securityControlReference();
|
|
141
|
+
const build = (pin: string, tanValue: string | undefined): Uint8Array =>
|
|
142
|
+
withHeaderAndTrailer(parts, signedBody(parts.segments, security, pin, tanValue, time, controlReference));
|
|
143
|
+
return new OutgoingMessage(build(security.pin, tan), () => build('***', tan === undefined ? undefined : '***'));
|
|
135
144
|
}
|
|
136
145
|
|
|
137
|
-
/**
|
|
138
|
-
function
|
|
139
|
-
|
|
146
|
+
/** `body` between the header and the trailer of message `messageNumber` of the dialog `dialogId`. */
|
|
147
|
+
function withHeaderAndTrailer(parts: { dialogId: string; messageNumber: number }, body: Body): Uint8Array {
|
|
148
|
+
const trailer = new SegmentSpec('HNHBS', 1, [parts.messageNumber]).build(body.nextNumber);
|
|
149
|
+
const header = (size: number): Uint8Array =>
|
|
150
|
+
new SegmentSpec('HNHBK', 3, [
|
|
151
|
+
size.toString().padStart(12, '0'),
|
|
152
|
+
300,
|
|
153
|
+
new Verbatim(parts.dialogId),
|
|
154
|
+
parts.messageNumber,
|
|
155
|
+
]).build(1);
|
|
156
|
+
// The header states the size of the whole message, itself included; its own size does not depend on it.
|
|
157
|
+
const size = header(0).length + body.segments.reduce((sum, s) => sum + s.length, 0) + trailer.length;
|
|
158
|
+
return concatBytes([header(size), ...body.segments, trailer]);
|
|
140
159
|
}
|
|
141
160
|
|
|
142
161
|
/** The segments of a message without security, as the anonymous dialog sends them. */
|
|
143
162
|
function plainBody(segments: readonly SegmentSpec[]): Body {
|
|
144
|
-
|
|
145
|
-
const bytes = new BytesBuilder();
|
|
146
|
-
built.forEach((b) => {
|
|
147
|
-
bytes.add(b);
|
|
148
|
-
});
|
|
149
|
-
return { bytes: bytes.takeBytes(), masked: built, nextNumber: built.length + 2 };
|
|
163
|
+
return { segments: segments.map((segment, i) => segment.build(i + 2)), nextNumber: segments.length + 2 };
|
|
150
164
|
}
|
|
151
165
|
|
|
152
|
-
/** The segments signed with
|
|
166
|
+
/** The segments signed with `pin` and optional `tan`, inside the encryption envelope. */
|
|
153
167
|
function signedBody(
|
|
154
168
|
segments: readonly SegmentSpec[],
|
|
155
169
|
security: PinTanCredentials,
|
|
170
|
+
pin: string,
|
|
156
171
|
tan: string | undefined,
|
|
157
172
|
time: Date,
|
|
173
|
+
controlReference: string,
|
|
158
174
|
): Body {
|
|
159
175
|
const profile = ['PIN', isTwoStep(security) ? 2 : 1];
|
|
160
|
-
const
|
|
161
|
-
const inner = new BytesBuilder();
|
|
162
|
-
const maskedInner: Uint8Array[] = [];
|
|
163
|
-
const addInner = (built: Uint8Array, masked?: Uint8Array): void => {
|
|
164
|
-
inner.add(built);
|
|
165
|
-
maskedInner.push(masked ?? built);
|
|
166
|
-
};
|
|
167
|
-
|
|
168
|
-
addInner(
|
|
176
|
+
const inner = [
|
|
169
177
|
new SegmentSpec('HNSHK', 4, [
|
|
170
178
|
profile,
|
|
171
179
|
security.securityFunction,
|
|
@@ -179,17 +187,10 @@ function signedBody(
|
|
|
179
187
|
signatureAlgorithm,
|
|
180
188
|
keyName(security, 'S'),
|
|
181
189
|
]).build(2),
|
|
182
|
-
|
|
190
|
+
];
|
|
183
191
|
let number = 3;
|
|
184
|
-
for (const segment of segments)
|
|
185
|
-
|
|
186
|
-
new SegmentSpec('HNSHA', 2, [controlReference, null, [pin, tanValue]]);
|
|
187
|
-
addInner(
|
|
188
|
-
signatureEnd(security.pin, tan).build(number),
|
|
189
|
-
signatureEnd('***', tan === undefined ? undefined : '***').build(number),
|
|
190
|
-
);
|
|
191
|
-
|
|
192
|
-
const innerBytes = inner.takeBytes();
|
|
192
|
+
for (const segment of segments) inner.push(segment.build(number++));
|
|
193
|
+
inner.push(new SegmentSpec('HNSHA', 2, [controlReference, null, [pin, tan]]).build(number));
|
|
193
194
|
const encryptionHead = new SegmentSpec('HNVSK', 3, [
|
|
194
195
|
profile,
|
|
195
196
|
plainText,
|
|
@@ -200,20 +201,12 @@ function signedBody(
|
|
|
200
201
|
keyName(security, 'V'),
|
|
201
202
|
noCompression,
|
|
202
203
|
]).build(998);
|
|
203
|
-
const
|
|
204
|
-
|
|
205
|
-
bytes.add(new SegmentSpec('HNVSD', 1, [new Binary(innerBytes)]).build(999));
|
|
206
|
-
const masked = [
|
|
207
|
-
encryptionHead,
|
|
208
|
-
latin1Encode(`HNVSD:999:1+@${lengthOf(maskedInner)}@`),
|
|
209
|
-
...maskedInner,
|
|
210
|
-
latin1Encode("'"),
|
|
211
|
-
];
|
|
212
|
-
return { bytes: bytes.takeBytes(), masked, nextNumber: number + 1 };
|
|
204
|
+
const encryptedData = new SegmentSpec('HNVSD', 1, [new Binary(concatBytes(inner))]).build(999);
|
|
205
|
+
return { segments: [encryptionHead, encryptedData], nextNumber: number + 1 };
|
|
213
206
|
}
|
|
214
207
|
|
|
215
208
|
/** A received message, with the segments of the encryption envelope unpacked. */
|
|
216
|
-
export class IncomingMessage3
|
|
209
|
+
export class IncomingMessage3 {
|
|
217
210
|
/**
|
|
218
211
|
* Return codes for the whole message (HIRMG), then for single request segments (HIRMS).
|
|
219
212
|
*
|
|
@@ -222,10 +215,12 @@ export class IncomingMessage3 implements BankResponse {
|
|
|
222
215
|
*/
|
|
223
216
|
readonly feedback: readonly FinTsFeedback[];
|
|
224
217
|
|
|
218
|
+
/** Whether the bank rejected the message as a whole (an error in HIRMG), rather than single orders in it. */
|
|
225
219
|
readonly messageRejected: boolean;
|
|
226
220
|
|
|
227
221
|
private constructor(
|
|
228
222
|
readonly segments: readonly Segment[],
|
|
223
|
+
/** The memory the message holds, in bytes: its length, and what its parts take beyond that. */
|
|
229
224
|
readonly size: number,
|
|
230
225
|
) {
|
|
231
226
|
this.feedback = Object.freeze(
|
|
@@ -238,34 +233,95 @@ export class IncomingMessage3 implements BankResponse {
|
|
|
238
233
|
}
|
|
239
234
|
|
|
240
235
|
/**
|
|
241
|
-
* Parses a message; throws `FormatError` if `data` is not
|
|
242
|
-
* maintenance notice
|
|
236
|
+
* Parses a message; throws `FormatError` if `data` is not one complete FinTS message, such as the HTML page of a
|
|
237
|
+
* proxy, a maintenance notice, a message cut short, which lacks the message trailer (HNHBS) at its end, or two
|
|
238
|
+
* messages run together, whose second would pass for part of the first. It does as well for more than one encrypted
|
|
239
|
+
* part (HNVSD), each of which `parseSegments` would allow as many data elements as the whole message.
|
|
240
|
+
*
|
|
241
|
+
* Its `size` counts the memory of the data elements it keeps along with its bytes, since a message of many short
|
|
242
|
+
* elements takes far more memory than bytes.
|
|
243
243
|
*/
|
|
244
244
|
static parse(data: Uint8Array): IncomingMessage3 {
|
|
245
|
-
|
|
245
|
+
let outer: Segment[] = [];
|
|
246
|
+
try {
|
|
247
|
+
outer = parseSegments(data);
|
|
248
|
+
} catch (error) {
|
|
249
|
+
// Text that is no FinTS message, such as a web page, seldom ends with a segment terminator; that it has no
|
|
250
|
+
// message header says more than what the parser trips over first.
|
|
251
|
+
if (!(error instanceof FormatError) || startsWithMessageHeader(data)) throw error;
|
|
252
|
+
}
|
|
246
253
|
if (outer[0]?.type !== 'HNHBK') throw new FormatError('Not a FinTS message: no message header');
|
|
254
|
+
if (outer.at(-1)?.type !== 'HNHBS') throw new FormatError('Incomplete FinTS message: no message trailer');
|
|
255
|
+
if (outer.filter((s) => s.type === 'HNHBK' || s.type === 'HNHBS').length > 2) {
|
|
256
|
+
throw new FormatError('More than one FinTS message');
|
|
257
|
+
}
|
|
258
|
+
if (outer.filter((s) => s.type === 'HNVSD').length > 1) {
|
|
259
|
+
throw new FormatError('More than one encrypted part (HNVSD)');
|
|
260
|
+
}
|
|
247
261
|
const segments: Segment[] = [];
|
|
248
262
|
for (const s of outer) {
|
|
249
|
-
if (s.type
|
|
250
|
-
const payload = s.binary(1);
|
|
251
|
-
if (payload !== undefined) segments.push(...parseSegments(payload));
|
|
252
|
-
} else {
|
|
263
|
+
if (s.type !== 'HNVSD') {
|
|
253
264
|
segments.push(s);
|
|
265
|
+
continue;
|
|
254
266
|
}
|
|
267
|
+
const payload = s.binary(1);
|
|
268
|
+
if (payload === undefined) throw new FormatError('Encrypted data (HNVSD) without binary data');
|
|
269
|
+
for (const inner of parseSegments(payload)) segments.push(inner);
|
|
255
270
|
}
|
|
256
|
-
|
|
271
|
+
let elements = 0;
|
|
272
|
+
for (const s of segments) for (const group of s.elements) elements += group.length;
|
|
273
|
+
return new IncomingMessage3(segments, data.length + elements * parsedElementBytes);
|
|
257
274
|
}
|
|
258
275
|
|
|
259
276
|
static combine(first: IncomingMessage3, second: IncomingMessage3): IncomingMessage3 {
|
|
260
277
|
return new IncomingMessage3([...first.segments, ...second.segments], first.size + second.size);
|
|
261
278
|
}
|
|
262
279
|
|
|
280
|
+
/** Free-text messages the bank addresses to the user. */
|
|
263
281
|
get bankMessages(): BankMessage[] {
|
|
264
282
|
return this.findAll('HIKIM').map((m) => new BankMessage(m.text(1) ?? '', m.text(2) ?? ''));
|
|
265
283
|
}
|
|
266
284
|
|
|
285
|
+
/** The dialog ID the bank assigned, as `Segment.verbatim` reads it, to send back byte for byte. */
|
|
267
286
|
get dialogId(): string | undefined {
|
|
268
|
-
return this.find('HNHBK')?.
|
|
287
|
+
return this.find('HNHBK')?.verbatim(3);
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
/**
|
|
291
|
+
* Whether the message answers message `number` of the dialog `dialogId`: header and trailer carry that number, and the
|
|
292
|
+
* header names that message where it names the message it refers to (Bezugsnachricht). The dialog ID counts only once
|
|
293
|
+
* the bank has assigned one; before, `dialogId` is `undefined`, since every first message goes out under the same.
|
|
294
|
+
*/
|
|
295
|
+
answers(number: number, dialogId: string | undefined): boolean {
|
|
296
|
+
const header = this.find('HNHBK');
|
|
297
|
+
// The trailer comes last, after the segments of the encryption envelope, which might hold one of their own.
|
|
298
|
+
const trailer = this.segments.at(-1);
|
|
299
|
+
if (header === undefined || trailer?.type !== 'HNHBS') return false;
|
|
300
|
+
const inDialog = (id: string | undefined): boolean => dialogId === undefined || id === dialogId;
|
|
301
|
+
const namesReference = header.group(5).some((e) => !e.isEmpty);
|
|
302
|
+
return (
|
|
303
|
+
header.integer(4) === number &&
|
|
304
|
+
trailer.integer(1) === number &&
|
|
305
|
+
inDialog(header.verbatim(3)) &&
|
|
306
|
+
(!namesReference || (header.integer(5, 1) === number && inDialog(header.verbatim(5, 0))))
|
|
307
|
+
);
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
/**
|
|
311
|
+
* Whether the message is the bank's dialog abort message (Formals B.7.6) for message `number` of the dialog
|
|
312
|
+
* `dialogId`: unencrypted, with nothing but the header, an error for the whole message (HIRMG) and the trailer. Its
|
|
313
|
+
* header names the dialog ID and the message number where the bank knows them, and `unbekannt` and 9999 where not.
|
|
314
|
+
*/
|
|
315
|
+
aborts(number: number, dialogId: string | undefined): boolean {
|
|
316
|
+
const header = this.segments[0]!;
|
|
317
|
+
const id = header.verbatim(3);
|
|
318
|
+
const messageNumber = header.integer(4);
|
|
319
|
+
return (
|
|
320
|
+
this.segments.length === 3 &&
|
|
321
|
+
this.messageRejected &&
|
|
322
|
+
(id === 'unbekannt' || id === dialogId) &&
|
|
323
|
+
(messageNumber === 9999 || messageNumber === number)
|
|
324
|
+
);
|
|
269
325
|
}
|
|
270
326
|
|
|
271
327
|
find(type: string): Segment | undefined {
|
|
@@ -277,6 +333,10 @@ export class IncomingMessage3 implements BankResponse {
|
|
|
277
333
|
}
|
|
278
334
|
}
|
|
279
335
|
|
|
336
|
+
/**
|
|
337
|
+
* The return codes of `s`. Their parameters are values the client or the app sends back, such as the position to
|
|
338
|
+
* continue at or new login data, so they are read as `Segment.verbatim` reads them.
|
|
339
|
+
*/
|
|
280
340
|
function feedbackOf(s: Segment): FinTsFeedback[] {
|
|
281
341
|
const feedback: FinTsFeedback[] = [];
|
|
282
342
|
for (let i = 1; i < s.length; i++) {
|
|
@@ -284,7 +344,7 @@ function feedbackOf(s: Segment): FinTsFeedback[] {
|
|
|
284
344
|
if (code === undefined) continue;
|
|
285
345
|
const parameters: string[] = [];
|
|
286
346
|
// Empty parameters stay, as empty text, so that each keeps its position.
|
|
287
|
-
for (let j = 3; j < s.group(i).length; j++) parameters.push(s.
|
|
347
|
+
for (let j = 3; j < s.group(i).length; j++) parameters.push(s.verbatim(i, j) ?? '');
|
|
288
348
|
feedback.push(new FinTsFeedback({ code, reference: s.text(i, 1), text: s.text(i, 2) ?? '', parameters }));
|
|
289
349
|
}
|
|
290
350
|
return feedback;
|
|
@@ -298,7 +358,8 @@ function feedbackOf(s: Segment): FinTsFeedback[] {
|
|
|
298
358
|
export function decodeTransport(body: Uint8Array): Uint8Array {
|
|
299
359
|
if (startsWithMessageHeader(body)) return body;
|
|
300
360
|
try {
|
|
301
|
-
|
|
361
|
+
// Base64 is ASCII, which UTF-8 decodes natively; anything else is no base64 either way.
|
|
362
|
+
return decodeBase64Lines(utf8Decode(body, true));
|
|
302
363
|
} catch (error) {
|
|
303
364
|
if (error instanceof FormatError) return body;
|
|
304
365
|
throw error;
|
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
// Internal to the package, and named after the FinTS specification. Documentation is where the meaning is not evident.
|
|
2
|
+
|
|
3
|
+
import type { FinTsConfig } from '../core/config.js';
|
|
4
|
+
import { FormatError } from '../util/errors.js';
|
|
5
|
+
import { readOptionalBoolean, readOptionalObject, readOptionalString, type JsonObject } from '../util/json.js';
|
|
6
|
+
import { trimBlanks } from '../util/text.js';
|
|
7
|
+
import type { IncomingMessage3 } from './message.js';
|
|
8
|
+
import { Bpd3, decodeSegments, Upd3 } from './parameters.js';
|
|
9
|
+
import { Verbatim } from './syntax.js';
|
|
10
|
+
|
|
11
|
+
/** The limits the bank parameter data set on the messages the client sends (`ParameterStore.limits`). */
|
|
12
|
+
export interface MessageLimits {
|
|
13
|
+
/** The shortest and longest PIN a login may carry. */
|
|
14
|
+
readonly pinLength: { readonly min: number | undefined; readonly max: number | undefined } | undefined;
|
|
15
|
+
|
|
16
|
+
/** The size of the largest message, in kilobytes. */
|
|
17
|
+
readonly maxMessageSize: number | undefined;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* The parameter data and IDs the bank sends the user `config` names, which the messages and business transactions
|
|
22
|
+
* take, and which the saved state keeps: the bank and user parameter data, the customer system ID, and the names of
|
|
23
|
+
* the TAN media as the bank sent them.
|
|
24
|
+
*/
|
|
25
|
+
export class ParameterStore {
|
|
26
|
+
private systemIdValue: string | undefined;
|
|
27
|
+
private bpd: Bpd3 | undefined;
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Whether the bank parameter data came from the anonymous dialog. They serve until those of a dialog signed with the
|
|
31
|
+
* PIN come: the bank may send a user other ones (PIN/TAN B.4.3.1), and the anonymous dialog may have had to do
|
|
32
|
+
* without the announcement of strong customer authentication. So signed dialogs report version 0 for them, which has
|
|
33
|
+
* the bank send its parameter data anew.
|
|
34
|
+
*/
|
|
35
|
+
private bpdAnonymous = false;
|
|
36
|
+
|
|
37
|
+
/** The limits a refresh took from the anonymous dialog's parameter data (`keepRefreshedLimits`). */
|
|
38
|
+
private refreshedLimits: MessageLimits | undefined;
|
|
39
|
+
private upd: Upd3 | undefined;
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* The names of the TAN media in the bank's last list whose text would not go back as the bank sent them, by their
|
|
43
|
+
* text, as `parseTanMedia` notes them (`readTanMediumNames` for a state of 0.2.0). Saved with the state, as the
|
|
44
|
+
* medium in use is.
|
|
45
|
+
*/
|
|
46
|
+
private tanMediumNames = new Map<string, string>();
|
|
47
|
+
|
|
48
|
+
constructor(readonly config: FinTsConfig) {}
|
|
49
|
+
|
|
50
|
+
/** The bank parameter data received so far, if any. */
|
|
51
|
+
get bankParameters(): Bpd3 | undefined {
|
|
52
|
+
return this.bpd;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** The user parameter data received so far, if any. */
|
|
56
|
+
get userParameters(): Upd3 | undefined {
|
|
57
|
+
return this.upd;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* The customer system ID the bank assigned in the synchronization, as `Segment.verbatim` reads it; `undefined`
|
|
62
|
+
* before. No dialog signed with the PIN starts without it.
|
|
63
|
+
*/
|
|
64
|
+
get systemId(): string | undefined {
|
|
65
|
+
return this.systemIdValue;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* The limits on the messages the client sends: those of the bank parameter data, unless a refresh took others
|
|
70
|
+
* (`keepRefreshedLimits`).
|
|
71
|
+
*/
|
|
72
|
+
get limits(): MessageLimits {
|
|
73
|
+
const bpd = this.bpd;
|
|
74
|
+
return this.refreshedLimits ?? { pinLength: bpd?.pinTan.pinLength, maxMessageSize: bpd?.bankInfo.maxMessageSize };
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* The version of the bank parameter data a dialog reports: 0 where there are none, only the anonymous dialog's, or
|
|
79
|
+
* where limits a refresh took stand in for theirs (`keepRefreshedLimits`).
|
|
80
|
+
*/
|
|
81
|
+
get reportedBpdVersion(): number {
|
|
82
|
+
return this.bpd === undefined || this.bpdAnonymous || this.refreshedLimits !== undefined ? 0 : this.bpd.version;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* The version of the user parameter data a signed dialog reports: 0 where there are none, or where they name another
|
|
87
|
+
* user ID than the one logging in, so that the bank sends the user's anew. They may be another user's, as where a
|
|
88
|
+
* relay mixed up the answers to the synchronizations of two users and the bank takes the other user's customer system
|
|
89
|
+
* ID. A bank that names the user by another ID than the login's, such as for an alias, only sends its user parameter
|
|
90
|
+
* data at every login.
|
|
91
|
+
*/
|
|
92
|
+
get reportedUpdVersion(): number {
|
|
93
|
+
const upd = this.upd;
|
|
94
|
+
return upd !== undefined && upd.userId === this.config.userId ? upd.version : 0;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Drops the customer system ID the bank no longer accepts, so that a synchronization is required again, and with it
|
|
99
|
+
* the user parameter data, which the next login delivers anew: what came along with an ID the bank does not know may
|
|
100
|
+
* not be the user's either (`reportedUpdVersion`). Returns whether there was an ID.
|
|
101
|
+
*/
|
|
102
|
+
forgetSystemId(): boolean {
|
|
103
|
+
const had = this.systemIdValue !== undefined;
|
|
104
|
+
this.systemIdValue = undefined;
|
|
105
|
+
this.upd = undefined;
|
|
106
|
+
return had;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Keeps the parameter data and IDs in `response`, an answer to the opening of a signed dialog: the login, which may be
|
|
111
|
+
* a synchronization, or its approval. The bank sends them there (Formals C.3.1); taken from the answer to an order,
|
|
112
|
+
* the parameter data a bank repeats there in part would replace the complete ones.
|
|
113
|
+
*/
|
|
114
|
+
keep(response: IncomingMessage3): void {
|
|
115
|
+
this.keepBankParameters(response, false);
|
|
116
|
+
this.upd = Upd3.fromSegments(response.segments, this.config.bankCode, this.config.countryCode) ?? this.upd;
|
|
117
|
+
this.systemIdValue = response.find('HISYN')?.verbatim(1) ?? this.systemIdValue;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Keeps the bank parameter data in `response`, the answer of an anonymous dialog, where there are none from a dialog
|
|
122
|
+
* signed with the PIN: until those come, the dialogs signed with the PIN have the bank send its parameter data anew,
|
|
123
|
+
* since it may send a user other ones (PIN/TAN B.4.3.1). Keeps nothing that belongs to a user.
|
|
124
|
+
*/
|
|
125
|
+
keepAnonymous(response: IncomingMessage3): void {
|
|
126
|
+
if (this.bpd === undefined || this.bpdAnonymous) this.keepBankParameters(response, true);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Keeps the limits of the bank parameter data in `response`, the answer of an anonymous dialog the client asked for
|
|
131
|
+
* after the limits kept ruled out a login: those may be outdated, as saved ones are. Parameter data of the version
|
|
132
|
+
* kept tell nothing new, and the bank may send a user other ones than the anonymous dialog (PIN/TAN B.4.3.1), so of
|
|
133
|
+
* those of another version it keeps the limits only, and the PIN lengths only where they state them (HIPINS). They
|
|
134
|
+
* stand in for those kept until a dialog signed with the PIN brings parameter data.
|
|
135
|
+
*/
|
|
136
|
+
keepRefreshedLimits(response: IncomingMessage3): void {
|
|
137
|
+
const fresh = Bpd3.fromSegments(response.segments, this.config.bankCode);
|
|
138
|
+
if (fresh === undefined || fresh.version === this.bpd?.version) return;
|
|
139
|
+
this.refreshedLimits = {
|
|
140
|
+
pinLength: fresh.pinTan.pinLength ?? this.limits.pinLength,
|
|
141
|
+
maxMessageSize: fresh.bankInfo.maxMessageSize,
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
private keepBankParameters(response: IncomingMessage3, anonymous: boolean): void {
|
|
146
|
+
const bpd = Bpd3.fromSegments(response.segments, this.config.bankCode);
|
|
147
|
+
if (bpd === undefined) return;
|
|
148
|
+
this.bpd = bpd;
|
|
149
|
+
this.bpdAnonymous = anonymous;
|
|
150
|
+
this.refreshedLimits = undefined;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/** Keeps `names`, the names of the bank's latest list of TAN media as `parseTanMedia` notes them. */
|
|
154
|
+
keepTanMediumNames(names: Map<string, string>): void {
|
|
155
|
+
this.tanMediumNames = names;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* The TAN medium `name` to send: as the bank sent it, where its last list of TAN media noted that (`parseTanMedia`);
|
|
160
|
+
* otherwise as text, which goes out without the blanks at either end a name the app passes may have.
|
|
161
|
+
*/
|
|
162
|
+
tanMediumToSend(name: string): string | Verbatim {
|
|
163
|
+
const asSent = this.tanMediumNames.get(name);
|
|
164
|
+
return asSent === undefined ? name : new Verbatim(asSent);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/** The parameter data and IDs as JSON data, for the client's saved state. */
|
|
168
|
+
saveState(): JsonObject {
|
|
169
|
+
return {
|
|
170
|
+
systemId: this.systemIdValue ?? null,
|
|
171
|
+
bpd: this.bpd?.encoded ?? null,
|
|
172
|
+
bpdAnonymous: this.bpdAnonymous,
|
|
173
|
+
upd: this.upd?.encoded ?? null,
|
|
174
|
+
tanMediumNames: Object.fromEntries(this.tanMediumNames),
|
|
175
|
+
[blanksNotedKey]: true,
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Restores what `saveState` saved, with `tanMedium`, the medium in use the state holds; throws if `saved` is damaged,
|
|
181
|
+
* leaving the store unchanged.
|
|
182
|
+
*/
|
|
183
|
+
restoreState(saved: JsonObject, tanMedium?: string): void {
|
|
184
|
+
const bpd = readOptionalString(saved, 'bpd');
|
|
185
|
+
const upd = readOptionalString(saved, 'upd');
|
|
186
|
+
const restoredSystemId = readOptionalString(saved, 'systemId');
|
|
187
|
+
// A state of fints-ts 0.1 or the Dart package 0.2.0 does not tell, so its parameter data are fetched anew.
|
|
188
|
+
const restoredBpdAnonymous = readOptionalBoolean(saved, 'bpdAnonymous') ?? true;
|
|
189
|
+
const restoredBpd = bpd === undefined ? undefined : Bpd3.fromSegments(decodeSegments(bpd), this.config.bankCode);
|
|
190
|
+
const restoredUpd =
|
|
191
|
+
upd === undefined
|
|
192
|
+
? undefined
|
|
193
|
+
: Upd3.fromSegments(decodeSegments(upd), this.config.bankCode, this.config.countryCode);
|
|
194
|
+
const restoredNames = readTanMediumNames(saved, tanMedium);
|
|
195
|
+
this.systemIdValue = restoredSystemId;
|
|
196
|
+
this.bpd = restoredBpd;
|
|
197
|
+
this.bpdAnonymous = restoredBpdAnonymous;
|
|
198
|
+
this.refreshedLimits = undefined;
|
|
199
|
+
this.upd = restoredUpd;
|
|
200
|
+
this.tanMediumNames = restoredNames;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/** The key of the saved state that tells the TAN medium names apart from those version 0.2.0 saved. */
|
|
205
|
+
const blanksNotedKey = 'tanMediumBlanksNoted';
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* The TAN medium names `saveState` saved; none in a state of fints-ts 0.1 or the Dart package 0.2.0.
|
|
209
|
+
*
|
|
210
|
+
* Version 0.2.0 noted no name with blanks at either end, and sent every name as it is, so `tanMedium` keeps going out
|
|
211
|
+
* with its blanks from such a state: the bank may have listed it so.
|
|
212
|
+
*/
|
|
213
|
+
function readTanMediumNames(saved: JsonObject, tanMedium: string | undefined): Map<string, string> {
|
|
214
|
+
const listed = readOptionalObject(saved, 'tanMediumNames');
|
|
215
|
+
const names = new Map(
|
|
216
|
+
Object.entries(listed ?? {}).map(([name, asSent]) => {
|
|
217
|
+
if (typeof asSent !== 'string') throw new FormatError('Saved state: a TAN medium name is not a string');
|
|
218
|
+
return [name, asSent];
|
|
219
|
+
}),
|
|
220
|
+
);
|
|
221
|
+
const savedBy020 = listed !== undefined && readOptionalBoolean(saved, blanksNotedKey) !== true;
|
|
222
|
+
if (savedBy020 && tanMedium !== undefined && trimBlanks(tanMedium) !== tanMedium && !names.has(tanMedium)) {
|
|
223
|
+
names.set(tanMedium, tanMedium);
|
|
224
|
+
}
|
|
225
|
+
return names;
|
|
226
|
+
}
|