@coderoftime/fints 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (272) hide show
  1. package/CHANGELOG.md +142 -1
  2. package/README.md +127 -37
  3. package/dist/client.d.ts +48 -0
  4. package/dist/client.d.ts.map +1 -0
  5. package/dist/client.js +199 -0
  6. package/dist/client.js.map +1 -0
  7. package/dist/core/account.d.ts +35 -7
  8. package/dist/core/account.d.ts.map +1 -1
  9. package/dist/core/account.js +84 -13
  10. package/dist/core/account.js.map +1 -1
  11. package/dist/core/amount.d.ts +2 -1
  12. package/dist/core/amount.d.ts.map +1 -1
  13. package/dist/core/amount.js +4 -1
  14. package/dist/core/amount.js.map +1 -1
  15. package/dist/core/balance.d.ts +1 -0
  16. package/dist/core/balance.d.ts.map +1 -1
  17. package/dist/core/balance.js +6 -5
  18. package/dist/core/balance.js.map +1 -1
  19. package/dist/core/bank-info.d.ts +1 -1
  20. package/dist/core/bank-info.js +1 -1
  21. package/dist/core/client.d.ts +50 -15
  22. package/dist/core/client.d.ts.map +1 -1
  23. package/dist/core/config.d.ts +28 -16
  24. package/dist/core/config.d.ts.map +1 -1
  25. package/dist/core/config.js +66 -26
  26. package/dist/core/config.js.map +1 -1
  27. package/dist/core/errors.d.ts +85 -23
  28. package/dist/core/errors.d.ts.map +1 -1
  29. package/dist/core/errors.js +112 -47
  30. package/dist/core/errors.js.map +1 -1
  31. package/dist/core/feedback.d.ts +4 -1
  32. package/dist/core/feedback.d.ts.map +1 -1
  33. package/dist/core/feedback.js +4 -1
  34. package/dist/core/feedback.js.map +1 -1
  35. package/dist/core/transaction.d.ts +6 -1
  36. package/dist/core/transaction.d.ts.map +1 -1
  37. package/dist/core/transaction.js +9 -4
  38. package/dist/core/transaction.js.map +1 -1
  39. package/dist/core/transport.d.ts +33 -6
  40. package/dist/core/transport.d.ts.map +1 -1
  41. package/dist/core/transport.js +77 -20
  42. package/dist/core/transport.js.map +1 -1
  43. package/dist/formats/camt.d.ts +24 -4
  44. package/dist/formats/camt.d.ts.map +1 -1
  45. package/dist/formats/camt.js +124 -41
  46. package/dist/formats/camt.js.map +1 -1
  47. package/dist/formats/mt940.d.ts +15 -3
  48. package/dist/formats/mt940.d.ts.map +1 -1
  49. package/dist/formats/mt940.js +161 -82
  50. package/dist/formats/mt940.js.map +1 -1
  51. package/dist/formats/readers.d.ts +6 -2
  52. package/dist/formats/readers.d.ts.map +1 -1
  53. package/dist/formats/readers.js +24 -29
  54. package/dist/formats/readers.js.map +1 -1
  55. package/dist/formats/record-budget.d.ts +7 -0
  56. package/dist/formats/record-budget.d.ts.map +1 -0
  57. package/dist/formats/record-budget.js +17 -0
  58. package/dist/formats/record-budget.js.map +1 -0
  59. package/dist/formats/sepa-purpose.d.ts +6 -0
  60. package/dist/formats/sepa-purpose.d.ts.map +1 -1
  61. package/dist/formats/sepa-purpose.js +23 -2
  62. package/dist/formats/sepa-purpose.js.map +1 -1
  63. package/dist/index.d.ts +3 -3
  64. package/dist/index.d.ts.map +1 -1
  65. package/dist/index.js +2 -2
  66. package/dist/index.js.map +1 -1
  67. package/dist/session/app-approval.d.ts +11 -5
  68. package/dist/session/app-approval.d.ts.map +1 -1
  69. package/dist/session/app-approval.js +23 -12
  70. package/dist/session/app-approval.js.map +1 -1
  71. package/dist/session/dialog-runner.d.ts +7 -10
  72. package/dist/session/dialog-runner.d.ts.map +1 -1
  73. package/dist/session/dialog-runner.js +15 -16
  74. package/dist/session/dialog-runner.js.map +1 -1
  75. package/dist/session/login-guard.d.ts +54 -49
  76. package/dist/session/login-guard.d.ts.map +1 -1
  77. package/dist/session/login-guard.js +156 -84
  78. package/dist/session/login-guard.js.map +1 -1
  79. package/dist/session/sepa-accounts.d.ts +20 -14
  80. package/dist/session/sepa-accounts.d.ts.map +1 -1
  81. package/dist/session/sepa-accounts.js +62 -46
  82. package/dist/session/sepa-accounts.js.map +1 -1
  83. package/dist/session/session-setup.d.ts +33 -25
  84. package/dist/session/session-setup.d.ts.map +1 -1
  85. package/dist/session/session-setup.js +87 -65
  86. package/dist/session/session-setup.js.map +1 -1
  87. package/dist/session/session-state.d.ts +9 -6
  88. package/dist/session/session-state.d.ts.map +1 -1
  89. package/dist/session/session-state.js +52 -17
  90. package/dist/session/session-state.js.map +1 -1
  91. package/dist/session/signed-dialog.d.ts +77 -0
  92. package/dist/session/signed-dialog.d.ts.map +1 -0
  93. package/dist/session/signed-dialog.js +170 -0
  94. package/dist/session/signed-dialog.js.map +1 -0
  95. package/dist/session/tan-selection.d.ts +26 -18
  96. package/dist/session/tan-selection.d.ts.map +1 -1
  97. package/dist/session/tan-selection.js +44 -33
  98. package/dist/session/tan-selection.js.map +1 -1
  99. package/dist/session/user-prompts.d.ts +8 -4
  100. package/dist/session/user-prompts.d.ts.map +1 -1
  101. package/dist/session/user-prompts.js +34 -14
  102. package/dist/session/user-prompts.js.map +1 -1
  103. package/dist/tan/challenge.d.ts +34 -13
  104. package/dist/tan/challenge.d.ts.map +1 -1
  105. package/dist/tan/challenge.js +25 -26
  106. package/dist/tan/challenge.js.map +1 -1
  107. package/dist/tan/flicker.d.ts +7 -1
  108. package/dist/tan/flicker.d.ts.map +1 -1
  109. package/dist/tan/flicker.js +40 -6
  110. package/dist/tan/flicker.js.map +1 -1
  111. package/dist/tan/tan-image.d.ts +3 -2
  112. package/dist/tan/tan-image.d.ts.map +1 -1
  113. package/dist/tan/tan-image.js +7 -5
  114. package/dist/tan/tan-image.js.map +1 -1
  115. package/dist/tan/tan-models.d.ts +5 -1
  116. package/dist/tan/tan-models.d.ts.map +1 -1
  117. package/dist/tan/tan-models.js +10 -5
  118. package/dist/tan/tan-models.js.map +1 -1
  119. package/dist/util/async.d.ts +0 -2
  120. package/dist/util/async.d.ts.map +1 -1
  121. package/dist/util/async.js +0 -4
  122. package/dist/util/async.js.map +1 -1
  123. package/dist/util/bytes.d.ts +2 -3
  124. package/dist/util/bytes.d.ts.map +1 -1
  125. package/dist/util/bytes.js +12 -20
  126. package/dist/util/bytes.js.map +1 -1
  127. package/dist/util/call-context.d.ts.map +1 -1
  128. package/dist/util/call-context.js +3 -2
  129. package/dist/util/call-context.js.map +1 -1
  130. package/dist/util/dates.d.ts +9 -16
  131. package/dist/util/dates.d.ts.map +1 -1
  132. package/dist/util/dates.js +39 -19
  133. package/dist/util/dates.js.map +1 -1
  134. package/dist/util/errors.d.ts +9 -1
  135. package/dist/util/errors.d.ts.map +1 -1
  136. package/dist/util/errors.js +12 -3
  137. package/dist/util/errors.js.map +1 -1
  138. package/dist/util/json.d.ts +4 -0
  139. package/dist/util/json.d.ts.map +1 -1
  140. package/dist/util/json.js +18 -0
  141. package/dist/util/json.js.map +1 -1
  142. package/dist/util/text.d.ts +21 -4
  143. package/dist/util/text.d.ts.map +1 -1
  144. package/dist/util/text.js +42 -13
  145. package/dist/util/text.js.map +1 -1
  146. package/dist/util/xml.d.ts +4 -3
  147. package/dist/util/xml.d.ts.map +1 -1
  148. package/dist/util/xml.js +37 -26
  149. package/dist/util/xml.js.map +1 -1
  150. package/dist/v3/account-extension.d.ts.map +1 -1
  151. package/dist/v3/account-extension.js +7 -10
  152. package/dist/v3/account-extension.js.map +1 -1
  153. package/dist/v3/bank-connection.d.ts +34 -0
  154. package/dist/v3/bank-connection.d.ts.map +1 -0
  155. package/dist/v3/bank-connection.js +58 -0
  156. package/dist/v3/bank-connection.js.map +1 -0
  157. package/dist/v3/dialog.d.ts +80 -46
  158. package/dist/v3/dialog.d.ts.map +1 -1
  159. package/dist/v3/dialog.js +162 -59
  160. package/dist/v3/dialog.js.map +1 -1
  161. package/dist/v3/message.d.ts +17 -4
  162. package/dist/v3/message.d.ts.map +1 -1
  163. package/dist/v3/message.js +62 -18
  164. package/dist/v3/message.js.map +1 -1
  165. package/dist/v3/parameter-store.d.ts +75 -0
  166. package/dist/v3/parameter-store.d.ts.map +1 -0
  167. package/dist/v3/parameter-store.js +143 -0
  168. package/dist/v3/parameter-store.js.map +1 -0
  169. package/dist/v3/parameters.d.ts +38 -12
  170. package/dist/v3/parameters.d.ts.map +1 -1
  171. package/dist/v3/parameters.js +91 -37
  172. package/dist/v3/parameters.js.map +1 -1
  173. package/dist/v3/protocol.d.ts +43 -50
  174. package/dist/v3/protocol.d.ts.map +1 -1
  175. package/dist/v3/protocol.js +58 -208
  176. package/dist/v3/protocol.js.map +1 -1
  177. package/dist/v3/queries.d.ts +51 -0
  178. package/dist/v3/queries.d.ts.map +1 -0
  179. package/dist/v3/queries.js +120 -0
  180. package/dist/v3/queries.js.map +1 -0
  181. package/dist/v3/segments.d.ts +29 -12
  182. package/dist/v3/segments.d.ts.map +1 -1
  183. package/dist/v3/segments.js +65 -23
  184. package/dist/v3/segments.js.map +1 -1
  185. package/dist/v3/syntax.d.ts +65 -14
  186. package/dist/v3/syntax.d.ts.map +1 -1
  187. package/dist/v3/syntax.js +167 -101
  188. package/dist/v3/syntax.js.map +1 -1
  189. package/package.json +10 -6
  190. package/src/client.ts +252 -0
  191. package/src/core/account.ts +86 -13
  192. package/src/core/amount.ts +3 -1
  193. package/src/core/balance.ts +6 -5
  194. package/src/core/bank-info.ts +1 -1
  195. package/src/core/client.ts +52 -15
  196. package/src/core/config.ts +69 -28
  197. package/src/core/errors.ts +122 -48
  198. package/src/core/feedback.ts +4 -1
  199. package/src/core/transaction.ts +9 -4
  200. package/src/core/transport.ts +98 -23
  201. package/src/formats/camt.ts +136 -39
  202. package/src/formats/mt940.ts +167 -81
  203. package/src/formats/readers.ts +24 -31
  204. package/src/formats/record-budget.ts +18 -0
  205. package/src/formats/sepa-purpose.ts +21 -2
  206. package/src/index.ts +4 -2
  207. package/src/session/app-approval.ts +51 -38
  208. package/src/session/dialog-runner.ts +31 -28
  209. package/src/session/login-guard.ts +174 -88
  210. package/src/session/sepa-accounts.ts +63 -45
  211. package/src/session/session-setup.ts +90 -71
  212. package/src/session/session-state.ts +56 -22
  213. package/src/session/signed-dialog.ts +192 -0
  214. package/src/session/tan-selection.ts +50 -39
  215. package/src/session/user-prompts.ts +35 -16
  216. package/src/tan/challenge.ts +40 -31
  217. package/src/tan/flicker.ts +44 -6
  218. package/src/tan/tan-image.ts +7 -5
  219. package/src/tan/tan-models.ts +10 -5
  220. package/src/util/async.ts +0 -5
  221. package/src/util/bytes.ts +13 -19
  222. package/src/util/call-context.ts +3 -2
  223. package/src/util/dates.ts +41 -24
  224. package/src/util/errors.ts +14 -3
  225. package/src/util/json.ts +16 -0
  226. package/src/util/text.ts +42 -12
  227. package/src/util/xml.ts +37 -26
  228. package/src/v3/account-extension.ts +7 -12
  229. package/src/v3/bank-connection.ts +67 -0
  230. package/src/v3/dialog.ts +175 -91
  231. package/src/v3/message.ts +69 -17
  232. package/src/v3/parameter-store.ts +160 -0
  233. package/src/v3/parameters.ts +110 -37
  234. package/src/v3/protocol.ts +70 -253
  235. package/src/v3/queries.ts +190 -0
  236. package/src/v3/segments.ts +65 -22
  237. package/src/v3/syntax.ts +153 -91
  238. package/dist/session/bank-connection.d.ts +0 -28
  239. package/dist/session/bank-connection.d.ts.map +0 -1
  240. package/dist/session/bank-connection.js +0 -48
  241. package/dist/session/bank-connection.js.map +0 -1
  242. package/dist/session/parameter-data.d.ts +0 -83
  243. package/dist/session/parameter-data.d.ts.map +0 -1
  244. package/dist/session/parameter-data.js +0 -68
  245. package/dist/session/parameter-data.js.map +0 -1
  246. package/dist/session/pin-tan-client.d.ts +0 -54
  247. package/dist/session/pin-tan-client.d.ts.map +0 -1
  248. package/dist/session/pin-tan-client.js +0 -153
  249. package/dist/session/pin-tan-client.js.map +0 -1
  250. package/dist/session/pin-tan-dialog.d.ts +0 -71
  251. package/dist/session/pin-tan-dialog.d.ts.map +0 -1
  252. package/dist/session/pin-tan-dialog.js +0 -124
  253. package/dist/session/pin-tan-dialog.js.map +0 -1
  254. package/dist/session/protocol.d.ts +0 -209
  255. package/dist/session/protocol.d.ts.map +0 -1
  256. package/dist/session/protocol.js +0 -75
  257. package/dist/session/protocol.js.map +0 -1
  258. package/dist/session/response-handler.d.ts +0 -48
  259. package/dist/session/response-handler.d.ts.map +0 -1
  260. package/dist/session/response-handler.js +0 -121
  261. package/dist/session/response-handler.js.map +0 -1
  262. package/dist/v3/client.d.ts +0 -24
  263. package/dist/v3/client.d.ts.map +0 -1
  264. package/dist/v3/client.js +0 -16
  265. package/dist/v3/client.js.map +0 -1
  266. package/src/session/bank-connection.ts +0 -51
  267. package/src/session/parameter-data.ts +0 -137
  268. package/src/session/pin-tan-client.ts +0 -186
  269. package/src/session/pin-tan-dialog.ts +0 -160
  270. package/src/session/protocol.ts +0 -311
  271. package/src/session/response-handler.ts +0 -130
  272. package/src/v3/client.ts +0 -29
