wao 0.27.2 → 0.27.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/esm/encode.js CHANGED
@@ -1,729 +1,39 @@
1
1
  import base64url from "base64url"
2
- import { hash } from "fast-sha256"
3
-
4
- function isBytes(value) {
5
- return (
6
- value instanceof ArrayBuffer ||
7
- ArrayBuffer.isView(value) ||
8
- Buffer.isBuffer(value) ||
9
- (value &&
10
- typeof value === "object" &&
11
- value.type === "Buffer" &&
12
- Array.isArray(value.data))
13
- )
14
- }
15
-
16
- function isPojo(value) {
17
- return (
18
- !isBytes(value) &&
19
- !Array.isArray(value) &&
20
- !(value instanceof Blob) &&
21
- typeof value === "object" &&
22
- value !== null
23
- )
24
- }
25
-
2
+ import {
3
+ getValueByPath,
4
+ getAoType,
5
+ isEmpty,
6
+ encodePrimitiveContent,
7
+ sortTypeAnnotations,
8
+ analyzeArray,
9
+ toBuffer,
10
+ formatFloat,
11
+ hasNonAscii,
12
+ sha256,
13
+ hasNewline,
14
+ isBytes,
15
+ isPojo,
16
+ } from "./encode-utils.js"
17
+ import encodeArrayItem from "./encode-array-item.js"
18
+ import collectBodyKeys from "./collect-body-keys.js"
26
19
  const MAX_HEADER_LENGTH = 4096
27
20
 
