wao 0.27.1 → 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/cjs/encode.js +1137 -952
- package/cjs/toerl.js +173 -0
- package/esm/encode.js +1063 -731
- package/esm/toerl.js +162 -0
- package/package.json +1 -1
package/esm/encode.js
CHANGED
|
@@ -55,14 +55,16 @@ async function sha256(data) {
|
|
|
55
55
|
}
|
|
56
56
|
|
|
57
57
|
function formatFloat(num) {
|
|
58
|
-
// Format float in scientific notation with proper padding
|
|
59
58
|
let exp = num.toExponential(20)
|
|
60
|
-
// Replace "1.23e+0" with "1.23e+00"
|
|
61
59
|
exp = exp.replace(/e\+(\d)$/, "e+0$1")
|
|
62
60
|
exp = exp.replace(/e-(\d)$/, "e-0$1")
|
|
63
61
|
return exp
|
|
64
62
|
}
|
|
65
63
|
|
|
64
|
+
function hasNonAscii(str) {
|
|
65
|
+
return /[^\x00-\x7F]/.test(str)
|
|
66
|
+
}
|
|
67
|
+
|
|
66
68
|
function encodeArrayItem(item) {
|
|
67
69
|
if (typeof item === "number") {
|
|
68
70
|
if (Number.isInteger(item)) {
|
|
@@ -82,7 +84,6 @@ function encodeArrayItem(item) {
|
|
|
82
84
|
} else if (typeof item === "boolean") {
|
|
83
85
|
return `"(ao-type-atom) \\"${item}\\""`
|
|
84
86
|
} else if (Array.isArray(item)) {
|
|
85
|
-
// Nested array
|
|
86
87
|
const nestedItems = item
|
|
87
88
|
.map(nestedItem => {
|
|
88
89
|
if (typeof nestedItem === "number") {
|
|
@@ -95,9 +96,58 @@ function encodeArrayItem(item) {
|
|
|
95
96
|
return `\\"${nestedItem}\\"`
|
|
96
97
|
} else if (nestedItem === null) {
|
|
97
98
|
return `\\"(ao-type-atom) \\\\\\"null\\\\\\"\\"`
|
|
99
|
+
} else if (nestedItem === undefined) {
|
|
100
|
+
return `\\"(ao-type-atom) \\\\\\"undefined\\\\\\"\\"`
|
|
98
101
|
} else if (typeof nestedItem === "symbol") {
|
|
99
102
|
const desc = nestedItem.description || "Symbol.for()"
|
|
100
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}\\"`
|
|
101
151
|
} else {
|
|
102
152
|
return `\\"${String(nestedItem)}\\"`
|
|
103
153
|
}
|
|
@@ -105,12 +155,10 @@ function encodeArrayItem(item) {
|
|
|
105
155
|
.join(", ")
|
|
106
156
|
return `"(ao-type-list) ${nestedItems}"`
|
|
107
157
|
} else if (isBytes(item)) {
|
|
108
|
-
// For empty binaries in arrays, return empty string
|
|
109
158
|
const buffer = toBuffer(item)
|
|
110
159
|
if (buffer.length === 0 || buffer.byteLength === 0) {
|
|
111
160
|
return `""`
|
|
112
161
|
}
|
|
113
|
-
// For non-empty binaries, we can't include them in headers
|
|
114
162
|
return `"(ao-type-binary)"`
|
|
115
163
|
} else if (isPojo(item)) {
|
|
116
164
|
const json = JSON.stringify(item)
|
|
@@ -121,67 +169,181 @@ function encodeArrayItem(item) {
|
|
|
121
169
|
}
|
|
122
170
|
}
|
|
123
171
|
|
|
124
|
-
function
|
|
125
|
-
if (
|
|
126
|
-
|
|
127
|
-
if (
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
)
|
|
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)
|
|
138
186
|
}
|
|
139
|
-
return false
|
|
140
187
|
}
|
|
141
188
|
|
|
142
189
|
function collectBodyKeys(obj, prefix = "") {
|
|
190
|
+
console.log("=== collectBodyKeys START ===")
|
|
191
|
+
console.log("Input object:", JSON.stringify(obj))
|
|
192
|
+
|
|
143
193
|
const keys = []
|
|
144
194
|
|
|
145
195
|
function traverse(current, path) {
|
|
146
|
-
|
|
196
|
+
console.log(`[traverse] Called with path: "${path}"`)
|
|
147
197
|
let hasSimpleFields = false
|
|
148
|
-
// Track nested paths that need body parts
|
|
149
198
|
const nestedPaths = []
|
|
199
|
+
let hasArraysWithObjects = false
|
|
150
200
|
|
|
151
201
|
for (const [key, value] of Object.entries(current)) {
|
|
152
202
|
const fullPath = path ? `${path}/${key}` : key
|
|
153
203
|
|
|
154
204
|
if (Array.isArray(value)) {
|
|
205
|
+
console.log(
|
|
206
|
+
`[traverse] Found array at ${fullPath}, length: ${value.length}`
|
|
207
|
+
)
|
|
155
208
|
const hasObjects = value.some(item => isPojo(item))
|
|
156
209
|
const hasNonObjects = value.some(item => !isPojo(item))
|
|
157
210
|
|
|
158
|
-
if (
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
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
|
+
)
|
|
164
238
|
})
|
|
165
239
|
|
|
166
|
-
//
|
|
167
|
-
if (
|
|
168
|
-
|
|
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
|
+
}
|
|
169
280
|
}
|
|
170
281
|
} else {
|
|
171
|
-
|
|
282
|
+
console.log(
|
|
283
|
+
`[traverse] Non-empty array without objects at ${fullPath} - marking as simple field`
|
|
284
|
+
)
|
|
172
285
|
hasSimpleFields = true
|
|
173
286
|
}
|
|
174
287
|
} else if (isPojo(value)) {
|
|
175
|
-
// Check if this is an empty object
|
|
176
288
|
if (Object.keys(value).length === 0) {
|
|
177
|
-
|
|
289
|
+
console.log(
|
|
290
|
+
`[traverse] Empty object at ${fullPath} - marking parent as having simple fields`
|
|
291
|
+
)
|
|
178
292
|
hasSimpleFields = true
|
|
179
293
|
} else {
|
|
180
|
-
//
|
|
181
|
-
|
|
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
|
+
}
|
|
182
341
|
}
|
|
183
342
|
} else if (isBytes(value)) {
|
|
184
|
-
|
|
343
|
+
const buffer = toBuffer(value)
|
|
344
|
+
if (buffer.length > 0) {
|
|
345
|
+
hasSimpleFields = true
|
|
346
|
+
}
|
|
185
347
|
} else if (
|
|
186
348
|
typeof value === "string" ||
|
|
187
349
|
typeof value === "number" ||
|
|
@@ -194,12 +356,38 @@ function collectBodyKeys(obj, prefix = "") {
|
|
|
194
356
|
}
|
|
195
357
|
}
|
|
196
358
|
|
|
197
|
-
// Add current path if it has simple fields or empty objects
|
|
198
359
|
if (hasSimpleFields) {
|
|
360
|
+
console.log(`[traverse] Adding "${path}" to keys (has simple fields)`)
|
|
199
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
|
+
}
|
|
200
389
|
}
|
|
201
390
|
|
|
202
|
-
// Process nested paths
|
|
203
391
|
for (const nestedPath of nestedPaths) {
|
|
204
392
|
const parts = nestedPath.split("/")
|
|
205
393
|
let nestedObj = obj
|
|
@@ -218,71 +406,300 @@ function collectBodyKeys(obj, prefix = "") {
|
|
|
218
406
|
}
|
|
219
407
|
}
|
|
220
408
|
|
|
221
|
-
|
|
409
|
+
const objKeys = Object.keys(obj)
|
|
410
|
+
|
|
222
411
|
for (const [key, value] of Object.entries(obj)) {
|
|
223
|
-
|
|
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
|
+
|
|
224
455
|
const hasObjects = value.some(item => isPojo(item))
|
|
225
456
|
const hasArrays = value.some(item => Array.isArray(item))
|
|
226
457
|
const hasNonObjects = value.some(item => !isPojo(item))
|
|
227
458
|
|
|
228
|
-
if
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
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
|
+
}
|
|
236
531
|
}
|
|
237
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
|
|
238
552
|
}
|
|
239
|
-
})
|
|
240
553
|
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
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
|
+
}
|
|
244
631
|
}
|
|
245
|
-
} else if (hasArrays) {
|
|
246
|
-
// Array containing arrays needs body part
|
|
247
|
-
keys.push(key)
|
|
248
|
-
} else {
|
|
249
|
-
// Simple array at top level - DO NOT add to body keys
|
|
250
|
-
// It will go in headers instead
|
|
251
632
|
}
|
|
252
633
|
} else if (isPojo(value)) {
|
|
253
|
-
|
|
634
|
+
console.log(`[main loop] Processing object at key: "${key}"`)
|
|
635
|
+
// Objects should be traversed, not have their fields individually added
|
|
254
636
|
traverse(value, key)
|
|
255
637
|
} else if (isBytes(value)) {
|
|
256
|
-
|
|
257
|
-
|
|
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
|
+
}
|
|
258
643
|
} else if (typeof value === "string" && value.includes("\n")) {
|
|
259
|
-
|
|
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}"`)
|
|
260
648
|
keys.push(key)
|
|
649
|
+
} else {
|
|
650
|
+
console.log(`[main loop] Skipping key: "${key}" (no match)`)
|
|
261
651
|
}
|
|
262
652
|
}
|
|
263
653
|
|
|
264
|
-
|
|
265
|
-
|
|
654
|
+
const result = [...new Set(keys)].filter(k => {
|
|
655
|
+
if (k === "") return false
|
|
266
656
|
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
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
|
|
282
697
|
}
|
|
283
698
|
|
|
284
699
|
async function encode(obj = {}) {
|
|
285
|
-
|
|
700
|
+
console.log("\n=== ENCODE START ===")
|
|
701
|
+
console.log("Encoding object:", JSON.stringify(obj))
|
|
702
|
+
|
|
286
703
|
const processValue = value => {
|
|
287
704
|
if (typeof value === "symbol") {
|
|
288
705
|
return value.description || "Symbol.for()"
|
|
@@ -303,231 +720,248 @@ async function encode(obj = {}) {
|
|
|
303
720
|
processedObj[k] = processValue(v)
|
|
304
721
|
}
|
|
305
722
|
|
|
306
|
-
// Remove debug logging for cleaner output
|
|
307
|
-
console.log("[encode] START with obj:", JSON.stringify(processedObj))
|
|
308
|
-
|
|
309
723
|
if (Object.keys(obj).length === 0) {
|
|
310
724
|
return { headers: {}, body: undefined }
|
|
311
725
|
}
|
|
312
726
|
|
|
313
|
-
|
|
727
|
+
const objKeys = Object.keys(obj)
|
|
728
|
+
|
|
729
|
+
if (objKeys.length === 1) {
|
|
730
|
+
const fieldName = objKeys[0]
|
|
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
|
+
}
|
|
750
|
+
|
|
314
751
|
const hasBodyBinary = obj.body && isBytes(obj.body)
|
|
315
752
|
const otherFields = Object.keys(obj).filter(k => k !== "body")
|
|
316
|
-
const allOthersSimpleOrEmptyBinary = otherFields.every(k => {
|
|
317
|
-
const v = obj[k]
|
|
318
|
-
// Allow empty binaries as "simple"
|
|
319
|
-
if (isBytes(v) && (v.length === 0 || v.byteLength === 0)) return true
|
|
320
|
-
return (
|
|
321
|
-
!isBytes(v) &&
|
|
322
|
-
!isPojo(v) &&
|
|
323
|
-
!(Array.isArray(v) && v.some(item => isPojo(item) || isBytes(item)))
|
|
324
|
-
)
|
|
325
|
-
})
|
|
326
753
|
|
|
327
|
-
if (hasBodyBinary &&
|
|
328
|
-
console.log("[encode] Special case: body with binary + simple fields")
|
|
329
|
-
// Special case: body with binary + other simple fields
|
|
754
|
+
if (hasBodyBinary && otherFields.length === 0) {
|
|
330
755
|
const headers = {}
|
|
331
|
-
const
|
|
756
|
+
const bodyBuffer = toBuffer(obj.body)
|
|
757
|
+
const bodyArrayBuffer = bodyBuffer.buffer.slice(
|
|
758
|
+
bodyBuffer.byteOffset,
|
|
759
|
+
bodyBuffer.byteOffset + bodyBuffer.byteLength
|
|
760
|
+
)
|
|
332
761
|
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
762
|
+
const contentDigest = await sha256(bodyArrayBuffer)
|
|
763
|
+
const base64 = base64url.toBase64(base64url.encode(contentDigest))
|
|
764
|
+
headers["content-digest"] = `sha-256=:${base64}:`
|
|
765
|
+
|
|
766
|
+
return { headers, body: obj.body }
|
|
767
|
+
}
|
|
768
|
+
|
|
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
|
|
338
779
|
)
|
|
339
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 (
|
|
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
|
+
}
|
|
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) {
|
|
340
864
|
if (value === null) {
|
|
341
865
|
headers[key] = '"null"'
|
|
342
|
-
headerTypes.push(`${key}="atom"`)
|
|
866
|
+
headerTypes.push(`${key.toLowerCase()}="atom"`)
|
|
343
867
|
} else if (value === undefined) {
|
|
344
868
|
headers[key] = '"undefined"'
|
|
345
|
-
headerTypes.push(`${key}="atom"`)
|
|
869
|
+
headerTypes.push(`${key.toLowerCase()}="atom"`)
|
|
346
870
|
} else if (typeof value === "boolean") {
|
|
347
871
|
headers[key] = `"${value}"`
|
|
348
|
-
headerTypes.push(`${key}="atom"`)
|
|
872
|
+
headerTypes.push(`${key.toLowerCase()}="atom"`)
|
|
349
873
|
} else if (typeof value === "symbol") {
|
|
350
874
|
headers[key] = `"${value.description || "Symbol.for()"}"`
|
|
351
|
-
headerTypes.push(`${key}="atom"`)
|
|
875
|
+
headerTypes.push(`${key.toLowerCase()}="atom"`)
|
|
352
876
|
} else if (typeof value === "number") {
|
|
353
877
|
headers[key] = String(value)
|
|
354
878
|
headerTypes.push(
|
|
355
|
-
`${key}="${Number.isInteger(value) ? "integer" : "float"}"`
|
|
879
|
+
`${key.toLowerCase()}="${Number.isInteger(value) ? "integer" : "float"}"`
|
|
356
880
|
)
|
|
357
881
|
} else if (typeof value === "string") {
|
|
358
882
|
if (value.length === 0) {
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
883
|
+
headerTypes.push(`${key.toLowerCase()}="empty-binary"`)
|
|
884
|
+
} else if (hasNonAscii(value)) {
|
|
885
|
+
continue
|
|
362
886
|
} else {
|
|
363
887
|
headers[key] = value
|
|
364
888
|
}
|
|
365
889
|
} else if (Array.isArray(value) && value.length === 0) {
|
|
366
|
-
|
|
367
|
-
headerTypes.push(`${key}="empty-list"`)
|
|
890
|
+
headerTypes.push(`${key.toLowerCase()}="empty-list"`)
|
|
368
891
|
} else if (Array.isArray(value) && !value.some(item => isPojo(item))) {
|
|
369
|
-
const
|
|
370
|
-
|
|
371
|
-
|
|
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
|
+
}
|
|
372
902
|
} else if (
|
|
373
903
|
isBytes(value) &&
|
|
374
904
|
(value.length === 0 || value.byteLength === 0)
|
|
375
905
|
) {
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
}
|
|
380
|
-
|
|
381
|
-
// Add ao-types if needed
|
|
382
|
-
if (headerTypes.length > 0) {
|
|
383
|
-
headers["ao-types"] = headerTypes.sort().join(", ")
|
|
384
|
-
}
|
|
385
|
-
|
|
386
|
-
// Set body to binary
|
|
387
|
-
const bodyBuffer = toBuffer(obj.body)
|
|
388
|
-
const bodyArrayBuffer = bodyBuffer.buffer.slice(
|
|
389
|
-
bodyBuffer.byteOffset,
|
|
390
|
-
bodyBuffer.byteOffset + bodyBuffer.byteLength
|
|
391
|
-
)
|
|
392
|
-
|
|
393
|
-
const contentDigest = await sha256(bodyArrayBuffer)
|
|
394
|
-
const base64 = base64url.toBase64(base64url.encode(contentDigest))
|
|
395
|
-
headers["content-digest"] = `sha-256=:${base64}:`
|
|
396
|
-
|
|
397
|
-
console.log(
|
|
398
|
-
"[encode] FINAL (body with binary) - headers:",
|
|
399
|
-
headers,
|
|
400
|
-
"body:",
|
|
401
|
-
obj.body
|
|
402
|
-
)
|
|
403
|
-
return { headers, body: obj.body }
|
|
404
|
-
}
|
|
405
|
-
|
|
406
|
-
// Check if single binary field
|
|
407
|
-
const objKeys = Object.keys(obj)
|
|
408
|
-
if (objKeys.length === 1 && isBytes(obj[objKeys[0]])) {
|
|
409
|
-
const fieldName = objKeys[0]
|
|
410
|
-
const binaryData = obj[fieldName]
|
|
411
|
-
|
|
412
|
-
const headers = {}
|
|
413
|
-
const bodyBuffer = toBuffer(binaryData)
|
|
414
|
-
const bodyArrayBuffer = bodyBuffer.buffer.slice(
|
|
415
|
-
bodyBuffer.byteOffset,
|
|
416
|
-
bodyBuffer.byteOffset + bodyBuffer.byteLength
|
|
417
|
-
)
|
|
418
|
-
|
|
419
|
-
const contentDigest = await sha256(bodyArrayBuffer)
|
|
420
|
-
const base64 = base64url.toBase64(base64url.encode(contentDigest))
|
|
421
|
-
headers["content-digest"] = `sha-256=:${base64}:`
|
|
422
|
-
|
|
423
|
-
// Add inline-body-key header to preserve the field name
|
|
424
|
-
if (fieldName !== "body") {
|
|
425
|
-
headers["inline-body-key"] = fieldName
|
|
426
|
-
}
|
|
427
|
-
|
|
428
|
-
console.log(
|
|
429
|
-
"[encode] FINAL (simple binary field) - headers:",
|
|
430
|
-
headers,
|
|
431
|
-
"body:",
|
|
432
|
-
binaryData
|
|
433
|
-
)
|
|
434
|
-
return { headers, body: binaryData }
|
|
435
|
-
}
|
|
436
|
-
|
|
437
|
-
// Continue with normal multipart processing
|
|
438
|
-
const headers = {}
|
|
439
|
-
const headerTypes = []
|
|
440
|
-
|
|
441
|
-
// Collect all body keys
|
|
442
|
-
const bodyKeys = collectBodyKeys(obj)
|
|
443
|
-
|
|
444
|
-
// Process simple header fields AND collect types for body fields
|
|
445
|
-
for (const [key, value] of Object.entries(obj)) {
|
|
446
|
-
console.log(
|
|
447
|
-
`[encode] Processing field: ${key} = ${JSON.stringify(value)}, type: ${typeof value}`
|
|
448
|
-
)
|
|
449
|
-
const needsBody =
|
|
450
|
-
bodyKeys.includes(key) || bodyKeys.some(k => k.startsWith(`${key}/`))
|
|
451
|
-
|
|
452
|
-
if (!needsBody) {
|
|
453
|
-
console.log(`[encode] Field ${key} doesn't need body, adding to headers`)
|
|
454
|
-
// Simple value goes in header
|
|
455
|
-
if (value === null) {
|
|
456
|
-
headers[key] = '"null"'
|
|
457
|
-
headerTypes.push(`${key}="atom"`)
|
|
458
|
-
} else if (value === undefined) {
|
|
459
|
-
headers[key] = '"undefined"'
|
|
460
|
-
headerTypes.push(`${key}="atom"`)
|
|
461
|
-
} else if (typeof value === "boolean") {
|
|
462
|
-
headers[key] = `"${value}"`
|
|
463
|
-
headerTypes.push(`${key}="atom"`)
|
|
464
|
-
} else if (typeof value === "symbol") {
|
|
465
|
-
headers[key] = `"${value.description || "Symbol.for()"}"`
|
|
466
|
-
headerTypes.push(`${key}="atom"`)
|
|
467
|
-
} else if (typeof value === "number") {
|
|
468
|
-
headers[key] = String(value)
|
|
469
|
-
headerTypes.push(
|
|
470
|
-
`${key}="${Number.isInteger(value) ? "integer" : "float"}"`
|
|
471
|
-
)
|
|
472
|
-
} else if (typeof value === "string") {
|
|
473
|
-
if (value.length === 0) {
|
|
474
|
-
headerTypes.push(`${key}="empty-binary"`)
|
|
475
|
-
// Don't add empty strings as headers
|
|
476
|
-
} else {
|
|
477
|
-
headers[key] = value
|
|
478
|
-
}
|
|
479
|
-
} else if (Array.isArray(value) && !value.some(item => isPojo(item))) {
|
|
480
|
-
// Simple array (no objects) goes in header
|
|
481
|
-
const encodedItems = value.map(item => encodeArrayItem(item)).join(", ")
|
|
482
|
-
headers[key] = encodedItems
|
|
483
|
-
headerTypes.push(`${key}="list"`)
|
|
906
|
+
headerTypes.push(`${key.toLowerCase()}="empty-binary"`)
|
|
907
|
+
} else if (isPojo(value) && Object.keys(value).length === 0) {
|
|
908
|
+
headerTypes.push(`${key.toLowerCase()}="empty-message"`)
|
|
484
909
|
}
|
|
485
910
|
} else {
|
|
486
|
-
// Field needs body - still need to add type info to ao-types
|
|
487
911
|
if (isBytes(value) && (value.length === 0 || value.byteLength === 0)) {
|
|
488
|
-
headerTypes.push(`${key}="empty-binary"`)
|
|
912
|
+
headerTypes.push(`${key.toLowerCase()}="empty-binary"`)
|
|
489
913
|
} else if (typeof value === "string" && value.length === 0) {
|
|
490
|
-
headerTypes.push(`${key}="empty-binary"`)
|
|
914
|
+
headerTypes.push(`${key.toLowerCase()}="empty-binary"`)
|
|
491
915
|
} else if (Array.isArray(value) && value.length === 0) {
|
|
492
|
-
headerTypes.push(`${key}="empty-list"`)
|
|
916
|
+
headerTypes.push(`${key.toLowerCase()}="empty-list"`)
|
|
493
917
|
} else if (isPojo(value) && Object.keys(value).length === 0) {
|
|
494
|
-
headerTypes.push(`${key}="empty-message"`)
|
|
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
|
+
)
|
|
495
930
|
}
|
|
496
931
|
}
|
|
497
932
|
}
|
|
498
933
|
|
|
499
|
-
// Add ao-types for arrays that go in body
|
|
500
934
|
for (const [key, value] of Object.entries(obj)) {
|
|
501
935
|
if (Array.isArray(value)) {
|
|
502
|
-
// Check if this array goes in the body
|
|
503
936
|
if (
|
|
504
937
|
bodyKeys.includes(key) ||
|
|
505
938
|
bodyKeys.some(k => k.startsWith(`${key}/`))
|
|
506
939
|
) {
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
headerTypes.push(`${key}="list"`)
|
|
940
|
+
if (!headerTypes.some(t => t.startsWith(`${key.toLowerCase()}=`))) {
|
|
941
|
+
headerTypes.push(`${key.toLowerCase()}="list"`)
|
|
510
942
|
}
|
|
511
943
|
}
|
|
512
944
|
}
|
|
513
945
|
}
|
|
514
946
|
|
|
515
|
-
// If no body needed
|
|
516
947
|
if (bodyKeys.length === 0) {
|
|
948
|
+
console.log("No bodyKeys, returning headers only")
|
|
517
949
|
if (headerTypes.length > 0) {
|
|
518
950
|
headers["ao-types"] = headerTypes.sort().join(", ")
|
|
519
951
|
}
|
|
520
|
-
console.log("[encode] FINAL - headers:", headers, "body:", undefined)
|
|
521
952
|
return { headers, body: undefined }
|
|
522
953
|
}
|
|
523
954
|
|
|
524
|
-
// Check if all body keys are for empty binaries - if so, treat as no body needed
|
|
525
955
|
const allBodyKeysAreEmptyBinaries = bodyKeys.every(key => {
|
|
526
956
|
const pathParts = key.split("/")
|
|
527
957
|
let value = obj
|
|
528
958
|
for (const part of pathParts) {
|
|
529
959
|
if (/^\d+$/.test(part)) {
|
|
530
|
-
|
|
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]
|
|
531
965
|
} else {
|
|
532
966
|
value = value[part]
|
|
533
967
|
}
|
|
@@ -536,29 +970,87 @@ async function encode(obj = {}) {
|
|
|
536
970
|
})
|
|
537
971
|
|
|
538
972
|
if (allBodyKeysAreEmptyBinaries) {
|
|
539
|
-
// Treat as header-only encoding
|
|
540
973
|
if (headerTypes.length > 0) {
|
|
541
974
|
headers["ao-types"] = headerTypes.sort().join(", ")
|
|
542
975
|
}
|
|
543
|
-
console.log(
|
|
544
|
-
"[encode] FINAL (all empty binaries) - headers:",
|
|
545
|
-
headers,
|
|
546
|
-
"body:",
|
|
547
|
-
undefined
|
|
548
|
-
)
|
|
549
976
|
return { headers, body: undefined }
|
|
550
977
|
}
|
|
551
978
|
|
|
552
|
-
|
|
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
|
+
}
|
|
990
|
+
|
|
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
|
+
})
|
|
1000
|
+
|
|
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
|
+
)
|
|
1007
|
+
|
|
1008
|
+
const contentDigest = await sha256(bodyArrayBuffer)
|
|
1009
|
+
const base64 = base64url.toBase64(base64url.encode(contentDigest))
|
|
1010
|
+
headers["content-digest"] = `sha-256=:${base64}:`
|
|
1011
|
+
|
|
1012
|
+
if (singleKey !== "body") {
|
|
1013
|
+
headers["inline-body-key"] = singleKey
|
|
1014
|
+
}
|
|
1015
|
+
|
|
1016
|
+
if (headerTypes.length > 0) {
|
|
1017
|
+
headers["ao-types"] = headerTypes.sort().join(", ")
|
|
1018
|
+
}
|
|
1019
|
+
|
|
1020
|
+
return { headers, body: value }
|
|
1021
|
+
}
|
|
1022
|
+
}
|
|
1023
|
+
|
|
1024
|
+
// Sort body keys: main array comes first, then element parts by index
|
|
553
1025
|
const sortedBodyKeys = bodyKeys.sort((a, b) => {
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
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)
|
|
1047
|
+
}
|
|
1048
|
+
|
|
1049
|
+
// Different arrays, sort by base name
|
|
557
1050
|
return a.localeCompare(b)
|
|
558
1051
|
})
|
|
559
1052
|
|
|
560
|
-
//
|
|
561
|
-
// then we need special handling per Erlang behavior
|
|
1053
|
+
// Check if we have the special case where data contains body with bytes and body contains data with bytes
|
|
562
1054
|
const hasSpecialDataBody =
|
|
563
1055
|
sortedBodyKeys.includes("data") &&
|
|
564
1056
|
sortedBodyKeys.includes("body") &&
|
|
@@ -571,34 +1063,26 @@ async function encode(obj = {}) {
|
|
|
571
1063
|
|
|
572
1064
|
headers["body-keys"] = sortedBodyKeys.map(k => `"${k}"`).join(", ")
|
|
573
1065
|
|
|
574
|
-
// Check for inline keys - but not in the special data/body case
|
|
575
1066
|
if (!hasSpecialDataBody) {
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
// Only set inline-body-key if we have ONLY body (not data)
|
|
579
|
-
if (sortedBodyKeys.includes("body") && !sortedBodyKeys.includes("data")) {
|
|
580
|
-
headers["inline-body-key"] = "body"
|
|
581
|
-
}
|
|
1067
|
+
if (sortedBodyKeys.includes("body") && sortedBodyKeys.length === 1) {
|
|
1068
|
+
headers["inline-body-key"] = "body"
|
|
582
1069
|
}
|
|
583
1070
|
}
|
|
584
1071
|
|
|
585
|
-
// Add ao-types header if needed
|
|
586
1072
|
if (headerTypes.length > 0) {
|
|
587
1073
|
headers["ao-types"] = headerTypes.sort().join(", ")
|
|
588
1074
|
}
|
|
589
1075
|
|
|
590
|
-
// Create multipart body parts
|
|
591
1076
|
const bodyParts = []
|
|
592
1077
|
|
|
593
1078
|
for (const bodyKey of sortedBodyKeys) {
|
|
1079
|
+
console.log(`\n[Body part] Processing bodyKey: ${bodyKey}`)
|
|
594
1080
|
const lines = []
|
|
595
1081
|
|
|
596
|
-
// Parse the path to get to the value
|
|
597
1082
|
const pathParts = bodyKey.split("/")
|
|
598
1083
|
let value = obj
|
|
599
1084
|
let parent = null
|
|
600
1085
|
|
|
601
|
-
// Get the actual value at this path
|
|
602
1086
|
for (let i = 0; i < pathParts.length; i++) {
|
|
603
1087
|
parent = value
|
|
604
1088
|
const part = pathParts[i]
|
|
@@ -610,339 +1094,75 @@ async function encode(obj = {}) {
|
|
|
610
1094
|
}
|
|
611
1095
|
}
|
|
612
1096
|
|
|
613
|
-
console.log(
|
|
614
|
-
"[encode] Processing body key:",
|
|
615
|
-
bodyKey,
|
|
616
|
-
"value type:",
|
|
617
|
-
Array.isArray(value) ? "array" : typeof value
|
|
618
|
-
)
|
|
619
|
-
|
|
620
|
-
// Skip if value is an array with only objects (no content for this body part)
|
|
621
|
-
if (Array.isArray(value) && value.every(item => isPojo(item))) {
|
|
622
|
-
continue
|
|
623
|
-
}
|
|
624
|
-
|
|
625
|
-
// Skip if this is an empty object
|
|
626
|
-
if (isPojo(value) && Object.keys(value).length === 0) {
|
|
627
|
-
continue
|
|
628
|
-
}
|
|
629
|
-
|
|
630
|
-
// Special handling for the data/body pattern
|
|
631
|
-
if (
|
|
632
|
-
hasSpecialDataBody &&
|
|
633
|
-
bodyKey === "data" &&
|
|
634
|
-
isPojo(value) &&
|
|
635
|
-
Object.keys(value).length === 1 &&
|
|
636
|
-
value.body &&
|
|
637
|
-
isBytes(value.body)
|
|
638
|
-
) {
|
|
639
|
-
// Skip creating inline content for 'data', will handle data/body separately
|
|
640
|
-
continue
|
|
641
|
-
}
|
|
642
|
-
|
|
643
|
-
console.log(
|
|
644
|
-
"[encode] Creating body part for key:",
|
|
645
|
-
bodyKey,
|
|
646
|
-
"value type:",
|
|
647
|
-
typeof value,
|
|
648
|
-
"isBytes:",
|
|
649
|
-
isBytes(value)
|
|
650
|
-
)
|
|
1097
|
+
console.log(`[Body part] Value at ${bodyKey}:`, JSON.stringify(value))
|
|
651
1098
|
|
|
652
|
-
//
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
lines.push(`content-disposition: inline`)
|
|
656
|
-
} else {
|
|
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}`)
|
|
657
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
|
|
658
1107
|
}
|
|
659
1108
|
|
|
660
|
-
|
|
661
|
-
isBytes: isBytes(value),
|
|
662
|
-
isPojo: isPojo(value),
|
|
663
|
-
isArray: Array.isArray(value),
|
|
664
|
-
valueType: typeof value,
|
|
665
|
-
})
|
|
666
|
-
|
|
1109
|
+
// Handle direct binary values
|
|
667
1110
|
if (isBytes(value)) {
|
|
668
|
-
console.log(
|
|
669
|
-
|
|
1111
|
+
console.log(`[Body part] Creating part for binary at ${bodyKey}`)
|
|
1112
|
+
lines.push(`content-disposition: form-data;name="${bodyKey}"`)
|
|
670
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
|
+
}
|
|
671
1118
|
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
bodyParts.push(new Blob([textPart, buffer]))
|
|
681
|
-
} else {
|
|
682
|
-
lines.push("") // Empty line after headers
|
|
683
|
-
lines.push("") // Another empty line before binary data
|
|
684
|
-
const textPart = lines.join("\r\n")
|
|
685
|
-
bodyParts.push(new Blob([textPart, buffer]))
|
|
686
|
-
}
|
|
687
|
-
} else if (isPojo(value)) {
|
|
688
|
-
console.log("[encode] Processing object value")
|
|
689
|
-
// Object - only include fields that aren't handled by nested body parts
|
|
690
|
-
const objectTypes = []
|
|
691
|
-
const fieldLines = []
|
|
692
|
-
const binaryFields = []
|
|
693
|
-
|
|
694
|
-
for (const [k, v] of Object.entries(value)) {
|
|
695
|
-
const childPath = `${bodyKey}/${k}`
|
|
696
|
-
|
|
697
|
-
// Skip if this field has its own body part
|
|
698
|
-
if (sortedBodyKeys.includes(childPath)) {
|
|
699
|
-
continue
|
|
700
|
-
}
|
|
701
|
-
|
|
702
|
-
// Skip if this is an array of objects (handled separately)
|
|
703
|
-
if (Array.isArray(v) && v.some(item => isPojo(item))) {
|
|
704
|
-
continue
|
|
705
|
-
}
|
|
706
|
-
|
|
707
|
-
// Add type info
|
|
708
|
-
if (Array.isArray(v)) {
|
|
709
|
-
objectTypes.push(`${k}="${v.length === 0 ? "empty-list" : "list"}"`)
|
|
710
|
-
} else if (
|
|
711
|
-
v === null ||
|
|
712
|
-
v === undefined ||
|
|
713
|
-
typeof v === "symbol" ||
|
|
714
|
-
typeof v === "boolean"
|
|
715
|
-
) {
|
|
716
|
-
objectTypes.push(`${k}="atom"`)
|
|
717
|
-
} else if (typeof v === "number") {
|
|
718
|
-
objectTypes.push(
|
|
719
|
-
`${k}="${Number.isInteger(v) ? "integer" : "float"}"`
|
|
720
|
-
)
|
|
721
|
-
} else if (typeof v === "string" && v.length === 0) {
|
|
722
|
-
objectTypes.push(`${k}="empty-binary"`)
|
|
723
|
-
} else if (isBytes(v) && (v.length === 0 || v.byteLength === 0)) {
|
|
724
|
-
objectTypes.push(`${k}="empty-binary"`)
|
|
725
|
-
} else if (isPojo(v) && Object.keys(v).length === 0) {
|
|
726
|
-
objectTypes.push(`${k}="empty-message"`)
|
|
727
|
-
}
|
|
728
|
-
|
|
729
|
-
// Add field value
|
|
730
|
-
if (typeof v === "string") {
|
|
731
|
-
fieldLines.push(`${k}: ${v}`)
|
|
732
|
-
} else if (typeof v === "number") {
|
|
733
|
-
fieldLines.push(`${k}: ${v}`)
|
|
734
|
-
} else if (typeof v === "boolean") {
|
|
735
|
-
fieldLines.push(`${k}: "${v}"`)
|
|
736
|
-
} else if (v === null) {
|
|
737
|
-
fieldLines.push(`${k}: "null"`)
|
|
738
|
-
} else if (v === undefined) {
|
|
739
|
-
fieldLines.push(`${k}: "undefined"`)
|
|
740
|
-
} else if (typeof v === "symbol") {
|
|
741
|
-
fieldLines.push(`${k}: "${v.description || "Symbol.for()"}"`)
|
|
742
|
-
} else if (isBytes(v)) {
|
|
743
|
-
const buffer = toBuffer(v)
|
|
744
|
-
// For inline data/body parts, binary fields get raw bytes
|
|
745
|
-
if (isInline) {
|
|
746
|
-
// Skip here - will be handled specially
|
|
747
|
-
continue
|
|
748
|
-
} else {
|
|
749
|
-
// For non-inline parts, we need to add raw bytes, not base64
|
|
750
|
-
// Store the binary field for later processing
|
|
751
|
-
binaryFields.push({ key: k, buffer })
|
|
752
|
-
continue
|
|
753
|
-
}
|
|
754
|
-
} else if (Array.isArray(v)) {
|
|
755
|
-
if (v.length === 0) {
|
|
756
|
-
fieldLines.push(`${k}: `)
|
|
757
|
-
} else {
|
|
758
|
-
const encodedItems = v.map(item => encodeArrayItem(item)).join(", ")
|
|
759
|
-
fieldLines.push(`${k}: ${encodedItems}`)
|
|
760
|
-
}
|
|
761
|
-
} else if (isPojo(v) && Object.keys(v).length === 0) {
|
|
762
|
-
// Empty object - no content line needed, just ao-type
|
|
763
|
-
}
|
|
764
|
-
}
|
|
765
|
-
|
|
766
|
-
// Check if this object only has empty collections
|
|
767
|
-
const onlyEmptyCollections = Object.entries(value).every(([k, v]) => {
|
|
768
|
-
const childPath = `${bodyKey}/${k}`
|
|
769
|
-
if (sortedBodyKeys.includes(childPath)) return true
|
|
770
|
-
if (Array.isArray(v) && v.some(item => isPojo(item))) return true
|
|
771
|
-
|
|
772
|
-
return (
|
|
773
|
-
(Array.isArray(v) && v.length === 0) ||
|
|
774
|
-
(isPojo(v) && Object.keys(v).length === 0) ||
|
|
775
|
-
(isBytes(v) && (v.length === 0 || v.byteLength === 0))
|
|
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 === "")
|
|
776
1127
|
)
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
// First: field lines
|
|
785
|
-
if (!onlyEmptyCollections) {
|
|
786
|
-
for (const line of fieldLines) {
|
|
787
|
-
orderedLines.push(line)
|
|
788
|
-
}
|
|
789
|
-
}
|
|
790
|
-
|
|
791
|
-
// Then: ao-types
|
|
792
|
-
if (objectTypes.length > 0) {
|
|
793
|
-
orderedLines.push(`ao-types: ${objectTypes.sort().join(", ")}`)
|
|
794
|
-
}
|
|
795
|
-
|
|
796
|
-
// Finally: content-disposition
|
|
797
|
-
orderedLines.push("content-disposition: inline")
|
|
798
|
-
|
|
799
|
-
// Check if this has binary fields
|
|
800
|
-
const binaryFields = Object.entries(value)
|
|
801
|
-
.filter(
|
|
802
|
-
([k, v]) =>
|
|
803
|
-
isBytes(v) && !sortedBodyKeys.includes(`${bodyKey}/${k}`)
|
|
804
|
-
)
|
|
805
|
-
.map(([k, v]) => ({
|
|
806
|
-
key: k,
|
|
807
|
-
buffer: toBuffer(v),
|
|
808
|
-
}))
|
|
809
|
-
|
|
810
|
-
if (binaryFields.length > 0) {
|
|
811
|
-
// Build the parts
|
|
812
|
-
const parts = []
|
|
813
|
-
|
|
814
|
-
// Add the text part
|
|
815
|
-
parts.push(Buffer.from(orderedLines.join("\r\n")))
|
|
816
|
-
|
|
817
|
-
// Add binary fields with raw bytes
|
|
818
|
-
for (const { key, buffer } of binaryFields) {
|
|
819
|
-
parts.push(Buffer.from(`\r\n${key}: `))
|
|
820
|
-
parts.push(buffer)
|
|
821
|
-
}
|
|
822
|
-
|
|
823
|
-
// Add trailing \r\n for inline parts with binary
|
|
824
|
-
parts.push(Buffer.from("\r\n"))
|
|
825
|
-
|
|
826
|
-
const fullBody = Buffer.concat(parts)
|
|
827
|
-
bodyParts.push(new Blob([fullBody]))
|
|
828
|
-
} else {
|
|
829
|
-
orderedLines.push("") // Add empty line for trailing \r\n
|
|
830
|
-
bodyParts.push(new Blob([orderedLines.join("\r\n")]))
|
|
831
|
-
}
|
|
832
|
-
} else {
|
|
833
|
-
// Normal handling (non-inline)
|
|
834
|
-
// ao-types first if needed
|
|
835
|
-
if (objectTypes.length > 0) {
|
|
836
|
-
lines.unshift(`ao-types: ${objectTypes.sort().join(", ")}`)
|
|
837
|
-
}
|
|
838
|
-
|
|
839
|
-
// Only add field lines if not all collections are empty
|
|
840
|
-
if (!onlyEmptyCollections) {
|
|
841
|
-
for (const line of fieldLines) {
|
|
842
|
-
lines.push(line)
|
|
843
|
-
}
|
|
844
|
-
}
|
|
845
|
-
|
|
846
|
-
// Then handle binary fields with raw bytes if any
|
|
847
|
-
if (binaryFields && binaryFields.length > 0) {
|
|
848
|
-
// Create parts array for proper ordering
|
|
849
|
-
const parts = []
|
|
850
|
-
|
|
851
|
-
// Add headers and text fields
|
|
852
|
-
const headerText = lines.join("\r\n") + "\r\n"
|
|
853
|
-
parts.push(Buffer.from(headerText))
|
|
854
|
-
|
|
855
|
-
// Add binary fields with raw bytes
|
|
856
|
-
for (const { key, buffer } of binaryFields) {
|
|
857
|
-
parts.push(Buffer.from(`${key}: `))
|
|
858
|
-
parts.push(buffer)
|
|
859
|
-
parts.push(Buffer.from("\r\n"))
|
|
860
|
-
}
|
|
861
|
-
|
|
862
|
-
const fullBody = Buffer.concat(parts)
|
|
863
|
-
bodyParts.push(new Blob([fullBody]))
|
|
864
|
-
} else {
|
|
865
|
-
lines.push("")
|
|
866
|
-
bodyParts.push(new Blob([lines.join("\r\n")]))
|
|
867
|
-
}
|
|
868
|
-
}
|
|
869
|
-
} else if (Array.isArray(value)) {
|
|
870
|
-
// Array field - check if it's a mixed array or array of arrays
|
|
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)
|
|
871
1134
|
const hasObjects = value.some(item => isPojo(item))
|
|
872
1135
|
const hasArrays = value.some(item => Array.isArray(item))
|
|
873
1136
|
const nonObjectItems = value
|
|
874
1137
|
.map((item, index) => ({ item, index: index + 1 }))
|
|
875
1138
|
.filter(({ item }) => !isPojo(item))
|
|
876
1139
|
|
|
877
|
-
if (
|
|
878
|
-
|
|
1140
|
+
if (hasOnlyNonEmptyObjects) {
|
|
1141
|
+
continue
|
|
1142
|
+
}
|
|
1143
|
+
|
|
1144
|
+
// For arrays containing only empty elements, we still need to show type info
|
|
1145
|
+
if (hasOnlyEmptyElements) {
|
|
879
1146
|
const fieldLines = []
|
|
880
1147
|
const partTypes = []
|
|
881
1148
|
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
}
|
|
891
|
-
} else if (typeof item === "string") {
|
|
892
|
-
fieldLines.push(`${index}: ${item}`)
|
|
893
|
-
} else if (
|
|
894
|
-
item === null ||
|
|
895
|
-
item === undefined ||
|
|
896
|
-
typeof item === "symbol" ||
|
|
897
|
-
typeof item === "boolean"
|
|
898
|
-
) {
|
|
899
|
-
partTypes.push(`${index}="atom"`)
|
|
900
|
-
if (item === null) {
|
|
901
|
-
fieldLines.push(`${index}: "null"`)
|
|
902
|
-
} else if (item === undefined) {
|
|
903
|
-
fieldLines.push(`${index}: "undefined"`)
|
|
904
|
-
} else if (typeof item === "symbol") {
|
|
905
|
-
fieldLines.push(
|
|
906
|
-
`${index}: "${item.description || "Symbol.for()"}"`
|
|
907
|
-
)
|
|
908
|
-
} else {
|
|
909
|
-
fieldLines.push(`${index}: "${item}"`)
|
|
910
|
-
}
|
|
911
|
-
} else if (isBytes(item)) {
|
|
912
|
-
// Binary items in arrays need special handling
|
|
913
|
-
const buffer = toBuffer(item)
|
|
914
|
-
if (buffer.length === 0) {
|
|
915
|
-
partTypes.push(`${index}="empty-binary"`)
|
|
916
|
-
}
|
|
917
|
-
// For now, skip binary items in mixed arrays
|
|
918
|
-
// They should be handled differently
|
|
919
|
-
partTypes.push(`${index}="binary"`)
|
|
920
|
-
} else if (Array.isArray(item)) {
|
|
921
|
-
partTypes.push(`${index}="list"`)
|
|
922
|
-
const encodedItems = item
|
|
923
|
-
.map(subItem => encodeArrayItem(subItem))
|
|
924
|
-
.join(", ")
|
|
925
|
-
fieldLines.push(`${index}: ${encodedItems}`)
|
|
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"`)
|
|
926
1158
|
}
|
|
927
|
-
}
|
|
1159
|
+
})
|
|
928
1160
|
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
console.log("[encode] Reordering for inline array:", {
|
|
932
|
-
bodyKey,
|
|
933
|
-
fieldLines,
|
|
934
|
-
partTypes,
|
|
935
|
-
})
|
|
1161
|
+
const isInline =
|
|
1162
|
+
bodyKey === "body" && headers["inline-body-key"] === "body"
|
|
936
1163
|
|
|
937
|
-
|
|
1164
|
+
if (isInline) {
|
|
938
1165
|
const orderedLines = []
|
|
939
|
-
|
|
940
|
-
// First: field lines
|
|
941
|
-
for (const line of fieldLines) {
|
|
942
|
-
orderedLines.push(line)
|
|
943
|
-
}
|
|
944
|
-
|
|
945
|
-
// Then: ao-types
|
|
946
1166
|
if (partTypes.length > 0) {
|
|
947
1167
|
orderedLines.push(
|
|
948
1168
|
`ao-types: ${partTypes
|
|
@@ -954,18 +1174,13 @@ async function encode(obj = {}) {
|
|
|
954
1174
|
.join(", ")}`
|
|
955
1175
|
)
|
|
956
1176
|
}
|
|
957
|
-
|
|
958
|
-
// Finally: content-disposition (from lines[0])
|
|
959
|
-
orderedLines.push(lines[0])
|
|
1177
|
+
orderedLines.push("content-disposition: inline")
|
|
960
1178
|
orderedLines.push("")
|
|
961
|
-
|
|
962
|
-
console.log("[encode] Ordered lines:", orderedLines)
|
|
963
|
-
|
|
964
1179
|
bodyParts.push(new Blob([orderedLines.join("\r\n")]))
|
|
965
1180
|
} else {
|
|
966
|
-
|
|
1181
|
+
const orderedLines = []
|
|
967
1182
|
if (partTypes.length > 0) {
|
|
968
|
-
|
|
1183
|
+
orderedLines.push(
|
|
969
1184
|
`ao-types: ${partTypes
|
|
970
1185
|
.sort((a, b) => {
|
|
971
1186
|
const aNum = parseInt(a.split("=")[0])
|
|
@@ -975,146 +1190,294 @@ async function encode(obj = {}) {
|
|
|
975
1190
|
.join(", ")}`
|
|
976
1191
|
)
|
|
977
1192
|
}
|
|
1193
|
+
orderedLines.push(`content-disposition: form-data;name="${bodyKey}"`)
|
|
978
1194
|
|
|
979
|
-
|
|
980
|
-
|
|
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")]))
|
|
981
1206
|
}
|
|
1207
|
+
}
|
|
1208
|
+
continue
|
|
1209
|
+
}
|
|
982
1210
|
|
|
983
|
-
|
|
984
|
-
|
|
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
|
+
}
|
|
985
1220
|
}
|
|
986
|
-
}
|
|
987
|
-
// Array of arrays or simple array - use indexed format
|
|
988
|
-
const fieldLines = []
|
|
989
|
-
const partTypes = []
|
|
1221
|
+
})
|
|
990
1222
|
|
|
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
|
+
)
|
|
1229
|
+
|
|
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) {
|
|
991
1235
|
value.forEach((item, idx) => {
|
|
992
1236
|
const index = idx + 1
|
|
993
1237
|
if (Array.isArray(item)) {
|
|
994
|
-
|
|
995
|
-
partTypes.push(`${index}="empty-list"`)
|
|
996
|
-
} else {
|
|
997
|
-
partTypes.push(`${index}="list"`)
|
|
998
|
-
const encodedItems = item
|
|
999
|
-
.map(subItem => encodeArrayItem(subItem))
|
|
1000
|
-
.join(", ")
|
|
1001
|
-
fieldLines.push(`${index}: ${encodedItems}`)
|
|
1002
|
-
}
|
|
1003
|
-
} else if (typeof item === "number") {
|
|
1004
|
-
if (Number.isInteger(item)) {
|
|
1005
|
-
partTypes.push(`${index}="integer"`)
|
|
1006
|
-
fieldLines.push(`${index}: ${item}`)
|
|
1007
|
-
} else {
|
|
1008
|
-
partTypes.push(`${index}="float"`)
|
|
1009
|
-
fieldLines.push(`${index}: ${formatFloat(item)}`)
|
|
1010
|
-
}
|
|
1011
|
-
} else if (typeof item === "string") {
|
|
1012
|
-
if (item.length === 0) {
|
|
1013
|
-
partTypes.push(`${index}="empty-binary"`)
|
|
1014
|
-
}
|
|
1015
|
-
fieldLines.push(`${index}: ${item}`)
|
|
1016
|
-
} else if (
|
|
1017
|
-
item === null ||
|
|
1018
|
-
item === undefined ||
|
|
1019
|
-
typeof item === "symbol" ||
|
|
1020
|
-
typeof item === "boolean"
|
|
1021
|
-
) {
|
|
1022
|
-
partTypes.push(`${index}="atom"`)
|
|
1023
|
-
if (item === null) {
|
|
1024
|
-
fieldLines.push(`${index}: "null"`)
|
|
1025
|
-
} else if (item === undefined) {
|
|
1026
|
-
fieldLines.push(`${index}: "undefined"`)
|
|
1027
|
-
} else if (typeof item === "symbol") {
|
|
1028
|
-
fieldLines.push(
|
|
1029
|
-
`${index}: "${item.description || "Symbol.for()"}"`
|
|
1030
|
-
)
|
|
1031
|
-
} else {
|
|
1032
|
-
fieldLines.push(`${index}: "${item}"`)
|
|
1033
|
-
}
|
|
1034
|
-
} else if (isBytes(item)) {
|
|
1035
|
-
const buffer = toBuffer(item)
|
|
1036
|
-
if (buffer.length === 0) {
|
|
1037
|
-
partTypes.push(`${index}="empty-binary"`)
|
|
1038
|
-
} else {
|
|
1039
|
-
partTypes.push(`${index}="binary"`)
|
|
1040
|
-
}
|
|
1041
|
-
// For indexed format, we also can't include raw bytes inline
|
|
1042
|
-
// This is a limitation of the format
|
|
1238
|
+
partTypes.push(`${index}="list"`)
|
|
1043
1239
|
}
|
|
1044
1240
|
})
|
|
1241
|
+
}
|
|
1045
1242
|
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
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
|
+
)
|
|
1049
1250
|
|
|
1050
|
-
|
|
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
|
+
})
|
|
1051
1261
|
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1262
|
+
const isMixedArray =
|
|
1263
|
+
hasObjects &&
|
|
1264
|
+
(hasEmptyStrings || hasEmptyObjects) &&
|
|
1265
|
+
!hasObjectsWithOnlyEmptyValues
|
|
1056
1266
|
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
`ao-types: ${partTypes
|
|
1061
|
-
.sort((a, b) => {
|
|
1062
|
-
const aNum = parseInt(a.split("=")[0])
|
|
1063
|
-
const bNum = parseInt(b.split("=")[0])
|
|
1064
|
-
return aNum - bNum
|
|
1065
|
-
})
|
|
1066
|
-
.join(", ")}`
|
|
1067
|
-
)
|
|
1068
|
-
}
|
|
1267
|
+
// Process ALL items for type information
|
|
1268
|
+
value.forEach((item, idx) => {
|
|
1269
|
+
const index = idx + 1
|
|
1069
1270
|
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1271
|
+
// Skip type info for elements that have their own parts
|
|
1272
|
+
if (indicesWithOwnParts.has(index)) {
|
|
1273
|
+
return
|
|
1274
|
+
}
|
|
1073
1275
|
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
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
|
|
1284
|
+
}
|
|
1285
|
+
|
|
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}`)
|
|
1089
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
|
+
}
|
|
1398
|
+
|
|
1399
|
+
orderedLines.push("content-disposition: inline")
|
|
1090
1400
|
|
|
1091
|
-
|
|
1401
|
+
if (fieldLines.length > 0) {
|
|
1402
|
+
orderedLines.push("")
|
|
1092
1403
|
for (const line of fieldLines) {
|
|
1093
|
-
|
|
1404
|
+
orderedLines.push(line)
|
|
1094
1405
|
}
|
|
1406
|
+
}
|
|
1407
|
+
|
|
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
|
+
}
|
|
1095
1431
|
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
bodyParts.push(new Blob([
|
|
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")]))
|
|
1099
1437
|
}
|
|
1100
|
-
} else if (!hasObjects && value.length === 0) {
|
|
1101
|
-
// Empty array
|
|
1102
|
-
const fieldName = pathParts[pathParts.length - 1]
|
|
1103
|
-
const partTypes = [`${fieldName}="empty-list"`]
|
|
1104
|
-
lines.unshift(`ao-types: ${partTypes.join(", ")}`)
|
|
1105
|
-
lines.push("")
|
|
1106
|
-
bodyParts.push(new Blob([lines.join("\r\n")]))
|
|
1107
1438
|
}
|
|
1108
|
-
|
|
1109
|
-
|
|
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`)
|
|
1446
|
+
} else {
|
|
1447
|
+
lines.push(`content-disposition: form-data;name="${bodyKey}"`)
|
|
1448
|
+
}
|
|
1449
|
+
|
|
1450
|
+
if (typeof value === "string") {
|
|
1110
1451
|
lines.push("")
|
|
1111
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()"}"`
|
|
1472
|
+
}
|
|
1473
|
+
|
|
1112
1474
|
lines.push("")
|
|
1475
|
+
lines.push(content)
|
|
1113
1476
|
bodyParts.push(new Blob([lines.join("\r\n")]))
|
|
1114
1477
|
}
|
|
1115
1478
|
}
|
|
1116
1479
|
|
|
1117
|
-
//
|
|
1480
|
+
// Add the special data/body part if needed
|
|
1118
1481
|
if (
|
|
1119
1482
|
hasSpecialDataBody &&
|
|
1120
1483
|
obj.data &&
|
|
@@ -1130,13 +1493,11 @@ async function encode(obj = {}) {
|
|
|
1130
1493
|
bodyParts.push(new Blob([specialPart, buffer]))
|
|
1131
1494
|
}
|
|
1132
1495
|
|
|
1133
|
-
// Calculate boundary from content
|
|
1134
1496
|
const partsContent = await Promise.all(bodyParts.map(part => part.text()))
|
|
1135
1497
|
const allContent = partsContent.join("")
|
|
1136
1498
|
const boundaryHash = await sha256(new TextEncoder().encode(allContent))
|
|
1137
1499
|
const boundary = base64url.encode(Buffer.from(boundaryHash))
|
|
1138
1500
|
|
|
1139
|
-
// Assemble final multipart body - NO newlines after each part except the last
|
|
1140
1501
|
const finalParts = []
|
|
1141
1502
|
for (let i = 0; i < bodyParts.length; i++) {
|
|
1142
1503
|
if (i === 0) {
|
|
@@ -1151,46 +1512,17 @@ async function encode(obj = {}) {
|
|
|
1151
1512
|
headers["content-type"] = `multipart/form-data; boundary="${boundary}"`
|
|
1152
1513
|
const body = new Blob(finalParts)
|
|
1153
1514
|
|
|
1154
|
-
// Calculate content digest
|
|
1155
1515
|
const finalContent = await body.arrayBuffer()
|
|
1156
|
-
const contentDigest = await sha256(finalContent)
|
|
1157
|
-
const base64 = base64url.toBase64(base64url.encode(contentDigest))
|
|
1158
|
-
headers["content-digest"] = `sha-256=:${base64}:`
|
|
1159
|
-
headers["content-length"] = String(finalContent.byteLength)
|
|
1160
|
-
|
|
1161
|
-
console.log("[encode] FINAL - headers:", headers, "body:", body)
|
|
1162
|
-
|
|
1163
|
-
// Debug: decode the multipart body to verify structure
|
|
1164
|
-
const bodyText = await body.text()
|
|
1165
|
-
console.log("\n[encode] DEBUG - Full body text:")
|
|
1166
|
-
console.log(bodyText)
|
|
1167
|
-
|
|
1168
|
-
// Parse multipart body
|
|
1169
|
-
const boundaryMatch = headers["content-type"].match(/boundary="([^"]+)"/)
|
|
1170
|
-
if (boundaryMatch) {
|
|
1171
|
-
const debugBoundary = boundaryMatch[1]
|
|
1172
|
-
const parts = bodyText.split(`--${debugBoundary}`)
|
|
1173
|
-
console.log("\n[encode] DEBUG - Multipart parts:")
|
|
1174
|
-
parts.forEach((part, idx) => {
|
|
1175
|
-
console.log(`Part ${idx}:`, JSON.stringify(part))
|
|
1176
|
-
})
|
|
1177
1516
|
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
lines.forEach((line, idx) => {
|
|
1183
|
-
if (line.includes("\u0000")) {
|
|
1184
|
-
console.log(
|
|
1185
|
-
`Line ${idx}: "${line.substring(0, line.indexOf("\u0000"))}" + [${line.length - line.indexOf("\u0000")} bytes]`
|
|
1186
|
-
)
|
|
1187
|
-
} else {
|
|
1188
|
-
console.log(`Line ${idx}: "${line}"`)
|
|
1189
|
-
}
|
|
1190
|
-
})
|
|
1191
|
-
}
|
|
1517
|
+
if (finalContent.byteLength > 0) {
|
|
1518
|
+
const contentDigest = await sha256(finalContent)
|
|
1519
|
+
const base64 = base64url.toBase64(base64url.encode(contentDigest))
|
|
1520
|
+
headers["content-digest"] = `sha-256=:${base64}:`
|
|
1192
1521
|
}
|
|
1193
1522
|
|
|
1523
|
+
headers["content-length"] = String(finalContent.byteLength)
|
|
1524
|
+
|
|
1525
|
+
console.log("=== ENCODE END ===\n")
|
|
1194
1526
|
return { headers, body }
|
|
1195
1527
|
}
|
|
1196
1528
|
|