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