wao 0.27.0 → 0.27.2

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
@@ -5,7 +5,11 @@ function isBytes(value) {
5
5
  return (
6
6
  value instanceof ArrayBuffer ||
7
7
  ArrayBuffer.isView(value) ||
8
- Buffer.isBuffer(value)
8
+ Buffer.isBuffer(value) ||
9
+ (value &&
10
+ typeof value === "object" &&
11
+ value.type === "Buffer" &&
12
+ Array.isArray(value.data))
9
13
  )
10
14
  }
11
15
 
@@ -19,99 +23,8 @@ function isPojo(value) {
19
23
  )
20
24
  }
21
25
 
22
- function hbEncodeValue(value) {
23
- if (isBytes(value)) {
24
- const length = value.byteLength || value.length || 0
25
- if (length === 0) return ["empty-binary", ""]
26
- return [undefined, value]
27
- }
28
-
29
- if (typeof value === "string") {
30
- if (value.length === 0) return ["empty-string", ""]
31
- return [undefined, value]
32
- }
33
-
34
- if (Array.isArray(value)) {
35
- if (value.length === 0) return ["empty-list", "[]"]
36
-
37
- const hasObjects = value.some(item => isPojo(item))
38
- if (hasObjects) {
39
- return ["list_with_objects", value]
40
- }
41
-
42
- const items = value.map(item => {
43
- if (typeof item === "string") {
44
- return `"${item}"`
45
- } else if (typeof item === "number") {
46
- if (Number.isInteger(item)) {
47
- return `"(ao-type-integer) ${item}"`
48
- } else {
49
- return `"(ao-type-float) ${item.toExponential(20).replace("e+0", "e+00")}"`
50
- }
51
- } else if (typeof item === "boolean") {
52
- return `"(ao-type-atom) \\"${item ? "true" : "false"}\\""`
53
- } else if (typeof item === "symbol") {
54
- const desc = item.description || "symbol"
55
- return `"(ao-type-atom) \\"${desc}\\""`
56
- } else if (item === null) {
57
- return `"(ao-type-atom) \\"null\\""`
58
- } else if (item === undefined) {
59
- return `"(ao-type-atom) \\"undefined\\""`
60
- } else if (isBytes(item)) {
61
- const length = item.byteLength || item.length || 0
62
- if (length === 0) {
63
- return `""`
64
- } else {
65
- const base64 = base64url.encode(Buffer.from(item))
66
- return `"(ao-type-binary) ${base64}"`
67
- }
68
- } else if (Array.isArray(item)) {
69
- const [, encoded] = hbEncodeValue(item)
70
- const escapedEncoded = encoded.replace(/"/g, '\\"')
71
- return `"(ao-type-list) ${escapedEncoded}"`
72
- } else {
73
- return `"${String(item)}"`
74
- }
75
- })
76
-
77
- return ["list", items.join(", ")]
78
- }
79
-
80
- if (typeof value === "number") {
81
- if (!Number.isInteger(value)) return ["float", `${value}`]
82
- return ["integer", String(value)]
83
- }
84
-
85
- if (typeof value === "boolean") {
86
- return ["atom", value ? "true" : "false"]
87
- }
88
-
89
- if (typeof value === "symbol") {
90
- const desc = value.description || "symbol"
91
- return ["atom", desc]
92
- }
93
-
94
- if (value === null) return ["atom", "null"]
95
- if (value === undefined) return ["atom", "undefined"]
96
-
97
- if (isPojo(value)) {
98
- throw new Error("Objects must be lifted")
99
- }
100
-
101
- throw new Error(`Cannot encode value: ${String(value)}`)
102
- }
103
-
104
26
  const MAX_HEADER_LENGTH = 4096
105
27
 
106
- function encode_body_keys(bodyKeys) {
107
- if (!bodyKeys || bodyKeys.length === 0) return ""
108
- const items = bodyKeys.map(key => {
109
- const escaped = key.replace(/"/g, '\\"')
110
- return `"${escaped}"`
111
- })
112
- return items.join(", ")
113
- }
114
-
115
28
  async function hasNewline(value) {
116
29
  if (typeof value === "string") return value.includes("\n")
117
30
  if (value instanceof Blob) {
@@ -141,157 +54,706 @@ async function sha256(data) {
141
54
  )
142
55
  }
143
56
 
144
- function collectParts(obj, path = "", parts = {}, types = {}) {
145
- for (const [key, value] of Object.entries(obj)) {
146
- const currentPath = path ? `${path}/${key}` : key
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
+ }
147
63
 
148
- if (value === null || value === undefined) {
149
- if (!parts[path]) parts[path] = {}
150
- parts[path][key] = value === null ? '"null"' : '"undefined"'
151
- types[currentPath] = "atom"
152
- } else if (isBytes(value)) {
153
- if (!parts[path]) parts[path] = {}
154
- parts[path][key] = value
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
+ }
155
369
 
156
- const length = value.byteLength || value.length || 0
157
- if (length === 0) {
158
- types[currentPath] = "empty-binary"
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
+ }
159
388
  }
160
- } else if (typeof value === "string") {
161
- if (value.length === 0) {
162
- types[currentPath] = "empty-binary"
163
- if (!parts[path]) parts[path] = {}
164
- parts[path][key] = ""
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`)
165
445
  } else {
166
- if (!parts[path]) parts[path] = {}
167
- parts[path][key] = value
446
+ console.log(`[main loop] Adding special data/body key: "${key}"`)
447
+ keys.push(key)
168
448
  }
169
449
  } else if (Array.isArray(value)) {
170
450
  if (value.length === 0) {
171
- types[currentPath] = "empty-list"
172
- if (!parts[path]) parts[path] = {}
173
- parts[path][key] = "[]"
174
- } else {
175
- const hasObjects = value.some(item => isPojo(item))
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))
176
458
 
177
- if (hasObjects) {
178
- // Arrays with objects: create separate parts for each indexed item
179
- types[currentPath] = "list"
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
+ )
180
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
181
492
  value.forEach((item, index) => {
182
- const indexKey = String(index + 1)
183
- const indexPath = `${currentPath}/${indexKey}`
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
+ }
184
553
 
554
+ value.forEach((item, index) => {
185
555
  if (isPojo(item)) {
186
- // Each object in the array becomes a separate part
187
- if (!parts[indexPath]) parts[indexPath] = {}
188
-
189
- for (const [objKey, objValue] of Object.entries(item)) {
190
- parts[indexPath][objKey] = objValue
191
-
192
- // Set types for object fields
193
- const fieldPath = `${indexPath}/${objKey}`
194
- if (typeof objValue === "number") {
195
- types[fieldPath] = Number.isInteger(objValue)
196
- ? "integer"
197
- : "float"
198
- } else if (typeof objValue === "boolean") {
199
- types[fieldPath] = "atom"
200
- } else if (typeof objValue === "symbol") {
201
- types[fieldPath] = "atom"
202
- // Store the symbol's description for later use
203
- parts[indexPath][objKey] = objValue
204
- } else if (
205
- typeof objValue === "string" &&
206
- objValue.length === 0
207
- ) {
208
- types[fieldPath] = "empty-binary"
209
- } else if (Array.isArray(objValue) && objValue.length === 0) {
210
- types[fieldPath] = "empty-list"
211
- } else if (
212
- isPojo(objValue) &&
213
- Object.keys(objValue).length === 0
214
- ) {
215
- types[fieldPath] = "empty-message"
216
- }
556
+ // Skip empty objects if array has only empty elements
557
+ if (skipEmptyObjects && Object.keys(item).length === 0) {
558
+ bodyPartCounter++
559
+ return
217
560
  }
218
- } else {
219
- // Non-object items in the array
220
- if (!parts[currentPath]) parts[currentPath] = {}
221
- parts[currentPath][indexKey] = item
222
-
223
- if (typeof item === "number") {
224
- types[`${currentPath}/${indexKey}`] = Number.isInteger(item)
225
- ? "integer"
226
- : "float"
227
- } else if (typeof item === "boolean") {
228
- types[`${currentPath}/${indexKey}`] = "atom"
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
+ }
229
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)
230
589
  }
590
+ bodyPartCounter++
231
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
+ }
232
611
  } else {
233
- // Simple arrays without objects
234
- const [type, encoded] = hbEncodeValue(value)
235
- if (!parts[path]) parts[path] = {}
236
- parts[path][key] = encoded
237
- types[currentPath] = type || "list"
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
+ }
238
631
  }
239
632
  }
240
633
  } else if (isPojo(value)) {
241
- if (Object.keys(value).length === 0) {
242
- types[currentPath] = "empty-message"
243
- if (!parts[path]) parts[path] = {}
244
- parts[path][key] = "{}"
245
- } else {
246
- const hasOnlyEmptyChildren = Object.entries(value).every(([k, v]) => {
247
- return (
248
- (Array.isArray(v) && v.length === 0) ||
249
- (isPojo(v) && Object.keys(v).length === 0)
250
- )
251
- })
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
+ }
252
653
 
253
- if (hasOnlyEmptyChildren) {
254
- if (!parts[currentPath]) parts[currentPath] = {}
255
- }
654
+ const result = [...new Set(keys)].filter(k => {
655
+ if (k === "") return false
256
656
 
257
- collectParts(value, currentPath, parts, types)
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
+ }
258
671
  }
259
- } else if (typeof value === "symbol") {
260
- if (!parts[path]) parts[path] = {}
261
- parts[path][key] = value.description || "symbol"
262
- types[currentPath] = "atom"
263
- } else if (typeof value === "boolean") {
264
- if (!parts[path]) parts[path] = {}
265
- parts[path][key] = value
266
- types[currentPath] = "atom"
267
- } else {
268
- if (!parts[path]) parts[path] = {}
269
- parts[path][key] = value
270
- if (typeof value === "number") {
271
- types[currentPath] = Number.isInteger(value) ? "integer" : "float"
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
+ }
272
687
  }
273
688
  }
274
- }
275
689
 
276
- return { parts, types }
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
277
697
  }
278
698
 
279
699
  async function encode(obj = {}) {
280
- console.log("[encode] START with obj:", JSON.stringify(obj))
700
+ console.log("\n=== ENCODE START ===")
701
+ console.log("Encoding object:", JSON.stringify(obj))
281
702
 
282
- if (Object.keys(obj).length === 0) return { headers: {}, body: undefined }
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
+
723
+ if (Object.keys(obj).length === 0) {
724
+ return { headers: {}, body: undefined }
725
+ }
283
726
 
284
- // Check if we have a simple binary field
285
727
  const objKeys = Object.keys(obj)
286
- if (objKeys.length === 1 && isBytes(obj[objKeys[0]])) {
287
- // Single binary field - return it directly
728
+
729
+ if (objKeys.length === 1) {
288
730
  const fieldName = objKeys[0]
289
- const binaryData = obj[fieldName]
731
+ const fieldValue = obj[fieldName]
732
+
733
+ if (
734
+ isBytes(fieldValue) &&
735
+ (fieldValue.length === 0 || fieldValue.byteLength === 0)
736
+ ) {
737
+ const headers = {}
738
+ headers["ao-types"] = `${fieldName.toLowerCase()}="empty-binary"`
739
+ return { headers, body: undefined }
740
+ }
741
+ }
742
+
743
+ if (
744
+ obj.body &&
745
+ isBytes(obj.body) &&
746
+ (obj.body.length === 0 || obj.body.byteLength === 0) &&
747
+ objKeys.length > 1
748
+ ) {
749
+ }
290
750
 
751
+ const hasBodyBinary = obj.body && isBytes(obj.body)
752
+ const otherFields = Object.keys(obj).filter(k => k !== "body")
753
+
754
+ if (hasBodyBinary && otherFields.length === 0) {
291
755
  const headers = {}
292
- const bodyBuffer = Buffer.isBuffer(binaryData)
293
- ? binaryData
294
- : Buffer.from(binaryData)
756
+ const bodyBuffer = toBuffer(obj.body)
295
757
  const bodyArrayBuffer = bodyBuffer.buffer.slice(
296
758
  bodyBuffer.byteOffset,
297
759
  bodyBuffer.byteOffset + bodyBuffer.byteLength
@@ -301,187 +763,309 @@ async function encode(obj = {}) {
301
763
  const base64 = base64url.toBase64(base64url.encode(contentDigest))
302
764
  headers["content-digest"] = `sha-256=:${base64}:`
303
765
 
304
- console.log(
305
- "[encode] FINAL (simple binary field) - headers:",
306
- headers,
307
- "body:",
308
- binaryData
309
- )
310
- return { headers, body: binaryData }
766
+ return { headers, body: obj.body }
311
767
  }
312
768
 
313
- if ("body" in obj && isBytes(obj.body)) {
314
- const headers = {}
315
- const types = []
316
- let needsMultipart = false
769
+ if (objKeys.length === 1) {
770
+ const fieldName = objKeys[0]
771
+ const fieldValue = obj[fieldName]
772
+
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
+ }
317
788
 
318
- for (const [key, value] of Object.entries(obj)) {
319
- if (key === "body") continue
789
+ return { headers, body: fieldValue }
790
+ } else if (
791
+ (fieldName === "data" || fieldName === "body") &&
792
+ (typeof fieldValue === "string" ||
793
+ typeof fieldValue === "boolean" ||
794
+ typeof fieldValue === "number" ||
795
+ fieldValue === null ||
796
+ fieldValue === undefined ||
797
+ typeof fieldValue === "symbol")
798
+ ) {
799
+ 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
+ }
815
+
816
+ const encoder = new TextEncoder()
817
+ const encoded = encoder.encode(bodyContent)
818
+ const contentDigest = await sha256(encoded.buffer)
819
+ const base64 = base64url.toBase64(base64url.encode(contentDigest))
820
+ headers["content-digest"] = `sha-256=:${base64}:`
821
+
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"}"`
832
+ }
833
+
834
+ if (fieldName !== "body") {
835
+ headers["inline-body-key"] = fieldName
836
+ }
837
+
838
+ return { headers, body: bodyContent }
839
+ } else if (typeof fieldValue === "string" && hasNonAscii(fieldValue)) {
840
+ const headers = {}
841
+ const encoder = new TextEncoder()
842
+ const encoded = encoder.encode(fieldValue)
843
+ const contentDigest = await sha256(encoded.buffer)
844
+ const base64 = base64url.toBase64(base64url.encode(contentDigest))
845
+ headers["content-digest"] = `sha-256=:${base64}:`
846
+
847
+ if (fieldName !== "body") {
848
+ headers["inline-body-key"] = fieldName
849
+ }
850
+
851
+ return { headers, body: fieldValue }
852
+ }
853
+ }
320
854
 
855
+ const bodyKeys = collectBodyKeys(obj)
856
+ const headers = {}
857
+ const headerTypes = []
858
+
859
+ for (const [key, value] of Object.entries(obj)) {
860
+ const needsBody =
861
+ bodyKeys.includes(key) || bodyKeys.some(k => k.startsWith(`${key}/`))
862
+
863
+ if (!needsBody) {
321
864
  if (value === null) {
322
- headers[key] = "null"
323
- types.push(`${key}="atom"`)
865
+ headers[key] = '"null"'
866
+ headerTypes.push(`${key.toLowerCase()}="atom"`)
324
867
  } else if (value === undefined) {
325
- headers[key] = "undefined"
326
- types.push(`${key}="atom"`)
327
- } else if (typeof value === "string" && value.length === 0) {
328
- types.push(`${key}="empty-binary"`)
329
- } else if (Array.isArray(value)) {
330
- if (value.length === 0) {
331
- types.push(`${key}="empty-list"`)
332
- } else if (value.some(item => isPojo(item))) {
333
- // Arrays with objects need multipart
334
- types.push(`${key}="list"`)
335
- needsMultipart = true
336
- break
337
- } else {
338
- const [type, encoded] = hbEncodeValue(value)
339
- headers[key] = encoded
340
- types.push(`${key}="${type}"`)
341
- }
342
- } else if (isBytes(value)) {
343
- if (value.length === 0 || value.byteLength === 0) {
344
- types.push(`${key}="empty-binary"`)
345
- } else {
346
- needsMultipart = true
347
- break
348
- }
868
+ headers[key] = '"undefined"'
869
+ headerTypes.push(`${key.toLowerCase()}="atom"`)
349
870
  } else if (typeof value === "boolean") {
350
871
  headers[key] = `"${value}"`
351
- types.push(`${key}="atom"`)
872
+ headerTypes.push(`${key.toLowerCase()}="atom"`)
352
873
  } else if (typeof value === "symbol") {
353
- headers[key] = value.description || "symbol"
354
- types.push(`${key}="atom"`)
874
+ headers[key] = `"${value.description || "Symbol.for()"}"`
875
+ headerTypes.push(`${key.toLowerCase()}="atom"`)
355
876
  } else if (typeof value === "number") {
356
877
  headers[key] = String(value)
357
- types.push(`${key}="${Number.isInteger(value) ? "integer" : "float"}"`)
878
+ headerTypes.push(
879
+ `${key.toLowerCase()}="${Number.isInteger(value) ? "integer" : "float"}"`
880
+ )
358
881
  } else if (typeof value === "string") {
359
- headers[key] = value
882
+ if (value.length === 0) {
883
+ headerTypes.push(`${key.toLowerCase()}="empty-binary"`)
884
+ } else if (hasNonAscii(value)) {
885
+ continue
886
+ } else {
887
+ headers[key] = value
888
+ }
889
+ } else if (Array.isArray(value) && value.length === 0) {
890
+ headerTypes.push(`${key.toLowerCase()}="empty-list"`)
891
+ } else if (Array.isArray(value) && !value.some(item => isPojo(item))) {
892
+ const hasNonAsciiItems = value.some(
893
+ item => typeof item === "string" && hasNonAscii(item)
894
+ )
895
+ if (!hasNonAsciiItems) {
896
+ const encodedItems = value
897
+ .map(item => encodeArrayItem(item))
898
+ .join(", ")
899
+ headers[key] = encodedItems
900
+ headerTypes.push(`${key.toLowerCase()}="list"`)
901
+ }
902
+ } else if (
903
+ isBytes(value) &&
904
+ (value.length === 0 || value.byteLength === 0)
905
+ ) {
906
+ headerTypes.push(`${key.toLowerCase()}="empty-binary"`)
360
907
  } else if (isPojo(value) && Object.keys(value).length === 0) {
361
- types.push(`${key}="empty-message"`)
362
- } else if (isPojo(value)) {
363
- needsMultipart = true
364
- break
908
+ headerTypes.push(`${key.toLowerCase()}="empty-message"`)
909
+ }
910
+ } 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
+ )
365
930
  }
