@coderoftime/fints 0.0.0-stage → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +11 -0
- package/LICENSE +28 -0
- package/README.md +156 -2
- package/dist/core/account.d.ts +140 -0
- package/dist/core/account.d.ts.map +1 -0
- package/dist/core/account.js +174 -0
- package/dist/core/account.js.map +1 -0
- package/dist/core/amount.d.ts +62 -0
- package/dist/core/amount.d.ts.map +1 -0
- package/dist/core/amount.js +189 -0
- package/dist/core/amount.js.map +1 -0
- package/dist/core/balance.d.ts +54 -0
- package/dist/core/balance.d.ts.map +1 -0
- package/dist/core/balance.js +53 -0
- package/dist/core/balance.js.map +1 -0
- package/dist/core/bank-info.d.ts +49 -0
- package/dist/core/bank-info.d.ts.map +1 -0
- package/dist/core/bank-info.js +52 -0
- package/dist/core/bank-info.js.map +1 -0
- package/dist/core/client.d.ts +126 -0
- package/dist/core/client.d.ts.map +1 -0
- package/dist/core/client.js +2 -0
- package/dist/core/client.js.map +1 -0
- package/dist/core/config.d.ts +68 -0
- package/dist/core/config.d.ts.map +1 -0
- package/dist/core/config.js +113 -0
- package/dist/core/config.js.map +1 -0
- package/dist/core/errors.d.ts +91 -0
- package/dist/core/errors.d.ts.map +1 -0
- package/dist/core/errors.js +182 -0
- package/dist/core/errors.js.map +1 -0
- package/dist/core/feedback.d.ts +69 -0
- package/dist/core/feedback.d.ts.map +1 -0
- package/dist/core/feedback.js +124 -0
- package/dist/core/feedback.js.map +1 -0
- package/dist/core/transaction.d.ts +98 -0
- package/dist/core/transaction.d.ts.map +1 -0
- package/dist/core/transaction.js +91 -0
- package/dist/core/transaction.js.map +1 -0
- package/dist/core/transport.d.ts +37 -0
- package/dist/core/transport.d.ts.map +1 -0
- package/dist/core/transport.js +92 -0
- package/dist/core/transport.js.map +1 -0
- package/dist/formats/camt.d.ts +12 -0
- package/dist/formats/camt.d.ts.map +1 -0
- package/dist/formats/camt.js +159 -0
- package/dist/formats/camt.js.map +1 -0
- package/dist/formats/mt940.d.ts +42 -0
- package/dist/formats/mt940.d.ts.map +1 -0
- package/dist/formats/mt940.js +297 -0
- package/dist/formats/mt940.js.map +1 -0
- package/dist/formats/readers.d.ts +11 -0
- package/dist/formats/readers.d.ts.map +1 -0
- package/dist/formats/readers.js +47 -0
- package/dist/formats/readers.js.map +1 -0
- package/dist/formats/sepa-purpose.d.ts +22 -0
- package/dist/formats/sepa-purpose.d.ts.map +1 -0
- package/dist/formats/sepa-purpose.js +68 -0
- package/dist/formats/sepa-purpose.js.map +1 -0
- package/dist/index.d.ts +28 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +27 -0
- package/dist/index.js.map +1 -0
- package/dist/session/app-approval.d.ts +60 -0
- package/dist/session/app-approval.d.ts.map +1 -0
- package/dist/session/app-approval.js +215 -0
- package/dist/session/app-approval.js.map +1 -0
- package/dist/session/bank-connection.d.ts +28 -0
- package/dist/session/bank-connection.d.ts.map +1 -0
- package/dist/session/bank-connection.js +48 -0
- package/dist/session/bank-connection.js.map +1 -0
- package/dist/session/dialog-runner.d.ts +34 -0
- package/dist/session/dialog-runner.d.ts.map +1 -0
- package/dist/session/dialog-runner.js +112 -0
- package/dist/session/dialog-runner.js.map +1 -0
- package/dist/session/login-guard.d.ts +73 -0
- package/dist/session/login-guard.d.ts.map +1 -0
- package/dist/session/login-guard.js +109 -0
- package/dist/session/login-guard.js.map +1 -0
- package/dist/session/parameter-data.d.ts +83 -0
- package/dist/session/parameter-data.d.ts.map +1 -0
- package/dist/session/parameter-data.js +68 -0
- package/dist/session/parameter-data.js.map +1 -0
- package/dist/session/pin-tan-client.d.ts +54 -0
- package/dist/session/pin-tan-client.d.ts.map +1 -0
- package/dist/session/pin-tan-client.js +153 -0
- package/dist/session/pin-tan-client.js.map +1 -0
- package/dist/session/pin-tan-dialog.d.ts +71 -0
- package/dist/session/pin-tan-dialog.d.ts.map +1 -0
- package/dist/session/pin-tan-dialog.js +124 -0
- package/dist/session/pin-tan-dialog.js.map +1 -0
- package/dist/session/protocol.d.ts +209 -0
- package/dist/session/protocol.d.ts.map +1 -0
- package/dist/session/protocol.js +75 -0
- package/dist/session/protocol.js.map +1 -0
- package/dist/session/response-handler.d.ts +48 -0
- package/dist/session/response-handler.d.ts.map +1 -0
- package/dist/session/response-handler.js +121 -0
- package/dist/session/response-handler.js.map +1 -0
- package/dist/session/sepa-accounts.d.ts +34 -0
- package/dist/session/sepa-accounts.d.ts.map +1 -0
- package/dist/session/sepa-accounts.js +96 -0
- package/dist/session/sepa-accounts.js.map +1 -0
- package/dist/session/session-setup.d.ts +72 -0
- package/dist/session/session-setup.d.ts.map +1 -0
- package/dist/session/session-setup.js +160 -0
- package/dist/session/session-setup.js.map +1 -0
- package/dist/session/session-state.d.ts +47 -0
- package/dist/session/session-state.d.ts.map +1 -0
- package/dist/session/session-state.js +147 -0
- package/dist/session/session-state.js.map +1 -0
- package/dist/session/tan-selection.d.ts +88 -0
- package/dist/session/tan-selection.d.ts.map +1 -0
- package/dist/session/tan-selection.js +178 -0
- package/dist/session/tan-selection.js.map +1 -0
- package/dist/session/user-prompts.d.ts +61 -0
- package/dist/session/user-prompts.d.ts.map +1 -0
- package/dist/session/user-prompts.js +164 -0
- package/dist/session/user-prompts.js.map +1 -0
- package/dist/tan/challenge.d.ts +114 -0
- package/dist/tan/challenge.d.ts.map +1 -0
- package/dist/tan/challenge.js +185 -0
- package/dist/tan/challenge.js.map +1 -0
- package/dist/tan/flicker.d.ts +36 -0
- package/dist/tan/flicker.d.ts.map +1 -0
- package/dist/tan/flicker.js +262 -0
- package/dist/tan/flicker.js.map +1 -0
- package/dist/tan/tan-image.d.ts +21 -0
- package/dist/tan/tan-image.d.ts.map +1 -0
- package/dist/tan/tan-image.js +41 -0
- package/dist/tan/tan-image.js.map +1 -0
- package/dist/tan/tan-models.d.ts +145 -0
- package/dist/tan/tan-models.d.ts.map +1 -0
- package/dist/tan/tan-models.js +148 -0
- package/dist/tan/tan-models.js.map +1 -0
- package/dist/util/async.d.ts +24 -0
- package/dist/util/async.d.ts.map +1 -0
- package/dist/util/async.js +49 -0
- package/dist/util/async.js.map +1 -0
- package/dist/util/bytes.d.ts +23 -0
- package/dist/util/bytes.d.ts.map +1 -0
- package/dist/util/bytes.js +136 -0
- package/dist/util/bytes.js.map +1 -0
- package/dist/util/call-context.d.ts +12 -0
- package/dist/util/call-context.d.ts.map +1 -0
- package/dist/util/call-context.js +29 -0
- package/dist/util/call-context.js.map +1 -0
- package/dist/util/dates.d.ts +62 -0
- package/dist/util/dates.d.ts.map +1 -0
- package/dist/util/dates.js +169 -0
- package/dist/util/dates.js.map +1 -0
- package/dist/util/errors.d.ts +9 -0
- package/dist/util/errors.d.ts.map +1 -0
- package/dist/util/errors.js +15 -0
- package/dist/util/errors.js.map +1 -0
- package/dist/util/immutable-map.d.ts +16 -0
- package/dist/util/immutable-map.d.ts.map +1 -0
- package/dist/util/immutable-map.js +38 -0
- package/dist/util/immutable-map.js.map +1 -0
- package/dist/util/json.d.ts +13 -0
- package/dist/util/json.d.ts.map +1 -0
- package/dist/util/json.js +44 -0
- package/dist/util/json.js.map +1 -0
- package/dist/util/text.d.ts +20 -0
- package/dist/util/text.d.ts.map +1 -0
- package/dist/util/text.js +78 -0
- package/dist/util/text.js.map +1 -0
- package/dist/util/xml.d.ts +35 -0
- package/dist/util/xml.d.ts.map +1 -0
- package/dist/util/xml.js +183 -0
- package/dist/util/xml.js.map +1 -0
- package/dist/v3/account-extension.d.ts +25 -0
- package/dist/v3/account-extension.d.ts.map +1 -0
- package/dist/v3/account-extension.js +176 -0
- package/dist/v3/account-extension.js.map +1 -0
- package/dist/v3/client.d.ts +24 -0
- package/dist/v3/client.d.ts.map +1 -0
- package/dist/v3/client.js +16 -0
- package/dist/v3/client.js.map +1 -0
- package/dist/v3/dialog.d.ts +78 -0
- package/dist/v3/dialog.d.ts.map +1 -0
- package/dist/v3/dialog.js +138 -0
- package/dist/v3/dialog.js.map +1 -0
- package/dist/v3/message.d.ts +72 -0
- package/dist/v3/message.d.ts.map +1 -0
- package/dist/v3/message.js +259 -0
- package/dist/v3/message.js.map +1 -0
- package/dist/v3/parameters.d.ts +79 -0
- package/dist/v3/parameters.d.ts.map +1 -0
- package/dist/v3/parameters.js +396 -0
- package/dist/v3/parameters.js.map +1 -0
- package/dist/v3/protocol.d.ts +68 -0
- package/dist/v3/protocol.d.ts.map +1 -0
- package/dist/v3/protocol.js +263 -0
- package/dist/v3/protocol.js.map +1 -0
- package/dist/v3/segments.d.ts +89 -0
- package/dist/v3/segments.d.ts.map +1 -0
- package/dist/v3/segments.js +283 -0
- package/dist/v3/segments.js.map +1 -0
- package/dist/v3/syntax.d.ts +76 -0
- package/dist/v3/syntax.d.ts.map +1 -0
- package/dist/v3/syntax.js +301 -0
- package/dist/v3/syntax.js.map +1 -0
- package/package.json +63 -4
- package/src/core/account.ts +250 -0
- package/src/core/amount.ts +209 -0
- package/src/core/balance.ts +84 -0
- package/src/core/bank-info.ts +66 -0
- package/src/core/client.ts +149 -0
- package/src/core/config.ts +141 -0
- package/src/core/errors.ts +203 -0
- package/src/core/feedback.ts +137 -0
- package/src/core/transaction.ts +156 -0
- package/src/core/transport.ts +114 -0
- package/src/formats/camt.ts +169 -0
- package/src/formats/mt940.ts +333 -0
- package/src/formats/readers.ts +56 -0
- package/src/formats/sepa-purpose.ts +75 -0
- package/src/index.ts +68 -0
- package/src/session/app-approval.ts +265 -0
- package/src/session/bank-connection.ts +51 -0
- package/src/session/dialog-runner.ts +119 -0
- package/src/session/login-guard.ts +135 -0
- package/src/session/parameter-data.ts +137 -0
- package/src/session/pin-tan-client.ts +186 -0
- package/src/session/pin-tan-dialog.ts +160 -0
- package/src/session/protocol.ts +311 -0
- package/src/session/response-handler.ts +130 -0
- package/src/session/sepa-accounts.ts +97 -0
- package/src/session/session-setup.ts +165 -0
- package/src/session/session-state.ts +170 -0
- package/src/session/tan-selection.ts +192 -0
- package/src/session/user-prompts.ts +174 -0
- package/src/tan/challenge.ts +252 -0
- package/src/tan/flicker.ts +270 -0
- package/src/tan/tan-image.ts +36 -0
- package/src/tan/tan-models.ts +247 -0
- package/src/util/async.ts +53 -0
- package/src/util/bytes.ts +135 -0
- package/src/util/call-context.ts +40 -0
- package/src/util/dates.ts +193 -0
- package/src/util/errors.ts +16 -0
- package/src/util/immutable-map.ts +47 -0
- package/src/util/json.ts +51 -0
- package/src/util/text.ts +82 -0
- package/src/util/xml.ts +172 -0
- package/src/v3/account-extension.ts +190 -0
- package/src/v3/client.ts +29 -0
- package/src/v3/dialog.ts +199 -0
- package/src/v3/message.ts +321 -0
- package/src/v3/parameters.ts +451 -0
- package/src/v3/protocol.ts +331 -0
- package/src/v3/segments.ts +331 -0
- package/src/v3/syntax.ts +307 -0
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { base64Encode, latin1Decode } from '../util/bytes.js';
|
|
2
|
+
|
|
3
|
+
const imageMimeType = /^image\/[a-zA-Z0-9.+-]+$/;
|
|
4
|
+
|
|
5
|
+
/** An image challenge (photoTAN, QR-TAN) with its MIME type. */
|
|
6
|
+
export class TanImage {
|
|
7
|
+
constructor(
|
|
8
|
+
/** The media type of `bytes`, for example `image/png`. */
|
|
9
|
+
readonly mimeType: string,
|
|
10
|
+
/** The encoded image. */
|
|
11
|
+
readonly bytes: Uint8Array,
|
|
12
|
+
) {}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Reads the bank's image container: a two-byte length and the MIME type, then a two-byte length and the image.
|
|
16
|
+
*
|
|
17
|
+
* `undefined` unless the MIME type is an image type and the container holds the whole image.
|
|
18
|
+
*/
|
|
19
|
+
static tryParse(data: Uint8Array): TanImage | undefined {
|
|
20
|
+
if (data.length < 4) return undefined;
|
|
21
|
+
const mimeLength = (data[0]! << 8) | data[1]!;
|
|
22
|
+
if (mimeLength === 0 || data.length < 2 + mimeLength + 2) return undefined;
|
|
23
|
+
const mime = latin1Decode(data.subarray(2, 2 + mimeLength));
|
|
24
|
+
if (!imageMimeType.test(mime)) return undefined;
|
|
25
|
+
const offset = 2 + mimeLength;
|
|
26
|
+
const imageLength = (data[offset]! << 8) | data[offset + 1]!;
|
|
27
|
+
const start = offset + 2;
|
|
28
|
+
if (start + imageLength > data.length) return undefined;
|
|
29
|
+
return new TanImage(mime, data.subarray(start, start + imageLength));
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** The image as a `data:` URL, ready for the `src` of an `<img>`. */
|
|
33
|
+
toDataUrl(): string {
|
|
34
|
+
return `data:${this.mimeType};base64,${base64Encode(this.bytes)}`;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
import { maskedId } from '../util/text.js';
|
|
2
|
+
|
|
3
|
+
/** Whether a TAN method needs the name of a TAN medium. */
|
|
4
|
+
export type TanMediumRequirement =
|
|
5
|
+
/** The method takes no medium name. */
|
|
6
|
+
| 'notAllowed'
|
|
7
|
+
/** The method takes a medium name but works without one. */
|
|
8
|
+
| 'optional'
|
|
9
|
+
/** The method needs the name of the medium. */
|
|
10
|
+
| 'required';
|
|
11
|
+
|
|
12
|
+
/** Reads the FinTS code: `0` not allowed, `1` optional, `2` required. */
|
|
13
|
+
export function tanMediumRequirementOf(code: string | undefined): TanMediumRequirement {
|
|
14
|
+
return code === '1' ? 'optional' : code === '2' ? 'required' : 'notAllowed';
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** What `TanMethod` takes; the bank states the details it knows. */
|
|
18
|
+
export interface TanMethodInit {
|
|
19
|
+
securityFunction: string;
|
|
20
|
+
name: string;
|
|
21
|
+
technicalId?: string | undefined;
|
|
22
|
+
dkMethod?: string | undefined;
|
|
23
|
+
dkVersion?: string | undefined;
|
|
24
|
+
maxTanLength?: number | undefined;
|
|
25
|
+
numericTan?: boolean;
|
|
26
|
+
returnValueText?: string | undefined;
|
|
27
|
+
maxChallengeLength?: number | undefined;
|
|
28
|
+
mediumRequirement?: TanMediumRequirement;
|
|
29
|
+
activeMediaCount?: number | undefined;
|
|
30
|
+
structuredChallenge?: boolean;
|
|
31
|
+
decoupled?: boolean;
|
|
32
|
+
decoupledMaxPolls?: number | undefined;
|
|
33
|
+
decoupledFirstPollDelaySeconds?: number | undefined;
|
|
34
|
+
decoupledPollIntervalSeconds?: number | undefined;
|
|
35
|
+
decoupledManualConfirmation?: boolean;
|
|
36
|
+
decoupledAutomaticPolling?: boolean;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** A TAN procedure offered by the bank (FinTS "Sicherheitsfunktion"). */
|
|
40
|
+
export class TanMethod {
|
|
41
|
+
/** Bank-specific code of the procedure, for example `942`; `999` is the one-step procedure. */
|
|
42
|
+
readonly securityFunction: string;
|
|
43
|
+
|
|
44
|
+
/** The name the bank gives the procedure, for example `pushTAN`. */
|
|
45
|
+
readonly name: string;
|
|
46
|
+
|
|
47
|
+
/** The bank's technical identification of the procedure. */
|
|
48
|
+
readonly technicalId: string | undefined;
|
|
49
|
+
|
|
50
|
+
/** DK procedure family such as `HHD1.4`, `mobileTAN` or `Decoupled`. */
|
|
51
|
+
readonly dkMethod: string | undefined;
|
|
52
|
+
|
|
53
|
+
/** Version of the `dkMethod`. */
|
|
54
|
+
readonly dkVersion: string | undefined;
|
|
55
|
+
|
|
56
|
+
/** Longest TAN the user may enter. */
|
|
57
|
+
readonly maxTanLength: number | undefined;
|
|
58
|
+
|
|
59
|
+
/** Whether the TAN consists of digits only. */
|
|
60
|
+
readonly numericTan: boolean;
|
|
61
|
+
|
|
62
|
+
/** The bank's label for the value the user sends back, for example `TAN`. */
|
|
63
|
+
readonly returnValueText: string | undefined;
|
|
64
|
+
|
|
65
|
+
/** Longest challenge text the bank sends for this procedure. */
|
|
66
|
+
readonly maxChallengeLength: number | undefined;
|
|
67
|
+
|
|
68
|
+
/** Whether a TAN medium name goes with this procedure. */
|
|
69
|
+
readonly mediumRequirement: TanMediumRequirement;
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Most TAN media a user can have active at once for this procedure, as the bank's parameters state it ("Anzahl
|
|
73
|
+
* unterstützter aktiver TAN-Medien"); not how many the user has.
|
|
74
|
+
*
|
|
75
|
+
* Where it is above 1 and `needsMediumName`, each message names the medium, since the user may have several.
|
|
76
|
+
*/
|
|
77
|
+
readonly activeMediaCount: number | undefined;
|
|
78
|
+
|
|
79
|
+
/** Whether the challenge text is structured (marked up) rather than plain. */
|
|
80
|
+
readonly structuredChallenge: boolean;
|
|
81
|
+
|
|
82
|
+
/** Whether approval happens in a separate app instead of entering a TAN. */
|
|
83
|
+
readonly decoupled: boolean;
|
|
84
|
+
|
|
85
|
+
/** Most status requests the bank allows for a decoupled approval; 0 allows any number. */
|
|
86
|
+
readonly decoupledMaxPolls: number | undefined;
|
|
87
|
+
|
|
88
|
+
/** How long to wait before the first status request of a decoupled approval, in seconds. */
|
|
89
|
+
readonly decoupledFirstPollDelaySeconds: number | undefined;
|
|
90
|
+
|
|
91
|
+
/** How long to wait between two status requests of a decoupled approval, in seconds. */
|
|
92
|
+
readonly decoupledPollIntervalSeconds: number | undefined;
|
|
93
|
+
|
|
94
|
+
/** Whether the user may report the approval, rather than the client asking the bank by itself. */
|
|
95
|
+
readonly decoupledManualConfirmation: boolean;
|
|
96
|
+
|
|
97
|
+
/** Whether the bank allows the client to ask for the approval by itself. */
|
|
98
|
+
readonly decoupledAutomaticPolling: boolean;
|
|
99
|
+
|
|
100
|
+
constructor(init: TanMethodInit) {
|
|
101
|
+
this.securityFunction = init.securityFunction;
|
|
102
|
+
this.name = init.name;
|
|
103
|
+
this.technicalId = init.technicalId;
|
|
104
|
+
this.dkMethod = init.dkMethod;
|
|
105
|
+
this.dkVersion = init.dkVersion;
|
|
106
|
+
this.maxTanLength = init.maxTanLength;
|
|
107
|
+
this.numericTan = init.numericTan ?? true;
|
|
108
|
+
this.returnValueText = init.returnValueText;
|
|
109
|
+
this.maxChallengeLength = init.maxChallengeLength;
|
|
110
|
+
this.mediumRequirement = init.mediumRequirement ?? 'notAllowed';
|
|
111
|
+
this.activeMediaCount = init.activeMediaCount;
|
|
112
|
+
this.structuredChallenge = init.structuredChallenge ?? false;
|
|
113
|
+
this.decoupled = init.decoupled ?? false;
|
|
114
|
+
this.decoupledMaxPolls = init.decoupledMaxPolls;
|
|
115
|
+
this.decoupledFirstPollDelaySeconds = init.decoupledFirstPollDelaySeconds;
|
|
116
|
+
this.decoupledPollIntervalSeconds = init.decoupledPollIntervalSeconds;
|
|
117
|
+
this.decoupledManualConfirmation = init.decoupledManualConfirmation ?? true;
|
|
118
|
+
this.decoupledAutomaticPolling = init.decoupledAutomaticPolling ?? true;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/** The one-step procedure (PIN only, no TAN). */
|
|
122
|
+
static readonly oneStep = new TanMethod({ securityFunction: '999', name: 'One-step (PIN only)' });
|
|
123
|
+
|
|
124
|
+
/** Whether this is the one-step procedure. */
|
|
125
|
+
get isOneStep(): boolean {
|
|
126
|
+
return this.securityFunction === TanMethod.oneStep.securityFunction;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/** Whether the bank needs the name of a TAN medium for this procedure. */
|
|
130
|
+
get needsMediumName(): boolean {
|
|
131
|
+
return this.mediumRequirement === 'required';
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
toString(): string {
|
|
135
|
+
return `TanMethod(${this.securityFunction}, ${this.name})`;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/** State of a TAN medium as reported by the bank. */
|
|
140
|
+
export type TanMediumStatus =
|
|
141
|
+
/** The medium is in use. */
|
|
142
|
+
| 'active'
|
|
143
|
+
/** The medium can be used but is not the active one. */
|
|
144
|
+
| 'available'
|
|
145
|
+
/** The medium is the active follow-up card. */
|
|
146
|
+
| 'activeFollowUpCard'
|
|
147
|
+
/** The medium is an available follow-up card. */
|
|
148
|
+
| 'availableFollowUpCard'
|
|
149
|
+
/** The bank states no status, or one that is not known. */
|
|
150
|
+
| 'unknown';
|
|
151
|
+
|
|
152
|
+
/** Reads the FinTS code `1` to `4`. */
|
|
153
|
+
export function tanMediumStatusOf(code: string | undefined): TanMediumStatus {
|
|
154
|
+
switch (code) {
|
|
155
|
+
case '1':
|
|
156
|
+
return 'active';
|
|
157
|
+
case '2':
|
|
158
|
+
return 'available';
|
|
159
|
+
case '3':
|
|
160
|
+
return 'activeFollowUpCard';
|
|
161
|
+
case '4':
|
|
162
|
+
return 'availableFollowUpCard';
|
|
163
|
+
default:
|
|
164
|
+
return 'unknown';
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/** What `TanMedium` takes; the bank states the details it knows. */
|
|
169
|
+
export interface TanMediumInit {
|
|
170
|
+
mediumClass: string;
|
|
171
|
+
status: TanMediumStatus;
|
|
172
|
+
name?: string | undefined;
|
|
173
|
+
securityFunction?: string | undefined;
|
|
174
|
+
cardNumber?: string | undefined;
|
|
175
|
+
cardSequenceNumber?: string | undefined;
|
|
176
|
+
mobileNumber?: string | undefined;
|
|
177
|
+
mobileNumberMasked?: string | undefined;
|
|
178
|
+
validFrom?: Date | undefined;
|
|
179
|
+
validUntil?: Date | undefined;
|
|
180
|
+
lastUsed?: Date | undefined;
|
|
181
|
+
activatedOn?: Date | undefined;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/** A TAN device registered for the user, such as a phone or TAN generator. */
|
|
185
|
+
export class TanMedium {
|
|
186
|
+
/** `G` TAN generator, `M` mobile phone, `L` TAN list, `S` secoder, `B` bilateral. */
|
|
187
|
+
readonly mediumClass: string;
|
|
188
|
+
|
|
189
|
+
/** Whether the medium is in use. */
|
|
190
|
+
readonly status: TanMediumStatus;
|
|
191
|
+
|
|
192
|
+
/** The name the medium is registered under; this is what the bank wants back to pick the medium. */
|
|
193
|
+
readonly name: string | undefined;
|
|
194
|
+
|
|
195
|
+
/** The TAN procedure the medium belongs to (HITAB version 5). */
|
|
196
|
+
readonly securityFunction: string | undefined;
|
|
197
|
+
|
|
198
|
+
/** Card number of a TAN generator. */
|
|
199
|
+
readonly cardNumber: string | undefined;
|
|
200
|
+
|
|
201
|
+
/** Sequence number of the card of a TAN generator. */
|
|
202
|
+
readonly cardSequenceNumber: string | undefined;
|
|
203
|
+
|
|
204
|
+
/** Phone number of a mobile phone medium, if the bank states it in full. */
|
|
205
|
+
readonly mobileNumber: string | undefined;
|
|
206
|
+
|
|
207
|
+
/** Phone number with the middle digits hidden. */
|
|
208
|
+
readonly mobileNumberMasked: string | undefined;
|
|
209
|
+
|
|
210
|
+
/** The medium is valid from this date. */
|
|
211
|
+
readonly validFrom: Date | undefined;
|
|
212
|
+
|
|
213
|
+
/** The medium is valid until this date. */
|
|
214
|
+
readonly validUntil: Date | undefined;
|
|
215
|
+
|
|
216
|
+
/** When the medium was used last. */
|
|
217
|
+
readonly lastUsed: Date | undefined;
|
|
218
|
+
|
|
219
|
+
/** When the medium was activated. */
|
|
220
|
+
readonly activatedOn: Date | undefined;
|
|
221
|
+
|
|
222
|
+
constructor(init: TanMediumInit) {
|
|
223
|
+
this.mediumClass = init.mediumClass;
|
|
224
|
+
this.status = init.status;
|
|
225
|
+
this.name = init.name;
|
|
226
|
+
this.securityFunction = init.securityFunction;
|
|
227
|
+
this.cardNumber = init.cardNumber;
|
|
228
|
+
this.cardSequenceNumber = init.cardSequenceNumber;
|
|
229
|
+
this.mobileNumber = init.mobileNumber;
|
|
230
|
+
this.mobileNumberMasked = init.mobileNumberMasked;
|
|
231
|
+
this.validFrom = init.validFrom;
|
|
232
|
+
this.validUntil = init.validUntil;
|
|
233
|
+
this.lastUsed = init.lastUsed;
|
|
234
|
+
this.activatedOn = init.activatedOn;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/** Whether the medium is the one in use. */
|
|
238
|
+
get isActive(): boolean {
|
|
239
|
+
return this.status === 'active' || this.status === 'activeFollowUpCard';
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/** The name, or the last digits of the card number, so that logs and crash reports do not collect the number. */
|
|
243
|
+
toString(): string {
|
|
244
|
+
const label = this.name ?? (this.cardNumber === undefined ? this.mediumClass : maskedId(this.cardNumber));
|
|
245
|
+
return `TanMedium(${label}, ${this.status})`;
|
|
246
|
+
}
|
|
247
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/** A promise with its resolve and reject functions, which tells whether it has settled. */
|
|
2
|
+
export class Deferred<T> {
|
|
3
|
+
readonly promise: Promise<T>;
|
|
4
|
+
private resolveFn!: (value: T) => void;
|
|
5
|
+
private rejectFn!: (error: unknown) => void;
|
|
6
|
+
private settled = false;
|
|
7
|
+
|
|
8
|
+
constructor() {
|
|
9
|
+
this.promise = new Promise<T>((resolve, reject) => {
|
|
10
|
+
this.resolveFn = resolve;
|
|
11
|
+
this.rejectFn = reject;
|
|
12
|
+
});
|
|
13
|
+
// A rejection nobody waits for is not an unhandled one: whoever awaits the promise still gets it.
|
|
14
|
+
this.promise.catch(() => undefined);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
get isCompleted(): boolean {
|
|
18
|
+
return this.settled;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
resolve(value: T): void {
|
|
22
|
+
if (this.settled) return;
|
|
23
|
+
this.settled = true;
|
|
24
|
+
this.resolveFn(value);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
reject(error: unknown): void {
|
|
28
|
+
if (this.settled) return;
|
|
29
|
+
this.settled = true;
|
|
30
|
+
this.rejectFn(error);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Runs asynchronous actions one after another, in the order they were submitted.
|
|
36
|
+
*
|
|
37
|
+
* A failing action does not stop the ones queued after it.
|
|
38
|
+
*/
|
|
39
|
+
export class SerialExecutor {
|
|
40
|
+
private last: Promise<unknown> = Promise.resolve();
|
|
41
|
+
|
|
42
|
+
/** Runs `action` once the actions submitted before have finished; the result is that of `action`. */
|
|
43
|
+
run<T>(action: () => Promise<T>): Promise<T> {
|
|
44
|
+
const result = this.last.then(action);
|
|
45
|
+
this.last = result.catch(() => undefined);
|
|
46
|
+
return result;
|
|
47
|
+
}
|
|
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
|
+
}
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import { FormatError } from './errors.js';
|
|
2
|
+
|
|
3
|
+
/** The `parts` joined into one array. */
|
|
4
|
+
export function concatBytes(parts: readonly Uint8Array[]): Uint8Array {
|
|
5
|
+
let length = 0;
|
|
6
|
+
for (const part of parts) length += part.length;
|
|
7
|
+
const result = new Uint8Array(length);
|
|
8
|
+
let offset = 0;
|
|
9
|
+
for (const part of parts) {
|
|
10
|
+
result.set(part, offset);
|
|
11
|
+
offset += part.length;
|
|
12
|
+
}
|
|
13
|
+
return result;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** Collects bytes, like a growing buffer. */
|
|
17
|
+
export class BytesBuilder {
|
|
18
|
+
private readonly parts: Uint8Array[] = [];
|
|
19
|
+
private size = 0;
|
|
20
|
+
|
|
21
|
+
get length(): number {
|
|
22
|
+
return this.size;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
add(bytes: Uint8Array | readonly number[]): void {
|
|
26
|
+
const part = bytes instanceof Uint8Array ? bytes : Uint8Array.from(bytes);
|
|
27
|
+
this.parts.push(part);
|
|
28
|
+
this.size += part.length;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
addByte(byte: number): void {
|
|
32
|
+
this.add(Uint8Array.of(byte));
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
takeBytes(): Uint8Array {
|
|
36
|
+
const result = concatBytes(this.parts);
|
|
37
|
+
this.parts.length = 0;
|
|
38
|
+
this.size = 0;
|
|
39
|
+
return result;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** ISO 8859-1 bytes of `text`, which must contain no character beyond U+00FF. */
|
|
44
|
+
export function latin1Encode(text: string): Uint8Array {
|
|
45
|
+
const bytes = new Uint8Array(text.length);
|
|
46
|
+
for (let i = 0; i < text.length; i++) bytes[i] = text.charCodeAt(i) & 0xff;
|
|
47
|
+
return bytes;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** The text of ISO 8859-1 `bytes`. */
|
|
51
|
+
export function latin1Decode(bytes: Uint8Array): string {
|
|
52
|
+
let text = '';
|
|
53
|
+
// In chunks, since a spread of a large array exceeds the argument limit of some engines.
|
|
54
|
+
for (let i = 0; i < bytes.length; i += 0x8000) {
|
|
55
|
+
text += String.fromCharCode(...bytes.subarray(i, i + 0x8000));
|
|
56
|
+
}
|
|
57
|
+
return text;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const utf8Encoder = new TextEncoder();
|
|
61
|
+
|
|
62
|
+
export function utf8Encode(text: string): Uint8Array {
|
|
63
|
+
return utf8Encoder.encode(text);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** Decodes UTF-8; throws `FormatError` for malformed data unless `allowMalformed`. */
|
|
67
|
+
export function utf8Decode(bytes: Uint8Array, allowMalformed = false): string {
|
|
68
|
+
try {
|
|
69
|
+
return new TextDecoder('utf-8', { fatal: !allowMalformed, ignoreBOM: true }).decode(bytes);
|
|
70
|
+
} catch {
|
|
71
|
+
throw new FormatError('Invalid UTF-8');
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const base64Alphabet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';
|
|
76
|
+
const base64Values = (() => {
|
|
77
|
+
const values = new Int16Array(128).fill(-1);
|
|
78
|
+
for (let i = 0; i < base64Alphabet.length; i++) values[base64Alphabet.charCodeAt(i)] = i;
|
|
79
|
+
// The URL-safe alphabet as well.
|
|
80
|
+
values['-'.charCodeAt(0)] = 62;
|
|
81
|
+
values['_'.charCodeAt(0)] = 63;
|
|
82
|
+
return values;
|
|
83
|
+
})();
|
|
84
|
+
|
|
85
|
+
export function base64Encode(bytes: Uint8Array): string {
|
|
86
|
+
let out = '';
|
|
87
|
+
let i = 0;
|
|
88
|
+
for (; i + 2 < bytes.length; i += 3) {
|
|
89
|
+
const n = (bytes[i]! << 16) | (bytes[i + 1]! << 8) | bytes[i + 2]!;
|
|
90
|
+
out +=
|
|
91
|
+
base64Alphabet[n >> 18]! +
|
|
92
|
+
base64Alphabet[(n >> 12) & 63]! +
|
|
93
|
+
base64Alphabet[(n >> 6) & 63]! +
|
|
94
|
+
base64Alphabet[n & 63]!;
|
|
95
|
+
}
|
|
96
|
+
const rest = bytes.length - i;
|
|
97
|
+
if (rest === 1) {
|
|
98
|
+
const n = bytes[i]! << 16;
|
|
99
|
+
out += base64Alphabet[n >> 18]! + base64Alphabet[(n >> 12) & 63]! + '==';
|
|
100
|
+
} else if (rest === 2) {
|
|
101
|
+
const n = (bytes[i]! << 16) | (bytes[i + 1]! << 8);
|
|
102
|
+
out += base64Alphabet[n >> 18]! + base64Alphabet[(n >> 12) & 63]! + base64Alphabet[(n >> 6) & 63]! + '=';
|
|
103
|
+
}
|
|
104
|
+
return out;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/** Decodes base64, in the standard or the URL-safe alphabet; throws `FormatError` if `text` is not base64. */
|
|
108
|
+
export function base64Decode(text: string): Uint8Array {
|
|
109
|
+
if (text.length % 4 !== 0) throw new FormatError('Invalid base64 length, must be a multiple of four');
|
|
110
|
+
let padding = 0;
|
|
111
|
+
if (text.endsWith('==')) padding = 2;
|
|
112
|
+
else if (text.endsWith('=')) padding = 1;
|
|
113
|
+
const out = new Uint8Array((text.length / 4) * 3 - padding);
|
|
114
|
+
let o = 0;
|
|
115
|
+
for (let i = 0; i < text.length; i += 4) {
|
|
116
|
+
let n = 0;
|
|
117
|
+
for (let j = 0; j < 4; j++) {
|
|
118
|
+
const c = text.charCodeAt(i + j);
|
|
119
|
+
const isPadding = c === 0x3d && i + 4 === text.length && j >= 4 - padding;
|
|
120
|
+
const value = isPadding ? 0 : c < 128 ? base64Values[c]! : -1;
|
|
121
|
+
if (value < 0) throw new FormatError('Invalid base64 character', text.charAt(i + j));
|
|
122
|
+
n = (n << 6) | value;
|
|
123
|
+
}
|
|
124
|
+
if (o < out.length) out[o++] = n >> 16;
|
|
125
|
+
if (o < out.length) out[o++] = (n >> 8) & 0xff;
|
|
126
|
+
if (o < out.length) out[o++] = n & 0xff;
|
|
127
|
+
}
|
|
128
|
+
return out;
|
|
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
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/** The part of Node.js's `AsyncLocalStorage` this package uses. */
|
|
2
|
+
interface AsyncStorage<T> {
|
|
3
|
+
getStore(): T | undefined;
|
|
4
|
+
run<R>(store: T, callback: () => R): R;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
type AsyncStorageConstructor = new <T>() => AsyncStorage<T>;
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* `AsyncLocalStorage` where the runtime has it (Node.js, Deno, Bun), found without importing `node:async_hooks`, so that
|
|
11
|
+
* bundlers for the browser have nothing to resolve.
|
|
12
|
+
*/
|
|
13
|
+
const AsyncStorageClass: AsyncStorageConstructor | undefined = (() => {
|
|
14
|
+
const runtime = globalThis as {
|
|
15
|
+
process?: { getBuiltinModule?: (id: string) => { AsyncLocalStorage?: AsyncStorageConstructor } | undefined };
|
|
16
|
+
};
|
|
17
|
+
try {
|
|
18
|
+
return runtime.process?.getBuiltinModule?.('node:async_hooks')?.AsyncLocalStorage;
|
|
19
|
+
} catch {
|
|
20
|
+
return undefined;
|
|
21
|
+
}
|
|
22
|
+
})();
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Tells which asynchronous call an operation runs in, through every `await` and callback it starts. Where the runtime
|
|
26
|
+
* cannot track that, as in browsers, `current` is always `undefined`.
|
|
27
|
+
*/
|
|
28
|
+
export class CallContext<T extends object> {
|
|
29
|
+
private readonly storage: AsyncStorage<T> | undefined = AsyncStorageClass ? new AsyncStorageClass<T>() : undefined;
|
|
30
|
+
|
|
31
|
+
/** The value of the call that runs now, if the runtime can tell. */
|
|
32
|
+
get current(): T | undefined {
|
|
33
|
+
return this.storage?.getStore();
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** Runs `action` as the call `value`. */
|
|
37
|
+
run<R>(value: T, action: () => R): R {
|
|
38
|
+
return this.storage ? this.storage.run(value, action) : action();
|
|
39
|
+
}
|
|
40
|
+
}
|