@coderoftime/fints 0.0.0-stage → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (254) hide show
  1. package/CHANGELOG.md +11 -0
  2. package/LICENSE +28 -0
  3. package/README.md +156 -2
  4. package/dist/core/account.d.ts +140 -0
  5. package/dist/core/account.d.ts.map +1 -0
  6. package/dist/core/account.js +174 -0
  7. package/dist/core/account.js.map +1 -0
  8. package/dist/core/amount.d.ts +62 -0
  9. package/dist/core/amount.d.ts.map +1 -0
  10. package/dist/core/amount.js +189 -0
  11. package/dist/core/amount.js.map +1 -0
  12. package/dist/core/balance.d.ts +54 -0
  13. package/dist/core/balance.d.ts.map +1 -0
  14. package/dist/core/balance.js +53 -0
  15. package/dist/core/balance.js.map +1 -0
  16. package/dist/core/bank-info.d.ts +49 -0
  17. package/dist/core/bank-info.d.ts.map +1 -0
  18. package/dist/core/bank-info.js +52 -0
  19. package/dist/core/bank-info.js.map +1 -0
  20. package/dist/core/client.d.ts +126 -0
  21. package/dist/core/client.d.ts.map +1 -0
  22. package/dist/core/client.js +2 -0
  23. package/dist/core/client.js.map +1 -0
  24. package/dist/core/config.d.ts +68 -0
  25. package/dist/core/config.d.ts.map +1 -0
  26. package/dist/core/config.js +113 -0
  27. package/dist/core/config.js.map +1 -0
  28. package/dist/core/errors.d.ts +91 -0
  29. package/dist/core/errors.d.ts.map +1 -0
  30. package/dist/core/errors.js +182 -0
  31. package/dist/core/errors.js.map +1 -0
  32. package/dist/core/feedback.d.ts +69 -0
  33. package/dist/core/feedback.d.ts.map +1 -0
  34. package/dist/core/feedback.js +124 -0
  35. package/dist/core/feedback.js.map +1 -0
  36. package/dist/core/transaction.d.ts +98 -0
  37. package/dist/core/transaction.d.ts.map +1 -0
  38. package/dist/core/transaction.js +91 -0
  39. package/dist/core/transaction.js.map +1 -0
  40. package/dist/core/transport.d.ts +37 -0
  41. package/dist/core/transport.d.ts.map +1 -0
  42. package/dist/core/transport.js +92 -0
  43. package/dist/core/transport.js.map +1 -0
  44. package/dist/formats/camt.d.ts +12 -0
  45. package/dist/formats/camt.d.ts.map +1 -0
  46. package/dist/formats/camt.js +159 -0
  47. package/dist/formats/camt.js.map +1 -0
  48. package/dist/formats/mt940.d.ts +42 -0
  49. package/dist/formats/mt940.d.ts.map +1 -0
  50. package/dist/formats/mt940.js +297 -0
  51. package/dist/formats/mt940.js.map +1 -0
  52. package/dist/formats/readers.d.ts +11 -0
  53. package/dist/formats/readers.d.ts.map +1 -0
  54. package/dist/formats/readers.js +47 -0
  55. package/dist/formats/readers.js.map +1 -0
  56. package/dist/formats/sepa-purpose.d.ts +22 -0
  57. package/dist/formats/sepa-purpose.d.ts.map +1 -0
  58. package/dist/formats/sepa-purpose.js +68 -0
  59. package/dist/formats/sepa-purpose.js.map +1 -0
  60. package/dist/index.d.ts +28 -0
  61. package/dist/index.d.ts.map +1 -0
  62. package/dist/index.js +27 -0
  63. package/dist/index.js.map +1 -0
  64. package/dist/session/app-approval.d.ts +60 -0
  65. package/dist/session/app-approval.d.ts.map +1 -0
  66. package/dist/session/app-approval.js +215 -0
  67. package/dist/session/app-approval.js.map +1 -0
  68. package/dist/session/bank-connection.d.ts +28 -0
  69. package/dist/session/bank-connection.d.ts.map +1 -0
  70. package/dist/session/bank-connection.js +48 -0
  71. package/dist/session/bank-connection.js.map +1 -0
  72. package/dist/session/dialog-runner.d.ts +34 -0
  73. package/dist/session/dialog-runner.d.ts.map +1 -0
  74. package/dist/session/dialog-runner.js +112 -0
  75. package/dist/session/dialog-runner.js.map +1 -0
  76. package/dist/session/login-guard.d.ts +73 -0
  77. package/dist/session/login-guard.d.ts.map +1 -0
  78. package/dist/session/login-guard.js +109 -0
  79. package/dist/session/login-guard.js.map +1 -0
  80. package/dist/session/parameter-data.d.ts +83 -0
  81. package/dist/session/parameter-data.d.ts.map +1 -0
  82. package/dist/session/parameter-data.js +68 -0
  83. package/dist/session/parameter-data.js.map +1 -0
  84. package/dist/session/pin-tan-client.d.ts +54 -0
  85. package/dist/session/pin-tan-client.d.ts.map +1 -0
  86. package/dist/session/pin-tan-client.js +153 -0
  87. package/dist/session/pin-tan-client.js.map +1 -0
  88. package/dist/session/pin-tan-dialog.d.ts +71 -0
  89. package/dist/session/pin-tan-dialog.d.ts.map +1 -0
  90. package/dist/session/pin-tan-dialog.js +124 -0
  91. package/dist/session/pin-tan-dialog.js.map +1 -0
  92. package/dist/session/protocol.d.ts +209 -0
  93. package/dist/session/protocol.d.ts.map +1 -0
  94. package/dist/session/protocol.js +75 -0
  95. package/dist/session/protocol.js.map +1 -0
  96. package/dist/session/response-handler.d.ts +48 -0
  97. package/dist/session/response-handler.d.ts.map +1 -0
  98. package/dist/session/response-handler.js +121 -0
  99. package/dist/session/response-handler.js.map +1 -0
  100. package/dist/session/sepa-accounts.d.ts +34 -0
  101. package/dist/session/sepa-accounts.d.ts.map +1 -0
  102. package/dist/session/sepa-accounts.js +96 -0
  103. package/dist/session/sepa-accounts.js.map +1 -0
  104. package/dist/session/session-setup.d.ts +72 -0
  105. package/dist/session/session-setup.d.ts.map +1 -0
  106. package/dist/session/session-setup.js +160 -0
  107. package/dist/session/session-setup.js.map +1 -0
  108. package/dist/session/session-state.d.ts +47 -0
  109. package/dist/session/session-state.d.ts.map +1 -0
  110. package/dist/session/session-state.js +147 -0
  111. package/dist/session/session-state.js.map +1 -0
  112. package/dist/session/tan-selection.d.ts +88 -0
  113. package/dist/session/tan-selection.d.ts.map +1 -0
  114. package/dist/session/tan-selection.js +178 -0
  115. package/dist/session/tan-selection.js.map +1 -0
  116. package/dist/session/user-prompts.d.ts +61 -0
  117. package/dist/session/user-prompts.d.ts.map +1 -0
  118. package/dist/session/user-prompts.js +164 -0
  119. package/dist/session/user-prompts.js.map +1 -0
  120. package/dist/tan/challenge.d.ts +114 -0
  121. package/dist/tan/challenge.d.ts.map +1 -0
  122. package/dist/tan/challenge.js +185 -0
  123. package/dist/tan/challenge.js.map +1 -0
  124. package/dist/tan/flicker.d.ts +36 -0
  125. package/dist/tan/flicker.d.ts.map +1 -0
  126. package/dist/tan/flicker.js +262 -0
  127. package/dist/tan/flicker.js.map +1 -0
  128. package/dist/tan/tan-image.d.ts +21 -0
  129. package/dist/tan/tan-image.d.ts.map +1 -0
  130. package/dist/tan/tan-image.js +41 -0
  131. package/dist/tan/tan-image.js.map +1 -0
  132. package/dist/tan/tan-models.d.ts +145 -0
  133. package/dist/tan/tan-models.d.ts.map +1 -0
  134. package/dist/tan/tan-models.js +148 -0
  135. package/dist/tan/tan-models.js.map +1 -0
  136. package/dist/util/async.d.ts +24 -0
  137. package/dist/util/async.d.ts.map +1 -0
  138. package/dist/util/async.js +49 -0
  139. package/dist/util/async.js.map +1 -0
  140. package/dist/util/bytes.d.ts +23 -0
  141. package/dist/util/bytes.d.ts.map +1 -0
  142. package/dist/util/bytes.js +136 -0
  143. package/dist/util/bytes.js.map +1 -0
  144. package/dist/util/call-context.d.ts +12 -0
  145. package/dist/util/call-context.d.ts.map +1 -0
  146. package/dist/util/call-context.js +29 -0
  147. package/dist/util/call-context.js.map +1 -0
  148. package/dist/util/dates.d.ts +62 -0
  149. package/dist/util/dates.d.ts.map +1 -0
  150. package/dist/util/dates.js +169 -0
  151. package/dist/util/dates.js.map +1 -0
  152. package/dist/util/errors.d.ts +9 -0
  153. package/dist/util/errors.d.ts.map +1 -0
  154. package/dist/util/errors.js +15 -0
  155. package/dist/util/errors.js.map +1 -0
  156. package/dist/util/immutable-map.d.ts +16 -0
  157. package/dist/util/immutable-map.d.ts.map +1 -0
  158. package/dist/util/immutable-map.js +38 -0
  159. package/dist/util/immutable-map.js.map +1 -0
  160. package/dist/util/json.d.ts +13 -0
  161. package/dist/util/json.d.ts.map +1 -0
  162. package/dist/util/json.js +44 -0
  163. package/dist/util/json.js.map +1 -0
  164. package/dist/util/text.d.ts +20 -0
  165. package/dist/util/text.d.ts.map +1 -0
  166. package/dist/util/text.js +78 -0
  167. package/dist/util/text.js.map +1 -0
  168. package/dist/util/xml.d.ts +35 -0
  169. package/dist/util/xml.d.ts.map +1 -0
  170. package/dist/util/xml.js +183 -0
  171. package/dist/util/xml.js.map +1 -0
  172. package/dist/v3/account-extension.d.ts +25 -0
  173. package/dist/v3/account-extension.d.ts.map +1 -0
  174. package/dist/v3/account-extension.js +176 -0
  175. package/dist/v3/account-extension.js.map +1 -0
  176. package/dist/v3/client.d.ts +24 -0
  177. package/dist/v3/client.d.ts.map +1 -0
  178. package/dist/v3/client.js +16 -0
  179. package/dist/v3/client.js.map +1 -0
  180. package/dist/v3/dialog.d.ts +78 -0
  181. package/dist/v3/dialog.d.ts.map +1 -0
  182. package/dist/v3/dialog.js +138 -0
  183. package/dist/v3/dialog.js.map +1 -0
  184. package/dist/v3/message.d.ts +72 -0
  185. package/dist/v3/message.d.ts.map +1 -0
  186. package/dist/v3/message.js +259 -0
  187. package/dist/v3/message.js.map +1 -0
  188. package/dist/v3/parameters.d.ts +79 -0
  189. package/dist/v3/parameters.d.ts.map +1 -0
  190. package/dist/v3/parameters.js +396 -0
  191. package/dist/v3/parameters.js.map +1 -0
  192. package/dist/v3/protocol.d.ts +68 -0
  193. package/dist/v3/protocol.d.ts.map +1 -0
  194. package/dist/v3/protocol.js +263 -0
  195. package/dist/v3/protocol.js.map +1 -0
  196. package/dist/v3/segments.d.ts +89 -0
  197. package/dist/v3/segments.d.ts.map +1 -0
  198. package/dist/v3/segments.js +283 -0
  199. package/dist/v3/segments.js.map +1 -0
  200. package/dist/v3/syntax.d.ts +76 -0
  201. package/dist/v3/syntax.d.ts.map +1 -0
  202. package/dist/v3/syntax.js +301 -0
  203. package/dist/v3/syntax.js.map +1 -0
  204. package/package.json +63 -4
  205. package/src/core/account.ts +250 -0
  206. package/src/core/amount.ts +209 -0
  207. package/src/core/balance.ts +84 -0
  208. package/src/core/bank-info.ts +66 -0
  209. package/src/core/client.ts +149 -0
  210. package/src/core/config.ts +141 -0
  211. package/src/core/errors.ts +203 -0
  212. package/src/core/feedback.ts +137 -0
  213. package/src/core/transaction.ts +156 -0
  214. package/src/core/transport.ts +114 -0
  215. package/src/formats/camt.ts +169 -0
  216. package/src/formats/mt940.ts +333 -0
  217. package/src/formats/readers.ts +56 -0
  218. package/src/formats/sepa-purpose.ts +75 -0
  219. package/src/index.ts +68 -0
  220. package/src/session/app-approval.ts +265 -0
  221. package/src/session/bank-connection.ts +51 -0
  222. package/src/session/dialog-runner.ts +119 -0
  223. package/src/session/login-guard.ts +135 -0
  224. package/src/session/parameter-data.ts +137 -0
  225. package/src/session/pin-tan-client.ts +186 -0
  226. package/src/session/pin-tan-dialog.ts +160 -0
  227. package/src/session/protocol.ts +311 -0
  228. package/src/session/response-handler.ts +130 -0
  229. package/src/session/sepa-accounts.ts +97 -0
  230. package/src/session/session-setup.ts +165 -0
  231. package/src/session/session-state.ts +170 -0
  232. package/src/session/tan-selection.ts +192 -0
  233. package/src/session/user-prompts.ts +174 -0
  234. package/src/tan/challenge.ts +252 -0
  235. package/src/tan/flicker.ts +270 -0
  236. package/src/tan/tan-image.ts +36 -0
  237. package/src/tan/tan-models.ts +247 -0
  238. package/src/util/async.ts +53 -0
  239. package/src/util/bytes.ts +135 -0
  240. package/src/util/call-context.ts +40 -0
  241. package/src/util/dates.ts +193 -0
  242. package/src/util/errors.ts +16 -0
  243. package/src/util/immutable-map.ts +47 -0
  244. package/src/util/json.ts +51 -0
  245. package/src/util/text.ts +82 -0
  246. package/src/util/xml.ts +172 -0
  247. package/src/v3/account-extension.ts +190 -0
  248. package/src/v3/client.ts +29 -0
  249. package/src/v3/dialog.ts +199 -0
  250. package/src/v3/message.ts +321 -0
  251. package/src/v3/parameters.ts +451 -0
  252. package/src/v3/protocol.ts +331 -0
  253. package/src/v3/segments.ts +331 -0
  254. package/src/v3/syntax.ts +307 -0