366
931
  }
932
+ }
367
933
 
368
- if (needsMultipart) {
369
- return encodeMultipart(obj)
934
+ for (const [key, value] of Object.entries(obj)) {
935
+ if (Array.isArray(value)) {
936
+ if (
937
+ bodyKeys.includes(key) ||
938
+ bodyKeys.some(k => k.startsWith(`${key}/`))
939
+ ) {
940
+ if (!headerTypes.some(t => t.startsWith(`${key.toLowerCase()}=`))) {
941
+ headerTypes.push(`${key.toLowerCase()}="list"`)
942
+ }
943
+ }
370
944
  }
945
+ }
371
946
 
372
- if (types.length > 0) {
373
- headers["ao-types"] = types.sort().join(", ")
947
+ if (bodyKeys.length === 0) {
948
+ console.log("No bodyKeys, returning headers only")
949
+ if (headerTypes.length > 0) {
950
+ headers["ao-types"] = headerTypes.sort().join(", ")
374
951
  }
952
+ return { headers, body: undefined }
953
+ }
375
954
 
376
- const body = obj.body
377
- const bodyBuffer = Buffer.isBuffer(body) ? body : Buffer.from(body)
378
- const bodyArrayBuffer = bodyBuffer.buffer.slice(
379
- bodyBuffer.byteOffset,
380
- bodyBuffer.byteOffset + bodyBuffer.byteLength
381
- )
382
-
383
- const contentDigest = await sha256(bodyArrayBuffer)
384
- const base64 = base64url.toBase64(base64url.encode(contentDigest))
385
- headers["content-digest"] = `sha-256=:${base64}:`
955
+ 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
+ }
969
+ return isBytes(value) && (value.length === 0 || value.byteLength === 0)
970
+ })
386
971
 
