@coderoftime/fints 0.0.0-stage → 0.1.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 +11 -0
- package/LICENSE +28 -0
- package/README.md +156 -2
- package/dist/core/account.d.ts +140 -0
- package/dist/core/account.d.ts.map +1 -0
- package/dist/core/account.js +174 -0
- package/dist/core/account.js.map +1 -0
- package/dist/core/amount.d.ts +62 -0
- package/dist/core/amount.d.ts.map +1 -0
- package/dist/core/amount.js +189 -0
- package/dist/core/amount.js.map +1 -0
- package/dist/core/balance.d.ts +54 -0
- package/dist/core/balance.d.ts.map +1 -0
- package/dist/core/balance.js +53 -0
- package/dist/core/balance.js.map +1 -0
- package/dist/core/bank-info.d.ts +49 -0
- package/dist/core/bank-info.d.ts.map +1 -0
- package/dist/core/bank-info.js +52 -0
- package/dist/core/bank-info.js.map +1 -0
- package/dist/core/client.d.ts +126 -0
- package/dist/core/client.d.ts.map +1 -0
- package/dist/core/client.js +2 -0
- package/dist/core/client.js.map +1 -0
- package/dist/core/config.d.ts +68 -0
- package/dist/core/config.d.ts.map +1 -0
- package/dist/core/config.js +113 -0
- package/dist/core/config.js.map +1 -0
- package/dist/core/errors.d.ts +91 -0
- package/dist/core/errors.d.ts.map +1 -0
- package/dist/core/errors.js +182 -0
- package/dist/core/errors.js.map +1 -0
- package/dist/core/feedback.d.ts +69 -0
- package/dist/core/feedback.d.ts.map +1 -0
- package/dist/core/feedback.js +124 -0
- package/dist/core/feedback.js.map +1 -0
- package/dist/core/transaction.d.ts +98 -0
- package/dist/core/transaction.d.ts.map +1 -0
- package/dist/core/transaction.js +91 -0
- package/dist/core/transaction.js.map +1 -0
- package/dist/core/transport.d.ts +37 -0
- package/dist/core/transport.d.ts.map +1 -0
- package/dist/core/transport.js +92 -0
- package/dist/core/transport.js.map +1 -0
- package/dist/formats/camt.d.ts +12 -0
- package/dist/formats/camt.d.ts.map +1 -0
- package/dist/formats/camt.js +159 -0
- package/dist/formats/camt.js.map +1 -0
- package/dist/formats/mt940.d.ts +42 -0
- package/dist/formats/mt940.d.ts.map +1 -0
- package/dist/formats/mt940.js +297 -0
- package/dist/formats/mt940.js.map +1 -0
- package/dist/formats/readers.d.ts +11 -0
- package/dist/formats/readers.d.ts.map +1 -0
- package/dist/formats/readers.js +47 -0
- package/dist/formats/readers.js.map +1 -0
- package/dist/formats/sepa-purpose.d.ts +22 -0
- package/dist/formats/sepa-purpose.d.ts.map +1 -0
- package/dist/formats/sepa-purpose.js +68 -0
- package/dist/formats/sepa-purpose.js.map +1 -0
- package/dist/index.d.ts +28 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +27 -0
- package/dist/index.js.map +1 -0
- package/dist/session/app-approval.d.ts +60 -0
- package/dist/session/app-approval.d.ts.map +1 -0
- package/dist/session/app-approval.js +215 -0
- package/dist/session/app-approval.js.map +1 -0
- package/dist/session/bank-connection.d.ts +28 -0
- package/dist/session/bank-connection.d.ts.map +1 -0
- package/dist/session/bank-connection.js +48 -0
- package/dist/session/bank-connection.js.map +1 -0
- package/dist/session/dialog-runner.d.ts +34 -0
- package/dist/session/dialog-runner.d.ts.map +1 -0
- package/dist/session/dialog-runner.js +112 -0
- package/dist/session/dialog-runner.js.map +1 -0
- package/dist/session/login-guard.d.ts +73 -0
- package/dist/session/login-guard.d.ts.map +1 -0
- package/dist/session/login-guard.js +109 -0
- package/dist/session/login-guard.js.map +1 -0
- package/dist/session/parameter-data.d.ts +83 -0
- package/dist/session/parameter-data.d.ts.map +1 -0
- package/dist/session/parameter-data.js +68 -0
- package/dist/session/parameter-data.js.map +1 -0
- package/dist/session/pin-tan-client.d.ts +54 -0
- package/dist/session/pin-tan-client.d.ts.map +1 -0
- package/dist/session/pin-tan-client.js +153 -0
- package/dist/session/pin-tan-client.js.map +1 -0
- package/dist/session/pin-tan-dialog.d.ts +71 -0
- package/dist/session/pin-tan-dialog.d.ts.map +1 -0
- package/dist/session/pin-tan-dialog.js +124 -0
- package/dist/session/pin-tan-dialog.js.map +1 -0
- package/dist/session/protocol.d.ts +209 -0
- package/dist/session/protocol.d.ts.map +1 -0
- package/dist/session/protocol.js +75 -0
- package/dist/session/protocol.js.map +1 -0
- package/dist/session/response-handler.d.ts +48 -0
- package/dist/session/response-handler.d.ts.map +1 -0
- package/dist/session/response-handler.js +121 -0
- package/dist/session/response-handler.js.map +1 -0
- package/dist/session/sepa-accounts.d.ts +34 -0
- package/dist/session/sepa-accounts.d.ts.map +1 -0
- package/dist/session/sepa-accounts.js +96 -0
- package/dist/session/sepa-accounts.js.map +1 -0
- package/dist/session/session-setup.d.ts +72 -0
- package/dist/session/session-setup.d.ts.map +1 -0
- package/dist/session/session-setup.js +160 -0
- package/dist/session/session-setup.js.map +1 -0
- package/dist/session/session-state.d.ts +47 -0
- package/dist/session/session-state.d.ts.map +1 -0
- package/dist/session/session-state.js +147 -0
- package/dist/session/session-state.js.map +1 -0
- package/dist/session/tan-selection.d.ts +88 -0
- package/dist/session/tan-selection.d.ts.map +1 -0
- package/dist/session/tan-selection.js +178 -0
- package/dist/session/tan-selection.js.map +1 -0
- package/dist/session/user-prompts.d.ts +61 -0
- package/dist/session/user-prompts.d.ts.map +1 -0
- package/dist/session/user-prompts.js +164 -0
- package/dist/session/user-prompts.js.map +1 -0
- package/dist/tan/challenge.d.ts +114 -0
- package/dist/tan/challenge.d.ts.map +1 -0
- package/dist/tan/challenge.js +185 -0
- package/dist/tan/challenge.js.map +1 -0
- package/dist/tan/flicker.d.ts +36 -0
- package/dist/tan/flicker.d.ts.map +1 -0
- package/dist/tan/flicker.js +262 -0
- package/dist/tan/flicker.js.map +1 -0
- package/dist/tan/tan-image.d.ts +21 -0
- package/dist/tan/tan-image.d.ts.map +1 -0
- package/dist/tan/tan-image.js +41 -0
- package/dist/tan/tan-image.js.map +1 -0
- package/dist/tan/tan-models.d.ts +145 -0
- package/dist/tan/tan-models.d.ts.map +1 -0
- package/dist/tan/tan-models.js +148 -0
- package/dist/tan/tan-models.js.map +1 -0
- package/dist/util/async.d.ts +24 -0
- package/dist/util/async.d.ts.map +1 -0
- package/dist/util/async.js +49 -0
- package/dist/util/async.js.map +1 -0
- package/dist/util/bytes.d.ts +23 -0
- package/dist/util/bytes.d.ts.map +1 -0
- package/dist/util/bytes.js +136 -0
- package/dist/util/bytes.js.map +1 -0
- package/dist/util/call-context.d.ts +12 -0
- package/dist/util/call-context.d.ts.map +1 -0
- package/dist/util/call-context.js +29 -0
- package/dist/util/call-context.js.map +1 -0
- package/dist/util/dates.d.ts +62 -0
- package/dist/util/dates.d.ts.map +1 -0
- package/dist/util/dates.js +169 -0
- package/dist/util/dates.js.map +1 -0
- package/dist/util/errors.d.ts +9 -0
- package/dist/util/errors.d.ts.map +1 -0
- package/dist/util/errors.js +15 -0
- package/dist/util/errors.js.map +1 -0
- package/dist/util/immutable-map.d.ts +16 -0
- package/dist/util/immutable-map.d.ts.map +1 -0
- package/dist/util/immutable-map.js +38 -0
- package/dist/util/immutable-map.js.map +1 -0
- package/dist/util/json.d.ts +13 -0
- package/dist/util/json.d.ts.map +1 -0
- package/dist/util/json.js +44 -0
- package/dist/util/json.js.map +1 -0
- package/dist/util/text.d.ts +20 -0
- package/dist/util/text.d.ts.map +1 -0
- package/dist/util/text.js +78 -0
- package/dist/util/text.js.map +1 -0
- package/dist/util/xml.d.ts +35 -0
- package/dist/util/xml.d.ts.map +1 -0
- package/dist/util/xml.js +183 -0
- package/dist/util/xml.js.map +1 -0
- package/dist/v3/account-extension.d.ts +25 -0
- package/dist/v3/account-extension.d.ts.map +1 -0
- package/dist/v3/account-extension.js +176 -0
- package/dist/v3/account-extension.js.map +1 -0
- package/dist/v3/client.d.ts +24 -0
- package/dist/v3/client.d.ts.map +1 -0
- package/dist/v3/client.js +16 -0
- package/dist/v3/client.js.map +1 -0
- package/dist/v3/dialog.d.ts +78 -0
- package/dist/v3/dialog.d.ts.map +1 -0
- package/dist/v3/dialog.js +138 -0
- package/dist/v3/dialog.js.map +1 -0
- package/dist/v3/message.d.ts +72 -0
- package/dist/v3/message.d.ts.map +1 -0
- package/dist/v3/message.js +259 -0
- package/dist/v3/message.js.map +1 -0
- package/dist/v3/parameters.d.ts +79 -0
- package/dist/v3/parameters.d.ts.map +1 -0
- package/dist/v3/parameters.js +396 -0
- package/dist/v3/parameters.js.map +1 -0
- package/dist/v3/protocol.d.ts +68 -0
- package/dist/v3/protocol.d.ts.map +1 -0
- package/dist/v3/protocol.js +263 -0
- package/dist/v3/protocol.js.map +1 -0
- package/dist/v3/segments.d.ts +89 -0
- package/dist/v3/segments.d.ts.map +1 -0
- package/dist/v3/segments.js +283 -0
- package/dist/v3/segments.js.map +1 -0
- package/dist/v3/syntax.d.ts +76 -0
- package/dist/v3/syntax.d.ts.map +1 -0
- package/dist/v3/syntax.js +301 -0
- package/dist/v3/syntax.js.map +1 -0
- package/package.json +63 -4
- package/src/core/account.ts +250 -0
- package/src/core/amount.ts +209 -0
- package/src/core/balance.ts +84 -0
- package/src/core/bank-info.ts +66 -0
- package/src/core/client.ts +149 -0
- package/src/core/config.ts +141 -0
- package/src/core/errors.ts +203 -0
- package/src/core/feedback.ts +137 -0
- package/src/core/transaction.ts +156 -0
- package/src/core/transport.ts +114 -0
- package/src/formats/camt.ts +169 -0
- package/src/formats/mt940.ts +333 -0
- package/src/formats/readers.ts +56 -0
- package/src/formats/sepa-purpose.ts +75 -0
- package/src/index.ts +68 -0
- package/src/session/app-approval.ts +265 -0
- package/src/session/bank-connection.ts +51 -0
- package/src/session/dialog-runner.ts +119 -0
- package/src/session/login-guard.ts +135 -0
- package/src/session/parameter-data.ts +137 -0
- package/src/session/pin-tan-client.ts +186 -0
- package/src/session/pin-tan-dialog.ts +160 -0
- package/src/session/protocol.ts +311 -0
- package/src/session/response-handler.ts +130 -0
- package/src/session/sepa-accounts.ts +97 -0
- package/src/session/session-setup.ts +165 -0
- package/src/session/session-state.ts +170 -0
- package/src/session/tan-selection.ts +192 -0
- package/src/session/user-prompts.ts +174 -0
- package/src/tan/challenge.ts +252 -0
- package/src/tan/flicker.ts +270 -0
- package/src/tan/tan-image.ts +36 -0
- package/src/tan/tan-models.ts +247 -0
- package/src/util/async.ts +53 -0
- package/src/util/bytes.ts +135 -0
- package/src/util/call-context.ts +40 -0
- package/src/util/dates.ts +193 -0
- package/src/util/errors.ts +16 -0
- package/src/util/immutable-map.ts +47 -0
- package/src/util/json.ts +51 -0
- package/src/util/text.ts +82 -0
- package/src/util/xml.ts +172 -0
- package/src/v3/account-extension.ts +190 -0
- package/src/v3/client.ts +29 -0
- package/src/v3/dialog.ts +199 -0
- package/src/v3/message.ts +321 -0
- package/src/v3/parameters.ts +451 -0
- package/src/v3/protocol.ts +331 -0
- package/src/v3/segments.ts +331 -0
- package/src/v3/syntax.ts +307 -0
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
// Internal to the package, and named after the FinTS specification. Documentation is where the meaning is not evident.
|
|
2
|
+
|
|
3
|
+
import { AccountDataStatus, type Account } from '../core/account.js';
|
|
4
|
+
import { Amount, withMark } from '../core/amount.js';
|
|
5
|
+
import { Balance } from '../core/balance.js';
|
|
6
|
+
import { parseFintsDate, parseUtcPlusOneDateTime } from '../util/dates.js';
|
|
7
|
+
import { FormatError } from '../util/errors.js';
|
|
8
|
+
|
|
9
|
+
/** A booked or pending balance of the extension: credit/debit mark, value, date and time. */
|
|
10
|
+
interface Saldo {
|
|
11
|
+
readonly amount: Amount;
|
|
12
|
+
readonly date: Date;
|
|
13
|
+
readonly time: Date | undefined;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** The balance of the extension (`AcctBal`, built like HISAL version 7), read without the account it belongs to. */
|
|
17
|
+
interface AccountBalance {
|
|
18
|
+
readonly booked: Saldo;
|
|
19
|
+
readonly pending: Saldo | undefined;
|
|
20
|
+
readonly creditLine: Amount | undefined;
|
|
21
|
+
readonly available: Amount | undefined;
|
|
22
|
+
readonly used: Amount | undefined;
|
|
23
|
+
readonly overdraft: Amount | undefined;
|
|
24
|
+
readonly bookingDate: Date | undefined;
|
|
25
|
+
readonly bookingTime: Date | undefined;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* The account extension of the user parameter data (HIUPD "Erweiterung, kontobezogen", Formals E.3.1): JSON with the
|
|
30
|
+
* BIC, the balance, and what the bank tells about the data it holds for the account.
|
|
31
|
+
*/
|
|
32
|
+
export class AccountExtension {
|
|
33
|
+
private constructor(
|
|
34
|
+
/** The BIC of the account, which spares the SEPA account list (Formals F.3.4). */
|
|
35
|
+
readonly bic: string | undefined,
|
|
36
|
+
/** What the bank tells about the account's data. */
|
|
37
|
+
readonly status: AccountDataStatus,
|
|
38
|
+
private readonly balance: AccountBalance | undefined,
|
|
39
|
+
/** Whether `balance` holds all a balance query would deliver (`BalComplete`). */
|
|
40
|
+
private readonly balanceComplete: boolean,
|
|
41
|
+
) {}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Reads `json`; `undefined` if it is not a JSON object or any element is malformed, which voids the whole extension
|
|
45
|
+
* (Formals F.2, KF3).
|
|
46
|
+
*/
|
|
47
|
+
static tryParse(json: string): AccountExtension | undefined {
|
|
48
|
+
try {
|
|
49
|
+
const decoded: unknown = JSON.parse(json);
|
|
50
|
+
return isObject(decoded) ? AccountExtension.read(new Json(decoded)) : undefined;
|
|
51
|
+
} catch (error) {
|
|
52
|
+
if (error instanceof FormatError || error instanceof SyntaxError) return undefined;
|
|
53
|
+
throw error;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
private static read(json: Json): AccountExtension {
|
|
58
|
+
const balance = json.object('AcctBal');
|
|
59
|
+
const next = json.object('AcctStatNext');
|
|
60
|
+
return new AccountExtension(
|
|
61
|
+
json.string('BIC'),
|
|
62
|
+
new AccountDataStatus({
|
|
63
|
+
inventory: json.object('Inventory')?.strings ?? new Map(),
|
|
64
|
+
lastTransactions: json.string('umsltzt'),
|
|
65
|
+
nextTransactionsAt: next === undefined ? undefined : moment(next, '000000'),
|
|
66
|
+
transactionBalancesCurrent: json.flag('BalStatAcct') || json.flag('BalStatAct'),
|
|
67
|
+
}),
|
|
68
|
+
balance === undefined ? undefined : readBalance(balance),
|
|
69
|
+
json.flag('BalComplete'),
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** The balance of `account`, where the extension holds all a balance query would deliver (Formals F.3.2). */
|
|
74
|
+
completeBalance(account: Account): Balance | undefined {
|
|
75
|
+
const b = this.balance;
|
|
76
|
+
if (b === undefined || !this.balanceComplete) return undefined;
|
|
77
|
+
return new Balance({
|
|
78
|
+
account,
|
|
79
|
+
booked: b.booked.amount,
|
|
80
|
+
bookedAsOf: b.booked.date,
|
|
81
|
+
bookedAsOfTime: b.booked.time,
|
|
82
|
+
pending: b.pending?.amount,
|
|
83
|
+
creditLine: b.creditLine,
|
|
84
|
+
available: b.available,
|
|
85
|
+
used: b.used,
|
|
86
|
+
overdraft: b.overdraft,
|
|
87
|
+
bookingDate: b.bookingDate,
|
|
88
|
+
bookingTime: b.bookingTime,
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function readBalance(json: Json): AccountBalance {
|
|
94
|
+
const currency = json.string('Cur') ?? fail('Extension balance without currency');
|
|
95
|
+
const amount = (key: string): Amount | undefined => {
|
|
96
|
+
const value = json.string(key);
|
|
97
|
+
return value === undefined ? undefined : Amount.parseDecimal(value, currency);
|
|
98
|
+
};
|
|
99
|
+
const saldo = (key: string): Saldo | undefined => {
|
|
100
|
+
const s = json.object(key);
|
|
101
|
+
if (s === undefined) return undefined;
|
|
102
|
+
const value = s.string('Val') ?? fail('Extension balance without value');
|
|
103
|
+
return {
|
|
104
|
+
amount: withMark(Amount.parseDecimal(value, currency), s.string('DebCrd'), 'C', 'D'),
|
|
105
|
+
date: parseFintsDate(s.string('Date')) ?? fail('Extension balance without date'),
|
|
106
|
+
time: moment(s),
|
|
107
|
+
};
|
|
108
|
+
};
|
|
109
|
+
const bookTime = json.object('BookTime');
|
|
110
|
+
return {
|
|
111
|
+
booked: saldo('CurValBal') ?? fail('Extension balance without booked balance'),
|
|
112
|
+
pending: saldo('InclPendTransBal'),
|
|
113
|
+
creditLine: amount('OverdraftLim'),
|
|
114
|
+
available: amount('AvailFunds'),
|
|
115
|
+
used: amount('AlrdyDrwnOnBal'),
|
|
116
|
+
overdraft: amount('OverDraft'),
|
|
117
|
+
bookingDate: bookTime === undefined ? undefined : parseFintsDate(bookTime.string('Date')),
|
|
118
|
+
bookingTime: bookTime === undefined ? undefined : moment(bookTime),
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* The moment of `Date` and `Time` in `json`, which the extension states in UTC+01:00 all year round; `undefined`
|
|
124
|
+
* without a time, unless `defaultTime` stands in for it.
|
|
125
|
+
*/
|
|
126
|
+
function moment(json: Json, defaultTime?: string): Date | undefined {
|
|
127
|
+
const time = json.string('Time') ?? defaultTime;
|
|
128
|
+
if (time === undefined) return undefined;
|
|
129
|
+
return parseUtcPlusOneDateTime(json.string('Date'), time) ?? fail('Extension date or time is malformed');
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function fail(message: string): never {
|
|
133
|
+
throw new FormatError(message);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function isObject(value: unknown): value is Record<string, unknown> {
|
|
137
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* A JSON object of the extension, read by key without regard to case: the specification itself spells some keys
|
|
142
|
+
* differently (`OverDraftLim` and `OverdraftLim`). Values of the wrong type are a `FormatError`.
|
|
143
|
+
*/
|
|
144
|
+
class Json {
|
|
145
|
+
private readonly values = new Map<string, unknown>();
|
|
146
|
+
|
|
147
|
+
constructor(values: Record<string, unknown>) {
|
|
148
|
+
for (const [key, value] of Object.entries(values)) this.values.set(key.toLowerCase(), value);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
private value(key: string): unknown {
|
|
152
|
+
return this.values.get(key.toLowerCase());
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/** A string, or a number written as string: dates and times come as either. */
|
|
156
|
+
string(key: string): string | undefined {
|
|
157
|
+
const value = this.value(key);
|
|
158
|
+
if (value === undefined || value === null) return undefined;
|
|
159
|
+
if (typeof value === 'string') return value.trim() === '' ? undefined : value.trim();
|
|
160
|
+
if (typeof value === 'number') return String(value);
|
|
161
|
+
throw new FormatError(`Extension element "${key}" is not text`);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
flag(key: string): boolean {
|
|
165
|
+
const value = this.value(key);
|
|
166
|
+
if (value === undefined || value === null) return false;
|
|
167
|
+
if (typeof value === 'boolean') return value;
|
|
168
|
+
if (typeof value === 'string') return value === 'J' || value.toLowerCase() === 'true';
|
|
169
|
+
throw new FormatError(`Extension element "${key}" is not a flag`);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
object(key: string): Json | undefined {
|
|
173
|
+
const value = this.value(key);
|
|
174
|
+
if (value === undefined || value === null) return undefined;
|
|
175
|
+
if (isObject(value)) return new Json(value);
|
|
176
|
+
throw new FormatError(`Extension element "${key}" is not an object`);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/** The object's values as strings, keyed in capitals, as segment IDs are written. */
|
|
180
|
+
get strings(): Map<string, string> {
|
|
181
|
+
const result = new Map<string, string>();
|
|
182
|
+
for (const [key, value] of this.values) {
|
|
183
|
+
if (value === null) result.set(key.toUpperCase(), '');
|
|
184
|
+
else if (typeof value === 'string') result.set(key.toUpperCase(), value);
|
|
185
|
+
else if (typeof value === 'number') result.set(key.toUpperCase(), String(value));
|
|
186
|
+
else throw new FormatError(`Extension element "${key}" is not text`);
|
|
187
|
+
}
|
|
188
|
+
return result;
|
|
189
|
+
}
|
|
190
|
+
}
|
package/src/v3/client.ts
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { FinTsClientOptions } from '../core/client.js';
|
|
2
|
+
import { FinTsConfig, type FinTsConfigInit } from '../core/config.js';
|
|
3
|
+
import { PinTanClient } from '../session/pin-tan-client.js';
|
|
4
|
+
import type { IncomingMessage3 } from './message.js';
|
|
5
|
+
import { FinTs3Protocol } from './protocol.js';
|
|
6
|
+
import type { SegmentSpec } from './syntax.js';
|
|
7
|
+
|
|
8
|
+
/** What `FinTs3Client` takes. */
|
|
9
|
+
export interface FinTs3ClientInit {
|
|
10
|
+
/** Where and as whom to log in: a `FinTsConfig`, or what its constructor takes. */
|
|
11
|
+
config: FinTsConfig | FinTsConfigInit;
|
|
12
|
+
/** The user's PIN; the client keeps it in memory only and never logs it. */
|
|
13
|
+
pin: string;
|
|
14
|
+
/** How the client reaches the user and the bank. */
|
|
15
|
+
options: FinTsClientOptions;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** FinTS 3.0 PIN/TAN client, the version German banks use. */
|
|
19
|
+
export class FinTs3Client extends PinTanClient<IncomingMessage3, SegmentSpec> {
|
|
20
|
+
/**
|
|
21
|
+
* Throws `TypeError` if the configuration is invalid, if the PIN is empty, or if the PIN or login data contain
|
|
22
|
+
* characters FinTS 3.0 cannot send: it uses ISO 8859-1, so `€` and most non-Latin letters are not possible. Throws as
|
|
23
|
+
* well for a product ID longer than 25 or a product version longer than 5 characters.
|
|
24
|
+
*/
|
|
25
|
+
constructor(init: FinTs3ClientInit) {
|
|
26
|
+
const config = init.config instanceof FinTsConfig ? init.config : new FinTsConfig(init.config);
|
|
27
|
+
super(init.options, (logger) => new FinTs3Protocol(config, init.pin, init.options.transport, logger));
|
|
28
|
+
}
|
|
29
|
+
}
|
package/src/v3/dialog.ts
ADDED
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
// Internal to the package, and named after the FinTS specification. Documentation is where the meaning is not evident.
|
|
2
|
+
|
|
3
|
+
import { FinTsProtocolError } from '../core/errors.js';
|
|
4
|
+
import { FeedbackCodes, hasCode } from '../core/feedback.js';
|
|
5
|
+
import { requireBankParameters } from '../session/parameter-data.js';
|
|
6
|
+
import { PinTanDialog } from '../session/pin-tan-dialog.js';
|
|
7
|
+
import type { Approval, StartWithOrder } from '../session/protocol.js';
|
|
8
|
+
import { TanMethod } from '../tan/tan-models.js';
|
|
9
|
+
import { IncomingMessage3 } from './message.js';
|
|
10
|
+
import type { Bpd3 } from './parameters.js';
|
|
11
|
+
import { Segments, TanResponse } from './segments.js';
|
|
12
|
+
import type { SegmentSpec } from './syntax.js';
|
|
13
|
+
|
|
14
|
+
/** Dialog ID of a dialog the bank has not answered yet, and customer system ID before synchronization. */
|
|
15
|
+
const unassigned = '0';
|
|
16
|
+
|
|
17
|
+
/** The first version of HKTAN that supports strong customer authentication. */
|
|
18
|
+
const firstScaVersion = 6;
|
|
19
|
+
|
|
20
|
+
/** The first version of HKTAN that supports app approvals, with the status request (process S). */
|
|
21
|
+
const firstDecoupledVersion = 7;
|
|
22
|
+
|
|
23
|
+
/** How a message is signed: the security function of the TAN method and the customer system ID. */
|
|
24
|
+
export interface Signing3 {
|
|
25
|
+
readonly securityFunction: string;
|
|
26
|
+
readonly systemId: string;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* What a dialog needs from the session: the parameter data received so far, the segments that open a dialog, and the
|
|
31
|
+
* way to the bank. The `FinTs3Protocol` provides it, and keeps the login data and the PIN to itself.
|
|
32
|
+
*/
|
|
33
|
+
export interface FinTs3DialogHost {
|
|
34
|
+
/** The bank parameter data received so far, if any. */
|
|
35
|
+
readonly bankParameters: Bpd3 | undefined;
|
|
36
|
+
|
|
37
|
+
/** The customer system ID the bank assigned; `undefined` before the synchronization. */
|
|
38
|
+
readonly systemId: string | undefined;
|
|
39
|
+
|
|
40
|
+
/** Identification (HKIDN) and processing preparation (HKVVB), which open every dialog. */
|
|
41
|
+
initSegments(systemId: string, anonymous?: boolean): SegmentSpec[];
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Sends `segments` as message `number` of the dialog `dialogId` and returns the bank's answer, not yet parsed.
|
|
45
|
+
*
|
|
46
|
+
* With `signing` the message is signed with the PIN and the `tan`, after `LoginGuard.check`; without, it goes out
|
|
47
|
+
* unsigned, as in an anonymous dialog.
|
|
48
|
+
*/
|
|
49
|
+
post(message: {
|
|
50
|
+
dialogId: string;
|
|
51
|
+
number: number;
|
|
52
|
+
segments: readonly SegmentSpec[];
|
|
53
|
+
signing: Signing3 | undefined;
|
|
54
|
+
tan: string | undefined;
|
|
55
|
+
}): Promise<Uint8Array>;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** One FinTS 3.0 dialog: numbered messages under the dialog ID the bank assigns in its first answer. */
|
|
59
|
+
export class FinTs3Dialog extends PinTanDialog<IncomingMessage3, SegmentSpec> {
|
|
60
|
+
private systemId: string;
|
|
61
|
+
private id = unassigned;
|
|
62
|
+
|
|
63
|
+
private constructor(
|
|
64
|
+
private readonly host: FinTs3DialogHost,
|
|
65
|
+
method: TanMethod,
|
|
66
|
+
tanMedium: string | undefined,
|
|
67
|
+
private readonly anonymous: boolean,
|
|
68
|
+
) {
|
|
69
|
+
super(method, tanMedium);
|
|
70
|
+
this.systemId = anonymous ? unassigned : (host.systemId ?? unassigned);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
static signed(host: FinTs3DialogHost, method: TanMethod, tanMedium?: string): FinTs3Dialog {
|
|
74
|
+
return new FinTs3Dialog(host, method, tanMedium, false);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** A dialog without login, which only delivers the bank parameter data. */
|
|
78
|
+
static anonymous(host: FinTs3DialogHost): FinTs3Dialog {
|
|
79
|
+
return new FinTs3Dialog(host, TanMethod.oneStep, undefined, true);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
protected get bankParameters(): Bpd3 | undefined {
|
|
83
|
+
return this.host.bankParameters;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
get isActive(): boolean {
|
|
87
|
+
return this.id !== unassigned && !this.hasEnded;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** The version of the HKTAN segments: that of the HITANS segment the bank describes the method in. */
|
|
91
|
+
private get tanVersion(): number {
|
|
92
|
+
return this.bankParameters?.tanVersion(this.method) ?? firstScaVersion;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
start(synchronize = false): Promise<IncomingMessage3> {
|
|
96
|
+
if (synchronize) this.systemId = unassigned;
|
|
97
|
+
return this.sendSegments([
|
|
98
|
+
...this.host.initSegments(this.systemId),
|
|
99
|
+
...(this.isTwoStep ? [this.tanRequest('HKIDN')] : []),
|
|
100
|
+
...(synchronize ? [Segments.synchronization()] : []),
|
|
101
|
+
]);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Opens the dialog, asking strong authentication for `order` with a two-step method, then sends `order` in the next
|
|
106
|
+
* message.
|
|
107
|
+
*/
|
|
108
|
+
startWith(order: SegmentSpec): StartWithOrder<IncomingMessage3> {
|
|
109
|
+
return {
|
|
110
|
+
login: () =>
|
|
111
|
+
this.sendSegments([
|
|
112
|
+
...this.host.initSegments(this.systemId),
|
|
113
|
+
...(this.isTwoStep ? [this.tanRequest(order.type)] : []),
|
|
114
|
+
]),
|
|
115
|
+
order: () => this.sendSegments([order]),
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Opens the anonymous dialog; the answer carries the bank parameter data.
|
|
121
|
+
*
|
|
122
|
+
* With `announceSca` the message also carries HKTAN, the way a client that can do strong customer authentication
|
|
123
|
+
* tells the bank so, whereupon the bank may send parameter data suited to it (PIN/TAN, B.4.3.1).
|
|
124
|
+
*/
|
|
125
|
+
requestBankParameters(announceSca = false): Promise<IncomingMessage3> {
|
|
126
|
+
return this.sendSegments([
|
|
127
|
+
...this.host.initSegments(unassigned, true),
|
|
128
|
+
...(announceSca ? [Segments.tanProcess4(firstScaVersion, 'HKIDN')] : []),
|
|
129
|
+
]);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
send(order: SegmentSpec): Promise<IncomingMessage3> {
|
|
133
|
+
const needsTan = this.isTwoStep && requireBankParameters(this.bankParameters).pinTan.requiresTan(order.type);
|
|
134
|
+
return this.sendSegments([order, ...(needsTan ? [this.tanRequest(order.type)] : [])]);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
protected async sendEnd(): Promise<void> {
|
|
138
|
+
await this.sendSegments([Segments.dialogEnd(this.id)]);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
private tanRequest(segmentType: string): SegmentSpec {
|
|
142
|
+
return Segments.tanProcess4(this.tanVersion, segmentType, this.mediumName);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* The approval a HITAN segment of process 4 asks for, answered with HKTAN process 2 (TAN) or S (status).
|
|
147
|
+
*
|
|
148
|
+
* Throws `FinTsProtocolError` for an app approval with a method of HITANS #6: the status request (process S) came with
|
|
149
|
+
* version 7, along with app approvals (PIN/TAN B.5.2).
|
|
150
|
+
*/
|
|
151
|
+
protected approvalIn(response: IncomingMessage3): Approval<IncomingMessage3> | undefined {
|
|
152
|
+
const tan = tanResponseOf(response, '4');
|
|
153
|
+
if (tan === undefined || tan.isDummy) return undefined;
|
|
154
|
+
if (hasCode(response.feedback, FeedbackCodes.decoupledPending) && this.tanVersion < firstDecoupledVersion) {
|
|
155
|
+
throw new FinTsProtocolError('The bank asks for an app approval, which needs HKTAN version 7');
|
|
156
|
+
}
|
|
157
|
+
const reference = tan.reference!;
|
|
158
|
+
return this.approvalOf(response, {
|
|
159
|
+
reference,
|
|
160
|
+
message: tan.challenge,
|
|
161
|
+
hhdUc: tan.hhdUc,
|
|
162
|
+
validUntil: tan.validUntil,
|
|
163
|
+
tanMediumName: tan.mediumName,
|
|
164
|
+
submitTan: (value) => this.sendSegments([Segments.tanProcess2(this.tanVersion, reference)], value),
|
|
165
|
+
checkStatus: () => this.sendSegments([Segments.tanProcessStatus(this.tanVersion, reference)]),
|
|
166
|
+
// A status answer that the approval is still pending may carry new instructions (PIN/TAN B.4.2.2.1, step 2b).
|
|
167
|
+
instructionsIn: (status) => tanResponseOf(status, 'S')?.challenge,
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
private sendSegments(segments: readonly SegmentSpec[], tan?: string): Promise<IncomingMessage3> {
|
|
172
|
+
return this.transmit(
|
|
173
|
+
(number) =>
|
|
174
|
+
this.host.post({
|
|
175
|
+
dialogId: this.id,
|
|
176
|
+
number,
|
|
177
|
+
segments,
|
|
178
|
+
signing: this.anonymous
|
|
179
|
+
? undefined
|
|
180
|
+
: { securityFunction: this.method.securityFunction, systemId: this.systemId },
|
|
181
|
+
tan,
|
|
182
|
+
}),
|
|
183
|
+
(answer) => IncomingMessage3.parse(answer),
|
|
184
|
+
);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/** The bank assigns the dialog ID in its answer to the first message. */
|
|
188
|
+
protected readDialogData(response: IncomingMessage3): void {
|
|
189
|
+
if (this.id === unassigned) this.id = response.dialogId ?? unassigned;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/** The TAN part of `response` that answers TAN `process`. */
|
|
194
|
+
function tanResponseOf(response: IncomingMessage3, process: string): TanResponse | undefined {
|
|
195
|
+
return response
|
|
196
|
+
.findAll('HITAN')
|
|
197
|
+
.map((s) => TanResponse.parse(s))
|
|
198
|
+
.find((t) => t.process === process);
|
|
199
|
+
}
|