@coderoftime/fints 0.1.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (289) hide show
  1. package/CHANGELOG.md +519 -1
  2. package/README.md +138 -40
  3. package/dist/client.d.ts +49 -0
  4. package/dist/client.d.ts.map +1 -0
  5. package/dist/client.js +242 -0
  6. package/dist/client.js.map +1 -0
  7. package/dist/core/account.d.ts +46 -7
  8. package/dist/core/account.d.ts.map +1 -1
  9. package/dist/core/account.js +143 -24
  10. package/dist/core/account.js.map +1 -1
  11. package/dist/core/amount.d.ts +15 -4
  12. package/dist/core/amount.d.ts.map +1 -1
  13. package/dist/core/amount.js +46 -9
  14. package/dist/core/amount.js.map +1 -1
  15. package/dist/core/balance.d.ts +4 -0
  16. package/dist/core/balance.d.ts.map +1 -1
  17. package/dist/core/balance.js +11 -5
  18. package/dist/core/balance.js.map +1 -1
  19. package/dist/core/bank-info.d.ts +21 -4
  20. package/dist/core/bank-info.d.ts.map +1 -1
  21. package/dist/core/bank-info.js +26 -5
  22. package/dist/core/bank-info.js.map +1 -1
  23. package/dist/core/client.d.ts +81 -19
  24. package/dist/core/client.d.ts.map +1 -1
  25. package/dist/core/config.d.ts +40 -20
  26. package/dist/core/config.d.ts.map +1 -1
  27. package/dist/core/config.js +85 -30
  28. package/dist/core/config.js.map +1 -1
  29. package/dist/core/errors.d.ts +100 -26
  30. package/dist/core/errors.d.ts.map +1 -1
  31. package/dist/core/errors.js +136 -53
  32. package/dist/core/errors.js.map +1 -1
  33. package/dist/core/feedback.d.ts +11 -1
  34. package/dist/core/feedback.d.ts.map +1 -1
  35. package/dist/core/feedback.js +14 -2
  36. package/dist/core/feedback.js.map +1 -1
  37. package/dist/core/transaction.d.ts +11 -1
  38. package/dist/core/transaction.d.ts.map +1 -1
  39. package/dist/core/transaction.js +21 -4
  40. package/dist/core/transaction.js.map +1 -1
  41. package/dist/core/transport.d.ts +48 -6
  42. package/dist/core/transport.d.ts.map +1 -1
  43. package/dist/core/transport.js +126 -29
  44. package/dist/core/transport.js.map +1 -1
  45. package/dist/formats/camt.d.ts +28 -5
  46. package/dist/formats/camt.d.ts.map +1 -1
  47. package/dist/formats/camt.js +132 -50
  48. package/dist/formats/camt.js.map +1 -1
  49. package/dist/formats/mt940.d.ts +29 -5
  50. package/dist/formats/mt940.d.ts.map +1 -1
  51. package/dist/formats/mt940.js +209 -113
  52. package/dist/formats/mt940.js.map +1 -1
  53. package/dist/formats/readers.d.ts +6 -2
  54. package/dist/formats/readers.d.ts.map +1 -1
  55. package/dist/formats/readers.js +28 -29
  56. package/dist/formats/readers.js.map +1 -1
  57. package/dist/formats/record-budget.d.ts +7 -0
  58. package/dist/formats/record-budget.d.ts.map +1 -0
  59. package/dist/formats/record-budget.js +17 -0
  60. package/dist/formats/record-budget.js.map +1 -0
  61. package/dist/formats/sepa-purpose.d.ts +9 -0
  62. package/dist/formats/sepa-purpose.d.ts.map +1 -1
  63. package/dist/formats/sepa-purpose.js +32 -2
  64. package/dist/formats/sepa-purpose.js.map +1 -1
  65. package/dist/index.d.ts +3 -3
  66. package/dist/index.d.ts.map +1 -1
  67. package/dist/index.js +2 -2
  68. package/dist/index.js.map +1 -1
  69. package/dist/session/app-approval.d.ts +11 -5
  70. package/dist/session/app-approval.d.ts.map +1 -1
  71. package/dist/session/app-approval.js +23 -12
  72. package/dist/session/app-approval.js.map +1 -1
  73. package/dist/session/dialog-runner.d.ts +11 -11
  74. package/dist/session/dialog-runner.d.ts.map +1 -1
  75. package/dist/session/dialog-runner.js +34 -19
  76. package/dist/session/dialog-runner.js.map +1 -1
  77. package/dist/session/login-guard.d.ts +54 -49
  78. package/dist/session/login-guard.d.ts.map +1 -1
  79. package/dist/session/login-guard.js +170 -83
  80. package/dist/session/login-guard.js.map +1 -1
  81. package/dist/session/sepa-accounts.d.ts +20 -14
  82. package/dist/session/sepa-accounts.d.ts.map +1 -1
  83. package/dist/session/sepa-accounts.js +62 -46
  84. package/dist/session/sepa-accounts.js.map +1 -1
  85. package/dist/session/session-setup.d.ts +41 -24
  86. package/dist/session/session-setup.d.ts.map +1 -1
  87. package/dist/session/session-setup.js +106 -63
  88. package/dist/session/session-setup.js.map +1 -1
  89. package/dist/session/session-state.d.ts +10 -6
  90. package/dist/session/session-state.d.ts.map +1 -1
  91. package/dist/session/session-state.js +72 -18
  92. package/dist/session/session-state.js.map +1 -1
  93. package/dist/session/signed-dialog.d.ts +83 -0
  94. package/dist/session/signed-dialog.d.ts.map +1 -0
  95. package/dist/session/signed-dialog.js +185 -0
  96. package/dist/session/signed-dialog.js.map +1 -0
  97. package/dist/session/tan-selection.d.ts +26 -18
  98. package/dist/session/tan-selection.d.ts.map +1 -1
  99. package/dist/session/tan-selection.js +44 -33
  100. package/dist/session/tan-selection.js.map +1 -1
  101. package/dist/session/user-prompts.d.ts +14 -5
  102. package/dist/session/user-prompts.d.ts.map +1 -1
  103. package/dist/session/user-prompts.js +53 -18
  104. package/dist/session/user-prompts.js.map +1 -1
  105. package/dist/tan/challenge.d.ts +47 -24
  106. package/dist/tan/challenge.d.ts.map +1 -1
  107. package/dist/tan/challenge.js +56 -51
  108. package/dist/tan/challenge.js.map +1 -1
  109. package/dist/tan/flicker.d.ts +11 -1
  110. package/dist/tan/flicker.d.ts.map +1 -1
  111. package/dist/tan/flicker.js +70 -24
  112. package/dist/tan/flicker.js.map +1 -1
  113. package/dist/tan/tan-image.d.ts +6 -2
  114. package/dist/tan/tan-image.d.ts.map +1 -1
  115. package/dist/tan/tan-image.js +14 -6
  116. package/dist/tan/tan-image.js.map +1 -1
  117. package/dist/tan/tan-models.d.ts +14 -2
  118. package/dist/tan/tan-models.d.ts.map +1 -1
  119. package/dist/tan/tan-models.js +24 -7
  120. package/dist/tan/tan-models.js.map +1 -1
  121. package/dist/util/arguments.d.ts +13 -0
  122. package/dist/util/arguments.d.ts.map +1 -0
  123. package/dist/util/arguments.js +40 -0
  124. package/dist/util/arguments.js.map +1 -0
  125. package/dist/util/async.d.ts +4 -3
  126. package/dist/util/async.d.ts.map +1 -1
  127. package/dist/util/async.js +6 -7
  128. package/dist/util/async.js.map +1 -1
  129. package/dist/util/bytes.d.ts +3 -4
  130. package/dist/util/bytes.d.ts.map +1 -1
  131. package/dist/util/bytes.js +13 -21
  132. package/dist/util/bytes.js.map +1 -1
  133. package/dist/util/call-context.d.ts.map +1 -1
  134. package/dist/util/call-context.js +3 -2
  135. package/dist/util/call-context.js.map +1 -1
  136. package/dist/util/dates.d.ts +6 -20
  137. package/dist/util/dates.d.ts.map +1 -1
  138. package/dist/util/dates.js +58 -28
  139. package/dist/util/dates.js.map +1 -1
  140. package/dist/util/errors.d.ts +13 -3
  141. package/dist/util/errors.d.ts.map +1 -1
  142. package/dist/util/errors.js +19 -6
  143. package/dist/util/errors.js.map +1 -1
  144. package/dist/util/immutable-map.d.ts +5 -2
  145. package/dist/util/immutable-map.d.ts.map +1 -1
  146. package/dist/util/immutable-map.js +15 -12
  147. package/dist/util/immutable-map.js.map +1 -1
  148. package/dist/util/inspect.d.ts +11 -0
  149. package/dist/util/inspect.d.ts.map +1 -0
  150. package/dist/util/inspect.js +14 -0
  151. package/dist/util/inspect.js.map +1 -0
  152. package/dist/util/json.d.ts +4 -0
  153. package/dist/util/json.d.ts.map +1 -1
  154. package/dist/util/json.js +18 -0
  155. package/dist/util/json.js.map +1 -1
  156. package/dist/util/text.d.ts +38 -5
  157. package/dist/util/text.d.ts.map +1 -1
  158. package/dist/util/text.js +90 -16
  159. package/dist/util/text.js.map +1 -1
  160. package/dist/util/xml.d.ts +9 -6
  161. package/dist/util/xml.d.ts.map +1 -1
  162. package/dist/util/xml.js +129 -56
  163. package/dist/util/xml.js.map +1 -1
  164. package/dist/v3/account-extension.d.ts.map +1 -1
  165. package/dist/v3/account-extension.js +7 -10
  166. package/dist/v3/account-extension.js.map +1 -1
  167. package/dist/v3/bank-connection.d.ts +54 -0
  168. package/dist/v3/bank-connection.d.ts.map +1 -0
  169. package/dist/v3/bank-connection.js +118 -0
  170. package/dist/v3/bank-connection.js.map +1 -0
  171. package/dist/v3/dialog.d.ts +84 -46
  172. package/dist/v3/dialog.d.ts.map +1 -1
  173. package/dist/v3/dialog.js +174 -60
  174. package/dist/v3/dialog.js.map +1 -1
  175. package/dist/v3/message.d.ts +32 -11
  176. package/dist/v3/message.d.ts.map +1 -1
  177. package/dist/v3/message.js +144 -83
  178. package/dist/v3/message.js.map +1 -1
  179. package/dist/v3/parameter-store.d.ts +111 -0
  180. package/dist/v3/parameter-store.d.ts.map +1 -0
  181. package/dist/v3/parameter-store.js +197 -0
  182. package/dist/v3/parameter-store.js.map +1 -0
  183. package/dist/v3/parameters.d.ts +44 -17
  184. package/dist/v3/parameters.d.ts.map +1 -1
  185. package/dist/v3/parameters.js +136 -60
  186. package/dist/v3/parameters.js.map +1 -1
  187. package/dist/v3/protocol.d.ts +50 -50
  188. package/dist/v3/protocol.d.ts.map +1 -1
  189. package/dist/v3/protocol.js +91 -210
  190. package/dist/v3/protocol.js.map +1 -1
  191. package/dist/v3/queries.d.ts +51 -0
  192. package/dist/v3/queries.d.ts.map +1 -0
  193. package/dist/v3/queries.js +128 -0
  194. package/dist/v3/queries.js.map +1 -0
  195. package/dist/v3/segments.d.ts +43 -19
  196. package/dist/v3/segments.d.ts.map +1 -1
  197. package/dist/v3/segments.js +84 -33
  198. package/dist/v3/segments.js.map +1 -1
  199. package/dist/v3/syntax.d.ts +68 -13
  200. package/dist/v3/syntax.d.ts.map +1 -1
  201. package/dist/v3/syntax.js +183 -102
  202. package/dist/v3/syntax.js.map +1 -1
  203. package/package.json +10 -6
  204. package/src/client.ts +293 -0
  205. package/src/core/account.ts +153 -24
  206. package/src/core/amount.ts +45 -10
  207. package/src/core/balance.ts +12 -5
  208. package/src/core/bank-info.ts +29 -5
  209. package/src/core/client.ts +83 -19
  210. package/src/core/config.ts +95 -35
  211. package/src/core/errors.ts +148 -54
  212. package/src/core/feedback.ts +16 -2
  213. package/src/core/transaction.ts +22 -4
  214. package/src/core/transport.ts +153 -32
  215. package/src/formats/camt.ts +153 -45
  216. package/src/formats/mt940.ts +244 -122
  217. package/src/formats/readers.ts +28 -31
  218. package/src/formats/record-budget.ts +18 -0
  219. package/src/formats/sepa-purpose.ts +29 -2
  220. package/src/index.ts +4 -2
  221. package/src/session/app-approval.ts +51 -38
  222. package/src/session/dialog-runner.ts +61 -31
  223. package/src/session/login-guard.ts +189 -87
  224. package/src/session/sepa-accounts.ts +63 -45
  225. package/src/session/session-setup.ts +105 -68
  226. package/src/session/session-state.ts +74 -23
  227. package/src/session/signed-dialog.ts +222 -0
  228. package/src/session/tan-selection.ts +50 -39
  229. package/src/session/user-prompts.ts +62 -18
  230. package/src/tan/challenge.ts +87 -64
  231. package/src/tan/flicker.ts +72 -23
  232. package/src/tan/tan-image.ts +15 -6
  233. package/src/tan/tan-models.ts +26 -7
  234. package/src/util/arguments.ts +40 -0
  235. package/src/util/async.ts +10 -9
  236. package/src/util/bytes.ts +14 -20
  237. package/src/util/call-context.ts +3 -2
  238. package/src/util/dates.ts +64 -33
  239. package/src/util/errors.ts +22 -6
  240. package/src/util/immutable-map.ts +15 -12
  241. package/src/util/inspect.ts +14 -0
  242. package/src/util/json.ts +16 -0
  243. package/src/util/text.ts +87 -15
  244. package/src/util/xml.ts +130 -52
  245. package/src/v3/account-extension.ts +7 -12
  246. package/src/v3/bank-connection.ts +126 -0
  247. package/src/v3/dialog.ts +186 -92
  248. package/src/v3/message.ts +140 -79
  249. package/src/v3/parameter-store.ts +226 -0
  250. package/src/v3/parameters.ts +166 -66
  251. package/src/v3/protocol.ts +103 -253
  252. package/src/v3/queries.ts +198 -0
  253. package/src/v3/segments.ts +86 -31
  254. package/src/v3/syntax.ts +172 -93
  255. package/dist/session/bank-connection.d.ts +0 -28
  256. package/dist/session/bank-connection.d.ts.map +0 -1
  257. package/dist/session/bank-connection.js +0 -48
  258. package/dist/session/bank-connection.js.map +0 -1
  259. package/dist/session/parameter-data.d.ts +0 -83
  260. package/dist/session/parameter-data.d.ts.map +0 -1
  261. package/dist/session/parameter-data.js +0 -68
  262. package/dist/session/parameter-data.js.map +0 -1
  263. package/dist/session/pin-tan-client.d.ts +0 -54
  264. package/dist/session/pin-tan-client.d.ts.map +0 -1
  265. package/dist/session/pin-tan-client.js +0 -153
  266. package/dist/session/pin-tan-client.js.map +0 -1
  267. package/dist/session/pin-tan-dialog.d.ts +0 -71
  268. package/dist/session/pin-tan-dialog.d.ts.map +0 -1
  269. package/dist/session/pin-tan-dialog.js +0 -124
  270. package/dist/session/pin-tan-dialog.js.map +0 -1
  271. package/dist/session/protocol.d.ts +0 -209
  272. package/dist/session/protocol.d.ts.map +0 -1
  273. package/dist/session/protocol.js +0 -75
  274. package/dist/session/protocol.js.map +0 -1
  275. package/dist/session/response-handler.d.ts +0 -48
  276. package/dist/session/response-handler.d.ts.map +0 -1
  277. package/dist/session/response-handler.js +0 -121
  278. package/dist/session/response-handler.js.map +0 -1
  279. package/dist/v3/client.d.ts +0 -24
  280. package/dist/v3/client.d.ts.map +0 -1
  281. package/dist/v3/client.js +0 -16
  282. package/dist/v3/client.js.map +0 -1
  283. package/src/session/bank-connection.ts +0 -51
  284. package/src/session/parameter-data.ts +0 -137
  285. package/src/session/pin-tan-client.ts +0 -186
  286. package/src/session/pin-tan-dialog.ts +0 -160
  287. package/src/session/protocol.ts +0 -311
  288. package/src/session/response-handler.ts +0 -130
  289. package/src/v3/client.ts +0 -29