387
- console.log(
388
- "[encode] FINAL (simple body encoding) - headers:",
389
- headers,
390
- "body:",
391
- body
392
- )
393
- return { headers, body }
972
+ if (allBodyKeysAreEmptyBinaries) {
973
+ if (headerTypes.length > 0) {
974
+ headers["ao-types"] = headerTypes.sort().join(", ")
975
+ }
976
+ return { headers, body: undefined }
394
977
  }
395
978
 
396
- return encodeMultipart(obj)
397
- }
398
-
399
- async function encodeMultipart(obj) {
400
- const { parts, types } = collectParts(obj)
401
- console.log("[encode] Parts:", parts, "Types:", types)
979
+ if (bodyKeys.length === 1) {
980
+ 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
+ }
402
990
 
403
- const headers = {}
404
- const bodyParts = []
405
- const bodyKeys = []
991
+ const otherFieldsAreEmpty = Object.entries(obj).every(([key, val]) => {
992
+ 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
+ )
999
+ })
406
1000
 
407
- // Collect header types for arrays with objects
408
- const headerTypes = []
409
- for (const [key, value] of Object.entries(obj)) {
410
- if (Array.isArray(value) && value.some(item => isPojo(item))) {
411
- headerTypes.push(`${key}="list"`)
412
- }
413
- }
1001
+ if (otherFieldsAreEmpty && isBytes(value) && value.length > 0) {
1002
+ const bodyBuffer = toBuffer(value)
1003
+ const bodyArrayBuffer = bodyBuffer.buffer.slice(
1004
+ bodyBuffer.byteOffset,
1005
+ bodyBuffer.byteOffset + bodyBuffer.byteLength
1006
+ )
414
1007
 
415
- for (const [path, content] of Object.entries(parts)) {
416
- if (path === "") {
417
- for (const [key, value] of Object.entries(content)) {
418
- if (isBytes(value)) {
419
- const length = value.byteLength || value.length || 0
420
- if (length === 0) {
421
- // Empty binaries stay in headers
422
- console.log(`[encode] Empty binary field ${key} staying in headers`)
423
- continue
424
- } else {
425
- console.log(`[encode] Binary field ${key} going to body`)
426
- bodyKeys.push(key)
427
- }
428
- } else {
429
- const valueStr = String(value)
430
- const type = types[key]
1008
+ const contentDigest = await sha256(bodyArrayBuffer)
1009
+ const base64 = base64url.toBase64(base64url.encode(contentDigest))
1010
+ headers["content-digest"] = `sha-256=:${base64}:`
431
1011
 
432
- if (type === "empty-binary" && valueStr === "") {
433
- continue
434
- }
1012
+ if (singleKey !== "body") {
1013
+ headers["inline-body-key"] = singleKey
1014
+ }
435
1015
 
436
- if (
437
- !(await hasNewline(valueStr)) &&
438
- !key.includes("/") &&
439
- Buffer.from(valueStr).byteLength <= MAX_HEADER_LENGTH &&
440
- !isPojo(value) &&
441
- !(key === "data" || key === "body") // Don't put inline keys in headers
442
- ) {
443
- if (typeof value === "number") {
444
- headers[key] = String(value)
445
- } else if (typeof value === "boolean") {
446
- headers[key] = `"${value}"`
447
- } else if (value === "[]" && type === "empty_list") {
448
- headers[key] = "[]"
449
- } else if (value === '"null"' || value === '"undefined"') {
450
- headers[key] = value
451
- } else {
452
- headers[key] = value
453
- }
454
- } else {
455
- bodyKeys.push(key)
456
- }
457
- }
1016
+ if (headerTypes.length > 0) {
1017
+ headers["ao-types"] = headerTypes.sort().join(", ")
458
1018
  }
459
- } else {
460
- bodyKeys.push(path)
1019
+
1020
+ return { headers, body: value }
461
1021
  }
462
1022
  }
463
1023
 
464
- console.log(
465
- "[encode] Headers before filtering:",
466
- headers,
467
- "BodyKeys:",
468
- bodyKeys
469
- )
470
-
471
- bodyKeys.sort()
472
-
473
- // Add types for values in headers
474
- for (const [key, value] of Object.entries(headers)) {
475
- const type = types[key]
476
- if (type) {
477
- headerTypes.push(`${key}="${type}"`)
1024
+ // Sort body keys: main array comes first, then element parts by index
1025
+ const sortedBodyKeys = bodyKeys.sort((a, b) => {
1026
+ const aIsArrayElement = /\/\d+$/.test(a)
1027
+ const bIsArrayElement = /\/\d+$/.test(b)
1028
+ const aBase = a.split("/")[0]
1029
+ const bBase = b.split("/")[0]
1030
+
1031
+ // If both are for the same array
1032
+ if (aBase === bBase) {
1033
+ // Main array comes before element parts
1034
+ if (!aIsArrayElement && bIsArrayElement) {
1035
+ return -1 // main array comes first
1036
+ }
1037
+ if (aIsArrayElement && !bIsArrayElement) {
1038
+ return 1 // element parts come after
1039
+ }
1040
+ // Both are elements - sort by index
1041
+ if (aIsArrayElement && bIsArrayElement) {
1042
+ const aIndex = parseInt(a.split("/")[1])
1043
+ const bIndex = parseInt(b.split("/")[1])
1044
+ return aIndex - bIndex
1045
+ }
1046
+ return a.localeCompare(b)
478
1047
  }
479
- }
480
1048
 
481
- // Add types for empty values not in headers
482
- for (const [key, type] of Object.entries(types)) {
483
- if (!key.includes("/") && type.startsWith("empty") && !headers[key]) {
484
- headerTypes.push(`${key}="${type.replace("empty_", "empty-")}"`)
1049
+ // Different arrays, sort by base name
1050
+ return a.localeCompare(b)
1051
+ })
1052
+
1053
+ // Check if we have the special case where data contains body with bytes and body contains data with bytes
1054
+ const hasSpecialDataBody =
1055
+ sortedBodyKeys.includes("data") &&
1056
+ sortedBodyKeys.includes("body") &&
1057
+ obj.data &&
1058
+ obj.data.body &&
1059
+ isBytes(obj.data.body) &&
1060
+ obj.body &&
1061
+ obj.body.data &&
1062
+ isBytes(obj.body.data)
1063
+
1064
+ headers["body-keys"] = sortedBodyKeys.map(k => `"${k}"`).join(", ")
1065
+
1066
+ if (!hasSpecialDataBody) {
1067
+ if (sortedBodyKeys.includes("body") && sortedBodyKeys.length === 1) {
1068
+ headers["inline-body-key"] = "body"
485
1069
  }
486
1070
  }
487
1071
 
@@ -489,211 +1073,457 @@ async function encodeMultipart(obj) {
489
1073
  headers["ao-types"] = headerTypes.sort().join(", ")
490
1074
  }
491
1075
 
492
- if (bodyKeys.length > 0) {
493
- headers["body-keys"] = encode_body_keys(bodyKeys)
1076
+ const bodyParts = []
1077
+
1078
+ for (const bodyKey of sortedBodyKeys) {
1079
+ console.log(`\n[Body part] Processing bodyKey: ${bodyKey}`)
1080
+ const lines = []
1081
+
1082
+ const pathParts = bodyKey.split("/")
1083
+ let value = obj
1084
+ let parent = null
494
1085
 
495
- if (bodyKeys.includes("data") || bodyKeys.includes("body")) {
496
- const inlineKey = bodyKeys.find(k => k === "data" || k === "body")
497
- headers["inline-body-key"] = inlineKey
1086
+ for (let i = 0; i < pathParts.length; i++) {
1087
+ parent = value
1088
+ const part = pathParts[i]
1089
+
1090
+ if (/^\d+$/.test(part)) {
1091
+ value = value[parseInt(part) - 1]
1092
+ } else {
1093
+ value = value[part]
1094
+ }
498
1095
  }
499
1096
 
500
- // Create multipart body parts
501
- for (const path of bodyKeys) {
502
- console.log(`[encode] Processing bodyKey: ${path}`)
503
-
504
- // Handle root-level fields
505
- if (!path.includes("/")) {
506
- // This is a root-level field
507
- const fieldValue = parts[""] && parts[""][path]
508
- console.log(`[encode] Root field ${path} value:`, fieldValue)
509
-
510
- if (fieldValue !== undefined) {
511
- if (isBytes(fieldValue)) {
512
- // Binary field - create a simple multipart section
513
- // The format should be:
514
- // content-disposition: form-data;name="fieldname"
515
- // [empty line]
516
- // [binary data]
517
- const headerStr = `content-disposition: form-data;name="${path}"\r\n\r\n`
518
- const headerBlob = new Blob([headerStr])
519
- const dataBlob = new Blob([fieldValue])
520
- bodyParts.push(new Blob([headerBlob, dataBlob]))
521
- console.log(`[encode] Added binary field ${path} to bodyParts`)
522
- continue
523
- } else {
524
- // Non-binary root field that needs to go in body
525
- const lines = []
1097
+ console.log(`[Body part] Value at ${bodyKey}:`, JSON.stringify(value))
526
1098
 
527
- // Check if this is an inline key
528
- const isInlineKey = path === "data" || path === "body"
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
+ }
529
1108
 
530
- if (isInlineKey) {
531
- lines.push(`content-disposition: inline`)
532
- } else {
533
- lines.push(`content-disposition: form-data;name="${path}"`)
534
- }
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
1117
+ }
535
1118
 
536
- // Add the field type if available
537
- const type = types[path]
538
- if (type) {
539
- lines.push(`ao-types: ${path}="${type}"`)
540
- }
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
1142
+ }
1143
+
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
+ })
541
1160
 
542
- // Add empty line before content
543
- lines.push("")
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}"`)
544
1194
 
545
- // Add the actual value
546
- lines.push(String(fieldValue))
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
547
1199
 
548
- bodyParts.push(new Blob([lines.join("\r\n")]))
549
- console.log(`[encode] Added non-binary field ${path} to bodyParts`)
550
- continue
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")]))
551
1206
  }
552
1207
  }
553
- }
554
-
555
- // Handle nested paths
556
- const content = parts[path]
557
- if (!content) {
558
- console.log(`[encode] No content found for path: ${path}`)
559
1208
  continue
560
1209
  }
561
1210
 
562
- const lines = []
563
- const binaryParts = []
564
- const isInlineKey =
565
- (path === "data" || path === "body") && !path.includes("/")
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
+ })
566
1222
 
567
- const sortedKeys = Object.keys(content).sort()
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
+ )
568
1229
 
