@coderoftime/fints 0.0.0-stage → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (249) hide show
  1. package/CHANGELOG.md +152 -0
  2. package/LICENSE +28 -0
  3. package/README.md +246 -2
  4. package/dist/client.d.ts +48 -0
  5. package/dist/client.d.ts.map +1 -0
  6. package/dist/client.js +199 -0
  7. package/dist/client.js.map +1 -0
  8. package/dist/core/account.d.ts +168 -0
  9. package/dist/core/account.d.ts.map +1 -0
  10. package/dist/core/account.js +245 -0
  11. package/dist/core/account.js.map +1 -0
  12. package/dist/core/amount.d.ts +63 -0
  13. package/dist/core/amount.d.ts.map +1 -0
  14. package/dist/core/amount.js +192 -0
  15. package/dist/core/amount.js.map +1 -0
  16. package/dist/core/balance.d.ts +55 -0
  17. package/dist/core/balance.d.ts.map +1 -0
  18. package/dist/core/balance.js +54 -0
  19. package/dist/core/balance.js.map +1 -0
  20. package/dist/core/bank-info.d.ts +49 -0
  21. package/dist/core/bank-info.d.ts.map +1 -0
  22. package/dist/core/bank-info.js +52 -0
  23. package/dist/core/bank-info.js.map +1 -0
  24. package/dist/core/client.d.ts +161 -0
  25. package/dist/core/client.d.ts.map +1 -0
  26. package/dist/core/client.js +2 -0
  27. package/dist/core/client.js.map +1 -0
  28. package/dist/core/config.d.ts +80 -0
  29. package/dist/core/config.d.ts.map +1 -0
  30. package/dist/core/config.js +153 -0
  31. package/dist/core/config.js.map +1 -0
  32. package/dist/core/errors.d.ts +153 -0
  33. package/dist/core/errors.d.ts.map +1 -0
  34. package/dist/core/errors.js +247 -0
  35. package/dist/core/errors.js.map +1 -0
  36. package/dist/core/feedback.d.ts +72 -0
  37. package/dist/core/feedback.d.ts.map +1 -0
  38. package/dist/core/feedback.js +127 -0
  39. package/dist/core/feedback.js.map +1 -0
  40. package/dist/core/transaction.d.ts +103 -0
  41. package/dist/core/transaction.d.ts.map +1 -0
  42. package/dist/core/transaction.js +96 -0
  43. package/dist/core/transaction.js.map +1 -0
  44. package/dist/core/transport.d.ts +64 -0
  45. package/dist/core/transport.d.ts.map +1 -0
  46. package/dist/core/transport.js +149 -0
  47. package/dist/core/transport.js.map +1 -0
  48. package/dist/formats/camt.d.ts +32 -0
  49. package/dist/formats/camt.d.ts.map +1 -0
  50. package/dist/formats/camt.js +242 -0
  51. package/dist/formats/camt.js.map +1 -0
  52. package/dist/formats/mt940.d.ts +54 -0
  53. package/dist/formats/mt940.d.ts.map +1 -0
  54. package/dist/formats/mt940.js +376 -0
  55. package/dist/formats/mt940.js.map +1 -0
  56. package/dist/formats/readers.d.ts +15 -0
  57. package/dist/formats/readers.d.ts.map +1 -0
  58. package/dist/formats/readers.js +42 -0
  59. package/dist/formats/readers.js.map +1 -0
  60. package/dist/formats/record-budget.d.ts +7 -0
  61. package/dist/formats/record-budget.d.ts.map +1 -0
  62. package/dist/formats/record-budget.js +17 -0
  63. package/dist/formats/record-budget.js.map +1 -0
  64. package/dist/formats/sepa-purpose.d.ts +28 -0
  65. package/dist/formats/sepa-purpose.d.ts.map +1 -0
  66. package/dist/formats/sepa-purpose.js +89 -0
  67. package/dist/formats/sepa-purpose.js.map +1 -0
  68. package/dist/index.d.ts +28 -0
  69. package/dist/index.d.ts.map +1 -0
  70. package/dist/index.js +27 -0
  71. package/dist/index.js.map +1 -0
  72. package/dist/session/app-approval.d.ts +66 -0
  73. package/dist/session/app-approval.d.ts.map +1 -0
  74. package/dist/session/app-approval.js +226 -0
  75. package/dist/session/app-approval.js.map +1 -0
  76. package/dist/session/dialog-runner.d.ts +31 -0
  77. package/dist/session/dialog-runner.d.ts.map +1 -0
  78. package/dist/session/dialog-runner.js +111 -0
  79. package/dist/session/dialog-runner.js.map +1 -0
  80. package/dist/session/login-guard.d.ts +78 -0
  81. package/dist/session/login-guard.d.ts.map +1 -0
  82. package/dist/session/login-guard.js +181 -0
  83. package/dist/session/login-guard.js.map +1 -0
  84. package/dist/session/sepa-accounts.d.ts +40 -0
  85. package/dist/session/sepa-accounts.d.ts.map +1 -0
  86. package/dist/session/sepa-accounts.js +112 -0
  87. package/dist/session/sepa-accounts.js.map +1 -0
  88. package/dist/session/session-setup.d.ts +80 -0
  89. package/dist/session/session-setup.d.ts.map +1 -0
  90. package/dist/session/session-setup.js +182 -0
  91. package/dist/session/session-setup.js.map +1 -0
  92. package/dist/session/session-state.d.ts +50 -0
  93. package/dist/session/session-state.d.ts.map +1 -0
  94. package/dist/session/session-state.js +182 -0
  95. package/dist/session/session-state.js.map +1 -0
  96. package/dist/session/signed-dialog.d.ts +77 -0
  97. package/dist/session/signed-dialog.d.ts.map +1 -0
  98. package/dist/session/signed-dialog.js +170 -0
  99. package/dist/session/signed-dialog.js.map +1 -0
  100. package/dist/session/tan-selection.d.ts +96 -0
  101. package/dist/session/tan-selection.d.ts.map +1 -0
  102. package/dist/session/tan-selection.js +189 -0
  103. package/dist/session/tan-selection.js.map +1 -0
  104. package/dist/session/user-prompts.d.ts +65 -0
  105. package/dist/session/user-prompts.d.ts.map +1 -0
  106. package/dist/session/user-prompts.js +184 -0
  107. package/dist/session/user-prompts.js.map +1 -0
  108. package/dist/tan/challenge.d.ts +135 -0
  109. package/dist/tan/challenge.d.ts.map +1 -0
  110. package/dist/tan/challenge.js +184 -0
  111. package/dist/tan/challenge.js.map +1 -0
  112. package/dist/tan/flicker.d.ts +42 -0
  113. package/dist/tan/flicker.d.ts.map +1 -0
  114. package/dist/tan/flicker.js +296 -0
  115. package/dist/tan/flicker.js.map +1 -0
  116. package/dist/tan/tan-image.d.ts +22 -0
  117. package/dist/tan/tan-image.d.ts.map +1 -0
  118. package/dist/tan/tan-image.js +43 -0
  119. package/dist/tan/tan-image.js.map +1 -0
  120. package/dist/tan/tan-models.d.ts +149 -0
  121. package/dist/tan/tan-models.d.ts.map +1 -0
  122. package/dist/tan/tan-models.js +153 -0
  123. package/dist/tan/tan-models.js.map +1 -0
  124. package/dist/util/async.d.ts +22 -0
  125. package/dist/util/async.d.ts.map +1 -0
  126. package/dist/util/async.js +45 -0
  127. package/dist/util/async.js.map +1 -0
  128. package/dist/util/bytes.d.ts +22 -0
  129. package/dist/util/bytes.d.ts.map +1 -0
  130. package/dist/util/bytes.js +128 -0
  131. package/dist/util/bytes.js.map +1 -0
  132. package/dist/util/call-context.d.ts +12 -0
  133. package/dist/util/call-context.d.ts.map +1 -0
  134. package/dist/util/call-context.js +30 -0
  135. package/dist/util/call-context.js.map +1 -0
  136. package/dist/util/dates.d.ts +55 -0
  137. package/dist/util/dates.d.ts.map +1 -0
  138. package/dist/util/dates.js +189 -0
  139. package/dist/util/dates.js.map +1 -0
  140. package/dist/util/errors.d.ts +17 -0
  141. package/dist/util/errors.d.ts.map +1 -0
  142. package/dist/util/errors.js +24 -0
  143. package/dist/util/errors.js.map +1 -0
  144. package/dist/util/immutable-map.d.ts +16 -0
  145. package/dist/util/immutable-map.d.ts.map +1 -0
  146. package/dist/util/immutable-map.js +38 -0
  147. package/dist/util/immutable-map.js.map +1 -0
  148. package/dist/util/json.d.ts +17 -0
  149. package/dist/util/json.d.ts.map +1 -0
  150. package/dist/util/json.js +62 -0
  151. package/dist/util/json.js.map +1 -0
  152. package/dist/util/text.d.ts +37 -0
  153. package/dist/util/text.d.ts.map +1 -0
  154. package/dist/util/text.js +107 -0
  155. package/dist/util/text.js.map +1 -0
  156. package/dist/util/xml.d.ts +36 -0
  157. package/dist/util/xml.d.ts.map +1 -0
  158. package/dist/util/xml.js +194 -0
  159. package/dist/util/xml.js.map +1 -0
  160. package/dist/v3/account-extension.d.ts +25 -0
  161. package/dist/v3/account-extension.d.ts.map +1 -0
  162. package/dist/v3/account-extension.js +173 -0
  163. package/dist/v3/account-extension.js.map +1 -0
  164. package/dist/v3/bank-connection.d.ts +34 -0
  165. package/dist/v3/bank-connection.d.ts.map +1 -0
  166. package/dist/v3/bank-connection.js +58 -0
  167. package/dist/v3/bank-connection.js.map +1 -0
  168. package/dist/v3/dialog.d.ts +112 -0
  169. package/dist/v3/dialog.d.ts.map +1 -0
  170. package/dist/v3/dialog.js +241 -0
  171. package/dist/v3/dialog.js.map +1 -0
  172. package/dist/v3/message.d.ts +85 -0
  173. package/dist/v3/message.d.ts.map +1 -0
  174. package/dist/v3/message.js +303 -0
  175. package/dist/v3/message.js.map +1 -0
  176. package/dist/v3/parameter-store.d.ts +75 -0
  177. package/dist/v3/parameter-store.d.ts.map +1 -0
  178. package/dist/v3/parameter-store.js +143 -0
  179. package/dist/v3/parameter-store.js.map +1 -0
  180. package/dist/v3/parameters.d.ts +105 -0
  181. package/dist/v3/parameters.d.ts.map +1 -0
  182. package/dist/v3/parameters.js +450 -0
  183. package/dist/v3/parameters.js.map +1 -0
  184. package/dist/v3/protocol.d.ts +61 -0
  185. package/dist/v3/protocol.d.ts.map +1 -0
  186. package/dist/v3/protocol.js +113 -0
  187. package/dist/v3/protocol.js.map +1 -0
  188. package/dist/v3/queries.d.ts +51 -0
  189. package/dist/v3/queries.d.ts.map +1 -0
  190. package/dist/v3/queries.js +120 -0
  191. package/dist/v3/queries.js.map +1 -0
  192. package/dist/v3/segments.d.ts +106 -0
  193. package/dist/v3/segments.d.ts.map +1 -0
  194. package/dist/v3/segments.js +325 -0
  195. package/dist/v3/segments.js.map +1 -0
  196. package/dist/v3/syntax.d.ts +127 -0
  197. package/dist/v3/syntax.d.ts.map +1 -0
  198. package/dist/v3/syntax.js +367 -0
  199. package/dist/v3/syntax.js.map +1 -0
  200. package/package.json +67 -4
  201. package/src/client.ts +252 -0
  202. package/src/core/account.ts +323 -0
  203. package/src/core/amount.ts +211 -0
  204. package/src/core/balance.ts +85 -0
  205. package/src/core/bank-info.ts +66 -0
  206. package/src/core/client.ts +186 -0
  207. package/src/core/config.ts +182 -0
  208. package/src/core/errors.ts +277 -0
  209. package/src/core/feedback.ts +140 -0
  210. package/src/core/transaction.ts +161 -0
  211. package/src/core/transport.ts +189 -0
  212. package/src/formats/camt.ts +266 -0
  213. package/src/formats/mt940.ts +419 -0
  214. package/src/formats/readers.ts +49 -0
  215. package/src/formats/record-budget.ts +18 -0
  216. package/src/formats/sepa-purpose.ts +94 -0
  217. package/src/index.ts +70 -0
  218. package/src/session/app-approval.ts +278 -0
  219. package/src/session/dialog-runner.ts +122 -0
  220. package/src/session/login-guard.ts +221 -0
  221. package/src/session/sepa-accounts.ts +115 -0
  222. package/src/session/session-setup.ts +184 -0
  223. package/src/session/session-state.ts +204 -0
  224. package/src/session/signed-dialog.ts +192 -0
  225. package/src/session/tan-selection.ts +203 -0
  226. package/src/session/user-prompts.ts +193 -0
  227. package/src/tan/challenge.ts +261 -0
  228. package/src/tan/flicker.ts +308 -0
  229. package/src/tan/tan-image.ts +38 -0
  230. package/src/tan/tan-models.ts +252 -0
  231. package/src/util/async.ts +48 -0
  232. package/src/util/bytes.ts +129 -0
  233. package/src/util/call-context.ts +41 -0
  234. package/src/util/dates.ts +210 -0
  235. package/src/util/errors.ts +27 -0
  236. package/src/util/immutable-map.ts +47 -0
  237. package/src/util/json.ts +67 -0
  238. package/src/util/text.ts +112 -0
  239. package/src/util/xml.ts +183 -0
  240. package/src/v3/account-extension.ts +185 -0
  241. package/src/v3/bank-connection.ts +67 -0
  242. package/src/v3/dialog.ts +283 -0
  243. package/src/v3/message.ts +373 -0
  244. package/src/v3/parameter-store.ts +160 -0
  245. package/src/v3/parameters.ts +524 -0
  246. package/src/v3/protocol.ts +148 -0
  247. package/src/v3/queries.ts +190 -0
  248. package/src/v3/segments.ts +374 -0
  249. package/src/v3/syntax.ts +369 -0
