@coderoftime/fints 0.2.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +377 -0
- package/README.md +31 -23
- package/dist/client.d.ts +3 -2
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +52 -9
- package/dist/client.js.map +1 -1
- package/dist/core/account.d.ts +15 -4
- package/dist/core/account.d.ts.map +1 -1
- package/dist/core/account.js +84 -36
- package/dist/core/account.js.map +1 -1
- package/dist/core/amount.d.ts +13 -3
- package/dist/core/amount.d.ts.map +1 -1
- package/dist/core/amount.js +42 -8
- package/dist/core/amount.js.map +1 -1
- package/dist/core/balance.d.ts +3 -0
- package/dist/core/balance.d.ts.map +1 -1
- package/dist/core/balance.js +5 -0
- package/dist/core/balance.js.map +1 -1
- package/dist/core/bank-info.d.ts +20 -3
- package/dist/core/bank-info.d.ts.map +1 -1
- package/dist/core/bank-info.js +25 -4
- package/dist/core/bank-info.js.map +1 -1
- package/dist/core/client.d.ts +42 -15
- package/dist/core/client.d.ts.map +1 -1
- package/dist/core/config.d.ts +15 -7
- package/dist/core/config.d.ts.map +1 -1
- package/dist/core/config.js +29 -14
- package/dist/core/config.js.map +1 -1
- package/dist/core/errors.d.ts +17 -5
- package/dist/core/errors.d.ts.map +1 -1
- package/dist/core/errors.js +25 -7
- package/dist/core/errors.js.map +1 -1
- package/dist/core/feedback.d.ts +7 -0
- package/dist/core/feedback.d.ts.map +1 -1
- package/dist/core/feedback.js +10 -1
- package/dist/core/feedback.js.map +1 -1
- package/dist/core/transaction.d.ts +5 -0
- package/dist/core/transaction.d.ts.map +1 -1
- package/dist/core/transaction.js +12 -0
- package/dist/core/transaction.js.map +1 -1
- package/dist/core/transport.d.ts +22 -7
- package/dist/core/transport.d.ts.map +1 -1
- package/dist/core/transport.js +51 -11
- package/dist/core/transport.js.map +1 -1
- package/dist/formats/camt.d.ts +7 -4
- package/dist/formats/camt.d.ts.map +1 -1
- package/dist/formats/camt.js +53 -54
- package/dist/formats/camt.js.map +1 -1
- package/dist/formats/mt940.d.ts +18 -6
- package/dist/formats/mt940.d.ts.map +1 -1
- package/dist/formats/mt940.js +83 -66
- package/dist/formats/mt940.js.map +1 -1
- package/dist/formats/readers.d.ts.map +1 -1
- package/dist/formats/readers.js +6 -2
- package/dist/formats/readers.js.map +1 -1
- package/dist/formats/sepa-purpose.d.ts +7 -4
- package/dist/formats/sepa-purpose.d.ts.map +1 -1
- package/dist/formats/sepa-purpose.js +18 -9
- package/dist/formats/sepa-purpose.js.map +1 -1
- package/dist/session/dialog-runner.d.ts +5 -2
- package/dist/session/dialog-runner.d.ts.map +1 -1
- package/dist/session/dialog-runner.js +23 -7
- package/dist/session/dialog-runner.js.map +1 -1
- package/dist/session/login-guard.d.ts +4 -4
- package/dist/session/login-guard.d.ts.map +1 -1
- package/dist/session/login-guard.js +29 -14
- package/dist/session/login-guard.js.map +1 -1
- package/dist/session/session-setup.d.ts +14 -5
- package/dist/session/session-setup.d.ts.map +1 -1
- package/dist/session/session-setup.js +30 -9
- package/dist/session/session-setup.js.map +1 -1
- package/dist/session/session-state.d.ts +4 -3
- package/dist/session/session-state.d.ts.map +1 -1
- package/dist/session/session-state.js +28 -9
- package/dist/session/session-state.js.map +1 -1
- package/dist/session/signed-dialog.d.ts +19 -13
- package/dist/session/signed-dialog.d.ts.map +1 -1
- package/dist/session/signed-dialog.js +45 -30
- package/dist/session/signed-dialog.js.map +1 -1
- package/dist/session/user-prompts.d.ts +6 -1
- package/dist/session/user-prompts.d.ts.map +1 -1
- package/dist/session/user-prompts.js +19 -4
- package/dist/session/user-prompts.js.map +1 -1
- package/dist/tan/challenge.d.ts +15 -13
- package/dist/tan/challenge.d.ts.map +1 -1
- package/dist/tan/challenge.js +32 -26
- package/dist/tan/challenge.js.map +1 -1
- package/dist/tan/flicker.d.ts +5 -1
- package/dist/tan/flicker.d.ts.map +1 -1
- package/dist/tan/flicker.js +42 -30
- package/dist/tan/flicker.js.map +1 -1
- package/dist/tan/tan-image.d.ts +4 -1
- package/dist/tan/tan-image.d.ts.map +1 -1
- package/dist/tan/tan-image.js +8 -2
- package/dist/tan/tan-image.js.map +1 -1
- package/dist/tan/tan-models.d.ts +9 -1
- package/dist/tan/tan-models.d.ts.map +1 -1
- package/dist/tan/tan-models.js +14 -2
- package/dist/tan/tan-models.js.map +1 -1
- package/dist/util/arguments.d.ts +13 -0
- package/dist/util/arguments.d.ts.map +1 -0
- package/dist/util/arguments.js +40 -0
- package/dist/util/arguments.js.map +1 -0
- package/dist/util/async.d.ts +4 -1
- package/dist/util/async.d.ts.map +1 -1
- package/dist/util/async.js +6 -3
- package/dist/util/async.js.map +1 -1
- package/dist/util/bytes.d.ts +1 -1
- package/dist/util/bytes.d.ts.map +1 -1
- package/dist/util/bytes.js +3 -3
- package/dist/util/bytes.js.map +1 -1
- package/dist/util/dates.d.ts +3 -10
- package/dist/util/dates.d.ts.map +1 -1
- package/dist/util/dates.js +49 -39
- package/dist/util/dates.js.map +1 -1
- package/dist/util/errors.d.ts +5 -3
- package/dist/util/errors.d.ts.map +1 -1
- package/dist/util/errors.js +8 -4
- package/dist/util/errors.js.map +1 -1
- package/dist/util/immutable-map.d.ts +5 -2
- package/dist/util/immutable-map.d.ts.map +1 -1
- package/dist/util/immutable-map.js +15 -12
- package/dist/util/immutable-map.js.map +1 -1
- package/dist/util/inspect.d.ts +11 -0
- package/dist/util/inspect.d.ts.map +1 -0
- package/dist/util/inspect.js +14 -0
- package/dist/util/inspect.js.map +1 -0
- package/dist/util/text.d.ts +18 -2
- package/dist/util/text.d.ts.map +1 -1
- package/dist/util/text.js +50 -5
- package/dist/util/text.js.map +1 -1
- package/dist/util/xml.d.ts +6 -4
- package/dist/util/xml.d.ts.map +1 -1
- package/dist/util/xml.js +106 -44
- package/dist/util/xml.js.map +1 -1
- package/dist/v3/bank-connection.d.ts +23 -3
- package/dist/v3/bank-connection.d.ts.map +1 -1
- package/dist/v3/bank-connection.js +66 -6
- package/dist/v3/bank-connection.js.map +1 -1
- package/dist/v3/dialog.d.ts +6 -2
- package/dist/v3/dialog.d.ts.map +1 -1
- package/dist/v3/dialog.js +20 -9
- package/dist/v3/dialog.js.map +1 -1
- package/dist/v3/message.d.ts +18 -10
- package/dist/v3/message.d.ts.map +1 -1
- package/dist/v3/message.js +92 -75
- package/dist/v3/message.js.map +1 -1
- package/dist/v3/parameter-store.d.ts +43 -7
- package/dist/v3/parameter-store.d.ts.map +1 -1
- package/dist/v3/parameter-store.js +69 -15
- package/dist/v3/parameter-store.js.map +1 -1
- package/dist/v3/parameters.d.ts +12 -11
- package/dist/v3/parameters.d.ts.map +1 -1
- package/dist/v3/parameters.js +57 -35
- package/dist/v3/parameters.js.map +1 -1
- package/dist/v3/protocol.d.ts +16 -9
- package/dist/v3/protocol.d.ts.map +1 -1
- package/dist/v3/protocol.js +57 -26
- package/dist/v3/protocol.js.map +1 -1
- package/dist/v3/queries.d.ts +2 -2
- package/dist/v3/queries.d.ts.map +1 -1
- package/dist/v3/queries.js +24 -16
- package/dist/v3/queries.js.map +1 -1
- package/dist/v3/segments.d.ts +18 -11
- package/dist/v3/segments.d.ts.map +1 -1
- package/dist/v3/segments.js +30 -21
- package/dist/v3/segments.js.map +1 -1
- package/dist/v3/syntax.d.ts +15 -11
- package/dist/v3/syntax.d.ts.map +1 -1
- package/dist/v3/syntax.js +30 -15
- package/dist/v3/syntax.js.map +1 -1
- package/package.json +1 -1
- package/src/client.ts +58 -17
- package/src/core/account.ts +91 -35
- package/src/core/amount.ts +42 -9
- package/src/core/balance.ts +6 -0
- package/src/core/bank-info.ts +28 -4
- package/src/core/client.ts +42 -15
- package/src/core/config.ts +37 -18
- package/src/core/errors.ts +27 -7
- package/src/core/feedback.ts +12 -1
- package/src/core/transaction.ts +13 -0
- package/src/core/transport.ts +62 -16
- package/src/formats/camt.ts +62 -51
- package/src/formats/mt940.ts +114 -78
- package/src/formats/readers.ts +6 -2
- package/src/formats/sepa-purpose.ts +17 -9
- package/src/session/dialog-runner.ts +34 -7
- package/src/session/login-guard.ts +29 -13
- package/src/session/session-setup.ts +27 -9
- package/src/session/session-state.ts +25 -8
- package/src/session/signed-dialog.ts +60 -30
- package/src/session/user-prompts.ts +28 -3
- package/src/tan/challenge.ts +50 -36
- package/src/tan/flicker.ts +40 -29
- package/src/tan/tan-image.ts +9 -2
- package/src/tan/tan-models.ts +16 -2
- package/src/util/arguments.ts +40 -0
- package/src/util/async.ts +10 -4
- package/src/util/bytes.ts +3 -3
- package/src/util/dates.ts +52 -38
- package/src/util/errors.ts +9 -4
- package/src/util/immutable-map.ts +15 -12
- package/src/util/inspect.ts +14 -0
- package/src/util/text.ts +47 -5
- package/src/util/xml.ts +105 -38
- package/src/v3/bank-connection.ts +65 -6
- package/src/v3/dialog.ts +18 -8
- package/src/v3/message.ts +81 -72
- package/src/v3/parameter-store.ts +82 -16
- package/src/v3/parameters.ts +68 -41
- package/src/v3/protocol.ts +61 -28
- package/src/v3/queries.ts +26 -18
- package/src/v3/segments.ts +32 -20
- package/src/v3/syntax.ts +31 -14
package/src/tan/challenge.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { latin1Decode } from '../util/bytes.js';
|
|
2
2
|
import { copyDate } from '../util/dates.js';
|
|
3
|
-
import {
|
|
3
|
+
import { frozenWithInspect, inspectCustom } from '../util/inspect.js';
|
|
4
|
+
import { characterReference, trimBlanks } from '../util/text.js';
|
|
4
5
|
import { challengeInstruction, FlickerCode, hasEmbeddedFlickerCode } from './flicker.js';
|
|
5
6
|
import { TanImage } from './tan-image.js';
|
|
6
7
|
import type { TanMedium, TanMethod } from './tan-models.js';
|
|
@@ -25,6 +26,9 @@ const formattingTag = /<\/?(?:b|i|u|ul|ol)\b[^<>]*>/gi;
|
|
|
25
26
|
const entityReference = /&(#x?[0-9a-fA-F]+|[a-zA-Z]+);/g;
|
|
26
27
|
const blankLines = /\n{3,}/g;
|
|
27
28
|
|
|
29
|
+
/** Longest challenge `plainMessage` formats: far longer than the specification allows (an..2048). */
|
|
30
|
+
const maxFormattedLength = 65_536;
|
|
31
|
+
|
|
28
32
|
const namedEntities: Record<string, string> = {
|
|
29
33
|
amp: '&',
|
|
30
34
|
lt: '<',
|
|
@@ -131,8 +135,9 @@ export class TanChallenge {
|
|
|
131
135
|
if (data !== undefined) {
|
|
132
136
|
image = TanImage.tryParse(data);
|
|
133
137
|
if (image === undefined) flicker = FlickerCode.tryParse(latin1Decode(data));
|
|
134
|
-
}
|
|
135
|
-
|
|
138
|
+
}
|
|
139
|
+
if (image === undefined && init.message !== undefined && hasEmbeddedFlickerCode(init.message)) {
|
|
140
|
+
flicker ??= FlickerCode.tryParse(init.message);
|
|
136
141
|
}
|
|
137
142
|
return new TanChallenge({
|
|
138
143
|
...init,
|
|
@@ -157,7 +162,7 @@ export class TanChallenge {
|
|
|
157
162
|
* A method with structured challenges (`TanMethod.structuredChallenge`) may use the formatting tags the specification
|
|
158
163
|
* defines. Line breaks, paragraphs and list items become new lines. Bold, italics, underlining and list tags are
|
|
159
164
|
* removed, and the entity references of such markup are resolved. Any other challenge stays as the bank sent it,
|
|
160
|
-
* which is how the specification wants it shown.
|
|
165
|
+
* which is how the specification wants it shown. So does a structured challenge of more than 65,536 characters.
|
|
161
166
|
*
|
|
162
167
|
* Show it as text, such as with `textContent`, or `{{ }}` in Vue, and `white-space: pre-line` for its line breaks;
|
|
163
168
|
* never as HTML (`innerHTML`, `v-html`). It keeps the tags the specification does not define, and `<` becomes
|
|
@@ -167,19 +172,33 @@ export class TanChallenge {
|
|
|
167
172
|
const text = this.message;
|
|
168
173
|
if (text === undefined) return undefined;
|
|
169
174
|
const instruction = challengeInstruction(text);
|
|
170
|
-
if (!this.method.structuredChallenge) return instruction;
|
|
171
|
-
return
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
175
|
+
if (!this.method.structuredChallenge || instruction.length > maxFormattedLength) return instruction;
|
|
176
|
+
return trimBlanks(
|
|
177
|
+
instruction
|
|
178
|
+
.replace(lineBreak, '\n')
|
|
179
|
+
.replace(blockTag, '\n')
|
|
180
|
+
.replace(formattingTag, '')
|
|
181
|
+
.replace(entityReference, (match, name: string) => decodeEntity(name) ?? match)
|
|
182
|
+
.replace(blankLines, '\n\n'),
|
|
183
|
+
);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* What Node.js's `util.inspect` and `console.log` show: the method, the kind of challenge and the attempt; not the
|
|
188
|
+
* text, the reference, the challenge data, the image or the medium, which may name the user, the account or the order.
|
|
189
|
+
*/
|
|
190
|
+
[inspectCustom](): string {
|
|
191
|
+
const kinds = [
|
|
192
|
+
...(this.isDecoupled ? ['decoupled'] : []),
|
|
193
|
+
...(this.flickerCode === undefined ? [] : ['flicker code']),
|
|
194
|
+
...(this.image === undefined ? [] : ['image']),
|
|
195
|
+
];
|
|
196
|
+
return `TanChallenge(${[this.method.toString(), ...kinds, `attempt ${this.attempt}`].join(', ')})`;
|
|
178
197
|
}
|
|
179
198
|
}
|
|
180
199
|
|
|
181
200
|
// Copies of a challenge for the prompts of one approval. Not methods of `TanChallenge`, so that they are not part of its
|
|
182
|
-
// API
|
|
201
|
+
// API. The own fields of a challenge are those of `TanChallengeInit`, so a copy spreads it.
|
|
183
202
|
|
|
184
203
|
/** `challenge` again, counted as the next prompt for the same approval, with the bank's new instructions `message`. */
|
|
185
204
|
export function nextAttempt(challenge: TanChallenge, message?: string): TanChallenge {
|
|
@@ -192,27 +211,15 @@ export function withResolved(challenge: TanChallenge, resolved: Promise<void>):
|
|
|
192
211
|
}
|
|
193
212
|
|
|
194
213
|
function copyChallenge(challenge: TanChallenge, changes: Partial<TanChallengeInit>): TanChallenge {
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
reference: challenge.reference,
|
|
198
|
-
message: challenge.message,
|
|
199
|
-
hhdUc: challenge.hhdUc,
|
|
200
|
-
flickerCode: challenge.flickerCode,
|
|
201
|
-
image: challenge.image,
|
|
202
|
-
isDecoupled: challenge.isDecoupled,
|
|
203
|
-
validUntil: challenge.validUntil,
|
|
204
|
-
tanMediumName: challenge.tanMediumName,
|
|
205
|
-
attempt: challenge.attempt,
|
|
206
|
-
resolved: challenge.resolved,
|
|
207
|
-
...changes,
|
|
208
|
-
});
|
|
214
|
+
const init: TanChallengeInit = challenge;
|
|
215
|
+
return new TanChallenge({ ...init, ...changes });
|
|
209
216
|
}
|
|
210
217
|
|
|
211
218
|
/** The user's answer to a `TanChallenge`: `TanAnswer.tan(…)`, `TanAnswer.approved` or `TanAnswer.cancelled`. */
|
|
212
219
|
export type TanAnswer =
|
|
213
220
|
/**
|
|
214
|
-
* The TAN the user entered; an empty one cancels a TAN challenge. Blanks around it do not count, and
|
|
215
|
-
*
|
|
221
|
+
* The TAN the user entered; an empty one cancels a TAN challenge. Blanks around it do not count, and full-width
|
|
222
|
+
* characters, as East Asian input methods type them, count as their ASCII counterparts.
|
|
216
223
|
*/
|
|
217
224
|
| { readonly kind: 'tan'; readonly tan: string }
|
|
218
225
|
/** The user reports the approval in the banking app (`TanChallenge.isDecoupled`); the client then asks the bank. */
|
|
@@ -220,13 +227,18 @@ export type TanAnswer =
|
|
|
220
227
|
/** The user cancels: the client ends the bank dialog, and the call fails with `FinTsTanCancelledError`. */
|
|
221
228
|
| { readonly kind: 'cancelled' };
|
|
222
229
|
|
|
223
|
-
|
|
224
|
-
|
|
230
|
+
/** `answer` frozen, which `util.inspect` shows by its kind, never with the TAN. */
|
|
231
|
+
function frozenAnswer(answer: TanAnswer): TanAnswer {
|
|
232
|
+
return frozenWithInspect(answer, (a) => `TanAnswer(${a.kind})`);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
const approvedAnswer = frozenAnswer({ kind: 'approved' });
|
|
236
|
+
const cancelledAnswer = frozenAnswer({ kind: 'cancelled' });
|
|
225
237
|
|
|
226
238
|
/** The answers a `TanCallback` returns. */
|
|
227
239
|
export const TanAnswer = Object.freeze({
|
|
228
240
|
/** The TAN the user entered; an empty one cancels a TAN challenge (`TanAnswer`). */
|
|
229
|
-
tan: (tan: string): TanAnswer =>
|
|
241
|
+
tan: (tan: string): TanAnswer => frozenAnswer({ kind: 'tan', tan }),
|
|
230
242
|
/** The user reports the approval in the banking app (`TanChallenge.isDecoupled`); the client then asks the bank. */
|
|
231
243
|
approved: approvedAnswer,
|
|
232
244
|
/** The user cancels: the client ends the bank dialog, and the call fails with `FinTsTanCancelledError`. */
|
|
@@ -252,10 +264,12 @@ export const TanAnswer = Object.freeze({
|
|
|
252
264
|
*/
|
|
253
265
|
export type TanCallback = (challenge: TanChallenge) => TanAnswer | Promise<TanAnswer>;
|
|
254
266
|
|
|
255
|
-
/** Picks one of several allowed TAN methods; the first is used when this returns `undefined`. */
|
|
267
|
+
/** Picks one of several allowed TAN methods; the first is used when this returns `undefined` or `null`. */
|
|
256
268
|
export type TanMethodSelector = (
|
|
257
269
|
methods: readonly TanMethod[],
|
|
258
|
-
) => TanMethod | undefined | Promise<TanMethod | undefined>;
|
|
270
|
+
) => TanMethod | null | undefined | Promise<TanMethod | null | undefined>;
|
|
259
271
|
|
|
260
|
-
/** Picks one of several active TAN media; the first is used when this returns `undefined`. */
|
|
261
|
-
export type TanMediumSelector = (
|
|
272
|
+
/** Picks one of several active TAN media; the first is used when this returns `undefined` or `null`. */
|
|
273
|
+
export type TanMediumSelector = (
|
|
274
|
+
media: readonly TanMedium[],
|
|
275
|
+
) => TanMedium | null | undefined | Promise<TanMedium | null | undefined>;
|
package/src/tan/flicker.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { FormatError } from '../util/errors.js';
|
|
2
|
-
import {
|
|
2
|
+
import { inspectCustom } from '../util/inspect.js';
|
|
3
|
+
import { trimBlanks, withoutWhitespace } from '../util/text.js';
|
|
3
4
|
|
|
4
5
|
/** Version of the chipTAN HHD specification a flicker code follows: 1.3, or 1.4, which adds control bytes. */
|
|
5
6
|
export type HhdVersion = 'hhd13' | 'hhd14';
|
|
@@ -10,17 +11,29 @@ const codeMarker = 'CHLGUC';
|
|
|
10
11
|
const textMarker = 'CHLGTEXT';
|
|
11
12
|
const markerLengthDigits = 4;
|
|
12
13
|
|
|
14
|
+
/**
|
|
15
|
+
* The flicker code and the instruction text of a combined challenge; `undefined` unless `message` starts with
|
|
16
|
+
* `CHLGUC`, after blanks, and states the length of the code. White space in the code wraps lines and is dropped.
|
|
17
|
+
*/
|
|
18
|
+
function splitCombined(message: string): { code: string; instruction: string } | undefined {
|
|
19
|
+
const text = trimBlanks(message, { end: false });
|
|
20
|
+
const end = text.indexOf(textMarker);
|
|
21
|
+
if (!text.startsWith(codeMarker) || end < 0) return undefined;
|
|
22
|
+
const lengthAndCode = withoutWhitespace(text.substring(codeMarker.length, end));
|
|
23
|
+
const code = lengthAndCode.substring(markerLengthDigits);
|
|
24
|
+
const length = lengthAndCode.substring(0, markerLengthDigits);
|
|
25
|
+
if (!/^[0-9]{4}$/.test(length) || parseInt(length, 10) !== code.length) return undefined;
|
|
26
|
+
return { code, instruction: text.substring(end + textMarker.length + markerLengthDigits) };
|
|
27
|
+
}
|
|
28
|
+
|
|
13
29
|
/** Whether `message` is a combined challenge with an embedded flicker code. */
|
|
14
30
|
export function hasEmbeddedFlickerCode(message: string): boolean {
|
|
15
|
-
return message
|
|
31
|
+
return splitCombined(message) !== undefined;
|
|
16
32
|
}
|
|
17
33
|
|
|
18
34
|
/** The instruction text of a combined challenge; `message` itself if it is none. */
|
|
19
35
|
export function challengeInstruction(message: string): string {
|
|
20
|
-
|
|
21
|
-
if (start < 0) return message;
|
|
22
|
-
const textStart = start + textMarker.length + markerLengthDigits;
|
|
23
|
-
return textStart < message.length ? message.substring(textStart) : '';
|
|
36
|
+
return splitCombined(message)?.instruction ?? message;
|
|
24
37
|
}
|
|
25
38
|
|
|
26
39
|
/**
|
|
@@ -60,15 +73,16 @@ export class FlickerCode {
|
|
|
60
73
|
) {}
|
|
61
74
|
|
|
62
75
|
/**
|
|
63
|
-
* Parses the challenge data sent by the bank
|
|
76
|
+
* Parses the challenge data sent by the bank, the code itself or a combined challenge, which embeds it in the
|
|
77
|
+
* challenge text (`CHLGUC`), wrapped into lines; throws `FormatError` if it is not a flicker code a TAN generator can
|
|
64
78
|
* take: one with characters after the length it states, an empty data element, data other than printable ASCII, or
|
|
65
79
|
* 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
80
|
*/
|
|
67
81
|
static parse(code: string): FlickerCode {
|
|
68
|
-
const
|
|
82
|
+
const embedded = splitCombined(code)?.code ?? trimBlanks(code);
|
|
69
83
|
for (const layout of layouts) {
|
|
70
84
|
try {
|
|
71
|
-
return FlickerCode.parseLayout(new Reader(
|
|
85
|
+
return FlickerCode.parseLayout(new Reader(embedded), layout.lcDigits, layout.lengthDigits);
|
|
72
86
|
} catch (error) {
|
|
73
87
|
// Not this layout; try the next one.
|
|
74
88
|
if (!(error instanceof FormatError)) throw error;
|
|
@@ -96,14 +110,15 @@ export class FlickerCode {
|
|
|
96
110
|
const controlBytes: number[] = [];
|
|
97
111
|
if ((startLength & 0x80) !== 0) {
|
|
98
112
|
do {
|
|
113
|
+
if (controlBytes.length === 10) throw new FormatError('Too many control bytes in flicker code');
|
|
99
114
|
controlBytes.push(challenge.hex(2));
|
|
100
|
-
} while ((controlBytes.at(-1)! & 0x80) !== 0
|
|
115
|
+
} while ((controlBytes.at(-1)! & 0x80) !== 0);
|
|
101
116
|
}
|
|
102
|
-
const startCode = new StartCode(challenge.take(startLength &
|
|
117
|
+
const startCode = new StartCode(challenge.take(elementLength(startLength & 0x7f)), controlBytes);
|
|
103
118
|
const dataElements: DataElement[] = [];
|
|
104
119
|
for (let i = 0; i < 3; i++) {
|
|
105
120
|
dataElements.push(
|
|
106
|
-
new DataElement(challenge.isAtEnd ? undefined : challenge.take(challenge.
|
|
121
|
+
new DataElement(challenge.isAtEnd ? undefined : challenge.take(elementLength(challenge.decimal(lengthDigits)))),
|
|
107
122
|
);
|
|
108
123
|
}
|
|
109
124
|
// Only a layout whose fields fill exactly the LC characters, which end the code, can be the right one.
|
|
@@ -183,15 +198,21 @@ export class FlickerCode {
|
|
|
183
198
|
}
|
|
184
199
|
return result;
|
|
185
200
|
}
|
|
201
|
+
|
|
202
|
+
/** What Node.js's `util.inspect` and `console.log` show: the version, not the data, which may hold IBAN and amount. */
|
|
203
|
+
[inspectCustom](): string {
|
|
204
|
+
return `FlickerCode(${this.version})`;
|
|
205
|
+
}
|
|
186
206
|
}
|
|
187
207
|
|
|
188
|
-
/**
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
208
|
+
/**
|
|
209
|
+
* The length a field of the challenge states for the start code or a data element: 1 to 63 characters. The challenge
|
|
210
|
+
* leaves out an element without data and sets no flag in bit 6, which only the rendered code carries; bit 7 of the start
|
|
211
|
+
* code, which announces control bytes, is masked off before.
|
|
212
|
+
*/
|
|
213
|
+
function elementLength(length: number): number {
|
|
214
|
+
if (length === 0 || length > 0x3f) throw new FormatError('Invalid data element length');
|
|
215
|
+
return length;
|
|
195
216
|
}
|
|
196
217
|
|
|
197
218
|
function xorChecksum(payload: string): string {
|
|
@@ -238,16 +259,6 @@ class Reader {
|
|
|
238
259
|
return parseInt(Reader.match(/^[0-9A-Fa-f]+$/, this.take(digits)), 16);
|
|
239
260
|
}
|
|
240
261
|
|
|
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
|
-
|
|
251
262
|
private static match(pattern: RegExp, field: string): string {
|
|
252
263
|
if (!pattern.test(field)) throw new FormatError('Invalid length field', field);
|
|
253
264
|
return field;
|
package/src/tan/tan-image.ts
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { base64Encode, latin1Decode } from '../util/bytes.js';
|
|
2
|
+
import { inspectCustom } from '../util/inspect.js';
|
|
3
|
+
import { counted } from '../util/text.js';
|
|
2
4
|
|
|
3
5
|
/** The formats of photoTAN and QR-TAN images: raster images, which unlike SVG cannot carry scripts. */
|
|
4
6
|
const imageMimeTypes = new Set(['image/png', 'image/jpeg', 'image/gif', 'image/webp', 'image/bmp']);
|
|
@@ -16,7 +18,7 @@ export class TanImage {
|
|
|
16
18
|
* Reads the bank's image container: a two-byte length and the MIME type, then a two-byte length and the image.
|
|
17
19
|
*
|
|
18
20
|
* `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.
|
|
21
|
+
* holds the whole image, of at least one byte.
|
|
20
22
|
*/
|
|
21
23
|
static tryParse(data: Uint8Array): TanImage | undefined {
|
|
22
24
|
if (data.length < 4) return undefined;
|
|
@@ -27,7 +29,7 @@ export class TanImage {
|
|
|
27
29
|
const offset = 2 + mimeLength;
|
|
28
30
|
const imageLength = (data[offset]! << 8) | data[offset + 1]!;
|
|
29
31
|
const start = offset + 2;
|
|
30
|
-
if (start + imageLength > data.length) return undefined;
|
|
32
|
+
if (imageLength === 0 || start + imageLength > data.length) return undefined;
|
|
31
33
|
return new TanImage(mime, data.subarray(start, start + imageLength));
|
|
32
34
|
}
|
|
33
35
|
|
|
@@ -35,4 +37,9 @@ export class TanImage {
|
|
|
35
37
|
toDataUrl(): string {
|
|
36
38
|
return `data:${this.mimeType};base64,${base64Encode(this.bytes)}`;
|
|
37
39
|
}
|
|
40
|
+
|
|
41
|
+
/** What Node.js's `util.inspect` and `console.log` show: type and size, not the image, which shows the order. */
|
|
42
|
+
[inspectCustom](): string {
|
|
43
|
+
return `TanImage(${this.mimeType}, ${counted(this.bytes.length, 'byte')})`;
|
|
44
|
+
}
|
|
38
45
|
}
|
package/src/tan/tan-models.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { copyDate } from '../util/dates.js';
|
|
2
|
+
import { inspectCustom } from '../util/inspect.js';
|
|
2
3
|
import { maskedId } from '../util/text.js';
|
|
3
4
|
|
|
4
5
|
/** Whether a TAN method needs the name of a TAN medium. */
|
|
@@ -138,6 +139,11 @@ export class TanMethod {
|
|
|
138
139
|
toString(): string {
|
|
139
140
|
return `TanMethod(${this.securityFunction}, ${this.name})`;
|
|
140
141
|
}
|
|
142
|
+
|
|
143
|
+
/** What Node.js's `util.inspect` and `console.log` show: `toString`, not the fields. */
|
|
144
|
+
[inspectCustom](): string {
|
|
145
|
+
return this.toString();
|
|
146
|
+
}
|
|
141
147
|
}
|
|
142
148
|
|
|
143
149
|
/** State of a TAN medium as reported by the bank. */
|
|
@@ -244,9 +250,17 @@ export class TanMedium {
|
|
|
244
250
|
return this.status === 'active' || this.status === 'activeFollowUpCard';
|
|
245
251
|
}
|
|
246
252
|
|
|
247
|
-
/**
|
|
253
|
+
/**
|
|
254
|
+
* The medium class, or the last digits of the card number, and the status: no name, which the user may have given the
|
|
255
|
+
* medium after themselves, such as `Max’s iPhone`, so that logs and crash reports collect neither.
|
|
256
|
+
*/
|
|
248
257
|
toString(): string {
|
|
249
|
-
const label = this.
|
|
258
|
+
const label = this.cardNumber === undefined ? this.mediumClass : maskedId(this.cardNumber);
|
|
250
259
|
return `TanMedium(${label}, ${this.status})`;
|
|
251
260
|
}
|
|
261
|
+
|
|
262
|
+
/** What Node.js's `util.inspect` and `console.log` show: `toString`, not the fields. */
|
|
263
|
+
[inspectCustom](): string {
|
|
264
|
+
return this.toString();
|
|
265
|
+
}
|
|
252
266
|
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { invalidArgument } from './errors.js';
|
|
2
|
+
|
|
3
|
+
// Plain JavaScript passes arguments of types the declarations rule out, which the Dart package's types refuse. These
|
|
4
|
+
// checks refuse them as well, with messages that leave out the value.
|
|
5
|
+
|
|
6
|
+
/** `value`; throws `TypeError` unless it is a string. */
|
|
7
|
+
export function requireString(value: unknown, name: string): string {
|
|
8
|
+
if (typeof value !== 'string') throw invalidArgument('Must be a string', name);
|
|
9
|
+
return value;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/** `value`, `undefined` for `null`; throws `TypeError` unless it is a string or neither. */
|
|
13
|
+
export function optionalString(value: unknown, name: string): string | undefined {
|
|
14
|
+
return value === undefined || value === null ? undefined : requireString(value, name);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** `value`, `undefined` for `null`; throws `TypeError` unless it is an integer or neither. */
|
|
18
|
+
export function optionalInteger(value: unknown, name: string): number | undefined {
|
|
19
|
+
if (value === undefined || value === null) return undefined;
|
|
20
|
+
if (!Number.isInteger(value)) throw invalidArgument('Must be an integer', name);
|
|
21
|
+
return value as number;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** `value`, `undefined` for `null`; throws `TypeError` unless it is a boolean or neither. */
|
|
25
|
+
export function optionalBoolean(value: unknown, name: string): boolean | undefined {
|
|
26
|
+
if (value === undefined || value === null) return undefined;
|
|
27
|
+
if (typeof value !== 'boolean') throw invalidArgument('Must be a boolean', name);
|
|
28
|
+
return value;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** `value`; throws `TypeError` unless it is a function. */
|
|
32
|
+
export function requireFunction<F>(value: F, name: string): F {
|
|
33
|
+
if (typeof value !== 'function') throw invalidArgument('Must be a function', name);
|
|
34
|
+
return value;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** `value`, `undefined` for `null`; throws `TypeError` unless it is a function or neither. */
|
|
38
|
+
export function optionalFunction<F>(value: F | null | undefined, name: string): F | undefined {
|
|
39
|
+
return value === undefined || value === null ? undefined : requireFunction(value, name);
|
|
40
|
+
}
|
package/src/util/async.ts
CHANGED
|
@@ -37,12 +37,18 @@ export class Deferred<T> {
|
|
|
37
37
|
* A failing action does not stop the ones queued after it.
|
|
38
38
|
*/
|
|
39
39
|
export class SerialExecutor {
|
|
40
|
-
private last: Promise<
|
|
40
|
+
private last: Promise<void> = Promise.resolve();
|
|
41
41
|
|
|
42
|
-
/**
|
|
42
|
+
/**
|
|
43
|
+
* Runs `action` once the actions submitted before have finished; the result is that of `action`, which the executor
|
|
44
|
+
* does not keep.
|
|
45
|
+
*/
|
|
43
46
|
run<T>(action: () => Promise<T>): Promise<T> {
|
|
44
|
-
const result = this.last.then(action);
|
|
45
|
-
this.last = result.
|
|
47
|
+
const result = this.last.then(() => action());
|
|
48
|
+
this.last = result.then(
|
|
49
|
+
() => undefined,
|
|
50
|
+
() => undefined,
|
|
51
|
+
);
|
|
46
52
|
return result;
|
|
47
53
|
}
|
|
48
54
|
}
|
package/src/util/bytes.ts
CHANGED
|
@@ -56,14 +56,14 @@ export function fromCharCodes(codes: Uint8Array | Uint16Array): string {
|
|
|
56
56
|
}
|
|
57
57
|
|
|
58
58
|
const utf8Encoder = new TextEncoder();
|
|
59
|
-
const utf8Decoder = new TextDecoder('utf-8', { fatal: true
|
|
60
|
-
const lenientUtf8Decoder = new TextDecoder('utf-8'
|
|
59
|
+
const utf8Decoder = new TextDecoder('utf-8', { fatal: true });
|
|
60
|
+
const lenientUtf8Decoder = new TextDecoder('utf-8');
|
|
61
61
|
|
|
62
62
|
export function utf8Encode(text: string): Uint8Array {
|
|
63
63
|
return utf8Encoder.encode(text);
|
|
64
64
|
}
|
|
65
65
|
|
|
66
|
-
/** Decodes UTF-8; throws `FormatError` for malformed data unless `allowMalformed`. */
|
|
66
|
+
/** Decodes UTF-8, without a leading BOM; throws `FormatError` for malformed data unless `allowMalformed`. */
|
|
67
67
|
export function utf8Decode(bytes: Uint8Array, allowMalformed = false): string {
|
|
68
68
|
try {
|
|
69
69
|
return (allowMalformed ? lenientUtf8Decoder : utf8Decoder).decode(bytes);
|
package/src/util/dates.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// A value is either a calendar date, a local `Date` at midnight of which only the day counts, or the exact moment of a
|
|
2
|
-
// date with a time of day, which the bank states in German time (see `germanTime`)
|
|
3
|
-
// date without a valid time of day is not a moment.
|
|
2
|
+
// date with a time of day, which the bank states in German time (see `germanTime`) unless it gives the UTC offset. The
|
|
3
|
+
// parsers keep the two apart: a date without a valid time of day is not a moment. A time of day runs from 00:00:00 to
|
|
4
|
+
// 23:59:59, and 24:00:00, which ISO 8601 and XML Schema allow for the end of a day, is midnight of the next.
|
|
4
5
|
|
|
5
6
|
const hourMs = 3_600_000;
|
|
6
7
|
|
|
@@ -30,8 +31,11 @@ export function formatIsoDate(d: Date): string {
|
|
|
30
31
|
const fintsDate = /^(\d{4})(\d{2})(\d{2})$/;
|
|
31
32
|
const fintsTime = /^(\d{2})(\d{2})(\d{2})/;
|
|
32
33
|
const isoDate = /^(\d{4})-(\d{2})-(\d{2})/;
|
|
33
|
-
|
|
34
|
-
const
|
|
34
|
+
// The time of an XML Schema `dateTime`, whose UTC offset lies between -14:00 and +14:00.
|
|
35
|
+
const isoTime = /^(\d{2}):(\d{2}):(\d{2})(\.\d+)?(Z|[+-](?:(?:0\d|1[0-3]):[0-5]\d|14:00))?$/;
|
|
36
|
+
|
|
37
|
+
/** A date and time of day as a clock shows them, the month counted from 1. */
|
|
38
|
+
type Clock = [year: number, month: number, day: number, hour: number, minute: number, second: number];
|
|
35
39
|
|
|
36
40
|
/** Parses a FinTS 3.0 date `YYYYMMDD` as a calendar date; `undefined` if it is missing or invalid. */
|
|
37
41
|
export function parseFintsDate(date: string | undefined): Date | undefined {
|
|
@@ -43,52 +47,64 @@ export function parseFintsDate(date: string | undefined): Date | undefined {
|
|
|
43
47
|
* invalid.
|
|
44
48
|
*/
|
|
45
49
|
export function parseFintsDateTime(date: string | undefined, time: string | undefined): Date | undefined {
|
|
46
|
-
|
|
50
|
+
const clock = clockOn(parseDate(fintsDate, date), match(fintsTime, time));
|
|
51
|
+
return clock && germanTime(...clock);
|
|
47
52
|
}
|
|
48
53
|
|
|
49
54
|
/**
|
|
50
|
-
* Parses an ISO 8601 date `YYYY-MM-DD` as a calendar date, ignoring
|
|
51
|
-
* if it is missing or invalid.
|
|
55
|
+
* Parses an ISO 8601 date `YYYY-MM-DD` as a calendar date, ignoring what follows it, such as a time zone; `undefined`
|
|
56
|
+
* if it is missing or invalid. A `T` right after the date makes it a date and time, which must be one `parseIsoMoment`
|
|
57
|
+
* reads, and the date the day in Germany at that moment: for a time in UTC late in the evening, the next day.
|
|
52
58
|
*/
|
|
53
59
|
export function parseIsoDate(date: string | undefined): Date | undefined {
|
|
54
|
-
return parseDate(isoDate, date);
|
|
60
|
+
if (date?.charAt(10) !== 'T') return parseDate(isoDate, date);
|
|
61
|
+
const moment = parseIsoMoment(date);
|
|
62
|
+
return moment && germanDay(moment);
|
|
55
63
|
}
|
|
56
64
|
|
|
57
65
|
/**
|
|
58
66
|
* 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
|
|
60
|
-
* `undefined` if it is
|
|
67
|
+
* UTC offset (`Z` or `±hh:mm` up to ±14:00), as the moment it names, to the second: in German time where it states no
|
|
68
|
+
* offset. `undefined` if it is invalid.
|
|
61
69
|
*/
|
|
62
|
-
|
|
63
|
-
const
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
const
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
70
|
+
function parseIsoMoment(dateTime: string): Date | undefined {
|
|
71
|
+
const t = isoTime.exec(dateTime.slice(11));
|
|
72
|
+
const clock = clockOn(parseDate(isoDate, dateTime), t);
|
|
73
|
+
if (clock === undefined) return undefined;
|
|
74
|
+
const zone = t?.[5];
|
|
75
|
+
return zone === undefined ? germanTime(...clock) : new Date(utc(...clock) - offsetMs(zone));
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** The UTC offset `Z` or `±hh:mm` in milliseconds. */
|
|
79
|
+
function offsetMs(zone: string): number {
|
|
80
|
+
if (zone === 'Z') return 0;
|
|
81
|
+
const ms = (Number(zone.slice(1, 3)) * 60 + Number(zone.slice(4))) * 60_000;
|
|
82
|
+
return zone.startsWith('-') ? -ms : ms;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function match(pattern: RegExp, text: string | undefined): RegExpExecArray | null {
|
|
86
|
+
return text === undefined ? null : pattern.exec(text);
|
|
72
87
|
}
|
|
73
88
|
|
|
74
89
|
function parseDate(pattern: RegExp, date: string | undefined): Date | undefined {
|
|
75
|
-
const d =
|
|
90
|
+
const d = match(pattern, date);
|
|
76
91
|
return d ? validDate(Number(d[1]), Number(d[2]), Number(d[3])) : undefined;
|
|
77
92
|
}
|
|
78
93
|
|
|
79
|
-
/**
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
94
|
+
/**
|
|
95
|
+
* The time of day `t` on `day`, where `t` matched hour, minute and second, and maybe a fraction of a second after them;
|
|
96
|
+
* `undefined` if either is missing or the time is invalid.
|
|
97
|
+
*/
|
|
98
|
+
function clockOn(day: Date | undefined, t: RegExpExecArray | null): Clock | undefined {
|
|
99
|
+
if (day === undefined || !t) return undefined;
|
|
83
100
|
const [hour, minute, second] = [Number(t[1]), Number(t[2]), Number(t[3])];
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
return germanTime(day.getFullYear(), day.getMonth() + 1, day.getDate(), ...t);
|
|
101
|
+
// The fraction counts only here: 24:00:00.5 lies past the end of the day.
|
|
102
|
+
if (hour === 24 && minute === 0 && second === 0 && !/[1-9]/.test(t[4] ?? '')) {
|
|
103
|
+
const next = localDate(day.getFullYear(), day.getMonth() + 1, day.getDate() + 1);
|
|
104
|
+
return [next.getFullYear(), next.getMonth() + 1, next.getDate(), 0, 0, 0];
|
|
105
|
+
}
|
|
106
|
+
if (hour > 23 || minute > 59 || second > 59) return undefined;
|
|
107
|
+
return [day.getFullYear(), day.getMonth() + 1, day.getDate(), hour, minute, second];
|
|
92
108
|
}
|
|
93
109
|
|
|
94
110
|
/**
|
|
@@ -97,10 +113,8 @@ function moment(day: Date | undefined, timePattern: RegExp, time: string | undef
|
|
|
97
113
|
* fewer than six digits lost its leading zeros as a JSON number.
|
|
98
114
|
*/
|
|
99
115
|
export function parseUtcPlusOneDateTime(date: string | undefined, time: string | undefined): Date | undefined {
|
|
100
|
-
const
|
|
101
|
-
|
|
102
|
-
if (day === undefined || t === undefined) return undefined;
|
|
103
|
-
return new Date(utc(day.getFullYear(), day.getMonth() + 1, day.getDate(), ...t) - hourMs);
|
|
116
|
+
const clock = clockOn(parseDate(fintsDate, date), match(fintsTime, time?.padStart(6, '0')));
|
|
117
|
+
return clock && new Date(utc(...clock) - hourMs);
|
|
104
118
|
}
|
|
105
119
|
|
|
106
120
|
/**
|
|
@@ -117,7 +131,7 @@ function germanTime(year: number, month: number, day: number, hour: number, minu
|
|
|
117
131
|
}
|
|
118
132
|
|
|
119
133
|
/** The calendar day a German clock shows at `moment`, as a local `Date` at midnight like every calendar date. */
|
|
120
|
-
|
|
134
|
+
function germanDay(moment: Date): Date {
|
|
121
135
|
const utcMs = moment.getTime();
|
|
122
136
|
const clock = new Date(utcMs + (isSummerTime(utcMs) ? 2 : 1) * hourMs);
|
|
123
137
|
return localDate(clock.getUTCFullYear(), clock.getUTCMonth() + 1, clock.getUTCDate());
|
package/src/util/errors.ts
CHANGED
|
@@ -2,17 +2,22 @@
|
|
|
2
2
|
* Data that do not have the expected format, such as a malformed MT940 line or amount.
|
|
3
3
|
*
|
|
4
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.
|
|
5
|
+
* crash reports do not collect them. For the same reason `source` is a getter, not a field: `JSON.stringify` and
|
|
6
|
+
* Node.js's report of an uncaught error show an error's fields as they are.
|
|
6
7
|
*/
|
|
7
8
|
export class FormatError extends Error {
|
|
8
9
|
override name = 'FormatError';
|
|
9
10
|
|
|
10
|
-
|
|
11
|
-
readonly source: string | undefined;
|
|
11
|
+
readonly #source: string | undefined;
|
|
12
12
|
|
|
13
13
|
constructor(message: string, source?: string) {
|
|
14
14
|
super(message);
|
|
15
|
-
this
|
|
15
|
+
this.#source = source;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** The part of the data that is malformed, where the parser can tell it. */
|
|
19
|
+
get source(): string | undefined {
|
|
20
|
+
return this.#source;
|
|
16
21
|
}
|
|
17
22
|
}
|
|
18
23
|
|