569
- // Collect ao-types for this part
1230
+ const fieldLines = []
570
1231
  const partTypes = []
571
- for (const key of sortedKeys) {
572
- const fullPath = path ? `${path}/${key}` : key
573
- const type = types[fullPath]
574
- if (type) {
575
- partTypes.push(`${key}="${type}"`)
576
- }
577
- }
578
1232
 
579
- if (partTypes.length > 0) {
580
- lines.push(`ao-types: ${partTypes.sort().join(", ")}`)
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
+ })
581
1241
  }
582
1242
 
583
- if (isInlineKey) {
584
- lines.push(`content-disposition: inline`)
585
- } else {
586
- lines.push(`content-disposition: form-data;name="${path}"`)
587
- }
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
1249
+ )
588
1250
 
589
- // Add content fields
590
- for (const key of sortedKeys) {
591
- const value = content[key]
592
- const fullPath = path ? `${path}/${key}` : key
593
- const type = types[fullPath]
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
+ })
594
1261
 
595
- if (
596
- type === "empty-message" ||
597
- type === "empty-list" ||
598
- type === "empty-binary"
599
- ) {
600
- continue
1262
+ const isMixedArray =
1263
+ hasObjects &&
1264
+ (hasEmptyStrings || hasEmptyObjects) &&
1265
+ !hasObjectsWithOnlyEmptyValues
1266
+
1267
+ // Process ALL items for type information
1268
+ value.forEach((item, idx) => {
1269
+ const index = idx + 1
1270
+
1271
+ // Skip type info for elements that have their own parts
1272
+ if (indicesWithOwnParts.has(index)) {
1273
+ return
601
1274
  }
602
1275
 
1276
+ // If we have nested object parts and this is an array with objects, skip processing it inline
603
1277
  if (
604
- (value === "[]" || value === "{}" || value === "") &&
605
- type &&
606
- type.startsWith("empty")
1278
+ hasNestedObjectParts &&
1279
+ Array.isArray(item) &&
1280
+ item.some(subItem => isPojo(subItem))
607
1281
  ) {
608
- continue
1282
+ // Type info already added above
1283
+ return
609
1284
  }
610
1285
 
611
- if (isBytes(value)) {
612
- binaryParts.push({ key, value })
613
- } else if (type === "atom" && typeof value === "boolean") {
614
- lines.push(`${key}: "${value}"`)
615
- } else if (typeof value === "symbol") {
616
- // Handle Symbol values
617
- const symbolValue = value.description || "symbol"
618
- lines.push(`${key}: ${symbolValue}`)
619
- } else {
620
- lines.push(`${key}: ${value}`)
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
+ )
621
1397
  }
622
- }
623
1398
 
624
- if (lines.length >= 1 || binaryParts.length > 0) {
625
- if (binaryParts.length > 0) {
626
- const allParts = []
1399
+ orderedLines.push("content-disposition: inline")
627
1400
 
628
- // Add text headers first
629
- if (lines.length > 0) {
630
- allParts.push(lines.join("\r\n"))
631
- allParts.push("\r\n")
1401
+ if (fieldLines.length > 0) {
1402
+ orderedLines.push("")
1403
+ for (const line of fieldLines) {
1404
+ orderedLines.push(line)
632
1405
  }
1406
+ }
633
1407
 
634
- // Add binary data
635
- for (const binaryPart of binaryParts) {
636
- allParts.push(`${binaryPart.key}: `)
637
- allParts.push(binaryPart.value)
638
- if (binaryParts.indexOf(binaryPart) < binaryParts.length - 1) {
639
- allParts.push("\r\n")
640
- }
641
- }
1408
+ bodyParts.push(new Blob([orderedLines.join("\r\n") + "\r\n"]))
1409
+ } 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
+ }
1424
+
1425
+ orderedLines.push(`content-disposition: form-data;name="${bodyKey}"`)
1426
+
1427
+ // Add field lines directly without blank line
1428
+ for (const line of fieldLines) {
1429
+ orderedLines.push(line)
1430
+ }
642
1431
 
643
- bodyParts.push(new Blob(allParts))
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"]))
644
1435
  } else {
645
- bodyParts.push(new Blob([lines.join("\r\n")]))
1436
+ bodyParts.push(new Blob([orderedLines.join("\r\n")]))
646
1437
  }
647
1438
  }
