@coderoftime/fints 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +142 -1
- package/README.md +127 -37
- package/dist/client.d.ts +48 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +199 -0
- package/dist/client.js.map +1 -0
- package/dist/core/account.d.ts +35 -7
- package/dist/core/account.d.ts.map +1 -1
- package/dist/core/account.js +84 -13
- package/dist/core/account.js.map +1 -1
- package/dist/core/amount.d.ts +2 -1
- package/dist/core/amount.d.ts.map +1 -1
- package/dist/core/amount.js +4 -1
- package/dist/core/amount.js.map +1 -1
- package/dist/core/balance.d.ts +1 -0
- package/dist/core/balance.d.ts.map +1 -1
- package/dist/core/balance.js +6 -5
- package/dist/core/balance.js.map +1 -1
- package/dist/core/bank-info.d.ts +1 -1
- package/dist/core/bank-info.js +1 -1
- package/dist/core/client.d.ts +50 -15
- package/dist/core/client.d.ts.map +1 -1
- package/dist/core/config.d.ts +28 -16
- package/dist/core/config.d.ts.map +1 -1
- package/dist/core/config.js +66 -26
- package/dist/core/config.js.map +1 -1
- package/dist/core/errors.d.ts +85 -23
- package/dist/core/errors.d.ts.map +1 -1
- package/dist/core/errors.js +112 -47
- package/dist/core/errors.js.map +1 -1
- package/dist/core/feedback.d.ts +4 -1
- package/dist/core/feedback.d.ts.map +1 -1
- package/dist/core/feedback.js +4 -1
- package/dist/core/feedback.js.map +1 -1
- package/dist/core/transaction.d.ts +6 -1
- package/dist/core/transaction.d.ts.map +1 -1
- package/dist/core/transaction.js +9 -4
- package/dist/core/transaction.js.map +1 -1
- package/dist/core/transport.d.ts +33 -6
- package/dist/core/transport.d.ts.map +1 -1
- package/dist/core/transport.js +77 -20
- package/dist/core/transport.js.map +1 -1
- package/dist/formats/camt.d.ts +24 -4
- package/dist/formats/camt.d.ts.map +1 -1
- package/dist/formats/camt.js +124 -41
- package/dist/formats/camt.js.map +1 -1
- package/dist/formats/mt940.d.ts +15 -3
- package/dist/formats/mt940.d.ts.map +1 -1
- package/dist/formats/mt940.js +161 -82
- package/dist/formats/mt940.js.map +1 -1
- package/dist/formats/readers.d.ts +6 -2
- package/dist/formats/readers.d.ts.map +1 -1
- package/dist/formats/readers.js +24 -29
- package/dist/formats/readers.js.map +1 -1
- package/dist/formats/record-budget.d.ts +7 -0
- package/dist/formats/record-budget.d.ts.map +1 -0
- package/dist/formats/record-budget.js +17 -0
- package/dist/formats/record-budget.js.map +1 -0
- package/dist/formats/sepa-purpose.d.ts +6 -0
- package/dist/formats/sepa-purpose.d.ts.map +1 -1
- package/dist/formats/sepa-purpose.js +23 -2
- package/dist/formats/sepa-purpose.js.map +1 -1
- package/dist/index.d.ts +3 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -2
- package/dist/index.js.map +1 -1
- package/dist/session/app-approval.d.ts +11 -5
- package/dist/session/app-approval.d.ts.map +1 -1
- package/dist/session/app-approval.js +23 -12
- package/dist/session/app-approval.js.map +1 -1
- package/dist/session/dialog-runner.d.ts +7 -10
- package/dist/session/dialog-runner.d.ts.map +1 -1
- package/dist/session/dialog-runner.js +15 -16
- package/dist/session/dialog-runner.js.map +1 -1
- package/dist/session/login-guard.d.ts +54 -49
- package/dist/session/login-guard.d.ts.map +1 -1
- package/dist/session/login-guard.js +156 -84
- package/dist/session/login-guard.js.map +1 -1
- package/dist/session/sepa-accounts.d.ts +20 -14
- package/dist/session/sepa-accounts.d.ts.map +1 -1
- package/dist/session/sepa-accounts.js +62 -46
- package/dist/session/sepa-accounts.js.map +1 -1
- package/dist/session/session-setup.d.ts +33 -25
- package/dist/session/session-setup.d.ts.map +1 -1
- package/dist/session/session-setup.js +87 -65
- package/dist/session/session-setup.js.map +1 -1
- package/dist/session/session-state.d.ts +9 -6
- package/dist/session/session-state.d.ts.map +1 -1
- package/dist/session/session-state.js +52 -17
- package/dist/session/session-state.js.map +1 -1
- package/dist/session/signed-dialog.d.ts +77 -0
- package/dist/session/signed-dialog.d.ts.map +1 -0
- package/dist/session/signed-dialog.js +170 -0
- package/dist/session/signed-dialog.js.map +1 -0
- package/dist/session/tan-selection.d.ts +26 -18
- package/dist/session/tan-selection.d.ts.map +1 -1
- package/dist/session/tan-selection.js +44 -33
- package/dist/session/tan-selection.js.map +1 -1
- package/dist/session/user-prompts.d.ts +8 -4
- package/dist/session/user-prompts.d.ts.map +1 -1
- package/dist/session/user-prompts.js +34 -14
- package/dist/session/user-prompts.js.map +1 -1
- package/dist/tan/challenge.d.ts +34 -13
- package/dist/tan/challenge.d.ts.map +1 -1
- package/dist/tan/challenge.js +25 -26
- package/dist/tan/challenge.js.map +1 -1
- package/dist/tan/flicker.d.ts +7 -1
- package/dist/tan/flicker.d.ts.map +1 -1
- package/dist/tan/flicker.js +40 -6
- package/dist/tan/flicker.js.map +1 -1
- package/dist/tan/tan-image.d.ts +3 -2
- package/dist/tan/tan-image.d.ts.map +1 -1
- package/dist/tan/tan-image.js +7 -5
- package/dist/tan/tan-image.js.map +1 -1
- package/dist/tan/tan-models.d.ts +5 -1
- package/dist/tan/tan-models.d.ts.map +1 -1
- package/dist/tan/tan-models.js +10 -5
- package/dist/tan/tan-models.js.map +1 -1
- package/dist/util/async.d.ts +0 -2
- package/dist/util/async.d.ts.map +1 -1
- package/dist/util/async.js +0 -4
- package/dist/util/async.js.map +1 -1
- package/dist/util/bytes.d.ts +2 -3
- package/dist/util/bytes.d.ts.map +1 -1
- package/dist/util/bytes.js +12 -20
- package/dist/util/bytes.js.map +1 -1
- package/dist/util/call-context.d.ts.map +1 -1
- package/dist/util/call-context.js +3 -2
- package/dist/util/call-context.js.map +1 -1
- package/dist/util/dates.d.ts +9 -16
- package/dist/util/dates.d.ts.map +1 -1
- package/dist/util/dates.js +39 -19
- package/dist/util/dates.js.map +1 -1
- package/dist/util/errors.d.ts +9 -1
- package/dist/util/errors.d.ts.map +1 -1
- package/dist/util/errors.js +12 -3
- package/dist/util/errors.js.map +1 -1
- package/dist/util/json.d.ts +4 -0
- package/dist/util/json.d.ts.map +1 -1
- package/dist/util/json.js +18 -0
- package/dist/util/json.js.map +1 -1
- package/dist/util/text.d.ts +21 -4
- package/dist/util/text.d.ts.map +1 -1
- package/dist/util/text.js +42 -13
- package/dist/util/text.js.map +1 -1
- package/dist/util/xml.d.ts +4 -3
- package/dist/util/xml.d.ts.map +1 -1
- package/dist/util/xml.js +37 -26
- package/dist/util/xml.js.map +1 -1
- package/dist/v3/account-extension.d.ts.map +1 -1
- package/dist/v3/account-extension.js +7 -10
- package/dist/v3/account-extension.js.map +1 -1
- package/dist/v3/bank-connection.d.ts +34 -0
- package/dist/v3/bank-connection.d.ts.map +1 -0
- package/dist/v3/bank-connection.js +58 -0
- package/dist/v3/bank-connection.js.map +1 -0
- package/dist/v3/dialog.d.ts +80 -46
- package/dist/v3/dialog.d.ts.map +1 -1
- package/dist/v3/dialog.js +162 -59
- package/dist/v3/dialog.js.map +1 -1
- package/dist/v3/message.d.ts +17 -4
- package/dist/v3/message.d.ts.map +1 -1
- package/dist/v3/message.js +62 -18
- package/dist/v3/message.js.map +1 -1
- package/dist/v3/parameter-store.d.ts +75 -0
- package/dist/v3/parameter-store.d.ts.map +1 -0
- package/dist/v3/parameter-store.js +143 -0
- package/dist/v3/parameter-store.js.map +1 -0
- package/dist/v3/parameters.d.ts +38 -12
- package/dist/v3/parameters.d.ts.map +1 -1
- package/dist/v3/parameters.js +91 -37
- package/dist/v3/parameters.js.map +1 -1
- package/dist/v3/protocol.d.ts +43 -50
- package/dist/v3/protocol.d.ts.map +1 -1
- package/dist/v3/protocol.js +58 -208
- package/dist/v3/protocol.js.map +1 -1
- package/dist/v3/queries.d.ts +51 -0
- package/dist/v3/queries.d.ts.map +1 -0
- package/dist/v3/queries.js +120 -0
- package/dist/v3/queries.js.map +1 -0
- package/dist/v3/segments.d.ts +29 -12
- package/dist/v3/segments.d.ts.map +1 -1
- package/dist/v3/segments.js +65 -23
- package/dist/v3/segments.js.map +1 -1
- package/dist/v3/syntax.d.ts +65 -14
- package/dist/v3/syntax.d.ts.map +1 -1
- package/dist/v3/syntax.js +167 -101
- package/dist/v3/syntax.js.map +1 -1
- package/package.json +10 -6
- package/src/client.ts +252 -0
- package/src/core/account.ts +86 -13
- package/src/core/amount.ts +3 -1
- package/src/core/balance.ts +6 -5
- package/src/core/bank-info.ts +1 -1
- package/src/core/client.ts +52 -15
- package/src/core/config.ts +69 -28
- package/src/core/errors.ts +122 -48
- package/src/core/feedback.ts +4 -1
- package/src/core/transaction.ts +9 -4
- package/src/core/transport.ts +98 -23
- package/src/formats/camt.ts +136 -39
- package/src/formats/mt940.ts +167 -81
- package/src/formats/readers.ts +24 -31
- package/src/formats/record-budget.ts +18 -0
- package/src/formats/sepa-purpose.ts +21 -2
- package/src/index.ts +4 -2
- package/src/session/app-approval.ts +51 -38
- package/src/session/dialog-runner.ts +31 -28
- package/src/session/login-guard.ts +174 -88
- package/src/session/sepa-accounts.ts +63 -45
- package/src/session/session-setup.ts +90 -71
- package/src/session/session-state.ts +56 -22
- package/src/session/signed-dialog.ts +192 -0
- package/src/session/tan-selection.ts +50 -39
- package/src/session/user-prompts.ts +35 -16
- package/src/tan/challenge.ts +40 -31
- package/src/tan/flicker.ts +44 -6
- package/src/tan/tan-image.ts +7 -5
- package/src/tan/tan-models.ts +10 -5
- package/src/util/async.ts +0 -5
- package/src/util/bytes.ts +13 -19
- package/src/util/call-context.ts +3 -2
- package/src/util/dates.ts +41 -24
- package/src/util/errors.ts +14 -3
- package/src/util/json.ts +16 -0
- package/src/util/text.ts +42 -12
- package/src/util/xml.ts +37 -26
- package/src/v3/account-extension.ts +7 -12
- package/src/v3/bank-connection.ts +67 -0
- package/src/v3/dialog.ts +175 -91
- package/src/v3/message.ts +69 -17
- package/src/v3/parameter-store.ts +160 -0
- package/src/v3/parameters.ts +110 -37
- package/src/v3/protocol.ts +70 -253
- package/src/v3/queries.ts +190 -0
- package/src/v3/segments.ts +65 -22
- package/src/v3/syntax.ts +153 -91
- package/dist/session/bank-connection.d.ts +0 -28
- package/dist/session/bank-connection.d.ts.map +0 -1
- package/dist/session/bank-connection.js +0 -48
- package/dist/session/bank-connection.js.map +0 -1
- package/dist/session/parameter-data.d.ts +0 -83
- package/dist/session/parameter-data.d.ts.map +0 -1
- package/dist/session/parameter-data.js +0 -68
- package/dist/session/parameter-data.js.map +0 -1
- package/dist/session/pin-tan-client.d.ts +0 -54
- package/dist/session/pin-tan-client.d.ts.map +0 -1
- package/dist/session/pin-tan-client.js +0 -153
- package/dist/session/pin-tan-client.js.map +0 -1
- package/dist/session/pin-tan-dialog.d.ts +0 -71
- package/dist/session/pin-tan-dialog.d.ts.map +0 -1
- package/dist/session/pin-tan-dialog.js +0 -124
- package/dist/session/pin-tan-dialog.js.map +0 -1
- package/dist/session/protocol.d.ts +0 -209
- package/dist/session/protocol.d.ts.map +0 -1
- package/dist/session/protocol.js +0 -75
- package/dist/session/protocol.js.map +0 -1
- package/dist/session/response-handler.d.ts +0 -48
- package/dist/session/response-handler.d.ts.map +0 -1
- package/dist/session/response-handler.js +0 -121
- package/dist/session/response-handler.js.map +0 -1
- package/dist/v3/client.d.ts +0 -24
- package/dist/v3/client.d.ts.map +0 -1
- package/dist/v3/client.js +0 -16
- package/dist/v3/client.js.map +0 -1
- package/src/session/bank-connection.ts +0 -51
- package/src/session/parameter-data.ts +0 -137
- package/src/session/pin-tan-client.ts +0 -186
- package/src/session/pin-tan-dialog.ts +0 -160
- package/src/session/protocol.ts +0 -311
- package/src/session/response-handler.ts +0 -130
- package/src/v3/client.ts +0 -29
package/src/tan/flicker.ts
CHANGED
|
@@ -26,7 +26,8 @@ export function challengeInstruction(message: string): string {
|
|
|
26
26
|
/**
|
|
27
27
|
* Digit counts of the challenge length (LC) and of the data element lengths (LDE) to try when parsing.
|
|
28
28
|
*
|
|
29
|
-
* HHD 1.4 states the LC with three digits, HHD 1.3 with two.
|
|
29
|
+
* HHD 1.4 states the LC with three digits, HHD 1.3 with two. Some banks state the LDE of HHD 1.4 with three digits;
|
|
30
|
+
* a code that both LDE widths read is taken with two, as the specification has it.
|
|
30
31
|
*/
|
|
31
32
|
const layouts = [
|
|
32
33
|
{ lcDigits: 3, lengthDigits: 2 },
|
|
@@ -34,6 +35,15 @@ const layouts = [
|
|
|
34
35
|
{ lcDigits: 2, lengthDigits: 2 },
|
|
35
36
|
];
|
|
36
37
|
|
|
38
|
+
/** The most bytes a length field states: HHD 1.3 flags ASCII data in bit 4 of it, HHD 1.4 in bit 6. */
|
|
39
|
+
const maxElementLength: Readonly<Record<HhdVersion, number>> = { hhd13: 0x0f, hhd14: 0x3f };
|
|
40
|
+
|
|
41
|
+
/** The most bytes of a rendered code, which its one-byte length (LC) states. */
|
|
42
|
+
const maxCodeLength = 0xff;
|
|
43
|
+
|
|
44
|
+
/** The characters a TAN generator shows; one byte each. */
|
|
45
|
+
const printableAscii = /^[\x20-\x7E]*$/;
|
|
46
|
+
|
|
37
47
|
function hex(value: number, width: number): string {
|
|
38
48
|
return value.toString(16).toUpperCase().padStart(width, '0');
|
|
39
49
|
}
|
|
@@ -49,7 +59,11 @@ export class FlickerCode {
|
|
|
49
59
|
private readonly dataElements: readonly DataElement[],
|
|
50
60
|
) {}
|
|
51
61
|
|
|
52
|
-
/**
|
|
62
|
+
/**
|
|
63
|
+
* Parses the challenge data sent by the bank; throws `FormatError` if it is not a flicker code a TAN generator can
|
|
64
|
+
* take: one with characters after the length it states, an empty data element, data other than printable ASCII, or
|
|
65
|
+
* more data than its length fields can state, such as more than 15 bytes in an element in HHD 1.3 or 255 bytes in all.
|
|
66
|
+
*/
|
|
53
67
|
static parse(code: string): FlickerCode {
|
|
54
68
|
const cleaned = extractCode(code);
|
|
55
69
|
for (const layout of layouts) {
|
|
@@ -89,12 +103,20 @@ export class FlickerCode {
|
|
|
89
103
|
const dataElements: DataElement[] = [];
|
|
90
104
|
for (let i = 0; i < 3; i++) {
|
|
91
105
|
dataElements.push(
|
|
92
|
-
new DataElement(challenge.isAtEnd ? undefined : challenge.take(challenge.
|
|
106
|
+
new DataElement(challenge.isAtEnd ? undefined : challenge.take(challenge.dataLength(lengthDigits))),
|
|
93
107
|
);
|
|
94
108
|
}
|
|
95
|
-
//
|
|
96
|
-
if (!challenge.isAtEnd) throw new FormatError('Flicker code fields do not match its length');
|
|
97
|
-
|
|
109
|
+
// Only a layout whose fields fill exactly the LC characters, which end the code, can be the right one.
|
|
110
|
+
if (!challenge.isAtEnd || !code.isAtEnd) throw new FormatError('Flicker code fields do not match its length');
|
|
111
|
+
const flicker = new FlickerCode(controlBytes.length === 0 ? 'hhd13' : 'hhd14', lc, startCode, dataElements);
|
|
112
|
+
if (!flicker.fitsTanGenerator) throw new FormatError('Flicker code data the TAN generator cannot take');
|
|
113
|
+
return flicker;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/** Whether the code renders as the bank sent it: its data printable ASCII, every length within its field. */
|
|
117
|
+
private get fitsTanGenerator(): boolean {
|
|
118
|
+
const elements = [this.startCode, ...this.dataElements];
|
|
119
|
+
return elements.every((element) => element.fits(this.version)) && this.payload().length <= 2 * maxCodeLength;
|
|
98
120
|
}
|
|
99
121
|
|
|
100
122
|
/** The data of the start code, which opens the challenge. */
|
|
@@ -216,6 +238,16 @@ class Reader {
|
|
|
216
238
|
return parseInt(Reader.match(/^[0-9A-Fa-f]+$/, this.take(digits)), 16);
|
|
217
239
|
}
|
|
218
240
|
|
|
241
|
+
/**
|
|
242
|
+
* A data element length field (LDE) of `digits` decimal digits: 1 to 63 characters. The challenge leaves out an
|
|
243
|
+
* element without data, and sets none of the flags in bits 6 and 7, which only the rendered code carries.
|
|
244
|
+
*/
|
|
245
|
+
dataLength(digits: number): number {
|
|
246
|
+
const length = this.decimal(digits);
|
|
247
|
+
if (length === 0 || length > 0x3f) throw new FormatError('Invalid data element length');
|
|
248
|
+
return length;
|
|
249
|
+
}
|
|
250
|
+
|
|
219
251
|
private static match(pattern: RegExp, field: string): string {
|
|
220
252
|
if (!pattern.test(field)) throw new FormatError('Invalid length field', field);
|
|
221
253
|
return field;
|
|
@@ -233,6 +265,12 @@ class DataElement {
|
|
|
233
265
|
return this.data === undefined || /^[0-9]+$/.test(this.data);
|
|
234
266
|
}
|
|
235
267
|
|
|
268
|
+
/** Whether the data are printable ASCII, and no more bytes than the length field of `version` can state. */
|
|
269
|
+
fits(version: HhdVersion): boolean {
|
|
270
|
+
const d = this.data;
|
|
271
|
+
return d === undefined || (printableAscii.test(d) && this.renderData().length / 2 <= maxElementLength[version]);
|
|
272
|
+
}
|
|
273
|
+
|
|
236
274
|
renderData(): string {
|
|
237
275
|
const d = this.data;
|
|
238
276
|
if (d === undefined) return '';
|
package/src/tan/tan-image.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { base64Encode, latin1Decode } from '../util/bytes.js';
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
/** The formats of photoTAN and QR-TAN images: raster images, which unlike SVG cannot carry scripts. */
|
|
4
|
+
const imageMimeTypes = new Set(['image/png', 'image/jpeg', 'image/gif', 'image/webp', 'image/bmp']);
|
|
4
5
|
|
|
5
6
|
/** An image challenge (photoTAN, QR-TAN) with its MIME type. */
|
|
6
7
|
export class TanImage {
|
|
@@ -14,14 +15,15 @@ export class TanImage {
|
|
|
14
15
|
/**
|
|
15
16
|
* Reads the bank's image container: a two-byte length and the MIME type, then a two-byte length and the image.
|
|
16
17
|
*
|
|
17
|
-
* `undefined` unless the MIME type is
|
|
18
|
+
* `undefined` unless the MIME type, in any case, is that of a PNG, JPEG, GIF, WebP or BMP image, and the container
|
|
19
|
+
* holds the whole image.
|
|
18
20
|
*/
|
|
19
21
|
static tryParse(data: Uint8Array): TanImage | undefined {
|
|
20
22
|
if (data.length < 4) return undefined;
|
|
21
23
|
const mimeLength = (data[0]! << 8) | data[1]!;
|
|
22
24
|
if (mimeLength === 0 || data.length < 2 + mimeLength + 2) return undefined;
|
|
23
|
-
const mime = latin1Decode(data.subarray(2, 2 + mimeLength));
|
|
24
|
-
if (!
|
|
25
|
+
const mime = latin1Decode(data.subarray(2, 2 + mimeLength)).toLowerCase();
|
|
26
|
+
if (!imageMimeTypes.has(mime)) return undefined;
|
|
25
27
|
const offset = 2 + mimeLength;
|
|
26
28
|
const imageLength = (data[offset]! << 8) | data[offset + 1]!;
|
|
27
29
|
const start = offset + 2;
|
|
@@ -29,7 +31,7 @@ export class TanImage {
|
|
|
29
31
|
return new TanImage(mime, data.subarray(start, start + imageLength));
|
|
30
32
|
}
|
|
31
33
|
|
|
32
|
-
/** The image as a `data:` URL,
|
|
34
|
+
/** The image as a `data:` URL, for the `src` of an `<img>` only: the bytes are the bank's, whatever their type says. */
|
|
33
35
|
toDataUrl(): string {
|
|
34
36
|
return `data:${this.mimeType};base64,${base64Encode(this.bytes)}`;
|
|
35
37
|
}
|
package/src/tan/tan-models.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { copyDate } from '../util/dates.js';
|
|
1
2
|
import { maskedId } from '../util/text.js';
|
|
2
3
|
|
|
3
4
|
/** Whether a TAN method needs the name of a TAN medium. */
|
|
@@ -82,7 +83,10 @@ export class TanMethod {
|
|
|
82
83
|
/** Whether approval happens in a separate app instead of entering a TAN. */
|
|
83
84
|
readonly decoupled: boolean;
|
|
84
85
|
|
|
85
|
-
/**
|
|
86
|
+
/**
|
|
87
|
+
* Most status requests the bank allows for a decoupled approval, 0 for any number. The client sends 60 where the bank
|
|
88
|
+
* states none, and polls for 10 minutes at most.
|
|
89
|
+
*/
|
|
86
90
|
readonly decoupledMaxPolls: number | undefined;
|
|
87
91
|
|
|
88
92
|
/** How long to wait before the first status request of a decoupled approval, in seconds. */
|
|
@@ -219,6 +223,7 @@ export class TanMedium {
|
|
|
219
223
|
/** When the medium was activated. */
|
|
220
224
|
readonly activatedOn: Date | undefined;
|
|
221
225
|
|
|
226
|
+
/** Takes copies of the dates of `init`: each medium has its own, which no other one shares. */
|
|
222
227
|
constructor(init: TanMediumInit) {
|
|
223
228
|
this.mediumClass = init.mediumClass;
|
|
224
229
|
this.status = init.status;
|
|
@@ -228,10 +233,10 @@ export class TanMedium {
|
|
|
228
233
|
this.cardSequenceNumber = init.cardSequenceNumber;
|
|
229
234
|
this.mobileNumber = init.mobileNumber;
|
|
230
235
|
this.mobileNumberMasked = init.mobileNumberMasked;
|
|
231
|
-
this.validFrom = init.validFrom;
|
|
232
|
-
this.validUntil = init.validUntil;
|
|
233
|
-
this.lastUsed = init.lastUsed;
|
|
234
|
-
this.activatedOn = init.activatedOn;
|
|
236
|
+
this.validFrom = copyDate(init.validFrom);
|
|
237
|
+
this.validUntil = copyDate(init.validUntil);
|
|
238
|
+
this.lastUsed = copyDate(init.lastUsed);
|
|
239
|
+
this.activatedOn = copyDate(init.activatedOn);
|
|
235
240
|
}
|
|
236
241
|
|
|
237
242
|
/** Whether the medium is the one in use. */
|
package/src/util/async.ts
CHANGED
|
@@ -46,8 +46,3 @@ export class SerialExecutor {
|
|
|
46
46
|
return result;
|
|
47
47
|
}
|
|
48
48
|
}
|
|
49
|
-
|
|
50
|
-
/** Waits for `promise` and ignores its result, so that neither a value nor an error goes unhandled. */
|
|
51
|
-
export function ignoreOutcome(promise: Promise<unknown>): void {
|
|
52
|
-
promise.catch(() => undefined);
|
|
53
|
-
}
|
package/src/util/bytes.ts
CHANGED
|
@@ -16,16 +16,9 @@ export function concatBytes(parts: readonly Uint8Array[]): Uint8Array {
|
|
|
16
16
|
/** Collects bytes, like a growing buffer. */
|
|
17
17
|
export class BytesBuilder {
|
|
18
18
|
private readonly parts: Uint8Array[] = [];
|
|
19
|
-
private size = 0;
|
|
20
|
-
|
|
21
|
-
get length(): number {
|
|
22
|
-
return this.size;
|
|
23
|
-
}
|
|
24
19
|
|
|
25
20
|
add(bytes: Uint8Array | readonly number[]): void {
|
|
26
|
-
|
|
27
|
-
this.parts.push(part);
|
|
28
|
-
this.size += part.length;
|
|
21
|
+
this.parts.push(bytes instanceof Uint8Array ? bytes : Uint8Array.from(bytes));
|
|
29
22
|
}
|
|
30
23
|
|
|
31
24
|
addByte(byte: number): void {
|
|
@@ -35,7 +28,6 @@ export class BytesBuilder {
|
|
|
35
28
|
takeBytes(): Uint8Array {
|
|
36
29
|
const result = concatBytes(this.parts);
|
|
37
30
|
this.parts.length = 0;
|
|
38
|
-
this.size = 0;
|
|
39
31
|
return result;
|
|
40
32
|
}
|
|
41
33
|
}
|
|
@@ -49,15 +41,23 @@ export function latin1Encode(text: string): Uint8Array {
|
|
|
49
41
|
|
|
50
42
|
/** The text of ISO 8859-1 `bytes`. */
|
|
51
43
|
export function latin1Decode(bytes: Uint8Array): string {
|
|
44
|
+
return fromCharCodes(bytes);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** The text of the UTF-16 code units `codes`. */
|
|
48
|
+
export function fromCharCodes(codes: Uint8Array | Uint16Array): string {
|
|
52
49
|
let text = '';
|
|
53
|
-
// In chunks, since a
|
|
54
|
-
|
|
55
|
-
|
|
50
|
+
// In chunks, since a large array exceeds the argument limit of some engines. `apply` takes the array as it is, where a
|
|
51
|
+
// spread would iterate over it.
|
|
52
|
+
for (let i = 0; i < codes.length; i += 0x8000) {
|
|
53
|
+
text += Reflect.apply(String.fromCharCode, undefined, codes.subarray(i, i + 0x8000)) as string;
|
|
56
54
|
}
|
|
57
55
|
return text;
|
|
58
56
|
}
|
|
59
57
|
|
|
60
58
|
const utf8Encoder = new TextEncoder();
|
|
59
|
+
const utf8Decoder = new TextDecoder('utf-8', { fatal: true, ignoreBOM: true });
|
|
60
|
+
const lenientUtf8Decoder = new TextDecoder('utf-8', { ignoreBOM: true });
|
|
61
61
|
|
|
62
62
|
export function utf8Encode(text: string): Uint8Array {
|
|
63
63
|
return utf8Encoder.encode(text);
|
|
@@ -66,7 +66,7 @@ export function utf8Encode(text: string): Uint8Array {
|
|
|
66
66
|
/** Decodes UTF-8; throws `FormatError` for malformed data unless `allowMalformed`. */
|
|
67
67
|
export function utf8Decode(bytes: Uint8Array, allowMalformed = false): string {
|
|
68
68
|
try {
|
|
69
|
-
return
|
|
69
|
+
return (allowMalformed ? lenientUtf8Decoder : utf8Decoder).decode(bytes);
|
|
70
70
|
} catch {
|
|
71
71
|
throw new FormatError('Invalid UTF-8');
|
|
72
72
|
}
|
|
@@ -127,9 +127,3 @@ export function base64Decode(text: string): Uint8Array {
|
|
|
127
127
|
}
|
|
128
128
|
return out;
|
|
129
129
|
}
|
|
130
|
-
|
|
131
|
-
export function bytesEqual(a: Uint8Array, b: Uint8Array): boolean {
|
|
132
|
-
if (a.length !== b.length) return false;
|
|
133
|
-
for (let i = 0; i < a.length; i++) if (a[i] !== b[i]) return false;
|
|
134
|
-
return true;
|
|
135
|
-
}
|
package/src/util/call-context.ts
CHANGED
|
@@ -7,8 +7,9 @@ interface AsyncStorage<T> {
|
|
|
7
7
|
type AsyncStorageConstructor = new <T>() => AsyncStorage<T>;
|
|
8
8
|
|
|
9
9
|
/**
|
|
10
|
-
* `AsyncLocalStorage` where the runtime has it (Node.js, Deno
|
|
11
|
-
* bundlers for the browser have nothing to
|
|
10
|
+
* `AsyncLocalStorage` where the runtime has it, found through `process.getBuiltinModule` (Node.js 22.3, Deno 2.1 and
|
|
11
|
+
* Bun 1.2.6 or later) rather than by importing `node:async_hooks`, so that bundlers for the browser have nothing to
|
|
12
|
+
* resolve.
|
|
12
13
|
*/
|
|
13
14
|
const AsyncStorageClass: AsyncStorageConstructor | undefined = (() => {
|
|
14
15
|
const runtime = globalThis as {
|
package/src/util/dates.ts
CHANGED
|
@@ -31,6 +31,7 @@ const fintsDate = /^(\d{4})(\d{2})(\d{2})$/;
|
|
|
31
31
|
const fintsTime = /^(\d{2})(\d{2})(\d{2})/;
|
|
32
32
|
const isoDate = /^(\d{4})-(\d{2})-(\d{2})/;
|
|
33
33
|
const isoTime = /^(\d{2}):(\d{2}):(\d{2})/;
|
|
34
|
+
const isoDateTime = /^(\d{4}-\d{2}-\d{2})T(\d{2}:\d{2}:\d{2})(?:\.\d+)?(Z|[+-]\d{2}:\d{2})?$/;
|
|
34
35
|
|
|
35
36
|
/** Parses a FinTS 3.0 date `YYYYMMDD` as a calendar date; `undefined` if it is missing or invalid. */
|
|
36
37
|
export function parseFintsDate(date: string | undefined): Date | undefined {
|
|
@@ -53,9 +54,21 @@ export function parseIsoDate(date: string | undefined): Date | undefined {
|
|
|
53
54
|
return parseDate(isoDate, date);
|
|
54
55
|
}
|
|
55
56
|
|
|
56
|
-
/**
|
|
57
|
-
|
|
58
|
-
|
|
57
|
+
/**
|
|
58
|
+
* Parses an ISO 8601 date and time as XML Schema writes it, `YYYY-MM-DDThh:mm:ss` with optional fractional seconds and
|
|
59
|
+
* UTC offset (`Z` or `±hh:mm`), as the moment it names, to the second: in German time where it states no offset.
|
|
60
|
+
* `undefined` if it is missing or invalid.
|
|
61
|
+
*/
|
|
62
|
+
export function parseIsoMoment(dateTime: string | undefined): Date | undefined {
|
|
63
|
+
const m = dateTime === undefined ? null : isoDateTime.exec(dateTime);
|
|
64
|
+
if (!m) return undefined;
|
|
65
|
+
const [, date, time, offset] = m;
|
|
66
|
+
const german = moment(parseIsoDate(date), isoTime, time);
|
|
67
|
+
if (german === undefined || offset === undefined) return german;
|
|
68
|
+
// Every engine reads a valid date and time with an offset alike (ECMAScript Date Time String Format), but some roll
|
|
69
|
+
// an invalid date, such as 30 February, over into the next month: `german` has checked them.
|
|
70
|
+
const exact = new Date(`${date}T${time}${offset}`);
|
|
71
|
+
return Number.isNaN(exact.getTime()) ? undefined : exact;
|
|
59
72
|
}
|
|
60
73
|
|
|
61
74
|
function parseDate(pattern: RegExp, date: string | undefined): Date | undefined {
|
|
@@ -63,13 +76,19 @@ function parseDate(pattern: RegExp, date: string | undefined): Date | undefined
|
|
|
63
76
|
return d ? validDate(Number(d[1]), Number(d[2]), Number(d[3])) : undefined;
|
|
64
77
|
}
|
|
65
78
|
|
|
79
|
+
/** Hour, minute and second of the time matched by `pattern` in `time`; `undefined` if it is missing or invalid. */
|
|
80
|
+
function parseTime(pattern: RegExp, time: string | undefined): [number, number, number] | undefined {
|
|
81
|
+
const t = time === undefined ? null : pattern.exec(time);
|
|
82
|
+
if (!t) return undefined;
|
|
83
|
+
const [hour, minute, second] = [Number(t[1]), Number(t[2]), Number(t[3])];
|
|
84
|
+
return hour > 23 || minute > 59 || second > 59 ? undefined : [hour, minute, second];
|
|
85
|
+
}
|
|
86
|
+
|
|
66
87
|
/** The moment of the German time matched by `timePattern` in `time` on `day`. */
|
|
67
88
|
function moment(day: Date | undefined, timePattern: RegExp, time: string | undefined): Date | undefined {
|
|
68
|
-
const t =
|
|
69
|
-
if (day === undefined ||
|
|
70
|
-
|
|
71
|
-
if (hour > 23 || minute > 59 || second > 59) return undefined;
|
|
72
|
-
return germanTime(day.getFullYear(), day.getMonth() + 1, day.getDate(), hour, minute, second);
|
|
89
|
+
const t = parseTime(timePattern, time);
|
|
90
|
+
if (day === undefined || t === undefined) return undefined;
|
|
91
|
+
return germanTime(day.getFullYear(), day.getMonth() + 1, day.getDate(), ...t);
|
|
73
92
|
}
|
|
74
93
|
|
|
75
94
|
/**
|
|
@@ -79,11 +98,9 @@ function moment(day: Date | undefined, timePattern: RegExp, time: string | undef
|
|
|
79
98
|
*/
|
|
80
99
|
export function parseUtcPlusOneDateTime(date: string | undefined, time: string | undefined): Date | undefined {
|
|
81
100
|
const day = parseDate(fintsDate, date);
|
|
82
|
-
const t =
|
|
83
|
-
if (day === undefined ||
|
|
84
|
-
|
|
85
|
-
if (hour > 23 || minute > 59 || second > 59) return undefined;
|
|
86
|
-
return new Date(utc(day.getFullYear(), day.getMonth() + 1, day.getDate(), hour, minute, second) - hourMs);
|
|
101
|
+
const t = parseTime(fintsTime, time?.padStart(6, '0'));
|
|
102
|
+
if (day === undefined || t === undefined) return undefined;
|
|
103
|
+
return new Date(utc(day.getFullYear(), day.getMonth() + 1, day.getDate(), ...t) - hourMs);
|
|
87
104
|
}
|
|
88
105
|
|
|
89
106
|
/**
|
|
@@ -93,14 +110,7 @@ export function parseUtcPlusOneDateTime(date: string | undefined, time: string |
|
|
|
93
110
|
* Sunday of October (the EU rule). The hour the clocks show twice in October counts as CEST; a time in the hour that is
|
|
94
111
|
* skipped in March counts as CET.
|
|
95
112
|
*/
|
|
96
|
-
|
|
97
|
-
year: number,
|
|
98
|
-
month: number,
|
|
99
|
-
day: number,
|
|
100
|
-
hour: number,
|
|
101
|
-
minute: number,
|
|
102
|
-
second: number,
|
|
103
|
-
): Date {
|
|
113
|
+
function germanTime(year: number, month: number, day: number, hour: number, minute: number, second: number): Date {
|
|
104
114
|
const clock = utc(year, month, day, hour, minute, second);
|
|
105
115
|
const summer = clock - 2 * hourMs;
|
|
106
116
|
return new Date(isSummerTime(summer) ? summer : clock - hourMs);
|
|
@@ -134,7 +144,7 @@ function utc(year: number, month: number, day: number, hour: number, minute: num
|
|
|
134
144
|
}
|
|
135
145
|
|
|
136
146
|
/** Local midnight of the day; `month` counts from 1, and years below 100 are not moved to 19xx. */
|
|
137
|
-
|
|
147
|
+
function localDate(year: number, month: number, day: number): Date {
|
|
138
148
|
const d = new Date(0);
|
|
139
149
|
d.setFullYear(year, month - 1, day);
|
|
140
150
|
d.setHours(0, 0, 0, 0);
|
|
@@ -142,7 +152,7 @@ export function localDate(year: number, month: number, day: number): Date {
|
|
|
142
152
|
}
|
|
143
153
|
|
|
144
154
|
/** The date, or `undefined` if `month` or `day` is out of range; `Date` would roll it over into another date. */
|
|
145
|
-
|
|
155
|
+
function validDate(year: number, month: number, day: number): Date | undefined {
|
|
146
156
|
if (month < 1 || month > 12 || day < 1 || day > daysInMonth(year, month)) return undefined;
|
|
147
157
|
return localDate(year, month, day);
|
|
148
158
|
}
|
|
@@ -170,11 +180,18 @@ export function parseSwiftDate(yymmdd: string): Date | undefined {
|
|
|
170
180
|
}
|
|
171
181
|
|
|
172
182
|
/** Number of days the `month` (1 to 12) of `year` has. */
|
|
173
|
-
|
|
183
|
+
function daysInMonth(year: number, month: number): number {
|
|
174
184
|
if (month === 2) return (year % 4 === 0 && year % 100 !== 0) || year % 400 === 0 ? 29 : 28;
|
|
175
185
|
return [4, 6, 9, 11].includes(month) ? 30 : 31;
|
|
176
186
|
}
|
|
177
187
|
|
|
188
|
+
/** A copy of `d`, so that a model keeps dates of its own: a `Date` can be changed in place. */
|
|
189
|
+
export function copyDate(d: Date): Date;
|
|
190
|
+
export function copyDate(d: Date | undefined): Date | undefined;
|
|
191
|
+
export function copyDate(d: Date | undefined): Date | undefined {
|
|
192
|
+
return d && new Date(d.getTime());
|
|
193
|
+
}
|
|
194
|
+
|
|
178
195
|
/** The calendar day `moment` falls on in local time, as a local `Date` at midnight. */
|
|
179
196
|
export function calendarDay(moment: Date): Date {
|
|
180
197
|
return localDate(moment.getFullYear(), moment.getMonth() + 1, moment.getDate());
|
package/src/util/errors.ts
CHANGED
|
@@ -1,15 +1,26 @@
|
|
|
1
|
-
/**
|
|
1
|
+
/**
|
|
2
|
+
* Data that do not have the expected format, such as a malformed MT940 line or amount.
|
|
3
|
+
*
|
|
4
|
+
* The message leaves out `source`, which may hold account data, such as amounts, names and IBANs, so that logs and
|
|
5
|
+
* crash reports do not collect them.
|
|
6
|
+
*/
|
|
2
7
|
export class FormatError extends Error {
|
|
8
|
+
override name = 'FormatError';
|
|
9
|
+
|
|
3
10
|
/** The part of the data that is malformed, where the parser can tell it. */
|
|
4
11
|
readonly source: string | undefined;
|
|
5
12
|
|
|
6
13
|
constructor(message: string, source?: string) {
|
|
7
|
-
super(
|
|
8
|
-
this.name = 'FormatError';
|
|
14
|
+
super(message);
|
|
9
15
|
this.source = source;
|
|
10
16
|
}
|
|
11
17
|
}
|
|
12
18
|
|
|
19
|
+
/** Throws a `FormatError`, where an expression is expected, such as `value ?? fail('…')`. */
|
|
20
|
+
export function fail(message: string, source?: string): never {
|
|
21
|
+
throw new FormatError(message, source);
|
|
22
|
+
}
|
|
23
|
+
|
|
13
24
|
/** An argument the function cannot work with, such as an empty PIN. The message never contains a PIN. */
|
|
14
25
|
export function invalidArgument(message: string, name?: string): TypeError {
|
|
15
26
|
return new TypeError(name === undefined ? message : `Invalid argument (${name}): ${message}`);
|
package/src/util/json.ts
CHANGED
|
@@ -25,6 +25,14 @@ export function readOptionalInt(json: JsonObject, key: string): number | undefin
|
|
|
25
25
|
return value;
|
|
26
26
|
}
|
|
27
27
|
|
|
28
|
+
/** The optional boolean at `key`; throws `FormatError` for a value of another type. */
|
|
29
|
+
export function readOptionalBoolean(json: JsonObject, key: string): boolean | undefined {
|
|
30
|
+
const value = json[key];
|
|
31
|
+
if (value === undefined || value === null) return undefined;
|
|
32
|
+
if (typeof value !== 'boolean') throw new FormatError(`Saved state: "${key}" is not a boolean`);
|
|
33
|
+
return value;
|
|
34
|
+
}
|
|
35
|
+
|
|
28
36
|
/** The object at `key`; throws `FormatError` if it is missing or not an object. */
|
|
29
37
|
export function readObject(json: JsonObject, key: string): JsonObject {
|
|
30
38
|
const value = json[key];
|
|
@@ -32,6 +40,14 @@ export function readObject(json: JsonObject, key: string): JsonObject {
|
|
|
32
40
|
return value;
|
|
33
41
|
}
|
|
34
42
|
|
|
43
|
+
/** The optional object at `key`; throws `FormatError` for a value of another type. */
|
|
44
|
+
export function readOptionalObject(json: JsonObject, key: string): JsonObject | undefined {
|
|
45
|
+
const value = json[key];
|
|
46
|
+
if (value === undefined || value === null) return undefined;
|
|
47
|
+
if (!isJsonObject(value)) throw new FormatError(`Saved state: "${key}" is not an object`);
|
|
48
|
+
return value;
|
|
49
|
+
}
|
|
50
|
+
|
|
35
51
|
/** The optional list at `key`, each element checked by `isElement`; throws `FormatError` for anything else. */
|
|
36
52
|
export function readOptionalList<T>(
|
|
37
53
|
json: JsonObject,
|
package/src/util/text.ts
CHANGED
|
@@ -1,14 +1,19 @@
|
|
|
1
|
-
import { base64Decode, latin1Decode, utf8Decode } from './bytes.js';
|
|
1
|
+
import { base64Decode, fromCharCodes, latin1Decode, utf8Decode } from './bytes.js';
|
|
2
2
|
import { FormatError } from './errors.js';
|
|
3
3
|
|
|
4
4
|
const xmlEncodingDeclaration = /encoding\s*=\s*["']([A-Za-z0-9._-]+)["']/;
|
|
5
5
|
const whitespace = /\s/g;
|
|
6
6
|
|
|
7
|
-
/**
|
|
7
|
+
/**
|
|
8
|
+
* Decodes text from the bank, which may be UTF-8 (with or without BOM) or ISO 8859-1; ASCII reads the same in both.
|
|
9
|
+
*
|
|
10
|
+
* UTF-8 comes first, since some banks send it. ISO 8859-1 text that happens to be valid UTF-8, such as `ß°`, reads as
|
|
11
|
+
* UTF-8 then: a rare misreading of text shown, as values sent back to the bank are read byte for byte
|
|
12
|
+
* (`Segment.verbatim`).
|
|
13
|
+
*/
|
|
8
14
|
export function decodeBankText(bytes: Uint8Array): string {
|
|
9
15
|
const hasBom = bytes.length >= 3 && bytes[0] === 0xef && bytes[1] === 0xbb && bytes[2] === 0xbf;
|
|
10
16
|
const content = hasBom ? bytes.subarray(3) : bytes;
|
|
11
|
-
if (content.every((b) => b < 0x80)) return latin1Decode(content);
|
|
12
17
|
try {
|
|
13
18
|
return utf8Decode(content);
|
|
14
19
|
} catch (error) {
|
|
@@ -44,11 +49,18 @@ const windows1252High = [
|
|
|
44
49
|
0x0153, 0x009d, 0x017e, 0x0178,
|
|
45
50
|
];
|
|
46
51
|
|
|
47
|
-
/**
|
|
52
|
+
/**
|
|
53
|
+
* Decodes Windows-1252, which matches ISO 8859-1 except for `€`, typographic quotes and a few letters.
|
|
54
|
+
*
|
|
55
|
+
* @internal Exported for the tests.
|
|
56
|
+
*/
|
|
48
57
|
export function decodeWindows1252(bytes: Uint8Array): string {
|
|
49
|
-
|
|
50
|
-
for (
|
|
51
|
-
|
|
58
|
+
const codes = new Uint16Array(bytes.length);
|
|
59
|
+
for (let i = 0; i < bytes.length; i++) {
|
|
60
|
+
const b = bytes[i]!;
|
|
61
|
+
codes[i] = b >= 0x80 && b < 0xa0 ? windows1252High[b - 0x80]! : b;
|
|
62
|
+
}
|
|
63
|
+
return fromCharCodes(codes);
|
|
52
64
|
}
|
|
53
65
|
|
|
54
66
|
/**
|
|
@@ -59,6 +71,29 @@ export function maskedId(id: string): string {
|
|
|
59
71
|
return id.length <= 4 ? '…' : `…${id.substring(id.length - 4)}`;
|
|
60
72
|
}
|
|
61
73
|
|
|
74
|
+
/** Whether `value` fits into ISO 8859-1, the FinTS 3.0 character set. */
|
|
75
|
+
export function isLatin1(value: string): boolean {
|
|
76
|
+
for (let i = 0; i < value.length; i++) if (value.charCodeAt(i) > 0xff) return false;
|
|
77
|
+
return true;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** `text` without blanks around it; `undefined` if that leaves nothing. */
|
|
81
|
+
export function nonBlank(text: string | undefined): string | undefined {
|
|
82
|
+
const trimmed = text?.trim();
|
|
83
|
+
return trimmed === '' ? undefined : trimmed;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* The character the numeric character reference `name` names, such as `#x41` or `#65`; `undefined` for any other name,
|
|
88
|
+
* and for a number that is no character as XML defines them: `NUL`, half a surrogate pair, or beyond Unicode.
|
|
89
|
+
*/
|
|
90
|
+
export function characterReference(name: string): string | undefined {
|
|
91
|
+
const m = /^#(?:x([0-9a-fA-F]+)|([0-9]+))$/.exec(name);
|
|
92
|
+
if (!m) return undefined;
|
|
93
|
+
const code = m[1] === undefined ? parseInt(m[2]!, 10) : parseInt(m[1], 16);
|
|
94
|
+
return code > 0 && (code < 0xd800 || code > 0xdfff) && code <= 0x10ffff ? String.fromCodePoint(code) : undefined;
|
|
95
|
+
}
|
|
96
|
+
|
|
62
97
|
/** `text` without spaces and line breaks. */
|
|
63
98
|
export function withoutWhitespace(text: string): string {
|
|
64
99
|
return text.replace(whitespace, '');
|
|
@@ -69,11 +104,6 @@ export function decodeBase64Lines(text: string): Uint8Array {
|
|
|
69
104
|
return base64Decode(withoutWhitespace(text));
|
|
70
105
|
}
|
|
71
106
|
|
|
72
|
-
/** `text` with every match of `pattern`, a literal string, escaped for use in a regular expression. */
|
|
73
|
-
export function escapeRegExp(text: string): string {
|
|
74
|
-
return text.replace(/[.*+?^${}()|[\]\\/-]/g, '\\$&');
|
|
75
|
-
}
|
|
76
|
-
|
|
77
107
|
/** The integer `text` states, decimal digits with an optional sign and blanks around; `undefined` for anything else. */
|
|
78
108
|
export function tryParseInt(text: string | undefined): number | undefined {
|
|
79
109
|
if (text === undefined) return undefined;
|