@@ -0,0 +1,307 @@
1
+ // Internal to the package, and named after the FinTS specification. Documentation is where the meaning is not evident.
2
+
3
+ import { BytesBuilder, latin1Decode, latin1Encode, utf8Encode } from '../util/bytes.js';
4
+ import { FormatError } from '../util/errors.js';
5
+ import { decodeBankText, tryParseInt } from '../util/text.js';
6
+
7
+ // FinTS 3.0 syntax characters.
8
+ const dataElementSeparator = 0x2b; // '+'
9
+ const groupElementSeparator = 0x3a; // ':'
10
+ const segmentTerminator = 0x27; // "'"
11
+ const escapeCharacter = 0x3f; // '?'
12
+ const binaryMarker = 0x40; // '@'
13
+
14
+ function isSyntaxCharacter(byte: number): boolean {
15
+ return (
16
+ byte === dataElementSeparator ||
17
+ byte === groupElementSeparator ||
18
+ byte === segmentTerminator ||
19
+ byte === escapeCharacter ||
20
+ byte === binaryMarker
21
+ );
22
+ }
23
+
24
+ /** One (group) data element of a received segment: unescaped text bytes or binary data. */
25
+ export class DataElement {
26
+ private decoded: string | undefined;
27
+
28
+ constructor(
29
+ readonly bytes: Uint8Array,
30
+ readonly isBinary: boolean,
31
+ ) {}
32
+
33
+ get isEmpty(): boolean {
34
+ return !this.isBinary && this.bytes.length === 0;
35
+ }
36
+
37
+ get text(): string {
38
+ this.decoded ??= decodeBankText(this.bytes);
39
+ return this.decoded;
40
+ }
41
+ }
42
+
43
+ /**
44
+ * A received segment: data elements separated by `+`, each a group of elements separated by `:`.
45
+ *
46
+ * Index 0 is the segment header (type, number, version, reference); the data elements follow from index 1.
47
+ */
48
+ export class Segment {
49
+ constructor(readonly elements: readonly (readonly DataElement[])[]) {}
50
+
51
+ get type(): string {
52
+ return this.text(0)?.trim() ?? '';
53
+ }
54
+
55
+ get version(): number {
56
+ return this.integer(0, 2) ?? 0;
57
+ }
58
+
59
+ get length(): number {
60
+ return this.elements.length;
61
+ }
62
+
63
+ group(index: number): readonly DataElement[] {
64
+ return this.elements[index] ?? [];
65
+ }
66
+
67
+ element(index: number, sub = 0): DataElement | undefined {
68
+ return this.group(index)[sub];
69
+ }
70
+
71
+ text(index: number, sub = 0): string | undefined {
72
+ const e = this.element(index, sub);
73
+ if (e === undefined || e.isEmpty) return undefined;
74
+ return e.text;
75
+ }
76
+
77
+ binary(index: number, sub = 0): Uint8Array | undefined {
78
+ const e = this.element(index, sub);
79
+ if (e === undefined || e.isEmpty) return undefined;
80
+ return e.bytes;
81
+ }
82
+
83
+ integer(index: number, sub = 0): number | undefined {
84
+ return tryParseInt(this.text(index, sub)?.trim());
85
+ }
86
+
87
+ /** The texts of the group at `index`; `undefined` for empty elements. */
88
+ texts(index: number): (string | undefined)[] {
89
+ return this.group(index).map((e) => (e.isEmpty ? undefined : e.text));
90
+ }
91
+
92
+ 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());
108
+ }
109
+ }
110
+
111
+ function writeEscaped(out: BytesBuilder, bytes: Uint8Array): void {
112
+ const escaped: number[] = [];
113
+ for (const b of bytes) {
114
+ if (isSyntaxCharacter(b)) escaped.push(escapeCharacter);
115
+ escaped.push(b);
116
+ }
117
+ out.add(escaped);
118
+ }
119
+
120
+ function writeBinary(out: BytesBuilder, bytes: Uint8Array): void {
121
+ out.add(latin1Encode(`@${bytes.length}@`));
122
+ out.add(bytes);
123
+ }
124
+
125
+ /**
126
+ * 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.
129
+ */
130
+ export function encodeText(value: string): Uint8Array {
131
+ return isLatin1(value) ? latin1Encode(value) : utf8Encode(value);
132
+ }
133
+
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;
138
+ }
139
+
140
+ /**
141
+ * Splits a FinTS 3.0 message into segments, resolving escapes and binary data (`@length@bytes`).
142
+ *
143
+ * Throws `FormatError` for a negative binary length.
144
+ */
145
+ export function parseSegments(data: Uint8Array): Segment[] {
146
+ 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;
157
+ };
158
+
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;
168
+ };
169
+
170
+ let i = 0;
171
+ 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;
177
+ continue;
178
+ }
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
+ }
211
+ }
212
+ i++;
213
+ }
214
+ if (segmentStarted && current.length > 0) endSegment();
215
+ return segments;
216
+ }
217
+
218
+ /** Binary data in a segment to send, written as `@length@bytes`. */
219
+ export class Binary {
220
+ constructor(readonly bytes: Uint8Array) {}
221
+ }
222
+
223
+ /** A value of a segment to send; see `SegmentSpec`. */
224
+ export type SegmentValue = string | number | boolean | Binary | null | undefined | readonly SegmentValue[];
225
+
226
+ /**
227
+ * A segment to send.
228
+ *
229
+ * `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.
232
+ */
233
+ export class SegmentSpec {
234
+ constructor(
235
+ readonly type: string,
236
+ readonly version: number,
237
+ readonly elements: readonly SegmentValue[],
238
+ /** The number of the segment an answer refers to (Bezugssegment); requests have none. */
239
+ readonly reference?: number,
240
+ ) {}
241
+
242
+ /**
243
+ * A segment given by the field numbers of its data elements, as in the specification (1 is the first data element
244
+ * after the header); missing numbers stay empty.
245
+ */
246
+ static numbered(type: string, version: number, fields: Readonly<Record<number, SegmentValue>>): SegmentSpec {
247
+ const count = Math.max(0, ...Object.keys(fields).map(Number));
248
+ const elements: SegmentValue[] = [];
249
+ for (let number = 1; number <= count; number++) elements.push(fields[number]);
250
+ return new SegmentSpec(type, version, elements);
251
+ }
252
+
253
+ /** Encodes the segment with `number`. */
254
+ build(number: number): Uint8Array {
255
+ const out = new BytesBuilder();
256
+ out.add(
257
+ encodeText(`${this.type}:${number}:${this.version}${this.reference === undefined ? '' : `:${this.reference}`}`),
258
+ );
259
+ const encoded = this.elements.map(encodeGroup);
260
+ while (encoded.length > 0 && encoded.at(-1)!.length === 0) encoded.pop();
261
+ for (const e of encoded) {
262
+ out.addByte(dataElementSeparator);
263
+ out.add(e);
264
+ }
265
+ out.addByte(segmentTerminator);
266
+ return out.takeBytes();
267
+ }
268
+ }
269
+
270
+ /** A single value of a segment to send, not a group. */
271
+ type SingleValue = Exclude<SegmentValue, readonly SegmentValue[]>;
272
+
273
+ function isGroup(value: SegmentValue): value is readonly SegmentValue[] {
274
+ return Array.isArray(value);
275
+ }
276
+
277
+ function encodeGroup(value: SegmentValue): Uint8Array {
278
+ const parts: Uint8Array[] = [];
279
+ const flatten = (v: SegmentValue): void => {
280
+ if (isGroup(v)) v.forEach(flatten);
281
+ else parts.push(encodeValue(v));
282
+ };
283
+ flatten(value);
284
+ while (parts.length > 0 && parts.at(-1)!.length === 0) parts.pop();
285
+ const out = new BytesBuilder();
286
+ parts.forEach((part, i) => {
287
+ if (i > 0) out.addByte(groupElementSeparator);
288
+ out.add(part);
289
+ });
290
+ return out.takeBytes();
291
+ }
292
+
293
+ function encodeValue(value: SingleValue): Uint8Array {
294
+ const out = new BytesBuilder();
295
+ if (value === null || value === undefined) {
296
+ // Empty.
297
+ } else if (value instanceof Binary) {
298
+ writeBinary(out, value.bytes);
299
+ } else if (typeof value === 'boolean') {
300
+ out.addByte(value ? 0x4a : 0x4e); // 'J' or 'N'
301
+ } else {
302
+ // All but binary data go without leading and trailing blanks (Formals B.4.1). Encode first, then escape the bytes,
303
+ // so that nothing unescaped slips into the syntax.
304
+ writeEscaped(out, encodeText(String(value).trim()));
305
+ }
306
+ return out.takeBytes();
307
+ }