@coderoftime/fints 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +142 -1
- package/README.md +127 -37
- package/dist/client.d.ts +48 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +199 -0
- package/dist/client.js.map +1 -0
- package/dist/core/account.d.ts +35 -7
- package/dist/core/account.d.ts.map +1 -1
- package/dist/core/account.js +84 -13
- package/dist/core/account.js.map +1 -1
- package/dist/core/amount.d.ts +2 -1
- package/dist/core/amount.d.ts.map +1 -1
- package/dist/core/amount.js +4 -1
- package/dist/core/amount.js.map +1 -1
- package/dist/core/balance.d.ts +1 -0
- package/dist/core/balance.d.ts.map +1 -1
- package/dist/core/balance.js +6 -5
- package/dist/core/balance.js.map +1 -1
- package/dist/core/bank-info.d.ts +1 -1
- package/dist/core/bank-info.js +1 -1
- package/dist/core/client.d.ts +50 -15
- package/dist/core/client.d.ts.map +1 -1
- package/dist/core/config.d.ts +28 -16
- package/dist/core/config.d.ts.map +1 -1
- package/dist/core/config.js +66 -26
- package/dist/core/config.js.map +1 -1
- package/dist/core/errors.d.ts +85 -23
- package/dist/core/errors.d.ts.map +1 -1
- package/dist/core/errors.js +112 -47
- package/dist/core/errors.js.map +1 -1
- package/dist/core/feedback.d.ts +4 -1
- package/dist/core/feedback.d.ts.map +1 -1
- package/dist/core/feedback.js +4 -1
- package/dist/core/feedback.js.map +1 -1
- package/dist/core/transaction.d.ts +6 -1
- package/dist/core/transaction.d.ts.map +1 -1
- package/dist/core/transaction.js +9 -4
- package/dist/core/transaction.js.map +1 -1
- package/dist/core/transport.d.ts +33 -6
- package/dist/core/transport.d.ts.map +1 -1
- package/dist/core/transport.js +77 -20
- package/dist/core/transport.js.map +1 -1
- package/dist/formats/camt.d.ts +24 -4
- package/dist/formats/camt.d.ts.map +1 -1
- package/dist/formats/camt.js +124 -41
- package/dist/formats/camt.js.map +1 -1
- package/dist/formats/mt940.d.ts +15 -3
- package/dist/formats/mt940.d.ts.map +1 -1
- package/dist/formats/mt940.js +161 -82
- package/dist/formats/mt940.js.map +1 -1
- package/dist/formats/readers.d.ts +6 -2
- package/dist/formats/readers.d.ts.map +1 -1
- package/dist/formats/readers.js +24 -29
- package/dist/formats/readers.js.map +1 -1
- package/dist/formats/record-budget.d.ts +7 -0
- package/dist/formats/record-budget.d.ts.map +1 -0
- package/dist/formats/record-budget.js +17 -0
- package/dist/formats/record-budget.js.map +1 -0
- package/dist/formats/sepa-purpose.d.ts +6 -0
- package/dist/formats/sepa-purpose.d.ts.map +1 -1
- package/dist/formats/sepa-purpose.js +23 -2
- package/dist/formats/sepa-purpose.js.map +1 -1
- package/dist/index.d.ts +3 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -2
- package/dist/index.js.map +1 -1
- package/dist/session/app-approval.d.ts +11 -5
- package/dist/session/app-approval.d.ts.map +1 -1
- package/dist/session/app-approval.js +23 -12
- package/dist/session/app-approval.js.map +1 -1
- package/dist/session/dialog-runner.d.ts +7 -10
- package/dist/session/dialog-runner.d.ts.map +1 -1
- package/dist/session/dialog-runner.js +15 -16
- package/dist/session/dialog-runner.js.map +1 -1
- package/dist/session/login-guard.d.ts +54 -49
- package/dist/session/login-guard.d.ts.map +1 -1
- package/dist/session/login-guard.js +156 -84
- package/dist/session/login-guard.js.map +1 -1
- package/dist/session/sepa-accounts.d.ts +20 -14
- package/dist/session/sepa-accounts.d.ts.map +1 -1
- package/dist/session/sepa-accounts.js +62 -46
- package/dist/session/sepa-accounts.js.map +1 -1
- package/dist/session/session-setup.d.ts +33 -25
- package/dist/session/session-setup.d.ts.map +1 -1
- package/dist/session/session-setup.js +87 -65
- package/dist/session/session-setup.js.map +1 -1
- package/dist/session/session-state.d.ts +9 -6
- package/dist/session/session-state.d.ts.map +1 -1
- package/dist/session/session-state.js +52 -17
- package/dist/session/session-state.js.map +1 -1
- package/dist/session/signed-dialog.d.ts +77 -0
- package/dist/session/signed-dialog.d.ts.map +1 -0
- package/dist/session/signed-dialog.js +170 -0
- package/dist/session/signed-dialog.js.map +1 -0
- package/dist/session/tan-selection.d.ts +26 -18
- package/dist/session/tan-selection.d.ts.map +1 -1
- package/dist/session/tan-selection.js +44 -33
- package/dist/session/tan-selection.js.map +1 -1
- package/dist/session/user-prompts.d.ts +8 -4
- package/dist/session/user-prompts.d.ts.map +1 -1
- package/dist/session/user-prompts.js +34 -14
- package/dist/session/user-prompts.js.map +1 -1
- package/dist/tan/challenge.d.ts +34 -13
- package/dist/tan/challenge.d.ts.map +1 -1
- package/dist/tan/challenge.js +25 -26
- package/dist/tan/challenge.js.map +1 -1
- package/dist/tan/flicker.d.ts +7 -1
- package/dist/tan/flicker.d.ts.map +1 -1
- package/dist/tan/flicker.js +40 -6
- package/dist/tan/flicker.js.map +1 -1
- package/dist/tan/tan-image.d.ts +3 -2
- package/dist/tan/tan-image.d.ts.map +1 -1
- package/dist/tan/tan-image.js +7 -5
- package/dist/tan/tan-image.js.map +1 -1
- package/dist/tan/tan-models.d.ts +5 -1
- package/dist/tan/tan-models.d.ts.map +1 -1
- package/dist/tan/tan-models.js +10 -5
- package/dist/tan/tan-models.js.map +1 -1
- package/dist/util/async.d.ts +0 -2
- package/dist/util/async.d.ts.map +1 -1
- package/dist/util/async.js +0 -4
- package/dist/util/async.js.map +1 -1
- package/dist/util/bytes.d.ts +2 -3
- package/dist/util/bytes.d.ts.map +1 -1
- package/dist/util/bytes.js +12 -20
- package/dist/util/bytes.js.map +1 -1
- package/dist/util/call-context.d.ts.map +1 -1
- package/dist/util/call-context.js +3 -2
- package/dist/util/call-context.js.map +1 -1
- package/dist/util/dates.d.ts +9 -16
- package/dist/util/dates.d.ts.map +1 -1
- package/dist/util/dates.js +39 -19
- package/dist/util/dates.js.map +1 -1
- package/dist/util/errors.d.ts +9 -1
- package/dist/util/errors.d.ts.map +1 -1
- package/dist/util/errors.js +12 -3
- package/dist/util/errors.js.map +1 -1
- package/dist/util/json.d.ts +4 -0
- package/dist/util/json.d.ts.map +1 -1
- package/dist/util/json.js +18 -0
- package/dist/util/json.js.map +1 -1
- package/dist/util/text.d.ts +21 -4
- package/dist/util/text.d.ts.map +1 -1
- package/dist/util/text.js +42 -13
- package/dist/util/text.js.map +1 -1
- package/dist/util/xml.d.ts +4 -3
- package/dist/util/xml.d.ts.map +1 -1
- package/dist/util/xml.js +37 -26
- package/dist/util/xml.js.map +1 -1
- package/dist/v3/account-extension.d.ts.map +1 -1
- package/dist/v3/account-extension.js +7 -10
- package/dist/v3/account-extension.js.map +1 -1
- package/dist/v3/bank-connection.d.ts +34 -0
- package/dist/v3/bank-connection.d.ts.map +1 -0
- package/dist/v3/bank-connection.js +58 -0
- package/dist/v3/bank-connection.js.map +1 -0
- package/dist/v3/dialog.d.ts +80 -46
- package/dist/v3/dialog.d.ts.map +1 -1
- package/dist/v3/dialog.js +162 -59
- package/dist/v3/dialog.js.map +1 -1
- package/dist/v3/message.d.ts +17 -4
- package/dist/v3/message.d.ts.map +1 -1
- package/dist/v3/message.js +62 -18
- package/dist/v3/message.js.map +1 -1
- package/dist/v3/parameter-store.d.ts +75 -0
- package/dist/v3/parameter-store.d.ts.map +1 -0
- package/dist/v3/parameter-store.js +143 -0
- package/dist/v3/parameter-store.js.map +1 -0
- package/dist/v3/parameters.d.ts +38 -12
- package/dist/v3/parameters.d.ts.map +1 -1
- package/dist/v3/parameters.js +91 -37
- package/dist/v3/parameters.js.map +1 -1
- package/dist/v3/protocol.d.ts +43 -50
- package/dist/v3/protocol.d.ts.map +1 -1
- package/dist/v3/protocol.js +58 -208
- package/dist/v3/protocol.js.map +1 -1
- package/dist/v3/queries.d.ts +51 -0
- package/dist/v3/queries.d.ts.map +1 -0
- package/dist/v3/queries.js +120 -0
- package/dist/v3/queries.js.map +1 -0
- package/dist/v3/segments.d.ts +29 -12
- package/dist/v3/segments.d.ts.map +1 -1
- package/dist/v3/segments.js +65 -23
- package/dist/v3/segments.js.map +1 -1
- package/dist/v3/syntax.d.ts +65 -14
- package/dist/v3/syntax.d.ts.map +1 -1
- package/dist/v3/syntax.js +167 -101
- package/dist/v3/syntax.js.map +1 -1
- package/package.json +10 -6
- package/src/client.ts +252 -0
- package/src/core/account.ts +86 -13
- package/src/core/amount.ts +3 -1
- package/src/core/balance.ts +6 -5
- package/src/core/bank-info.ts +1 -1
- package/src/core/client.ts +52 -15
- package/src/core/config.ts +69 -28
- package/src/core/errors.ts +122 -48
- package/src/core/feedback.ts +4 -1
- package/src/core/transaction.ts +9 -4
- package/src/core/transport.ts +98 -23
- package/src/formats/camt.ts +136 -39
- package/src/formats/mt940.ts +167 -81
- package/src/formats/readers.ts +24 -31
- package/src/formats/record-budget.ts +18 -0
- package/src/formats/sepa-purpose.ts +21 -2
- package/src/index.ts +4 -2
- package/src/session/app-approval.ts +51 -38
- package/src/session/dialog-runner.ts +31 -28
- package/src/session/login-guard.ts +174 -88
- package/src/session/sepa-accounts.ts +63 -45
- package/src/session/session-setup.ts +90 -71
- package/src/session/session-state.ts +56 -22
- package/src/session/signed-dialog.ts +192 -0
- package/src/session/tan-selection.ts +50 -39
- package/src/session/user-prompts.ts +35 -16
- package/src/tan/challenge.ts +40 -31
- package/src/tan/flicker.ts +44 -6
- package/src/tan/tan-image.ts +7 -5
- package/src/tan/tan-models.ts +10 -5
- package/src/util/async.ts +0 -5
- package/src/util/bytes.ts +13 -19
- package/src/util/call-context.ts +3 -2
- package/src/util/dates.ts +41 -24
- package/src/util/errors.ts +14 -3
- package/src/util/json.ts +16 -0
- package/src/util/text.ts +42 -12
- package/src/util/xml.ts +37 -26
- package/src/v3/account-extension.ts +7 -12
- package/src/v3/bank-connection.ts +67 -0
- package/src/v3/dialog.ts +175 -91
- package/src/v3/message.ts +69 -17
- package/src/v3/parameter-store.ts +160 -0
- package/src/v3/parameters.ts +110 -37
- package/src/v3/protocol.ts +70 -253
- package/src/v3/queries.ts +190 -0
- package/src/v3/segments.ts +65 -22
- package/src/v3/syntax.ts +153 -91
- package/dist/session/bank-connection.d.ts +0 -28
- package/dist/session/bank-connection.d.ts.map +0 -1
- package/dist/session/bank-connection.js +0 -48
- package/dist/session/bank-connection.js.map +0 -1
- package/dist/session/parameter-data.d.ts +0 -83
- package/dist/session/parameter-data.d.ts.map +0 -1
- package/dist/session/parameter-data.js +0 -68
- package/dist/session/parameter-data.js.map +0 -1
- package/dist/session/pin-tan-client.d.ts +0 -54
- package/dist/session/pin-tan-client.d.ts.map +0 -1
- package/dist/session/pin-tan-client.js +0 -153
- package/dist/session/pin-tan-client.js.map +0 -1
- package/dist/session/pin-tan-dialog.d.ts +0 -71
- package/dist/session/pin-tan-dialog.d.ts.map +0 -1
- package/dist/session/pin-tan-dialog.js +0 -124
- package/dist/session/pin-tan-dialog.js.map +0 -1
- package/dist/session/protocol.d.ts +0 -209
- package/dist/session/protocol.d.ts.map +0 -1
- package/dist/session/protocol.js +0 -75
- package/dist/session/protocol.js.map +0 -1
- package/dist/session/response-handler.d.ts +0 -48
- package/dist/session/response-handler.d.ts.map +0 -1
- package/dist/session/response-handler.js +0 -121
- package/dist/session/response-handler.js.map +0 -1
- package/dist/v3/client.d.ts +0 -24
- package/dist/v3/client.d.ts.map +0 -1
- package/dist/v3/client.js +0 -16
- package/dist/v3/client.js.map +0 -1
- package/src/session/bank-connection.ts +0 -51
- package/src/session/parameter-data.ts +0 -137
- package/src/session/pin-tan-client.ts +0 -186
- package/src/session/pin-tan-dialog.ts +0 -160
- package/src/session/protocol.ts +0 -311
- package/src/session/response-handler.ts +0 -130
- package/src/v3/client.ts +0 -29
package/src/v3/message.ts
CHANGED
|
@@ -2,13 +2,21 @@
|
|
|
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, BytesBuilder,
|
|
6
|
+
import { base64Encode, BytesBuilder, 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
|
+
dataElementMemory,
|
|
13
|
+
maxDataElements,
|
|
14
|
+
parseSegments,
|
|
15
|
+
type Segment,
|
|
16
|
+
SegmentSpec,
|
|
17
|
+
type SegmentValue,
|
|
18
|
+
Verbatim,
|
|
19
|
+
} from './syntax.js';
|
|
12
20
|
|
|
13
21
|
/** Signature data of a PIN/TAN message. */
|
|
14
22
|
export interface PinTanCredentials {
|
|
@@ -35,7 +43,7 @@ const issuer = 1;
|
|
|
35
43
|
|
|
36
44
|
/** Security identification: message sender (MS), empty CID, then the customer system ID. */
|
|
37
45
|
function sender(systemId: string): SegmentValue {
|
|
38
|
-
return [1, null, systemId];
|
|
46
|
+
return [1, null, new Verbatim(systemId)];
|
|
39
47
|
}
|
|
40
48
|
|
|
41
49
|
/** Security reference number, the same in every message. */
|
|
@@ -125,7 +133,12 @@ export function buildMessage(parts: {
|
|
|
125
133
|
const trailer = new SegmentSpec('HNHBS', 1, [parts.messageNumber]).build(body.nextNumber);
|
|
126
134
|
// The header states the size of the whole message, itself included; its own size does not depend on it.
|
|
127
135
|
const header = (size: number): Uint8Array =>
|
|
128
|
-
new SegmentSpec('HNHBK', 3, [
|
|
136
|
+
new SegmentSpec('HNHBK', 3, [
|
|
137
|
+
size.toString().padStart(12, '0'),
|
|
138
|
+
300,
|
|
139
|
+
new Verbatim(parts.dialogId),
|
|
140
|
+
parts.messageNumber,
|
|
141
|
+
]).build(1);
|
|
129
142
|
const sizeOf = (sections: readonly Uint8Array[]): number => header(0).length + lengthOf(sections) + trailer.length;
|
|
130
143
|
const result = new BytesBuilder();
|
|
131
144
|
result.add(header(sizeOf([body.bytes])));
|
|
@@ -213,7 +226,7 @@ function signedBody(
|
|
|
213
226
|
}
|
|
214
227
|
|
|
215
228
|
/** A received message, with the segments of the encryption envelope unpacked. */
|
|
216
|
-
export class IncomingMessage3
|
|
229
|
+
export class IncomingMessage3 {
|
|
217
230
|
/**
|
|
218
231
|
* Return codes for the whole message (HIRMG), then for single request segments (HIRMS).
|
|
219
232
|
*
|
|
@@ -222,10 +235,12 @@ export class IncomingMessage3 implements BankResponse {
|
|
|
222
235
|
*/
|
|
223
236
|
readonly feedback: readonly FinTsFeedback[];
|
|
224
237
|
|
|
238
|
+
/** Whether the bank rejected the message as a whole (an error in HIRMG), rather than single orders in it. */
|
|
225
239
|
readonly messageRejected: boolean;
|
|
226
240
|
|
|
227
241
|
private constructor(
|
|
228
242
|
readonly segments: readonly Segment[],
|
|
243
|
+
/** The memory the message holds, in bytes: its length, and what its parts take beyond that. */
|
|
229
244
|
readonly size: number,
|
|
230
245
|
) {
|
|
231
246
|
this.feedback = Object.freeze(
|
|
@@ -238,34 +253,66 @@ export class IncomingMessage3 implements BankResponse {
|
|
|
238
253
|
}
|
|
239
254
|
|
|
240
255
|
/**
|
|
241
|
-
* Parses a message; throws `FormatError` if `data` is not
|
|
242
|
-
* maintenance notice
|
|
256
|
+
* Parses a message; throws `FormatError` if `data` is not one complete FinTS message, such as the HTML page of a
|
|
257
|
+
* 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.
|
|
259
|
+
*
|
|
260
|
+
* Its `size` counts the memory of its data elements along with its bytes, since a message of many short elements
|
|
261
|
+
* takes far more memory than bytes.
|
|
243
262
|
*/
|
|
244
263
|
static parse(data: Uint8Array): IncomingMessage3 {
|
|
245
|
-
const
|
|
264
|
+
const budget = { elements: maxDataElements };
|
|
265
|
+
const outer = parseSegments(data, budget);
|
|
246
266
|
if (outer[0]?.type !== 'HNHBK') throw new FormatError('Not a FinTS message: no message header');
|
|
267
|
+
if (outer.at(-1)?.type !== 'HNHBS') throw new FormatError('Incomplete FinTS message: no message trailer');
|
|
268
|
+
if (outer.filter((s) => s.type === 'HNHBK' || s.type === 'HNHBS').length > 2) {
|
|
269
|
+
throw new FormatError('More than one FinTS message');
|
|
270
|
+
}
|
|
247
271
|
const segments: Segment[] = [];
|
|
248
272
|
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 {
|
|
273
|
+
if (s.type !== 'HNVSD') {
|
|
253
274
|
segments.push(s);
|
|
275
|
+
continue;
|
|
254
276
|
}
|
|
277
|
+
const payload = s.binary(1);
|
|
278
|
+
if (payload === undefined) throw new FormatError('Encrypted data (HNVSD) without binary data');
|
|
279
|
+
for (const inner of parseSegments(payload, budget)) segments.push(inner);
|
|
255
280
|
}
|
|
256
|
-
return new IncomingMessage3(segments, data.length);
|
|
281
|
+
return new IncomingMessage3(segments, data.length + (maxDataElements - budget.elements) * dataElementMemory);
|
|
257
282
|
}
|
|
258
283
|
|
|
259
284
|
static combine(first: IncomingMessage3, second: IncomingMessage3): IncomingMessage3 {
|
|
260
285
|
return new IncomingMessage3([...first.segments, ...second.segments], first.size + second.size);
|
|
261
286
|
}
|
|
262
287
|
|
|
288
|
+
/** Free-text messages the bank addresses to the user. */
|
|
263
289
|
get bankMessages(): BankMessage[] {
|
|
264
290
|
return this.findAll('HIKIM').map((m) => new BankMessage(m.text(1) ?? '', m.text(2) ?? ''));
|
|
265
291
|
}
|
|
266
292
|
|
|
293
|
+
/** The dialog ID the bank assigned, as `Segment.verbatim` reads it, to send back byte for byte. */
|
|
267
294
|
get dialogId(): string | undefined {
|
|
268
|
-
return this.find('HNHBK')?.
|
|
295
|
+
return this.find('HNHBK')?.verbatim(3);
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
/**
|
|
299
|
+
* Whether the message answers message `number` of the dialog `dialogId`: header and trailer carry that number, and the
|
|
300
|
+
* header names that message where it names the message it refers to (Bezugsnachricht). The dialog ID counts only once
|
|
301
|
+
* the bank has assigned one; before, `dialogId` is `undefined`, since every first message goes out under the same.
|
|
302
|
+
*/
|
|
303
|
+
answers(number: number, dialogId: string | undefined): boolean {
|
|
304
|
+
const header = this.find('HNHBK');
|
|
305
|
+
// The trailer comes last, after the segments of the encryption envelope, which might hold one of their own.
|
|
306
|
+
const trailer = this.segments.at(-1);
|
|
307
|
+
if (header === undefined || trailer?.type !== 'HNHBS') return false;
|
|
308
|
+
const inDialog = (id: string | undefined): boolean => dialogId === undefined || id === dialogId;
|
|
309
|
+
const namesReference = header.group(5).some((e) => !e.isEmpty);
|
|
310
|
+
return (
|
|
311
|
+
header.integer(4) === number &&
|
|
312
|
+
trailer.integer(1) === number &&
|
|
313
|
+
inDialog(header.verbatim(3)) &&
|
|
314
|
+
(!namesReference || (header.integer(5, 1) === number && inDialog(header.verbatim(5, 0))))
|
|
315
|
+
);
|
|
269
316
|
}
|
|
270
317
|
|
|
271
318
|
find(type: string): Segment | undefined {
|
|
@@ -277,6 +324,10 @@ export class IncomingMessage3 implements BankResponse {
|
|
|
277
324
|
}
|
|
278
325
|
}
|
|
279
326
|
|
|
327
|
+
/**
|
|
328
|
+
* The return codes of `s`. Their parameters are values the client or the app sends back, such as the position to
|
|
329
|
+
* continue at or new login data, so they are read as `Segment.verbatim` reads them.
|
|
330
|
+
*/
|
|
280
331
|
function feedbackOf(s: Segment): FinTsFeedback[] {
|
|
281
332
|
const feedback: FinTsFeedback[] = [];
|
|
282
333
|
for (let i = 1; i < s.length; i++) {
|
|
@@ -284,7 +335,7 @@ function feedbackOf(s: Segment): FinTsFeedback[] {
|
|
|
284
335
|
if (code === undefined) continue;
|
|
285
336
|
const parameters: string[] = [];
|
|
286
337
|
// 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.
|
|
338
|
+
for (let j = 3; j < s.group(i).length; j++) parameters.push(s.verbatim(i, j) ?? '');
|
|
288
339
|
feedback.push(new FinTsFeedback({ code, reference: s.text(i, 1), text: s.text(i, 2) ?? '', parameters }));
|
|
289
340
|
}
|
|
290
341
|
return feedback;
|
|
@@ -298,7 +349,8 @@ function feedbackOf(s: Segment): FinTsFeedback[] {
|
|
|
298
349
|
export function decodeTransport(body: Uint8Array): Uint8Array {
|
|
299
350
|
if (startsWithMessageHeader(body)) return body;
|
|
300
351
|
try {
|
|
301
|
-
|
|
352
|
+
// Base64 is ASCII, which UTF-8 decodes natively; anything else is no base64 either way.
|
|
353
|
+
return decodeBase64Lines(utf8Decode(body, true));
|
|
302
354
|
} catch (error) {
|
|
303
355
|
if (error instanceof FormatError) return body;
|
|
304
356
|
throw error;
|
|
@@ -0,0 +1,160 @@
|
|
|
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 type { IncomingMessage3 } from './message.js';
|
|
7
|
+
import { Bpd3, decodeSegments, Upd3 } from './parameters.js';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* The parameter data and IDs the bank sends the user `config` names, which the messages and business transactions
|
|
11
|
+
* take, and which the saved state keeps: the bank and user parameter data, the customer system ID, and the names of
|
|
12
|
+
* the TAN media as the bank sent them.
|
|
13
|
+
*/
|
|
14
|
+
export class ParameterStore {
|
|
15
|
+
private systemIdValue: string | undefined;
|
|
16
|
+
private bpd: Bpd3 | undefined;
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Whether the bank parameter data came from the anonymous dialog. They serve until those of a dialog signed with the
|
|
20
|
+
* 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
|
|
21
|
+
* without the announcement of strong customer authentication. So signed dialogs report version 0 for them, which has
|
|
22
|
+
* the bank send its parameter data anew.
|
|
23
|
+
*/
|
|
24
|
+
private bpdAnonymous = false;
|
|
25
|
+
private upd: Upd3 | undefined;
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* The names of the TAN media in the bank's last list whose text differs from what the bank sent, by their text, as
|
|
29
|
+
* `parseTanMedia` notes them. Saved with the state, as the medium in use is.
|
|
30
|
+
*/
|
|
31
|
+
private tanMediumNames = new Map<string, string>();
|
|
32
|
+
|
|
33
|
+
constructor(readonly config: FinTsConfig) {}
|
|
34
|
+
|
|
35
|
+
/** The bank parameter data received so far, if any. */
|
|
36
|
+
get bankParameters(): Bpd3 | undefined {
|
|
37
|
+
return this.bpd;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** The user parameter data received so far, if any. */
|
|
41
|
+
get userParameters(): Upd3 | undefined {
|
|
42
|
+
return this.upd;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* The customer system ID the bank assigned in the synchronization, as `Segment.verbatim` reads it; `undefined`
|
|
47
|
+
* before. No dialog signed with the PIN starts without it.
|
|
48
|
+
*/
|
|
49
|
+
get systemId(): string | undefined {
|
|
50
|
+
return this.systemIdValue;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** The version of the bank parameter data a dialog reports: 0 where there are none, or only the anonymous dialog's. */
|
|
54
|
+
get reportedBpdVersion(): number {
|
|
55
|
+
return this.bpd === undefined || this.bpdAnonymous ? 0 : this.bpd.version;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* The version of the user parameter data a signed dialog reports: 0 where there are none, or where they name another
|
|
60
|
+
* 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
|
|
61
|
+
* relay mixed up the answers to the synchronizations of two users and the bank takes the other user's customer system
|
|
62
|
+
* ID. A bank that names the user by another ID than the login's, such as for an alias, only sends its user parameter
|
|
63
|
+
* data at every login.
|
|
64
|
+
*/
|
|
65
|
+
get reportedUpdVersion(): number {
|
|
66
|
+
const upd = this.upd;
|
|
67
|
+
return upd !== undefined && upd.userId === this.config.userId ? upd.version : 0;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Drops the customer system ID the bank no longer accepts, so that a synchronization is required again, and with it
|
|
72
|
+
* the user parameter data, which the next login delivers anew: what came along with an ID the bank does not know may
|
|
73
|
+
* not be the user's either (`reportedUpdVersion`). Returns whether there was an ID.
|
|
74
|
+
*/
|
|
75
|
+
forgetSystemId(): boolean {
|
|
76
|
+
const had = this.systemIdValue !== undefined;
|
|
77
|
+
this.systemIdValue = undefined;
|
|
78
|
+
this.upd = undefined;
|
|
79
|
+
return had;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Keeps the parameter data and IDs in `response`, an answer to the opening of a signed dialog: the login, which may be
|
|
84
|
+
* a synchronization, or its approval. The bank sends them there (Formals C.3.1); taken from the answer to an order,
|
|
85
|
+
* the parameter data a bank repeats there in part would replace the complete ones.
|
|
86
|
+
*/
|
|
87
|
+
keep(response: IncomingMessage3): void {
|
|
88
|
+
this.keepBankParameters(response, false);
|
|
89
|
+
this.upd = Upd3.fromSegments(response.segments, this.config.bankCode, this.config.countryCode) ?? this.upd;
|
|
90
|
+
this.systemIdValue = response.find('HISYN')?.verbatim(1) ?? this.systemIdValue;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Keeps the bank parameter data in `response`, the answer of an anonymous dialog, where there are none from a dialog
|
|
95
|
+
* signed with the PIN: until those come, the dialogs signed with the PIN have the bank send its parameter data anew,
|
|
96
|
+
* since it may send a user other ones (PIN/TAN B.4.3.1). Keeps nothing that belongs to a user.
|
|
97
|
+
*/
|
|
98
|
+
keepAnonymous(response: IncomingMessage3): void {
|
|
99
|
+
if (this.bpd === undefined || this.bpdAnonymous) this.keepBankParameters(response, true);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
private keepBankParameters(response: IncomingMessage3, anonymous: boolean): void {
|
|
103
|
+
const bpd = Bpd3.fromSegments(response.segments, this.config.bankCode);
|
|
104
|
+
if (bpd === undefined) return;
|
|
105
|
+
this.bpd = bpd;
|
|
106
|
+
this.bpdAnonymous = anonymous;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/** Keeps `names`, the names of the bank's latest list of TAN media as `parseTanMedia` notes them. */
|
|
110
|
+
keepTanMediumNames(names: Map<string, string>): void {
|
|
111
|
+
this.tanMediumNames = names;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/** The TAN medium `name` as the bank sent it in its list of TAN media; `name` itself for one it did not list. */
|
|
115
|
+
tanMediumAsSent(name: string): string {
|
|
116
|
+
return this.tanMediumNames.get(name) ?? name;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/** The parameter data and IDs as JSON data, for the client's saved state. */
|
|
120
|
+
saveState(): JsonObject {
|
|
121
|
+
return {
|
|
122
|
+
systemId: this.systemIdValue ?? null,
|
|
123
|
+
bpd: this.bpd?.encoded ?? null,
|
|
124
|
+
bpdAnonymous: this.bpdAnonymous,
|
|
125
|
+
upd: this.upd?.encoded ?? null,
|
|
126
|
+
tanMediumNames: Object.fromEntries(this.tanMediumNames),
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/** Restores what `saveState` saved; throws if `saved` is damaged, leaving the store unchanged. */
|
|
131
|
+
restoreState(saved: JsonObject): void {
|
|
132
|
+
const bpd = readOptionalString(saved, 'bpd');
|
|
133
|
+
const upd = readOptionalString(saved, 'upd');
|
|
134
|
+
const restoredSystemId = readOptionalString(saved, 'systemId');
|
|
135
|
+
// A state of the Dart package or an earlier version does not tell, so its parameter data are fetched anew.
|
|
136
|
+
const restoredBpdAnonymous = readOptionalBoolean(saved, 'bpdAnonymous') ?? true;
|
|
137
|
+
const restoredBpd = bpd === undefined ? undefined : Bpd3.fromSegments(decodeSegments(bpd), this.config.bankCode);
|
|
138
|
+
const restoredUpd =
|
|
139
|
+
upd === undefined
|
|
140
|
+
? undefined
|
|
141
|
+
: Upd3.fromSegments(decodeSegments(upd), this.config.bankCode, this.config.countryCode);
|
|
142
|
+
const restoredNames = readTanMediumNames(saved);
|
|
143
|
+
this.systemIdValue = restoredSystemId;
|
|
144
|
+
this.bpd = restoredBpd;
|
|
145
|
+
this.bpdAnonymous = restoredBpdAnonymous;
|
|
146
|
+
this.upd = restoredUpd;
|
|
147
|
+
this.tanMediumNames = restoredNames;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/** The TAN medium names `saveState` saved; none in a state of the Dart package or an earlier version. */
|
|
152
|
+
function readTanMediumNames(saved: JsonObject): Map<string, string> {
|
|
153
|
+
const names = readOptionalObject(saved, 'tanMediumNames') ?? {};
|
|
154
|
+
return new Map(
|
|
155
|
+
Object.entries(names).map(([name, asSent]) => {
|
|
156
|
+
if (typeof asSent !== 'string') throw new FormatError('Saved state: a TAN medium name is not a string');
|
|
157
|
+
return [name, asSent];
|
|
158
|
+
}),
|
|
159
|
+
);
|
|
160
|
+
}
|
package/src/v3/parameters.ts
CHANGED
|
@@ -3,10 +3,10 @@
|
|
|
3
3
|
import { Account } from '../core/account.js';
|
|
4
4
|
import type { Balance } from '../core/balance.js';
|
|
5
5
|
import { BankInfo } from '../core/bank-info.js';
|
|
6
|
-
import {
|
|
6
|
+
import { FinTsProtocolError } from '../core/errors.js';
|
|
7
7
|
import { TanMethod, tanMediumRequirementOf } from '../tan/tan-models.js';
|
|
8
|
-
import { base64Decode, base64Encode,
|
|
9
|
-
import { tryParseInt } from '../util/text.js';
|
|
8
|
+
import { base64Decode, base64Encode, concatBytes } from '../util/bytes.js';
|
|
9
|
+
import { nonBlank, tryParseInt } from '../util/text.js';
|
|
10
10
|
import { AccountExtension } from './account-extension.js';
|
|
11
11
|
import { parseSegments, type Segment } from './syntax.js';
|
|
12
12
|
|
|
@@ -46,14 +46,23 @@ function defined<T>(values: readonly (T | undefined)[]): T[] {
|
|
|
46
46
|
return values.filter((v): v is T => v !== undefined);
|
|
47
47
|
}
|
|
48
48
|
|
|
49
|
+
/** `bpd`, or `FinTsProtocolError` if the bank has not sent its parameter data yet. */
|
|
50
|
+
export function requireBankParameters(bpd: Bpd3 | undefined): Bpd3 {
|
|
51
|
+
if (bpd === undefined) throw new FinTsProtocolError('No bank parameter data available');
|
|
52
|
+
return bpd;
|
|
53
|
+
}
|
|
54
|
+
|
|
49
55
|
/** FinTS 3.0 bank parameter data (BPD): HIBPA and the parameter segments (`HI…S`), parsed once. */
|
|
50
|
-
export class Bpd3
|
|
56
|
+
export class Bpd3 {
|
|
51
57
|
readonly bankInfo: BankInfo;
|
|
52
58
|
|
|
53
59
|
/** The two-step TAN methods of HITANS versions 6 and 7, by security function. */
|
|
54
60
|
private readonly offeredMethods: ReadonlyMap<string, OfferedMethod>;
|
|
55
61
|
|
|
62
|
+
/** Two-step TAN methods the bank offers and the client can run. */
|
|
56
63
|
readonly tanMethods: readonly TanMethod[];
|
|
64
|
+
|
|
65
|
+
/** Security functions of the two-step methods the bank offers in a form the client cannot run. */
|
|
57
66
|
readonly unsupportedTanMethods: readonly string[];
|
|
58
67
|
readonly pinTan: PinTanParameters;
|
|
59
68
|
|
|
@@ -69,6 +78,8 @@ export class Bpd3 implements BankParameters {
|
|
|
69
78
|
*/
|
|
70
79
|
readonly dialogLanguage: number;
|
|
71
80
|
|
|
81
|
+
private encodedValue: string | undefined;
|
|
82
|
+
|
|
72
83
|
private constructor(
|
|
73
84
|
readonly segments: readonly Segment[],
|
|
74
85
|
hibpa: Segment,
|
|
@@ -99,16 +110,14 @@ export class Bpd3 implements BankParameters {
|
|
|
99
110
|
this.dialogLanguage = dialogLanguageOf(segments, this.bankInfo);
|
|
100
111
|
}
|
|
101
112
|
|
|
113
|
+
/** The version of the parameter data, which the client reports back so that the bank sends newer ones only. */
|
|
102
114
|
get version(): number {
|
|
103
115
|
return this.bankInfo.bpdVersion;
|
|
104
116
|
}
|
|
105
117
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
allowsWithPinTan(transaction: string): boolean {
|
|
111
|
-
return this.pinTan.allowsWithPinTan(transaction);
|
|
118
|
+
/** The segments as the saved state keeps them, encoded once. */
|
|
119
|
+
get encoded(): string {
|
|
120
|
+
return (this.encodedValue ??= encodeSegments(this.segments));
|
|
112
121
|
}
|
|
113
122
|
|
|
114
123
|
/** The BPD in `all`, or `undefined` if they contain no HIBPA segment. */
|
|
@@ -123,8 +132,13 @@ export class Bpd3 implements BankParameters {
|
|
|
123
132
|
return `HI${transaction.substring(2)}S`;
|
|
124
133
|
}
|
|
125
134
|
|
|
135
|
+
/** The highest version of `transaction` that the bank offers and the client `implemented`. */
|
|
126
136
|
bestVersion(transaction: string, implemented: ReadonlySet<number>): number | undefined {
|
|
127
|
-
|
|
137
|
+
let best: number | undefined;
|
|
138
|
+
for (const version of this.versions.get(Bpd3.parameterName(transaction)) ?? []) {
|
|
139
|
+
if (implemented.has(version) && (best === undefined || version > best)) best = version;
|
|
140
|
+
}
|
|
141
|
+
return best;
|
|
128
142
|
}
|
|
129
143
|
|
|
130
144
|
/** The version of the HKTAN segments that go with `method`; `undefined` for a method the bank does not offer. */
|
|
@@ -293,7 +307,8 @@ interface UpdEntry {
|
|
|
293
307
|
readonly extension: AccountExtension | undefined;
|
|
294
308
|
}
|
|
295
309
|
|
|
296
|
-
/** The version and the usage ("UPD-Verwendung") of the user parameter data in HIUPA. */
|
|
310
|
+
/** The user ID (Benutzerkennung), the version and the usage ("UPD-Verwendung") of the user parameter data in HIUPA. */
|
|
311
|
+
const hiupaUserId = 1;
|
|
297
312
|
const hiupaVersion = 2;
|
|
298
313
|
const hiupaUsage = 3;
|
|
299
314
|
|
|
@@ -301,15 +316,30 @@ const hiupaUsage = 3;
|
|
|
301
316
|
const transactionCode = /^[A-Z]{4,6}$/;
|
|
302
317
|
|
|
303
318
|
/** FinTS 3.0 user parameter data (UPD): HIUPA and the accounts (HIUPD), parsed once. */
|
|
304
|
-
export class Upd3
|
|
319
|
+
export class Upd3 {
|
|
320
|
+
/** The user the bank sent them for; `undefined` where it names none. */
|
|
321
|
+
readonly userId: string | undefined;
|
|
322
|
+
|
|
323
|
+
/** The version of the parameter data, which the client reports back so that the bank sends newer ones only. */
|
|
305
324
|
readonly version: number;
|
|
325
|
+
|
|
326
|
+
/**
|
|
327
|
+
* Whether the lists of permitted transactions are complete: the bank permits nothing they leave out ("UPD-Verwendung"
|
|
328
|
+
* 0).
|
|
329
|
+
*/
|
|
306
330
|
readonly listsAllTransactions: boolean;
|
|
331
|
+
|
|
332
|
+
/** The transactions any entry lists as permitted, with or without an account. */
|
|
307
333
|
readonly listedTransactions: ReadonlySet<string>;
|
|
308
334
|
|
|
309
335
|
/** The accounts with the extension each carries, where it does. */
|
|
310
336
|
private readonly entries: readonly UpdEntry[];
|
|
337
|
+
|
|
338
|
+
/** The accounts the user may access. */
|
|
311
339
|
readonly accounts: readonly Account[];
|
|
312
340
|
|
|
341
|
+
private encodedValue: string | undefined;
|
|
342
|
+
|
|
313
343
|
/** `listsAll` tells whether HIUPA declares the lists of permitted transactions complete ("UPD-Verwendung" 0). */
|
|
314
344
|
private constructor(
|
|
315
345
|
readonly segments: readonly Segment[],
|
|
@@ -319,6 +349,7 @@ export class Upd3 implements UserParameters {
|
|
|
319
349
|
defaultCountry: string,
|
|
320
350
|
) {
|
|
321
351
|
const hiupds = segments.filter((s) => s.type === 'HIUPD');
|
|
352
|
+
this.userId = hiupa.text(hiupaUserId)?.trim();
|
|
322
353
|
this.version = hiupa.integer(hiupaVersion) ?? 0;
|
|
323
354
|
this.listsAllTransactions = listsAll;
|
|
324
355
|
this.listedTransactions = new Set(hiupds.flatMap(transactionsOf));
|
|
@@ -328,11 +359,15 @@ export class Upd3 implements UserParameters {
|
|
|
328
359
|
this.accounts = Object.freeze(this.entries.map((e) => e.account));
|
|
329
360
|
}
|
|
330
361
|
|
|
331
|
-
/**
|
|
332
|
-
get
|
|
333
|
-
return
|
|
362
|
+
/** The segments as the saved state keeps them, encoded once. */
|
|
363
|
+
get encoded(): string {
|
|
364
|
+
return (this.encodedValue ??= encodeSegments(this.segments));
|
|
334
365
|
}
|
|
335
366
|
|
|
367
|
+
/**
|
|
368
|
+
* The balance of `account` the user parameter data carry, where it holds all a balance query would deliver, so that
|
|
369
|
+
* the client asks for none (Formals E.3.1, F.3.2); `undefined` otherwise.
|
|
370
|
+
*/
|
|
336
371
|
completeBalance(account: Account): Balance | undefined {
|
|
337
372
|
return this.entries.find((e) => e.account.matches(account))?.extension?.completeBalance(account);
|
|
338
373
|
}
|
|
@@ -398,19 +433,8 @@ function accountOf(
|
|
|
398
433
|
listsAll: boolean,
|
|
399
434
|
): Account | undefined {
|
|
400
435
|
const version = s.version;
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
let subAccount: string | undefined;
|
|
404
|
-
let country: string | undefined;
|
|
405
|
-
let bank: string | undefined;
|
|
406
|
-
if (nationalAccount.length >= 4) {
|
|
407
|
-
[number, subAccount, country, bank] = nationalAccount;
|
|
408
|
-
} else if (nationalAccount.length === 3) {
|
|
409
|
-
// Older banks leave out the sub-account.
|
|
410
|
-
[number, country, bank] = nationalAccount;
|
|
411
|
-
} else if (nationalAccount.length > 0) {
|
|
412
|
-
number = nationalAccount[0];
|
|
413
|
-
}
|
|
436
|
+
// Every version names the account as KTV2, by position: a sub-account or bank code the bank leaves out is empty.
|
|
437
|
+
const [number, subAccount, country, bank] = s.texts(1);
|
|
414
438
|
const iban = version >= 6 ? s.text(2) : undefined;
|
|
415
439
|
if (number === undefined && iban === undefined) return undefined;
|
|
416
440
|
// Field positions by HIUPD version: version 6 inserts the IBAN, version 5 the account type.
|
|
@@ -420,9 +444,6 @@ function accountOf(
|
|
|
420
444
|
: version === 5
|
|
421
445
|
? { customer: 2, type: 3, currency: 4, name1: 5, name2: 6, product: 7 }
|
|
422
446
|
: { customer: 2, type: undefined, currency: 3, name1: 4, name2: 5, product: 6 };
|
|
423
|
-
const owner = defined([s.text(fields.name1), s.text(fields.name2)])
|
|
424
|
-
.join(' ')
|
|
425
|
-
.trim();
|
|
426
447
|
return new Account({
|
|
427
448
|
accountNumber: number,
|
|
428
449
|
subAccount,
|
|
@@ -432,18 +453,70 @@ function accountOf(
|
|
|
432
453
|
customerId: s.text(fields.customer),
|
|
433
454
|
typeCode: fields.type === undefined ? undefined : s.integer(fields.type),
|
|
434
455
|
currency: s.text(fields.currency),
|
|
435
|
-
ownerName:
|
|
456
|
+
ownerName: nonBlank(defined([s.text(fields.name1), s.text(fields.name2)]).join(' ')),
|
|
436
457
|
productName: s.text(fields.product),
|
|
437
458
|
allowedTransactions: transactionsOf(s),
|
|
438
459
|
allowedTransactionsComplete: listsAll,
|
|
439
460
|
});
|
|
440
461
|
}
|
|
441
462
|
|
|
463
|
+
/**
|
|
464
|
+
* The version of `transaction` to send, for `account` where it concerns one: the highest the bank offers and the client
|
|
465
|
+
* `implemented`. `undefined` where there is none, or the parameter data do not permit the transaction
|
|
466
|
+
* (`permitsTransaction`).
|
|
467
|
+
*/
|
|
468
|
+
export function offeredVersion(
|
|
469
|
+
bpd: Bpd3 | undefined,
|
|
470
|
+
upd: Upd3 | undefined,
|
|
471
|
+
transaction: string,
|
|
472
|
+
implemented: ReadonlySet<number>,
|
|
473
|
+
options: { account?: Account; requiresEntry?: boolean },
|
|
474
|
+
): number | undefined {
|
|
475
|
+
return permitsTransaction(bpd, upd, transaction, options) ? bpd?.bestVersion(transaction, implemented) : undefined;
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
/**
|
|
479
|
+
* Whether the parameter data let the user send `transaction`, for `account` where it concerns one: the bank allows it
|
|
480
|
+
* with PIN/TAN, and the user parameter data permit it (Formals E.1, F.3.4; PIN/TAN B.1, B.8.1).
|
|
481
|
+
*
|
|
482
|
+
* - `account`, which the session has taken as the bank knows it (`SepaAccounts.complete`), permits what it lists, and
|
|
483
|
+
* anything else only where its list is incomplete ("UPD-Verwendung" 1). One only the SEPA account list names lists
|
|
484
|
+
* nothing, and so permits anything; where the lists are complete, the session refuses one the bank names nowhere.
|
|
485
|
+
* - A transaction without account, such as the TAN media list, is permitted where some entry lists it or the lists are
|
|
486
|
+
* incomplete.
|
|
487
|
+
* - One that `requiresEntry`, the SEPA account list, must be listed even where the lists are incomplete (Formals F.3.4),
|
|
488
|
+
* unless they list nothing at all, which tells nothing.
|
|
489
|
+
*/
|
|
490
|
+
function permitsTransaction(
|
|
491
|
+
bpd: Bpd3 | undefined,
|
|
492
|
+
upd: Upd3 | undefined,
|
|
493
|
+
transaction: string,
|
|
494
|
+
options: { account?: Account; requiresEntry?: boolean },
|
|
495
|
+
): boolean {
|
|
496
|
+
if (bpd !== undefined && !bpd.pinTan.allowsWithPinTan(transaction)) return false;
|
|
497
|
+
if (upd === undefined) return true;
|
|
498
|
+
if (options.account !== undefined) return allows(options.account, transaction);
|
|
499
|
+
const listed = upd.listedTransactions;
|
|
500
|
+
if (listed.has(transaction)) return true;
|
|
501
|
+
return !upd.listsAllTransactions && (!(options.requiresEntry ?? false) || listed.size === 0);
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
/** Whether the bank lists `transaction`, such as `HKCAZ`, as permitted for `account`. */
|
|
505
|
+
export function lists(account: Account, transaction: string): boolean {
|
|
506
|
+
return account.allowedTransactions.includes(transaction);
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
/**
|
|
510
|
+
* Whether the bank may permit `transaction` on `account`: it lists it, or its list is not complete
|
|
511
|
+
* (`Account.allowedTransactionsComplete`), so that only the bank can tell.
|
|
512
|
+
*/
|
|
513
|
+
function allows(account: Account, transaction: string): boolean {
|
|
514
|
+
return lists(account, transaction) || !account.allowedTransactionsComplete;
|
|
515
|
+
}
|
|
516
|
+
|
|
442
517
|
/** Serializes parameter segments for the saved state. */
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
for (const s of segments) out.add(s.encode());
|
|
446
|
-
return base64Encode(out.takeBytes());
|
|
518
|
+
function encodeSegments(segments: readonly Segment[]): string {
|
|
519
|
+
return base64Encode(concatBytes(segments.map((s) => s.encode())));
|
|
447
520
|
}
|
|
448
521
|
|
|
449
522
|
export function decodeSegments(data: string): Segment[] {
|