@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/protocol.ts
CHANGED
|
@@ -7,13 +7,19 @@ import type { FinTsTransport } from '../core/transport.js';
|
|
|
7
7
|
import { decodeBankText } from '../util/text.js';
|
|
8
8
|
import { BankConnection, checkMessageSize } from './bank-connection.js';
|
|
9
9
|
import { buildMessage, decodeTransport, encodeTransport } from './message.js';
|
|
10
|
-
import type { ParameterStore } from './parameter-store.js';
|
|
10
|
+
import type { MessageLimits, ParameterStore } from './parameter-store.js';
|
|
11
11
|
import { Segments } from './segments.js';
|
|
12
12
|
import type { SegmentSpec } from './syntax.js';
|
|
13
13
|
|
|
14
14
|
/** Customer ID the specification reserves for anonymous dialogs. */
|
|
15
15
|
const anonymousCustomerId = '9999999999';
|
|
16
16
|
|
|
17
|
+
/**
|
|
18
|
+
* How long the request on its way when the client is closed may take, and then its dialog ends, where the transport
|
|
19
|
+
* takes the abort (`beginClose`).
|
|
20
|
+
*/
|
|
21
|
+
const closingGraceMs = 5000;
|
|
22
|
+
|
|
17
23
|
/** How a message is signed: the security function of the TAN method and the customer system ID. */
|
|
18
24
|
export interface Signing3 {
|
|
19
25
|
readonly securityFunction: string;
|
|
@@ -29,6 +35,15 @@ export interface PinGuard {
|
|
|
29
35
|
loginSent(): void;
|
|
30
36
|
}
|
|
31
37
|
|
|
38
|
+
/**
|
|
39
|
+
* Throws if `limits` rule out a message of `size` bytes, or the length of `loginPin`, the PIN of a login: `TypeError`
|
|
40
|
+
* for the PIN, which the bank would count as a failed attempt, and `FinTsProtocolError` for the size.
|
|
41
|
+
*/
|
|
42
|
+
function checkLimits(limits: MessageLimits, size: number, loginPin: string | undefined): void {
|
|
43
|
+
if (loginPin !== undefined) requirePinLength(loginPin, limits.pinLength?.min, limits.pinLength?.max);
|
|
44
|
+
checkMessageSize(size, limits.maxMessageSize);
|
|
45
|
+
}
|
|
46
|
+
|
|
32
47
|
/** Whether `segments` are those of a dialog end, the one message a closing client still sends. */
|
|
33
48
|
function isDialogEnd(segments: readonly SegmentSpec[]): boolean {
|
|
34
49
|
return segments.length === 1 && segments[0]!.type === 'HKEND';
|
|
@@ -43,9 +58,11 @@ export class FinTs3Protocol {
|
|
|
43
58
|
private readonly config: FinTsConfig;
|
|
44
59
|
private readonly connection: BankConnection;
|
|
45
60
|
|
|
46
|
-
/** Whether the client is closing, so that only dialog ends go out (`beginClose`). */
|
|
47
61
|
private closing = false;
|
|
48
62
|
|
|
63
|
+
/** The error with which the limits last stopped a login (`stoppedLogin`). */
|
|
64
|
+
private stoppedLoginError: unknown;
|
|
65
|
+
|
|
49
66
|
constructor(
|
|
50
67
|
readonly parameters: ParameterStore,
|
|
51
68
|
private readonly guard: PinGuard,
|
|
@@ -91,26 +108,28 @@ export class FinTs3Protocol {
|
|
|
91
108
|
}): Promise<Uint8Array> {
|
|
92
109
|
if (this.closing && !isDialogEnd(message.segments)) throw clientClosedError();
|
|
93
110
|
const signing = message.signing;
|
|
111
|
+
const security =
|
|
112
|
+
signing === undefined
|
|
113
|
+
? undefined
|
|
114
|
+
: {
|
|
115
|
+
countryCode: this.config.countryCode,
|
|
116
|
+
bankCode: this.config.bankCode,
|
|
117
|
+
userId: this.config.userId,
|
|
118
|
+
systemId: signing.systemId,
|
|
119
|
+
pin: this.guard.pin,
|
|
120
|
+
securityFunction: signing.securityFunction,
|
|
121
|
+
};
|
|
94
122
|
// The first message of a signed dialog logs in: from here on the bank may count the PIN.
|
|
95
|
-
const login =
|
|
123
|
+
const login = security !== undefined && message.number === 1;
|
|
96
124
|
const outgoing = buildMessage({
|
|
97
125
|
dialogId: message.dialogId,
|
|
98
126
|
messageNumber: message.number,
|
|
99
127
|
segments: message.segments,
|
|
100
|
-
security
|
|
101
|
-
signing === undefined
|
|
102
|
-
? undefined
|
|
103
|
-
: {
|
|
104
|
-
countryCode: this.config.countryCode,
|
|
105
|
-
bankCode: this.config.bankCode,
|
|
106
|
-
userId: this.config.userId,
|
|
107
|
-
systemId: signing.systemId,
|
|
108
|
-
pin: login ? this.pinForLogin() : this.guard.pin,
|
|
109
|
-
securityFunction: signing.securityFunction,
|
|
110
|
-
},
|
|
128
|
+
security,
|
|
111
129
|
tan: message.tan,
|
|
112
130
|
});
|
|
113
|
-
|
|
131
|
+
this.checkOutgoing(outgoing.bytes.length, login ? security.pin : undefined);
|
|
132
|
+
this.connection.checkSendable();
|
|
114
133
|
this.connection.log('outgoing', () => outgoing.masked);
|
|
115
134
|
if (login) this.guard.loginSent();
|
|
116
135
|
const bytes = decodeTransport(await this.connection.post(encodeTransport(outgoing.bytes), 'text/plain'));
|
|
@@ -119,27 +138,41 @@ export class FinTs3Protocol {
|
|
|
119
138
|
}
|
|
120
139
|
|
|
121
140
|
/**
|
|
122
|
-
*
|
|
141
|
+
* Throws if the limits of the parameter data rule out a message of `size` bytes, or the length of `loginPin`, the PIN
|
|
142
|
+
* of a login (`checkLimits`). The later messages of a dialog, its end included, take the PIN the bank has accepted,
|
|
143
|
+
* whatever the parameter data it delivered since say.
|
|
123
144
|
*
|
|
124
|
-
*
|
|
125
|
-
*
|
|
126
|
-
* the PIN the bank has accepted, whatever the parameter data it delivered since say.
|
|
145
|
+
* Remembers the error that stops a login (`stoppedLogin`): the parameter data may be outdated, as saved ones are,
|
|
146
|
+
* and new ones would come only in answer to the login they rule out.
|
|
127
147
|
*/
|
|
128
|
-
private
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
148
|
+
private checkOutgoing(size: number, loginPin: string | undefined): void {
|
|
149
|
+
try {
|
|
150
|
+
checkLimits(this.parameters.limits, size, loginPin);
|
|
151
|
+
} catch (error) {
|
|
152
|
+
if (loginPin !== undefined) this.stoppedLoginError = error;
|
|
153
|
+
throw error;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/** Whether `error` is the one with which the limits of the parameter data last stopped a login. */
|
|
158
|
+
stoppedLogin(error: unknown): boolean {
|
|
159
|
+
return this.stoppedLoginError !== undefined && this.stoppedLoginError === error;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/** Whether the client is closing, so that only dialog ends go out (`beginClose`). */
|
|
163
|
+
get isClosing(): boolean {
|
|
164
|
+
return this.closing;
|
|
133
165
|
}
|
|
134
166
|
|
|
135
167
|
/**
|
|
136
|
-
*
|
|
137
|
-
*
|
|
138
|
-
*
|
|
168
|
+
* Sends nothing but dialog ends from now on: any other message fails with `FinTsStateError` before it goes out, so
|
|
169
|
+
* that a call under way stops at its next one. The request on its way has `closingGraceMs` to bring its answer, which
|
|
170
|
+
* counts as any other, and is aborted then; the dialog ends have as long again once it is over, and none goes out
|
|
171
|
+
* later. The client calls it as soon as it is closed, and `close` once it has ended its dialog.
|
|
139
172
|
*/
|
|
140
173
|
beginClose(): void {
|
|
141
174
|
this.closing = true;
|
|
142
|
-
this.connection.
|
|
175
|
+
this.connection.abortRequestsAfter(closingGraceMs);
|
|
143
176
|
}
|
|
144
177
|
|
|
145
178
|
close(): void {
|
package/src/v3/queries.ts
CHANGED
|
@@ -104,8 +104,9 @@ export function balanceQuery(parameters: ParameterStore, account: Account): Quer
|
|
|
104
104
|
if (offered === undefined) {
|
|
105
105
|
throw new FinTsUnsupportedError('The bank reports no balance (HKSAL) for this account');
|
|
106
106
|
}
|
|
107
|
+
const nationalAllowed = requireBankParameters(parameters.bankParameters).nationalAccountAllowed;
|
|
107
108
|
return {
|
|
108
|
-
order: (continuation) => Segments.balance(offered, account, continuation),
|
|
109
|
+
order: (continuation) => Segments.balance(offered, account, nationalAllowed, continuation),
|
|
109
110
|
read: (pages) =>
|
|
110
111
|
parseBalance(
|
|
111
112
|
balanceRecord(
|
|
@@ -123,8 +124,8 @@ export function balanceQuery(parameters: ParameterStore, account: Account): Quer
|
|
|
123
124
|
* the bank knows it, with the transactions the bank permits for it and its currency (`SepaAccounts.complete`).
|
|
124
125
|
*
|
|
125
126
|
* The client reads camt.052 only, so camt counts as offered only where the bank offers a camt.052 format. With `auto`
|
|
126
|
-
* the client takes camt where the bank offers it, unless
|
|
127
|
-
*
|
|
127
|
+
* the client takes camt where the bank offers it, unless MT940 can be requested for the account and the user parameter
|
|
128
|
+
* data list only MT940 transactions for it.
|
|
128
129
|
*
|
|
129
130
|
* Throws `FinTsUnsupportedError` if the bank does not offer the chosen format for the account.
|
|
130
131
|
*/
|
|
@@ -135,14 +136,15 @@ export function transactionsQuery(
|
|
|
135
136
|
from: Date | undefined,
|
|
136
137
|
to: Date | undefined,
|
|
137
138
|
): Query<Transaction[]> {
|
|
138
|
-
const
|
|
139
|
-
|
|
140
|
-
);
|
|
141
|
-
const camtVersion = camtFormats.length === 0 ? undefined : version(parameters, 'HKCAZ', [1], { account });
|
|
139
|
+
const bpd = requireBankParameters(parameters.bankParameters);
|
|
140
|
+
const camtVersion = bpd.camtFormats.length === 0 ? undefined : version(parameters, 'HKCAZ', [1], { account });
|
|
141
|
+
const mt940Version = version(parameters, 'HKKAZ', [6, 7], { account });
|
|
142
142
|
const camt =
|
|
143
143
|
format === 'camt' ||
|
|
144
|
-
(format === 'auto' &&
|
|
145
|
-
|
|
144
|
+
(format === 'auto' &&
|
|
145
|
+
camtVersion !== undefined &&
|
|
146
|
+
(mt940Version === undefined || lists(account, 'HKCAZ') || !lists(account, 'HKKAZ')));
|
|
147
|
+
const offered = camt ? camtVersion : mt940Version;
|
|
146
148
|
if (offered === undefined) {
|
|
147
149
|
throw new FinTsUnsupportedError(
|
|
148
150
|
`The bank offers no ${camt ? 'camt ' : ''}transactions (${camt ? 'HKCAZ' : 'HKKAZ'}) for this account`,
|
|
@@ -150,9 +152,14 @@ export function transactionsQuery(
|
|
|
150
152
|
}
|
|
151
153
|
if (camt) {
|
|
152
154
|
return {
|
|
153
|
-
order: (continuation) =>
|
|
155
|
+
order: (continuation) =>
|
|
156
|
+
Segments.transactionsCamt(offered, account, bpd.nationalAccountAllowed, bpd.camtFormats, {
|
|
157
|
+
from,
|
|
158
|
+
to,
|
|
159
|
+
continuation,
|
|
160
|
+
}),
|
|
154
161
|
// The first element names the account, the third element group holds the booked documents, the fourth
|
|
155
|
-
// element the pending one.
|
|
162
|
+
// element the pending one: binary data, which a bank may send as text instead.
|
|
156
163
|
read: (pages) => {
|
|
157
164
|
const segments = all(pages, 'HICAZ');
|
|
158
165
|
if (!segments.every((s) => camtAccount(s).matches(account))) {
|
|
@@ -165,23 +172,24 @@ export function transactionsQuery(
|
|
|
165
172
|
.filter((d) => !d.isEmpty)
|
|
166
173
|
.map((d) => d.bytes),
|
|
167
174
|
),
|
|
168
|
-
segments.map((s) => s.
|
|
175
|
+
segments.map((s) => s.bytes(4)).filter((d) => d !== undefined),
|
|
169
176
|
);
|
|
170
177
|
},
|
|
171
178
|
};
|
|
172
179
|
}
|
|
173
180
|
return {
|
|
174
|
-
order: (continuation) =>
|
|
175
|
-
|
|
176
|
-
//
|
|
177
|
-
//
|
|
181
|
+
order: (continuation) =>
|
|
182
|
+
Segments.transactionsMt940(offered, account, bpd.nationalAccountAllowed, { from, to, continuation }),
|
|
183
|
+
// The first element holds the booked transactions, the second the pending ones: binary data, which a bank may send
|
|
184
|
+
// as text instead. Unlike HICAZ, HIKAZ names no account, and banks write the account line of a statement (`:25:`)
|
|
185
|
+
// in many ways, so it is not compared with `account`: a strict comparison would fail every query of such a bank.
|
|
178
186
|
read: (pages) =>
|
|
179
187
|
mt940Transactions(
|
|
180
188
|
all(pages, 'HIKAZ')
|
|
181
|
-
.map((s) => s.
|
|
189
|
+
.map((s) => s.bytes(1))
|
|
182
190
|
.filter((d) => d !== undefined),
|
|
183
191
|
all(pages, 'HIKAZ')
|
|
184
|
-
.map((s) => s.
|
|
192
|
+
.map((s) => s.bytes(2))
|
|
185
193
|
.filter((d) => d !== undefined),
|
|
186
194
|
// The currency of statements that state none.
|
|
187
195
|
account.currency,
|
package/src/v3/segments.ts
CHANGED
|
@@ -6,6 +6,7 @@ import { Balance } from '../core/balance.js';
|
|
|
6
6
|
import { TanMedium, tanMediumStatusOf } from '../tan/tan-models.js';
|
|
7
7
|
import { formatFintsDate, parseFintsDate, parseFintsDateTime } from '../util/dates.js';
|
|
8
8
|
import { FormatError } from '../util/errors.js';
|
|
9
|
+
import { trimBlanks } from '../util/text.js';
|
|
9
10
|
import { type Segment, SegmentSpec, type SegmentValue, Verbatim } from './syntax.js';
|
|
10
11
|
|
|
11
12
|
/**
|
|
@@ -26,9 +27,13 @@ function nationalAccount(a: Account): SegmentValue[] {
|
|
|
26
27
|
return [a.accountNumber, a.subAccount, a.countryCode, a.bankCode];
|
|
27
28
|
}
|
|
28
29
|
|
|
29
|
-
/**
|
|
30
|
-
|
|
31
|
-
|
|
30
|
+
/**
|
|
31
|
+
* International account (KTI): IBAN and BIC, then the national account where the bank allows it (HISPAS). An IBAN goes
|
|
32
|
+
* only with its BIC (Messages B.3.2): without one the national account names the account, where it has a number.
|
|
33
|
+
*/
|
|
34
|
+
function internationalAccount(a: Account, nationalAllowed: boolean): SegmentValue[] {
|
|
35
|
+
const iban = a.bic !== undefined || a.accountNumber === undefined ? a.iban : undefined;
|
|
36
|
+
return [iban, a.bic, ...(nationalAllowed || iban === undefined ? nationalAccount(a) : [])];
|
|
32
37
|
}
|
|
33
38
|
|
|
34
39
|
function dateOrEmpty(date: Date | undefined): string | undefined {
|
|
@@ -89,14 +94,10 @@ export const Segments = Object.freeze({
|
|
|
89
94
|
|
|
90
95
|
/**
|
|
91
96
|
* HKTAN process 4: asks for strong customer authentication of the `segmentType` sent in the same message, with the TAN
|
|
92
|
-
* medium `mediumName
|
|
97
|
+
* medium `mediumName`: a `Verbatim` where it has to go back as the bank sent it (`ParameterStore.tanMediumToSend`).
|
|
93
98
|
*/
|
|
94
|
-
tanProcess4(version: number, segmentType: string, mediumName?: string): SegmentSpec {
|
|
95
|
-
return SegmentSpec.numbered('HKTAN', version, {
|
|
96
|
-
1: '4',
|
|
97
|
-
2: segmentType,
|
|
98
|
-
[tanMediumField]: Verbatim.of(mediumName),
|
|
99
|
-
});
|
|
99
|
+
tanProcess4(version: number, segmentType: string, mediumName?: string | Verbatim): SegmentSpec {
|
|
100
|
+
return SegmentSpec.numbered('HKTAN', version, { 1: '4', 2: segmentType, [tanMediumField]: mediumName });
|
|
100
101
|
},
|
|
101
102
|
|
|
102
103
|
/** HKTAN process 2: submits the TAN (carried in the signature) for the order with `reference`. */
|
|
@@ -117,24 +118,28 @@ export const Segments = Object.freeze({
|
|
|
117
118
|
});
|
|
118
119
|
},
|
|
119
120
|
|
|
120
|
-
/**
|
|
121
|
-
|
|
121
|
+
/**
|
|
122
|
+
* HKSAL: the balance of one account (not of all accounts), named by its national account as well where
|
|
123
|
+
* `nationalAllowed` (HISPAS).
|
|
124
|
+
*/
|
|
125
|
+
balance(version: number, account: Account, nationalAllowed: boolean, continuation?: string): SegmentSpec {
|
|
122
126
|
return new SegmentSpec('HKSAL', version, [
|
|
123
|
-
version >= 7 ? internationalAccount(account) : nationalAccount(account),
|
|
127
|
+
version >= 7 ? internationalAccount(account, nationalAllowed) : nationalAccount(account),
|
|
124
128
|
false,
|
|
125
129
|
null,
|
|
126
130
|
Verbatim.of(continuation),
|
|
127
131
|
]);
|
|
128
132
|
},
|
|
129
133
|
|
|
130
|
-
/** HKKAZ: MT940 transactions of one account
|
|
134
|
+
/** HKKAZ: MT940 transactions of one account, named by its national account as well where `nationalAllowed`. */
|
|
131
135
|
transactionsMt940(
|
|
132
136
|
version: number,
|
|
133
137
|
account: Account,
|
|
138
|
+
nationalAllowed: boolean,
|
|
134
139
|
period: { from?: Date | undefined; to?: Date | undefined; continuation?: string | undefined },
|
|
135
140
|
): SegmentSpec {
|
|
136
141
|
return new SegmentSpec('HKKAZ', version, [
|
|
137
|
-
version >= 7 ? internationalAccount(account) : nationalAccount(account),
|
|
142
|
+
version >= 7 ? internationalAccount(account, nationalAllowed) : nationalAccount(account),
|
|
138
143
|
false,
|
|
139
144
|
dateOrEmpty(period.from),
|
|
140
145
|
dateOrEmpty(period.to),
|
|
@@ -143,15 +148,19 @@ export const Segments = Object.freeze({
|
|
|
143
148
|
]);
|
|
144
149
|
},
|
|
145
150
|
|
|
146
|
-
/**
|
|
151
|
+
/**
|
|
152
|
+
* HKCAZ: camt transactions of one account in one of `formats`, named by its national account as well where
|
|
153
|
+
* `nationalAllowed`.
|
|
154
|
+
*/
|
|
147
155
|
transactionsCamt(
|
|
148
156
|
version: number,
|
|
149
157
|
account: Account,
|
|
158
|
+
nationalAllowed: boolean,
|
|
150
159
|
formats: readonly string[],
|
|
151
160
|
period: { from?: Date | undefined; to?: Date | undefined; continuation?: string | undefined },
|
|
152
161
|
): SegmentSpec {
|
|
153
162
|
return new SegmentSpec('HKCAZ', version, [
|
|
154
|
-
internationalAccount(account),
|
|
163
|
+
internationalAccount(account, nationalAllowed),
|
|
155
164
|
formats,
|
|
156
165
|
false,
|
|
157
166
|
dateOrEmpty(period.from),
|
|
@@ -333,8 +342,9 @@ const accountWidth = 4;
|
|
|
333
342
|
/**
|
|
334
343
|
* Parses the TAN media list (HITAB versions 4 and 5, PIN/TAN "TAN-Medium-Liste").
|
|
335
344
|
*
|
|
336
|
-
* Each name whose text
|
|
337
|
-
* by its text:
|
|
345
|
+
* Each name whose text would not go back as the bank sent it goes into `namesAsSent` as `Segment.verbatim` reads it,
|
|
346
|
+
* keyed by its text: a name in UTF-8, which its text does not tell where it fits ISO 8859-1, and one with blanks at
|
|
347
|
+
* either end, which text goes out without (Formals B.4.1).
|
|
338
348
|
*/
|
|
339
349
|
export function parseTanMedia(s: Segment, namesAsSent?: Map<string, string>): TanMedium[] {
|
|
340
350
|
// Version 5 adds the security function in front of the card number.
|
|
@@ -352,7 +362,9 @@ export function parseTanMedia(s: Segment, namesAsSent?: Map<string, string>): Ta
|
|
|
352
362
|
if (mediumClass === undefined) continue;
|
|
353
363
|
const name = fields[after + 3];
|
|
354
364
|
const nameAsSent = s.verbatim(i, after + 3);
|
|
355
|
-
if (name !== undefined && nameAsSent !== undefined && nameAsSent !== name)
|
|
365
|
+
if (name !== undefined && nameAsSent !== undefined && nameAsSent !== trimBlanks(name)) {
|
|
366
|
+
namesAsSent?.set(name, nameAsSent);
|
|
367
|
+
}
|
|
356
368
|
media.push(
|
|
357
369
|
new TanMedium({
|
|
358
370
|
mediumClass,
|
package/src/v3/syntax.ts
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import { BytesBuilder, latin1Decode, latin1Encode, utf8Encode } from '../util/bytes.js';
|
|
4
4
|
import { FormatError } from '../util/errors.js';
|
|
5
|
-
import { decodeBankText, isLatin1, tryParseInt } from '../util/text.js';
|
|
5
|
+
import { decodeBankText, isLatin1, trimBlanks, tryParseInt } from '../util/text.js';
|
|
6
6
|
|
|
7
7
|
// FinTS 3.0 syntax characters.
|
|
8
8
|
const dataElementSeparator = 0x2b; // '+'
|
|
@@ -59,6 +59,8 @@ export class DataElement {
|
|
|
59
59
|
* Index 0 is the segment header (type, number, version, reference); the data elements follow from index 1.
|
|
60
60
|
*/
|
|
61
61
|
export class Segment {
|
|
62
|
+
private typeValue: string | undefined;
|
|
63
|
+
|
|
62
64
|
constructor(
|
|
63
65
|
readonly elements: readonly (readonly DataElement[])[],
|
|
64
66
|
/** The bytes the segment was read from: `start` to `end` in `source`, terminator included. */
|
|
@@ -68,7 +70,8 @@ export class Segment {
|
|
|
68
70
|
) {}
|
|
69
71
|
|
|
70
72
|
get type(): string {
|
|
71
|
-
|
|
73
|
+
this.typeValue ??= trimBlanks(this.text(0) ?? '');
|
|
74
|
+
return this.typeValue;
|
|
72
75
|
}
|
|
73
76
|
|
|
74
77
|
get version(): number {
|
|
@@ -99,6 +102,12 @@ export class Segment {
|
|
|
99
102
|
return e?.isBinary === true ? e.bytes : undefined;
|
|
100
103
|
}
|
|
101
104
|
|
|
105
|
+
/** The binary data or the text without escape characters at `index`; `undefined` for an empty element. */
|
|
106
|
+
bytes(index: number, sub = 0): Uint8Array | undefined {
|
|
107
|
+
const e = this.element(index, sub);
|
|
108
|
+
return e === undefined || e.isEmpty ? undefined : e.bytes;
|
|
109
|
+
}
|
|
110
|
+
|
|
102
111
|
/**
|
|
103
112
|
* The element at `index` as the bank sent it, for a value the client sends back, such as the dialog ID: untrimmed,
|
|
104
113
|
* and one character per byte (ISO 8859-1, the FinTS 3.0 character set), so that `Verbatim` gives the bank back the
|
|
@@ -111,7 +120,7 @@ export class Segment {
|
|
|
111
120
|
}
|
|
112
121
|
|
|
113
122
|
integer(index: number, sub = 0): number | undefined {
|
|
114
|
-
return tryParseInt(this.text(index, sub)
|
|
123
|
+
return tryParseInt(this.text(index, sub));
|
|
115
124
|
}
|
|
116
125
|
|
|
117
126
|
/** The texts of the group at `index`; `undefined` for empty elements. */
|
|
@@ -151,17 +160,20 @@ export function encodeText(value: string): Uint8Array {
|
|
|
151
160
|
}
|
|
152
161
|
|
|
153
162
|
/**
|
|
154
|
-
* The data elements one
|
|
155
|
-
* parameter data of
|
|
156
|
-
*
|
|
163
|
+
* The data elements and segments one parse may produce together, segment headers and group elements included: what
|
|
164
|
+
* the user parameter data of 6,000 accounts take, at about 155 data elements each, which is about what 9,999 KB, the
|
|
165
|
+
* largest message size HIBPA can state, hold of real data. Without a limit, a message of nothing but separators would
|
|
166
|
+
* take a hundred times its size in memory.
|
|
167
|
+
*
|
|
168
|
+
* @internal Exported for the tests.
|
|
157
169
|
*/
|
|
158
|
-
export const
|
|
170
|
+
export const maxParts = 1_000_000;
|
|
159
171
|
|
|
160
172
|
/**
|
|
161
|
-
*
|
|
162
|
-
*
|
|
173
|
+
* What `IncomingMessage3.size` counts for each data element it keeps: more than any of them takes once parsed, so that
|
|
174
|
+
* a message of many short elements counts as the memory it holds rather than its bytes.
|
|
163
175
|
*/
|
|
164
|
-
export const
|
|
176
|
+
export const parsedElementBytes = 512;
|
|
165
177
|
|
|
166
178
|
/** Every empty data element of a received segment; it has no bytes to change. */
|
|
167
179
|
const emptyElement = new DataElement(new Uint8Array(0), 0, 0, false);
|
|
@@ -184,10 +196,14 @@ function isDigit(byte: number | undefined): boolean {
|
|
|
184
196
|
*
|
|
185
197
|
* The elements share the memory of `data`. Throws `FormatError` for anything a complete message cannot hold: a
|
|
186
198
|
* malformed binary length, binary data beyond the end of `data` or not followed by a separator, a last segment without
|
|
187
|
-
* terminator, and more
|
|
199
|
+
* terminator, and more than `maxParts` data elements and segments, an empty one included.
|
|
188
200
|
*/
|
|
189
|
-
export function parseSegments(data: Uint8Array
|
|
201
|
+
export function parseSegments(data: Uint8Array): Segment[] {
|
|
190
202
|
const segments: Segment[] = [];
|
|
203
|
+
let parts = 0;
|
|
204
|
+
const count = (): void => {
|
|
205
|
+
if (++parts > maxParts) throw new FormatError(`More than ${maxParts} data elements and segments`);
|
|
206
|
+
};
|
|
191
207
|
// Created with their first element: an array that starts empty reserves room for more than most of them hold.
|
|
192
208
|
let elements: DataElement[][] | undefined;
|
|
193
209
|
let group: DataElement[] | undefined;
|
|
@@ -228,7 +244,7 @@ export function parseSegments(data: Uint8Array, budget = { elements: maxDataElem
|
|
|
228
244
|
i++;
|
|
229
245
|
continue;
|
|
230
246
|
}
|
|
231
|
-
|
|
247
|
+
count();
|
|
232
248
|
if (elements === undefined && group === undefined) segmentStart = i;
|
|
233
249
|
const element = data[i] === binaryMarker ? readBinary() : readText();
|
|
234
250
|
if (group === undefined) group = [element];
|
|
@@ -239,6 +255,7 @@ export function parseSegments(data: Uint8Array, budget = { elements: maxDataElem
|
|
|
239
255
|
else elements.push(group);
|
|
240
256
|
group = undefined;
|
|
241
257
|
if (separator === segmentTerminator) {
|
|
258
|
+
count();
|
|
242
259
|
const segment = new Segment(elements, data, segmentStart, i);
|
|
243
260
|
// Skip empty segments, such as a stray double terminator.
|
|
244
261
|
if (segment.type !== '') segments.push(segment);
|
|
@@ -363,7 +380,7 @@ function encodeValue(value: SingleValue): Uint8Array {
|
|
|
363
380
|
} else {
|
|
364
381
|
// All but binary data go without leading and trailing blanks (Formals B.4.1). Encode first, then escape the bytes,
|
|
365
382
|
// so that nothing unescaped slips into the syntax.
|
|
366
|
-
writeEscaped(out, encodeText(String(value)
|
|
383
|
+
writeEscaped(out, encodeText(trimBlanks(String(value))));
|
|
367
384
|
}
|
|
368
385
|
return out.takeBytes();
|
|
369
386
|
}
|