@coderoftime/fints 0.0.0-stage → 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 +152 -0
- package/LICENSE +28 -0
- package/README.md +246 -2
- 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 +168 -0
- package/dist/core/account.d.ts.map +1 -0
- package/dist/core/account.js +245 -0
- package/dist/core/account.js.map +1 -0
- package/dist/core/amount.d.ts +63 -0
- package/dist/core/amount.d.ts.map +1 -0
- package/dist/core/amount.js +192 -0
- package/dist/core/amount.js.map +1 -0
- package/dist/core/balance.d.ts +55 -0
- package/dist/core/balance.d.ts.map +1 -0
- package/dist/core/balance.js +54 -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 +161 -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 +80 -0
- package/dist/core/config.d.ts.map +1 -0
- package/dist/core/config.js +153 -0
- package/dist/core/config.js.map +1 -0
- package/dist/core/errors.d.ts +153 -0
- package/dist/core/errors.d.ts.map +1 -0
- package/dist/core/errors.js +247 -0
- package/dist/core/errors.js.map +1 -0
- package/dist/core/feedback.d.ts +72 -0
- package/dist/core/feedback.d.ts.map +1 -0
- package/dist/core/feedback.js +127 -0
- package/dist/core/feedback.js.map +1 -0
- package/dist/core/transaction.d.ts +103 -0
- package/dist/core/transaction.d.ts.map +1 -0
- package/dist/core/transaction.js +96 -0
- package/dist/core/transaction.js.map +1 -0
- package/dist/core/transport.d.ts +64 -0
- package/dist/core/transport.d.ts.map +1 -0
- package/dist/core/transport.js +149 -0
- package/dist/core/transport.js.map +1 -0
- package/dist/formats/camt.d.ts +32 -0
- package/dist/formats/camt.d.ts.map +1 -0
- package/dist/formats/camt.js +242 -0
- package/dist/formats/camt.js.map +1 -0
- package/dist/formats/mt940.d.ts +54 -0
- package/dist/formats/mt940.d.ts.map +1 -0
- package/dist/formats/mt940.js +376 -0
- package/dist/formats/mt940.js.map +1 -0
- package/dist/formats/readers.d.ts +15 -0
- package/dist/formats/readers.d.ts.map +1 -0
- package/dist/formats/readers.js +42 -0
- package/dist/formats/readers.js.map +1 -0
- 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 +28 -0
- package/dist/formats/sepa-purpose.d.ts.map +1 -0
- package/dist/formats/sepa-purpose.js +89 -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 +66 -0
- package/dist/session/app-approval.d.ts.map +1 -0
- package/dist/session/app-approval.js +226 -0
- package/dist/session/app-approval.js.map +1 -0
- package/dist/session/dialog-runner.d.ts +31 -0
- package/dist/session/dialog-runner.d.ts.map +1 -0
- package/dist/session/dialog-runner.js +111 -0
- package/dist/session/dialog-runner.js.map +1 -0
- package/dist/session/login-guard.d.ts +78 -0
- package/dist/session/login-guard.d.ts.map +1 -0
- package/dist/session/login-guard.js +181 -0
- package/dist/session/login-guard.js.map +1 -0
- package/dist/session/sepa-accounts.d.ts +40 -0
- package/dist/session/sepa-accounts.d.ts.map +1 -0
- package/dist/session/sepa-accounts.js +112 -0
- package/dist/session/sepa-accounts.js.map +1 -0
- package/dist/session/session-setup.d.ts +80 -0
- package/dist/session/session-setup.d.ts.map +1 -0
- package/dist/session/session-setup.js +182 -0
- package/dist/session/session-setup.js.map +1 -0
- package/dist/session/session-state.d.ts +50 -0
- package/dist/session/session-state.d.ts.map +1 -0
- package/dist/session/session-state.js +182 -0
- package/dist/session/session-state.js.map +1 -0
- 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 +96 -0
- package/dist/session/tan-selection.d.ts.map +1 -0
- package/dist/session/tan-selection.js +189 -0
- package/dist/session/tan-selection.js.map +1 -0
- package/dist/session/user-prompts.d.ts +65 -0
- package/dist/session/user-prompts.d.ts.map +1 -0
- package/dist/session/user-prompts.js +184 -0
- package/dist/session/user-prompts.js.map +1 -0
- package/dist/tan/challenge.d.ts +135 -0
- package/dist/tan/challenge.d.ts.map +1 -0
- package/dist/tan/challenge.js +184 -0
- package/dist/tan/challenge.js.map +1 -0
- package/dist/tan/flicker.d.ts +42 -0
- package/dist/tan/flicker.d.ts.map +1 -0
- package/dist/tan/flicker.js +296 -0
- package/dist/tan/flicker.js.map +1 -0
- package/dist/tan/tan-image.d.ts +22 -0
- package/dist/tan/tan-image.d.ts.map +1 -0
- package/dist/tan/tan-image.js +43 -0
- package/dist/tan/tan-image.js.map +1 -0
- package/dist/tan/tan-models.d.ts +149 -0
- package/dist/tan/tan-models.d.ts.map +1 -0
- package/dist/tan/tan-models.js +153 -0
- package/dist/tan/tan-models.js.map +1 -0
- package/dist/util/async.d.ts +22 -0
- package/dist/util/async.d.ts.map +1 -0
- package/dist/util/async.js +45 -0
- package/dist/util/async.js.map +1 -0
- package/dist/util/bytes.d.ts +22 -0
- package/dist/util/bytes.d.ts.map +1 -0
- package/dist/util/bytes.js +128 -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 +30 -0
- package/dist/util/call-context.js.map +1 -0
- package/dist/util/dates.d.ts +55 -0
- package/dist/util/dates.d.ts.map +1 -0
- package/dist/util/dates.js +189 -0
- package/dist/util/dates.js.map +1 -0
- package/dist/util/errors.d.ts +17 -0
- package/dist/util/errors.d.ts.map +1 -0
- package/dist/util/errors.js +24 -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 +17 -0
- package/dist/util/json.d.ts.map +1 -0
- package/dist/util/json.js +62 -0
- package/dist/util/json.js.map +1 -0
- package/dist/util/text.d.ts +37 -0
- package/dist/util/text.d.ts.map +1 -0
- package/dist/util/text.js +107 -0
- package/dist/util/text.js.map +1 -0
- package/dist/util/xml.d.ts +36 -0
- package/dist/util/xml.d.ts.map +1 -0
- package/dist/util/xml.js +194 -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 +173 -0
- package/dist/v3/account-extension.js.map +1 -0
- 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 +112 -0
- package/dist/v3/dialog.d.ts.map +1 -0
- package/dist/v3/dialog.js +241 -0
- package/dist/v3/dialog.js.map +1 -0
- package/dist/v3/message.d.ts +85 -0
- package/dist/v3/message.d.ts.map +1 -0
- package/dist/v3/message.js +303 -0
- package/dist/v3/message.js.map +1 -0
- 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 +105 -0
- package/dist/v3/parameters.d.ts.map +1 -0
- package/dist/v3/parameters.js +450 -0
- package/dist/v3/parameters.js.map +1 -0
- package/dist/v3/protocol.d.ts +61 -0
- package/dist/v3/protocol.d.ts.map +1 -0
- package/dist/v3/protocol.js +113 -0
- package/dist/v3/protocol.js.map +1 -0
- 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 +106 -0
- package/dist/v3/segments.d.ts.map +1 -0
- package/dist/v3/segments.js +325 -0
- package/dist/v3/segments.js.map +1 -0
- package/dist/v3/syntax.d.ts +127 -0
- package/dist/v3/syntax.d.ts.map +1 -0
- package/dist/v3/syntax.js +367 -0
- package/dist/v3/syntax.js.map +1 -0
- package/package.json +67 -4
- package/src/client.ts +252 -0
- package/src/core/account.ts +323 -0
- package/src/core/amount.ts +211 -0
- package/src/core/balance.ts +85 -0
- package/src/core/bank-info.ts +66 -0
- package/src/core/client.ts +186 -0
- package/src/core/config.ts +182 -0
- package/src/core/errors.ts +277 -0
- package/src/core/feedback.ts +140 -0
- package/src/core/transaction.ts +161 -0
- package/src/core/transport.ts +189 -0
- package/src/formats/camt.ts +266 -0
- package/src/formats/mt940.ts +419 -0
- package/src/formats/readers.ts +49 -0
- package/src/formats/record-budget.ts +18 -0
- package/src/formats/sepa-purpose.ts +94 -0
- package/src/index.ts +70 -0
- package/src/session/app-approval.ts +278 -0
- package/src/session/dialog-runner.ts +122 -0
- package/src/session/login-guard.ts +221 -0
- package/src/session/sepa-accounts.ts +115 -0
- package/src/session/session-setup.ts +184 -0
- package/src/session/session-state.ts +204 -0
- package/src/session/signed-dialog.ts +192 -0
- package/src/session/tan-selection.ts +203 -0
- package/src/session/user-prompts.ts +193 -0
- package/src/tan/challenge.ts +261 -0
- package/src/tan/flicker.ts +308 -0
- package/src/tan/tan-image.ts +38 -0
- package/src/tan/tan-models.ts +252 -0
- package/src/util/async.ts +48 -0
- package/src/util/bytes.ts +129 -0
- package/src/util/call-context.ts +41 -0
- package/src/util/dates.ts +210 -0
- package/src/util/errors.ts +27 -0
- package/src/util/immutable-map.ts +47 -0
- package/src/util/json.ts +67 -0
- package/src/util/text.ts +112 -0
- package/src/util/xml.ts +183 -0
- package/src/v3/account-extension.ts +185 -0
- package/src/v3/bank-connection.ts +67 -0
- package/src/v3/dialog.ts +283 -0
- package/src/v3/message.ts +373 -0
- package/src/v3/parameter-store.ts +160 -0
- package/src/v3/parameters.ts +524 -0
- package/src/v3/protocol.ts +148 -0
- package/src/v3/queries.ts +190 -0
- package/src/v3/segments.ts +374 -0
- package/src/v3/syntax.ts +369 -0
package/src/v3/syntax.ts
ADDED
|
@@ -0,0 +1,369 @@
|
|
|
1
|
+
// Internal to the package, and named after the FinTS specification. Documentation is where the meaning is not evident.
|
|
2
|
+
|
|
3
|
+
import { BytesBuilder, latin1Decode, latin1Encode, utf8Encode } from '../util/bytes.js';
|
|
4
|
+
import { FormatError } from '../util/errors.js';
|
|
5
|
+
import { decodeBankText, isLatin1, tryParseInt } from '../util/text.js';
|
|
6
|
+
|
|
7
|
+
// FinTS 3.0 syntax characters.
|
|
8
|
+
const dataElementSeparator = 0x2b; // '+'
|
|
9
|
+
const groupElementSeparator = 0x3a; // ':'
|
|
10
|
+
const segmentTerminator = 0x27; // "'"
|
|
11
|
+
const escapeCharacter = 0x3f; // '?'
|
|
12
|
+
const binaryMarker = 0x40; // '@'
|
|
13
|
+
|
|
14
|
+
function isSyntaxCharacter(byte: number): boolean {
|
|
15
|
+
return (
|
|
16
|
+
byte === dataElementSeparator ||
|
|
17
|
+
byte === groupElementSeparator ||
|
|
18
|
+
byte === segmentTerminator ||
|
|
19
|
+
byte === escapeCharacter ||
|
|
20
|
+
byte === binaryMarker
|
|
21
|
+
);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* One (group) data element of a received segment: text or binary data, from `start` to `end` in `source`. A view or
|
|
26
|
+
* copy of its own would take more memory than most elements hold.
|
|
27
|
+
*/
|
|
28
|
+
export class DataElement {
|
|
29
|
+
private decoded: string | undefined;
|
|
30
|
+
|
|
31
|
+
constructor(
|
|
32
|
+
private readonly source: Uint8Array,
|
|
33
|
+
private readonly start: number,
|
|
34
|
+
private readonly end: number,
|
|
35
|
+
readonly isBinary: boolean,
|
|
36
|
+
/** Whether the text holds escape characters, which `bytes` leaves out. */
|
|
37
|
+
private readonly escaped = false,
|
|
38
|
+
) {}
|
|
39
|
+
|
|
40
|
+
/** The binary data, or the text without escape characters. */
|
|
41
|
+
get bytes(): Uint8Array {
|
|
42
|
+
const bytes = this.source.subarray(this.start, this.end);
|
|
43
|
+
return this.escaped ? unescape(bytes) : bytes;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
get isEmpty(): boolean {
|
|
47
|
+
return !this.isBinary && this.start === this.end;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
get text(): string {
|
|
51
|
+
this.decoded ??= decodeBankText(this.bytes);
|
|
52
|
+
return this.decoded;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* A received segment: data elements separated by `+`, each a group of elements separated by `:`.
|
|
58
|
+
*
|
|
59
|
+
* Index 0 is the segment header (type, number, version, reference); the data elements follow from index 1.
|
|
60
|
+
*/
|
|
61
|
+
export class Segment {
|
|
62
|
+
constructor(
|
|
63
|
+
readonly elements: readonly (readonly DataElement[])[],
|
|
64
|
+
/** The bytes the segment was read from: `start` to `end` in `source`, terminator included. */
|
|
65
|
+
private readonly source: Uint8Array,
|
|
66
|
+
private readonly start: number,
|
|
67
|
+
private readonly end: number,
|
|
68
|
+
) {}
|
|
69
|
+
|
|
70
|
+
get type(): string {
|
|
71
|
+
return this.text(0)?.trim() ?? '';
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
get version(): number {
|
|
75
|
+
return this.integer(0, 2) ?? 0;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
get length(): number {
|
|
79
|
+
return this.elements.length;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
group(index: number): readonly DataElement[] {
|
|
83
|
+
return this.elements[index] ?? [];
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
element(index: number, sub = 0): DataElement | undefined {
|
|
87
|
+
return this.group(index)[sub];
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
text(index: number, sub = 0): string | undefined {
|
|
91
|
+
const e = this.element(index, sub);
|
|
92
|
+
if (e === undefined || e.isEmpty) return undefined;
|
|
93
|
+
return e.text;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** The binary data at `index`; `undefined` for an element that holds text. */
|
|
97
|
+
binary(index: number, sub = 0): Uint8Array | undefined {
|
|
98
|
+
const e = this.element(index, sub);
|
|
99
|
+
return e?.isBinary === true ? e.bytes : undefined;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* The element at `index` as the bank sent it, for a value the client sends back, such as the dialog ID: untrimmed,
|
|
104
|
+
* and one character per byte (ISO 8859-1, the FinTS 3.0 character set), so that `Verbatim` gives the bank back the
|
|
105
|
+
* very bytes it sent, in whatever encoding. `undefined` for an empty element.
|
|
106
|
+
*/
|
|
107
|
+
verbatim(index: number, sub = 0): string | undefined {
|
|
108
|
+
const e = this.element(index, sub);
|
|
109
|
+
if (e === undefined || e.isEmpty) return undefined;
|
|
110
|
+
return latin1Decode(e.bytes);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
integer(index: number, sub = 0): number | undefined {
|
|
114
|
+
return tryParseInt(this.text(index, sub)?.trim());
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/** The texts of the group at `index`; `undefined` for empty elements. */
|
|
118
|
+
texts(index: number): (string | undefined)[] {
|
|
119
|
+
return this.group(index).map((e) => (e.isEmpty ? undefined : e.text));
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/** The bytes the segment was read from, as the bank sent them, terminator included. */
|
|
123
|
+
encode(): Uint8Array {
|
|
124
|
+
return this.source.subarray(this.start, this.end);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function writeEscaped(out: BytesBuilder, bytes: Uint8Array): void {
|
|
129
|
+
const escaped: number[] = [];
|
|
130
|
+
for (const b of bytes) {
|
|
131
|
+
if (isSyntaxCharacter(b)) escaped.push(escapeCharacter);
|
|
132
|
+
escaped.push(b);
|
|
133
|
+
}
|
|
134
|
+
out.add(escaped);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function writeBinary(out: BytesBuilder, bytes: Uint8Array): void {
|
|
138
|
+
out.add(latin1Encode(`@${bytes.length}@`));
|
|
139
|
+
out.add(bytes);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Encodes `value` in ISO 8859-1, the FinTS 3.0 character set, or in UTF-8 where it does not fit: such text can only
|
|
144
|
+
* come from a bank that sends UTF-8, such as a TAN medium named `Max’s iPhone`. `FinTsConfig` checks the login data for
|
|
145
|
+
* ISO 8859-1 up front, and the client the PIN and the TAN.
|
|
146
|
+
*
|
|
147
|
+
* @internal Exported for the tests.
|
|
148
|
+
*/
|
|
149
|
+
export function encodeText(value: string): Uint8Array {
|
|
150
|
+
return isLatin1(value) ? latin1Encode(value) : utf8Encode(value);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* The data elements one message may hold, segment headers and group elements included: several times what the
|
|
155
|
+
* parameter data of a user with hundreds of accounts take, and few enough that a hostile message cannot exhaust the
|
|
156
|
+
* memory, at about `dataElementMemory` bytes each.
|
|
157
|
+
*/
|
|
158
|
+
export const maxDataElements = 200_000;
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* The memory a data element of a received message takes, in bytes, with its share of the segment and of a return code:
|
|
162
|
+
* about this much in V8, however few bytes of the message it is made of.
|
|
163
|
+
*/
|
|
164
|
+
export const dataElementMemory = 256;
|
|
165
|
+
|
|
166
|
+
/** Every empty data element of a received segment; it has no bytes to change. */
|
|
167
|
+
const emptyElement = new DataElement(new Uint8Array(0), 0, 0, false);
|
|
168
|
+
|
|
169
|
+
function isSeparator(byte: number | undefined): boolean {
|
|
170
|
+
return byte === dataElementSeparator || byte === groupElementSeparator || byte === segmentTerminator;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/** Line breaks and spaces between segments are padding, not content. */
|
|
174
|
+
function isPadding(byte: number): boolean {
|
|
175
|
+
return byte === 0x0a || byte === 0x0d || byte === 0x20;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function isDigit(byte: number | undefined): boolean {
|
|
179
|
+
return byte !== undefined && byte >= 0x30 && byte <= 0x39;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Splits a FinTS 3.0 message into segments, resolving escapes and binary data (`@length@bytes`).
|
|
184
|
+
*
|
|
185
|
+
* The elements share the memory of `data`. Throws `FormatError` for anything a complete message cannot hold: a
|
|
186
|
+
* malformed binary length, binary data beyond the end of `data` or not followed by a separator, a last segment without
|
|
187
|
+
* terminator, and more elements than `budget` has left, which parses of the same message share.
|
|
188
|
+
*/
|
|
189
|
+
export function parseSegments(data: Uint8Array, budget = { elements: maxDataElements }): Segment[] {
|
|
190
|
+
const segments: Segment[] = [];
|
|
191
|
+
// Created with their first element: an array that starts empty reserves room for more than most of them hold.
|
|
192
|
+
let elements: DataElement[][] | undefined;
|
|
193
|
+
let group: DataElement[] | undefined;
|
|
194
|
+
let segmentStart = 0;
|
|
195
|
+
let i = 0;
|
|
196
|
+
|
|
197
|
+
const readBinary = (): DataElement => {
|
|
198
|
+
let end = i + 1;
|
|
199
|
+
let length = 0;
|
|
200
|
+
while (isDigit(data[end])) {
|
|
201
|
+
length = length * 10 + data[end++]! - 0x30;
|
|
202
|
+
if (length > data.length) throw new FormatError('Binary data beyond the end of the message');
|
|
203
|
+
}
|
|
204
|
+
if (end === i + 1 || data[end] !== binaryMarker) throw new FormatError('Malformed binary length');
|
|
205
|
+
const start = end + 1;
|
|
206
|
+
if (length > data.length - start) throw new FormatError('Binary data beyond the end of the message');
|
|
207
|
+
i = start + length;
|
|
208
|
+
if (i < data.length && !isSeparator(data[i])) throw new FormatError('Binary data not followed by a separator');
|
|
209
|
+
return new DataElement(data, start, i, true);
|
|
210
|
+
};
|
|
211
|
+
|
|
212
|
+
const readText = (): DataElement => {
|
|
213
|
+
const start = i;
|
|
214
|
+
let escaped = false;
|
|
215
|
+
while (i < data.length && !isSeparator(data[i])) {
|
|
216
|
+
if (data[i] === escapeCharacter) {
|
|
217
|
+
escaped = true;
|
|
218
|
+
i++;
|
|
219
|
+
}
|
|
220
|
+
i++;
|
|
221
|
+
}
|
|
222
|
+
if (i > data.length) throw new FormatError('Escape character at the end of the message');
|
|
223
|
+
return i === start ? emptyElement : new DataElement(data, start, i, false, escaped);
|
|
224
|
+
};
|
|
225
|
+
|
|
226
|
+
while (i < data.length) {
|
|
227
|
+
if (elements === undefined && group === undefined && isPadding(data[i]!)) {
|
|
228
|
+
i++;
|
|
229
|
+
continue;
|
|
230
|
+
}
|
|
231
|
+
if (--budget.elements < 0) throw new FormatError('Too many data elements');
|
|
232
|
+
if (elements === undefined && group === undefined) segmentStart = i;
|
|
233
|
+
const element = data[i] === binaryMarker ? readBinary() : readText();
|
|
234
|
+
if (group === undefined) group = [element];
|
|
235
|
+
else group.push(element);
|
|
236
|
+
const separator = data[i++];
|
|
237
|
+
if (separator === groupElementSeparator) continue;
|
|
238
|
+
if (elements === undefined) elements = [group];
|
|
239
|
+
else elements.push(group);
|
|
240
|
+
group = undefined;
|
|
241
|
+
if (separator === segmentTerminator) {
|
|
242
|
+
const segment = new Segment(elements, data, segmentStart, i);
|
|
243
|
+
// Skip empty segments, such as a stray double terminator.
|
|
244
|
+
if (segment.type !== '') segments.push(segment);
|
|
245
|
+
elements = undefined;
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
if (elements !== undefined || group !== undefined) throw new FormatError('Segment without terminator');
|
|
249
|
+
return segments;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/** `text` without the escape characters in front of syntax characters. */
|
|
253
|
+
function unescape(text: Uint8Array): Uint8Array {
|
|
254
|
+
const out = new Uint8Array(text.length);
|
|
255
|
+
let o = 0;
|
|
256
|
+
for (let j = 0; j < text.length; j++) {
|
|
257
|
+
if (text[j] === escapeCharacter) j++;
|
|
258
|
+
out[o++] = text[j]!;
|
|
259
|
+
}
|
|
260
|
+
return out.subarray(0, o);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/** Binary data in a segment to send, written as `@length@bytes`. */
|
|
264
|
+
export class Binary {
|
|
265
|
+
constructor(readonly bytes: Uint8Array) {}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* A value the bank issued, such as the dialog ID, to send back as `Segment.verbatim` reads it: escaped, but not
|
|
270
|
+
* trimmed, so that the bank gets back the very bytes it sent. Text from elsewhere, such as a TAN medium name the app
|
|
271
|
+
* passes, goes out as `encodeText` encodes it.
|
|
272
|
+
*/
|
|
273
|
+
export class Verbatim {
|
|
274
|
+
constructor(readonly text: string) {}
|
|
275
|
+
|
|
276
|
+
/** `text` as a `Verbatim`; `undefined` stays empty. */
|
|
277
|
+
static of(text: string | undefined): Verbatim | undefined {
|
|
278
|
+
return text === undefined ? undefined : new Verbatim(text);
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
/** A value of a segment to send; see `SegmentSpec`. */
|
|
283
|
+
export type SegmentValue = string | number | boolean | Binary | Verbatim | null | undefined | readonly SegmentValue[];
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
* A segment to send.
|
|
287
|
+
*
|
|
288
|
+
* `elements` holds the data elements after the header: `null` or `undefined` for an empty element, an array for a data
|
|
289
|
+
* element group, a boolean for `J`/`N`, `Binary` for binary data, `Verbatim` for a value the bank issued, and anything
|
|
290
|
+
* else as its text, without leading and trailing blanks. Trailing empty elements are left out, as the specification
|
|
291
|
+
* asks.
|
|
292
|
+
*/
|
|
293
|
+
export class SegmentSpec {
|
|
294
|
+
constructor(
|
|
295
|
+
readonly type: string,
|
|
296
|
+
readonly version: number,
|
|
297
|
+
readonly elements: readonly SegmentValue[],
|
|
298
|
+
/** The number of the segment an answer refers to (Bezugssegment); requests have none. */
|
|
299
|
+
readonly reference?: number,
|
|
300
|
+
) {}
|
|
301
|
+
|
|
302
|
+
/**
|
|
303
|
+
* A segment given by the field numbers of its data elements, as in the specification (1 is the first data element
|
|
304
|
+
* after the header); missing numbers stay empty.
|
|
305
|
+
*/
|
|
306
|
+
static numbered(type: string, version: number, fields: Readonly<Record<number, SegmentValue>>): SegmentSpec {
|
|
307
|
+
const count = Math.max(0, ...Object.keys(fields).map(Number));
|
|
308
|
+
const elements: SegmentValue[] = [];
|
|
309
|
+
for (let number = 1; number <= count; number++) elements.push(fields[number]);
|
|
310
|
+
return new SegmentSpec(type, version, elements);
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
/** Encodes the segment with `number`. */
|
|
314
|
+
build(number: number): Uint8Array {
|
|
315
|
+
const out = new BytesBuilder();
|
|
316
|
+
out.add(
|
|
317
|
+
encodeText(`${this.type}:${number}:${this.version}${this.reference === undefined ? '' : `:${this.reference}`}`),
|
|
318
|
+
);
|
|
319
|
+
const encoded = this.elements.map(encodeGroup);
|
|
320
|
+
while (encoded.length > 0 && encoded.at(-1)!.length === 0) encoded.pop();
|
|
321
|
+
for (const e of encoded) {
|
|
322
|
+
out.addByte(dataElementSeparator);
|
|
323
|
+
out.add(e);
|
|
324
|
+
}
|
|
325
|
+
out.addByte(segmentTerminator);
|
|
326
|
+
return out.takeBytes();
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
/** A single value of a segment to send, not a group. */
|
|
331
|
+
type SingleValue = Exclude<SegmentValue, readonly SegmentValue[]>;
|
|
332
|
+
|
|
333
|
+
function isGroup(value: SegmentValue): value is readonly SegmentValue[] {
|
|
334
|
+
return Array.isArray(value);
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
function encodeGroup(value: SegmentValue): Uint8Array {
|
|
338
|
+
const parts: Uint8Array[] = [];
|
|
339
|
+
const flatten = (v: SegmentValue): void => {
|
|
340
|
+
if (isGroup(v)) v.forEach(flatten);
|
|
341
|
+
else parts.push(encodeValue(v));
|
|
342
|
+
};
|
|
343
|
+
flatten(value);
|
|
344
|
+
while (parts.length > 0 && parts.at(-1)!.length === 0) parts.pop();
|
|
345
|
+
const out = new BytesBuilder();
|
|
346
|
+
parts.forEach((part, i) => {
|
|
347
|
+
if (i > 0) out.addByte(groupElementSeparator);
|
|
348
|
+
out.add(part);
|
|
349
|
+
});
|
|
350
|
+
return out.takeBytes();
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
function encodeValue(value: SingleValue): Uint8Array {
|
|
354
|
+
const out = new BytesBuilder();
|
|
355
|
+
if (value === null || value === undefined) {
|
|
356
|
+
// Empty.
|
|
357
|
+
} else if (value instanceof Binary) {
|
|
358
|
+
writeBinary(out, value.bytes);
|
|
359
|
+
} else if (value instanceof Verbatim) {
|
|
360
|
+
writeEscaped(out, encodeText(value.text));
|
|
361
|
+
} else if (typeof value === 'boolean') {
|
|
362
|
+
out.addByte(value ? 0x4a : 0x4e); // 'J' or 'N'
|
|
363
|
+
} else {
|
|
364
|
+
// All but binary data go without leading and trailing blanks (Formals B.4.1). Encode first, then escape the bytes,
|
|
365
|
+
// so that nothing unescaped slips into the syntax.
|
|
366
|
+
writeEscaped(out, encodeText(String(value).trim()));
|
|
367
|
+
}
|
|
368
|
+
return out.takeBytes();
|
|
369
|
+
}
|