28
- async function hasNewline(value) {
29
- if (typeof value === "string") return value.includes("\n")
30
- if (value instanceof Blob) {
31
- value = await value.text()
32
- return value.includes("\n")
33
- }
34
- if (isBytes(value)) return Buffer.from(value).includes("\n")
35
- return false
36
- }
37
-
38
- async function sha256(data) {
39
- let uint8Array
40
- if (data instanceof ArrayBuffer) {
41
- uint8Array = new Uint8Array(data)
42
- } else if (data instanceof Uint8Array) {
43
- uint8Array = data
44
- } else if (ArrayBuffer.isView(data)) {
45
- uint8Array = new Uint8Array(data.buffer, data.byteOffset, data.byteLength)
46
- } else {
47
- throw new Error("sha256 expects ArrayBuffer or ArrayBufferView")
48
- }
49
-
50
- const hashResult = hash(uint8Array)
51
- return hashResult.buffer.slice(
52
- hashResult.byteOffset,
53
- hashResult.byteOffset + hashResult.byteLength
54
- )
55
- }
56
-
57
- function formatFloat(num) {
58
- let exp = num.toExponential(20)
59
- exp = exp.replace(/e\+(\d)$/, "e+0$1")
60
- exp = exp.replace(/e-(\d)$/, "e-0$1")
61
- return exp
62
- }
63
-
64
- function hasNonAscii(str) {
65
- return /[^\x00-\x7F]/.test(str)
66
- }
67
-
68
- function encodeArrayItem(item) {
69
- if (typeof item === "number") {
70
- if (Number.isInteger(item)) {
71
- return `"(ao-type-integer) ${item}"`
72
- } else {
73
- return `"(ao-type-float) ${formatFloat(item)}"`
74
- }
75
- } else if (typeof item === "string") {
76
- return `"${item}"`
77
- } else if (item === null) {
78
- return `"(ao-type-atom) \\"null\\""`
79
- } else if (item === undefined) {
80
- return `"(ao-type-atom) \\"undefined\\""`
81
- } else if (typeof item === "symbol") {
82
- const desc = item.description || "Symbol.for()"
83
- return `"(ao-type-atom) \\"${desc}\\""`
84
- } else if (typeof item === "boolean") {
85
- return `"(ao-type-atom) \\"${item}\\""`
86
- } else if (Array.isArray(item)) {
87
- const nestedItems = item
88
- .map(nestedItem => {
89
- if (typeof nestedItem === "number") {
90
- if (Number.isInteger(nestedItem)) {
91
- return `\\"(ao-type-integer) ${nestedItem}\\"`
92
- } else {
93
- return `\\"(ao-type-float) ${formatFloat(nestedItem)}\\"`
94
- }
95
- } else if (typeof nestedItem === "string") {
96
- return `\\"${nestedItem}\\"`
97
- } else if (nestedItem === null) {
98
- return `\\"(ao-type-atom) \\\\\\"null\\\\\\"\\"`
99
- } else if (nestedItem === undefined) {
100
- return `\\"(ao-type-atom) \\\\\\"undefined\\\\\\"\\"`
101
- } else if (typeof nestedItem === "symbol") {
102
- const desc = nestedItem.description || "Symbol.for()"
103
- return `\\"(ao-type-atom) \\\\\\"${desc}\\\\\\"\\"`
104
- } else if (typeof nestedItem === "boolean") {
105
- return `\\"(ao-type-atom) \\\\\\"${nestedItem}\\\\\\"\\"`
106
- } else if (Array.isArray(nestedItem)) {
107
- // Handle nested arrays recursively
108
- const deeperItems = nestedItem
109
- .map(deepItem => {
110
- if (typeof deepItem === "number") {
111
- if (Number.isInteger(deepItem)) {
112
- return `\\\\\\"(ao-type-integer) ${deepItem}\\\\\\"`
113
- } else {
114
- return `\\\\\\"(ao-type-float) ${formatFloat(deepItem)}\\\\\\"`
115
- }
116
- } else if (typeof deepItem === "string") {
117
- return `\\\\\\"${deepItem}\\\\\\"`
118
- } else if (Array.isArray(deepItem)) {
119
- // Even deeper nesting - need to escape more
120
- const deepestItems = deepItem
121
- .map(deepestItem => {
122
- if (typeof deepestItem === "number") {
123
- if (Number.isInteger(deepestItem)) {
124
- return `\\\\\\\\\\\\\\"(ao-type-integer) ${deepestItem}\\\\\\\\\\\\\\"`
125
- } else {
126
- return `\\\\\\\\\\\\\\"(ao-type-float) ${formatFloat(deepestItem)}\\\\\\\\\\\\\\"`
127
- }
128
- } else if (typeof deepestItem === "string") {
129
- return `\\\\\\\\\\\\\\"${deepestItem}\\\\\\\\\\\\\\"`
130
- } else {
131
- return `\\\\\\\\\\\\\\"${String(deepestItem)}\\\\\\\\\\\\\\"`
132
- }
133
- })
134
- .join(", ")
135
- return `\\\\\\"(ao-type-list) ${deepestItems}\\\\\\"`
136
- } else if (deepItem === null) {
137
- return `\\\\\\"(ao-type-atom) \\\\\\\\\\\\\\"null\\\\\\\\\\\\\\"\\\\\\"`
138
- } else if (deepItem === undefined) {
139
- return `\\\\\\"(ao-type-atom) \\\\\\\\\\\\\\"undefined\\\\\\\\\\\\\\"\\\\\\"`
140
- } else if (typeof deepItem === "symbol") {
141
- const desc = deepItem.description || "Symbol.for()"
142
- return `\\\\\\"(ao-type-atom) \\\\\\\\\\\\\\"${desc}\\\\\\\\\\\\\\"\\\\\\"`
143
- } else if (typeof deepItem === "boolean") {
144
- return `\\\\\\"(ao-type-atom) \\\\\\\\\\\\\\"${deepItem}\\\\\\\\\\\\\\"\\\\\\"`
145
- } else {
146
- return `\\\\\\"${String(deepItem)}\\\\\\"`
147
- }
148
- })
149
- .join(", ")
150
- return `\\"(ao-type-list) ${deeperItems}\\"`
151
- } else {
152
- return `\\"${String(nestedItem)}\\"`
153
- }
154
- })
155
- .join(", ")
156
- return `"(ao-type-list) ${nestedItems}"`
157
- } else if (isBytes(item)) {
158
- const buffer = toBuffer(item)
159
- if (buffer.length === 0 || buffer.byteLength === 0) {
160
- return `""`
161
- }
162
- return `"(ao-type-binary)"`
163
- } else if (isPojo(item)) {
164
- const json = JSON.stringify(item)
165
- const escaped = json.replace(/\\/g, "\\\\").replace(/"/g, '\\"')
166
- return `"(ao-type-map) ${escaped}"`
167
- } else {
168
- return `"${String(item)}"`
169
- }
170
- }
171
-
172
- function toBuffer(value) {
173
- if (Buffer.isBuffer(value)) {
174
- return value
175
- } else if (
176
- value &&
177
- typeof value === "object" &&
178
- value.type === "Buffer" &&
179
- Array.isArray(value.data)
180
- ) {
181
- return Buffer.from(value.data)
182
- } else if (value instanceof ArrayBuffer || ArrayBuffer.isView(value)) {
183
- return Buffer.from(value)
184
- } else {
185
- return Buffer.from(value)
186
- }
187
- }
188
-
189
- function collectBodyKeys(obj, prefix = "") {
190
- console.log("=== collectBodyKeys START ===")
191
- console.log("Input object:", JSON.stringify(obj))
192
-
193
- const keys = []
194
-
195
- function traverse(current, path) {
196
- console.log(`[traverse] Called with path: "${path}"`)
197
- let hasSimpleFields = false
198
- const nestedPaths = []
199
- let hasArraysWithObjects = false
200
-
201
- for (const [key, value] of Object.entries(current)) {
202
- const fullPath = path ? `${path}/${key}` : key
203
-
204
- if (Array.isArray(value)) {
205
- console.log(
206
- `[traverse] Found array at ${fullPath}, length: ${value.length}`
207
- )
208
- const hasObjects = value.some(item => isPojo(item))
209
- const hasNonObjects = value.some(item => !isPojo(item))
210
-
211
- if (value.length === 0) {
212
- console.log(
213
- `[traverse] Empty array at ${fullPath} - marking parent as having simple fields`
214
- )
215
- hasSimpleFields = true
216
- } else if (hasObjects) {
217
- hasArraysWithObjects = true
218
- // Check if we need special handling for mixed arrays
219
- const hasEmptyStrings = value.some(
220
- item => typeof item === "string" && item === ""
221
- )
222
- const hasEmptyObjects = value.some(
223
- item => isPojo(item) && Object.keys(item).length === 0
224
- )
225
- const hasNonEmptyObjects = value.some(
226
- item => isPojo(item) && Object.keys(item).length > 0
227
- )
228
-
229
- // Check if objects contain only empty values (not empty objects)
230
- const hasObjectsWithOnlyEmptyValues = value.some(item => {
231
- if (!isPojo(item) || Object.keys(item).length === 0) return false
232
- return Object.values(item).every(
233
- v =>
234
- (typeof v === "string" && v === "") ||
235
- (Array.isArray(v) && v.length === 0) ||
236
- (isPojo(v) && Object.keys(v).length === 0)
237
- )
238
- })
239
-
240
- // Only use special handling if we have BOTH empty elements AND non-empty objects
241
- if ((hasEmptyStrings || hasEmptyObjects) && hasNonEmptyObjects) {
242
- // Special case: mixed array with empty strings/objects - only non-empty objects get parts
243
- value.forEach((item, index) => {
244
- if (isPojo(item) && Object.keys(item).length > 0) {
245
- const itemPath = `${fullPath}/${index + 1}`
246
- keys.push(itemPath)
247
- nestedPaths.push(itemPath)
248
- }
249
- })
250
- if (hasNonObjects) {
251
- hasSimpleFields = true
252
- keys.push(fullPath)
253
- }
254
- } else if (hasObjectsWithOnlyEmptyValues && !hasNonObjects) {
255
- // Special case: objects that contain only empty values should get parts
256
- value.forEach((item, index) => {
257
- if (isPojo(item)) {
258
- const itemPath = `${fullPath}/${index + 1}`
259
- keys.push(itemPath)
260
- if (Object.keys(item).length > 0) {
261
- nestedPaths.push(itemPath)
262
- }
263
- }
264
- })
265
- } else {
266
- // Normal case: all objects get parts
267
- value.forEach((item, index) => {
268
- if (isPojo(item)) {
269
- const itemPath = `${fullPath}/${index + 1}`
270
- keys.push(itemPath)
271
- if (Object.keys(item).length > 0) {
272
- nestedPaths.push(itemPath)
273
- }
274
- }
275
- })
276
- if (hasNonObjects) {
277
- hasSimpleFields = true
278
- keys.push(fullPath)
279
- }
280
- }
281
- } else {
282
- console.log(
283
- `[traverse] Non-empty array without objects at ${fullPath} - marking as simple field`
284
- )
285
- hasSimpleFields = true
286
- }
287
- } else if (isPojo(value)) {
288
- if (Object.keys(value).length === 0) {
289
- console.log(
290
- `[traverse] Empty object at ${fullPath} - marking parent as having simple fields`
291
- )
292
- hasSimpleFields = true
293
- } else {
294
- // Don't traverse into the object if it only contains empty values
295
- const containsOnlyEmptyCollections = Object.entries(value).every(
296
- ([k, v]) => {
297
- return (
298
- (Array.isArray(v) && v.length === 0) ||
299
- (isPojo(v) && Object.keys(v).length === 0) ||
300
- (isBytes(v) && (v.length === 0 || v.byteLength === 0)) ||
301
- (typeof v === "string" && v.length === 0)
302
- )
303
- }
304
- )
305
-
306
- if (containsOnlyEmptyCollections && Object.keys(value).length > 0) {
307
- console.log(
308
- `[traverse] Object at ${fullPath} contains only empty collections - adding as body key`
309
- )
310
- keys.push(fullPath)
311
- } else {
312
- // Check if this object contains arrays with only empty elements
313
- const hasArraysWithOnlyEmptyElements = Object.entries(value).some(
314
- ([k, v]) => {
315
- return (
316
- Array.isArray(v) &&
317
- v.length > 0 &&
318
- v.every(
319
- item =>
320
- (Array.isArray(item) && item.length === 0) ||
321
- (isPojo(item) && Object.keys(item).length === 0) ||
322
- (typeof item === "string" && item === "")
323
- )
324
- )
325
- }
326
- )
327
-
328
- if (hasArraysWithOnlyEmptyElements) {
329
- // This object needs a body part to show its array types
330
- console.log(
331
- `[traverse] Object at ${fullPath} has arrays with empty elements - adding as body key`
332
- )
333
- keys.push(fullPath)
334
- }
335
-
336
- console.log(
337
- `[traverse] Non-empty object at ${fullPath} - will traverse into it`
338
- )
339
- nestedPaths.push(fullPath)
340
- }
341
- }
342
- } else if (isBytes(value)) {
343
- const buffer = toBuffer(value)
344
- if (buffer.length > 0) {
345
- hasSimpleFields = true
346
- }
347
- } else if (
348
- typeof value === "string" ||
349
- typeof value === "number" ||
350
- typeof value === "boolean" ||
351
- value === null ||
352
- value === undefined ||
353
- typeof value === "symbol"
354
- ) {
355
- hasSimpleFields = true
356
- }
357
- }
358
-
359
- if (hasSimpleFields) {
360
- console.log(`[traverse] Adding "${path}" to keys (has simple fields)`)
361
- keys.push(path)
362
- } else if (hasArraysWithObjects && path) {
363
- // If the object only contains arrays with objects, we still need to add it as a body key
364
- console.log(
365
- `[traverse] Adding "${path}" to keys (contains arrays with objects)`
366
- )
367
- keys.push(path)
368
- }
369
-
370
- // Check for arrays with only empty elements that need their own body parts
371
- for (const [key, value] of Object.entries(current)) {
372
- const fullPath = path ? `${path}/${key}` : key
373
-
374
- if (Array.isArray(value) && value.length > 0) {
375
- const hasOnlyEmptyElements = value.every(
376
- item =>
377
- (Array.isArray(item) && item.length === 0) ||
378
- (isPojo(item) && Object.keys(item).length === 0) ||
379
- (typeof item === "string" && item === "")
380
- )
381
-
382
- if (hasOnlyEmptyElements) {
383
- console.log(
384
- `[traverse] Array at ${fullPath} has only empty elements - adding as body key`
385
- )
386
- keys.push(fullPath)
387
- }
388
- }
389
- }
390
-
391
- for (const nestedPath of nestedPaths) {
392
- const parts = nestedPath.split("/")
393
- let nestedObj = obj
394
-
395
- for (const part of parts) {
396
- if (/^\d+$/.test(part)) {
397
- nestedObj = nestedObj[parseInt(part) - 1]
398
- } else {
399
- nestedObj = nestedObj[part]
400
- }
401
- }
402
-
403
- if (isPojo(nestedObj)) {
404
- traverse(nestedObj, nestedPath)
405
- }
406
- }
407
- }
408
-
409
- const objKeys = Object.keys(obj)
410
-
411
- for (const [key, value] of Object.entries(obj)) {
412
- console.log(`\n[main loop] Processing key: "${key}"`)
413
- console.log(
414
- `[main loop] Value type: ${Array.isArray(value) ? "array" : typeof value}`
415
- )
416
- console.log(
417
- `[main loop] Array length: ${Array.isArray(value) ? value.length : "N/A"}`
418
- )
419
-
420
- if (
421
- (key === "data" || key === "body") &&
422
- (typeof value === "string" ||
423
- typeof value === "boolean" ||
424
- typeof value === "number" ||
425
- value === null ||
426
- value === undefined ||
427
- typeof value === "symbol") &&
428
- objKeys.length > 1
429
- ) {
430
- // Special handling: only add to body keys if there's no other data/body field with an object
431
- if (
432
- key === "data" &&
433
- obj.body &&
434
- isPojo(obj.body) &&
435
- Object.keys(obj.body).length > 0
436
- ) {
437
- console.log(`[main loop] Skipping special data field`)
438
- } else if (
439
- key === "body" &&
440
- obj.data &&
441
- isPojo(obj.data) &&
442
- Object.keys(obj.data).length > 0
443
- ) {
444
- console.log(`[main loop] Skipping special body field`)
445
- } else {
446
- console.log(`[main loop] Adding special data/body key: "${key}"`)
447
- keys.push(key)
448
- }
449
- } else if (Array.isArray(value)) {
450
- if (value.length === 0) {
451
- console.log(`[main loop] SKIPPING empty array for key: "${key}"`)
452
- continue
453
- }
454
-
455
- const hasObjects = value.some(item => isPojo(item))
456
- const hasArrays = value.some(item => Array.isArray(item))
457
- const hasNonObjects = value.some(item => !isPojo(item))
458
-
459
- // Check if this is an array of arrays containing objects
460
- const hasArraysOfObjects = value.some(
461
- item => Array.isArray(item) && item.some(subItem => isPojo(subItem))
462
- )
463
-
464
- console.log(
465
- `[main loop] Array analysis: hasObjects=${hasObjects}, hasArrays=${hasArrays}, hasNonObjects=${hasNonObjects}, hasArraysOfObjects=${hasArraysOfObjects}`
466
- )
467
-
468
- if (value.length > 0) {
469
- let bodyPartCounter = 1 // Start counting from 1
470
-
471
- // Check for special mixed array case
472
- const hasEmptyStrings = value.some(
473
- item => typeof item === "string" && item === ""
474
- )
475
- const hasEmptyObjects = value.some(
476
- item => isPojo(item) && Object.keys(item).length === 0
477
- )
478
-
479
- // Check for objects that contain only empty values
480
- const hasObjectsWithOnlyEmptyValues = value.some(item => {
481
- if (!isPojo(item) || Object.keys(item).length === 0) return false
482
- return Object.values(item).every(
483
- v =>
484
- (typeof v === "string" && v === "") ||
485
- (Array.isArray(v) && v.length === 0) ||
486
- (isPojo(v) && Object.keys(v).length === 0)
487
- )
488
- })
489
-
490
- if (hasArraysOfObjects) {
491
- // Handle arrays of arrays containing objects
492
- value.forEach((item, index) => {
493
- if (Array.isArray(item)) {
494
- item.forEach((subItem, subIndex) => {
495
- if (isPojo(subItem)) {
496
- const path = `${key}/${index + 1}/${subIndex + 1}`
497
- console.log(
498
- `[main loop] Adding nested object path: "${path}"`
499
- )
500
- keys.push(path)
501
- }
502
- })
503
- }
504
- bodyPartCounter++
505
- })
506
- // Always add the main array key
507
- console.log(`[main loop] ADDING main array key: "${key}"`)
508
- keys.push(key)
509
- } else if (
510
- hasObjects &&
511
- (hasEmptyStrings || hasEmptyObjects) &&
512
- !hasObjectsWithOnlyEmptyValues
513
- ) {
514
- // Special handling: only non-empty objects get parts
515
- value.forEach((item, index) => {
516
- if (isPojo(item) && Object.keys(item).length > 0) {
517
- const path = `${key}/${bodyPartCounter}`
518
- console.log(
519
- `[main loop] Adding non-empty object path: "${path}" (array index ${index})`
520
- )
521
- keys.push(path)
522
- // Add paths for nested objects
523
- for (const [nestedKey, nestedValue] of Object.entries(item)) {
524
- if (isPojo(nestedValue)) {
525
- const nestedPath = `${key}/${bodyPartCounter}/${nestedKey}`
526
- console.log(
527
- `[main loop] Adding nested object path: "${nestedPath}"`
528
- )
529
- keys.push(nestedPath)
530
- }
531
- }
532
- }
533
- bodyPartCounter++
534
- })
535
- // Always add the main array key
536
- console.log(`[main loop] ADDING main array key: "${key}"`)
537
- keys.push(key)
538
- } else if (hasObjects) {
539
- // Normal handling: all objects get parts (except if parent array has only empty elements)
540
- let skipEmptyObjects = false
541
-
542
- // Check if this array contains only empty elements
543
- const arrayHasOnlyEmptyElements = value.every(
544
- item =>
545
- (Array.isArray(item) && item.length === 0) ||
546
- (isPojo(item) && Object.keys(item).length === 0) ||
547
- (typeof item === "string" && item === "")
548
- )
549
-
550
- if (arrayHasOnlyEmptyElements) {
551
- skipEmptyObjects = true
552
- }
553
-
554
- value.forEach((item, index) => {
555
- if (isPojo(item)) {
556
- // Skip empty objects if array has only empty elements
557
- if (skipEmptyObjects && Object.keys(item).length === 0) {
558
- bodyPartCounter++
559
- return
560
- }
561
-
562
- const path = `${key}/${bodyPartCounter}`
563
- console.log(
564
- `[main loop] Adding object path: "${path}" (array index ${index}, empty=${Object.keys(item).length === 0})`
565
- )
566
- keys.push(path)
567
- // Add paths for nested objects (but not empty ones)
568
- if (Object.keys(item).length > 0) {
569
- for (const [nestedKey, nestedValue] of Object.entries(item)) {
570
- if (
571
- isPojo(nestedValue) &&
572
- Object.keys(nestedValue).length > 0
573
- ) {
574
- const nestedPath = `${key}/${bodyPartCounter}/${nestedKey}`
575
- console.log(
576
- `[main loop] Adding nested object path: "${nestedPath}"`
577
- )
578
- keys.push(nestedPath)
579
- }
580
- }
581
- }
582
- } else if (typeof item === "string" && item === "") {
583
- // Empty strings may get parts in some formats
584
- const path = `${key}/${bodyPartCounter}`
585
- console.log(
586
- `[main loop] Adding empty string path: "${path}" (array index ${index})`
587
- )
588
- keys.push(path)
589
- }
590
- bodyPartCounter++
591
- })
592
- // Don't add main array key for arrays with only objects containing empty values
593
- if (
594
- !hasObjectsWithOnlyEmptyValues ||
595
- value.some(item => !isPojo(item))
596
- ) {
597
- // Check if array has only empty elements
598
- const hasOnlyEmptyElements = value.every(
599
- item =>
600
- (Array.isArray(item) && item.length === 0) ||
601
- (isPojo(item) && Object.keys(item).length === 0) ||
602
- (typeof item === "string" && item === "")
603
- )
604
-
605
- if (!hasOnlyEmptyElements) {
606
- // Always add the main array key
607
- console.log(`[main loop] ADDING main array key: "${key}"`)
608
- keys.push(key)
609
- }
610
- }
611
- } else {
612
- // Check if array has only empty elements
613
- const hasOnlyEmptyArraysOrObjects = value.every(
614
- item =>
615
- (Array.isArray(item) && item.length === 0) ||
616
- (isPojo(item) && Object.keys(item).length === 0) ||
617
- (typeof item === "string" && item === "")
618
- )
619
-
620
- if (hasOnlyEmptyArraysOrObjects && value.length > 0) {
621
- // Always add the main array key for arrays with only empty elements
622
- console.log(
623
- `[main loop] ADDING main array key for empty elements: "${key}"`
624
- )
625
- keys.push(key)
626
- } else if (!hasOnlyEmptyArraysOrObjects) {
627
- // Always add the main array key
628
- console.log(`[main loop] ADDING main array key: "${key}"`)
629
- keys.push(key)
630
- }
631
- }
632
- }
633
- } else if (isPojo(value)) {
634
- console.log(`[main loop] Processing object at key: "${key}"`)
635
- // Objects should be traversed, not have their fields individually added
636
- traverse(value, key)
637
- } else if (isBytes(value)) {
638
- const buffer = toBuffer(value)
639
- if (buffer.length > 0) {
640
- console.log(`[main loop] Adding key for non-empty bytes: "${key}"`)
641
- keys.push(key)
642
- }
643
- } else if (typeof value === "string" && value.includes("\n")) {
644
- console.log(`[main loop] Adding key for string with newline: "${key}"`)
645
- keys.push(key)
646
- } else if (typeof value === "string" && hasNonAscii(value)) {
647
- console.log(`[main loop] Adding key for non-ASCII string: "${key}"`)
648
- keys.push(key)
649
- } else {
650
- console.log(`[main loop] Skipping key: "${key}" (no match)`)
651
- }
652
- }
653
-
654
- const result = [...new Set(keys)].filter(k => {
655
- if (k === "") return false
656
-
657
- // Check if this is a path to an empty object inside an array with only empty elements
658
- const parts = k.split("/")
659
- if (parts.length >= 2 && /^\d+$/.test(parts[parts.length - 1])) {
660
- // This is an array element path like "maps/1"
661
- const arrayPath = parts.slice(0, -1).join("/")
662
- let arrayValue = obj
663
-
664
- // Navigate to the array
665
- for (const part of parts.slice(0, -1)) {
666
- if (/^\d+$/.test(part)) {
667
- arrayValue = arrayValue[parseInt(part) - 1]
668
- } else {
669
- arrayValue = arrayValue[part]
670
- }
671
- }
672
-
673
- // Check if this array contains only empty elements
674
- if (Array.isArray(arrayValue)) {
675
- const hasOnlyEmptyElements = arrayValue.every(
676
- item =>
677
- (Array.isArray(item) && item.length === 0) ||
678
- (isPojo(item) && Object.keys(item).length === 0) ||
679
- (typeof item === "string" && item === "")
680
- )
681
-
682
- if (hasOnlyEmptyElements) {
683
- // Filter out paths to individual empty elements
684
- console.log(`[filter] Removing path to empty element: "${k}"`)
685
- return false
686
- }
687
- }
688
- }
689
-
690
- return true
691
- })
692
- console.log("\n=== collectBodyKeys RESULT ===")
693
- console.log("Final bodyKeys:", JSON.stringify(result))
694
- console.log("=== collectBodyKeys END ===\n")
695
-
696
- return result
21
+ // Step 1: Process and normalize input values (handle symbols, nested objects/arrays)
22
+ function processInputValues(obj) {
23
+ // Currently this is a no-op, but will be used for input validation/normalization
24
+ return obj
697
25
  }
698
26
 
699
- async function encode(obj = {}) {
700
- console.log("\n=== ENCODE START ===")
701
- console.log("Encoding object:", JSON.stringify(obj))
702
-
703
- const processValue = value => {
704
- if (typeof value === "symbol") {
705
- return value.description || "Symbol.for()"
706
- } else if (Array.isArray(value)) {
707
- return value.map(processValue)
708
- } else if (isPojo(value)) {
709
- const result = {}
710
- for (const [k, v] of Object.entries(value)) {
711
- result[k] = processValue(v)
712
- }
713
- return result
714
- }
715
- return value
716
- }
717
-
718
- const processedObj = {}
719
- for (const [k, v] of Object.entries(obj)) {
720
- processedObj[k] = processValue(v)
721
- }
722
-
27
+ // Step 2: Handle empty object case
28
+ function handleEmptyObject(obj) {
723
29
  if (Object.keys(obj).length === 0) {
724
30
  return { headers: {}, body: undefined }
725
31
  }
32
+ return null
33
+ }
726
34
 
35
+ // Step 3: Handle single field with empty binary
36
+ function handleSingleEmptyBinaryField(obj) {
727
37
  const objKeys = Object.keys(obj)
728
38
 
729
39
  if (objKeys.length === 1) {
@@ -740,14 +50,11 @@ async function encode(obj = {}) {
740
50
  }
741
51
  }
742
52
 
743
- if (
744
- obj.body &&
745
- isBytes(obj.body) &&
746
- (obj.body.length === 0 || obj.body.byteLength === 0) &&
747
- objKeys.length > 1
748
- ) {
749
- }
53
+ return null
54
+ }
750
55
 
56
+ // Step 4: Handle single field with binary data
57
+ async function handleSingleBinaryField(obj) {
751
58
  const hasBodyBinary = obj.body && isBytes(obj.body)
752
59
  const otherFields = Object.keys(obj).filter(k => k !== "body")
753
60
 
@@ -766,28 +73,18 @@ async function encode(obj = {}) {
766
73
  return { headers, body: obj.body }
767
74
  }
768
75
 
76
+ return null
77
+ }
78
+
79
+ // Step 5: Handle single field with primitive value (string/number/boolean/null/undefined/symbol)
80
+ async function handleSinglePrimitiveField(obj) {
81
+ const objKeys = Object.keys(obj)
82
+
769
83
  if (objKeys.length === 1) {
770
84
  const fieldName = objKeys[0]
771
85
  const fieldValue = obj[fieldName]
772
86
 
773
- if (isBytes(fieldValue) && fieldValue.length > 0) {
774
- const headers = {}
775
- const bodyBuffer = toBuffer(fieldValue)
776
- const bodyArrayBuffer = bodyBuffer.buffer.slice(
777
- bodyBuffer.byteOffset,
778
- bodyBuffer.byteOffset + bodyBuffer.byteLength
779
- )
780
-
781
- const contentDigest = await sha256(bodyArrayBuffer)
782
- const base64 = base64url.toBase64(base64url.encode(contentDigest))
783
- headers["content-digest"] = `sha-256=:${base64}:`
784
-
785
- if (fieldName !== "body") {
786
- headers["inline-body-key"] = fieldName
787
- }
788
-
789
- return { headers, body: fieldValue }
790
- } else if (
87
+ if (
791
88
  (fieldName === "data" || fieldName === "body") &&
792
89
  (typeof fieldValue === "string" ||
793
90
  typeof fieldValue === "boolean" ||
@@ -797,21 +94,7 @@ async function encode(obj = {}) {
797
94
  typeof fieldValue === "symbol")
798
95
  ) {
799
96
  const headers = {}
800
-
801
- let bodyContent
802
- if (typeof fieldValue === "string") {
803
- bodyContent = fieldValue
804
- } else if (typeof fieldValue === "boolean") {
805
- bodyContent = `"${fieldValue}"`
806
- } else if (typeof fieldValue === "number") {
807
- bodyContent = String(fieldValue)
808
- } else if (fieldValue === null) {
809
- bodyContent = '"null"'
810
- } else if (fieldValue === undefined) {
811
- bodyContent = '"undefined"'
812
- } else if (typeof fieldValue === "symbol") {
813
- bodyContent = `"${fieldValue.description || "Symbol.for()"}"`
814
- }
97
+ const bodyContent = encodePrimitiveContent(fieldValue)
815
98
 
816
99
  const encoder = new TextEncoder()
817
100
  const encoded = encoder.encode(bodyContent)
@@ -819,16 +102,9 @@ async function encode(obj = {}) {
819
102
  const base64 = base64url.toBase64(base64url.encode(contentDigest))
820
103
  headers["content-digest"] = `sha-256=:${base64}:`
821
104
 
822
- if (
823
- typeof fieldValue === "boolean" ||
824
- fieldValue === null ||
825
- fieldValue === undefined ||
826
- typeof fieldValue === "symbol"
827
- ) {
828
- headers["ao-types"] = `${fieldName.toLowerCase()}="atom"`
829
- } else if (typeof fieldValue === "number") {
830
- headers["ao-types"] =
831
- `${fieldName.toLowerCase()}="${Number.isInteger(fieldValue) ? "integer" : "float"}"`
105
+ const aoType = getAoType(fieldValue)
106
+ if (aoType === "atom" || aoType === "integer" || aoType === "float") {
107
+ headers["ao-types"] = `${fieldName.toLowerCase()}="${aoType}"`
832
108
  }
833
109
 
834
110
  if (fieldName !== "body") {
@@ -836,7 +112,52 @@ async function encode(obj = {}) {
836
112
  }
837
113
 
838
114
  return { headers, body: bodyContent }
839
- } else if (typeof fieldValue === "string" && hasNonAscii(fieldValue)) {
115
+ }
116
+ }
117
+
118
+ return null
119
+ }
120
+
121
+ // Step 6a: Handle single field with non-empty binary (not body field)
122
+ async function handleSingleNonEmptyBinaryField(obj) {
123
+ const objKeys = Object.keys(obj)
124
+
125
+ if (objKeys.length === 1) {
126
+ const fieldName = objKeys[0]
127
+ const fieldValue = obj[fieldName]
128
+
129
+ if (isBytes(fieldValue) && fieldValue.length > 0) {
130
+ const headers = {}
131
+ const bodyBuffer = toBuffer(fieldValue)
132
+ const bodyArrayBuffer = bodyBuffer.buffer.slice(
133
+ bodyBuffer.byteOffset,
134
+ bodyBuffer.byteOffset + bodyBuffer.byteLength
135
+ )
136
+
137
+ const contentDigest = await sha256(bodyArrayBuffer)
138
+ const base64 = base64url.toBase64(base64url.encode(contentDigest))
139
+ headers["content-digest"] = `sha-256=:${base64}:`
140
+
141
+ if (fieldName !== "body") {
142
+ headers["inline-body-key"] = fieldName
143
+ }
144
+
145
+ return { headers, body: fieldValue }
146
+ }
147
+ }
148
+
149
+ return null
150
+ }
151
+
152
+ // Step 6: Handle single field with non-ASCII string
153
+ async function handleSingleNonAsciiStringField(obj) {
154
+ const objKeys = Object.keys(obj)
155
+
156
+ if (objKeys.length === 1) {
157
+ const fieldName = objKeys[0]
158
+ const fieldValue = obj[fieldName]
159
+
160
+ if (typeof fieldValue === "string" && hasNonAscii(fieldValue)) {
840
161
  const headers = {}
841
162
  const encoder = new TextEncoder()
842
163
  const encoded = encoder.encode(fieldValue)
@@ -852,10 +173,16 @@ async function encode(obj = {}) {
852
173
  }
853
174
  }
854
175
 
855
- const bodyKeys = collectBodyKeys(obj)
856
- const headers = {}
857
- const headerTypes = []
176
+ return null
177
+ }
178
+
179
+ // Step 7: Collect all keys that need to go in body
180
+ function collectBodyKeysStep(obj) {
181
+ return collectBodyKeys(obj)
182
+ }
858
183
 
184
+ // Step 8: Process fields that can go in headers
185
+ function processHeaderFields(obj, bodyKeys, headers, headerTypes) {
859
186
  for (const [key, value] of Object.entries(obj)) {
860
187
  const needsBody =
861
188
  bodyKeys.includes(key) || bodyKeys.some(k => k.startsWith(`${key}/`))
@@ -908,29 +235,15 @@ async function encode(obj = {}) {
908
235
  headerTypes.push(`${key.toLowerCase()}="empty-message"`)
909
236
  }
910
237
  } else {
911
- if (isBytes(value) && (value.length === 0 || value.byteLength === 0)) {
912
- headerTypes.push(`${key.toLowerCase()}="empty-binary"`)
913
- } else if (typeof value === "string" && value.length === 0) {
914
- headerTypes.push(`${key.toLowerCase()}="empty-binary"`)
915
- } else if (Array.isArray(value) && value.length === 0) {
916
- headerTypes.push(`${key.toLowerCase()}="empty-list"`)
917
- } else if (isPojo(value) && Object.keys(value).length === 0) {
918
- headerTypes.push(`${key.toLowerCase()}="empty-message"`)
919
- } else if (
920
- typeof value === "boolean" ||
921
- value === null ||
922
- value === undefined ||
923
- typeof value === "symbol"
924
- ) {
925
- headerTypes.push(`${key.toLowerCase()}="atom"`)
926
- } else if (typeof value === "number") {
927
- headerTypes.push(
928
- `${key.toLowerCase()}="${Number.isInteger(value) ? "integer" : "float"}"`
929
- )
238
+ // Fields that need body still get type annotations
239
+ const aoType = getAoType(value)
240
+ if (aoType) {
241
+ headerTypes.push(`${key.toLowerCase()}="${aoType}"`)
930
242
  }
931
243
  }
932
244
  }
933
245
 
246
+ // Second pass for array type annotations
934
247
  for (const [key, value] of Object.entries(obj)) {
935
248
  if (Array.isArray(value)) {
936
249
  if (
@@ -943,9 +256,11 @@ async function encode(obj = {}) {
943
256
  }
944
257
  }
945
258
  }
259
+ }
946
260
 
261
+ // Step 9: Handle case where all body keys are empty binaries
262
+ function handleAllEmptyBinaryBodyKeys(obj, bodyKeys, headers, headerTypes) {
947
263
  if (bodyKeys.length === 0) {
948
- console.log("No bodyKeys, returning headers only")
949
264
  if (headerTypes.length > 0) {
950
265
  headers["ao-types"] = headerTypes.sort().join(", ")
951
266
  }
@@ -953,19 +268,7 @@ async function encode(obj = {}) {
953
268
  }
954
269
 
955
270
  const allBodyKeysAreEmptyBinaries = bodyKeys.every(key => {
956
- const pathParts = key.split("/")
957
- let value = obj
958
- for (const part of pathParts) {
959
- if (/^\d+$/.test(part)) {
960
- const index = parseInt(part) - 1
961
- console.log(
962
- `[Body part] Getting array element at index ${index} from part ${part}`
963
- )
964
- value = value[index]
965
- } else {
966
- value = value[part]
967
- }
968
- }
271
+ const value = getValueByPath(obj, key)
969
272
  return isBytes(value) && (value.length === 0 || value.byteLength === 0)
970
273
  })
971
274
 
@@ -976,26 +279,23 @@ async function encode(obj = {}) {
976
279
  return { headers, body: undefined }
977
280
  }
978
281
 
282
+ return null
283
+ }
284
+
285
+ // Step 10: Handle single body key optimization
286
+ async function handleSingleBodyKeyOptimization(
287
+ obj,
288
+ bodyKeys,
289
+ headers,
290
+ headerTypes
291
+ ) {
979
292
  if (bodyKeys.length === 1) {
980
293
  const singleKey = bodyKeys[0]
981
- const pathParts = singleKey.split("/")
982
- let value = obj
983
- for (const part of pathParts) {
984
- if (/^\d+$/.test(part)) {
985
- value = value[parseInt(part) - 1]
986
- } else {
987
- value = value[part]
988
- }
989
- }
294
+ const value = getValueByPath(obj, singleKey)
990
295
 
991
296
  const otherFieldsAreEmpty = Object.entries(obj).every(([key, val]) => {
992
297
  if (key === singleKey) return true
993
- return (
994
- (Array.isArray(val) && val.length === 0) ||
995
- (isPojo(val) && Object.keys(val).length === 0) ||
996
- (isBytes(val) && (val.length === 0 || val.byteLength === 0)) ||
997
- (typeof val === "string" && val.length === 0)
998
- )
298
+ return isEmpty(val)
999
299
  })
1000
300
 
1001
301
  if (otherFieldsAreEmpty && isBytes(value) && value.length > 0) {
@@ -1021,23 +321,23 @@ async function encode(obj = {}) {
1021
321
  }
1022
322
  }
1023
323
 
1024
- // Sort body keys: main array comes first, then element parts by index
1025
- const sortedBodyKeys = bodyKeys.sort((a, b) => {
324
+ return null
325
+ }
326
+
327
+ // Step 11: Sort body keys
328
+ function sortBodyKeys(bodyKeys) {
329
+ return bodyKeys.sort((a, b) => {
1026
330
  const aIsArrayElement = /\/\d+$/.test(a)
1027
331
  const bIsArrayElement = /\/\d+$/.test(b)
1028
332
  const aBase = a.split("/")[0]
1029
333
  const bBase = b.split("/")[0]
1030
-
1031
- // If both are for the same array
1032
334
  if (aBase === bBase) {
1033
- // Main array comes before element parts
1034
335
  if (!aIsArrayElement && bIsArrayElement) {
1035
- return -1 // main array comes first
336
+ return -1
1036
337
  }
1037
338
  if (aIsArrayElement && !bIsArrayElement) {
1038
- return 1 // element parts come after
339
+ return 1
1039
340
  }
1040
- // Both are elements - sort by index
1041
341
  if (aIsArrayElement && bIsArrayElement) {
1042
342
  const aIndex = parseInt(a.split("/")[1])
1043
343
  const bIndex = parseInt(b.split("/")[1])
@@ -1045,13 +345,13 @@ async function encode(obj = {}) {
1045
345
  }
1046
346
  return a.localeCompare(b)
1047
347
  }
1048
-
1049
- // Different arrays, sort by base name
1050
348
  return a.localeCompare(b)
1051
349
  })
350
+ }
1052
351
 
1053
- // Check if we have the special case where data contains body with bytes and body contains data with bytes
1054
- const hasSpecialDataBody =
352
+ // Step 12: Check for special data/body case
353
+ function checkSpecialDataBodyCase(obj, sortedBodyKeys) {
354
+ return (
1055
355
  sortedBodyKeys.includes("data") &&
1056
356
  sortedBodyKeys.includes("body") &&
1057
357
  obj.data &&
@@ -1060,424 +360,593 @@ async function encode(obj = {}) {
1060
360
  obj.body &&
1061
361
  obj.body.data &&
1062
362
  isBytes(obj.body.data)
363
+ )
364
+ }
1063
365
 
1064
- headers["body-keys"] = sortedBodyKeys.map(k => `"${k}"`).join(", ")
366
+ // Step 13.2.2: Handle empty string in nested path
367
+ function handleEmptyStringInNestedPath(bodyKey, value, pathParts) {
368
+ if (typeof value === "string" && value === "" && pathParts.length > 1) {
369
+ const lines = []
370
+ lines.push(`content-disposition: form-data;name="${bodyKey}"`)
371
+ lines.push("")
372
+ lines.push("")
373
+ return new Blob([lines.join("\r\n")])
374
+ }
375
+ return null
376
+ }
1065
377
 
1066
- if (!hasSpecialDataBody) {
1067
- if (sortedBodyKeys.includes("body") && sortedBodyKeys.length === 1) {
1068
- headers["inline-body-key"] = "body"
378
+ // Step 13.2.3.2: Handle arrays with only empty elements
379
+ function handleArrayWithOnlyEmptyElements(
380
+ bodyKey,
381
+ value,
382
+ headers,
383
+ sortedBodyKeys
384
+ ) {
385
+ const fieldLines = []
386
+ const partTypes = []
387
+
388
+ value.forEach((item, idx) => {
389
+ const index = idx + 1
390
+ const itemType = getAoType(item)
391
+ if (itemType) {
392
+ partTypes.push(`${index}="${itemType}"`)
1069
393
  }
1070
- }
394
+ })
1071
395
 
1072
- if (headerTypes.length > 0) {
1073
- headers["ao-types"] = headerTypes.sort().join(", ")
396
+ const isInline = bodyKey === "body" && headers["inline-body-key"] === "body"
397
+
398
+ if (isInline) {
399
+ const orderedLines = []
400
+ if (partTypes.length > 0) {
401
+ orderedLines.push(
402
+ `ao-types: ${sortTypeAnnotations(partTypes).join(", ")}`
403
+ )
404
+ }
405
+ orderedLines.push("content-disposition: inline")
406
+ orderedLines.push("")
407
+ return new Blob([orderedLines.join("\r\n")])
408
+ } else {
409
+ const orderedLines = []
410
+ if (partTypes.length > 0) {
411
+ orderedLines.push(
412
+ `ao-types: ${sortTypeAnnotations(partTypes).join(", ")}`
413
+ )
414
+ }
415
+ orderedLines.push(`content-disposition: form-data;name="${bodyKey}"`)
416
+
417
+ const isLastBodyPart =
418
+ sortedBodyKeys.indexOf(bodyKey) === sortedBodyKeys.length - 1
419
+ const hasOnlyTypes = partTypes.length > 0 && fieldLines.length === 0
420
+
421
+ if (isLastBodyPart && hasOnlyTypes) {
422
+ return new Blob([orderedLines.join("\r\n")])
423
+ } else {
424
+ orderedLines.push("")
425
+ return new Blob([orderedLines.join("\r\n")])
426
+ }
1074
427
  }
428
+ }
1075
429
 
1076
- const bodyParts = []
430
+ // Step 13.2.3.3: Build indices with own parts
431
+ function buildIndicesWithOwnParts(bodyKey, sortedBodyKeys) {
432
+ const indicesWithOwnParts = new Set()
433
+ sortedBodyKeys.forEach(key => {
434
+ if (key.startsWith(bodyKey + "/")) {
435
+ const subPath = key.substring(bodyKey.length + 1)
436
+ const match = subPath.match(/^(\d+)/)
437
+ if (match) {
438
+ indicesWithOwnParts.add(parseInt(match[1]))
439
+ }
440
+ }
441
+ })
442
+ return indicesWithOwnParts
443
+ }
1077
444
 
1078
- for (const bodyKey of sortedBodyKeys) {
1079
- console.log(`\n[Body part] Processing bodyKey: ${bodyKey}`)
1080
- const lines = []
445
+ // Step 13.2.3.4: Process array items
446
+ function processArrayItems(
447
+ value,
448
+ indicesWithOwnParts,
449
+ hasNestedObjectParts,
450
+ pathParts
451
+ ) {
452
+ const fieldLines = []
453
+ const partTypes = []
454
+
455
+ if (hasNestedObjectParts) {
456
+ value.forEach((item, idx) => {
457
+ const index = idx + 1
458
+ if (Array.isArray(item)) {
459
+ partTypes.push(`${index}="list"`)
460
+ }
461
+ })
462
+ }
1081
463
 
1082
- const pathParts = bodyKey.split("/")
1083
- let value = obj
1084
- let parent = null
464
+ value.forEach((item, idx) => {
465
+ const index = idx + 1
1085
466
 
1086
- for (let i = 0; i < pathParts.length; i++) {
1087
- parent = value
1088
- const part = pathParts[i]
467
+ if (indicesWithOwnParts.has(index)) {
468
+ return
469
+ }
470
+ if (
471
+ hasNestedObjectParts &&
472
+ Array.isArray(item) &&
473
+ item.some(subItem => isPojo(subItem))
474
+ ) {
475
+ return
476
+ }
1089
477
 
1090
- if (/^\d+$/.test(part)) {
1091
- value = value[parseInt(part) - 1]
478
+ if (typeof item === "string" && item === "") {
479
+ partTypes.push(`${index}="empty-binary"`)
480
+ } else if (isPojo(item) && Object.keys(item).length === 0) {
481
+ partTypes.push(`${index}="empty-message"`)
482
+ } else if (isPojo(item)) {
483
+ // Non-empty objects are handled elsewhere
484
+ } else if (Array.isArray(item)) {
485
+ if (item.length === 0) {
486
+ partTypes.push(`${index}="empty-list"`)
487
+ } else {
488
+ partTypes.push(`${index}="list"`)
489
+ const encodedItems = item
490
+ .map(subItem => {
491
+ if (typeof subItem === "number") {
492
+ if (Number.isInteger(subItem)) {
493
+ return `"(ao-type-integer) ${subItem}"`
494
+ } else {
495
+ return `"(ao-type-float) ${formatFloat(subItem)}"`
496
+ }
497
+ } else if (typeof subItem === "string") {
498
+ return `"${subItem}"`
499
+ } else if (subItem === null) {
500
+ return `"(ao-type-atom) \\"null\\""`
501
+ } else if (subItem === undefined) {
502
+ return `"(ao-type-atom) \\"undefined\\""`
503
+ } else if (typeof subItem === "symbol") {
504
+ const desc = subItem.description || "Symbol.for()"
505
+ return `"(ao-type-atom) \\"${desc}\\""`
506
+ } else if (typeof subItem === "boolean") {
507
+ return `"(ao-type-atom) \\"${subItem}\\""`
508
+ } else if (Array.isArray(subItem)) {
509
+ return encodeArrayItem(subItem)
510
+ } else if (isBytes(subItem)) {
511
+ const buffer = toBuffer(subItem)
512
+ if (buffer.length === 0 || buffer.byteLength === 0) {
513
+ return `""`
514
+ }
515
+ return `"(ao-type-binary)"`
516
+ } else if (isPojo(subItem)) {
517
+ const json = JSON.stringify(subItem)
518
+ const escaped = json.replace(/\\/g, "\\\\").replace(/"/g, '\\"')
519
+ return `"(ao-type-map) ${escaped}"`
520
+ } else {
521
+ return `"${String(subItem)}"`
522
+ }
523
+ })
524
+ .join(", ")
525
+ fieldLines.push(`${index}: ${encodedItems}`)
526
+ }
527
+ } else if (typeof item === "number") {
528
+ if (Number.isInteger(item)) {
529
+ partTypes.push(`${index}="integer"`)
530
+ fieldLines.push(`${index}: ${item}`)
531
+ } else {
532
+ partTypes.push(`${index}="float"`)
533
+ fieldLines.push(`${index}: ${formatFloat(item)}`)
534
+ }
535
+ } else if (typeof item === "string") {
536
+ fieldLines.push(`${index}: ${item}`)
537
+ } else if (
538
+ item === null ||
539
+ item === undefined ||
540
+ typeof item === "symbol" ||
541
+ typeof item === "boolean"
542
+ ) {
543
+ partTypes.push(`${index}="atom"`)
544
+ if (item === null) {
545
+ fieldLines.push(`${index}: null`)
546
+ } else if (item === undefined) {
547
+ fieldLines.push(`${index}: undefined`)
548
+ } else if (typeof item === "symbol") {
549
+ const desc = item.description || "Symbol.for()"
550
+ fieldLines.push(`${index}: ${desc}`)
1092
551
  } else {
1093
- value = value[part]
552
+ fieldLines.push(`${index}: ${item}`)
553
+ }
554
+ } else if (isBytes(item)) {
555
+ const buffer = toBuffer(item)
556
+ if (buffer.length === 0) {
557
+ partTypes.push(`${index}="empty-binary"`)
558
+ } else {
559
+ partTypes.push(`${index}="binary"`)
1094
560
  }
1095
561
  }
562
+ })
1096
563
 
1097
- console.log(`[Body part] Value at ${bodyKey}:`, JSON.stringify(value))
564
+ return { fieldLines, partTypes }
565
+ }
1098
566
 
1099
- // Special handling for empty strings in arrays
1100
- if (typeof value === "string" && value === "" && pathParts.length > 1) {
1101
- console.log(`[Body part] Creating part for empty string at ${bodyKey}`)
1102
- lines.push(`content-disposition: form-data;name="${bodyKey}"`)
1103
- lines.push("")
1104
- lines.push("")
1105
- bodyParts.push(new Blob([lines.join("\r\n")]))
1106
- continue
1107
- }
567
+ // Step 13.2.3.5: Create array body part
568
+ function createArrayBodyPart(bodyKey, fieldLines, partTypes, headers) {
569
+ const isInline = bodyKey === "body" && headers["inline-body-key"] === "body"
1108
570
 
1109
- // Handle direct binary values
1110
- if (isBytes(value)) {
1111
- console.log(`[Body part] Creating part for binary at ${bodyKey}`)
1112
- lines.push(`content-disposition: form-data;name="${bodyKey}"`)
1113
- const buffer = toBuffer(value)
1114
- const headerText = lines.join("\r\n") + "\r\n\r\n"
1115
- bodyParts.push(new Blob([headerText, buffer]))
1116
- continue
571
+ if (isInline) {
572
+ const orderedLines = []
573
+ if (partTypes.length > 0) {
574
+ orderedLines.push(
575
+ `ao-types: ${sortTypeAnnotations(partTypes).join(", ")}`
576
+ )
1117
577
  }
1118
-
1119
- if (Array.isArray(value)) {
1120
- const hasOnlyEmptyElements =
1121
- value.length > 0 &&
1122
- value.every(
1123
- item =>
1124
- (Array.isArray(item) && item.length === 0) ||
1125
- (isPojo(item) && Object.keys(item).length === 0) ||
1126
- (typeof item === "string" && item === "")
1127
- )
1128
- const hasOnlyNonEmptyObjects =
1129
- value.length > 0 &&
1130
- value.every(item => isPojo(item) && Object.keys(item).length > 0)
1131
- const hasOnlyEmptyObjects =
1132
- value.length > 0 &&
1133
- value.every(item => isPojo(item) && Object.keys(item).length === 0)
1134
- const hasObjects = value.some(item => isPojo(item))
1135
- const hasArrays = value.some(item => Array.isArray(item))
1136
- const nonObjectItems = value
1137
- .map((item, index) => ({ item, index: index + 1 }))
1138
- .filter(({ item }) => !isPojo(item))
1139
-
1140
- if (hasOnlyNonEmptyObjects) {
1141
- continue
578
+ orderedLines.push("content-disposition: inline")
579
+ if (fieldLines.length > 0) {
580
+ orderedLines.push("")
581
+ for (const line of fieldLines) {
582
+ orderedLines.push(line)
1142
583
  }
584
+ }
585
+ return new Blob([orderedLines.join("\r\n") + "\r\n"])
586
+ } else {
587
+ const orderedLines = []
588
+ if (partTypes.length > 0) {
589
+ orderedLines.push(
590
+ `ao-types: ${sortTypeAnnotations(partTypes).join(", ")}`
591
+ )
592
+ }
593
+ orderedLines.push(`content-disposition: form-data;name="${bodyKey}"`)
594
+ for (const line of fieldLines) {
595
+ orderedLines.push(line)
596
+ }
597
+ if (fieldLines.length > 0) {
598
+ return new Blob([orderedLines.join("\r\n") + "\r\n"])
599
+ } else {
600
+ return new Blob([orderedLines.join("\r\n")])
601
+ }
602
+ }
603
+ }
1143
604
 
1144
- // For arrays containing only empty elements, we still need to show type info
1145
- if (hasOnlyEmptyElements) {
1146
- const fieldLines = []
1147
- const partTypes = []
1148
-
1149
- // Process items for type information
1150
- value.forEach((item, idx) => {
1151
- const index = idx + 1
1152
- if (Array.isArray(item) && item.length === 0) {
1153
- partTypes.push(`${index}="empty-list"`)
1154
- } else if (isPojo(item) && Object.keys(item).length === 0) {
1155
- partTypes.push(`${index}="empty-message"`)
1156
- } else if (typeof item === "string" && item === "") {
1157
- partTypes.push(`${index}="empty-binary"`)
1158
- }
1159
- })
1160
-
1161
- const isInline =
1162
- bodyKey === "body" && headers["inline-body-key"] === "body"
1163
-
1164
- if (isInline) {
1165
- const orderedLines = []
1166
- if (partTypes.length > 0) {
1167
- orderedLines.push(
1168
- `ao-types: ${partTypes
1169
- .sort((a, b) => {
1170
- const aNum = parseInt(a.split("=")[0])
1171
- const bNum = parseInt(b.split("=")[0])
1172
- return aNum - bNum
1173
- })
1174
- .join(", ")}`
1175
- )
1176
- }
1177
- orderedLines.push("content-disposition: inline")
1178
- orderedLines.push("")
1179
- bodyParts.push(new Blob([orderedLines.join("\r\n")]))
1180
- } else {
1181
- const orderedLines = []
1182
- if (partTypes.length > 0) {
1183
- orderedLines.push(
1184
- `ao-types: ${partTypes
1185
- .sort((a, b) => {
1186
- const aNum = parseInt(a.split("=")[0])
1187
- const bNum = parseInt(b.split("=")[0])
1188
- return aNum - bNum
1189
- })
1190
- .join(", ")}`
1191
- )
1192
- }
1193
- orderedLines.push(`content-disposition: form-data;name="${bodyKey}"`)
1194
-
1195
- // Check if this is the last body part
1196
- const isLastBodyPart =
1197
- sortedBodyKeys.indexOf(bodyKey) === sortedBodyKeys.length - 1
1198
- const hasOnlyTypes = partTypes.length > 0 && fieldLines.length === 0
1199
-
1200
- if (isLastBodyPart && hasOnlyTypes) {
1201
- // Don't add empty line for last part with only types
1202
- bodyParts.push(new Blob([orderedLines.join("\r\n")]))
1203
- } else {
1204
- orderedLines.push("")
1205
- bodyParts.push(new Blob([orderedLines.join("\r\n")]))
1206
- }
1207
- }
1208
- continue
1209
- }
605
+ // Step 13.2.3: Handle array values
606
+ function handleArrayValue(bodyKey, value, headers, sortedBodyKeys, pathParts) {
607
+ const arrayInfo = analyzeArray(value)
1210
608
 
1211
- // Build list of which indices have their own parts
1212
- const indicesWithOwnParts = new Set()
1213
- sortedBodyKeys.forEach(key => {
1214
- if (key.startsWith(bodyKey + "/")) {
1215
- const subPath = key.substring(bodyKey.length + 1)
1216
- const match = subPath.match(/^(\d+)/)
1217
- if (match) {
1218
- indicesWithOwnParts.add(parseInt(match[1]))
1219
- }
1220
- }
1221
- })
609
+ if (arrayInfo.hasOnlyNonEmptyObjects) {
610
+ return null
611
+ }
1222
612
 
1223
- // Check if this array contains sub-arrays with objects that have their own parts
1224
- const hasNestedObjectParts = sortedBodyKeys.some(
1225
- key =>
1226
- key.startsWith(bodyKey + "/") &&
1227
- key.split("/").length > pathParts.length + 1
1228
- )
613
+ if (arrayInfo.hasOnlyEmptyElements) {
614
+ return handleArrayWithOnlyEmptyElements(
615
+ bodyKey,
616
+ value,
617
+ headers,
618
+ sortedBodyKeys
619
+ )
620
+ }
1229
621
 
1230
- const fieldLines = []
1231
- const partTypes = []
1232
-
1233
- // For arrays that contain sub-arrays with objects, we need to add type info for the sub-arrays
1234
- if (hasNestedObjectParts) {
1235
- value.forEach((item, idx) => {
1236
- const index = idx + 1
1237
- if (Array.isArray(item)) {
1238
- partTypes.push(`${index}="list"`)
1239
- }
1240
- })
1241
- }
622
+ const indicesWithOwnParts = buildIndicesWithOwnParts(bodyKey, sortedBodyKeys)
623
+ const hasNestedObjectParts = sortedBodyKeys.some(
624
+ key =>
625
+ key.startsWith(bodyKey + "/") &&
626
+ key.split("/").length > pathParts.length + 1
627
+ )
1242
628
 
1243
- // Check if this array has mixed content with empty strings/objects
1244
- const hasEmptyStrings = value.some(
1245
- item => typeof item === "string" && item === ""
1246
- )
1247
- const hasEmptyObjects = value.some(
1248
- item => isPojo(item) && Object.keys(item).length === 0
629
+ const { fieldLines, partTypes } = processArrayItems(
630
+ value,
631
+ indicesWithOwnParts,
632
+ hasNestedObjectParts,
633
+ pathParts
634
+ )
635
+ return createArrayBodyPart(bodyKey, fieldLines, partTypes, headers)
636
+ }
637
+
638
+ // Step 13.2.4.3: Process object fields
639
+ function processObjectFields(value, bodyKey, sortedBodyKeys) {
640
+ const objectTypes = []
641
+ const fieldLines = []
642
+ const binaryFields = []
643
+ const arrayTypes = []
644
+
645
+ // First collect array types
646
+ for (const [k, v] of Object.entries(value)) {
647
+ if (Array.isArray(v)) {
648
+ arrayTypes.push(
649
+ `${k.toLowerCase()}="${v.length === 0 ? "empty-list" : "list"}"`
1249
650
  )
651
+ }
652
+ }
1250
653
 
1251
- // Check if we have objects with only empty values (like {empty: ""})
1252
- const hasObjectsWithOnlyEmptyValues = value.some(item => {
1253
- if (!isPojo(item) || Object.keys(item).length === 0) return false
1254
- return Object.values(item).every(
1255
- v =>
1256
- (typeof v === "string" && v === "") ||
1257
- (Array.isArray(v) && v.length === 0) ||
1258
- (isPojo(v) && Object.keys(v).length === 0)
1259
- )
1260
- })
654
+ // Then process other fields
655
+ for (const [k, v] of Object.entries(value)) {
656
+ const childPath = `${bodyKey}/${k}`
1261
657
 
1262
- const isMixedArray =
1263
- hasObjects &&
1264
- (hasEmptyStrings || hasEmptyObjects) &&
1265
- !hasObjectsWithOnlyEmptyValues
658
+ if (sortedBodyKeys.includes(childPath)) {
659
+ continue
660
+ }
1266
661
 
1267
- // Process ALL items for type information
1268
- value.forEach((item, idx) => {
1269
- const index = idx + 1
662
+ if (Array.isArray(v) && v.some(item => isPojo(item))) {
663
+ const hasOnlyEmpty = v.every(item => isEmpty(item))
664
+ if (hasOnlyEmpty) {
665
+ continue
666
+ }
667
+ }
1270
668
 
1271
- // Skip type info for elements that have their own parts
1272
- if (indicesWithOwnParts.has(index)) {
1273
- return
1274
- }
669
+ if (Array.isArray(v)) {
670
+ // Type already added in arrayTypes
671
+ } else if (
672
+ v === null ||
673
+ v === undefined ||
674
+ typeof v === "symbol" ||
675
+ typeof v === "boolean"
676
+ ) {
677
+ objectTypes.push(`${k.toLowerCase()}="atom"`)
678
+ } else if (typeof v === "number") {
679
+ objectTypes.push(
680
+ `${k.toLowerCase()}="${Number.isInteger(v) ? "integer" : "float"}"`
681
+ )
682
+ } else if (typeof v === "string" && v.length === 0) {
683
+ objectTypes.push(`${k.toLowerCase()}="empty-binary"`)
684
+ } else if (isBytes(v) && (v.length === 0 || v.byteLength === 0)) {
685
+ objectTypes.push(`${k.toLowerCase()}="empty-binary"`)
686
+ } else if (isPojo(v) && Object.keys(v).length === 0) {
687
+ objectTypes.push(`${k.toLowerCase()}="empty-message"`)
688
+ }
1275
689
 
1276
- // If we have nested object parts and this is an array with objects, skip processing it inline
1277
- if (
1278
- hasNestedObjectParts &&
1279
- Array.isArray(item) &&
1280
- item.some(subItem => isPojo(subItem))
1281
- ) {
1282
- // Type info already added above
1283
- return
690
+ if (typeof v === "string") {
691
+ if (v.length === 0) {
692
+ fieldLines.push(`${k}: `)
693
+ } else {
694
+ fieldLines.push(`${k}: ${v}`)
695
+ }
696
+ } else if (typeof v === "number") {
697
+ fieldLines.push(`${k}: ${v}`)
698
+ } else if (typeof v === "boolean") {
699
+ fieldLines.push(`${k}: "${v}"`)
700
+ } else if (v === null) {
701
+ fieldLines.push(`${k}: "null"`)
702
+ } else if (v === undefined) {
703
+ fieldLines.push(`${k}: "undefined"`)
704
+ } else if (typeof v === "symbol") {
705
+ const desc = v.description || "Symbol.for()"
706
+ fieldLines.push(`${k}: "${desc}"`)
707
+ } else if (isBytes(v)) {
708
+ const buffer = toBuffer(v)
709
+ binaryFields.push({ key: k, buffer })
710
+ } else if (Array.isArray(v) && v.length > 0) {
711
+ const childPath = `${bodyKey}/${k}`
712
+ if (!sortedBodyKeys.includes(childPath)) {
713
+ const hasObjects = v.some(item => isPojo(item))
714
+ if (!hasObjects) {
715
+ const encodedItems = v.map(item => encodeArrayItem(item)).join(", ")
716
+ fieldLines.push(`${k}: ${encodedItems}`)
1284
717
  }
718
+ }
719
+ }
720
+ }
1285
721
 
1286
- // For all arrays (not just mixed ones), we need to process all items
1287
- if (typeof item === "string" && item === "") {
1288
- // Empty strings get type annotation but no field line
1289
- partTypes.push(`${index}="empty-binary"`)
1290
- } else if (isPojo(item) && Object.keys(item).length === 0) {
1291
- // Empty objects don't get field lines but do get type annotations
1292
- partTypes.push(`${index}="empty-message"`)
1293
- } else if (isPojo(item)) {
1294
- // Non-empty objects might have parts
1295
- } else if (Array.isArray(item)) {
1296
- if (item.length === 0) {
1297
- // Empty arrays don't get field lines but do get type annotations
1298
- partTypes.push(`${index}="empty-list"`)
1299
- } else {
1300
- partTypes.push(`${index}="list"`)
1301
- // Encode array
1302
- const encodedItems = item
1303
- .map(subItem => {
1304
- if (typeof subItem === "number") {
1305
- if (Number.isInteger(subItem)) {
1306
- return `"(ao-type-integer) ${subItem}"`
1307
- } else {
1308
- return `"(ao-type-float) ${formatFloat(subItem)}"`
1309
- }
1310
- } else if (typeof subItem === "string") {
1311
- return `"${subItem}"`
1312
- } else if (subItem === null) {
1313
- return `"(ao-type-atom) \\"null\\""`
1314
- } else if (subItem === undefined) {
1315
- return `"(ao-type-atom) \\"undefined\\""`
1316
- } else if (typeof subItem === "symbol") {
1317
- const desc = subItem.description || "Symbol.for()"
1318
- return `"(ao-type-atom) \\"${desc}\\""`
1319
- } else if (typeof subItem === "boolean") {
1320
- return `"(ao-type-atom) \\"${subItem}\\""`
1321
- } else if (Array.isArray(subItem)) {
1322
- // Use the full encodeArrayItem for nested arrays
1323
- return encodeArrayItem(subItem)
1324
- } else if (isBytes(subItem)) {
1325
- const buffer = toBuffer(subItem)
1326
- if (buffer.length === 0 || buffer.byteLength === 0) {
1327
- return `""`
1328
- }
1329
- return `"(ao-type-binary)"`
1330
- } else if (isPojo(subItem)) {
1331
- const json = JSON.stringify(subItem)
1332
- const escaped = json
1333
- .replace(/\\/g, "\\\\")
1334
- .replace(/"/g, '\\"')
1335
- return `"(ao-type-map) ${escaped}"`
1336
- } else {
1337
- return `"${String(subItem)}"`
1338
- }
1339
- })
1340
- .join(", ")
1341
- fieldLines.push(`${index}: ${encodedItems}`)
1342
- }
1343
- } else if (typeof item === "number") {
1344
- if (Number.isInteger(item)) {
1345
- partTypes.push(`${index}="integer"`)
1346
- fieldLines.push(`${index}: ${item}`)
1347
- } else {
1348
- partTypes.push(`${index}="float"`)
1349
- fieldLines.push(`${index}: ${formatFloat(item)}`)
1350
- }
1351
- } else if (typeof item === "string") {
1352
- // Non-empty strings just get field lines, no type annotation
1353
- fieldLines.push(`${index}: ${item}`)
1354
- } else if (
1355
- item === null ||
1356
- item === undefined ||
1357
- typeof item === "symbol" ||
1358
- typeof item === "boolean"
1359
- ) {
1360
- partTypes.push(`${index}="atom"`)
1361
- if (item === null) {
1362
- fieldLines.push(`${index}: null`)
1363
- } else if (item === undefined) {
1364
- fieldLines.push(`${index}: undefined`)
1365
- } else if (typeof item === "symbol") {
1366
- const desc = item.description || "Symbol.for()"
1367
- fieldLines.push(`${index}: ${desc}`)
1368
- } else {
1369
- fieldLines.push(`${index}: ${item}`)
1370
- }
1371
- } else if (isBytes(item)) {
1372
- const buffer = toBuffer(item)
1373
- if (buffer.length === 0) {
1374
- partTypes.push(`${index}="empty-binary"`)
1375
- } else {
1376
- partTypes.push(`${index}="binary"`)
1377
- }
1378
- }
1379
- })
1380
-
1381
- const isInline =
1382
- bodyKey === "body" && headers["inline-body-key"] === "body"
1383
-
1384
- if (isInline) {
1385
- const orderedLines = []
1386
-
1387
- if (partTypes.length > 0) {
1388
- orderedLines.push(
1389
- `ao-types: ${partTypes
1390
- .sort((a, b) => {
1391
- const aNum = parseInt(a.split("=")[0])
1392
- const bNum = parseInt(b.split("=")[0])
1393
- return aNum - bNum
1394
- })
1395
- .join(", ")}`
1396
- )
1397
- }
722
+ const allTypes = [...arrayTypes, ...objectTypes]
723
+ return { allTypes, fieldLines, binaryFields }
724
+ }
1398
725
 
1399
- orderedLines.push("content-disposition: inline")
726
+ // Step 13.2.4.5: Create object body part
727
+ function createObjectBodyPart(
728
+ bodyKey,
729
+ value,
730
+ allTypes,
731
+ fieldLines,
732
+ binaryFields,
733
+ headers,
734
+ sortedBodyKeys
735
+ ) {
736
+ const isInline = bodyKey === "body" && headers["inline-body-key"] === "body"
737
+ const lines = []
738
+
739
+ if (isInline) {
740
+ lines.push(`content-disposition: inline`)
741
+ } else {
742
+ lines.push(`content-disposition: form-data;name="${bodyKey}"`)
743
+ }
1400
744
 
1401
- if (fieldLines.length > 0) {
1402
- orderedLines.push("")
1403
- for (const line of fieldLines) {
1404
- orderedLines.push(line)
1405
- }
1406
- }
745
+ if (isInline) {
746
+ const orderedLines = []
747
+ for (const line of fieldLines) {
748
+ orderedLines.push(line)
749
+ }
750
+ if (allTypes.length > 0) {
751
+ orderedLines.push(`ao-types: ${allTypes.sort().join(", ")}`)
752
+ }
753
+ orderedLines.push("content-disposition: inline")
1407
754
 
1408
- bodyParts.push(new Blob([orderedLines.join("\r\n") + "\r\n"]))
755
+ const binaryFieldsForInline = Object.entries(value)
756
+ .filter(
757
+ ([k, v]) => isBytes(v) && !sortedBodyKeys.includes(`${bodyKey}/${k}`)
758
+ )
759
+ .map(([k, v]) => ({
760
+ key: k,
761
+ buffer: toBuffer(v),
762
+ }))
763
+
764
+ if (binaryFieldsForInline.length > 0) {
765
+ const parts = []
766
+ parts.push(Buffer.from(orderedLines.join("\r\n")))
767
+ for (const { key, buffer } of binaryFieldsForInline) {
768
+ parts.push(Buffer.from(`\r\n${key}: `))
769
+ parts.push(buffer)
770
+ }
771
+ parts.push(Buffer.from("\r\n"))
772
+ const fullBody = Buffer.concat(parts)
773
+ return new Blob([fullBody])
774
+ } else {
775
+ const isLastBodyPart =
776
+ sortedBodyKeys.indexOf(bodyKey) === sortedBodyKeys.length - 1
777
+ const hasOnlyTypes = allTypes.length > 0 && fieldLines.length === 0
778
+ if (isLastBodyPart && hasOnlyTypes) {
779
+ return new Blob([orderedLines.join("\r\n")])
780
+ } else if (fieldLines.length === 0) {
781
+ return new Blob([orderedLines.join("\r\n")])
1409
782
  } else {
1410
- // Put ao-types first, then content-disposition, then field lines
1411
- const orderedLines = []
1412
-
1413
- if (partTypes.length > 0) {
1414
- orderedLines.push(
1415
- `ao-types: ${partTypes
1416
- .sort((a, b) => {
1417
- const aNum = parseInt(a.split("=")[0])
1418
- const bNum = parseInt(b.split("=")[0])
1419
- return aNum - bNum
1420
- })
1421
- .join(", ")}`
1422
- )
1423
- }
783
+ return new Blob([orderedLines.join("\r\n") + "\r\n"])
784
+ }
785
+ }
786
+ } else {
787
+ const orderedLines = []
788
+ if (allTypes.length > 0) {
789
+ orderedLines.push(`ao-types: ${allTypes.sort().join(", ")}`)
790
+ }
791
+ orderedLines.push(`content-disposition: form-data;name="${bodyKey}"`)
1424
792
 
1425
- orderedLines.push(`content-disposition: form-data;name="${bodyKey}"`)
793
+ const hasBinaryFields = binaryFields && binaryFields.length > 0
794
+ if (hasBinaryFields || fieldLines.length === 0) {
795
+ orderedLines.push("")
796
+ }
1426
797
 
1427
- // Add field lines directly without blank line
1428
- for (const line of fieldLines) {
1429
- orderedLines.push(line)
1430
- }
798
+ for (const line of fieldLines) {
799
+ orderedLines.push(line)
800
+ }
1431
801
 
1432
- // Add trailing blank line if we have field lines
1433
- if (fieldLines.length > 0) {
1434
- bodyParts.push(new Blob([orderedLines.join("\r\n") + "\r\n"]))
1435
- } else {
1436
- bodyParts.push(new Blob([orderedLines.join("\r\n")]))
802
+ if (binaryFields && binaryFields.length > 0) {
803
+ const parts = []
804
+ const headerText = orderedLines.join("\r\n")
805
+ parts.push(Buffer.from(headerText))
806
+ for (let i = 0; i < binaryFields.length; i++) {
807
+ const { key, buffer } = binaryFields[i]
808
+ if (i > 0) {
809
+ parts.push(Buffer.from("\r\n"))
1437
810
  }
811
+ parts.push(Buffer.from(`${key}: `))
812
+ parts.push(buffer)
1438
813
  }
1439
- continue
1440
- }
1441
-
1442
- // This should not be reached for binary values as they're handled above
1443
- const isInline = bodyKey === "body" && headers["inline-body-key"] === "body"
1444
- if (isInline) {
1445
- lines.push(`content-disposition: inline`)
814
+ parts.push(Buffer.from("\r\n"))
815
+ const fullBody = Buffer.concat(parts)
816
+ return new Blob([fullBody])
1446
817
  } else {
1447
- lines.push(`content-disposition: form-data;name="${bodyKey}"`)
818
+ if (fieldLines.length > 0) {
819
+ return new Blob([orderedLines.join("\r\n") + "\r\n"])
820
+ } else {
821
+ return new Blob([orderedLines.join("\r\n")])
822
+ }
1448
823
  }
824
+ }
825
+ }
1449
826
 
1450
- if (typeof value === "string") {
1451
- lines.push("")
1452
- lines.push(value)
1453
- bodyParts.push(new Blob([lines.join("\r\n")]))
1454
- } else if (
1455
- typeof value === "boolean" ||
1456
- typeof value === "number" ||
1457
- value === null ||
1458
- value === undefined ||
1459
- typeof value === "symbol"
1460
- ) {
1461
- let content
1462
- if (typeof value === "boolean") {
1463
- content = `"${value}"`
1464
- } else if (typeof value === "number") {
1465
- content = String(value)
1466
- } else if (value === null) {
1467
- content = '"null"'
1468
- } else if (value === undefined) {
1469
- content = '"undefined"'
1470
- } else if (typeof value === "symbol") {
1471
- content = `"${value.description || "Symbol.for()"}"`
827
+ // Step 13.2.4: Handle object values
828
+ function handleObjectValue(
829
+ obj,
830
+ bodyKey,
831
+ value,
832
+ headers,
833
+ sortedBodyKeys,
834
+ pathParts,
835
+ hasSpecialDataBody
836
+ ) {
837
+ if (Object.keys(value).length === 0) {
838
+ // Skip empty objects in certain contexts
839
+ const parentPath = pathParts.slice(0, -1).join("/")
840
+ const parentValue = parentPath ? getValueByPath(obj, parentPath) : obj
841
+
842
+ if (Array.isArray(parentValue)) {
843
+ const parentArrayInfo = analyzeArray(parentValue)
844
+ if (
845
+ parentArrayInfo.hasObjects &&
846
+ (parentArrayInfo.hasEmptyStrings || parentArrayInfo.hasEmptyObjects)
847
+ ) {
848
+ return null
1472
849
  }
850
+ }
851
+ return null
852
+ }
853
+
854
+ // Skip special data/body case
855
+ if (
856
+ hasSpecialDataBody &&
857
+ bodyKey === "data" &&
858
+ Object.keys(value).length === 1 &&
859
+ value.body &&
860
+ isBytes(value.body)
861
+ ) {
862
+ return null
863
+ }
864
+
865
+ const { allTypes, fieldLines, binaryFields } = processObjectFields(
866
+ value,
867
+ bodyKey,
868
+ sortedBodyKeys
869
+ )
870
+
871
+ // Check if object should be skipped
872
+ const hasOnlyEmptyCollections = Object.entries(value).every(([k, v]) =>
873
+ isEmpty(v)
874
+ )
875
+ const hasArraysWithOnlyEmptyElements = Object.entries(value).some(
876
+ ([k, v]) =>
877
+ Array.isArray(v) && v.length > 0 && v.every(item => isEmpty(item))
878
+ )
1473
879
 
1474
- lines.push("")
1475
- lines.push(content)
1476
- bodyParts.push(new Blob([lines.join("\r\n")]))
880
+ const shouldSkipObject = Object.entries(value).every(([k, v]) => {
881
+ const childPath = `${bodyKey}/${k}`
882
+ if (sortedBodyKeys.includes(childPath)) return true
883
+ if (Array.isArray(v) && v.some(item => isPojo(item))) {
884
+ const hasOnlyEmpty = v.every(item => isEmpty(item))
885
+ return hasOnlyEmpty || sortedBodyKeys.includes(childPath)
1477
886
  }
887
+ return false
888
+ })
889
+
890
+ if (
891
+ shouldSkipObject &&
892
+ !hasOnlyEmptyCollections &&
893
+ !hasArraysWithOnlyEmptyElements
894
+ ) {
895
+ return null
896
+ }
897
+
898
+ return createObjectBodyPart(
899
+ bodyKey,
900
+ value,
901
+ allTypes,
902
+ fieldLines,
903
+ binaryFields,
904
+ headers,
905
+ sortedBodyKeys
906
+ )
907
+ }
908
+
909
+ // Step 13.2.5: Handle primitive values
910
+ function handlePrimitiveValue(bodyKey, value, headers) {
911
+ const isInline = bodyKey === "body" && headers["inline-body-key"] === "body"
912
+ const lines = []
913
+
914
+ if (isInline) {
915
+ lines.push(`content-disposition: inline`)
916
+ } else {
917
+ lines.push(`content-disposition: form-data;name="${bodyKey}"`)
1478
918
  }
1479
919
 
1480
- // Add the special data/body part if needed
920
+ if (typeof value === "string") {
921
+ lines.push("")
922
+ lines.push(value)
923
+ return new Blob([lines.join("\r\n")])
924
+ } else {
925
+ const content = encodePrimitiveContent(value)
926
+ lines.push("")
927
+ lines.push(content)
928
+ return new Blob([lines.join("\r\n")])
929
+ }
930
+ }
931
+
932
+ // Step 13.2.6: Handle binary values
933
+ function handleBinaryValue(bodyKey, value, headers) {
934
+ const isInline = bodyKey === "body" && headers["inline-body-key"] === "body"
935
+ const lines = []
936
+
937
+ if (isInline) {
938
+ lines.push(`content-disposition: inline`)
939
+ } else {
940
+ lines.push(`content-disposition: form-data;name="${bodyKey}"`)
941
+ }
942
+
943
+ const buffer = toBuffer(value)
944
+ const headerText = lines.join("\r\n") + "\r\n\r\n"
945
+ return new Blob([headerText, buffer])
946
+ }
947
+
948
+ // Step 13.3: Handle special data/body case
949
+ function handleSpecialDataBodyCase(obj, hasSpecialDataBody) {
1481
950
  if (
1482
951
  hasSpecialDataBody &&
1483
952
  obj.data &&
@@ -1490,14 +959,108 @@ async function encode(obj = {}) {
1490
959
  "",
1491
960
  "",
1492
961
  ].join("\r\n")
1493
- bodyParts.push(new Blob([specialPart, buffer]))
962
+ return new Blob([specialPart, buffer])
1494
963
  }
964
+ return null
965
+ }
966
+
967
+ // Step 13: Build body parts for each body key
968
+ function buildBodyParts(obj, sortedBodyKeys, headers, hasSpecialDataBody) {
969
+ // Step 13.1: Initialize body parts collection
970
+ const bodyParts = []
971
+
972
+ // Step 13.2: Process each body key
973
+ for (const bodyKey of sortedBodyKeys) {
974
+ // Step 13.2.1: Get value for current body key
975
+ const value = getValueByPath(obj, bodyKey)
976
+ const pathParts = bodyKey.split("/")
977
+
978
+ // Step 13.2.2: Handle empty string in nested path
979
+ const emptyStringPart = handleEmptyStringInNestedPath(
980
+ bodyKey,
981
+ value,
982
+ pathParts
983
+ )
984
+ if (emptyStringPart) {
985
+ bodyParts.push(emptyStringPart)
986
+ continue
987
+ }
988
+
989
+ // Step 13.2.3: Handle array values
990
+ if (Array.isArray(value)) {
991
+ const arrayPart = handleArrayValue(
992
+ bodyKey,
993
+ value,
994
+ headers,
995
+ sortedBodyKeys,
996
+ pathParts
997
+ )
998
+ if (arrayPart) {
999
+ bodyParts.push(arrayPart)
1000
+ }
1001
+ continue
1002
+ }
1003
+
1004
+ // Step 13.2.4: Handle object values
1005
+ if (isPojo(value)) {
1006
+ const objectPart = handleObjectValue(
1007
+ obj,
1008
+ bodyKey,
1009
+ value,
1010
+ headers,
1011
+ sortedBodyKeys,
1012
+ pathParts,
1013
+ hasSpecialDataBody
1014
+ )
1015
+ if (objectPart) {
1016
+ bodyParts.push(objectPart)
1017
+ }
1018
+ continue
1019
+ }
1495
1020
 
1021
+ // Step 13.2.5: Handle primitive values
1022
+ if (
1023
+ typeof value === "string" ||
1024
+ typeof value === "boolean" ||
1025
+ typeof value === "number" ||
1026
+ value === null ||
1027
+ value === undefined ||
1028
+ typeof value === "symbol"
1029
+ ) {
1030
+ const primitivePart = handlePrimitiveValue(bodyKey, value, headers)
1031
+ bodyParts.push(primitivePart)
1032
+ continue
1033
+ }
1034
+
1035
+ // Step 13.2.6: Handle binary values
1036
+ if (isBytes(value)) {
1037
+ const binaryPart = handleBinaryValue(bodyKey, value, headers)
1038
+ bodyParts.push(binaryPart)
1039
+ continue
1040
+ }
1041
+ }
1042
+
1043
+ // Step 13.3: Handle special data/body case
1044
+ const specialPart = handleSpecialDataBodyCase(obj, hasSpecialDataBody)
1045
+ if (specialPart) {
1046
+ bodyParts.push(specialPart)
1047
+ }
1048
+
1049
+ // Step 13.4: Return body parts
1050
+ return bodyParts
1051
+ }
1052
+
1053
+ // Step 14: Generate multipart boundary
1054
+ async function generateBoundary(bodyParts) {
1496
1055
  const partsContent = await Promise.all(bodyParts.map(part => part.text()))
1497
1056
  const allContent = partsContent.join("")
1498
1057
  const boundaryHash = await sha256(new TextEncoder().encode(allContent))
1499
1058
  const boundary = base64url.encode(Buffer.from(boundaryHash))
1059
+ return boundary
1060
+ }
1500
1061
 
1062
+ // Step 15: Assemble final multipart body
1063
+ function assembleMultipartBody(bodyParts, boundary) {
1501
1064
  const finalParts = []
1502
1065
  for (let i = 0; i < bodyParts.length; i++) {
1503
1066
  if (i === 0) {
@@ -1509,20 +1072,130 @@ async function encode(obj = {}) {
1509
1072
  }
1510
1073
  finalParts.push(new Blob([`\r\n--${boundary}--`]))
1511
1074
 
1512
- headers["content-type"] = `multipart/form-data; boundary="${boundary}"`
1513
- const body = new Blob(finalParts)
1075
+ return new Blob(finalParts)
1076
+ }
1514
1077
 
1078
+ // Step 16: Calculate content digest
1079
+ async function calculateContentDigest(body) {
1515
1080
  const finalContent = await body.arrayBuffer()
1516
1081
 
1517
1082
  if (finalContent.byteLength > 0) {
1518
1083
  const contentDigest = await sha256(finalContent)
1519
1084
  const base64 = base64url.toBase64(base64url.encode(contentDigest))
1520
- headers["content-digest"] = `sha-256=:${base64}:`
1085
+ return { digest: base64, byteLength: finalContent.byteLength }
1521
1086
  }
1522
1087
 
1523
- headers["content-length"] = String(finalContent.byteLength)
1088
+ return { digest: null, byteLength: finalContent.byteLength }
1089
+ }
1090
+
1091
+ // Step 17: Set final headers (content-type, content-length)
1092
+ function setFinalHeaders(headers, boundary, contentDigest, byteLength) {
1093
+ headers["content-type"] = `multipart/form-data; boundary="${boundary}"`
1094
+
1095
+ if (contentDigest) {
1096
+ headers["content-digest"] = `sha-256=:${contentDigest}:`
1097
+ }
1098
+
1099
+ headers["content-length"] = String(byteLength)
1100
+ }
1101
+
1102
+ async function encode(obj = {}) {
1103
+ // Step 1: Process and normalize input values
1104
+ const processedObj = processInputValues(obj)
1105
+
1106
+ // Step 2: Handle empty object case
1107
+ const emptyResult = handleEmptyObject(processedObj)
1108
+ if (emptyResult) return emptyResult
1109
+
1110
+ // Step 3: Handle single field with empty binary
1111
+ const emptyBinaryResult = handleSingleEmptyBinaryField(processedObj)
1112
+ if (emptyBinaryResult) return emptyBinaryResult
1113
+
1114
+ // Step 4: Handle single field with binary data
1115
+ const singleBinaryResult = await handleSingleBinaryField(processedObj)
1116
+ if (singleBinaryResult) return singleBinaryResult
1117
+
1118
+ // Step 5: Handle single field with primitive value
1119
+ const primitiveResult = await handleSinglePrimitiveField(processedObj)
1120
+ if (primitiveResult) return primitiveResult
1121
+
1122
+ // Step 6a: Handle single field with non-empty binary
1123
+ const nonEmptyBinaryResult =
1124
+ await handleSingleNonEmptyBinaryField(processedObj)
1125
+ if (nonEmptyBinaryResult) return nonEmptyBinaryResult
1126
+
1127
+ // Step 6: Handle single field with non-ASCII string
1128
+ const nonAsciiResult = await handleSingleNonAsciiStringField(processedObj)
1129
+ if (nonAsciiResult) return nonAsciiResult
1130
+
1131
+ // Step 7: Collect all keys that need to go in body
1132
+ const bodyKeys = collectBodyKeysStep(processedObj)
1133
+
1134
+ const objKeys = Object.keys(obj)
1135
+ const headers = {}
1136
+ const headerTypes = []
1137
+
1138
+ // Step 8: Process fields that can go in headers
1139
+ processHeaderFields(obj, bodyKeys, headers, headerTypes)
1140
+
1141
+ // Step 9: Handle case where all body keys are empty binaries
1142
+ const emptyBinaryBodyResult = handleAllEmptyBinaryBodyKeys(
1143
+ obj,
1144
+ bodyKeys,
1145
+ headers,
1146
+ headerTypes
1147
+ )
1148
+ if (emptyBinaryBodyResult) return emptyBinaryBodyResult
1149
+
1150
+ // Step 10: Handle single body key optimization
1151
+ const singleBodyKeyResult = await handleSingleBodyKeyOptimization(
1152
+ obj,
1153
+ bodyKeys,
1154
+ headers,
1155
+ headerTypes
1156
+ )
1157
+ if (singleBodyKeyResult) return singleBodyKeyResult
1158
+
1159
+ // Step 11: Sort body keys
1160
+ const sortedBodyKeys = sortBodyKeys(bodyKeys)
1161
+
1162
+ // Step 12: Check for special data/body case
1163
+ const hasSpecialDataBody = checkSpecialDataBodyCase(obj, sortedBodyKeys)
1164
+
1165
+ headers["body-keys"] = sortedBodyKeys.map(k => `"${k}"`).join(", ")
1166
+
1167
+ if (!hasSpecialDataBody) {
1168
+ if (sortedBodyKeys.includes("body") && sortedBodyKeys.length === 1) {
1169
+ headers["inline-body-key"] = "body"
1170
+ }
1171
+ }
1172
+
1173
+ if (headerTypes.length > 0) {
1174
+ headers["ao-types"] = headerTypes.sort().join(", ")
1175
+ }
1176
+
1177
+ // Step 13: Build body parts for each body key
1178
+ const bodyParts = buildBodyParts(
1179
+ obj,
1180
+ sortedBodyKeys,
1181
+ headers,
1182
+ hasSpecialDataBody
1183
+ )
1184
+
1185
+ // Step 14: Generate multipart boundary
1186
+ const boundary = await generateBoundary(bodyParts)
1187
+
1188
+ // Step 15: Assemble final multipart body
1189
+ const body = assembleMultipartBody(bodyParts, boundary)
1190
+
1191
+ // Step 16: Calculate content digest
1192
+ const { digest: contentDigest, byteLength } =
1193
+ await calculateContentDigest(body)
1194
+
1195
+ // Step 17: Set final headers (content-type, content-length)
1196
+ setFinalHeaders(headers, boundary, contentDigest, byteLength)
1524
1197
 
1525
- console.log("=== ENCODE END ===\n")
1198
+ // Step 18: Return result
1526
1199
  return { headers, body }
1527
1200
  }
1528
1201