@@ -3,24 +3,31 @@
3
3
  import { Account } from '../core/account.js';
4
4
  import { Amount, withMark } from '../core/amount.js';
5
5
  import { Balance } from '../core/balance.js';
6
- import { noReference } from '../session/protocol.js';
7
6
  import { TanMedium, tanMediumStatusOf } from '../tan/tan-models.js';
8
7
  import { formatFintsDate, parseFintsDate, parseFintsDateTime } from '../util/dates.js';
9
8
  import { FormatError } from '../util/errors.js';
10
- import { type Segment, SegmentSpec, type SegmentValue } from './syntax.js';
9
+ import { type Segment, SegmentSpec, type SegmentValue, Verbatim } from './syntax.js';
10
+
11
+ /**
12
+ * The filler the specification names for a reference that does not exist.
13
+ *
14
+ * The bank puts it into a dummy answer when no approval is needed. The client sends it as the TAN medium name in the
15
+ * dialog that looks up the TAN media, before it knows a medium; the bank ignores it there (PIN/TAN B.4.3.1.3).
16
+ */
17
+ export const noReference = 'noref';
11
18
 
12
19
  /** Bank identification (KIK): country code and bank code. */
13
- export function bankIdentification(countryCode: string, bankCode: string): SegmentValue {
20
+ function bankIdentification(countryCode: string, bankCode: string): SegmentValue {
14
21
  return [countryCode, bankCode];
15
22
  }
16
23
 
17
24
  /** National account (KTV): account number, sub-account and bank identification. */
18
- export function nationalAccount(a: Account): SegmentValue[] {
25
+ function nationalAccount(a: Account): SegmentValue[] {
19
26
  return [a.accountNumber, a.subAccount, a.countryCode, a.bankCode];
20
27
  }
21
28
 
22
29
  /** International account (KTI): IBAN, BIC, then the national account. */
23
- export function internationalAccount(a: Account): SegmentValue[] {
30
+ function internationalAccount(a: Account): SegmentValue[] {
24
31
  return [a.iban, a.bic, ...nationalAccount(a)];
25
32
  }
26
33
 
@@ -33,7 +40,10 @@ const orderReferenceField = 5;
33
40
  const moreTansField = 6;
34
41
  const tanMediumField = 11;
35
42
 
36
- /** Builders for the request segments the client sends. */
43
+ /**
44
+ * Builders for the request segments the client sends. Values the bank issued, such as the customer system ID, the TAN
45
+ * order reference and the position to continue at, go back as `Verbatim`.
46
+ */
37
47
  export const Segments = Object.freeze({
38
48
  /** HKIDN: who is logging in, from which customer system. */
39
49
  identification(parts: {
@@ -46,7 +56,7 @@ export const Segments = Object.freeze({
46
56
  return new SegmentSpec('HKIDN', 2, [
47
57
  bankIdentification(parts.countryCode, parts.bankCode),
48
58
  parts.customerId,
49
- parts.systemId,
59
+ new Verbatim(parts.systemId),
50
60
  parts.systemIdRequired ? 1 : 0,
51
61
  ]);
52
62
  },
@@ -74,22 +84,37 @@ export const Segments = Object.freeze({
74
84
  },
75
85
 
76
86
  dialogEnd(dialogId: string): SegmentSpec {
77
- return new SegmentSpec('HKEND', 1, [dialogId]);
87
+ return new SegmentSpec('HKEND', 1, [new Verbatim(dialogId)]);
78
88
  },
79
89
 
80
- /** HKTAN process 4: asks for strong customer authentication of the `segmentType` sent in the same message. */
90
+ /**
91
+ * HKTAN process 4: asks for strong customer authentication of the `segmentType` sent in the same message, with the TAN
92
+ * medium `mediumName`, as the bank sent it.
93
+ */
81
94
  tanProcess4(version: number, segmentType: string, mediumName?: string): SegmentSpec {
82
- return SegmentSpec.numbered('HKTAN', version, { 1: '4', 2: segmentType, [tanMediumField]: mediumName });
95
+ return SegmentSpec.numbered('HKTAN', version, {
96
+ 1: '4',
97
+ 2: segmentType,
98
+ [tanMediumField]: Verbatim.of(mediumName),
99
+ });
83
100
  },
84
101
 
85
102
  /** HKTAN process 2: submits the TAN (carried in the signature) for the order with `reference`. */
86
103
  tanProcess2(version: number, reference: string): SegmentSpec {
87
- return SegmentSpec.numbered('HKTAN', version, { 1: '2', [orderReferenceField]: reference, [moreTansField]: false });
104
+ return SegmentSpec.numbered('HKTAN', version, {
105
+ 1: '2',
106
+ [orderReferenceField]: new Verbatim(reference),
107
+ [moreTansField]: false,
108
+ });
88
109
  },
89
110
 
90
111
  /** HKTAN process S: asks whether the order with `reference` was approved in the banking app. */
91
112
  tanProcessStatus(version: number, reference: string): SegmentSpec {
92
- return SegmentSpec.numbered('HKTAN', version, { 1: 'S', [orderReferenceField]: reference, [moreTansField]: false });
113
+ return SegmentSpec.numbered('HKTAN', version, {
114
+ 1: 'S',
115
+ [orderReferenceField]: new Verbatim(reference),
116
+ [moreTansField]: false,
117
+ });
93
118
  },
94
119
 
95
120
  /** HKSAL: the balance of one account (not of all accounts). */
@@ -98,7 +123,7 @@ export const Segments = Object.freeze({
98
123
  version >= 7 ? internationalAccount(account) : nationalAccount(account),
99
124
  false,
100
125
  null,
101
- continuation,
126
+ Verbatim.of(continuation),
102
127
  ]);
103
128
  },
104
129
 
@@ -114,7 +139,7 @@ export const Segments = Object.freeze({
114
139
  dateOrEmpty(period.from),
115
140
  dateOrEmpty(period.to),
116
141
  null,
117
- period.continuation,
142
+ Verbatim.of(period.continuation),
118
143
  ]);
119
144
  },
120
145
 
@@ -132,13 +157,13 @@ export const Segments = Object.freeze({
132
157
  dateOrEmpty(period.from),
133
158
  dateOrEmpty(period.to),
134
159
  null,
135
- period.continuation,
160
+ Verbatim.of(period.continuation),
136
161
  ]);
137
162
  },
138
163
 
139
164
  /** HKSPA: IBAN and BIC of all accounts. */
140
165
  sepaAccounts(version: number, continuation?: string): SegmentSpec {
141
- return new SegmentSpec('HKSPA', version, [null, ...(version >= 2 ? [null, continuation] : [])]);
166
+ return new SegmentSpec('HKSPA', version, [null, ...(version >= 2 ? [null, Verbatim.of(continuation)] : [])]);
142
167
  },
143
168
 
144
169
  /** HKTAB: all TAN media of all classes. */
@@ -155,6 +180,7 @@ export class TanResponse {
155
180
  constructor(
156
181
  /** The TAN process the segment answers: `4` asks for an approval, `S` answers a status request. */
157
182
  readonly process: string | undefined,
183
+ /** The order reference, as `Segment.verbatim` reads it, to send back byte for byte. */
158
184
  readonly reference: string | undefined,
159
185
  readonly challenge: string | undefined,
160
186
  readonly hhdUc: Uint8Array | undefined,
@@ -165,7 +191,7 @@ export class TanResponse {
165
191
  static parse(s: Segment): TanResponse {
166
192
  return new TanResponse(
167
193
  s.text(Hitan.process),
168
- s.text(Hitan.reference),
194
+ s.verbatim(Hitan.reference),
169
195
  s.text(Hitan.challenge),
170
196
  s.binary(Hitan.hhdUc),
171
197
  parseFintsDateTime(s.text(Hitan.validUntil, 0), s.text(Hitan.validUntil, 1)),
@@ -242,8 +268,17 @@ export function parseBalance(s: Segment, account: Account): Balance {
242
268
  * The account a balance (HISAL) belongs to: international (IBAN, BIC, then national) from version 7, national before.
243
269
  */
244
270
  export function balanceAccount(s: Segment): Account {
245
- const fields = s.texts(Hisal.account);
246
- const international = s.version >= 7;
271
+ return accountAt(s, Hisal.account, s.version >= 7);
272
+ }
273
+
274
+ /** The account camt transactions (HICAZ) belong to, which the first element names (international). */
275
+ export function camtAccount(s: Segment): Account {
276
+ return accountAt(s, 1, true);
277
+ }
278
+
279
+ /** The account at `index` of `s`: an `international` one (KTI), else a national one (KTV). */
280
+ function accountAt(s: Segment, index: number, international: boolean): Account {
281
+ const fields = s.texts(index);
247
282
  const national = international ? 2 : 0;
248
283
  return new Account({
249
284
  iban: international ? fields[0] : undefined,
@@ -295,8 +330,13 @@ export function parseSepaAccounts(s: Segment, defaultCountry: string, defaultBan
295
330
  */
296
331
  const accountWidth = 4;
297
332
 
298
- /** Parses the TAN media list (HITAB versions 4 and 5, PIN/TAN "TAN-Medium-Liste"). */
299
- export function parseTanMedia(s: Segment): TanMedium[] {
333
+ /**
334
+ * Parses the TAN media list (HITAB versions 4 and 5, PIN/TAN "TAN-Medium-Liste").
335
+ *
336
+ * Each name whose text is not what `Segment.verbatim` reads, such as a name in UTF-8, goes into `namesAsSent`, keyed
337
+ * by its text: it has to go back as the bank sent it, which its text does not tell where it fits ISO 8859-1.
338
+ */
339
+ export function parseTanMedia(s: Segment, namesAsSent?: Map<string, string>): TanMedium[] {
300
340
  // Version 5 adds the security function in front of the card number.
301
341
  const version5 = s.version >= 5;
302
342
  const cardNumber = version5 ? 3 : 2;
@@ -310,6 +350,9 @@ export function parseTanMedia(s: Segment): TanMedium[] {
310
350
  const fields = s.texts(i);
311
351
  const mediumClass = fields[0];
312
352
  if (mediumClass === undefined) continue;
353
+ const name = fields[after + 3];
354
+ const nameAsSent = s.verbatim(i, after + 3);
355
+ if (name !== undefined && nameAsSent !== undefined && nameAsSent !== name) namesAsSent?.set(name, nameAsSent);
313
356
  media.push(
314
357
  new TanMedium({
315
358
  mediumClass,
@@ -319,7 +362,7 @@ export function parseTanMedia(s: Segment): TanMedium[] {
319
362
  cardSequenceNumber: fields[cardNumber + 1],
320
363
  validFrom: parseFintsDate(fields[after]),
321
364
  validUntil: parseFintsDate(fields[after + 1]),
322
- name: fields[after + 3],
365
+ name,
323
366
  mobileNumberMasked: fields[after + 4],
324
367
  mobileNumber: fields[after + 5],
325
368
  lastUsed: parseFintsDate(fields[after + 13]),
package/src/v3/syntax.ts CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  import { BytesBuilder, latin1Decode, latin1Encode, utf8Encode } from '../util/bytes.js';
4
4
  import { FormatError } from '../util/errors.js';
5
- import { decodeBankText, tryParseInt } from '../util/text.js';
5
+ import { decodeBankText, isLatin1, tryParseInt } from '../util/text.js';
6
6
 
7
7
  // FinTS 3.0 syntax characters.
8
8
  const dataElementSeparator = 0x2b; // '+'
@@ -21,17 +21,30 @@ function isSyntaxCharacter(byte: number): boolean {
21
21
  );
22
22
  }
23
23
 
24
- /** One (group) data element of a received segment: unescaped text bytes or binary data. */
24
+ /**
25
+ * One (group) data element of a received segment: text or binary data, from `start` to `end` in `source`. A view or
26
+ * copy of its own would take more memory than most elements hold.
27
+ */
25
28
  export class DataElement {
26
29
  private decoded: string | undefined;
27
30
 
28
31
  constructor(
29
- readonly bytes: Uint8Array,
32
+ private readonly source: Uint8Array,
33
+ private readonly start: number,
34
+ private readonly end: number,
30
35
  readonly isBinary: boolean,
36
+ /** Whether the text holds escape characters, which `bytes` leaves out. */
37
+ private readonly escaped = false,
31
38
  ) {}
32
39
 
40
+ /** The binary data, or the text without escape characters. */
41
+ get bytes(): Uint8Array {
42
+ const bytes = this.source.subarray(this.start, this.end);
43
+ return this.escaped ? unescape(bytes) : bytes;
44
+ }
45
+
33
46
  get isEmpty(): boolean {
34
- return !this.isBinary && this.bytes.length === 0;
47
+ return !this.isBinary && this.start === this.end;
35
48
  }
36
49
 
37
50
  get text(): string {
@@ -46,7 +59,13 @@ export class DataElement {
46
59
  * Index 0 is the segment header (type, number, version, reference); the data elements follow from index 1.
47
60
  */
48
61
  export class Segment {
49
- constructor(readonly elements: readonly (readonly DataElement[])[]) {}
62
+ constructor(
63
+ readonly elements: readonly (readonly DataElement[])[],
64
+ /** The bytes the segment was read from: `start` to `end` in `source`, terminator included. */
65
+ private readonly source: Uint8Array,
66
+ private readonly start: number,
67
+ private readonly end: number,
68
+ ) {}
50
69
 
51
70
  get type(): string {
52
71
  return this.text(0)?.trim() ?? '';
@@ -74,10 +93,21 @@ export class Segment {
74
93
  return e.text;
75
94
  }
76
95
 
96
+ /** The binary data at `index`; `undefined` for an element that holds text. */
77
97
  binary(index: number, sub = 0): Uint8Array | undefined {
98
+ const e = this.element(index, sub);
99
+ return e?.isBinary === true ? e.bytes : undefined;
100
+ }
101
+
102
+ /**
103
+ * The element at `index` as the bank sent it, for a value the client sends back, such as the dialog ID: untrimmed,
104
+ * and one character per byte (ISO 8859-1, the FinTS 3.0 character set), so that `Verbatim` gives the bank back the
105
+ * very bytes it sent, in whatever encoding. `undefined` for an empty element.
106
+ */
107
+ verbatim(index: number, sub = 0): string | undefined {
78
108
  const e = this.element(index, sub);
79
109
  if (e === undefined || e.isEmpty) return undefined;
80
- return e.bytes;
110
+ return latin1Decode(e.bytes);
81
111
  }
82
112
 
83
113
  integer(index: number, sub = 0): number | undefined {
@@ -89,22 +119,9 @@ export class Segment {
89
119
  return this.group(index).map((e) => (e.isEmpty ? undefined : e.text));
90
120
  }
91
121
 
122
+ /** The bytes the segment was read from, as the bank sent them, terminator included. */
92
123
  encode(): Uint8Array {
93
- const out = new BytesBuilder();
94
- this.elements.forEach((g, i) => {
95
- if (i > 0) out.addByte(dataElementSeparator);
96
- g.forEach((e, j) => {
97
- if (j > 0) out.addByte(groupElementSeparator);
98
- if (e.isBinary) writeBinary(out, e.bytes);
99
- else writeEscaped(out, e.bytes);
100
- });
101
- });
102
- out.addByte(segmentTerminator);
103
- return out.takeBytes();
104
- }
105
-
106
- toString(): string {
107
- return decodeBankText(this.encode());
124
+ return this.source.subarray(this.start, this.end);
108
125
  }
109
126
  }
110
127
 
@@ -124,111 +141,154 @@ function writeBinary(out: BytesBuilder, bytes: Uint8Array): void {
124
141
 
125
142
  /**
126
143
  * Encodes `value` in ISO 8859-1, the FinTS 3.0 character set, or in UTF-8 where it does not fit: such text can only
127
- * come from a bank that sends UTF-8, such as a TAN medium named `Max’s iPhone`. The clients check the login data for
128
- * ISO 8859-1 up front.
144
+ * come from a bank that sends UTF-8, such as a TAN medium named `Max’s iPhone`. `FinTsConfig` checks the login data for
145
+ * ISO 8859-1 up front, and the client the PIN and the TAN.
146
+ *
147
+ * @internal Exported for the tests.
129
148
  */
130
149
  export function encodeText(value: string): Uint8Array {
131
150
  return isLatin1(value) ? latin1Encode(value) : utf8Encode(value);
132
151
  }
133
152
 
134
- /** Whether `value` fits into ISO 8859-1, the FinTS 3.0 character set. */
135
- export function isLatin1(value: string): boolean {
136
- for (let i = 0; i < value.length; i++) if (value.charCodeAt(i) > 0xff) return false;
137
- return true;
153
+ /**
154
+ * The data elements one message may hold, segment headers and group elements included: several times what the
155
+ * parameter data of a user with hundreds of accounts take, and few enough that a hostile message cannot exhaust the
156
+ * memory, at about `dataElementMemory` bytes each.
157
+ */
158
+ export const maxDataElements = 200_000;
159
+
160
+ /**
161
+ * The memory a data element of a received message takes, in bytes, with its share of the segment and of a return code:
162
+ * about this much in V8, however few bytes of the message it is made of.
163
+ */
164
+ export const dataElementMemory = 256;
165
+
166
+ /** Every empty data element of a received segment; it has no bytes to change. */
167
+ const emptyElement = new DataElement(new Uint8Array(0), 0, 0, false);
168
+
169
+ function isSeparator(byte: number | undefined): boolean {
170
+ return byte === dataElementSeparator || byte === groupElementSeparator || byte === segmentTerminator;
171
+ }
172
+
173
+ /** Line breaks and spaces between segments are padding, not content. */
174
+ function isPadding(byte: number): boolean {
175
+ return byte === 0x0a || byte === 0x0d || byte === 0x20;
176
+ }
177
+
178
+ function isDigit(byte: number | undefined): boolean {
179
+ return byte !== undefined && byte >= 0x30 && byte <= 0x39;
138
180
  }
139
181
 
140
182
  /**
141
183
  * Splits a FinTS 3.0 message into segments, resolving escapes and binary data (`@length@bytes`).
142
184
  *
143
- * Throws `FormatError` for a negative binary length.
185
+ * The elements share the memory of `data`. Throws `FormatError` for anything a complete message cannot hold: a
186
+ * malformed binary length, binary data beyond the end of `data` or not followed by a separator, a last segment without
187
+ * terminator, and more elements than `budget` has left, which parses of the same message share.
144
188
  */
145
- export function parseSegments(data: Uint8Array): Segment[] {
189
+ export function parseSegments(data: Uint8Array, budget = { elements: maxDataElements }): Segment[] {
146
190
  const segments: Segment[] = [];
147
- let elements: DataElement[][] = [];
148
- let group: DataElement[] = [];
149
- let current: number[] = [];
150
- let binary: Uint8Array | undefined;
151
- let segmentStarted = false;
152
-
153
- const endElement = (): void => {
154
- group.push(binary !== undefined ? new DataElement(binary, true) : new DataElement(Uint8Array.from(current), false));
155
- current = [];
156
- binary = undefined;
191
+ // Created with their first element: an array that starts empty reserves room for more than most of them hold.
192
+ let elements: DataElement[][] | undefined;
193
+ let group: DataElement[] | undefined;
194
+ let segmentStart = 0;
195
+ let i = 0;
196
+
197
+ const readBinary = (): DataElement => {
198
+ let end = i + 1;
199
+ let length = 0;
200
+ while (isDigit(data[end])) {
201
+ length = length * 10 + data[end++]! - 0x30;
202
+ if (length > data.length) throw new FormatError('Binary data beyond the end of the message');
203
+ }
204
+ if (end === i + 1 || data[end] !== binaryMarker) throw new FormatError('Malformed binary length');
205
+ const start = end + 1;
206
+ if (length > data.length - start) throw new FormatError('Binary data beyond the end of the message');
207
+ i = start + length;
208
+ if (i < data.length && !isSeparator(data[i])) throw new FormatError('Binary data not followed by a separator');
209
+ return new DataElement(data, start, i, true);
157
210
  };
158
211
 
159
- const endSegment = (): void => {
160
- endElement();
161
- elements.push(group);
162
- const segment = new Segment(elements);
163
- // Skip empty segments, such as a stray double terminator.
164
- if (segment.type !== '') segments.push(segment);
165
- elements = [];
166
- group = [];
167
- segmentStarted = false;
212
+ const readText = (): DataElement => {
213
+ const start = i;
214
+ let escaped = false;
215
+ while (i < data.length && !isSeparator(data[i])) {
216
+ if (data[i] === escapeCharacter) {
217
+ escaped = true;
218
+ i++;
219
+ }
220
+ i++;
221
+ }
222
+ if (i > data.length) throw new FormatError('Escape character at the end of the message');
223
+ return i === start ? emptyElement : new DataElement(data, start, i, false, escaped);
168
224
  };
169
225
 
170
- let i = 0;
171
226
  while (i < data.length) {
172
- const c = data[i]!;
173
- if (c === escapeCharacter) {
174
- if (i + 1 < data.length) current.push(data[i + 1]!);
175
- segmentStarted = true;
176
- i += 2;
227
+ if (elements === undefined && group === undefined && isPadding(data[i]!)) {
228
+ i++;
177
229
  continue;
178
230
  }
179
- if (c === binaryMarker && current.length === 0 && binary === undefined) {
180
- const end = data.indexOf(binaryMarker, i + 1);
181
- const length = end > i + 1 ? tryParseInt(latin1Decode(data.subarray(i + 1, end))) : undefined;
182
- if (length !== undefined && length < 0) throw new FormatError('Negative binary length');
183
- // Compared this way round, a huge length cannot overflow the sum.
184
- if (length !== undefined && length <= data.length - (end + 1)) {
185
- binary = data.subarray(end + 1, end + 1 + length);
186
- segmentStarted = true;
187
- i = end + 1 + length;
188
- continue;
189
- }
231
+ if (--budget.elements < 0) throw new FormatError('Too many data elements');
232
+ if (elements === undefined && group === undefined) segmentStart = i;
233
+ const element = data[i] === binaryMarker ? readBinary() : readText();
234
+ if (group === undefined) group = [element];
235
+ else group.push(element);
236
+ const separator = data[i++];
237
+ if (separator === groupElementSeparator) continue;
238
+ if (elements === undefined) elements = [group];
239
+ else elements.push(group);
240
+ group = undefined;
241
+ if (separator === segmentTerminator) {
242
+ const segment = new Segment(elements, data, segmentStart, i);
243
+ // Skip empty segments, such as a stray double terminator.
244
+ if (segment.type !== '') segments.push(segment);
245
+ elements = undefined;
190
246
  }
191
- if (c === groupElementSeparator) {
192
- endElement();
193
- } else if (c === dataElementSeparator) {
194
- endElement();
195
- elements.push(group);
196
- group = [];
197
- } else if (c === segmentTerminator) {
198
- endSegment();
199
- } else {
200
- // Line breaks and spaces between segments are padding, not content.
201
- const isPadding =
202
- !segmentStarted &&
203
- current.length === 0 &&
204
- elements.length === 0 &&
205
- group.length === 0 &&
206
- (c === 0x0a || c === 0x0d || c === 0x20);
207
- if (!isPadding) {
208
- current.push(c);
209
- segmentStarted = true;
210
- }
211
- }
212
- i++;
213
247
  }
214
- if (segmentStarted && current.length > 0) endSegment();
248
+ if (elements !== undefined || group !== undefined) throw new FormatError('Segment without terminator');
215
249
  return segments;
216
250
  }
217
251
 
252
+ /** `text` without the escape characters in front of syntax characters. */
253
+ function unescape(text: Uint8Array): Uint8Array {
254
+ const out = new Uint8Array(text.length);
255
+ let o = 0;
256
+ for (let j = 0; j < text.length; j++) {
257
+ if (text[j] === escapeCharacter) j++;
258
+ out[o++] = text[j]!;
259
+ }
260
+ return out.subarray(0, o);
261
+ }
262
+
218
263
  /** Binary data in a segment to send, written as `@length@bytes`. */
219
264
  export class Binary {
220
265
  constructor(readonly bytes: Uint8Array) {}
221
266
  }
222
267
 
268
+ /**
269
+ * A value the bank issued, such as the dialog ID, to send back as `Segment.verbatim` reads it: escaped, but not
270
+ * trimmed, so that the bank gets back the very bytes it sent. Text from elsewhere, such as a TAN medium name the app
271
+ * passes, goes out as `encodeText` encodes it.
272
+ */
273
+ export class Verbatim {
274
+ constructor(readonly text: string) {}
275
+
276
+ /** `text` as a `Verbatim`; `undefined` stays empty. */
277
+ static of(text: string | undefined): Verbatim | undefined {
278
+ return text === undefined ? undefined : new Verbatim(text);
279
+ }
280
+ }
281
+
223
282
  /** A value of a segment to send; see `SegmentSpec`. */
224
- export type SegmentValue = string | number | boolean | Binary | null | undefined | readonly SegmentValue[];
283
+ export type SegmentValue = string | number | boolean | Binary | Verbatim | null | undefined | readonly SegmentValue[];
225
284
 
226
285
  /**
227
286
  * A segment to send.
228
287
  *
229
288
  * `elements` holds the data elements after the header: `null` or `undefined` for an empty element, an array for a data
230
- * element group, a boolean for `J`/`N`, `Binary` for binary data and anything else as its text, without leading and
231
- * trailing blanks. Trailing empty elements are left out, as the specification asks.
289
+ * element group, a boolean for `J`/`N`, `Binary` for binary data, `Verbatim` for a value the bank issued, and anything
290
+ * else as its text, without leading and trailing blanks. Trailing empty elements are left out, as the specification
291
+ * asks.
232
292
  */
233
293
  export class SegmentSpec {
234
294
  constructor(
@@ -296,6 +356,8 @@ function encodeValue(value: SingleValue): Uint8Array {
296
356
  // Empty.
297
357
  } else if (value instanceof Binary) {
298
358
  writeBinary(out, value.bytes);
359
+ } else if (value instanceof Verbatim) {
360
+ writeEscaped(out, encodeText(value.text));
299
361
  } else if (typeof value === 'boolean') {
300
362
  out.addByte(value ? 0x4a : 0x4e); // 'J' or 'N'
301
363
  } else {
@@ -1,28 +0,0 @@
1
- import type { MessageDirection, MessageLogger } from '../core/client.js';
2
- import { type FinTsTransport } from '../core/transport.js';
3
- /**
4
- * Throws `FinTsProtocolError` if a message of `size` bytes exceeds the bank's `limit` in kilobytes (1024 bytes): the
5
- * specification does not allow to send such a message.
6
- */
7
- export declare function checkMessageSize(size: number, limit: number | undefined): void;
8
- /** The bank's FinTS endpoint: posts messages there and reports them to the logger. */
9
- export declare class BankConnection {
10
- /** Where the messages go. */
11
- readonly url: URL;
12
- private readonly logger;
13
- private readonly transport;
14
- /** A connection to `url` through `transport`, an `HttpTransport` by default. */
15
- constructor(
16
- /** Where the messages go. */
17
- url: URL, transport: FinTsTransport | undefined, logger: MessageLogger | undefined);
18
- /**
19
- * Posts `body`; reports any failure of the transport as `FinTsError`, with the original as its `cause`: a custom
20
- * transport that calls `fetch` fails with a `TypeError` where the network does.
21
- */
22
- post(body: Uint8Array, contentType: string): Promise<Uint8Array>;
23
- /** Reports a message to the logger; `message` is only built when a logger is set. */
24
- log(direction: MessageDirection, message: () => string): void;
25
- /** Closes the transport. */
26
- close(): void;
27
- }
28
- //# sourceMappingURL=bank-connection.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"bank-connection.d.ts","sourceRoot":"","sources":["../../src/session/bank-connection.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,aAAa,EAAE,MAAM,mBAAmB,CAAC;AAEzE,OAAO,EAAiB,KAAK,cAAc,EAAE,MAAM,sBAAsB,CAAC;AAE1E;;;GAGG;AACH,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,SAAS,GAAG,IAAI,CAI9E;AAED,sFAAsF;AACtF,qBAAa,cAAc;IAKvB,6BAA6B;IAC7B,QAAQ,CAAC,GAAG,EAAE,GAAG;IAEjB,OAAO,CAAC,QAAQ,CAAC,MAAM;IAPzB,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAiB;IAE3C,gFAAgF;;IAE9E,6BAA6B;IACpB,GAAG,EAAE,GAAG,EACjB,SAAS,EAAE,cAAc,GAAG,SAAS,EACpB,MAAM,EAAE,aAAa,GAAG,SAAS;IAKpD;;;OAGG;IACG,IAAI,CAAC,IAAI,EAAE,UAAU,EAAE,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,CAAC;IAStE,qFAAqF;IACrF,GAAG,CAAC,SAAS,EAAE,gBAAgB,EAAE,OAAO,EAAE,MAAM,MAAM,GAAG,IAAI;IAI7D,4BAA4B;IAC5B,KAAK,IAAI,IAAI;CAGd"}
@@ -1,48 +0,0 @@
1
- import { FinTsError, FinTsProtocolError, FinTsTransportError } from '../core/errors.js';
2
- import { HttpTransport } from '../core/transport.js';
3
- /**
4
- * Throws `FinTsProtocolError` if a message of `size` bytes exceeds the bank's `limit` in kilobytes (1024 bytes): the
5
- * specification does not allow to send such a message.
6
- */
7
- export function checkMessageSize(size, limit) {
8
- if (limit !== undefined && limit > 0 && size > limit * 1024) {
9
- throw new FinTsProtocolError(`A message of ${size} bytes exceeds the bank's maximum message size of ${limit} KB`);
10
- }
11
- }
12
- /** The bank's FinTS endpoint: posts messages there and reports them to the logger. */
13
- export class BankConnection {
14
- url;
15
- logger;
16
- transport;
17
- /** A connection to `url` through `transport`, an `HttpTransport` by default. */
18
- constructor(
19
- /** Where the messages go. */
20
- url, transport, logger) {
21
- this.url = url;
22
- this.logger = logger;
23
- this.transport = transport ?? new HttpTransport();
24
- }
25
- /**
26
- * Posts `body`; reports any failure of the transport as `FinTsError`, with the original as its `cause`: a custom
27
- * transport that calls `fetch` fails with a `TypeError` where the network does.
28
- */
29
- async post(body, contentType) {
30
- try {
31
- return await this.transport.post(this.url, body, contentType);
32
- }
33
- catch (error) {
34
- if (error instanceof FinTsError)
35
- throw error;
36
- throw new FinTsTransportError(error instanceof Error ? error.message : String(error), { cause: error });
37
- }
38
- }
39
- /** Reports a message to the logger; `message` is only built when a logger is set. */
40
- log(direction, message) {
41
- this.logger?.(direction, message());
42
- }
43
- /** Closes the transport. */
44
- close() {
45
- this.transport.close();
46
- }
47
- }
48
- //# sourceMappingURL=bank-connection.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"bank-connection.js","sourceRoot":"","sources":["../../src/session/bank-connection.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,UAAU,EAAE,kBAAkB,EAAE,mBAAmB,EAAE,MAAM,mBAAmB,CAAC;AACxF,OAAO,EAAE,aAAa,EAAuB,MAAM,sBAAsB,CAAC;AAE1E;;;GAGG;AACH,MAAM,UAAU,gBAAgB,CAAC,IAAY,EAAE,KAAyB;IACtE,IAAI,KAAK,KAAK,SAAS,IAAI,KAAK,GAAG,CAAC,IAAI,IAAI,GAAG,KAAK,GAAG,IAAI,EAAE,CAAC;QAC5D,MAAM,IAAI,kBAAkB,CAAC,gBAAgB,IAAI,qDAAqD,KAAK,KAAK,CAAC,CAAC;IACpH,CAAC;AACH,CAAC;AAED,sFAAsF;AACtF,MAAM,OAAO,cAAc;IAMd;IAEQ;IAPF,SAAS,CAAiB;IAE3C,gFAAgF;IAChF;IACE,6BAA6B;IACpB,GAAQ,EACjB,SAAqC,EACpB,MAAiC;QAFzC,QAAG,GAAH,GAAG,CAAK;QAEA,WAAM,GAAN,MAAM,CAA2B;QAElD,IAAI,CAAC,SAAS,GAAG,SAAS,IAAI,IAAI,aAAa,EAAE,CAAC;IACpD,CAAC;IAED;;;OAGG;IACH,KAAK,CAAC,IAAI,CAAC,IAAgB,EAAE,WAAmB;QAC9C,IAAI,CAAC;YACH,OAAO,MAAM,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,IAAI,EAAE,WAAW,CAAC,CAAC;QAChE,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,IAAI,KAAK,YAAY,UAAU;gBAAE,MAAM,KAAK,CAAC;YAC7C,MAAM,IAAI,mBAAmB,CAAC,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,EAAE,EAAE,KAAK,EAAE,KAAK,EAAE,CAAC,CAAC;QAC1G,CAAC;IACH,CAAC;IAED,qFAAqF;IACrF,GAAG,CAAC,SAA2B,EAAE,OAAqB;QACpD,IAAI,CAAC,MAAM,EAAE,CAAC,SAAS,EAAE,OAAO,EAAE,CAAC,CAAC;IACtC,CAAC;IAED,4BAA4B;IAC5B,KAAK;QACH,IAAI,CAAC,SAAS,CAAC,KAAK,EAAE,CAAC;IACzB,CAAC;CACF"}