@coderoftime/fints 0.2.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 +377 -0
- package/README.md +31 -23
- package/dist/client.d.ts +3 -2
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +52 -9
- package/dist/client.js.map +1 -1
- package/dist/core/account.d.ts +15 -4
- package/dist/core/account.d.ts.map +1 -1
- package/dist/core/account.js +84 -36
- package/dist/core/account.js.map +1 -1
- package/dist/core/amount.d.ts +13 -3
- package/dist/core/amount.d.ts.map +1 -1
- package/dist/core/amount.js +42 -8
- package/dist/core/amount.js.map +1 -1
- package/dist/core/balance.d.ts +3 -0
- package/dist/core/balance.d.ts.map +1 -1
- package/dist/core/balance.js +5 -0
- package/dist/core/balance.js.map +1 -1
- package/dist/core/bank-info.d.ts +20 -3
- package/dist/core/bank-info.d.ts.map +1 -1
- package/dist/core/bank-info.js +25 -4
- package/dist/core/bank-info.js.map +1 -1
- package/dist/core/client.d.ts +42 -15
- package/dist/core/client.d.ts.map +1 -1
- package/dist/core/config.d.ts +15 -7
- package/dist/core/config.d.ts.map +1 -1
- package/dist/core/config.js +29 -14
- package/dist/core/config.js.map +1 -1
- package/dist/core/errors.d.ts +17 -5
- package/dist/core/errors.d.ts.map +1 -1
- package/dist/core/errors.js +25 -7
- package/dist/core/errors.js.map +1 -1
- package/dist/core/feedback.d.ts +7 -0
- package/dist/core/feedback.d.ts.map +1 -1
- package/dist/core/feedback.js +10 -1
- package/dist/core/feedback.js.map +1 -1
- package/dist/core/transaction.d.ts +5 -0
- package/dist/core/transaction.d.ts.map +1 -1
- package/dist/core/transaction.js +12 -0
- package/dist/core/transaction.js.map +1 -1
- package/dist/core/transport.d.ts +22 -7
- package/dist/core/transport.d.ts.map +1 -1
- package/dist/core/transport.js +51 -11
- package/dist/core/transport.js.map +1 -1
- package/dist/formats/camt.d.ts +7 -4
- package/dist/formats/camt.d.ts.map +1 -1
- package/dist/formats/camt.js +53 -54
- package/dist/formats/camt.js.map +1 -1
- package/dist/formats/mt940.d.ts +18 -6
- package/dist/formats/mt940.d.ts.map +1 -1
- package/dist/formats/mt940.js +83 -66
- package/dist/formats/mt940.js.map +1 -1
- package/dist/formats/readers.d.ts.map +1 -1
- package/dist/formats/readers.js +6 -2
- package/dist/formats/readers.js.map +1 -1
- package/dist/formats/sepa-purpose.d.ts +7 -4
- package/dist/formats/sepa-purpose.d.ts.map +1 -1
- package/dist/formats/sepa-purpose.js +18 -9
- package/dist/formats/sepa-purpose.js.map +1 -1
- package/dist/session/dialog-runner.d.ts +5 -2
- package/dist/session/dialog-runner.d.ts.map +1 -1
- package/dist/session/dialog-runner.js +23 -7
- package/dist/session/dialog-runner.js.map +1 -1
- package/dist/session/login-guard.d.ts +4 -4
- package/dist/session/login-guard.d.ts.map +1 -1
- package/dist/session/login-guard.js +29 -14
- package/dist/session/login-guard.js.map +1 -1
- package/dist/session/session-setup.d.ts +14 -5
- package/dist/session/session-setup.d.ts.map +1 -1
- package/dist/session/session-setup.js +30 -9
- package/dist/session/session-setup.js.map +1 -1
- package/dist/session/session-state.d.ts +4 -3
- package/dist/session/session-state.d.ts.map +1 -1
- package/dist/session/session-state.js +28 -9
- package/dist/session/session-state.js.map +1 -1
- package/dist/session/signed-dialog.d.ts +19 -13
- package/dist/session/signed-dialog.d.ts.map +1 -1
- package/dist/session/signed-dialog.js +45 -30
- package/dist/session/signed-dialog.js.map +1 -1
- package/dist/session/user-prompts.d.ts +6 -1
- package/dist/session/user-prompts.d.ts.map +1 -1
- package/dist/session/user-prompts.js +19 -4
- package/dist/session/user-prompts.js.map +1 -1
- package/dist/tan/challenge.d.ts +15 -13
- package/dist/tan/challenge.d.ts.map +1 -1
- package/dist/tan/challenge.js +32 -26
- package/dist/tan/challenge.js.map +1 -1
- package/dist/tan/flicker.d.ts +5 -1
- package/dist/tan/flicker.d.ts.map +1 -1
- package/dist/tan/flicker.js +42 -30
- package/dist/tan/flicker.js.map +1 -1
- package/dist/tan/tan-image.d.ts +4 -1
- package/dist/tan/tan-image.d.ts.map +1 -1
- package/dist/tan/tan-image.js +8 -2
- package/dist/tan/tan-image.js.map +1 -1
- package/dist/tan/tan-models.d.ts +9 -1
- package/dist/tan/tan-models.d.ts.map +1 -1
- package/dist/tan/tan-models.js +14 -2
- 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 -1
- package/dist/util/async.d.ts.map +1 -1
- package/dist/util/async.js +6 -3
- package/dist/util/async.js.map +1 -1
- package/dist/util/bytes.d.ts +1 -1
- package/dist/util/bytes.d.ts.map +1 -1
- package/dist/util/bytes.js +3 -3
- package/dist/util/bytes.js.map +1 -1
- package/dist/util/dates.d.ts +3 -10
- package/dist/util/dates.d.ts.map +1 -1
- package/dist/util/dates.js +49 -39
- package/dist/util/dates.js.map +1 -1
- package/dist/util/errors.d.ts +5 -3
- package/dist/util/errors.d.ts.map +1 -1
- package/dist/util/errors.js +8 -4
- 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/text.d.ts +18 -2
- package/dist/util/text.d.ts.map +1 -1
- package/dist/util/text.js +50 -5
- package/dist/util/text.js.map +1 -1
- package/dist/util/xml.d.ts +6 -4
- package/dist/util/xml.d.ts.map +1 -1
- package/dist/util/xml.js +106 -44
- package/dist/util/xml.js.map +1 -1
- package/dist/v3/bank-connection.d.ts +23 -3
- package/dist/v3/bank-connection.d.ts.map +1 -1
- package/dist/v3/bank-connection.js +66 -6
- package/dist/v3/bank-connection.js.map +1 -1
- package/dist/v3/dialog.d.ts +6 -2
- package/dist/v3/dialog.d.ts.map +1 -1
- package/dist/v3/dialog.js +20 -9
- package/dist/v3/dialog.js.map +1 -1
- package/dist/v3/message.d.ts +18 -10
- package/dist/v3/message.d.ts.map +1 -1
- package/dist/v3/message.js +92 -75
- package/dist/v3/message.js.map +1 -1
- package/dist/v3/parameter-store.d.ts +43 -7
- package/dist/v3/parameter-store.d.ts.map +1 -1
- package/dist/v3/parameter-store.js +69 -15
- package/dist/v3/parameter-store.js.map +1 -1
- package/dist/v3/parameters.d.ts +12 -11
- package/dist/v3/parameters.d.ts.map +1 -1
- package/dist/v3/parameters.js +57 -35
- package/dist/v3/parameters.js.map +1 -1
- package/dist/v3/protocol.d.ts +16 -9
- package/dist/v3/protocol.d.ts.map +1 -1
- package/dist/v3/protocol.js +57 -26
- package/dist/v3/protocol.js.map +1 -1
- package/dist/v3/queries.d.ts +2 -2
- package/dist/v3/queries.d.ts.map +1 -1
- package/dist/v3/queries.js +24 -16
- package/dist/v3/queries.js.map +1 -1
- package/dist/v3/segments.d.ts +18 -11
- package/dist/v3/segments.d.ts.map +1 -1
- package/dist/v3/segments.js +30 -21
- package/dist/v3/segments.js.map +1 -1
- package/dist/v3/syntax.d.ts +15 -11
- package/dist/v3/syntax.d.ts.map +1 -1
- package/dist/v3/syntax.js +30 -15
- package/dist/v3/syntax.js.map +1 -1
- package/package.json +1 -1
- package/src/client.ts +58 -17
- package/src/core/account.ts +91 -35
- package/src/core/amount.ts +42 -9
- package/src/core/balance.ts +6 -0
- package/src/core/bank-info.ts +28 -4
- package/src/core/client.ts +42 -15
- package/src/core/config.ts +37 -18
- package/src/core/errors.ts +27 -7
- package/src/core/feedback.ts +12 -1
- package/src/core/transaction.ts +13 -0
- package/src/core/transport.ts +62 -16
- package/src/formats/camt.ts +62 -51
- package/src/formats/mt940.ts +114 -78
- package/src/formats/readers.ts +6 -2
- package/src/formats/sepa-purpose.ts +17 -9
- package/src/session/dialog-runner.ts +34 -7
- package/src/session/login-guard.ts +29 -13
- package/src/session/session-setup.ts +27 -9
- package/src/session/session-state.ts +25 -8
- package/src/session/signed-dialog.ts +60 -30
- package/src/session/user-prompts.ts +28 -3
- package/src/tan/challenge.ts +50 -36
- package/src/tan/flicker.ts +40 -29
- package/src/tan/tan-image.ts +9 -2
- package/src/tan/tan-models.ts +16 -2
- package/src/util/arguments.ts +40 -0
- package/src/util/async.ts +10 -4
- package/src/util/bytes.ts +3 -3
- package/src/util/dates.ts +52 -38
- package/src/util/errors.ts +9 -4
- package/src/util/immutable-map.ts +15 -12
- package/src/util/inspect.ts +14 -0
- package/src/util/text.ts +47 -5
- package/src/util/xml.ts +105 -38
- package/src/v3/bank-connection.ts +65 -6
- package/src/v3/dialog.ts +18 -8
- package/src/v3/message.ts +81 -72
- package/src/v3/parameter-store.ts +82 -16
- package/src/v3/parameters.ts +68 -41
- package/src/v3/protocol.ts +61 -28
- package/src/v3/queries.ts +26 -18
- package/src/v3/segments.ts +32 -20
- package/src/v3/syntax.ts +31 -14
package/src/v3/message.ts
CHANGED
|
@@ -3,14 +3,13 @@
|
|
|
3
3
|
import { BankMessage } from '../core/bank-info.js';
|
|
4
4
|
import { FinTsFeedback } from '../core/feedback.js';
|
|
5
5
|
import { TanMethod } from '../tan/tan-models.js';
|
|
6
|
-
import { base64Encode,
|
|
6
|
+
import { base64Encode, concatBytes, latin1Encode, utf8Decode } from '../util/bytes.js';
|
|
7
7
|
import { formatFintsDate, formatFintsTime } from '../util/dates.js';
|
|
8
8
|
import { FormatError } from '../util/errors.js';
|
|
9
9
|
import { decodeBankText, decodeBase64Lines } from '../util/text.js';
|
|
10
10
|
import {
|
|
11
11
|
Binary,
|
|
12
|
-
|
|
13
|
-
maxDataElements,
|
|
12
|
+
parsedElementBytes,
|
|
14
13
|
parseSegments,
|
|
15
14
|
type Segment,
|
|
16
15
|
SegmentSpec,
|
|
@@ -77,10 +76,14 @@ function keyName(c: PinTanCredentials, keyType: string): SegmentValue {
|
|
|
77
76
|
return [c.countryCode, c.bankCode, c.userId, keyType, 0, 0];
|
|
78
77
|
}
|
|
79
78
|
|
|
80
|
-
/** 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. */
|
|
81
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);
|
|
82
83
|
const values = new Uint32Array(1);
|
|
83
|
-
|
|
84
|
+
do {
|
|
85
|
+
crypto.getRandomValues(values);
|
|
86
|
+
} while (values[0]! >= limit);
|
|
84
87
|
return values[0]! % bound;
|
|
85
88
|
}
|
|
86
89
|
|
|
@@ -93,20 +96,21 @@ function securityControlReference(): string {
|
|
|
93
96
|
export class OutgoingMessage {
|
|
94
97
|
constructor(
|
|
95
98
|
readonly bytes: Uint8Array,
|
|
96
|
-
|
|
97
|
-
private readonly maskedParts: readonly Uint8Array[],
|
|
99
|
+
private readonly buildMasked: () => Uint8Array,
|
|
98
100
|
) {}
|
|
99
101
|
|
|
100
|
-
/**
|
|
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
|
+
*/
|
|
101
106
|
get masked(): string {
|
|
102
|
-
return this.
|
|
107
|
+
return decodeBankText(this.buildMasked());
|
|
103
108
|
}
|
|
104
109
|
}
|
|
105
110
|
|
|
106
|
-
/** The segments between header and trailer,
|
|
111
|
+
/** The segments between header and trailer, and the number of the trailer. */
|
|
107
112
|
interface Body {
|
|
108
|
-
readonly
|
|
109
|
-
readonly masked: readonly Uint8Array[];
|
|
113
|
+
readonly segments: readonly Uint8Array[];
|
|
110
114
|
readonly nextNumber: number;
|
|
111
115
|
}
|
|
112
116
|
|
|
@@ -116,7 +120,8 @@ interface Body {
|
|
|
116
120
|
* With `security` the segments are signed with PIN and optional `tan` (HNSHK … HNSHA) and wrapped in the encryption
|
|
117
121
|
* envelope (HNVSK, HNVSD) that PIN/TAN uses without actual encryption.
|
|
118
122
|
*
|
|
119
|
-
* 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.
|
|
120
125
|
*/
|
|
121
126
|
export function buildMessage(parts: {
|
|
122
127
|
dialogId: string;
|
|
@@ -126,12 +131,21 @@ export function buildMessage(parts: {
|
|
|
126
131
|
tan?: string | undefined;
|
|
127
132
|
now?: Date;
|
|
128
133
|
}): OutgoingMessage {
|
|
129
|
-
const
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
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 : '***'));
|
|
144
|
+
}
|
|
145
|
+
|
|
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 {
|
|
133
148
|
const trailer = new SegmentSpec('HNHBS', 1, [parts.messageNumber]).build(body.nextNumber);
|
|
134
|
-
// The header states the size of the whole message, itself included; its own size does not depend on it.
|
|
135
149
|
const header = (size: number): Uint8Array =>
|
|
136
150
|
new SegmentSpec('HNHBK', 3, [
|
|
137
151
|
size.toString().padStart(12, '0'),
|
|
@@ -139,46 +153,27 @@ export function buildMessage(parts: {
|
|
|
139
153
|
new Verbatim(parts.dialogId),
|
|
140
154
|
parts.messageNumber,
|
|
141
155
|
]).build(1);
|
|
142
|
-
|
|
143
|
-
const
|
|
144
|
-
|
|
145
|
-
result.add(body.bytes);
|
|
146
|
-
result.add(trailer);
|
|
147
|
-
return new OutgoingMessage(result.takeBytes(), [header(sizeOf(body.masked)), ...body.masked, trailer]);
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
/** The number of bytes in `parts` together. */
|
|
151
|
-
function lengthOf(parts: readonly Uint8Array[]): number {
|
|
152
|
-
return parts.reduce((sum, part) => sum + part.length, 0);
|
|
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]);
|
|
153
159
|
}
|
|
154
160
|
|
|
155
161
|
/** The segments of a message without security, as the anonymous dialog sends them. */
|
|
156
162
|
function plainBody(segments: readonly SegmentSpec[]): Body {
|
|
157
|
-
|
|
158
|
-
const bytes = new BytesBuilder();
|
|
159
|
-
built.forEach((b) => {
|
|
160
|
-
bytes.add(b);
|
|
161
|
-
});
|
|
162
|
-
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 };
|
|
163
164
|
}
|
|
164
165
|
|
|
165
|
-
/** The segments signed with
|
|
166
|
+
/** The segments signed with `pin` and optional `tan`, inside the encryption envelope. */
|
|
166
167
|
function signedBody(
|
|
167
168
|
segments: readonly SegmentSpec[],
|
|
168
169
|
security: PinTanCredentials,
|
|
170
|
+
pin: string,
|
|
169
171
|
tan: string | undefined,
|
|
170
172
|
time: Date,
|
|
173
|
+
controlReference: string,
|
|
171
174
|
): Body {
|
|
172
175
|
const profile = ['PIN', isTwoStep(security) ? 2 : 1];
|
|
173
|
-
const
|
|
174
|
-
const inner = new BytesBuilder();
|
|
175
|
-
const maskedInner: Uint8Array[] = [];
|
|
176
|
-
const addInner = (built: Uint8Array, masked?: Uint8Array): void => {
|
|
177
|
-
inner.add(built);
|
|
178
|
-
maskedInner.push(masked ?? built);
|
|
179
|
-
};
|
|
180
|
-
|
|
181
|
-
addInner(
|
|
176
|
+
const inner = [
|
|
182
177
|
new SegmentSpec('HNSHK', 4, [
|
|
183
178
|
profile,
|
|
184
179
|
security.securityFunction,
|
|
@@ -192,17 +187,10 @@ function signedBody(
|
|
|
192
187
|
signatureAlgorithm,
|
|
193
188
|
keyName(security, 'S'),
|
|
194
189
|
]).build(2),
|
|
195
|
-
|
|
190
|
+
];
|
|
196
191
|
let number = 3;
|
|
197
|
-
for (const segment of segments)
|
|
198
|
-
|
|
199
|
-
new SegmentSpec('HNSHA', 2, [controlReference, null, [pin, tanValue]]);
|
|
200
|
-
addInner(
|
|
201
|
-
signatureEnd(security.pin, tan).build(number),
|
|
202
|
-
signatureEnd('***', tan === undefined ? undefined : '***').build(number),
|
|
203
|
-
);
|
|
204
|
-
|
|
205
|
-
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));
|
|
206
194
|
const encryptionHead = new SegmentSpec('HNVSK', 3, [
|
|
207
195
|
profile,
|
|
208
196
|
plainText,
|
|
@@ -213,16 +201,8 @@ function signedBody(
|
|
|
213
201
|
keyName(security, 'V'),
|
|
214
202
|
noCompression,
|
|
215
203
|
]).build(998);
|
|
216
|
-
const
|
|
217
|
-
|
|
218
|
-
bytes.add(new SegmentSpec('HNVSD', 1, [new Binary(innerBytes)]).build(999));
|
|
219
|
-
const masked = [
|
|
220
|
-
encryptionHead,
|
|
221
|
-
latin1Encode(`HNVSD:999:1+@${lengthOf(maskedInner)}@`),
|
|
222
|
-
...maskedInner,
|
|
223
|
-
latin1Encode("'"),
|
|
224
|
-
];
|
|
225
|
-
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 };
|
|
226
206
|
}
|
|
227
207
|
|
|
228
208
|
/** A received message, with the segments of the encryption envelope unpacked. */
|
|
@@ -255,19 +235,29 @@ export class IncomingMessage3 {
|
|
|
255
235
|
/**
|
|
256
236
|
* Parses a message; throws `FormatError` if `data` is not one complete FinTS message, such as the HTML page of a
|
|
257
237
|
* proxy, a maintenance notice, a message cut short, which lacks the message trailer (HNHBS) at its end, or two
|
|
258
|
-
* messages run together, whose second would pass for part of the first.
|
|
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.
|
|
259
240
|
*
|
|
260
|
-
* Its `size` counts the memory of
|
|
261
|
-
* takes far more memory than bytes.
|
|
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.
|
|
262
243
|
*/
|
|
263
244
|
static parse(data: Uint8Array): IncomingMessage3 {
|
|
264
|
-
|
|
265
|
-
|
|
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
|
+
}
|
|
266
253
|
if (outer[0]?.type !== 'HNHBK') throw new FormatError('Not a FinTS message: no message header');
|
|
267
254
|
if (outer.at(-1)?.type !== 'HNHBS') throw new FormatError('Incomplete FinTS message: no message trailer');
|
|
268
255
|
if (outer.filter((s) => s.type === 'HNHBK' || s.type === 'HNHBS').length > 2) {
|
|
269
256
|
throw new FormatError('More than one FinTS message');
|
|
270
257
|
}
|
|
258
|
+
if (outer.filter((s) => s.type === 'HNVSD').length > 1) {
|
|
259
|
+
throw new FormatError('More than one encrypted part (HNVSD)');
|
|
260
|
+
}
|
|
271
261
|
const segments: Segment[] = [];
|
|
272
262
|
for (const s of outer) {
|
|
273
263
|
if (s.type !== 'HNVSD') {
|
|
@@ -276,9 +266,11 @@ export class IncomingMessage3 {
|
|
|
276
266
|
}
|
|
277
267
|
const payload = s.binary(1);
|
|
278
268
|
if (payload === undefined) throw new FormatError('Encrypted data (HNVSD) without binary data');
|
|
279
|
-
for (const inner of parseSegments(payload
|
|
269
|
+
for (const inner of parseSegments(payload)) segments.push(inner);
|
|
280
270
|
}
|
|
281
|
-
|
|
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);
|
|
282
274
|
}
|
|
283
275
|
|
|
284
276
|
static combine(first: IncomingMessage3, second: IncomingMessage3): IncomingMessage3 {
|
|
@@ -315,6 +307,23 @@ export class IncomingMessage3 {
|
|
|
315
307
|
);
|
|
316
308
|
}
|
|
317
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
|
+
);
|
|
325
|
+
}
|
|
326
|
+
|
|
318
327
|
find(type: string): Segment | undefined {
|
|
319
328
|
return this.segments.find((s) => s.type === type);
|
|
320
329
|
}
|
|
@@ -3,8 +3,19 @@
|
|
|
3
3
|
import type { FinTsConfig } from '../core/config.js';
|
|
4
4
|
import { FormatError } from '../util/errors.js';
|
|
5
5
|
import { readOptionalBoolean, readOptionalObject, readOptionalString, type JsonObject } from '../util/json.js';
|
|
6
|
+
import { trimBlanks } from '../util/text.js';
|
|
6
7
|
import type { IncomingMessage3 } from './message.js';
|
|
7
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
|
+
}
|
|
8
19
|
|
|
9
20
|
/**
|
|
10
21
|
* The parameter data and IDs the bank sends the user `config` names, which the messages and business transactions
|
|
@@ -22,11 +33,15 @@ export class ParameterStore {
|
|
|
22
33
|
* the bank send its parameter data anew.
|
|
23
34
|
*/
|
|
24
35
|
private bpdAnonymous = false;
|
|
36
|
+
|
|
37
|
+
/** The limits a refresh took from the anonymous dialog's parameter data (`keepRefreshedLimits`). */
|
|
38
|
+
private refreshedLimits: MessageLimits | undefined;
|
|
25
39
|
private upd: Upd3 | undefined;
|
|
26
40
|
|
|
27
41
|
/**
|
|
28
|
-
* The names of the TAN media in the bank's last list whose text
|
|
29
|
-
* `parseTanMedia` notes them. Saved with the state, as the
|
|
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.
|
|
30
45
|
*/
|
|
31
46
|
private tanMediumNames = new Map<string, string>();
|
|
32
47
|
|
|
@@ -50,9 +65,21 @@ export class ParameterStore {
|
|
|
50
65
|
return this.systemIdValue;
|
|
51
66
|
}
|
|
52
67
|
|
|
53
|
-
/**
|
|
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
|
+
*/
|
|
54
81
|
get reportedBpdVersion(): number {
|
|
55
|
-
return this.bpd === undefined || this.bpdAnonymous ? 0 : this.bpd.version;
|
|
82
|
+
return this.bpd === undefined || this.bpdAnonymous || this.refreshedLimits !== undefined ? 0 : this.bpd.version;
|
|
56
83
|
}
|
|
57
84
|
|
|
58
85
|
/**
|
|
@@ -99,11 +126,28 @@ export class ParameterStore {
|
|
|
99
126
|
if (this.bpd === undefined || this.bpdAnonymous) this.keepBankParameters(response, true);
|
|
100
127
|
}
|
|
101
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
|
+
|
|
102
145
|
private keepBankParameters(response: IncomingMessage3, anonymous: boolean): void {
|
|
103
146
|
const bpd = Bpd3.fromSegments(response.segments, this.config.bankCode);
|
|
104
147
|
if (bpd === undefined) return;
|
|
105
148
|
this.bpd = bpd;
|
|
106
149
|
this.bpdAnonymous = anonymous;
|
|
150
|
+
this.refreshedLimits = undefined;
|
|
107
151
|
}
|
|
108
152
|
|
|
109
153
|
/** Keeps `names`, the names of the bank's latest list of TAN media as `parseTanMedia` notes them. */
|
|
@@ -111,9 +155,13 @@ export class ParameterStore {
|
|
|
111
155
|
this.tanMediumNames = names;
|
|
112
156
|
}
|
|
113
157
|
|
|
114
|
-
/**
|
|
115
|
-
|
|
116
|
-
|
|
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);
|
|
117
165
|
}
|
|
118
166
|
|
|
119
167
|
/** The parameter data and IDs as JSON data, for the client's saved state. */
|
|
@@ -124,37 +172,55 @@ export class ParameterStore {
|
|
|
124
172
|
bpdAnonymous: this.bpdAnonymous,
|
|
125
173
|
upd: this.upd?.encoded ?? null,
|
|
126
174
|
tanMediumNames: Object.fromEntries(this.tanMediumNames),
|
|
175
|
+
[blanksNotedKey]: true,
|
|
127
176
|
};
|
|
128
177
|
}
|
|
129
178
|
|
|
130
|
-
/**
|
|
131
|
-
|
|
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 {
|
|
132
184
|
const bpd = readOptionalString(saved, 'bpd');
|
|
133
185
|
const upd = readOptionalString(saved, 'upd');
|
|
134
186
|
const restoredSystemId = readOptionalString(saved, 'systemId');
|
|
135
|
-
// A state of the Dart package
|
|
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.
|
|
136
188
|
const restoredBpdAnonymous = readOptionalBoolean(saved, 'bpdAnonymous') ?? true;
|
|
137
189
|
const restoredBpd = bpd === undefined ? undefined : Bpd3.fromSegments(decodeSegments(bpd), this.config.bankCode);
|
|
138
190
|
const restoredUpd =
|
|
139
191
|
upd === undefined
|
|
140
192
|
? undefined
|
|
141
193
|
: Upd3.fromSegments(decodeSegments(upd), this.config.bankCode, this.config.countryCode);
|
|
142
|
-
const restoredNames = readTanMediumNames(saved);
|
|
194
|
+
const restoredNames = readTanMediumNames(saved, tanMedium);
|
|
143
195
|
this.systemIdValue = restoredSystemId;
|
|
144
196
|
this.bpd = restoredBpd;
|
|
145
197
|
this.bpdAnonymous = restoredBpdAnonymous;
|
|
198
|
+
this.refreshedLimits = undefined;
|
|
146
199
|
this.upd = restoredUpd;
|
|
147
200
|
this.tanMediumNames = restoredNames;
|
|
148
201
|
}
|
|
149
202
|
}
|
|
150
203
|
|
|
151
|
-
/** The
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
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]) => {
|
|
156
217
|
if (typeof asSent !== 'string') throw new FormatError('Saved state: a TAN medium name is not a string');
|
|
157
218
|
return [name, asSent];
|
|
158
219
|
}),
|
|
159
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;
|
|
160
226
|
}
|
package/src/v3/parameters.ts
CHANGED
|
@@ -6,7 +6,7 @@ import { BankInfo } from '../core/bank-info.js';
|
|
|
6
6
|
import { FinTsProtocolError } from '../core/errors.js';
|
|
7
7
|
import { TanMethod, tanMediumRequirementOf } from '../tan/tan-models.js';
|
|
8
8
|
import { base64Decode, base64Encode, concatBytes } from '../util/bytes.js';
|
|
9
|
-
import { nonBlank, tryParseInt } from '../util/text.js';
|
|
9
|
+
import { nonBlank, trimBlanks, tryParseInt } from '../util/text.js';
|
|
10
10
|
import { AccountExtension } from './account-extension.js';
|
|
11
11
|
import { parseSegments, type Segment } from './syntax.js';
|
|
12
12
|
|
|
@@ -33,6 +33,12 @@ const Hibpa = {
|
|
|
33
33
|
maxTimeout: 9,
|
|
34
34
|
} as const;
|
|
35
35
|
|
|
36
|
+
/** The size or time at `index` of `hibpa`, a field of at most four digits; `undefined` for a larger value, as for none. */
|
|
37
|
+
function hibpaLimit(hibpa: Segment, index: number): number | undefined {
|
|
38
|
+
const value = hibpa.integer(index);
|
|
39
|
+
return value !== undefined && value <= 9999 ? value : undefined;
|
|
40
|
+
}
|
|
41
|
+
|
|
36
42
|
/**
|
|
37
43
|
* The data element of every parameter segment (`HI…S`) that holds its specific parameters, after the maximum number of
|
|
38
44
|
* orders, the minimum number of signatures and the security class.
|
|
@@ -66,9 +72,15 @@ export class Bpd3 {
|
|
|
66
72
|
readonly unsupportedTanMethods: readonly string[];
|
|
67
73
|
readonly pinTan: PinTanParameters;
|
|
68
74
|
|
|
69
|
-
/** camt formats the bank offers for HKCAZ. */
|
|
75
|
+
/** The camt.052 formats the bank offers for HKCAZ, the one camt message the client reads. */
|
|
70
76
|
readonly camtFormats: readonly string[];
|
|
71
77
|
|
|
78
|
+
/**
|
|
79
|
+
* Whether an international account (KTI) may name the national account as well: HISPAS does not rule it out
|
|
80
|
+
* ("Nationale Kontoverbindung erlaubt").
|
|
81
|
+
*/
|
|
82
|
+
readonly nationalAccountAllowed: boolean;
|
|
83
|
+
|
|
72
84
|
/** Versions of each parameter segment, by segment type. */
|
|
73
85
|
private readonly versions: ReadonlyMap<string, ReadonlySet<number>>;
|
|
74
86
|
|
|
@@ -78,22 +90,19 @@ export class Bpd3 {
|
|
|
78
90
|
*/
|
|
79
91
|
readonly dialogLanguage: number;
|
|
80
92
|
|
|
81
|
-
|
|
93
|
+
/** The segments as the saved state keeps them. */
|
|
94
|
+
readonly encoded: string;
|
|
82
95
|
|
|
83
|
-
private constructor(
|
|
84
|
-
readonly segments: readonly Segment[],
|
|
85
|
-
hibpa: Segment,
|
|
86
|
-
fallbackBankCode: string,
|
|
87
|
-
) {
|
|
96
|
+
private constructor(segments: readonly Segment[], hibpa: Segment, fallbackBankCode: string) {
|
|
88
97
|
this.bankInfo = new BankInfo({
|
|
89
98
|
bankCode: hibpa.text(Hibpa.bankId, 1) ?? fallbackBankCode,
|
|
90
99
|
name: hibpa.text(Hibpa.bankName),
|
|
91
100
|
bpdVersion: hibpa.integer(Hibpa.version) ?? 0,
|
|
92
101
|
languages: defined(hibpa.texts(Hibpa.languages)),
|
|
93
102
|
supportedVersions: defined(hibpa.texts(Hibpa.fintsVersions)),
|
|
94
|
-
maxMessageSize: hibpa
|
|
95
|
-
minTimeout: hibpa
|
|
96
|
-
maxTimeout: hibpa
|
|
103
|
+
maxMessageSize: hibpaLimit(hibpa, Hibpa.maxMessageSize),
|
|
104
|
+
minTimeout: hibpaLimit(hibpa, Hibpa.minTimeout),
|
|
105
|
+
maxTimeout: hibpaLimit(hibpa, Hibpa.maxTimeout),
|
|
97
106
|
});
|
|
98
107
|
this.offeredMethods = parseTanMethods(segments);
|
|
99
108
|
const offered = [...this.offeredMethods.values()];
|
|
@@ -105,9 +114,11 @@ export class Bpd3 {
|
|
|
105
114
|
this.camtFormats = segments
|
|
106
115
|
.filter((s) => s.type === 'HICAZS')
|
|
107
116
|
.flatMap((s) => defined(s.texts(parameterGroup)))
|
|
108
|
-
.filter((format) => format.includes('camt'));
|
|
117
|
+
.filter((format) => format.includes('camt.052'));
|
|
118
|
+
this.nationalAccountAllowed = highest(segments, 'HISPAS')?.text(parameterGroup, 1) !== 'N';
|
|
109
119
|
this.versions = versionsByType(segments);
|
|
110
120
|
this.dialogLanguage = dialogLanguageOf(segments, this.bankInfo);
|
|
121
|
+
this.encoded = encodeSegments(segments);
|
|
111
122
|
}
|
|
112
123
|
|
|
113
124
|
/** The version of the parameter data, which the client reports back so that the bank sends newer ones only. */
|
|
@@ -115,11 +126,6 @@ export class Bpd3 {
|
|
|
115
126
|
return this.bankInfo.bpdVersion;
|
|
116
127
|
}
|
|
117
128
|
|
|
118
|
-
/** The segments as the saved state keeps them, encoded once. */
|
|
119
|
-
get encoded(): string {
|
|
120
|
-
return (this.encodedValue ??= encodeSegments(this.segments));
|
|
121
|
-
}
|
|
122
|
-
|
|
123
129
|
/** The BPD in `all`, or `undefined` if they contain no HIBPA segment. */
|
|
124
130
|
static fromSegments(all: readonly Segment[], fallbackBankCode: string): Bpd3 | undefined {
|
|
125
131
|
const segments = all.filter(isParameterSegment);
|
|
@@ -154,13 +160,23 @@ function dialogLanguageOf(segments: readonly Segment[], bankInfo: BankInfo): num
|
|
|
154
160
|
return bankInfo.languages.map((l) => tryParseInt(l)).find((l) => l !== undefined && languages.has(l)) ?? 0;
|
|
155
161
|
}
|
|
156
162
|
|
|
163
|
+
const generalParameterSegments = new Set(['HIBPA', 'HIKOM', 'HISHV', 'HIKPV']);
|
|
164
|
+
|
|
157
165
|
function isParameterSegment(s: Segment): boolean {
|
|
158
166
|
const type = s.type;
|
|
159
|
-
if (
|
|
160
|
-
|
|
167
|
+
if (generalParameterSegments.has(type)) return true;
|
|
168
|
+
// Return codes for single segments end with an S, as parameter segments do.
|
|
169
|
+
if (type === 'HIRMS') return false;
|
|
161
170
|
return type.length >= 5 && (type.startsWith('HI') || type.startsWith('DI')) && type.endsWith('S');
|
|
162
171
|
}
|
|
163
172
|
|
|
173
|
+
/** The segment of `type` with the highest version in `segments`. */
|
|
174
|
+
function highest(segments: readonly Segment[], type: string): Segment | undefined {
|
|
175
|
+
let found: Segment | undefined;
|
|
176
|
+
for (const s of segments) if (s.type === type && (found === undefined || s.version > found.version)) found = s;
|
|
177
|
+
return found;
|
|
178
|
+
}
|
|
179
|
+
|
|
164
180
|
function versionsByType(segments: readonly Segment[]): Map<string, Set<number>> {
|
|
165
181
|
const versions = new Map<string, Set<number>>();
|
|
166
182
|
for (const s of segments) {
|
|
@@ -270,17 +286,14 @@ function parseHitans(s: Segment): OfferedMethod[] {
|
|
|
270
286
|
export class PinTanParameters {
|
|
271
287
|
private constructor(
|
|
272
288
|
private readonly tanRequired: ReadonlyMap<string, boolean>,
|
|
273
|
-
/** The shortest and longest PIN the bank takes. */
|
|
274
|
-
readonly pinLength: { min: number | undefined; max: number | undefined },
|
|
289
|
+
/** The shortest and longest PIN the bank takes; `undefined` where the bank parameter data have no HIPINS. */
|
|
290
|
+
readonly pinLength: { min: number | undefined; max: number | undefined } | undefined,
|
|
275
291
|
) {}
|
|
276
292
|
|
|
277
293
|
/** Reads the HIPINS segment of the highest version. */
|
|
278
294
|
static parse(segments: readonly Segment[]): PinTanParameters {
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
if (hipins === undefined || s.version > hipins.version) hipins = s;
|
|
282
|
-
}
|
|
283
|
-
if (hipins === undefined) return new PinTanParameters(new Map(), { min: undefined, max: undefined });
|
|
295
|
+
const hipins = highest(segments, 'HIPINS');
|
|
296
|
+
if (hipins === undefined) return new PinTanParameters(new Map(), undefined);
|
|
284
297
|
const fields = hipins.texts(parameterGroup);
|
|
285
298
|
const tanRequired = new Map<string, boolean>();
|
|
286
299
|
// Five general fields, the PIN lengths first; then pairs of segment type and TAN flag.
|
|
@@ -338,18 +351,19 @@ export class Upd3 {
|
|
|
338
351
|
/** The accounts the user may access. */
|
|
339
352
|
readonly accounts: readonly Account[];
|
|
340
353
|
|
|
341
|
-
|
|
354
|
+
/** The segments as the saved state keeps them. */
|
|
355
|
+
readonly encoded: string;
|
|
342
356
|
|
|
343
357
|
/** `listsAll` tells whether HIUPA declares the lists of permitted transactions complete ("UPD-Verwendung" 0). */
|
|
344
358
|
private constructor(
|
|
345
|
-
|
|
359
|
+
segments: readonly Segment[],
|
|
346
360
|
hiupa: Segment,
|
|
347
361
|
listsAll: boolean,
|
|
348
362
|
defaultBankCode: string,
|
|
349
363
|
defaultCountry: string,
|
|
350
364
|
) {
|
|
351
365
|
const hiupds = segments.filter((s) => s.type === 'HIUPD');
|
|
352
|
-
this.userId = hiupa.text(hiupaUserId)
|
|
366
|
+
this.userId = nonBlank(hiupa.text(hiupaUserId));
|
|
353
367
|
this.version = hiupa.integer(hiupaVersion) ?? 0;
|
|
354
368
|
this.listsAllTransactions = listsAll;
|
|
355
369
|
this.listedTransactions = new Set(hiupds.flatMap(transactionsOf));
|
|
@@ -357,11 +371,7 @@ export class Upd3 {
|
|
|
357
371
|
hiupds.map((s) => entryOf(s, defaultBankCode, defaultCountry, listsAll)).filter((e) => e !== undefined),
|
|
358
372
|
);
|
|
359
373
|
this.accounts = Object.freeze(this.entries.map((e) => e.account));
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
/** The segments as the saved state keeps them, encoded once. */
|
|
363
|
-
get encoded(): string {
|
|
364
|
-
return (this.encodedValue ??= encodeSegments(this.segments));
|
|
374
|
+
this.encoded = encodeSegments(segments);
|
|
365
375
|
}
|
|
366
376
|
|
|
367
377
|
/**
|
|
@@ -407,11 +417,13 @@ function transactionsOf(s: Segment): string[] {
|
|
|
407
417
|
function extensionOf(s: Segment): AccountExtension | undefined {
|
|
408
418
|
if (s.version < 6) return undefined;
|
|
409
419
|
for (let i = allowedFrom(s.version); i < s.length; i++) {
|
|
410
|
-
const text =
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
420
|
+
const text = trimBlanks(
|
|
421
|
+
s
|
|
422
|
+
.group(i)
|
|
423
|
+
.map((e) => e.text)
|
|
424
|
+
.join(':'),
|
|
425
|
+
);
|
|
426
|
+
if (text.startsWith('{')) return AccountExtension.tryParse(text);
|
|
415
427
|
}
|
|
416
428
|
return undefined;
|
|
417
429
|
}
|
|
@@ -426,6 +438,22 @@ function entryOf(s: Segment, defaultBankCode: string, defaultCountry: string, li
|
|
|
426
438
|
};
|
|
427
439
|
}
|
|
428
440
|
|
|
441
|
+
/** Whether `code` is a country code, three digits, such as `280`; a German bank code has eight. */
|
|
442
|
+
function isCountryCode(code: string | undefined): boolean {
|
|
443
|
+
return code !== undefined && /^[0-9]{3}$/.test(code);
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
/**
|
|
447
|
+
* The national account of the HIUPD `s`: number, sub-account, country and bank code (KTV2), by position in every
|
|
448
|
+
* version, so that a sub-account or bank code the bank leaves out is empty. Three elements whose second is a country
|
|
449
|
+
* code and whose third is not are the older layout without sub-account: number, country and bank code.
|
|
450
|
+
*/
|
|
451
|
+
function nationalAccountOf(s: Segment): (string | undefined)[] {
|
|
452
|
+
const ktv = s.texts(1);
|
|
453
|
+
const [number, second, third] = ktv;
|
|
454
|
+
return ktv.length === 3 && isCountryCode(second) && !isCountryCode(third) ? [number, undefined, second, third] : ktv;
|
|
455
|
+
}
|
|
456
|
+
|
|
429
457
|
function accountOf(
|
|
430
458
|
s: Segment,
|
|
431
459
|
defaultBankCode: string,
|
|
@@ -433,8 +461,7 @@ function accountOf(
|
|
|
433
461
|
listsAll: boolean,
|
|
434
462
|
): Account | undefined {
|
|
435
463
|
const version = s.version;
|
|
436
|
-
|
|
437
|
-
const [number, subAccount, country, bank] = s.texts(1);
|
|
464
|
+
const [number, subAccount, country, bank] = nationalAccountOf(s);
|
|
438
465
|
const iban = version >= 6 ? s.text(2) : undefined;
|
|
439
466
|
if (number === undefined && iban === undefined) return undefined;
|
|
440
467
|
// Field positions by HIUPD version: version 6 inserts the IBAN, version 5 the account type.
|