@@ -0,0 +1,241 @@
1
+ // Internal to the package, and named after the FinTS specification. Documentation is where the meaning is not evident.
2
+ import { FinTsError, FinTsProtocolError, FinTsTransportError, parseBankData } from '../core/errors.js';
3
+ import { endsDialog, FeedbackCodes, hasCode, hasErrors } from '../core/feedback.js';
4
+ import { challengeText, TanChallenge } from '../tan/challenge.js';
5
+ import { TanMethod } from '../tan/tan-models.js';
6
+ import { IncomingMessage3 } from './message.js';
7
+ import { requireBankParameters } from './parameters.js';
8
+ import { Segments, TanResponse } from './segments.js';
9
+ /** Dialog ID of a dialog the bank has not answered yet, and customer system ID before synchronization. */
10
+ const unassigned = '0';
11
+ /** The first version of HKTAN that supports strong customer authentication. */
12
+ const firstScaVersion = 6;
13
+ /** The first version of HKTAN that supports app approvals, with the status request (process S). */
14
+ const firstDecoupledVersion = 7;
15
+ /**
16
+ * How long a dialog may stay idle where the bank states no timeout: on the safe side, since a dialog given up too early
17
+ * only costs a new login, while one kept too long fails the next call.
18
+ */
19
+ const defaultIdleLimitMs = 5 * 60_000;
20
+ /**
21
+ * One FinTS 3.0 dialog: numbered messages under the dialog ID the bank assigns in its first answer, signed with a TAN
22
+ * method and medium, and the approvals the bank asks for. The `FinTs3Protocol` builds the messages and signs them.
23
+ */
24
+ export class FinTs3Dialog {
25
+ protocol;
26
+ method;
27
+ tanMedium;
28
+ anonymous;
29
+ systemId;
30
+ id = unassigned;
31
+ messageNumber = 1;
32
+ ended = false;
33
+ /** When the bank last answered without rejecting the whole message, which shows it still knows the dialog. */
34
+ lastAnswer;
35
+ constructor(protocol, method,
36
+ /** The TAN medium the messages name, where the method takes one. */
37
+ tanMedium, anonymous) {
38
+ this.protocol = protocol;
39
+ this.method = method;
40
+ this.tanMedium = tanMedium;
41
+ this.anonymous = anonymous;
42
+ this.systemId = anonymous ? unassigned : (protocol.parameters.systemId ?? unassigned);
43
+ }
44
+ static signed(protocol, method, tanMedium) {
45
+ return new FinTs3Dialog(protocol, method, tanMedium, false);
46
+ }
47
+ /** A dialog without login, which only delivers the bank parameter data. */
48
+ static anonymous(protocol) {
49
+ return new FinTs3Dialog(protocol, TanMethod.oneStep, undefined, true);
50
+ }
51
+ /** Whether the bank knows this dialog and it has not ended. */
52
+ get isActive() {
53
+ return this.id !== unassigned && !this.hasEnded;
54
+ }
55
+ /**
56
+ * Whether the dialog has ended, on either side.
57
+ *
58
+ * The bank ends a dialog idle for longer than its maximum timeout (HIBPA, Formals C.9), or five minutes where it
59
+ * states none. Such a dialog is neither used nor ended again: the bank would refuse any message in it.
60
+ */
61
+ get hasEnded() {
62
+ return this.ended || this.idleTooLong;
63
+ }
64
+ get idleTooLong() {
65
+ const last = this.lastAnswer;
66
+ const timeout = this.protocol.parameters.bankParameters?.bankInfo.maxTimeout;
67
+ // A timeout of 0 or below: the bank keeps idle dialogs without limit.
68
+ const limit = timeout === undefined ? defaultIdleLimitMs : timeout <= 0 ? undefined : timeout * 1000;
69
+ return last !== undefined && limit !== undefined && Date.now() - last > limit;
70
+ }
71
+ /** Whether the messages are signed with a two-step TAN method, so that the bank may ask for approvals. */
72
+ get isTwoStep() {
73
+ return !this.method.isOneStep;
74
+ }
75
+ /** The version of the HKTAN segments: that of the HITANS segment the bank describes the method in. */
76
+ get tanVersion() {
77
+ return this.protocol.parameters.bankParameters?.tanVersion(this.method) ?? firstScaVersion;
78
+ }
79
+ /** Opens the dialog; with `synchronize` the bank also assigns a customer system ID. */
80
+ start(synchronize = false) {
81
+ if (synchronize)
82
+ this.systemId = unassigned;
83
+ return this.sendSegments([
84
+ ...this.protocol.initSegments(this.systemId),
85
+ ...(this.isTwoStep ? [this.tanRequest('HKIDN')] : []),
86
+ ...(synchronize ? [Segments.synchronization()] : []),
87
+ ]);
88
+ }
89
+ /**
90
+ * The messages that open the dialog and send `order` while no TAN medium is chosen yet, as for listing the media:
91
+ * `login` opens the dialog, asking strong authentication for `order` with a two-step method, and `order` sends the
92
+ * order in a message of its own.
93
+ */
94
+ startWith(order) {
95
+ return {
96
+ login: () => this.sendSegments([
97
+ ...this.protocol.initSegments(this.systemId),
98
+ ...(this.isTwoStep ? [this.tanRequest(order.type)] : []),
99
+ ]),
100
+ order: () => this.sendSegments([order]),
101
+ };
102
+ }
103
+ /**
104
+ * Opens the anonymous dialog; the answer carries the bank parameter data.
105
+ *
106
+ * With `announceSca` the message also carries HKTAN, the way a client that can do strong customer authentication
107
+ * tells the bank so, whereupon the bank may send parameter data suited to it (PIN/TAN, B.4.3.1).
108
+ */
109
+ requestBankParameters(announceSca = false) {
110
+ return this.sendSegments([
111
+ ...this.protocol.initSegments(unassigned, true),
112
+ ...(announceSca ? [Segments.tanProcess4(firstScaVersion, 'HKIDN')] : []),
113
+ ]);
114
+ }
115
+ /** Sends `order`, asking for strong customer authentication where the bank requires it. */
116
+ send(order) {
117
+ const needsTan = this.isTwoStep && requireBankParameters(this.protocol.parameters.bankParameters).pinTan.requiresTan(order.type);
118
+ return this.sendSegments([order, ...(needsTan ? [this.tanRequest(order.type)] : [])]);
119
+ }
120
+ /**
121
+ * Ends the dialog at the bank and returns the answer; `undefined` if the dialog is not active or the end failed.
122
+ *
123
+ * Bank and connection errors are ignored, since the bank may have ended the dialog on its side already.
124
+ */
125
+ async end() {
126
+ if (!this.isActive)
127
+ return undefined;
128
+ this.ended = true;
129
+ try {
130
+ return await this.sendSegments([Segments.dialogEnd(this.id)]);
131
+ }
132
+ catch (error) {
133
+ if (!(error instanceof FinTsError))
134
+ throw error;
135
+ return undefined;
136
+ }
137
+ }
138
+ /** Forgets the dialog without contacting the bank, for example after the bank rejected the PIN. */
139
+ discard() {
140
+ this.ended = true;
141
+ }
142
+ /**
143
+ * The approval `response`, the dialog's latest answer, asks for with return code 0030 (TAN) or 3955 (banking app):
144
+ * HITAN of process 4, answered with HKTAN process 2 (TAN) or S (status); `undefined` if it asks for none, or carries
145
+ * only the dummy challenge of a message the bank exempts from strong authentication.
146
+ *
147
+ * The bank has not executed the message then, so a request without a challenge is a `FinTsProtocolError`: read as
148
+ * done, it would report an empty result. So is an app approval with a method of HITANS #6: the status request
149
+ * (process S) came with version 7, along with app approvals (PIN/TAN B.5.2).
150
+ */
151
+ approvalAskedIn(response) {
152
+ const feedback = response.feedback;
153
+ const decoupled = hasCode(feedback, FeedbackCodes.decoupledPending);
154
+ if (hasErrors(feedback) || !(hasCode(feedback, FeedbackCodes.tanRequired) || decoupled))
155
+ return undefined;
156
+ const tan = tanResponseOf(response, '4');
157
+ if (tan === undefined || tan.isDummy) {
158
+ throw new FinTsProtocolError('The bank asks for an approval, but sends no challenge');
159
+ }
160
+ if (decoupled && this.tanVersion < firstDecoupledVersion) {
161
+ throw new FinTsProtocolError('The bank asks for an app approval, which needs HKTAN version 7');
162
+ }
163
+ const reference = tan.reference;
164
+ return {
165
+ challenge: TanChallenge.create({
166
+ method: this.method,
167
+ reference,
168
+ message: tan.challenge,
169
+ hhdUc: tan.hhdUc,
170
+ isDecoupled: decoupled || this.method.decoupled,
171
+ validUntil: tan.validUntil,
172
+ tanMediumName: tan.mediumName ?? this.tanMedium,
173
+ }),
174
+ submitTan: (value) => this.sendSegments([Segments.tanProcess2(this.tanVersion, reference)], value),
175
+ checkStatus: () => this.sendSegments([Segments.tanProcessStatus(this.tanVersion, reference)]),
176
+ waitForPush: hasCode(feedback, FeedbackCodes.decoupledWaitForPush),
177
+ // A status answer that the approval is still pending may carry new instructions (PIN/TAN B.4.2.2.1, step 2b).
178
+ instructionsIn: (status) => challengeText(tanResponseOf(status, 'S')?.challenge),
179
+ };
180
+ }
181
+ tanRequest(segmentType) {
182
+ const medium = this.method.mediumRequirement === 'notAllowed' ? undefined : this.tanMedium;
183
+ return Segments.tanProcess4(this.tanVersion, segmentType, medium === undefined ? undefined : this.protocol.parameters.tanMediumAsSent(medium));
184
+ }
185
+ /**
186
+ * Sends `segments`, with `tan` where they complete an approval, as the next message, and reads the bank's answer. The
187
+ * message counts once the bank has answered, whatever it answered.
188
+ *
189
+ * The bank assigns the dialog ID in its answer to the first message, so the answer to another user's first message
190
+ * passes for it (`ParameterStore.reportedUpdVersion`). Any other answer to another message fails with
191
+ * `FinTsProtocolError` before anything of it is kept: a relay or proxy that mixes up answers would hand the client
192
+ * another user's data, or a page of another query.
193
+ *
194
+ * The dialog ends on the client's side, without a dialog end, where it is over for the bank: after a return code that
195
+ * ends it, such as 9800, and after a rejected opening message (Formals C.1.2), or a broken connection (C.6).
196
+ *
197
+ * Only an answer that does not reject the whole message starts the idle time anew (`hasEnded`): a rejection may come
198
+ * from a bank that has ended the dialog without saying so.
199
+ */
200
+ async sendSegments(segments, tan) {
201
+ const number = this.messageNumber;
202
+ let answer;
203
+ try {
204
+ answer = await this.protocol.post({
205
+ dialogId: this.id,
206
+ number,
207
+ segments,
208
+ signing: this.anonymous
209
+ ? undefined
210
+ : { securityFunction: this.method.securityFunction, systemId: this.systemId },
211
+ tan,
212
+ });
213
+ }
214
+ catch (error) {
215
+ if (error instanceof FinTsTransportError)
216
+ this.discard();
217
+ throw error;
218
+ }
219
+ this.messageNumber++;
220
+ const response = parseBankData(() => IncomingMessage3.parse(answer));
221
+ const assigned = this.id === unassigned ? undefined : this.id;
222
+ if (!response.answers(number, assigned)) {
223
+ throw new FinTsProtocolError('The answer belongs to another message than the one sent');
224
+ }
225
+ if (assigned === undefined)
226
+ this.id = response.dialogId ?? unassigned;
227
+ if (!response.messageRejected)
228
+ this.lastAnswer = Date.now();
229
+ if (endsDialog(response.feedback) || (number === 1 && response.messageRejected))
230
+ this.discard();
231
+ return response;
232
+ }
233
+ }
234
+ /** The TAN part of `response` that answers TAN `process`. */
235
+ function tanResponseOf(response, process) {
236
+ return response
237
+ .findAll('HITAN')
238
+ .map((s) => TanResponse.parse(s))
239
+ .find((t) => t.process === process);
240
+ }
241
+ //# sourceMappingURL=dialog.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"dialog.js","sourceRoot":"","sources":["../../src/v3/dialog.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,85 @@
1
+ import { BankMessage } from '../core/bank-info.js';
2
+ import { FinTsFeedback } from '../core/feedback.js';
3
+ import { type Segment, SegmentSpec } from './syntax.js';
4
+ /** Signature data of a PIN/TAN message. */
5
+ export interface PinTanCredentials {
6
+ readonly countryCode: string;
7
+ readonly bankCode: string;
8
+ readonly userId: string;
9
+ readonly systemId: string;
10
+ readonly pin: string;
11
+ readonly securityFunction: string;
12
+ }
13
+ /** A message to send, and the same message with PIN and TAN masked for logging. */
14
+ export declare class OutgoingMessage {
15
+ readonly bytes: Uint8Array;
16
+ /** The parts of the masked message, each decoded on its own as the bank's text is. */
17
+ private readonly maskedParts;
18
+ constructor(bytes: Uint8Array,
19
+ /** The parts of the masked message, each decoded on its own as the bank's text is. */
20
+ maskedParts: readonly Uint8Array[]);
21
+ /** The message with PIN and TAN masked; decoded only when a logger asks for it. */
22
+ get masked(): string;
23
+ }
24
+ /**
25
+ * Builds a message: header (HNHBK), the `segments`, trailer (HNHBS).
26
+ *
27
+ * With `security` the segments are signed with PIN and optional `tan` (HNSHK … HNSHA) and wrapped in the encryption
28
+ * envelope (HNVSK, HNVSD) that PIN/TAN uses without actual encryption.
29
+ *
30
+ * The masked message states the sizes of its masked content, so that the log does not tell how long PIN and TAN are.
31
+ */
32
+ export declare function buildMessage(parts: {
33
+ dialogId: string;
34
+ messageNumber: number;
35
+ segments: readonly SegmentSpec[];
36
+ security?: PinTanCredentials | undefined;
37
+ tan?: string | undefined;
38
+ now?: Date;
39
+ }): OutgoingMessage;
40
+ /** A received message, with the segments of the encryption envelope unpacked. */
41
+ export declare class IncomingMessage3 {
42
+ readonly segments: readonly Segment[];
43
+ /** The memory the message holds, in bytes: its length, and what its parts take beyond that. */
44
+ readonly size: number;
45
+ /**
46
+ * Return codes for the whole message (HIRMG), then for single request segments (HIRMS).
47
+ *
48
+ * The segment a HIRMS refers to is not kept: each message carries one order, and the answer that completes an
49
+ * approval reports on the order of an earlier message (PIN/TAN B.4.2.1).
50
+ */
51
+ readonly feedback: readonly FinTsFeedback[];
52
+ /** Whether the bank rejected the message as a whole (an error in HIRMG), rather than single orders in it. */
53
+ readonly messageRejected: boolean;
54
+ private constructor();
55
+ /**
56
+ * Parses a message; throws `FormatError` if `data` is not one complete FinTS message, such as the HTML page of a
57
+ * proxy, a maintenance notice, a message cut short, which lacks the message trailer (HNHBS) at its end, or two
58
+ * messages run together, whose second would pass for part of the first.
59
+ *
60
+ * Its `size` counts the memory of its data elements along with its bytes, since a message of many short elements
61
+ * takes far more memory than bytes.
62
+ */
63
+ static parse(data: Uint8Array): IncomingMessage3;
64
+ static combine(first: IncomingMessage3, second: IncomingMessage3): IncomingMessage3;
65
+ /** Free-text messages the bank addresses to the user. */
66
+ get bankMessages(): BankMessage[];
67
+ /** The dialog ID the bank assigned, as `Segment.verbatim` reads it, to send back byte for byte. */
68
+ get dialogId(): string | undefined;
69
+ /**
70
+ * Whether the message answers message `number` of the dialog `dialogId`: header and trailer carry that number, and the
71
+ * header names that message where it names the message it refers to (Bezugsnachricht). The dialog ID counts only once
72
+ * the bank has assigned one; before, `dialogId` is `undefined`, since every first message goes out under the same.
73
+ */
74
+ answers(number: number, dialogId: string | undefined): boolean;
75
+ find(type: string): Segment | undefined;
76
+ findAll(type: string): Segment[];
77
+ }
78
+ /**
79
+ * Unpacks the transport encoding: PIN/TAN servers answer in base64, a few with the raw message.
80
+ *
81
+ * Returns anything else unchanged, for `IncomingMessage3.parse` to reject.
82
+ */
83
+ export declare function decodeTransport(body: Uint8Array): Uint8Array;
84
+ export declare function encodeTransport(message: Uint8Array): Uint8Array;
85
+ //# sourceMappingURL=message.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"message.d.ts","sourceRoot":"","sources":["../../src/v3/message.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,WAAW,EAAE,MAAM,sBAAsB,CAAC;AACnD,OAAO,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AAMpD,OAAO,EAKL,KAAK,OAAO,EACZ,WAAW,EAGZ,MAAM,aAAa,CAAC;AAErB,2CAA2C;AAC3C,MAAM,WAAW,iBAAiB;IAChC,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,gBAAgB,EAAE,MAAM,CAAC;CACnC;AA+DD,mFAAmF;AACnF,qBAAa,eAAe;IAExB,QAAQ,CAAC,KAAK,EAAE,UAAU;IAC1B,sFAAsF;IACtF,OAAO,CAAC,QAAQ,CAAC,WAAW;gBAFnB,KAAK,EAAE,UAAU;IAC1B,sFAAsF;IACrE,WAAW,EAAE,SAAS,UAAU,EAAE;IAGrD,mFAAmF;IACnF,IAAI,MAAM,IAAI,MAAM,CAEnB;CACF;AASD;;;;;;;GAOG;AACH,wBAAgB,YAAY,CAAC,KAAK,EAAE;IAClC,QAAQ,EAAE,MAAM,CAAC;IACjB,aAAa,EAAE,MAAM,CAAC;IACtB,QAAQ,EAAE,SAAS,WAAW,EAAE,CAAC;IACjC,QAAQ,CAAC,EAAE,iBAAiB,GAAG,SAAS,CAAC;IACzC,GAAG,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IACzB,GAAG,CAAC,EAAE,IAAI,CAAC;CACZ,GAAG,eAAe,CAoBlB;AAgFD,iFAAiF;AACjF,qBAAa,gBAAgB;IAazB,QAAQ,CAAC,QAAQ,EAAE,SAAS,OAAO,EAAE;IACrC,+FAA+F;IAC/F,QAAQ,CAAC,IAAI,EAAE,MAAM;IAdvB;;;;;OAKG;IACH,QAAQ,CAAC,QAAQ,EAAE,SAAS,aAAa,EAAE,CAAC;IAE5C,6GAA6G;IAC7G,QAAQ,CAAC,eAAe,EAAE,OAAO,CAAC;IAElC,OAAO;IAcP;;;;;;;OAOG;IACH,MAAM,CAAC,KAAK,CAAC,IAAI,EAAE,UAAU,GAAG,gBAAgB;IAqBhD,MAAM,CAAC,OAAO,CAAC,KAAK,EAAE,gBAAgB,EAAE,MAAM,EAAE,gBAAgB,GAAG,gBAAgB;IAInF,yDAAyD;IACzD,IAAI,YAAY,IAAI,WAAW,EAAE,CAEhC;IAED,mGAAmG;IACnG,IAAI,QAAQ,IAAI,MAAM,GAAG,SAAS,CAEjC;IAED;;;;OAIG;IACH,OAAO,CAAC,MAAM,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,SAAS,GAAG,OAAO;IAe9D,IAAI,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,GAAG,SAAS;IAIvC,OAAO,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,EAAE;CAGjC;AAmBD;;;;GAIG;AACH,wBAAgB,eAAe,CAAC,IAAI,EAAE,UAAU,GAAG,UAAU,CAS5D;AAaD,wBAAgB,eAAe,CAAC,OAAO,EAAE,UAAU,GAAG,UAAU,CAE/D"}
@@ -0,0 +1,303 @@
1
+ // Internal to the package, and named after the FinTS specification. Documentation is where the meaning is not evident.
2
+ import { BankMessage } from '../core/bank-info.js';
3
+ import { FinTsFeedback } from '../core/feedback.js';
4
+ import { TanMethod } from '../tan/tan-models.js';
5
+ import { base64Encode, BytesBuilder, latin1Encode, utf8Decode } from '../util/bytes.js';
6
+ import { formatFintsDate, formatFintsTime } from '../util/dates.js';
7
+ import { FormatError } from '../util/errors.js';
8
+ import { decodeBankText, decodeBase64Lines } from '../util/text.js';
9
+ import { Binary, dataElementMemory, maxDataElements, parseSegments, SegmentSpec, Verbatim, } from './syntax.js';
10
+ function isTwoStep(c) {
11
+ return c.securityFunction !== TanMethod.oneStep.securityFunction;
12
+ }
13
+ // Codes of the PIN/TAN security envelope, from the FinTS security specification. PIN/TAN neither hashes, signs nor
14
+ // encrypts, but the envelope must name the algorithms of the chip card procedures all the same.
15
+ /** Area the signature covers: signature head and data (SHM). */
16
+ const signedArea = 1;
17
+ /** Role of the security supplier: issuer (ISS). */
18
+ const issuer = 1;
19
+ /** Security identification: message sender (MS), empty CID, then the customer system ID. */
20
+ function sender(systemId) {
21
+ return [1, null, new Verbatim(systemId)];
22
+ }
23
+ /** Security reference number, the same in every message. */
24
+ const securityReference = 1;
25
+ /** Security time stamp (STS). */
26
+ function timestamp(time) {
27
+ return [1, formatFintsDate(time), formatFintsTime(time)];
28
+ }
29
+ /** Hash algorithm: owner hashing (OHA), mutually agreed algorithm (ZZZ), initialisation value in clear (IVC). */
30
+ const hashAlgorithm = [1, 999, 1];
31
+ /** Signature algorithm: owner signing (OSG), RSA, ISO 9796-1. */
32
+ const signatureAlgorithm = [6, 10, 16];
33
+ /** Security function of the encryption head: plain text, since PIN/TAN does not encrypt. */
34
+ const plainText = 998;
35
+ /**
36
+ * Encryption algorithm: owner symmetric (OSY), CBC, 2-key triple DES, a key of zeros, symmetric key (KYE),
37
+ * initialisation value in clear (IVC).
38
+ */
39
+ const encryptionAlgorithm = [2, 2, 13, new Binary(new Uint8Array(8)), 5, 1];
40
+ /** Compression function: none. */
41
+ const noCompression = 0;
42
+ /** Key name: bank, user, key type `S` (signature) or `V` (encryption), key number and version 0. */
43
+ function keyName(c, keyType) {
44
+ return [c.countryCode, c.bankCode, c.userId, keyType, 0, 0];
45
+ }
46
+ /** A random integer from 0 below `bound`, from a cryptographically secure source. */
47
+ function secureRandom(bound) {
48
+ const values = new Uint32Array(1);
49
+ crypto.getRandomValues(values);
50
+ return values[0] % bound;
51
+ }
52
+ /** A random 14-digit reference that ties the signature head (HNSHK) to the signature end (HNSHA). */
53
+ function securityControlReference() {
54
+ return `${secureRandom(900000000) + 100000000}${secureRandom(100000).toString().padStart(5, '0')}`;
55
+ }
56
+ /** A message to send, and the same message with PIN and TAN masked for logging. */
57
+ export class OutgoingMessage {
58
+ bytes;
59
+ maskedParts;
60
+ constructor(bytes,
61
+ /** The parts of the masked message, each decoded on its own as the bank's text is. */
62
+ maskedParts) {
63
+ this.bytes = bytes;
64
+ this.maskedParts = maskedParts;
65
+ }
66
+ /** The message with PIN and TAN masked; decoded only when a logger asks for it. */
67
+ get masked() {
68
+ return this.maskedParts.map(decodeBankText).join('');
69
+ }
70
+ }
71
+ /**
72
+ * Builds a message: header (HNHBK), the `segments`, trailer (HNHBS).
73
+ *
74
+ * With `security` the segments are signed with PIN and optional `tan` (HNSHK … HNSHA) and wrapped in the encryption
75
+ * envelope (HNVSK, HNVSD) that PIN/TAN uses without actual encryption.
76
+ *
77
+ * The masked message states the sizes of its masked content, so that the log does not tell how long PIN and TAN are.
78
+ */
79
+ export function buildMessage(parts) {
80
+ const body = parts.security === undefined
81
+ ? plainBody(parts.segments)
82
+ : signedBody(parts.segments, parts.security, parts.tan, parts.now ?? new Date());
83
+ const trailer = new SegmentSpec('HNHBS', 1, [parts.messageNumber]).build(body.nextNumber);
84
+ // The header states the size of the whole message, itself included; its own size does not depend on it.
85
+ const header = (size) => new SegmentSpec('HNHBK', 3, [
86
+ size.toString().padStart(12, '0'),
87
+ 300,
88
+ new Verbatim(parts.dialogId),
89
+ parts.messageNumber,
90
+ ]).build(1);
91
+ const sizeOf = (sections) => header(0).length + lengthOf(sections) + trailer.length;
92
+ const result = new BytesBuilder();
93
+ result.add(header(sizeOf([body.bytes])));
94
+ result.add(body.bytes);
95
+ result.add(trailer);
96
+ return new OutgoingMessage(result.takeBytes(), [header(sizeOf(body.masked)), ...body.masked, trailer]);
97
+ }
98
+ /** The number of bytes in `parts` together. */
99
+ function lengthOf(parts) {
100
+ return parts.reduce((sum, part) => sum + part.length, 0);
101
+ }
102
+ /** The segments of a message without security, as the anonymous dialog sends them. */
103
+ function plainBody(segments) {
104
+ const built = segments.map((segment, i) => segment.build(i + 2));
105
+ const bytes = new BytesBuilder();
106
+ built.forEach((b) => {
107
+ bytes.add(b);
108
+ });
109
+ return { bytes: bytes.takeBytes(), masked: built, nextNumber: built.length + 2 };
110
+ }
111
+ /** The segments signed with PIN and optional `tan`, inside the encryption envelope. */
112
+ function signedBody(segments, security, tan, time) {
113
+ const profile = ['PIN', isTwoStep(security) ? 2 : 1];
114
+ const controlReference = securityControlReference();
115
+ const inner = new BytesBuilder();
116
+ const maskedInner = [];
117
+ const addInner = (built, masked) => {
118
+ inner.add(built);
119
+ maskedInner.push(masked ?? built);
120
+ };
121
+ addInner(new SegmentSpec('HNSHK', 4, [
122
+ profile,
123
+ security.securityFunction,
124
+ controlReference,
125
+ signedArea,
126
+ issuer,
127
+ sender(security.systemId),
128
+ securityReference,
129
+ timestamp(time),
130
+ hashAlgorithm,
131
+ signatureAlgorithm,
132
+ keyName(security, 'S'),
133
+ ]).build(2));
134
+ let number = 3;
135
+ for (const segment of segments)
136
+ addInner(segment.build(number++));
137
+ const signatureEnd = (pin, tanValue) => new SegmentSpec('HNSHA', 2, [controlReference, null, [pin, tanValue]]);
138
+ addInner(signatureEnd(security.pin, tan).build(number), signatureEnd('***', tan === undefined ? undefined : '***').build(number));
139
+ const innerBytes = inner.takeBytes();
140
+ const encryptionHead = new SegmentSpec('HNVSK', 3, [
141
+ profile,
142
+ plainText,
143
+ issuer,
144
+ sender(security.systemId),
145
+ timestamp(time),
146
+ encryptionAlgorithm,
147
+ keyName(security, 'V'),
148
+ noCompression,
149
+ ]).build(998);
150
+ const bytes = new BytesBuilder();
151
+ bytes.add(encryptionHead);
152
+ bytes.add(new SegmentSpec('HNVSD', 1, [new Binary(innerBytes)]).build(999));
153
+ const masked = [
154
+ encryptionHead,
155
+ latin1Encode(`HNVSD:999:1+@${lengthOf(maskedInner)}@`),
156
+ ...maskedInner,
157
+ latin1Encode("'"),
158
+ ];
159
+ return { bytes: bytes.takeBytes(), masked, nextNumber: number + 1 };
160
+ }
161
+ /** A received message, with the segments of the encryption envelope unpacked. */
162
+ export class IncomingMessage3 {
163
+ segments;
164
+ size;
165
+ /**
166
+ * Return codes for the whole message (HIRMG), then for single request segments (HIRMS).
167
+ *
168
+ * The segment a HIRMS refers to is not kept: each message carries one order, and the answer that completes an
169
+ * approval reports on the order of an earlier message (PIN/TAN B.4.2.1).
170
+ */
171
+ feedback;
172
+ /** Whether the bank rejected the message as a whole (an error in HIRMG), rather than single orders in it. */
173
+ messageRejected;
174
+ constructor(segments,
175
+ /** The memory the message holds, in bytes: its length, and what its parts take beyond that. */
176
+ size) {
177
+ this.segments = segments;
178
+ this.size = size;
179
+ this.feedback = Object.freeze(['HIRMG', 'HIRMS'].flatMap((type) => segments.filter((s) => s.type === type).flatMap(feedbackOf)));
180
+ this.messageRejected = segments
181
+ .filter((s) => s.type === 'HIRMG')
182
+ .flatMap(feedbackOf)
183
+ .some((f) => f.isError);
184
+ }
185
+ /**
186
+ * Parses a message; throws `FormatError` if `data` is not one complete FinTS message, such as the HTML page of a
187
+ * proxy, a maintenance notice, a message cut short, which lacks the message trailer (HNHBS) at its end, or two
188
+ * messages run together, whose second would pass for part of the first.
189
+ *
190
+ * Its `size` counts the memory of its data elements along with its bytes, since a message of many short elements
191
+ * takes far more memory than bytes.
192
+ */
193
+ static parse(data) {
194
+ const budget = { elements: maxDataElements };
195
+ const outer = parseSegments(data, budget);
196
+ if (outer[0]?.type !== 'HNHBK')
197
+ throw new FormatError('Not a FinTS message: no message header');
198
+ if (outer.at(-1)?.type !== 'HNHBS')
199
+ throw new FormatError('Incomplete FinTS message: no message trailer');
200
+ if (outer.filter((s) => s.type === 'HNHBK' || s.type === 'HNHBS').length > 2) {
201
+ throw new FormatError('More than one FinTS message');
202
+ }
203
+ const segments = [];
204
+ for (const s of outer) {
205
+ if (s.type !== 'HNVSD') {
206
+ segments.push(s);
207
+ continue;
208
+ }
209
+ const payload = s.binary(1);
210
+ if (payload === undefined)
211
+ throw new FormatError('Encrypted data (HNVSD) without binary data');
212
+ for (const inner of parseSegments(payload, budget))
213
+ segments.push(inner);
214
+ }
215
+ return new IncomingMessage3(segments, data.length + (maxDataElements - budget.elements) * dataElementMemory);
216
+ }
217
+ static combine(first, second) {
218
+ return new IncomingMessage3([...first.segments, ...second.segments], first.size + second.size);
219
+ }
220
+ /** Free-text messages the bank addresses to the user. */
221
+ get bankMessages() {
222
+ return this.findAll('HIKIM').map((m) => new BankMessage(m.text(1) ?? '', m.text(2) ?? ''));
223
+ }
224
+ /** The dialog ID the bank assigned, as `Segment.verbatim` reads it, to send back byte for byte. */
225
+ get dialogId() {
226
+ return this.find('HNHBK')?.verbatim(3);
227
+ }
228
+ /**
229
+ * Whether the message answers message `number` of the dialog `dialogId`: header and trailer carry that number, and the
230
+ * header names that message where it names the message it refers to (Bezugsnachricht). The dialog ID counts only once
231
+ * the bank has assigned one; before, `dialogId` is `undefined`, since every first message goes out under the same.
232
+ */
233
+ answers(number, dialogId) {
234
+ const header = this.find('HNHBK');
235
+ // The trailer comes last, after the segments of the encryption envelope, which might hold one of their own.
236
+ const trailer = this.segments.at(-1);
237
+ if (header === undefined || trailer?.type !== 'HNHBS')
238
+ return false;
239
+ const inDialog = (id) => dialogId === undefined || id === dialogId;
240
+ const namesReference = header.group(5).some((e) => !e.isEmpty);
241
+ return (header.integer(4) === number &&
242
+ trailer.integer(1) === number &&
243
+ inDialog(header.verbatim(3)) &&
244
+ (!namesReference || (header.integer(5, 1) === number && inDialog(header.verbatim(5, 0)))));
245
+ }
246
+ find(type) {
247
+ return this.segments.find((s) => s.type === type);
248
+ }
249
+ findAll(type) {
250
+ return this.segments.filter((s) => s.type === type);
251
+ }
252
+ }
253
+ /**
254
+ * The return codes of `s`. Their parameters are values the client or the app sends back, such as the position to
255
+ * continue at or new login data, so they are read as `Segment.verbatim` reads them.
256
+ */
257
+ function feedbackOf(s) {
258
+ const feedback = [];
259
+ for (let i = 1; i < s.length; i++) {
260
+ const code = s.text(i);
261
+ if (code === undefined)
262
+ continue;
263
+ const parameters = [];
264
+ // Empty parameters stay, as empty text, so that each keeps its position.
265
+ for (let j = 3; j < s.group(i).length; j++)
266
+ parameters.push(s.verbatim(i, j) ?? '');
267
+ feedback.push(new FinTsFeedback({ code, reference: s.text(i, 1), text: s.text(i, 2) ?? '', parameters }));
268
+ }
269
+ return feedback;
270
+ }
271
+ /**
272
+ * Unpacks the transport encoding: PIN/TAN servers answer in base64, a few with the raw message.
273
+ *
274
+ * Returns anything else unchanged, for `IncomingMessage3.parse` to reject.
275
+ */
276
+ export function decodeTransport(body) {
277
+ if (startsWithMessageHeader(body))
278
+ return body;
279
+ try {
280
+ // Base64 is ASCII, which UTF-8 decodes natively; anything else is no base64 either way.
281
+ return decodeBase64Lines(utf8Decode(body, true));
282
+ }
283
+ catch (error) {
284
+ if (error instanceof FormatError)
285
+ return body;
286
+ throw error;
287
+ }
288
+ }
289
+ const messageHeader = latin1Encode('HNHBK');
290
+ const whitespaceBytes = new Set([0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x20]);
291
+ /** Whether `body` is a raw message: its header segment comes first, after any white space. */
292
+ function startsWithMessageHeader(body) {
293
+ let start = 0;
294
+ while (start < body.length && whitespaceBytes.has(body[start]))
295
+ start++;
296
+ if (body.length - start < messageHeader.length)
297
+ return false;
298
+ return messageHeader.every((byte, i) => body[start + i] === byte);
299
+ }
300
+ export function encodeTransport(message) {
301
+ return latin1Encode(base64Encode(message));
302
+ }
303
+ //# sourceMappingURL=message.js.map