1439
+ continue
648
1440
  }
649
1441
 
650
- console.log(`[encode] Total bodyParts: ${bodyParts.length}`)
651
-
652
- // Create boundary based on parts content
653
- const partsForBoundary = []
654
- for (const part of bodyParts) {
655
- const partContent = await part.text()
656
- partsForBoundary.push(partContent)
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`)
1446
+ } else {
1447
+ lines.push(`content-disposition: form-data;name="${bodyKey}"`)
657
1448
  }
658
- const allPartsContent = partsForBoundary.join("")
659
- console.log(`[encode] All parts content length: ${allPartsContent.length}`)
660
-
661
- const allPartsBuffer = Buffer.from(allPartsContent)
662
- const hashResult = await sha256(
663
- allPartsBuffer.buffer.slice(
664
- allPartsBuffer.byteOffset,
665
- allPartsBuffer.byteOffset + allPartsBuffer.byteLength
666
- )
667
- )
668
- const boundary = base64url.encode(Buffer.from(hashResult))
669
1449
 
670
- // Create final multipart body
671
- const finalParts = []
672
- for (let i = 0; i < bodyParts.length; i++) {
673
- finalParts.push(`--${boundary}`)
674
- finalParts.push(`\r\n`)
675
- finalParts.push(bodyParts[i])
676
- if (i < bodyParts.length - 1) {
677
- finalParts.push(`\r\n`)
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()"}"`
678
1472
  }
