@coderoftime/fints 0.1.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 +519 -1
- package/README.md +138 -40
- package/dist/client.d.ts +49 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +242 -0
- package/dist/client.js.map +1 -0
- package/dist/core/account.d.ts +46 -7
- package/dist/core/account.d.ts.map +1 -1
- package/dist/core/account.js +143 -24
- package/dist/core/account.js.map +1 -1
- package/dist/core/amount.d.ts +15 -4
- package/dist/core/amount.d.ts.map +1 -1
- package/dist/core/amount.js +46 -9
- package/dist/core/amount.js.map +1 -1
- package/dist/core/balance.d.ts +4 -0
- package/dist/core/balance.d.ts.map +1 -1
- package/dist/core/balance.js +11 -5
- package/dist/core/balance.js.map +1 -1
- package/dist/core/bank-info.d.ts +21 -4
- package/dist/core/bank-info.d.ts.map +1 -1
- package/dist/core/bank-info.js +26 -5
- package/dist/core/bank-info.js.map +1 -1
- package/dist/core/client.d.ts +81 -19
- package/dist/core/client.d.ts.map +1 -1
- package/dist/core/config.d.ts +40 -20
- package/dist/core/config.d.ts.map +1 -1
- package/dist/core/config.js +85 -30
- package/dist/core/config.js.map +1 -1
- package/dist/core/errors.d.ts +100 -26
- package/dist/core/errors.d.ts.map +1 -1
- package/dist/core/errors.js +136 -53
- package/dist/core/errors.js.map +1 -1
- package/dist/core/feedback.d.ts +11 -1
- package/dist/core/feedback.d.ts.map +1 -1
- package/dist/core/feedback.js +14 -2
- package/dist/core/feedback.js.map +1 -1
- package/dist/core/transaction.d.ts +11 -1
- package/dist/core/transaction.d.ts.map +1 -1
- package/dist/core/transaction.js +21 -4
- package/dist/core/transaction.js.map +1 -1
- package/dist/core/transport.d.ts +48 -6
- package/dist/core/transport.d.ts.map +1 -1
- package/dist/core/transport.js +126 -29
- package/dist/core/transport.js.map +1 -1
- package/dist/formats/camt.d.ts +28 -5
- package/dist/formats/camt.d.ts.map +1 -1
- package/dist/formats/camt.js +132 -50
- package/dist/formats/camt.js.map +1 -1
- package/dist/formats/mt940.d.ts +29 -5
- package/dist/formats/mt940.d.ts.map +1 -1
- package/dist/formats/mt940.js +209 -113
- 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 +28 -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 +9 -0
- package/dist/formats/sepa-purpose.d.ts.map +1 -1
- package/dist/formats/sepa-purpose.js +32 -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 +11 -11
- package/dist/session/dialog-runner.d.ts.map +1 -1
- package/dist/session/dialog-runner.js +34 -19
- 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 +170 -83
- 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 +41 -24
- package/dist/session/session-setup.d.ts.map +1 -1
- package/dist/session/session-setup.js +106 -63
- package/dist/session/session-setup.js.map +1 -1
- package/dist/session/session-state.d.ts +10 -6
- package/dist/session/session-state.d.ts.map +1 -1
- package/dist/session/session-state.js +72 -18
- package/dist/session/session-state.js.map +1 -1
- package/dist/session/signed-dialog.d.ts +83 -0
- package/dist/session/signed-dialog.d.ts.map +1 -0
- package/dist/session/signed-dialog.js +185 -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 +14 -5
- package/dist/session/user-prompts.d.ts.map +1 -1
- package/dist/session/user-prompts.js +53 -18
- package/dist/session/user-prompts.js.map +1 -1
- package/dist/tan/challenge.d.ts +47 -24
- package/dist/tan/challenge.d.ts.map +1 -1
- package/dist/tan/challenge.js +56 -51
- package/dist/tan/challenge.js.map +1 -1
- package/dist/tan/flicker.d.ts +11 -1
- package/dist/tan/flicker.d.ts.map +1 -1
- package/dist/tan/flicker.js +70 -24
- package/dist/tan/flicker.js.map +1 -1
- package/dist/tan/tan-image.d.ts +6 -2
- package/dist/tan/tan-image.d.ts.map +1 -1
- package/dist/tan/tan-image.js +14 -6
- package/dist/tan/tan-image.js.map +1 -1
- package/dist/tan/tan-models.d.ts +14 -2
- package/dist/tan/tan-models.d.ts.map +1 -1
- package/dist/tan/tan-models.js +24 -7
- 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 -3
- package/dist/util/async.d.ts.map +1 -1
- package/dist/util/async.js +6 -7
- package/dist/util/async.js.map +1 -1
- package/dist/util/bytes.d.ts +3 -4
- package/dist/util/bytes.d.ts.map +1 -1
- package/dist/util/bytes.js +13 -21
- 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 +6 -20
- package/dist/util/dates.d.ts.map +1 -1
- package/dist/util/dates.js +58 -28
- package/dist/util/dates.js.map +1 -1
- package/dist/util/errors.d.ts +13 -3
- package/dist/util/errors.d.ts.map +1 -1
- package/dist/util/errors.js +19 -6
- 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/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 +38 -5
- package/dist/util/text.d.ts.map +1 -1
- package/dist/util/text.js +90 -16
- package/dist/util/text.js.map +1 -1
- package/dist/util/xml.d.ts +9 -6
- package/dist/util/xml.d.ts.map +1 -1
- package/dist/util/xml.js +129 -56
- 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 +54 -0
- package/dist/v3/bank-connection.d.ts.map +1 -0
- package/dist/v3/bank-connection.js +118 -0
- package/dist/v3/bank-connection.js.map +1 -0
- package/dist/v3/dialog.d.ts +84 -46
- package/dist/v3/dialog.d.ts.map +1 -1
- package/dist/v3/dialog.js +174 -60
- package/dist/v3/dialog.js.map +1 -1
- package/dist/v3/message.d.ts +32 -11
- package/dist/v3/message.d.ts.map +1 -1
- package/dist/v3/message.js +144 -83
- package/dist/v3/message.js.map +1 -1
- package/dist/v3/parameter-store.d.ts +111 -0
- package/dist/v3/parameter-store.d.ts.map +1 -0
- package/dist/v3/parameter-store.js +197 -0
- package/dist/v3/parameter-store.js.map +1 -0
- package/dist/v3/parameters.d.ts +44 -17
- package/dist/v3/parameters.d.ts.map +1 -1
- package/dist/v3/parameters.js +136 -60
- package/dist/v3/parameters.js.map +1 -1
- package/dist/v3/protocol.d.ts +50 -50
- package/dist/v3/protocol.d.ts.map +1 -1
- package/dist/v3/protocol.js +91 -210
- 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 +128 -0
- package/dist/v3/queries.js.map +1 -0
- package/dist/v3/segments.d.ts +43 -19
- package/dist/v3/segments.d.ts.map +1 -1
- package/dist/v3/segments.js +84 -33
- package/dist/v3/segments.js.map +1 -1
- package/dist/v3/syntax.d.ts +68 -13
- package/dist/v3/syntax.d.ts.map +1 -1
- package/dist/v3/syntax.js +183 -102
- package/dist/v3/syntax.js.map +1 -1
- package/package.json +10 -6
- package/src/client.ts +293 -0
- package/src/core/account.ts +153 -24
- package/src/core/amount.ts +45 -10
- package/src/core/balance.ts +12 -5
- package/src/core/bank-info.ts +29 -5
- package/src/core/client.ts +83 -19
- package/src/core/config.ts +95 -35
- package/src/core/errors.ts +148 -54
- package/src/core/feedback.ts +16 -2
- package/src/core/transaction.ts +22 -4
- package/src/core/transport.ts +153 -32
- package/src/formats/camt.ts +153 -45
- package/src/formats/mt940.ts +244 -122
- package/src/formats/readers.ts +28 -31
- package/src/formats/record-budget.ts +18 -0
- package/src/formats/sepa-purpose.ts +29 -2
- package/src/index.ts +4 -2
- package/src/session/app-approval.ts +51 -38
- package/src/session/dialog-runner.ts +61 -31
- package/src/session/login-guard.ts +189 -87
- package/src/session/sepa-accounts.ts +63 -45
- package/src/session/session-setup.ts +105 -68
- package/src/session/session-state.ts +74 -23
- package/src/session/signed-dialog.ts +222 -0
- package/src/session/tan-selection.ts +50 -39
- package/src/session/user-prompts.ts +62 -18
- package/src/tan/challenge.ts +87 -64
- package/src/tan/flicker.ts +72 -23
- package/src/tan/tan-image.ts +15 -6
- package/src/tan/tan-models.ts +26 -7
- package/src/util/arguments.ts +40 -0
- package/src/util/async.ts +10 -9
- package/src/util/bytes.ts +14 -20
- package/src/util/call-context.ts +3 -2
- package/src/util/dates.ts +64 -33
- package/src/util/errors.ts +22 -6
- package/src/util/immutable-map.ts +15 -12
- package/src/util/inspect.ts +14 -0
- package/src/util/json.ts +16 -0
- package/src/util/text.ts +87 -15
- package/src/util/xml.ts +130 -52
- package/src/v3/account-extension.ts +7 -12
- package/src/v3/bank-connection.ts +126 -0
- package/src/v3/dialog.ts +186 -92
- package/src/v3/message.ts +140 -79
- package/src/v3/parameter-store.ts +226 -0
- package/src/v3/parameters.ts +166 -66
- package/src/v3/protocol.ts +103 -253
- package/src/v3/queries.ts +198 -0
- package/src/v3/segments.ts +86 -31
- package/src/v3/syntax.ts +172 -93
- 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
|
@@ -1,47 +1,50 @@
|
|
|
1
|
-
/**
|
|
1
|
+
/**
|
|
2
|
+
* A map that cannot be changed, also not at runtime: it has no `set`, `delete` or `clear`, and no other code reaches
|
|
3
|
+
* the map that holds its entries.
|
|
4
|
+
*/
|
|
2
5
|
export class ImmutableMap<K, V> implements ReadonlyMap<K, V> {
|
|
3
|
-
|
|
6
|
+
readonly #map: Map<K, V>;
|
|
4
7
|
|
|
5
8
|
constructor(entries?: Iterable<readonly [K, V]>) {
|
|
6
|
-
this
|
|
9
|
+
this.#map = new Map(entries);
|
|
7
10
|
}
|
|
8
11
|
|
|
9
12
|
get size(): number {
|
|
10
|
-
return this
|
|
13
|
+
return this.#map.size;
|
|
11
14
|
}
|
|
12
15
|
|
|
13
16
|
get(key: K): V | undefined {
|
|
14
|
-
return this
|
|
17
|
+
return this.#map.get(key);
|
|
15
18
|
}
|
|
16
19
|
|
|
17
20
|
has(key: K): boolean {
|
|
18
|
-
return this
|
|
21
|
+
return this.#map.has(key);
|
|
19
22
|
}
|
|
20
23
|
|
|
21
24
|
forEach(callback: (value: V, key: K, map: ReadonlyMap<K, V>) => void): void {
|
|
22
|
-
this
|
|
25
|
+
this.#map.forEach((value, key) => {
|
|
23
26
|
callback(value, key, this);
|
|
24
27
|
});
|
|
25
28
|
}
|
|
26
29
|
|
|
27
30
|
entries(): MapIterator<[K, V]> {
|
|
28
|
-
return this
|
|
31
|
+
return this.#map.entries();
|
|
29
32
|
}
|
|
30
33
|
|
|
31
34
|
keys(): MapIterator<K> {
|
|
32
|
-
return this
|
|
35
|
+
return this.#map.keys();
|
|
33
36
|
}
|
|
34
37
|
|
|
35
38
|
values(): MapIterator<V> {
|
|
36
|
-
return this
|
|
39
|
+
return this.#map.values();
|
|
37
40
|
}
|
|
38
41
|
|
|
39
42
|
[Symbol.iterator](): MapIterator<[K, V]> {
|
|
40
|
-
return this
|
|
43
|
+
return this.#map.entries();
|
|
41
44
|
}
|
|
42
45
|
|
|
43
46
|
/** A plain object of the entries, for JSON and for display. */
|
|
44
47
|
toJSON(): Record<string, V> {
|
|
45
|
-
return Object.fromEntries([...this
|
|
48
|
+
return Object.fromEntries([...this.#map].map(([key, value]) => [String(key), value]));
|
|
46
49
|
}
|
|
47
50
|
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The key of the method by which Node.js's `util.inspect`, and so `console.log`, shows an object, as do Deno and Bun:
|
|
3
|
+
* its registered name, so that no Node.js import is needed.
|
|
4
|
+
*/
|
|
5
|
+
export const inspectCustom = Symbol.for('nodejs.util.inspect.custom');
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* `value` frozen, with `show` as what `util.inspect` shows of it instead of its fields. The method is no enumerable
|
|
9
|
+
* property, so that `JSON.stringify`, `structuredClone` and comparisons in tests see the same plain object as before.
|
|
10
|
+
*/
|
|
11
|
+
export function frozenWithInspect<T extends object>(value: T, show: (value: T) => string): Readonly<T> {
|
|
12
|
+
Object.defineProperty(value, inspectCustom, { value: () => show(value) });
|
|
13
|
+
return Object.freeze(value);
|
|
14
|
+
}
|
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,20 @@
|
|
|
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
|
+
const whiteSpaceCharacter = /\s/;
|
|
6
7
|
|
|
7
|
-
/**
|
|
8
|
+
/**
|
|
9
|
+
* Decodes text from the bank, which may be UTF-8 (with or without BOM) or ISO 8859-1; ASCII reads the same in both.
|
|
10
|
+
*
|
|
11
|
+
* UTF-8 comes first, since some banks send it. ISO 8859-1 text that happens to be valid UTF-8, such as `ß°`, reads as
|
|
12
|
+
* UTF-8 then: a rare misreading of text shown, as values sent back to the bank are read byte for byte
|
|
13
|
+
* (`Segment.verbatim`).
|
|
14
|
+
*/
|
|
8
15
|
export function decodeBankText(bytes: Uint8Array): string {
|
|
9
16
|
const hasBom = bytes.length >= 3 && bytes[0] === 0xef && bytes[1] === 0xbb && bytes[2] === 0xbf;
|
|
10
17
|
const content = hasBom ? bytes.subarray(3) : bytes;
|
|
11
|
-
if (content.every((b) => b < 0x80)) return latin1Decode(content);
|
|
12
18
|
try {
|
|
13
19
|
return utf8Decode(content);
|
|
14
20
|
} catch (error) {
|
|
@@ -44,11 +50,18 @@ const windows1252High = [
|
|
|
44
50
|
0x0153, 0x009d, 0x017e, 0x0178,
|
|
45
51
|
];
|
|
46
52
|
|
|
47
|
-
/**
|
|
53
|
+
/**
|
|
54
|
+
* Decodes Windows-1252, which matches ISO 8859-1 except for `€`, typographic quotes and a few letters.
|
|
55
|
+
*
|
|
56
|
+
* @internal Exported for the tests.
|
|
57
|
+
*/
|
|
48
58
|
export function decodeWindows1252(bytes: Uint8Array): string {
|
|
49
|
-
|
|
50
|
-
for (
|
|
51
|
-
|
|
59
|
+
const codes = new Uint16Array(bytes.length);
|
|
60
|
+
for (let i = 0; i < bytes.length; i++) {
|
|
61
|
+
const b = bytes[i]!;
|
|
62
|
+
codes[i] = b >= 0x80 && b < 0xa0 ? windows1252High[b - 0x80]! : b;
|
|
63
|
+
}
|
|
64
|
+
return fromCharCodes(codes);
|
|
52
65
|
}
|
|
53
66
|
|
|
54
67
|
/**
|
|
@@ -59,6 +72,63 @@ export function maskedId(id: string): string {
|
|
|
59
72
|
return id.length <= 4 ? '…' : `…${id.substring(id.length - 4)}`;
|
|
60
73
|
}
|
|
61
74
|
|
|
75
|
+
/** `count` and `noun`, in the plural unless `count` is 1, such as `3 transactions`. */
|
|
76
|
+
export function counted(count: number, noun: string): string {
|
|
77
|
+
return `${count} ${noun}${count === 1 ? '' : 's'}`;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** Whether `value` fits into ISO 8859-1, the FinTS 3.0 character set. */
|
|
81
|
+
export function isLatin1(value: string): boolean {
|
|
82
|
+
for (let i = 0; i < value.length; i++) if (value.charCodeAt(i) > 0xff) return false;
|
|
83
|
+
return true;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Whether the UTF-16 code unit `code` is a blank: white space to `String.prototype.trim`, or U+0085 (NEL), which it
|
|
88
|
+
* keeps although Unicode counts it as white space. ISO 8859-1 bank data hold it as the byte 0x85.
|
|
89
|
+
*/
|
|
90
|
+
function isBlank(code: number): boolean {
|
|
91
|
+
// Below U+1680, the first white space beyond these, without a regular expression, which takes many times as long.
|
|
92
|
+
if (code < 0x1680) return code === 0x20 || (code >= 0x09 && code <= 0x0d) || code === 0x85 || code === 0xa0;
|
|
93
|
+
return whiteSpaceCharacter.test(String.fromCharCode(code));
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** `text` without blanks (`isBlank`) at its `start` and its `end`. */
|
|
97
|
+
export function trimBlanks(text: string, { start = true, end = true } = {}): string {
|
|
98
|
+
let first = 0;
|
|
99
|
+
let last = text.length;
|
|
100
|
+
if (start) while (first < last && isBlank(text.charCodeAt(first))) first++;
|
|
101
|
+
if (end) while (last > first && isBlank(text.charCodeAt(last - 1))) last--;
|
|
102
|
+
return text.substring(first, last);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/** `text` without blanks around it (`trimBlanks`); `undefined` if that leaves nothing. */
|
|
106
|
+
export function nonBlank(text: string | undefined): string | undefined {
|
|
107
|
+
const trimmed = text === undefined ? undefined : trimBlanks(text);
|
|
108
|
+
return trimmed === '' ? undefined : trimmed;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* The character the numeric character reference `name` names, such as `#x41` or `#65`; `undefined` for any other name,
|
|
113
|
+
* and for a number that is no character as XML defines them: `NUL`, half a surrogate pair, or beyond Unicode.
|
|
114
|
+
*/
|
|
115
|
+
export function characterReference(name: string): string | undefined {
|
|
116
|
+
const m = /^#(?:x([0-9a-fA-F]+)|([0-9]+))$/.exec(name);
|
|
117
|
+
if (!m) return undefined;
|
|
118
|
+
const code = m[1] === undefined ? parseInt(m[2]!, 10) : parseInt(m[1], 16);
|
|
119
|
+
return code > 0 && (code < 0xd800 || code > 0xdfff) && code <= 0x10ffff ? String.fromCodePoint(code) : undefined;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* `text` with its full-width characters, which East Asian input methods type, as their ASCII counterparts: U+FF01 to
|
|
124
|
+
* U+FF5E as U+0021 to U+007E, and the ideographic space U+3000 as a blank.
|
|
125
|
+
*/
|
|
126
|
+
export function withAsciiForms(text: string): string {
|
|
127
|
+
return text
|
|
128
|
+
.replace(/[\uff01-\uff5e]/g, (c) => String.fromCharCode(c.charCodeAt(0) - 0xfee0))
|
|
129
|
+
.replaceAll('\u3000', ' ');
|
|
130
|
+
}
|
|
131
|
+
|
|
62
132
|
/** `text` without spaces and line breaks. */
|
|
63
133
|
export function withoutWhitespace(text: string): string {
|
|
64
134
|
return text.replace(whitespace, '');
|
|
@@ -69,14 +139,16 @@ export function decodeBase64Lines(text: string): Uint8Array {
|
|
|
69
139
|
return base64Decode(withoutWhitespace(text));
|
|
70
140
|
}
|
|
71
141
|
|
|
72
|
-
/**
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
/** The integer `text` states, decimal digits with an optional sign and blanks around; `undefined` for anything else. */
|
|
142
|
+
/**
|
|
143
|
+
* The integer `text` states, decimal digits with an optional sign and blanks around, `-0` as 0; `undefined` for
|
|
144
|
+
* anything else, such as hex, and for a value beyond `Number.MAX_SAFE_INTEGER`, which a number holds only
|
|
145
|
+
* approximately.
|
|
146
|
+
*/
|
|
78
147
|
export function tryParseInt(text: string | undefined): number | undefined {
|
|
79
148
|
if (text === undefined) return undefined;
|
|
80
|
-
const
|
|
81
|
-
|
|
149
|
+
const integer = trimBlanks(text);
|
|
150
|
+
if (!/^[+-]?\d+$/.test(integer)) return undefined;
|
|
151
|
+
const value = Number(integer);
|
|
152
|
+
if (!Number.isSafeInteger(value)) return undefined;
|
|
153
|
+
return value === 0 ? 0 : value;
|
|
82
154
|
}
|
package/src/util/xml.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { FormatError } from './errors.js';
|
|
2
|
+
import { characterReference, nonBlank } from './text.js';
|
|
2
3
|
|
|
3
4
|
/**
|
|
4
5
|
* An element of a parsed XML document, looked up by local name: camt documents come with and without namespace
|
|
@@ -10,7 +11,7 @@ export class XmlElement {
|
|
|
10
11
|
constructor(
|
|
11
12
|
/** The name without namespace prefix. */
|
|
12
13
|
readonly localName: string,
|
|
13
|
-
/** The attributes by name
|
|
14
|
+
/** The attributes by name, namespace prefix and all. */
|
|
14
15
|
readonly attributes: ReadonlyMap<string, string>,
|
|
15
16
|
) {}
|
|
16
17
|
|
|
@@ -23,13 +24,14 @@ export class XmlElement {
|
|
|
23
24
|
return this.children.map((c) => (c instanceof XmlElement ? c.innerText : c)).join('');
|
|
24
25
|
}
|
|
25
26
|
|
|
26
|
-
|
|
27
|
-
|
|
27
|
+
/** The attribute called `name`: an unprefixed name finds no prefixed attribute, which is of another namespace. */
|
|
28
|
+
getAttribute(name: string): string | undefined {
|
|
29
|
+
return this.attributes.get(name);
|
|
28
30
|
}
|
|
29
31
|
|
|
30
32
|
/** The first child element called `localName`. */
|
|
31
33
|
child(localName: string): XmlElement | undefined {
|
|
32
|
-
return this.
|
|
34
|
+
return this.children.find((c): c is XmlElement => c instanceof XmlElement && c.localName === localName);
|
|
33
35
|
}
|
|
34
36
|
|
|
35
37
|
/** The element reached by following child names along `path`; this element for an empty path. */
|
|
@@ -39,71 +41,122 @@ export class XmlElement {
|
|
|
39
41
|
|
|
40
42
|
/** The trimmed text of the element at `path`, or `undefined` if it is missing or empty. */
|
|
41
43
|
textAt(path: readonly string[]): string | undefined {
|
|
42
|
-
|
|
43
|
-
return text === undefined || text === '' ? undefined : text;
|
|
44
|
+
return nonBlank(this.at(path)?.innerText);
|
|
44
45
|
}
|
|
45
46
|
|
|
46
|
-
/**
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
const visit = (element: XmlElement): void => {
|
|
50
|
-
for (const c of element.childElements) {
|
|
51
|
-
if (c.localName === localName) found.push(c);
|
|
52
|
-
visit(c);
|
|
53
|
-
}
|
|
54
|
-
};
|
|
55
|
-
visit(this);
|
|
56
|
-
return found;
|
|
47
|
+
/** The child elements called `localName`, in document order. */
|
|
48
|
+
childrenNamed(localName: string): XmlElement[] {
|
|
49
|
+
return this.children.filter((c): c is XmlElement => c instanceof XmlElement && c.localName === localName);
|
|
57
50
|
}
|
|
58
51
|
}
|
|
59
52
|
|
|
60
|
-
|
|
61
|
-
const
|
|
53
|
+
/** The characters an XML name starts with (XML 1.0, production 4). */
|
|
54
|
+
const nameStartCharacters = String.raw`:A-Z_a-z\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u02FF\u0370-\u037D\u037F-\u1FFF\u200C-\u200D\u2070-\u218F\u2C00-\u2FEF\u3001-\uD7FF\uF900-\uFDCF\uFDF0-\uFFFD\u{10000}-\u{EFFFF}`;
|
|
55
|
+
/**
|
|
56
|
+
* An XML name (XML 1.0, production 5). The combining marks lead the class of the characters after the first: after
|
|
57
|
+
* another one, ESLint's `no-misleading-character-class` takes them for a combined character.
|
|
58
|
+
*/
|
|
59
|
+
const xmlName = String.raw`[${nameStartCharacters}][\u0300-\u036F${nameStartCharacters}\-.0-9\u00B7\u203F-\u2040]*`;
|
|
60
|
+
const namePattern = new RegExp(xmlName, 'uy');
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* White space to Unicode, U+0085 (NEL) among them, which separates the parts of a tag, and `<?xml` of the rest of the
|
|
64
|
+
* XML declaration, as the Dart package has it.
|
|
65
|
+
*/
|
|
66
|
+
const blank = String.raw`[\s\u0085]`;
|
|
67
|
+
const blankPattern = new RegExp(blank);
|
|
68
|
+
const attributePattern = new RegExp(
|
|
69
|
+
String.raw`${blank}+(${xmlName})${blank}*=${blank}*(?:"([^"<]*)"|'([^'<]*)')`,
|
|
70
|
+
'uy',
|
|
71
|
+
);
|
|
72
|
+
/** The start of the XML declaration, whose content is not read. */
|
|
73
|
+
const declarationPattern = new RegExp(String.raw`<\?xml(?:${blank}|\?>)`, 'y');
|
|
74
|
+
/** The text of a reference after its `&`, up to the `;` that ends it. */
|
|
75
|
+
const referenceNamePattern = /[^;&\s]*/y;
|
|
62
76
|
|
|
63
77
|
/** The match of the sticky `pattern` at `position` in `text`. */
|
|
64
78
|
function matchAt(pattern: RegExp, text: string, position: number): RegExpExecArray | null {
|
|
65
79
|
pattern.lastIndex = position;
|
|
66
80
|
return pattern.exec(text);
|
|
67
81
|
}
|
|
68
|
-
const predefinedEntities
|
|
82
|
+
const predefinedEntities = new Map([
|
|
83
|
+
['amp', '&'],
|
|
84
|
+
['lt', '<'],
|
|
85
|
+
['gt', '>'],
|
|
86
|
+
['quot', '"'],
|
|
87
|
+
['apos', "'"],
|
|
88
|
+
]);
|
|
89
|
+
|
|
90
|
+
/** Deepest nesting of elements the parser takes: camt needs fewer than 20 levels, and `innerText` recurses. */
|
|
91
|
+
const maxDepth = 100;
|
|
69
92
|
|
|
93
|
+
/**
|
|
94
|
+
* Most nodes besides text a document may hold, unless it is long enough for one node per `charactersPerNode`
|
|
95
|
+
* characters: real camt spends 16 to 40 characters on each, while nodes as short as `<a/>` take a hundred times their
|
|
96
|
+
* length in memory.
|
|
97
|
+
*/
|
|
98
|
+
const maxNodes = 500_000;
|
|
99
|
+
const charactersPerNode = 16;
|
|
100
|
+
|
|
101
|
+
const noAttributes: ReadonlyMap<string, string> = new Map();
|
|
102
|
+
|
|
103
|
+
/** The name without namespace prefix; `:a` has none, but is no unprefixed `a` either. */
|
|
70
104
|
function localNameOf(qualified: string): string {
|
|
71
105
|
const colon = qualified.indexOf(':');
|
|
72
|
-
return colon
|
|
106
|
+
return colon > 0 ? qualified.substring(colon + 1) : qualified;
|
|
73
107
|
}
|
|
74
108
|
|
|
75
|
-
/**
|
|
109
|
+
/**
|
|
110
|
+
* Resolves the character and predefined entity references in `text`; throws `FormatError` for any other, at the first.
|
|
111
|
+
* A global `replace` would find all of them before it looked at the first: a gigabyte for megabytes of `&`.
|
|
112
|
+
*/
|
|
76
113
|
function decodeEntities(text: string): string {
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
114
|
+
let decoded = '';
|
|
115
|
+
let position = 0;
|
|
116
|
+
for (let ampersand = text.indexOf('&'); ampersand >= 0; ampersand = text.indexOf('&', position)) {
|
|
117
|
+
const name = matchAt(referenceNamePattern, text, ampersand + 1)![0];
|
|
118
|
+
const end = ampersand + 1 + name.length;
|
|
119
|
+
if (text.charAt(end) !== ';') throw new FormatError('Invalid XML: unterminated entity reference');
|
|
120
|
+
decoded += text.substring(position, ampersand) + resolved(name, text.substring(ampersand, end + 1));
|
|
121
|
+
position = end + 1;
|
|
122
|
+
}
|
|
123
|
+
return position === 0 ? text : decoded + text.substring(position);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/** The text the `reference` called `name` stands for. */
|
|
127
|
+
function resolved(name: string, reference: string): string {
|
|
128
|
+
if (name.startsWith('#')) {
|
|
129
|
+
const character = characterReference(name);
|
|
130
|
+
if (character === undefined) throw new FormatError('Invalid XML character reference', reference);
|
|
131
|
+
return character;
|
|
132
|
+
}
|
|
133
|
+
const value = predefinedEntities.get(name);
|
|
134
|
+
if (value === undefined) throw new FormatError('Invalid XML: unknown entity', reference);
|
|
135
|
+
return value;
|
|
92
136
|
}
|
|
93
137
|
|
|
94
138
|
/**
|
|
95
|
-
* Parses an XML document and returns its root element; throws `FormatError` if it is not well-formed
|
|
139
|
+
* Parses an XML document and returns its root element; throws `FormatError` if it is not well-formed, nests elements
|
|
140
|
+
* deeper than 100 levels, or holds more than 500,000 nodes besides text (elements, attributes, comments, processing
|
|
141
|
+
* instructions, the XML declaration and CDATA sections) or one per 16 characters, whichever is more.
|
|
96
142
|
*
|
|
97
143
|
* A document type declaration is refused: bank data have none, and its entities could expand without limit.
|
|
98
144
|
*/
|
|
99
145
|
export function parseXml(xml: string): XmlElement {
|
|
100
146
|
let position = 0;
|
|
101
147
|
const stack: XmlElement[] = [];
|
|
148
|
+
const openNames: string[] = [];
|
|
102
149
|
let root: XmlElement | undefined;
|
|
150
|
+
let declared = false;
|
|
151
|
+
let nodes = 0;
|
|
152
|
+
const nodeLimit = Math.max(maxNodes, Math.floor(xml.length / charactersPerNode));
|
|
103
153
|
|
|
104
154
|
const fail = (reason: string): never => {
|
|
105
155
|
throw new FormatError(`Invalid XML: ${reason} at position ${position}`);
|
|
106
156
|
};
|
|
157
|
+
const countNode = (): void => {
|
|
158
|
+
if (++nodes > nodeLimit) fail(`more than ${nodeLimit} nodes`);
|
|
159
|
+
};
|
|
107
160
|
const skipPast = (terminator: string, what: string): void => {
|
|
108
161
|
const end = xml.indexOf(terminator, position);
|
|
109
162
|
if (end < 0) fail(`unterminated ${what}`);
|
|
@@ -115,18 +168,37 @@ export function parseXml(xml: string): XmlElement {
|
|
|
115
168
|
const textEnd = lt < 0 ? xml.length : lt;
|
|
116
169
|
if (textEnd > position) {
|
|
117
170
|
const text = xml.substring(position, textEnd);
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
171
|
+
// Blanks are dropped only where they follow no text, as between elements: kept, they would double the nodes of a
|
|
172
|
+
// document with line breaks. Between two texts they are content, and a counted node separates the two.
|
|
173
|
+
if (nonBlank(text) !== undefined || typeof stack.at(-1)?.children.at(-1) === 'string') {
|
|
174
|
+
(stack.at(-1) ?? fail('text outside the root element')).children.push(decodeEntities(text));
|
|
175
|
+
}
|
|
121
176
|
position = textEnd;
|
|
122
177
|
continue;
|
|
123
178
|
}
|
|
124
179
|
if (xml.startsWith('<?', position)) {
|
|
180
|
+
countNode();
|
|
181
|
+
if (matchAt(declarationPattern, xml, position)) {
|
|
182
|
+
if (root) fail('XML declaration inside or after the root element');
|
|
183
|
+
if (declared) fail('a second XML declaration');
|
|
184
|
+
declared = true;
|
|
185
|
+
} else {
|
|
186
|
+
position += 2;
|
|
187
|
+
const target = matchAt(namePattern, xml, position)?.[0] ?? fail('missing processing instruction target');
|
|
188
|
+
position += target.length;
|
|
189
|
+
if (!xml.startsWith('?>', position) && !blankPattern.test(xml.charAt(position))) {
|
|
190
|
+
fail('malformed processing instruction');
|
|
191
|
+
}
|
|
192
|
+
}
|
|
125
193
|
skipPast('?>', 'processing instruction');
|
|
126
194
|
} else if (xml.startsWith('<!--', position)) {
|
|
195
|
+
countNode();
|
|
196
|
+
// Past the `<!--`, whose dashes end no comment: `<!-->` starts one.
|
|
197
|
+
position += 4;
|
|
127
198
|
skipPast('-->', 'comment');
|
|
128
199
|
} else if (xml.startsWith('<![CDATA[', position)) {
|
|
129
200
|
const parent = stack.at(-1) ?? fail('CDATA outside the root element');
|
|
201
|
+
countNode();
|
|
130
202
|
const start = position + 9;
|
|
131
203
|
skipPast(']]>', 'CDATA section');
|
|
132
204
|
parent.children.push(xml.substring(start, position - 3));
|
|
@@ -136,37 +208,43 @@ export function parseXml(xml: string): XmlElement {
|
|
|
136
208
|
position += 2;
|
|
137
209
|
const name = matchAt(namePattern, xml, position)?.[0] ?? fail('missing end tag name');
|
|
138
210
|
position += name.length;
|
|
139
|
-
while (
|
|
211
|
+
while (blankPattern.test(xml.charAt(position))) position++;
|
|
140
212
|
if (xml.charAt(position) !== '>') fail('malformed end tag');
|
|
141
213
|
position++;
|
|
142
|
-
|
|
143
|
-
if (
|
|
214
|
+
stack.pop();
|
|
215
|
+
if (openNames.pop() !== name) fail(`unexpected end tag ${name}`);
|
|
144
216
|
} else {
|
|
217
|
+
if (stack.length >= maxDepth) fail(`elements nested deeper than ${maxDepth} levels`);
|
|
218
|
+
countNode();
|
|
145
219
|
position++;
|
|
146
220
|
const name = matchAt(namePattern, xml, position)?.[0] ?? fail('missing element name');
|
|
147
221
|
position += name.length;
|
|
148
|
-
|
|
222
|
+
let attributes: Map<string, string> | undefined;
|
|
149
223
|
for (;;) {
|
|
150
224
|
const attribute = matchAt(attributePattern, xml, position);
|
|
151
225
|
if (!attribute) break;
|
|
152
226
|
position += attribute[0].length;
|
|
227
|
+
countNode();
|
|
153
228
|
const attributeName = attribute[1]!;
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
229
|
+
attributes ??= new Map();
|
|
230
|
+
if (attributes.has(attributeName)) fail(`duplicate attribute ${attributeName}`);
|
|
231
|
+
attributes.set(attributeName, decodeEntities(attribute[2] ?? attribute[3] ?? ''));
|
|
157
232
|
}
|
|
158
|
-
while (
|
|
233
|
+
while (blankPattern.test(xml.charAt(position))) position++;
|
|
159
234
|
const selfClosing = xml.startsWith('/>', position);
|
|
160
235
|
if (!selfClosing && xml.charAt(position) !== '>') fail(`malformed start tag ${name}`);
|
|
161
236
|
position += selfClosing ? 2 : 1;
|
|
162
|
-
const element = new XmlElement(localNameOf(name), attributes);
|
|
237
|
+
const element = new XmlElement(localNameOf(name), attributes ?? noAttributes);
|
|
163
238
|
const parent = stack.at(-1);
|
|
164
239
|
if (parent) parent.children.push(element);
|
|
165
240
|
else if (root) fail('a second root element');
|
|
166
241
|
else root = element;
|
|
167
|
-
if (!selfClosing)
|
|
242
|
+
if (!selfClosing) {
|
|
243
|
+
stack.push(element);
|
|
244
|
+
openNames.push(name);
|
|
245
|
+
}
|
|
168
246
|
}
|
|
169
247
|
}
|
|
170
|
-
if (stack.length > 0) fail(`unclosed element ${
|
|
248
|
+
if (stack.length > 0) fail(`unclosed element ${openNames.at(-1)!}`);
|
|
171
249
|
return root ?? fail('no root element');
|
|
172
250
|
}
|
|
@@ -4,7 +4,9 @@ import { AccountDataStatus, type Account } from '../core/account.js';
|
|
|
4
4
|
import { Amount, withMark } from '../core/amount.js';
|
|
5
5
|
import { Balance } from '../core/balance.js';
|
|
6
6
|
import { parseFintsDate, parseUtcPlusOneDateTime } from '../util/dates.js';
|
|
7
|
-
import { FormatError } from '../util/errors.js';
|
|
7
|
+
import { fail, FormatError } from '../util/errors.js';
|
|
8
|
+
import { isJsonObject } from '../util/json.js';
|
|
9
|
+
import { nonBlank } from '../util/text.js';
|
|
8
10
|
|
|
9
11
|
/** A booked or pending balance of the extension: credit/debit mark, value, date and time. */
|
|
10
12
|
interface Saldo {
|
|
@@ -47,7 +49,7 @@ export class AccountExtension {
|
|
|
47
49
|
static tryParse(json: string): AccountExtension | undefined {
|
|
48
50
|
try {
|
|
49
51
|
const decoded: unknown = JSON.parse(json);
|
|
50
|
-
return
|
|
52
|
+
return isJsonObject(decoded) ? AccountExtension.read(new Json(decoded)) : undefined;
|
|
51
53
|
} catch (error) {
|
|
52
54
|
if (error instanceof FormatError || error instanceof SyntaxError) return undefined;
|
|
53
55
|
throw error;
|
|
@@ -76,6 +78,7 @@ export class AccountExtension {
|
|
|
76
78
|
if (b === undefined || !this.balanceComplete) return undefined;
|
|
77
79
|
return new Balance({
|
|
78
80
|
account,
|
|
81
|
+
productName: account.productName,
|
|
79
82
|
booked: b.booked.amount,
|
|
80
83
|
bookedAsOf: b.booked.date,
|
|
81
84
|
bookedAsOfTime: b.booked.time,
|
|
@@ -129,14 +132,6 @@ function moment(json: Json, defaultTime?: string): Date | undefined {
|
|
|
129
132
|
return parseUtcPlusOneDateTime(json.string('Date'), time) ?? fail('Extension date or time is malformed');
|
|
130
133
|
}
|
|
131
134
|
|
|
132
|
-
function fail(message: string): never {
|
|
133
|
-
throw new FormatError(message);
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
function isObject(value: unknown): value is Record<string, unknown> {
|
|
137
|
-
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
138
|
-
}
|
|
139
|
-
|
|
140
135
|
/**
|
|
141
136
|
* A JSON object of the extension, read by key without regard to case: the specification itself spells some keys
|
|
142
137
|
* differently (`OverDraftLim` and `OverdraftLim`). Values of the wrong type are a `FormatError`.
|
|
@@ -156,7 +151,7 @@ class Json {
|
|
|
156
151
|
string(key: string): string | undefined {
|
|
157
152
|
const value = this.value(key);
|
|
158
153
|
if (value === undefined || value === null) return undefined;
|
|
159
|
-
if (typeof value === 'string') return
|
|
154
|
+
if (typeof value === 'string') return nonBlank(value);
|
|
160
155
|
if (typeof value === 'number') return String(value);
|
|
161
156
|
throw new FormatError(`Extension element "${key}" is not text`);
|
|
162
157
|
}
|
|
@@ -172,7 +167,7 @@ class Json {
|
|
|
172
167
|
object(key: string): Json | undefined {
|
|
173
168
|
const value = this.value(key);
|
|
174
169
|
if (value === undefined || value === null) return undefined;
|
|
175
|
-
if (
|
|
170
|
+
if (isJsonObject(value)) return new Json(value);
|
|
176
171
|
throw new FormatError(`Extension element "${key}" is not an object`);
|
|
177
172
|
}
|
|
178
173
|
|