@@ -3,25 +3,37 @@
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 { trimBlanks } from '../util/text.js';
10
+ import { type Segment, SegmentSpec, type SegmentValue, Verbatim } from './syntax.js';
11
+
12
+ /**
13
+ * The filler the specification names for a reference that does not exist.
14
+ *
15
+ * The bank puts it into a dummy answer when no approval is needed. The client sends it as the TAN medium name in the
16
+ * dialog that looks up the TAN media, before it knows a medium; the bank ignores it there (PIN/TAN B.4.3.1.3).
17
+ */
18
+ export const noReference = 'noref';
11
19
 
12
20
  /** Bank identification (KIK): country code and bank code. */
13
- export function bankIdentification(countryCode: string, bankCode: string): SegmentValue {
21
+ function bankIdentification(countryCode: string, bankCode: string): SegmentValue {
14
22
  return [countryCode, bankCode];
15
23
  }
16
24
 
17
25
  /** National account (KTV): account number, sub-account and bank identification. */
18
- export function nationalAccount(a: Account): SegmentValue[] {
26
+ function nationalAccount(a: Account): SegmentValue[] {
19
27
  return [a.accountNumber, a.subAccount, a.countryCode, a.bankCode];
20
28
  }
21
29
 
22
- /** International account (KTI): IBAN, BIC, then the national account. */
23
- export function internationalAccount(a: Account): SegmentValue[] {
24
- return [a.iban, a.bic, ...nationalAccount(a)];
30
+ /**
31
+ * International account (KTI): IBAN and BIC, then the national account where the bank allows it (HISPAS). An IBAN goes
32
+ * only with its BIC (Messages B.3.2): without one the national account names the account, where it has a number.
33
+ */
34
+ function internationalAccount(a: Account, nationalAllowed: boolean): SegmentValue[] {
35
+ const iban = a.bic !== undefined || a.accountNumber === undefined ? a.iban : undefined;
36
+ return [iban, a.bic, ...(nationalAllowed || iban === undefined ? nationalAccount(a) : [])];
25
37
  }
26
38
 
27
39
  function dateOrEmpty(date: Date | undefined): string | undefined {
@@ -33,7 +45,10 @@ const orderReferenceField = 5;
33
45
  const moreTansField = 6;
34
46
  const tanMediumField = 11;
35
47
 
36
- /** Builders for the request segments the client sends. */
48
+ /**
49
+ * Builders for the request segments the client sends. Values the bank issued, such as the customer system ID, the TAN
50
+ * order reference and the position to continue at, go back as `Verbatim`.
51
+ */
37
52
  export const Segments = Object.freeze({
38
53
  /** HKIDN: who is logging in, from which customer system. */
39
54
  identification(parts: {
@@ -46,7 +61,7 @@ export const Segments = Object.freeze({
46
61
  return new SegmentSpec('HKIDN', 2, [
47
62
  bankIdentification(parts.countryCode, parts.bankCode),
48
63
  parts.customerId,
49
- parts.systemId,
64
+ new Verbatim(parts.systemId),
50
65
  parts.systemIdRequired ? 1 : 0,
51
66
  ]);
52
67
  },
@@ -74,71 +89,90 @@ export const Segments = Object.freeze({
74
89
  },
75
90
 
76
91
  dialogEnd(dialogId: string): SegmentSpec {
77
- return new SegmentSpec('HKEND', 1, [dialogId]);
92
+ return new SegmentSpec('HKEND', 1, [new Verbatim(dialogId)]);
78
93
  },
79
94
 
80
- /** HKTAN process 4: asks for strong customer authentication of the `segmentType` sent in the same message. */
81
- tanProcess4(version: number, segmentType: string, mediumName?: string): SegmentSpec {
95
+ /**
96
+ * HKTAN process 4: asks for strong customer authentication of the `segmentType` sent in the same message, with the TAN
97
+ * medium `mediumName`: a `Verbatim` where it has to go back as the bank sent it (`ParameterStore.tanMediumToSend`).
98
+ */
99
+ tanProcess4(version: number, segmentType: string, mediumName?: string | Verbatim): SegmentSpec {
82
100
  return SegmentSpec.numbered('HKTAN', version, { 1: '4', 2: segmentType, [tanMediumField]: mediumName });
83
101
  },
84
102
 
85
103
  /** HKTAN process 2: submits the TAN (carried in the signature) for the order with `reference`. */
86
104
  tanProcess2(version: number, reference: string): SegmentSpec {
87
- return SegmentSpec.numbered('HKTAN', version, { 1: '2', [orderReferenceField]: reference, [moreTansField]: false });
105
+ return SegmentSpec.numbered('HKTAN', version, {
106
+ 1: '2',
107
+ [orderReferenceField]: new Verbatim(reference),
108
+ [moreTansField]: false,
109
+ });
88
110
  },
89
111
 
90
112
  /** HKTAN process S: asks whether the order with `reference` was approved in the banking app. */
91
113
  tanProcessStatus(version: number, reference: string): SegmentSpec {
92
- return SegmentSpec.numbered('HKTAN', version, { 1: 'S', [orderReferenceField]: reference, [moreTansField]: false });
114
+ return SegmentSpec.numbered('HKTAN', version, {
115
+ 1: 'S',
116
+ [orderReferenceField]: new Verbatim(reference),
117
+ [moreTansField]: false,
118
+ });
93
119
  },
94
120
 
95
- /** HKSAL: the balance of one account (not of all accounts). */
96
- balance(version: number, account: Account, continuation?: string): SegmentSpec {
121
+ /**
122
+ * HKSAL: the balance of one account (not of all accounts), named by its national account as well where
123
+ * `nationalAllowed` (HISPAS).
124
+ */
125
+ balance(version: number, account: Account, nationalAllowed: boolean, continuation?: string): SegmentSpec {
97
126
  return new SegmentSpec('HKSAL', version, [
98
- version >= 7 ? internationalAccount(account) : nationalAccount(account),
127
+ version >= 7 ? internationalAccount(account, nationalAllowed) : nationalAccount(account),
99
128
  false,
100
129
  null,
101
- continuation,
130
+ Verbatim.of(continuation),
102
131
  ]);
103
132
  },
104
133
 
105
- /** HKKAZ: MT940 transactions of one account. */
134
+ /** HKKAZ: MT940 transactions of one account, named by its national account as well where `nationalAllowed`. */
106
135
  transactionsMt940(
107
136
  version: number,
108
137
  account: Account,
138
+ nationalAllowed: boolean,
109
139
  period: { from?: Date | undefined; to?: Date | undefined; continuation?: string | undefined },
110
140
  ): SegmentSpec {
111
141
  return new SegmentSpec('HKKAZ', version, [
112
- version >= 7 ? internationalAccount(account) : nationalAccount(account),
142
+ version >= 7 ? internationalAccount(account, nationalAllowed) : nationalAccount(account),
113
143
  false,
114
144
  dateOrEmpty(period.from),
115
145
  dateOrEmpty(period.to),
116
146
  null,
117
- period.continuation,
147
+ Verbatim.of(period.continuation),
118
148
  ]);
119
149
  },
120
150
 
121
- /** HKCAZ: camt transactions of one account in one of `formats`. */
151
+ /**
152
+ * HKCAZ: camt transactions of one account in one of `formats`, named by its national account as well where
153
+ * `nationalAllowed`.
154
+ */
122
155
  transactionsCamt(
123
156
  version: number,
124
157
  account: Account,
158
+ nationalAllowed: boolean,
125
159
  formats: readonly string[],
126
160
  period: { from?: Date | undefined; to?: Date | undefined; continuation?: string | undefined },
127
161
  ): SegmentSpec {
128
162
  return new SegmentSpec('HKCAZ', version, [
129
- internationalAccount(account),
163
+ internationalAccount(account, nationalAllowed),
130
164
  formats,
131
165
  false,
132
166
  dateOrEmpty(period.from),
133
167
  dateOrEmpty(period.to),
134
168
  null,
135
- period.continuation,
169
+ Verbatim.of(period.continuation),
136
170
  ]);
137
171
  },
138
172
 
139
173
  /** HKSPA: IBAN and BIC of all accounts. */
140
174
  sepaAccounts(version: number, continuation?: string): SegmentSpec {
141
- return new SegmentSpec('HKSPA', version, [null, ...(version >= 2 ? [null, continuation] : [])]);
175
+ return new SegmentSpec('HKSPA', version, [null, ...(version >= 2 ? [null, Verbatim.of(continuation)] : [])]);
142
176
  },
143
177
 
144
178
  /** HKTAB: all TAN media of all classes. */
@@ -155,6 +189,7 @@ export class TanResponse {
155
189
  constructor(
156
190
  /** The TAN process the segment answers: `4` asks for an approval, `S` answers a status request. */
157
191
  readonly process: string | undefined,
192
+ /** The order reference, as `Segment.verbatim` reads it, to send back byte for byte. */
158
193
  readonly reference: string | undefined,
159
194
  readonly challenge: string | undefined,
160
195
  readonly hhdUc: Uint8Array | undefined,
@@ -165,7 +200,7 @@ export class TanResponse {
165
200
  static parse(s: Segment): TanResponse {
166
201
  return new TanResponse(
167
202
  s.text(Hitan.process),
168
- s.text(Hitan.reference),
203
+ s.verbatim(Hitan.reference),
169
204
  s.text(Hitan.challenge),
170
205
  s.binary(Hitan.hhdUc),
171
206
  parseFintsDateTime(s.text(Hitan.validUntil, 0), s.text(Hitan.validUntil, 1)),
@@ -242,8 +277,17 @@ export function parseBalance(s: Segment, account: Account): Balance {
242
277
  * The account a balance (HISAL) belongs to: international (IBAN, BIC, then national) from version 7, national before.
243
278
  */
244
279
  export function balanceAccount(s: Segment): Account {
245
- const fields = s.texts(Hisal.account);
246
- const international = s.version >= 7;
280
+ return accountAt(s, Hisal.account, s.version >= 7);
281
+ }
282
+
283
+ /** The account camt transactions (HICAZ) belong to, which the first element names (international). */
284
+ export function camtAccount(s: Segment): Account {
285
+ return accountAt(s, 1, true);
286
+ }
287
+
288
+ /** The account at `index` of `s`: an `international` one (KTI), else a national one (KTV). */
289
+ function accountAt(s: Segment, index: number, international: boolean): Account {
290
+ const fields = s.texts(index);
247
291
  const national = international ? 2 : 0;
248
292
  return new Account({
249
293
  iban: international ? fields[0] : undefined,
@@ -295,8 +339,14 @@ export function parseSepaAccounts(s: Segment, defaultCountry: string, defaultBan
295
339
  */
296
340
  const accountWidth = 4;
297
341
 
298
- /** Parses the TAN media list (HITAB versions 4 and 5, PIN/TAN "TAN-Medium-Liste"). */
299
- export function parseTanMedia(s: Segment): TanMedium[] {
342
+ /**
343
+ * Parses the TAN media list (HITAB versions 4 and 5, PIN/TAN "TAN-Medium-Liste").
344
+ *
345
+ * Each name whose text would not go back as the bank sent it goes into `namesAsSent` as `Segment.verbatim` reads it,
346
+ * keyed by its text: a name in UTF-8, which its text does not tell where it fits ISO 8859-1, and one with blanks at
347
+ * either end, which text goes out without (Formals B.4.1).
348
+ */
349
+ export function parseTanMedia(s: Segment, namesAsSent?: Map<string, string>): TanMedium[] {
300
350
  // Version 5 adds the security function in front of the card number.
301
351
  const version5 = s.version >= 5;
302
352
  const cardNumber = version5 ? 3 : 2;
@@ -310,6 +360,11 @@ export function parseTanMedia(s: Segment): TanMedium[] {
310
360
  const fields = s.texts(i);
311
361
  const mediumClass = fields[0];
312
362
  if (mediumClass === undefined) continue;
363
+ const name = fields[after + 3];
364
+ const nameAsSent = s.verbatim(i, after + 3);
365
+ if (name !== undefined && nameAsSent !== undefined && nameAsSent !== trimBlanks(name)) {
366
+ namesAsSent?.set(name, nameAsSent);
367
+ }
313
368
  media.push(
314
369
  new TanMedium({
315
370
  mediumClass,
@@ -319,7 +374,7 @@ export function parseTanMedia(s: Segment): TanMedium[] {
319
374
  cardSequenceNumber: fields[cardNumber + 1],
320
375
  validFrom: parseFintsDate(fields[after]),
321
376
  validUntil: parseFintsDate(fields[after + 1]),
322
- name: fields[after + 3],
377
+ name,
323
378
  mobileNumberMasked: fields[after + 4],
324
379
  mobileNumber: fields[after + 5],
325
380
  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, trimBlanks, 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,10 +59,19 @@ 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
+ private typeValue: string | undefined;
63
+
64
+ constructor(
65
+ readonly elements: readonly (readonly DataElement[])[],
66
+ /** The bytes the segment was read from: `start` to `end` in `source`, terminator included. */
67
+ private readonly source: Uint8Array,
68
+ private readonly start: number,
69
+ private readonly end: number,
70
+ ) {}
50
71
 
51
72
  get type(): string {
52
- return this.text(0)?.trim() ?? '';
73
+ this.typeValue ??= trimBlanks(this.text(0) ?? '');
74
+ return this.typeValue;
53
75
  }
54
76
 
55
77
  get version(): number {
@@ -74,14 +96,31 @@ export class Segment {
74
96
  return e.text;
75
97
  }
76
98
 
99
+ /** The binary data at `index`; `undefined` for an element that holds text. */
77
100
  binary(index: number, sub = 0): Uint8Array | undefined {
101
+ const e = this.element(index, sub);
102
+ return e?.isBinary === true ? e.bytes : undefined;
103
+ }
104
+
105
+ /** The binary data or the text without escape characters at `index`; `undefined` for an empty element. */
106
+ bytes(index: number, sub = 0): Uint8Array | undefined {
107
+ const e = this.element(index, sub);
108
+ return e === undefined || e.isEmpty ? undefined : e.bytes;
109
+ }
110
+
111
+ /**
112
+ * The element at `index` as the bank sent it, for a value the client sends back, such as the dialog ID: untrimmed,
113
+ * and one character per byte (ISO 8859-1, the FinTS 3.0 character set), so that `Verbatim` gives the bank back the
114
+ * very bytes it sent, in whatever encoding. `undefined` for an empty element.
115
+ */
116
+ verbatim(index: number, sub = 0): string | undefined {
78
117
  const e = this.element(index, sub);
79
118
  if (e === undefined || e.isEmpty) return undefined;
80
- return e.bytes;
119
+ return latin1Decode(e.bytes);
81
120
  }
82
121
 
83
122
  integer(index: number, sub = 0): number | undefined {
84
- return tryParseInt(this.text(index, sub)?.trim());
123
+ return tryParseInt(this.text(index, sub));
85
124
  }
86
125
 
87
126
  /** The texts of the group at `index`; `undefined` for empty elements. */
@@ -89,22 +128,9 @@ export class Segment {
89
128
  return this.group(index).map((e) => (e.isEmpty ? undefined : e.text));
90
129
  }
91
130
 
131
+ /** The bytes the segment was read from, as the bank sent them, terminator included. */
92
132
  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());
133
+ return this.source.subarray(this.start, this.end);
108
134
  }
109
135
  }
110
136
 
@@ -124,111 +150,162 @@ function writeBinary(out: BytesBuilder, bytes: Uint8Array): void {
124
150
 
125
151
  /**
126
152
  * 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.
153
+ * come from a bank that sends UTF-8, such as a TAN medium named `Max’s iPhone`. `FinTsConfig` checks the login data for
154
+ * ISO 8859-1 up front, and the client the PIN and the TAN.
155
+ *
156
+ * @internal Exported for the tests.
129
157
  */
130
158
  export function encodeText(value: string): Uint8Array {
131
159
  return isLatin1(value) ? latin1Encode(value) : utf8Encode(value);
132
160
  }
133
161
 
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;
162
+ /**
163
+ * The data elements and segments one parse may produce together, segment headers and group elements included: what
164
+ * the user parameter data of 6,000 accounts take, at about 155 data elements each, which is about what 9,999 KB, the
165
+ * largest message size HIBPA can state, hold of real data. Without a limit, a message of nothing but separators would
166
+ * take a hundred times its size in memory.
167
+ *
168
+ * @internal Exported for the tests.
169
+ */
170
+ export const maxParts = 1_000_000;
171
+
172
+ /**
173
+ * What `IncomingMessage3.size` counts for each data element it keeps: more than any of them takes once parsed, so that
174
+ * a message of many short elements counts as the memory it holds rather than its bytes.
175
+ */
176
+ export const parsedElementBytes = 512;
177
+
178
+ /** Every empty data element of a received segment; it has no bytes to change. */
179
+ const emptyElement = new DataElement(new Uint8Array(0), 0, 0, false);
180
+
181
+ function isSeparator(byte: number | undefined): boolean {
182
+ return byte === dataElementSeparator || byte === groupElementSeparator || byte === segmentTerminator;
183
+ }
184
+
185
+ /** Line breaks and spaces between segments are padding, not content. */
186
+ function isPadding(byte: number): boolean {
187
+ return byte === 0x0a || byte === 0x0d || byte === 0x20;
188
+ }
189
+
190
+ function isDigit(byte: number | undefined): boolean {
191
+ return byte !== undefined && byte >= 0x30 && byte <= 0x39;
138
192
  }
139
193
 
140
194
  /**
141
195
  * Splits a FinTS 3.0 message into segments, resolving escapes and binary data (`@length@bytes`).
142
196
  *
143
- * Throws `FormatError` for a negative binary length.
197
+ * The elements share the memory of `data`. Throws `FormatError` for anything a complete message cannot hold: a
198
+ * malformed binary length, binary data beyond the end of `data` or not followed by a separator, a last segment without
199
+ * terminator, and more than `maxParts` data elements and segments, an empty one included.
144
200
  */
145
201
  export function parseSegments(data: Uint8Array): Segment[] {
146
202
  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;
203
+ let parts = 0;
204
+ const count = (): void => {
205
+ if (++parts > maxParts) throw new FormatError(`More than ${maxParts} data elements and segments`);
157
206
  };
207
+ // Created with their first element: an array that starts empty reserves room for more than most of them hold.
208
+ let elements: DataElement[][] | undefined;
209
+ let group: DataElement[] | undefined;
210
+ let segmentStart = 0;
211
+ let i = 0;
158
212
 
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;
213
+ const readBinary = (): DataElement => {
214
+ let end = i + 1;
215
+ let length = 0;
216
+ while (isDigit(data[end])) {
217
+ length = length * 10 + data[end++]! - 0x30;
218
+ if (length > data.length) throw new FormatError('Binary data beyond the end of the message');
219
+ }
220
+ if (end === i + 1 || data[end] !== binaryMarker) throw new FormatError('Malformed binary length');
221
+ const start = end + 1;
222
+ if (length > data.length - start) throw new FormatError('Binary data beyond the end of the message');
223
+ i = start + length;
224
+ if (i < data.length && !isSeparator(data[i])) throw new FormatError('Binary data not followed by a separator');
225
+ return new DataElement(data, start, i, true);
226
+ };
227
+
228
+ const readText = (): DataElement => {
229
+ const start = i;
230
+ let escaped = false;
231
+ while (i < data.length && !isSeparator(data[i])) {
232
+ if (data[i] === escapeCharacter) {
233
+ escaped = true;
234
+ i++;
235
+ }
236
+ i++;
237
+ }
238
+ if (i > data.length) throw new FormatError('Escape character at the end of the message');
239
+ return i === start ? emptyElement : new DataElement(data, start, i, false, escaped);
168
240
  };
169
241
 
170
- let i = 0;
171
242
  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;
243
+ if (elements === undefined && group === undefined && isPadding(data[i]!)) {
244
+ i++;
177
245
  continue;
178
246
  }
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
- }
190
- }
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
- }
247
+ count();
248
+ if (elements === undefined && group === undefined) segmentStart = i;
249
+ const element = data[i] === binaryMarker ? readBinary() : readText();
250
+ if (group === undefined) group = [element];
251
+ else group.push(element);
252
+ const separator = data[i++];
253
+ if (separator === groupElementSeparator) continue;
254
+ if (elements === undefined) elements = [group];
255
+ else elements.push(group);
256
+ group = undefined;
257
+ if (separator === segmentTerminator) {
258
+ count();
259
+ const segment = new Segment(elements, data, segmentStart, i);
260
+ // Skip empty segments, such as a stray double terminator.
261
+ if (segment.type !== '') segments.push(segment);
262
+ elements = undefined;
211
263
  }
212
- i++;
213
264
  }
214
- if (segmentStarted && current.length > 0) endSegment();
265
+ if (elements !== undefined || group !== undefined) throw new FormatError('Segment without terminator');
215
266
  return segments;
216
267
  }
217
268
 
269
+ /** `text` without the escape characters in front of syntax characters. */
270
+ function unescape(text: Uint8Array): Uint8Array {
271
+ const out = new Uint8Array(text.length);
272
+ let o = 0;
273
+ for (let j = 0; j < text.length; j++) {
274
+ if (text[j] === escapeCharacter) j++;
275
+ out[o++] = text[j]!;
276
+ }
277
+ return out.subarray(0, o);
278
+ }
279
+
218
280
  /** Binary data in a segment to send, written as `@length@bytes`. */
219
281
  export class Binary {
220
282
  constructor(readonly bytes: Uint8Array) {}
221
283
  }
222
284
 
285
+ /**
286
+ * A value the bank issued, such as the dialog ID, to send back as `Segment.verbatim` reads it: escaped, but not
287
+ * trimmed, so that the bank gets back the very bytes it sent. Text from elsewhere, such as a TAN medium name the app
288
+ * passes, goes out as `encodeText` encodes it.
289
+ */
290
+ export class Verbatim {
291
+ constructor(readonly text: string) {}
292
+
293
+ /** `text` as a `Verbatim`; `undefined` stays empty. */
294
+ static of(text: string | undefined): Verbatim | undefined {
295
+ return text === undefined ? undefined : new Verbatim(text);
296
+ }
297
+ }
298
+
223
299
  /** A value of a segment to send; see `SegmentSpec`. */
224
- export type SegmentValue = string | number | boolean | Binary | null | undefined | readonly SegmentValue[];
300
+ export type SegmentValue = string | number | boolean | Binary | Verbatim | null | undefined | readonly SegmentValue[];
225
301
 
226
302
  /**
227
303
  * A segment to send.
228
304
  *
229
305
  * `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.
306
+ * element group, a boolean for `J`/`N`, `Binary` for binary data, `Verbatim` for a value the bank issued, and anything
307
+ * else as its text, without leading and trailing blanks. Trailing empty elements are left out, as the specification
308
+ * asks.
232
309
  */
233
310
  export class SegmentSpec {
234
311
  constructor(
@@ -296,12 +373,14 @@ function encodeValue(value: SingleValue): Uint8Array {
296
373
  // Empty.
297
374
  } else if (value instanceof Binary) {
298
375
  writeBinary(out, value.bytes);
376
+ } else if (value instanceof Verbatim) {
377
+ writeEscaped(out, encodeText(value.text));
299
378
  } else if (typeof value === 'boolean') {
300
379
  out.addByte(value ? 0x4a : 0x4e); // 'J' or 'N'
301
380
  } else {
302
381
  // All but binary data go without leading and trailing blanks (Formals B.4.1). Encode first, then escape the bytes,
303
382
  // so that nothing unescaped slips into the syntax.
304
- writeEscaped(out, encodeText(String(value).trim()));
383
+ writeEscaped(out, encodeText(trimBlanks(String(value))));
305
384
  }
306
385
  return out.takeBytes();
307
386
  }
@@ -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"}