1473
+
1474
+ lines.push("")
1475
+ lines.push(content)
1476
+ bodyParts.push(new Blob([lines.join("\r\n")]))
679
1477
  }
680
- finalParts.push(`\r\n--${boundary}--`)
1478
+ }
1479
+
1480
+ // Add the special data/body part if needed
1481
+ if (
1482
+ hasSpecialDataBody &&
1483
+ obj.data &&
1484
+ obj.data.body &&
1485
+ isBytes(obj.data.body)
1486
+ ) {
1487
+ const buffer = toBuffer(obj.data.body)
1488
+ const specialPart = [
1489
+ `content-disposition: form-data;name="data/body"`,
1490
+ "",
1491
+ "",
1492
+ ].join("\r\n")
1493
+ bodyParts.push(new Blob([specialPart, buffer]))
1494
+ }
681
1495
 
682
- headers["content-type"] = `multipart/form-data; boundary="${boundary}"`
683
- const body = new Blob(finalParts)
1496
+ const partsContent = await Promise.all(bodyParts.map(part => part.text()))
1497
+ const allContent = partsContent.join("")
1498
+ const boundaryHash = await sha256(new TextEncoder().encode(allContent))
1499
+ const boundary = base64url.encode(Buffer.from(boundaryHash))
684
1500
 
685
- const finalContent = await body.arrayBuffer()
1501
+ const finalParts = []
1502
+ for (let i = 0; i < bodyParts.length; i++) {
1503
+ if (i === 0) {
1504
+ finalParts.push(new Blob([`--${boundary}\r\n`]))
1505
+ } else {
1506
+ finalParts.push(new Blob([`\r\n--${boundary}\r\n`]))
1507
+ }
1508
+ finalParts.push(bodyParts[i])
1509
+ }
1510
+ finalParts.push(new Blob([`\r\n--${boundary}--`]))
1511
+
1512
+ headers["content-type"] = `multipart/form-data; boundary="${boundary}"`
1513
+ const body = new Blob(finalParts)
1514
+
1515
+ const finalContent = await body.arrayBuffer()
1516
+
1517
+ if (finalContent.byteLength > 0) {
686
1518
  const contentDigest = await sha256(finalContent)
687
1519
  const base64 = base64url.toBase64(base64url.encode(contentDigest))
688
1520
  headers["content-digest"] = `sha-256=:${base64}:`
689
- headers["content-length"] = String(finalContent.byteLength)
690
-
691
- console.log("[encode] FINAL - headers:", headers, "body:", body)
692
- return { headers, body }
693
1521
  }
694
1522
 
695
- console.log("[encode] FINAL - headers:", headers, "body:", undefined)
696
- return { headers, body: undefined }
1523
+ headers["content-length"] = String(finalContent.byteLength)
1524
+
1525
+ console.log("=== ENCODE END ===\n")
1526
+ return { headers, body }
697
1527
  }
698
1528
 
699
1529
  export async function enc(fields) {