wao 0.27.3 → 0.28.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/cjs/collect-body-keys.js +470 -0
- package/cjs/encode-array-item.js +110 -0
- package/cjs/encode-utils.js +241 -0
- package/cjs/encode.js +1191 -1921
- package/cjs/id.js +234 -0
- package/cjs/send.js +4 -0
- package/cjs/signer-utils.js +1 -0
- package/cjs/signer.js +59 -10
- package/cjs/utils.js +15 -1
- package/esm/collect-body-keys.js +436 -0
- package/esm/encode-array-item.js +112 -0
- package/esm/encode-utils.js +185 -0
- package/esm/encode.js +881 -1531
- package/esm/id.js +222 -0
- package/esm/send.js +4 -3
- package/esm/signer-utils.js +1 -1
- package/esm/signer.js +33 -8
- package/esm/utils.js +7 -0
- package/package.json +1 -1
package/esm/encode.js
CHANGED
|
@@ -1,739 +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) && value.length > 0) {
|
|
343
|
-
hasSimpleFields = true
|
|
344
|
-
} else if (
|
|
345
|
-
typeof value === "string" ||
|
|
346
|
-
typeof value === "number" ||
|
|
347
|
-
typeof value === "boolean" ||
|
|
348
|
-
value === null ||
|
|
349
|
-
value === undefined ||
|
|
350
|
-
typeof value === "symbol"
|
|
351
|
-
) {
|
|
352
|
-
hasSimpleFields = true
|
|
353
|
-
}
|
|
354
|
-
}
|
|
355
|
-
|
|
356
|
-
if (hasSimpleFields) {
|
|
357
|
-
console.log(`[traverse] Adding "${path}" to keys (has simple fields)`)
|
|
358
|
-
keys.push(path)
|
|
359
|
-
} else if (hasArraysWithObjects && path) {
|
|
360
|
-
// If the object only contains arrays with objects, we still need to add it as a body key
|
|
361
|
-
console.log(
|
|
362
|
-
`[traverse] Adding "${path}" to keys (contains arrays with objects)`
|
|
363
|
-
)
|
|
364
|
-
keys.push(path)
|
|
365
|
-
}
|
|
366
|
-
|
|
367
|
-
// Check for arrays with only empty elements that need their own body parts
|
|
368
|
-
for (const [key, value] of Object.entries(current)) {
|
|
369
|
-
const fullPath = path ? `${path}/${key}` : key
|
|
370
|
-
|
|
371
|
-
if (Array.isArray(value) && value.length > 0) {
|
|
372
|
-
const hasOnlyEmptyElements = value.every(
|
|
373
|
-
item =>
|
|
374
|
-
(Array.isArray(item) && item.length === 0) ||
|
|
375
|
-
(isPojo(item) && Object.keys(item).length === 0) ||
|
|
376
|
-
(typeof item === "string" && item === "")
|
|
377
|
-
)
|
|
378
|
-
|
|
379
|
-
if (hasOnlyEmptyElements) {
|
|
380
|
-
console.log(
|
|
381
|
-
`[traverse] Array at ${fullPath} has only empty elements - adding as body key`
|
|
382
|
-
)
|
|
383
|
-
keys.push(fullPath)
|
|
384
|
-
}
|
|
385
|
-
}
|
|
386
|
-
}
|
|
387
|
-
|
|
388
|
-
for (const nestedPath of nestedPaths) {
|
|
389
|
-
const parts = nestedPath.split("/")
|
|
390
|
-
let nestedObj = obj
|
|
391
|
-
|
|
392
|
-
for (const part of parts) {
|
|
393
|
-
if (/^\d+$/.test(part)) {
|
|
394
|
-
nestedObj = nestedObj[parseInt(part) - 1]
|
|
395
|
-
} else {
|
|
396
|
-
nestedObj = nestedObj[part]
|
|
397
|
-
}
|
|
398
|
-
}
|
|
399
|
-
|
|
400
|
-
if (isPojo(nestedObj)) {
|
|
401
|
-
traverse(nestedObj, nestedPath)
|
|
402
|
-
}
|
|
403
|
-
}
|
|
404
|
-
}
|
|
405
|
-
|
|
406
|
-
const objKeys = Object.keys(obj)
|
|
407
|
-
|
|
408
|
-
for (const [key, value] of Object.entries(obj)) {
|
|
409
|
-
console.log(`\n[main loop] Processing key: "${key}"`)
|
|
410
|
-
console.log(
|
|
411
|
-
`[main loop] Value type: ${Array.isArray(value) ? "array" : typeof value}`
|
|
412
|
-
)
|
|
413
|
-
console.log(
|
|
414
|
-
`[main loop] Array length: ${Array.isArray(value) ? value.length : "N/A"}`
|
|
415
|
-
)
|
|
416
|
-
|
|
417
|
-
if (
|
|
418
|
-
(key === "data" || key === "body") &&
|
|
419
|
-
(typeof value === "string" ||
|
|
420
|
-
typeof value === "boolean" ||
|
|
421
|
-
typeof value === "number" ||
|
|
422
|
-
value === null ||
|
|
423
|
-
value === undefined ||
|
|
424
|
-
typeof value === "symbol") &&
|
|
425
|
-
objKeys.length > 1
|
|
426
|
-
) {
|
|
427
|
-
// Special handling: only add to body keys if there's no other data/body field with an object
|
|
428
|
-
if (
|
|
429
|
-
key === "data" &&
|
|
430
|
-
obj.body &&
|
|
431
|
-
isPojo(obj.body) &&
|
|
432
|
-
Object.keys(obj.body).length > 0
|
|
433
|
-
) {
|
|
434
|
-
console.log(`[main loop] Skipping special data field`)
|
|
435
|
-
} else if (
|
|
436
|
-
key === "body" &&
|
|
437
|
-
obj.data &&
|
|
438
|
-
isPojo(obj.data) &&
|
|
439
|
-
Object.keys(obj.data).length > 0
|
|
440
|
-
) {
|
|
441
|
-
console.log(`[main loop] Skipping special body field`)
|
|
442
|
-
} else {
|
|
443
|
-
console.log(`[main loop] Adding special data/body key: "${key}"`)
|
|
444
|
-
keys.push(key)
|
|
445
|
-
}
|
|
446
|
-
} else if (Array.isArray(value)) {
|
|
447
|
-
if (value.length === 0) {
|
|
448
|
-
console.log(`[main loop] SKIPPING empty array for key: "${key}"`)
|
|
449
|
-
continue
|
|
450
|
-
}
|
|
451
|
-
|
|
452
|
-
const hasObjects = value.some(item => isPojo(item))
|
|
453
|
-
const hasArrays = value.some(item => Array.isArray(item))
|
|
454
|
-
const hasNonObjects = value.some(item => !isPojo(item))
|
|
455
|
-
|
|
456
|
-
// Check if this is an array of arrays containing objects
|
|
457
|
-
const hasArraysOfObjects = value.some(
|
|
458
|
-
item => Array.isArray(item) && item.some(subItem => isPojo(subItem))
|
|
459
|
-
)
|
|
460
|
-
|
|
461
|
-
console.log(
|
|
462
|
-
`[main loop] Array analysis: hasObjects=${hasObjects}, hasArrays=${hasArrays}, hasNonObjects=${hasNonObjects}, hasArraysOfObjects=${hasArraysOfObjects}`
|
|
463
|
-
)
|
|
464
|
-
|
|
465
|
-
if (value.length > 0) {
|
|
466
|
-
let bodyPartCounter = 1 // Start counting from 1
|
|
467
|
-
|
|
468
|
-
// Check for special mixed array case
|
|
469
|
-
const hasEmptyStrings = value.some(
|
|
470
|
-
item => typeof item === "string" && item === ""
|
|
471
|
-
)
|
|
472
|
-
const hasEmptyObjects = value.some(
|
|
473
|
-
item => isPojo(item) && Object.keys(item).length === 0
|
|
474
|
-
)
|
|
475
|
-
|
|
476
|
-
// Check for objects that contain only empty values
|
|
477
|
-
const hasObjectsWithOnlyEmptyValues = value.some(item => {
|
|
478
|
-
if (!isPojo(item) || Object.keys(item).length === 0) return false
|
|
479
|
-
return Object.values(item).every(
|
|
480
|
-
v =>
|
|
481
|
-
(typeof v === "string" && v === "") ||
|
|
482
|
-
(Array.isArray(v) && v.length === 0) ||
|
|
483
|
-
(isPojo(v) && Object.keys(v).length === 0)
|
|
484
|
-
)
|
|
485
|
-
})
|
|
486
|
-
|
|
487
|
-
if (hasArraysOfObjects) {
|
|
488
|
-
// Handle arrays of arrays containing objects
|
|
489
|
-
value.forEach((item, index) => {
|
|
490
|
-
if (Array.isArray(item)) {
|
|
491
|
-
item.forEach((subItem, subIndex) => {
|
|
492
|
-
if (isPojo(subItem)) {
|
|
493
|
-
const path = `${key}/${index + 1}/${subIndex + 1}`
|
|
494
|
-
console.log(
|
|
495
|
-
`[main loop] Adding nested object path: "${path}"`
|
|
496
|
-
)
|
|
497
|
-
keys.push(path)
|
|
498
|
-
}
|
|
499
|
-
})
|
|
500
|
-
}
|
|
501
|
-
bodyPartCounter++
|
|
502
|
-
})
|
|
503
|
-
// Always add the main array key
|
|
504
|
-
console.log(`[main loop] ADDING main array key: "${key}"`)
|
|
505
|
-
keys.push(key)
|
|
506
|
-
} else if (
|
|
507
|
-
hasObjects &&
|
|
508
|
-
(hasEmptyStrings || hasEmptyObjects) &&
|
|
509
|
-
!hasObjectsWithOnlyEmptyValues
|
|
510
|
-
) {
|
|
511
|
-
// Special handling: only non-empty objects get parts
|
|
512
|
-
value.forEach((item, index) => {
|
|
513
|
-
if (isPojo(item) && Object.keys(item).length > 0) {
|
|
514
|
-
const path = `${key}/${bodyPartCounter}`
|
|
515
|
-
console.log(
|
|
516
|
-
`[main loop] Adding non-empty object path: "${path}" (array index ${index})`
|
|
517
|
-
)
|
|
518
|
-
keys.push(path)
|
|
519
|
-
// Add paths for nested objects
|
|
520
|
-
for (const [nestedKey, nestedValue] of Object.entries(item)) {
|
|
521
|
-
if (isPojo(nestedValue)) {
|
|
522
|
-
const nestedPath = `${key}/${bodyPartCounter}/${nestedKey}`
|
|
523
|
-
console.log(
|
|
524
|
-
`[main loop] Adding nested object path: "${nestedPath}"`
|
|
525
|
-
)
|
|
526
|
-
keys.push(nestedPath)
|
|
527
|
-
}
|
|
528
|
-
}
|
|
529
|
-
}
|
|
530
|
-
bodyPartCounter++
|
|
531
|
-
})
|
|
532
|
-
// Always add the main array key
|
|
533
|
-
console.log(`[main loop] ADDING main array key: "${key}"`)
|
|
534
|
-
keys.push(key)
|
|
535
|
-
} else if (hasObjects) {
|
|
536
|
-
// Normal handling: all objects get parts (except if parent array has only empty elements)
|
|
537
|
-
let skipEmptyObjects = false
|
|
538
|
-
|
|
539
|
-
// Check if this array contains only empty elements
|
|
540
|
-
const arrayHasOnlyEmptyElements = value.every(
|
|
541
|
-
item =>
|
|
542
|
-
(Array.isArray(item) && item.length === 0) ||
|
|
543
|
-
(isPojo(item) && Object.keys(item).length === 0) ||
|
|
544
|
-
(typeof item === "string" && item === "")
|
|
545
|
-
)
|
|
546
|
-
|
|
547
|
-
if (arrayHasOnlyEmptyElements) {
|
|
548
|
-
skipEmptyObjects = true
|
|
549
|
-
}
|
|
550
|
-
|
|
551
|
-
value.forEach((item, index) => {
|
|
552
|
-
if (isPojo(item)) {
|
|
553
|
-
// Skip empty objects if array has only empty elements
|
|
554
|
-
if (skipEmptyObjects && Object.keys(item).length === 0) {
|
|
555
|
-
bodyPartCounter++
|
|
556
|
-
return
|
|
557
|
-
}
|
|
558
|
-
|
|
559
|
-
const path = `${key}/${bodyPartCounter}`
|
|
560
|
-
console.log(
|
|
561
|
-
`[main loop] Adding object path: "${path}" (array index ${index}, empty=${Object.keys(item).length === 0})`
|
|
562
|
-
)
|
|
563
|
-
keys.push(path)
|
|
564
|
-
// Add paths for nested objects (but not empty ones)
|
|
565
|
-
if (Object.keys(item).length > 0) {
|
|
566
|
-
for (const [nestedKey, nestedValue] of Object.entries(item)) {
|
|
567
|
-
if (
|
|
568
|
-
isPojo(nestedValue) &&
|
|
569
|
-
Object.keys(nestedValue).length > 0
|
|
570
|
-
) {
|
|
571
|
-
const nestedPath = `${key}/${bodyPartCounter}/${nestedKey}`
|
|
572
|
-
console.log(
|
|
573
|
-
`[main loop] Adding nested object path: "${nestedPath}"`
|
|
574
|
-
)
|
|
575
|
-
keys.push(nestedPath)
|
|
576
|
-
}
|
|
577
|
-
}
|
|
578
|
-
}
|
|
579
|
-
} else if (typeof item === "string" && item === "") {
|
|
580
|
-
// Empty strings may get parts in some formats
|
|
581
|
-
const path = `${key}/${bodyPartCounter}`
|
|
582
|
-
console.log(
|
|
583
|
-
`[main loop] Adding empty string path: "${path}" (array index ${index})`
|
|
584
|
-
)
|
|
585
|
-
keys.push(path)
|
|
586
|
-
}
|
|
587
|
-
bodyPartCounter++
|
|
588
|
-
})
|
|
589
|
-
// Don't add main array key for arrays with only objects containing empty values
|
|
590
|
-
if (
|
|
591
|
-
!hasObjectsWithOnlyEmptyValues ||
|
|
592
|
-
value.some(item => !isPojo(item))
|
|
593
|
-
) {
|
|
594
|
-
// Check if array has only empty elements
|
|
595
|
-
const hasOnlyEmptyElements = value.every(
|
|
596
|
-
item =>
|
|
597
|
-
(Array.isArray(item) && item.length === 0) ||
|
|
598
|
-
(isPojo(item) && Object.keys(item).length === 0) ||
|
|
599
|
-
(typeof item === "string" && item === "")
|
|
600
|
-
)
|
|
601
|
-
|
|
602
|
-
if (!hasOnlyEmptyElements) {
|
|
603
|
-
// Always add the main array key
|
|
604
|
-
console.log(`[main loop] ADDING main array key: "${key}"`)
|
|
605
|
-
keys.push(key)
|
|
606
|
-
}
|
|
607
|
-
}
|
|
608
|
-
} else {
|
|
609
|
-
// Check if array has only empty elements
|
|
610
|
-
const hasOnlyEmptyArraysOrObjects = value.every(
|
|
611
|
-
item =>
|
|
612
|
-
(Array.isArray(item) && item.length === 0) ||
|
|
613
|
-
(isPojo(item) && Object.keys(item).length === 0) ||
|
|
614
|
-
(typeof item === "string" && item === "")
|
|
615
|
-
)
|
|
616
|
-
|
|
617
|
-
if (hasOnlyEmptyArraysOrObjects && value.length > 0) {
|
|
618
|
-
// Always add the main array key for arrays with only empty elements
|
|
619
|
-
console.log(
|
|
620
|
-
`[main loop] ADDING main array key for empty elements: "${key}"`
|
|
621
|
-
)
|
|
622
|
-
keys.push(key)
|
|
623
|
-
} else if (!hasOnlyEmptyArraysOrObjects) {
|
|
624
|
-
// Always add the main array key
|
|
625
|
-
console.log(`[main loop] ADDING main array key: "${key}"`)
|
|
626
|
-
keys.push(key)
|
|
627
|
-
}
|
|
628
|
-
}
|
|
629
|
-
}
|
|
630
|
-
} else if (isPojo(value)) {
|
|
631
|
-
console.log(`[main loop] Traversing object at key: "${key}"`)
|
|
632
|
-
// Check if this object contains arrays with only empty elements
|
|
633
|
-
let hasArraysWithOnlyEmptyElements = false
|
|
634
|
-
for (const [k, v] of Object.entries(value)) {
|
|
635
|
-
if (
|
|
636
|
-
Array.isArray(v) &&
|
|
637
|
-
v.length > 0 &&
|
|
638
|
-
v.every(
|
|
639
|
-
item =>
|
|
640
|
-
(Array.isArray(item) && item.length === 0) ||
|
|
641
|
-
(isPojo(item) && Object.keys(item).length === 0) ||
|
|
642
|
-
(typeof item === "string" && item === "")
|
|
643
|
-
)
|
|
644
|
-
) {
|
|
645
|
-
hasArraysWithOnlyEmptyElements = true
|
|
646
|
-
keys.push(`${key}/${k}`)
|
|
647
|
-
}
|
|
648
|
-
}
|
|
649
|
-
traverse(value, key)
|
|
650
|
-
} else if (isBytes(value) && value.length > 0) {
|
|
651
|
-
console.log(`[main loop] Adding key for non-empty bytes: "${key}"`)
|
|
652
|
-
keys.push(key)
|
|
653
|
-
} else if (typeof value === "string" && value.includes("\n")) {
|
|
654
|
-
console.log(`[main loop] Adding key for string with newline: "${key}"`)
|
|
655
|
-
keys.push(key)
|
|
656
|
-
} else if (typeof value === "string" && hasNonAscii(value)) {
|
|
657
|
-
console.log(`[main loop] Adding key for non-ASCII string: "${key}"`)
|
|
658
|
-
keys.push(key)
|
|
659
|
-
} else {
|
|
660
|
-
console.log(`[main loop] Skipping key: "${key}" (no match)`)
|
|
661
|
-
}
|
|
662
|
-
}
|
|
663
|
-
|
|
664
|
-
const result = [...new Set(keys)].filter(k => {
|
|
665
|
-
if (k === "") return false
|
|
666
|
-
|
|
667
|
-
// Check if this is a path to an empty object inside an array with only empty elements
|
|
668
|
-
const parts = k.split("/")
|
|
669
|
-
if (parts.length >= 2 && /^\d+$/.test(parts[parts.length - 1])) {
|
|
670
|
-
// This is an array element path like "maps/1"
|
|
671
|
-
const arrayPath = parts.slice(0, -1).join("/")
|
|
672
|
-
let arrayValue = obj
|
|
673
|
-
|
|
674
|
-
// Navigate to the array
|
|
675
|
-
for (const part of parts.slice(0, -1)) {
|
|
676
|
-
if (/^\d+$/.test(part)) {
|
|
677
|
-
arrayValue = arrayValue[parseInt(part) - 1]
|
|
678
|
-
} else {
|
|
679
|
-
arrayValue = arrayValue[part]
|
|
680
|
-
}
|
|
681
|
-
}
|
|
682
|
-
|
|
683
|
-
// Check if this array contains only empty elements
|
|
684
|
-
if (Array.isArray(arrayValue)) {
|
|
685
|
-
const hasOnlyEmptyElements = arrayValue.every(
|
|
686
|
-
item =>
|
|
687
|
-
(Array.isArray(item) && item.length === 0) ||
|
|
688
|
-
(isPojo(item) && Object.keys(item).length === 0) ||
|
|
689
|
-
(typeof item === "string" && item === "")
|
|
690
|
-
)
|
|
691
|
-
|
|
692
|
-
if (hasOnlyEmptyElements) {
|
|
693
|
-
// Filter out paths to individual empty elements
|
|
694
|
-
console.log(`[filter] Removing path to empty element: "${k}"`)
|
|
695
|
-
return false
|
|
696
|
-
}
|
|
697
|
-
}
|
|
698
|
-
}
|
|
699
|
-
|
|
700
|
-
return true
|
|
701
|
-
})
|
|
702
|
-
console.log("\n=== collectBodyKeys RESULT ===")
|
|
703
|
-
console.log("Final bodyKeys:", JSON.stringify(result))
|
|
704
|
-
console.log("=== collectBodyKeys END ===\n")
|
|
705
|
-
|
|
706
|
-
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
|
|
707
25
|
}
|
|
708
26
|
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
console.log("Encoding object:", JSON.stringify(obj))
|
|
712
|
-
|
|
713
|
-
const processValue = value => {
|
|
714
|
-
if (typeof value === "symbol") {
|
|
715
|
-
return value.description || "Symbol.for()"
|
|
716
|
-
} else if (Array.isArray(value)) {
|
|
717
|
-
return value.map(processValue)
|
|
718
|
-
} else if (isPojo(value)) {
|
|
719
|
-
const result = {}
|
|
720
|
-
for (const [k, v] of Object.entries(value)) {
|
|
721
|
-
result[k] = processValue(v)
|
|
722
|
-
}
|
|
723
|
-
return result
|
|
724
|
-
}
|
|
725
|
-
return value
|
|
726
|
-
}
|
|
727
|
-
|
|
728
|
-
const processedObj = {}
|
|
729
|
-
for (const [k, v] of Object.entries(obj)) {
|
|
730
|
-
processedObj[k] = processValue(v)
|
|
731
|
-
}
|
|
732
|
-
|
|
27
|
+
// Step 2: Handle empty object case
|
|
28
|
+
function handleEmptyObject(obj) {
|
|
733
29
|
if (Object.keys(obj).length === 0) {
|
|
734
30
|
return { headers: {}, body: undefined }
|
|
735
31
|
}
|
|
32
|
+
return null
|
|
33
|
+
}
|
|
736
34
|
|
|
35
|
+
// Step 3: Handle single field with empty binary
|
|
36
|
+
function handleSingleEmptyBinaryField(obj) {
|
|
737
37
|
const objKeys = Object.keys(obj)
|
|
738
38
|
|
|
739
39
|
if (objKeys.length === 1) {
|
|
@@ -750,14 +50,11 @@ async function encode(obj = {}) {
|
|
|
750
50
|
}
|
|
751
51
|
}
|
|
752
52
|
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
isBytes(obj.body) &&
|
|
756
|
-
(obj.body.length === 0 || obj.body.byteLength === 0) &&
|
|
757
|
-
objKeys.length > 1
|
|
758
|
-
) {
|
|
759
|
-
}
|
|
53
|
+
return null
|
|
54
|
+
}
|
|
760
55
|
|
|
56
|
+
// Step 4: Handle single field with binary data
|
|
57
|
+
async function handleSingleBinaryField(obj) {
|
|
761
58
|
const hasBodyBinary = obj.body && isBytes(obj.body)
|
|
762
59
|
const otherFields = Object.keys(obj).filter(k => k !== "body")
|
|
763
60
|
|
|
@@ -776,28 +73,18 @@ async function encode(obj = {}) {
|
|
|
776
73
|
return { headers, body: obj.body }
|
|
777
74
|
}
|
|
778
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
|
+
|
|
779
83
|
if (objKeys.length === 1) {
|
|
780
84
|
const fieldName = objKeys[0]
|
|
781
85
|
const fieldValue = obj[fieldName]
|
|
782
86
|
|
|
783
|
-
if (
|
|
784
|
-
const headers = {}
|
|
785
|
-
const bodyBuffer = toBuffer(fieldValue)
|
|
786
|
-
const bodyArrayBuffer = bodyBuffer.buffer.slice(
|
|
787
|
-
bodyBuffer.byteOffset,
|
|
788
|
-
bodyBuffer.byteOffset + bodyBuffer.byteLength
|
|
789
|
-
)
|
|
790
|
-
|
|
791
|
-
const contentDigest = await sha256(bodyArrayBuffer)
|
|
792
|
-
const base64 = base64url.toBase64(base64url.encode(contentDigest))
|
|
793
|
-
headers["content-digest"] = `sha-256=:${base64}:`
|
|
794
|
-
|
|
795
|
-
if (fieldName !== "body") {
|
|
796
|
-
headers["inline-body-key"] = fieldName
|
|
797
|
-
}
|
|
798
|
-
|
|
799
|
-
return { headers, body: fieldValue }
|
|
800
|
-
} else if (
|
|
87
|
+
if (
|
|
801
88
|
(fieldName === "data" || fieldName === "body") &&
|
|
802
89
|
(typeof fieldValue === "string" ||
|
|
803
90
|
typeof fieldValue === "boolean" ||
|
|
@@ -807,21 +94,7 @@ async function encode(obj = {}) {
|
|
|
807
94
|
typeof fieldValue === "symbol")
|
|
808
95
|
) {
|
|
809
96
|
const headers = {}
|
|
810
|
-
|
|
811
|
-
let bodyContent
|
|
812
|
-
if (typeof fieldValue === "string") {
|
|
813
|
-
bodyContent = fieldValue
|
|
814
|
-
} else if (typeof fieldValue === "boolean") {
|
|
815
|
-
bodyContent = `"${fieldValue}"`
|
|
816
|
-
} else if (typeof fieldValue === "number") {
|
|
817
|
-
bodyContent = String(fieldValue)
|
|
818
|
-
} else if (fieldValue === null) {
|
|
819
|
-
bodyContent = '"null"'
|
|
820
|
-
} else if (fieldValue === undefined) {
|
|
821
|
-
bodyContent = '"undefined"'
|
|
822
|
-
} else if (typeof fieldValue === "symbol") {
|
|
823
|
-
bodyContent = `"${fieldValue.description || "Symbol.for()"}"`
|
|
824
|
-
}
|
|
97
|
+
const bodyContent = encodePrimitiveContent(fieldValue)
|
|
825
98
|
|
|
826
99
|
const encoder = new TextEncoder()
|
|
827
100
|
const encoded = encoder.encode(bodyContent)
|
|
@@ -829,16 +102,9 @@ async function encode(obj = {}) {
|
|
|
829
102
|
const base64 = base64url.toBase64(base64url.encode(contentDigest))
|
|
830
103
|
headers["content-digest"] = `sha-256=:${base64}:`
|
|
831
104
|
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
fieldValue === undefined ||
|
|
836
|
-
typeof fieldValue === "symbol"
|
|
837
|
-
) {
|
|
838
|
-
headers["ao-types"] = `${fieldName.toLowerCase()}="atom"`
|
|
839
|
-
} else if (typeof fieldValue === "number") {
|
|
840
|
-
headers["ao-types"] =
|
|
841
|
-
`${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}"`
|
|
842
108
|
}
|
|
843
109
|
|
|
844
110
|
if (fieldName !== "body") {
|
|
@@ -846,7 +112,52 @@ async function encode(obj = {}) {
|
|
|
846
112
|
}
|
|
847
113
|
|
|
848
114
|
return { headers, body: bodyContent }
|
|
849
|
-
}
|
|
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)) {
|
|
850
161
|
const headers = {}
|
|
851
162
|
const encoder = new TextEncoder()
|
|
852
163
|
const encoded = encoder.encode(fieldValue)
|
|
@@ -862,11 +173,16 @@ async function encode(obj = {}) {
|
|
|
862
173
|
}
|
|
863
174
|
}
|
|
864
175
|
|
|
865
|
-
|
|
866
|
-
|
|
176
|
+
return null
|
|
177
|
+
}
|
|
867
178
|
|
|
868
|
-
|
|
179
|
+
// Step 7: Collect all keys that need to go in body
|
|
180
|
+
function collectBodyKeysStep(obj) {
|
|
181
|
+
return collectBodyKeys(obj)
|
|
182
|
+
}
|
|
869
183
|
|
|
184
|
+
// Step 8: Process fields that can go in headers
|
|
185
|
+
function processHeaderFields(obj, bodyKeys, headers, headerTypes) {
|
|
870
186
|
for (const [key, value] of Object.entries(obj)) {
|
|
871
187
|
const needsBody =
|
|
872
188
|
bodyKeys.includes(key) || bodyKeys.some(k => k.startsWith(`${key}/`))
|
|
@@ -919,29 +235,15 @@ async function encode(obj = {}) {
|
|
|
919
235
|
headerTypes.push(`${key.toLowerCase()}="empty-message"`)
|
|
920
236
|
}
|
|
921
237
|
} else {
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
headerTypes.push(`${key.toLowerCase()}="
|
|
926
|
-
} else if (Array.isArray(value) && value.length === 0) {
|
|
927
|
-
headerTypes.push(`${key.toLowerCase()}="empty-list"`)
|
|
928
|
-
} else if (isPojo(value) && Object.keys(value).length === 0) {
|
|
929
|
-
headerTypes.push(`${key.toLowerCase()}="empty-message"`)
|
|
930
|
-
} else if (
|
|
931
|
-
typeof value === "boolean" ||
|
|
932
|
-
value === null ||
|
|
933
|
-
value === undefined ||
|
|
934
|
-
typeof value === "symbol"
|
|
935
|
-
) {
|
|
936
|
-
headerTypes.push(`${key.toLowerCase()}="atom"`)
|
|
937
|
-
} else if (typeof value === "number") {
|
|
938
|
-
headerTypes.push(
|
|
939
|
-
`${key.toLowerCase()}="${Number.isInteger(value) ? "integer" : "float"}"`
|
|
940
|
-
)
|
|
238
|
+
// Fields that need body still get type annotations
|
|
239
|
+
const aoType = getAoType(value)
|
|
240
|
+
if (aoType) {
|
|
241
|
+
headerTypes.push(`${key.toLowerCase()}="${aoType}"`)
|
|
941
242
|
}
|
|
942
243
|
}
|
|
943
244
|
}
|
|
944
245
|
|
|
246
|
+
// Second pass for array type annotations
|
|
945
247
|
for (const [key, value] of Object.entries(obj)) {
|
|
946
248
|
if (Array.isArray(value)) {
|
|
947
249
|
if (
|
|
@@ -954,9 +256,11 @@ async function encode(obj = {}) {
|
|
|
954
256
|
}
|
|
955
257
|
}
|
|
956
258
|
}
|
|
259
|
+
}
|
|
957
260
|
|
|
261
|
+
// Step 9: Handle case where all body keys are empty binaries
|
|
262
|
+
function handleAllEmptyBinaryBodyKeys(obj, bodyKeys, headers, headerTypes) {
|
|
958
263
|
if (bodyKeys.length === 0) {
|
|
959
|
-
console.log("No bodyKeys, returning headers only")
|
|
960
264
|
if (headerTypes.length > 0) {
|
|
961
265
|
headers["ao-types"] = headerTypes.sort().join(", ")
|
|
962
266
|
}
|
|
@@ -964,19 +268,7 @@ async function encode(obj = {}) {
|
|
|
964
268
|
}
|
|
965
269
|
|
|
966
270
|
const allBodyKeysAreEmptyBinaries = bodyKeys.every(key => {
|
|
967
|
-
const
|
|
968
|
-
let value = obj
|
|
969
|
-
for (const part of pathParts) {
|
|
970
|
-
if (/^\d+$/.test(part)) {
|
|
971
|
-
const index = parseInt(part) - 1
|
|
972
|
-
console.log(
|
|
973
|
-
`[Body part] Getting array element at index ${index} from part ${part}`
|
|
974
|
-
)
|
|
975
|
-
value = value[index]
|
|
976
|
-
} else {
|
|
977
|
-
value = value[part]
|
|
978
|
-
}
|
|
979
|
-
}
|
|
271
|
+
const value = getValueByPath(obj, key)
|
|
980
272
|
return isBytes(value) && (value.length === 0 || value.byteLength === 0)
|
|
981
273
|
})
|
|
982
274
|
|
|
@@ -987,36 +279,43 @@ async function encode(obj = {}) {
|
|
|
987
279
|
return { headers, body: undefined }
|
|
988
280
|
}
|
|
989
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
|
+
) {
|
|
990
292
|
if (bodyKeys.length === 1) {
|
|
991
293
|
const singleKey = bodyKeys[0]
|
|
992
|
-
const
|
|
993
|
-
|
|
994
|
-
for
|
|
995
|
-
|
|
996
|
-
|
|
294
|
+
const value = getValueByPath(obj, singleKey)
|
|
295
|
+
|
|
296
|
+
// Apply optimization for binary data OR strings with newlines
|
|
297
|
+
if (
|
|
298
|
+
(isBytes(value) && value.length > 0) ||
|
|
299
|
+
(typeof value === "string" && value.includes("\n"))
|
|
300
|
+
) {
|
|
301
|
+
let contentToHash
|
|
302
|
+
let bodyContent = value
|
|
303
|
+
|
|
304
|
+
if (isBytes(value)) {
|
|
305
|
+
const bodyBuffer = toBuffer(value)
|
|
306
|
+
contentToHash = bodyBuffer.buffer.slice(
|
|
307
|
+
bodyBuffer.byteOffset,
|
|
308
|
+
bodyBuffer.byteOffset + bodyBuffer.byteLength
|
|
309
|
+
)
|
|
997
310
|
} else {
|
|
998
|
-
|
|
311
|
+
// For strings, encode to UTF-8 for hashing
|
|
312
|
+
const encoder = new TextEncoder()
|
|
313
|
+
const encoded = encoder.encode(value)
|
|
314
|
+
contentToHash = encoded.buffer
|
|
315
|
+
bodyContent = value
|
|
999
316
|
}
|
|
1000
|
-
}
|
|
1001
317
|
|
|
1002
|
-
|
|
1003
|
-
if (key === singleKey) return true
|
|
1004
|
-
return (
|
|
1005
|
-
(Array.isArray(val) && val.length === 0) ||
|
|
1006
|
-
(isPojo(val) && Object.keys(val).length === 0) ||
|
|
1007
|
-
(isBytes(val) && (val.length === 0 || val.byteLength === 0)) ||
|
|
1008
|
-
(typeof val === "string" && val.length === 0)
|
|
1009
|
-
)
|
|
1010
|
-
})
|
|
1011
|
-
|
|
1012
|
-
if (otherFieldsAreEmpty && isBytes(value) && value.length > 0) {
|
|
1013
|
-
const bodyBuffer = toBuffer(value)
|
|
1014
|
-
const bodyArrayBuffer = bodyBuffer.buffer.slice(
|
|
1015
|
-
bodyBuffer.byteOffset,
|
|
1016
|
-
bodyBuffer.byteOffset + bodyBuffer.byteLength
|
|
1017
|
-
)
|
|
1018
|
-
|
|
1019
|
-
const contentDigest = await sha256(bodyArrayBuffer)
|
|
318
|
+
const contentDigest = await sha256(contentToHash)
|
|
1020
319
|
const base64 = base64url.toBase64(base64url.encode(contentDigest))
|
|
1021
320
|
headers["content-digest"] = `sha-256=:${base64}:`
|
|
1022
321
|
|
|
@@ -1028,27 +327,27 @@ async function encode(obj = {}) {
|
|
|
1028
327
|
headers["ao-types"] = headerTypes.sort().join(", ")
|
|
1029
328
|
}
|
|
1030
329
|
|
|
1031
|
-
return { headers, body:
|
|
330
|
+
return { headers, body: bodyContent }
|
|
1032
331
|
}
|
|
1033
332
|
}
|
|
1034
333
|
|
|
1035
|
-
|
|
1036
|
-
|
|
334
|
+
return null
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
// Step 11: Sort body keys
|
|
338
|
+
function sortBodyKeys(bodyKeys) {
|
|
339
|
+
return bodyKeys.sort((a, b) => {
|
|
1037
340
|
const aIsArrayElement = /\/\d+$/.test(a)
|
|
1038
341
|
const bIsArrayElement = /\/\d+$/.test(b)
|
|
1039
342
|
const aBase = a.split("/")[0]
|
|
1040
343
|
const bBase = b.split("/")[0]
|
|
1041
|
-
|
|
1042
|
-
// If both are for the same array
|
|
1043
344
|
if (aBase === bBase) {
|
|
1044
|
-
// Main array comes before element parts
|
|
1045
345
|
if (!aIsArrayElement && bIsArrayElement) {
|
|
1046
|
-
return -1
|
|
346
|
+
return -1
|
|
1047
347
|
}
|
|
1048
348
|
if (aIsArrayElement && !bIsArrayElement) {
|
|
1049
|
-
return 1
|
|
349
|
+
return 1
|
|
1050
350
|
}
|
|
1051
|
-
// Both are elements - sort by index
|
|
1052
351
|
if (aIsArrayElement && bIsArrayElement) {
|
|
1053
352
|
const aIndex = parseInt(a.split("/")[1])
|
|
1054
353
|
const bIndex = parseInt(b.split("/")[1])
|
|
@@ -1056,13 +355,13 @@ async function encode(obj = {}) {
|
|
|
1056
355
|
}
|
|
1057
356
|
return a.localeCompare(b)
|
|
1058
357
|
}
|
|
1059
|
-
|
|
1060
|
-
// Different arrays, sort by base name
|
|
1061
358
|
return a.localeCompare(b)
|
|
1062
359
|
})
|
|
360
|
+
}
|
|
1063
361
|
|
|
1064
|
-
|
|
1065
|
-
|
|
362
|
+
// Step 12: Check for special data/body case
|
|
363
|
+
function checkSpecialDataBodyCase(obj, sortedBodyKeys) {
|
|
364
|
+
return (
|
|
1066
365
|
sortedBodyKeys.includes("data") &&
|
|
1067
366
|
sortedBodyKeys.includes("body") &&
|
|
1068
367
|
obj.data &&
|
|
@@ -1071,771 +370,698 @@ async function encode(obj = {}) {
|
|
|
1071
370
|
obj.body &&
|
|
1072
371
|
obj.body.data &&
|
|
1073
372
|
isBytes(obj.body.data)
|
|
373
|
+
)
|
|
374
|
+
}
|
|
1074
375
|
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
if (
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
376
|
+
// Step 13.2.2: Handle empty string in nested path
|
|
377
|
+
function handleEmptyStringInNestedPath(bodyKey, value, pathParts) {
|
|
378
|
+
if (typeof value === "string" && value === "" && pathParts.length > 1) {
|
|
379
|
+
const lines = []
|
|
380
|
+
lines.push(`content-disposition: form-data;name="${bodyKey}"`)
|
|
381
|
+
lines.push("")
|
|
382
|
+
lines.push("")
|
|
383
|
+
return new Blob([lines.join("\r\n")])
|
|
1081
384
|
}
|
|
385
|
+
return null
|
|
386
|
+
}
|
|
1082
387
|
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
388
|
+
// Step 13.2.3.2: Handle arrays with only empty elements
|
|
389
|
+
function handleArrayWithOnlyEmptyElements(
|
|
390
|
+
bodyKey,
|
|
391
|
+
value,
|
|
392
|
+
headers,
|
|
393
|
+
sortedBodyKeys
|
|
394
|
+
) {
|
|
395
|
+
const fieldLines = []
|
|
396
|
+
const partTypes = []
|
|
397
|
+
|
|
398
|
+
value.forEach((item, idx) => {
|
|
399
|
+
const index = idx + 1
|
|
400
|
+
const itemType = getAoType(item)
|
|
401
|
+
if (itemType) {
|
|
402
|
+
partTypes.push(`${index}="${itemType}"`)
|
|
403
|
+
}
|
|
404
|
+
})
|
|
1086
405
|
|
|
1087
|
-
const
|
|
406
|
+
const isInline = bodyKey === "body" && headers["inline-body-key"] === "body"
|
|
1088
407
|
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
408
|
+
if (isInline) {
|
|
409
|
+
const orderedLines = []
|
|
410
|
+
if (partTypes.length > 0) {
|
|
411
|
+
orderedLines.push(
|
|
412
|
+
`ao-types: ${sortTypeAnnotations(partTypes).join(", ")}`
|
|
413
|
+
)
|
|
414
|
+
}
|
|
415
|
+
orderedLines.push("content-disposition: inline")
|
|
416
|
+
orderedLines.push("")
|
|
417
|
+
return new Blob([orderedLines.join("\r\n")])
|
|
418
|
+
} else {
|
|
419
|
+
const orderedLines = []
|
|
420
|
+
if (partTypes.length > 0) {
|
|
421
|
+
orderedLines.push(
|
|
422
|
+
`ao-types: ${sortTypeAnnotations(partTypes).join(", ")}`
|
|
423
|
+
)
|
|
424
|
+
}
|
|
425
|
+
orderedLines.push(`content-disposition: form-data;name="${bodyKey}"`)
|
|
1092
426
|
|
|
1093
|
-
const
|
|
1094
|
-
|
|
1095
|
-
|
|
427
|
+
const isLastBodyPart =
|
|
428
|
+
sortedBodyKeys.indexOf(bodyKey) === sortedBodyKeys.length - 1
|
|
429
|
+
const hasOnlyTypes = partTypes.length > 0 && fieldLines.length === 0
|
|
1096
430
|
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
431
|
+
if (isLastBodyPart && hasOnlyTypes) {
|
|
432
|
+
return new Blob([orderedLines.join("\r\n")])
|
|
433
|
+
} else {
|
|
434
|
+
orderedLines.push("")
|
|
435
|
+
return new Blob([orderedLines.join("\r\n")])
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
}
|
|
1100
439
|
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
440
|
+
// Step 13.2.3.3: Build indices with own parts
|
|
441
|
+
function buildIndicesWithOwnParts(bodyKey, sortedBodyKeys) {
|
|
442
|
+
const indicesWithOwnParts = new Set()
|
|
443
|
+
sortedBodyKeys.forEach(key => {
|
|
444
|
+
if (key.startsWith(bodyKey + "/")) {
|
|
445
|
+
const subPath = key.substring(bodyKey.length + 1)
|
|
446
|
+
const match = subPath.match(/^(\d+)/)
|
|
447
|
+
if (match) {
|
|
448
|
+
indicesWithOwnParts.add(parseInt(match[1]))
|
|
1105
449
|
}
|
|
1106
450
|
}
|
|
451
|
+
})
|
|
452
|
+
return indicesWithOwnParts
|
|
453
|
+
}
|
|
1107
454
|
|
|
1108
|
-
|
|
455
|
+
// Step 13.2.3.4: Process array items
|
|
456
|
+
function processArrayItems(
|
|
457
|
+
value,
|
|
458
|
+
indicesWithOwnParts,
|
|
459
|
+
hasNestedObjectParts,
|
|
460
|
+
pathParts
|
|
461
|
+
) {
|
|
462
|
+
const fieldLines = []
|
|
463
|
+
const partTypes = []
|
|
464
|
+
|
|
465
|
+
if (hasNestedObjectParts) {
|
|
466
|
+
value.forEach((item, idx) => {
|
|
467
|
+
const index = idx + 1
|
|
468
|
+
if (Array.isArray(item)) {
|
|
469
|
+
partTypes.push(`${index}="list"`)
|
|
470
|
+
}
|
|
471
|
+
})
|
|
472
|
+
}
|
|
1109
473
|
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
474
|
+
value.forEach((item, idx) => {
|
|
475
|
+
const index = idx + 1
|
|
476
|
+
|
|
477
|
+
if (indicesWithOwnParts.has(index)) {
|
|
478
|
+
return
|
|
479
|
+
}
|
|
480
|
+
if (
|
|
481
|
+
hasNestedObjectParts &&
|
|
482
|
+
Array.isArray(item) &&
|
|
483
|
+
item.some(subItem => isPojo(subItem))
|
|
484
|
+
) {
|
|
485
|
+
return
|
|
1118
486
|
}
|
|
1119
487
|
|
|
1120
|
-
if (
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
)
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
488
|
+
if (typeof item === "string" && item === "") {
|
|
489
|
+
partTypes.push(`${index}="empty-binary"`)
|
|
490
|
+
} else if (isPojo(item) && Object.keys(item).length === 0) {
|
|
491
|
+
partTypes.push(`${index}="empty-message"`)
|
|
492
|
+
} else if (isPojo(item)) {
|
|
493
|
+
// Non-empty objects are handled elsewhere
|
|
494
|
+
} else if (Array.isArray(item)) {
|
|
495
|
+
if (item.length === 0) {
|
|
496
|
+
partTypes.push(`${index}="empty-list"`)
|
|
497
|
+
} else {
|
|
498
|
+
partTypes.push(`${index}="list"`)
|
|
499
|
+
const encodedItems = item
|
|
500
|
+
.map(subItem => {
|
|
501
|
+
if (typeof subItem === "number") {
|
|
502
|
+
if (Number.isInteger(subItem)) {
|
|
503
|
+
return `"(ao-type-integer) ${subItem}"`
|
|
504
|
+
} else {
|
|
505
|
+
return `"(ao-type-float) ${formatFloat(subItem)}"`
|
|
506
|
+
}
|
|
507
|
+
} else if (typeof subItem === "string") {
|
|
508
|
+
return `"${subItem}"`
|
|
509
|
+
} else if (subItem === null) {
|
|
510
|
+
return `"(ao-type-atom) \\"null\\""`
|
|
511
|
+
} else if (subItem === undefined) {
|
|
512
|
+
return `"(ao-type-atom) \\"undefined\\""`
|
|
513
|
+
} else if (typeof subItem === "symbol") {
|
|
514
|
+
const desc = subItem.description || "Symbol.for()"
|
|
515
|
+
return `"(ao-type-atom) \\"${desc}\\""`
|
|
516
|
+
} else if (typeof subItem === "boolean") {
|
|
517
|
+
return `"(ao-type-atom) \\"${subItem}\\""`
|
|
518
|
+
} else if (Array.isArray(subItem)) {
|
|
519
|
+
return encodeArrayItem(subItem)
|
|
520
|
+
} else if (isBytes(subItem)) {
|
|
521
|
+
const buffer = toBuffer(subItem)
|
|
522
|
+
if (buffer.length === 0 || buffer.byteLength === 0) {
|
|
523
|
+
return `""`
|
|
524
|
+
}
|
|
525
|
+
return `"(ao-type-binary)"`
|
|
526
|
+
} else if (isPojo(subItem)) {
|
|
527
|
+
const json = JSON.stringify(subItem)
|
|
528
|
+
const escaped = json.replace(/\\/g, "\\\\").replace(/"/g, '\\"')
|
|
529
|
+
return `"(ao-type-map) ${escaped}"`
|
|
530
|
+
} else {
|
|
531
|
+
return `"${String(subItem)}"`
|
|
532
|
+
}
|
|
533
|
+
})
|
|
534
|
+
.join(", ")
|
|
535
|
+
fieldLines.push(`${index}: ${encodedItems}`)
|
|
1143
536
|
}
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
value.forEach((item, idx) => {
|
|
1152
|
-
const index = idx + 1
|
|
1153
|
-
if (Array.isArray(item) && item.length === 0) {
|
|
1154
|
-
partTypes.push(`${index}="empty-list"`)
|
|
1155
|
-
} else if (isPojo(item) && Object.keys(item).length === 0) {
|
|
1156
|
-
partTypes.push(`${index}="empty-message"`)
|
|
1157
|
-
} else if (typeof item === "string" && item === "") {
|
|
1158
|
-
partTypes.push(`${index}="empty-binary"`)
|
|
1159
|
-
}
|
|
1160
|
-
})
|
|
1161
|
-
|
|
1162
|
-
const isInline =
|
|
1163
|
-
bodyKey === "body" && headers["inline-body-key"] === "body"
|
|
1164
|
-
|
|
1165
|
-
if (isInline) {
|
|
1166
|
-
const orderedLines = []
|
|
1167
|
-
if (partTypes.length > 0) {
|
|
1168
|
-
orderedLines.push(
|
|
1169
|
-
`ao-types: ${partTypes
|
|
1170
|
-
.sort((a, b) => {
|
|
1171
|
-
const aNum = parseInt(a.split("=")[0])
|
|
1172
|
-
const bNum = parseInt(b.split("=")[0])
|
|
1173
|
-
return aNum - bNum
|
|
1174
|
-
})
|
|
1175
|
-
.join(", ")}`
|
|
1176
|
-
)
|
|
1177
|
-
}
|
|
1178
|
-
orderedLines.push("content-disposition: inline")
|
|
1179
|
-
orderedLines.push("")
|
|
1180
|
-
bodyParts.push(new Blob([orderedLines.join("\r\n")]))
|
|
1181
|
-
} else {
|
|
1182
|
-
const orderedLines = []
|
|
1183
|
-
if (partTypes.length > 0) {
|
|
1184
|
-
orderedLines.push(
|
|
1185
|
-
`ao-types: ${partTypes
|
|
1186
|
-
.sort((a, b) => {
|
|
1187
|
-
const aNum = parseInt(a.split("=")[0])
|
|
1188
|
-
const bNum = parseInt(b.split("=")[0])
|
|
1189
|
-
return aNum - bNum
|
|
1190
|
-
})
|
|
1191
|
-
.join(", ")}`
|
|
1192
|
-
)
|
|
1193
|
-
}
|
|
1194
|
-
orderedLines.push(`content-disposition: form-data;name="${bodyKey}"`)
|
|
1195
|
-
|
|
1196
|
-
// Check if this is the last body part
|
|
1197
|
-
const isLastBodyPart =
|
|
1198
|
-
sortedBodyKeys.indexOf(bodyKey) === sortedBodyKeys.length - 1
|
|
1199
|
-
const hasOnlyTypes = partTypes.length > 0 && fieldLines.length === 0
|
|
1200
|
-
|
|
1201
|
-
if (isLastBodyPart && hasOnlyTypes) {
|
|
1202
|
-
// Don't add empty line for last part with only types
|
|
1203
|
-
bodyParts.push(new Blob([orderedLines.join("\r\n")]))
|
|
1204
|
-
} else {
|
|
1205
|
-
orderedLines.push("")
|
|
1206
|
-
bodyParts.push(new Blob([orderedLines.join("\r\n")]))
|
|
1207
|
-
}
|
|
1208
|
-
}
|
|
1209
|
-
continue
|
|
537
|
+
} else if (typeof item === "number") {
|
|
538
|
+
if (Number.isInteger(item)) {
|
|
539
|
+
partTypes.push(`${index}="integer"`)
|
|
540
|
+
fieldLines.push(`${index}: ${item}`)
|
|
541
|
+
} else {
|
|
542
|
+
partTypes.push(`${index}="float"`)
|
|
543
|
+
fieldLines.push(`${index}: ${formatFloat(item)}`)
|
|
1210
544
|
}
|
|
545
|
+
} else if (typeof item === "string") {
|
|
546
|
+
fieldLines.push(`${index}: ${item}`)
|
|
547
|
+
} else if (
|
|
548
|
+
item === null ||
|
|
549
|
+
item === undefined ||
|
|
550
|
+
typeof item === "symbol" ||
|
|
551
|
+
typeof item === "boolean"
|
|
552
|
+
) {
|
|
553
|
+
partTypes.push(`${index}="atom"`)
|
|
554
|
+
if (item === null) {
|
|
555
|
+
fieldLines.push(`${index}: null`)
|
|
556
|
+
} else if (item === undefined) {
|
|
557
|
+
fieldLines.push(`${index}: undefined`)
|
|
558
|
+
} else if (typeof item === "symbol") {
|
|
559
|
+
const desc = item.description || "Symbol.for()"
|
|
560
|
+
fieldLines.push(`${index}: ${desc}`)
|
|
561
|
+
} else {
|
|
562
|
+
fieldLines.push(`${index}: ${item}`)
|
|
563
|
+
}
|
|
564
|
+
} else if (isBytes(item)) {
|
|
565
|
+
const buffer = toBuffer(item)
|
|
566
|
+
if (buffer.length === 0) {
|
|
567
|
+
partTypes.push(`${index}="empty-binary"`)
|
|
568
|
+
} else {
|
|
569
|
+
partTypes.push(`${index}="binary"`)
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
})
|
|
1211
573
|
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
sortedBodyKeys.forEach(key => {
|
|
1215
|
-
if (key.startsWith(bodyKey + "/")) {
|
|
1216
|
-
const subPath = key.substring(bodyKey.length + 1)
|
|
1217
|
-
const match = subPath.match(/^(\d+)/)
|
|
1218
|
-
if (match) {
|
|
1219
|
-
indicesWithOwnParts.add(parseInt(match[1]))
|
|
1220
|
-
}
|
|
1221
|
-
}
|
|
1222
|
-
})
|
|
574
|
+
return { fieldLines, partTypes }
|
|
575
|
+
}
|
|
1223
576
|
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
key.startsWith(bodyKey + "/") &&
|
|
1228
|
-
key.split("/").length > pathParts.length + 1
|
|
1229
|
-
)
|
|
577
|
+
// Step 13.2.3.5: Create array body part
|
|
578
|
+
function createArrayBodyPart(bodyKey, fieldLines, partTypes, headers) {
|
|
579
|
+
const isInline = bodyKey === "body" && headers["inline-body-key"] === "body"
|
|
1230
580
|
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
value.forEach((item, idx) => {
|
|
1237
|
-
const index = idx + 1
|
|
1238
|
-
if (Array.isArray(item)) {
|
|
1239
|
-
partTypes.push(`${index}="list"`)
|
|
1240
|
-
}
|
|
1241
|
-
})
|
|
1242
|
-
}
|
|
1243
|
-
|
|
1244
|
-
// Check if this array has mixed content with empty strings/objects
|
|
1245
|
-
const hasEmptyStrings = value.some(
|
|
1246
|
-
item => typeof item === "string" && item === ""
|
|
581
|
+
if (isInline) {
|
|
582
|
+
const orderedLines = []
|
|
583
|
+
if (partTypes.length > 0) {
|
|
584
|
+
orderedLines.push(
|
|
585
|
+
`ao-types: ${sortTypeAnnotations(partTypes).join(", ")}`
|
|
1247
586
|
)
|
|
1248
|
-
|
|
1249
|
-
|
|
587
|
+
}
|
|
588
|
+
orderedLines.push("content-disposition: inline")
|
|
589
|
+
if (fieldLines.length > 0) {
|
|
590
|
+
orderedLines.push("")
|
|
591
|
+
for (const line of fieldLines) {
|
|
592
|
+
orderedLines.push(line)
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
return new Blob([orderedLines.join("\r\n") + "\r\n"])
|
|
596
|
+
} else {
|
|
597
|
+
const orderedLines = []
|
|
598
|
+
if (partTypes.length > 0) {
|
|
599
|
+
orderedLines.push(
|
|
600
|
+
`ao-types: ${sortTypeAnnotations(partTypes).join(", ")}`
|
|
1250
601
|
)
|
|
602
|
+
}
|
|
603
|
+
orderedLines.push(`content-disposition: form-data;name="${bodyKey}"`)
|
|
604
|
+
for (const line of fieldLines) {
|
|
605
|
+
orderedLines.push(line)
|
|
606
|
+
}
|
|
607
|
+
if (fieldLines.length > 0) {
|
|
608
|
+
return new Blob([orderedLines.join("\r\n") + "\r\n"])
|
|
609
|
+
} else {
|
|
610
|
+
return new Blob([orderedLines.join("\r\n")])
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
}
|
|
1251
614
|
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
return Object.values(item).every(
|
|
1256
|
-
v =>
|
|
1257
|
-
(typeof v === "string" && v === "") ||
|
|
1258
|
-
(Array.isArray(v) && v.length === 0) ||
|
|
1259
|
-
(isPojo(v) && Object.keys(v).length === 0)
|
|
1260
|
-
)
|
|
1261
|
-
})
|
|
1262
|
-
|
|
1263
|
-
const isMixedArray =
|
|
1264
|
-
hasObjects &&
|
|
1265
|
-
(hasEmptyStrings || hasEmptyObjects) &&
|
|
1266
|
-
!hasObjectsWithOnlyEmptyValues
|
|
1267
|
-
|
|
1268
|
-
// Process ALL items for type information
|
|
1269
|
-
value.forEach((item, idx) => {
|
|
1270
|
-
const index = idx + 1
|
|
1271
|
-
|
|
1272
|
-
// Skip type info for elements that have their own parts
|
|
1273
|
-
if (indicesWithOwnParts.has(index)) {
|
|
1274
|
-
return
|
|
1275
|
-
}
|
|
1276
|
-
|
|
1277
|
-
// If we have nested object parts and this is an array with objects, skip processing it inline
|
|
1278
|
-
if (
|
|
1279
|
-
hasNestedObjectParts &&
|
|
1280
|
-
Array.isArray(item) &&
|
|
1281
|
-
item.some(subItem => isPojo(subItem))
|
|
1282
|
-
) {
|
|
1283
|
-
// Type info already added above
|
|
1284
|
-
return
|
|
1285
|
-
}
|
|
615
|
+
// Step 13.2.3: Handle array values
|
|
616
|
+
function handleArrayValue(bodyKey, value, headers, sortedBodyKeys, pathParts) {
|
|
617
|
+
const arrayInfo = analyzeArray(value)
|
|
1286
618
|
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
partTypes.push(`${index}="empty-binary"`)
|
|
1291
|
-
} else if (isPojo(item) && Object.keys(item).length === 0) {
|
|
1292
|
-
// Empty objects don't get field lines but do get type annotations
|
|
1293
|
-
partTypes.push(`${index}="empty-message"`)
|
|
1294
|
-
} else if (isPojo(item)) {
|
|
1295
|
-
// Non-empty objects might have parts
|
|
1296
|
-
} else if (Array.isArray(item)) {
|
|
1297
|
-
if (item.length === 0) {
|
|
1298
|
-
// Empty arrays don't get field lines but do get type annotations
|
|
1299
|
-
partTypes.push(`${index}="empty-list"`)
|
|
1300
|
-
} else {
|
|
1301
|
-
partTypes.push(`${index}="list"`)
|
|
1302
|
-
// Encode array
|
|
1303
|
-
const encodedItems = item
|
|
1304
|
-
.map(subItem => {
|
|
1305
|
-
if (typeof subItem === "number") {
|
|
1306
|
-
if (Number.isInteger(subItem)) {
|
|
1307
|
-
return `"(ao-type-integer) ${subItem}"`
|
|
1308
|
-
} else {
|
|
1309
|
-
return `"(ao-type-float) ${formatFloat(subItem)}"`
|
|
1310
|
-
}
|
|
1311
|
-
} else if (typeof subItem === "string") {
|
|
1312
|
-
return `"${subItem}"`
|
|
1313
|
-
} else if (subItem === null) {
|
|
1314
|
-
return `"(ao-type-atom) \\"null\\""`
|
|
1315
|
-
} else if (subItem === undefined) {
|
|
1316
|
-
return `"(ao-type-atom) \\"undefined\\""`
|
|
1317
|
-
} else if (typeof subItem === "symbol") {
|
|
1318
|
-
const desc = subItem.description || "Symbol.for()"
|
|
1319
|
-
return `"(ao-type-atom) \\"${desc}\\""`
|
|
1320
|
-
} else if (typeof subItem === "boolean") {
|
|
1321
|
-
return `"(ao-type-atom) \\"${subItem}\\""`
|
|
1322
|
-
} else if (Array.isArray(subItem)) {
|
|
1323
|
-
// Use the full encodeArrayItem for nested arrays
|
|
1324
|
-
return encodeArrayItem(subItem)
|
|
1325
|
-
} else if (isBytes(subItem)) {
|
|
1326
|
-
const buffer = toBuffer(subItem)
|
|
1327
|
-
if (buffer.length === 0 || buffer.byteLength === 0) {
|
|
1328
|
-
return `""`
|
|
1329
|
-
}
|
|
1330
|
-
return `"(ao-type-binary)"`
|
|
1331
|
-
} else if (isPojo(subItem)) {
|
|
1332
|
-
const json = JSON.stringify(subItem)
|
|
1333
|
-
const escaped = json
|
|
1334
|
-
.replace(/\\/g, "\\\\")
|
|
1335
|
-
.replace(/"/g, '\\"')
|
|
1336
|
-
return `"(ao-type-map) ${escaped}"`
|
|
1337
|
-
} else {
|
|
1338
|
-
return `"${String(subItem)}"`
|
|
1339
|
-
}
|
|
1340
|
-
})
|
|
1341
|
-
.join(", ")
|
|
1342
|
-
fieldLines.push(`${index}: ${encodedItems}`)
|
|
1343
|
-
}
|
|
1344
|
-
} else if (typeof item === "number") {
|
|
1345
|
-
if (Number.isInteger(item)) {
|
|
1346
|
-
partTypes.push(`${index}="integer"`)
|
|
1347
|
-
fieldLines.push(`${index}: ${item}`)
|
|
1348
|
-
} else {
|
|
1349
|
-
partTypes.push(`${index}="float"`)
|
|
1350
|
-
fieldLines.push(`${index}: ${formatFloat(item)}`)
|
|
1351
|
-
}
|
|
1352
|
-
} else if (typeof item === "string") {
|
|
1353
|
-
// Non-empty strings just get field lines, no type annotation
|
|
1354
|
-
fieldLines.push(`${index}: ${item}`)
|
|
1355
|
-
} else if (
|
|
1356
|
-
item === null ||
|
|
1357
|
-
item === undefined ||
|
|
1358
|
-
typeof item === "symbol" ||
|
|
1359
|
-
typeof item === "boolean"
|
|
1360
|
-
) {
|
|
1361
|
-
partTypes.push(`${index}="atom"`)
|
|
1362
|
-
if (item === null) {
|
|
1363
|
-
fieldLines.push(`${index}: null`)
|
|
1364
|
-
} else if (item === undefined) {
|
|
1365
|
-
fieldLines.push(`${index}: undefined`)
|
|
1366
|
-
} else if (typeof item === "symbol") {
|
|
1367
|
-
const desc = item.description || "Symbol.for()"
|
|
1368
|
-
fieldLines.push(`${index}: ${desc}`)
|
|
1369
|
-
} else {
|
|
1370
|
-
fieldLines.push(`${index}: ${item}`)
|
|
1371
|
-
}
|
|
1372
|
-
} else if (isBytes(item)) {
|
|
1373
|
-
const buffer = toBuffer(item)
|
|
1374
|
-
if (buffer.length === 0) {
|
|
1375
|
-
partTypes.push(`${index}="empty-binary"`)
|
|
1376
|
-
} else {
|
|
1377
|
-
partTypes.push(`${index}="binary"`)
|
|
1378
|
-
}
|
|
1379
|
-
}
|
|
1380
|
-
})
|
|
1381
|
-
|
|
1382
|
-
const isInline =
|
|
1383
|
-
bodyKey === "body" && headers["inline-body-key"] === "body"
|
|
1384
|
-
|
|
1385
|
-
if (isInline) {
|
|
1386
|
-
const orderedLines = []
|
|
1387
|
-
|
|
1388
|
-
if (partTypes.length > 0) {
|
|
1389
|
-
orderedLines.push(
|
|
1390
|
-
`ao-types: ${partTypes
|
|
1391
|
-
.sort((a, b) => {
|
|
1392
|
-
const aNum = parseInt(a.split("=")[0])
|
|
1393
|
-
const bNum = parseInt(b.split("=")[0])
|
|
1394
|
-
return aNum - bNum
|
|
1395
|
-
})
|
|
1396
|
-
.join(", ")}`
|
|
1397
|
-
)
|
|
1398
|
-
}
|
|
619
|
+
if (arrayInfo.hasOnlyNonEmptyObjects) {
|
|
620
|
+
return null
|
|
621
|
+
}
|
|
1399
622
|
|
|
1400
|
-
|
|
623
|
+
if (arrayInfo.hasOnlyEmptyElements) {
|
|
624
|
+
return handleArrayWithOnlyEmptyElements(
|
|
625
|
+
bodyKey,
|
|
626
|
+
value,
|
|
627
|
+
headers,
|
|
628
|
+
sortedBodyKeys
|
|
629
|
+
)
|
|
630
|
+
}
|
|
1401
631
|
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
632
|
+
const indicesWithOwnParts = buildIndicesWithOwnParts(bodyKey, sortedBodyKeys)
|
|
633
|
+
const hasNestedObjectParts = sortedBodyKeys.some(
|
|
634
|
+
key =>
|
|
635
|
+
key.startsWith(bodyKey + "/") &&
|
|
636
|
+
key.split("/").length > pathParts.length + 1
|
|
637
|
+
)
|
|
1408
638
|
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
.sort((a, b) => {
|
|
1418
|
-
const aNum = parseInt(a.split("=")[0])
|
|
1419
|
-
const bNum = parseInt(b.split("=")[0])
|
|
1420
|
-
return aNum - bNum
|
|
1421
|
-
})
|
|
1422
|
-
.join(", ")}`
|
|
1423
|
-
)
|
|
1424
|
-
}
|
|
639
|
+
const { fieldLines, partTypes } = processArrayItems(
|
|
640
|
+
value,
|
|
641
|
+
indicesWithOwnParts,
|
|
642
|
+
hasNestedObjectParts,
|
|
643
|
+
pathParts
|
|
644
|
+
)
|
|
645
|
+
return createArrayBodyPart(bodyKey, fieldLines, partTypes, headers)
|
|
646
|
+
}
|
|
1425
647
|
|
|
1426
|
-
|
|
648
|
+
// Step 13.2.4.3: Process object fields
|
|
649
|
+
function processObjectFields(value, bodyKey, sortedBodyKeys) {
|
|
650
|
+
const objectTypes = []
|
|
651
|
+
const fieldLines = []
|
|
652
|
+
const binaryFields = []
|
|
653
|
+
const arrayTypes = []
|
|
654
|
+
|
|
655
|
+
// First collect array types
|
|
656
|
+
for (const [k, v] of Object.entries(value)) {
|
|
657
|
+
if (Array.isArray(v)) {
|
|
658
|
+
arrayTypes.push(
|
|
659
|
+
`${k.toLowerCase()}="${v.length === 0 ? "empty-list" : "list"}"`
|
|
660
|
+
)
|
|
661
|
+
}
|
|
662
|
+
}
|
|
1427
663
|
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
}
|
|
664
|
+
// Then process other fields
|
|
665
|
+
for (const [k, v] of Object.entries(value)) {
|
|
666
|
+
const childPath = `${bodyKey}/${k}`
|
|
1432
667
|
|
|
1433
|
-
|
|
1434
|
-
if (fieldLines.length > 0) {
|
|
1435
|
-
bodyParts.push(new Blob([orderedLines.join("\r\n") + "\r\n"]))
|
|
1436
|
-
} else {
|
|
1437
|
-
bodyParts.push(new Blob([orderedLines.join("\r\n")]))
|
|
1438
|
-
}
|
|
1439
|
-
}
|
|
668
|
+
if (sortedBodyKeys.includes(childPath)) {
|
|
1440
669
|
continue
|
|
1441
670
|
}
|
|
1442
671
|
|
|
1443
|
-
if (isPojo(
|
|
1444
|
-
|
|
1445
|
-
if (
|
|
1446
|
-
// Check if this is a parent array context where empty objects should get parts
|
|
1447
|
-
const parentPath = pathParts.slice(0, -1).join("/")
|
|
1448
|
-
let parentValue = obj
|
|
1449
|
-
for (const part of pathParts.slice(0, -1)) {
|
|
1450
|
-
if (/^\d+$/.test(part)) {
|
|
1451
|
-
parentValue = parentValue[parseInt(part) - 1]
|
|
1452
|
-
} else {
|
|
1453
|
-
parentValue = parentValue[part]
|
|
1454
|
-
}
|
|
1455
|
-
}
|
|
1456
|
-
|
|
1457
|
-
if (Array.isArray(parentValue)) {
|
|
1458
|
-
// Check if parent array has mixed content
|
|
1459
|
-
const hasEmptyStrings = parentValue.some(
|
|
1460
|
-
item => typeof item === "string" && item === ""
|
|
1461
|
-
)
|
|
1462
|
-
const hasEmptyObjects = parentValue.some(
|
|
1463
|
-
item => isPojo(item) && Object.keys(item).length === 0
|
|
1464
|
-
)
|
|
1465
|
-
const hasObjects = parentValue.some(item => isPojo(item))
|
|
1466
|
-
|
|
1467
|
-
if (hasObjects && (hasEmptyStrings || hasEmptyObjects)) {
|
|
1468
|
-
// Special mixed array case - empty objects don't get parts
|
|
1469
|
-
console.log(
|
|
1470
|
-
`[Body part] Skipping empty object in mixed array at ${bodyKey}`
|
|
1471
|
-
)
|
|
1472
|
-
continue
|
|
1473
|
-
}
|
|
1474
|
-
}
|
|
1475
|
-
|
|
1476
|
-
// Normal case - empty objects might get parts
|
|
1477
|
-
console.log(`[Body part] Empty object at ${bodyKey}`)
|
|
1478
|
-
// For now, skip empty objects
|
|
672
|
+
if (Array.isArray(v) && v.some(item => isPojo(item))) {
|
|
673
|
+
const hasOnlyEmpty = v.every(item => isEmpty(item))
|
|
674
|
+
if (hasOnlyEmpty) {
|
|
1479
675
|
continue
|
|
1480
676
|
}
|
|
677
|
+
}
|
|
1481
678
|
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
679
|
+
if (Array.isArray(v)) {
|
|
680
|
+
// Type already added in arrayTypes
|
|
681
|
+
} else if (
|
|
682
|
+
v === null ||
|
|
683
|
+
v === undefined ||
|
|
684
|
+
typeof v === "symbol" ||
|
|
685
|
+
typeof v === "boolean"
|
|
686
|
+
) {
|
|
687
|
+
objectTypes.push(`${k.toLowerCase()}="atom"`)
|
|
688
|
+
} else if (typeof v === "number") {
|
|
689
|
+
objectTypes.push(
|
|
690
|
+
`${k.toLowerCase()}="${Number.isInteger(v) ? "integer" : "float"}"`
|
|
691
|
+
)
|
|
692
|
+
} else if (typeof v === "string" && v.length === 0) {
|
|
693
|
+
objectTypes.push(`${k.toLowerCase()}="empty-binary"`)
|
|
694
|
+
} else if (isBytes(v) && (v.length === 0 || v.byteLength === 0)) {
|
|
695
|
+
objectTypes.push(`${k.toLowerCase()}="empty-binary"`)
|
|
696
|
+
} else if (isPojo(v) && Object.keys(v).length === 0) {
|
|
697
|
+
objectTypes.push(`${k.toLowerCase()}="empty-message"`)
|
|
698
|
+
}
|
|
1492
699
|
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
if (isInline) {
|
|
1497
|
-
lines.push(`content-disposition: inline`)
|
|
700
|
+
if (typeof v === "string") {
|
|
701
|
+
if (v.length === 0) {
|
|
702
|
+
fieldLines.push(`${k}: `)
|
|
1498
703
|
} else {
|
|
1499
|
-
|
|
704
|
+
fieldLines.push(`${k}: ${v}`)
|
|
1500
705
|
}
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
(
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
v.length > 0 &&
|
|
1523
|
-
v.every(
|
|
1524
|
-
item =>
|
|
1525
|
-
(Array.isArray(item) && item.length === 0) ||
|
|
1526
|
-
(isPojo(item) && Object.keys(item).length === 0) ||
|
|
1527
|
-
(typeof item === "string" && item === "")
|
|
1528
|
-
)
|
|
1529
|
-
)
|
|
1530
|
-
}
|
|
1531
|
-
)
|
|
1532
|
-
|
|
1533
|
-
// Collect type information for arrays in the object BEFORE processing fields
|
|
1534
|
-
const arrayTypes = []
|
|
1535
|
-
for (const [k, v] of Object.entries(value)) {
|
|
1536
|
-
if (Array.isArray(v)) {
|
|
1537
|
-
arrayTypes.push(
|
|
1538
|
-
`${k.toLowerCase()}="${v.length === 0 ? "empty-list" : "list"}"`
|
|
1539
|
-
)
|
|
706
|
+
} else if (typeof v === "number") {
|
|
707
|
+
fieldLines.push(`${k}: ${v}`)
|
|
708
|
+
} else if (typeof v === "boolean") {
|
|
709
|
+
fieldLines.push(`${k}: "${v}"`)
|
|
710
|
+
} else if (v === null) {
|
|
711
|
+
fieldLines.push(`${k}: "null"`)
|
|
712
|
+
} else if (v === undefined) {
|
|
713
|
+
fieldLines.push(`${k}: "undefined"`)
|
|
714
|
+
} else if (typeof v === "symbol") {
|
|
715
|
+
const desc = v.description || "Symbol.for()"
|
|
716
|
+
fieldLines.push(`${k}: "${desc}"`)
|
|
717
|
+
} else if (isBytes(v)) {
|
|
718
|
+
const buffer = toBuffer(v)
|
|
719
|
+
binaryFields.push({ key: k, buffer })
|
|
720
|
+
} else if (Array.isArray(v) && v.length > 0) {
|
|
721
|
+
const childPath = `${bodyKey}/${k}`
|
|
722
|
+
if (!sortedBodyKeys.includes(childPath)) {
|
|
723
|
+
const hasObjects = v.some(item => isPojo(item))
|
|
724
|
+
if (!hasObjects) {
|
|
725
|
+
const encodedItems = v.map(item => encodeArrayItem(item)).join(", ")
|
|
726
|
+
fieldLines.push(`${k}: ${encodedItems}`)
|
|
1540
727
|
}
|
|
1541
728
|
}
|
|
729
|
+
}
|
|
730
|
+
}
|
|
1542
731
|
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
if (sortedBodyKeys.includes(childPath)) {
|
|
1547
|
-
continue
|
|
1548
|
-
}
|
|
732
|
+
const allTypes = [...arrayTypes, ...objectTypes]
|
|
733
|
+
return { allTypes, fieldLines, binaryFields }
|
|
734
|
+
}
|
|
1549
735
|
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
736
|
+
// Step 13.2.4.5: Create object body part
|
|
737
|
+
function createObjectBodyPart(
|
|
738
|
+
bodyKey,
|
|
739
|
+
value,
|
|
740
|
+
allTypes,
|
|
741
|
+
fieldLines,
|
|
742
|
+
binaryFields,
|
|
743
|
+
headers,
|
|
744
|
+
sortedBodyKeys
|
|
745
|
+
) {
|
|
746
|
+
const isInline = bodyKey === "body" && headers["inline-body-key"] === "body"
|
|
747
|
+
const lines = []
|
|
748
|
+
|
|
749
|
+
if (isInline) {
|
|
750
|
+
const orderedLines = []
|
|
751
|
+
|
|
752
|
+
// For inline mode: fields first, then headers
|
|
753
|
+
for (const line of fieldLines) {
|
|
754
|
+
orderedLines.push(line)
|
|
755
|
+
}
|
|
1566
756
|
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
v === undefined ||
|
|
1572
|
-
typeof v === "symbol" ||
|
|
1573
|
-
typeof v === "boolean"
|
|
1574
|
-
) {
|
|
1575
|
-
objectTypes.push(`${k.toLowerCase()}="atom"`)
|
|
1576
|
-
} else if (typeof v === "number") {
|
|
1577
|
-
objectTypes.push(
|
|
1578
|
-
`${k.toLowerCase()}="${Number.isInteger(v) ? "integer" : "float"}"`
|
|
1579
|
-
)
|
|
1580
|
-
} else if (typeof v === "string" && v.length === 0) {
|
|
1581
|
-
objectTypes.push(`${k.toLowerCase()}="empty-binary"`)
|
|
1582
|
-
} else if (isBytes(v) && (v.length === 0 || v.byteLength === 0)) {
|
|
1583
|
-
objectTypes.push(`${k.toLowerCase()}="empty-binary"`)
|
|
1584
|
-
} else if (isPojo(v) && Object.keys(v).length === 0) {
|
|
1585
|
-
objectTypes.push(`${k.toLowerCase()}="empty-message"`)
|
|
1586
|
-
}
|
|
757
|
+
if (allTypes.length > 0) {
|
|
758
|
+
orderedLines.push(`ao-types: ${allTypes.sort().join(", ")}`)
|
|
759
|
+
}
|
|
760
|
+
orderedLines.push("content-disposition: inline")
|
|
1587
761
|
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
}
|
|
1604
|
-
|
|
1605
|
-
fieldLines.push(`${k}: "${desc}"`)
|
|
1606
|
-
} else if (isBytes(v)) {
|
|
1607
|
-
const buffer = toBuffer(v)
|
|
1608
|
-
binaryFields.push({ key: k, buffer })
|
|
1609
|
-
continue
|
|
1610
|
-
} else if (Array.isArray(v)) {
|
|
1611
|
-
if (v.length === 0) {
|
|
1612
|
-
// Don't add field line for empty array
|
|
1613
|
-
} else {
|
|
1614
|
-
// Check if this array will have its own body part
|
|
1615
|
-
const childPath = `${bodyKey}/${k}`
|
|
1616
|
-
if (!sortedBodyKeys.includes(childPath)) {
|
|
1617
|
-
// Check if this array contains objects - if so, don't add field line
|
|
1618
|
-
const hasObjects = v.some(item => isPojo(item))
|
|
1619
|
-
if (!hasObjects) {
|
|
1620
|
-
const encodedItems = v
|
|
1621
|
-
.map(item => encodeArrayItem(item))
|
|
1622
|
-
.join(", ")
|
|
1623
|
-
fieldLines.push(`${k}: ${encodedItems}`)
|
|
1624
|
-
}
|
|
1625
|
-
}
|
|
1626
|
-
}
|
|
1627
|
-
} else if (isPojo(v) && Object.keys(v).length === 0) {
|
|
1628
|
-
// Empty object - don't add field line
|
|
1629
|
-
}
|
|
762
|
+
const binaryFieldsForInline = Object.entries(value)
|
|
763
|
+
.filter(
|
|
764
|
+
([k, v]) => isBytes(v) && !sortedBodyKeys.includes(`${bodyKey}/${k}`)
|
|
765
|
+
)
|
|
766
|
+
.map(([k, v]) => ({
|
|
767
|
+
key: k,
|
|
768
|
+
buffer: toBuffer(v),
|
|
769
|
+
}))
|
|
770
|
+
|
|
771
|
+
if (binaryFieldsForInline.length > 0) {
|
|
772
|
+
const parts = []
|
|
773
|
+
// Join all text lines first
|
|
774
|
+
parts.push(Buffer.from(orderedLines.join("\r\n")))
|
|
775
|
+
// Then add binary fields
|
|
776
|
+
for (const { key, buffer } of binaryFieldsForInline) {
|
|
777
|
+
parts.push(Buffer.from(`\r\n${key}: `))
|
|
778
|
+
parts.push(buffer)
|
|
1630
779
|
}
|
|
780
|
+
parts.push(Buffer.from("\r\n"))
|
|
781
|
+
const fullBody = Buffer.concat(parts)
|
|
782
|
+
return new Blob([fullBody])
|
|
783
|
+
} else {
|
|
784
|
+
return new Blob([orderedLines.join("\r\n") + "\r\n"])
|
|
785
|
+
}
|
|
786
|
+
} else {
|
|
787
|
+
// Non-inline mode remains the same
|
|
788
|
+
const orderedLines = []
|
|
789
|
+
if (allTypes.length > 0) {
|
|
790
|
+
orderedLines.push(`ao-types: ${allTypes.sort().join(", ")}`)
|
|
791
|
+
}
|
|
792
|
+
orderedLines.push(`content-disposition: form-data;name="${bodyKey}"`)
|
|
793
|
+
|
|
794
|
+
const hasBinaryFields = binaryFields && binaryFields.length > 0
|
|
795
|
+
if (hasBinaryFields || fieldLines.length === 0) {
|
|
796
|
+
orderedLines.push("")
|
|
797
|
+
}
|
|
1631
798
|
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
(isPojo(item) && Object.keys(item).length === 0) ||
|
|
1645
|
-
(typeof item === "string" && item === "")
|
|
1646
|
-
)
|
|
1647
|
-
return hasOnlyEmpty || sortedBodyKeys.includes(childPath)
|
|
799
|
+
for (const line of fieldLines) {
|
|
800
|
+
orderedLines.push(line)
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
if (binaryFields && binaryFields.length > 0) {
|
|
804
|
+
const parts = []
|
|
805
|
+
const headerText = orderedLines.join("\r\n")
|
|
806
|
+
parts.push(Buffer.from(headerText))
|
|
807
|
+
for (let i = 0; i < binaryFields.length; i++) {
|
|
808
|
+
const { key, buffer } = binaryFields[i]
|
|
809
|
+
if (i > 0) {
|
|
810
|
+
parts.push(Buffer.from("\r\n"))
|
|
1648
811
|
}
|
|
1649
|
-
|
|
1650
|
-
|
|
812
|
+
parts.push(Buffer.from(`${key}: `))
|
|
813
|
+
parts.push(buffer)
|
|
814
|
+
}
|
|
815
|
+
parts.push(Buffer.from("\r\n"))
|
|
816
|
+
const fullBody = Buffer.concat(parts)
|
|
817
|
+
return new Blob([fullBody])
|
|
818
|
+
} else {
|
|
819
|
+
if (fieldLines.length > 0) {
|
|
820
|
+
return new Blob([orderedLines.join("\r\n") + "\r\n"])
|
|
821
|
+
} else {
|
|
822
|
+
return new Blob([orderedLines.join("\r\n")])
|
|
823
|
+
}
|
|
824
|
+
}
|
|
825
|
+
}
|
|
826
|
+
}
|
|
1651
827
|
|
|
828
|
+
// Step 13.2.4: Handle object values
|
|
829
|
+
function handleObjectValue(
|
|
830
|
+
obj,
|
|
831
|
+
bodyKey,
|
|
832
|
+
value,
|
|
833
|
+
headers,
|
|
834
|
+
sortedBodyKeys,
|
|
835
|
+
pathParts,
|
|
836
|
+
hasSpecialDataBody
|
|
837
|
+
) {
|
|
838
|
+
if (Object.keys(value).length === 0) {
|
|
839
|
+
// Skip empty objects in certain contexts
|
|
840
|
+
const parentPath = pathParts.slice(0, -1).join("/")
|
|
841
|
+
const parentValue = parentPath ? getValueByPath(obj, parentPath) : obj
|
|
842
|
+
|
|
843
|
+
if (Array.isArray(parentValue)) {
|
|
844
|
+
const parentArrayInfo = analyzeArray(parentValue)
|
|
1652
845
|
if (
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
!hasArraysWithOnlyEmptyElements
|
|
846
|
+
parentArrayInfo.hasObjects &&
|
|
847
|
+
(parentArrayInfo.hasEmptyStrings || parentArrayInfo.hasEmptyObjects)
|
|
1656
848
|
) {
|
|
1657
|
-
|
|
849
|
+
return null
|
|
1658
850
|
}
|
|
851
|
+
}
|
|
852
|
+
return null
|
|
853
|
+
}
|
|
1659
854
|
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
|
|
1666
|
-
|
|
1667
|
-
|
|
1668
|
-
|
|
1669
|
-
|
|
1670
|
-
})
|
|
1671
|
-
|
|
1672
|
-
if (isInline) {
|
|
1673
|
-
const orderedLines = []
|
|
855
|
+
// Skip special data/body case
|
|
856
|
+
if (
|
|
857
|
+
hasSpecialDataBody &&
|
|
858
|
+
bodyKey === "data" &&
|
|
859
|
+
Object.keys(value).length === 1 &&
|
|
860
|
+
value.body &&
|
|
861
|
+
isBytes(value.body)
|
|
862
|
+
) {
|
|
863
|
+
return null
|
|
864
|
+
}
|
|
1674
865
|
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
|
|
866
|
+
const { allTypes, fieldLines, binaryFields } = processObjectFields(
|
|
867
|
+
value,
|
|
868
|
+
bodyKey,
|
|
869
|
+
sortedBodyKeys
|
|
870
|
+
)
|
|
1679
871
|
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
|
|
872
|
+
// Check if object should be skipped
|
|
873
|
+
const hasOnlyEmptyCollections = Object.entries(value).every(([k, v]) =>
|
|
874
|
+
isEmpty(v)
|
|
875
|
+
)
|
|
876
|
+
const hasArraysWithOnlyEmptyElements = Object.entries(value).some(
|
|
877
|
+
([k, v]) =>
|
|
878
|
+
Array.isArray(v) && v.length > 0 && v.every(item => isEmpty(item))
|
|
879
|
+
)
|
|
1683
880
|
|
|
1684
|
-
|
|
881
|
+
const shouldSkipObject = Object.entries(value).every(([k, v]) => {
|
|
882
|
+
const childPath = `${bodyKey}/${k}`
|
|
883
|
+
if (sortedBodyKeys.includes(childPath)) return true
|
|
884
|
+
if (Array.isArray(v) && v.some(item => isPojo(item))) {
|
|
885
|
+
const hasOnlyEmpty = v.every(item => isEmpty(item))
|
|
886
|
+
return hasOnlyEmpty || sortedBodyKeys.includes(childPath)
|
|
887
|
+
}
|
|
888
|
+
return false
|
|
889
|
+
})
|
|
1685
890
|
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
buffer: toBuffer(v),
|
|
1694
|
-
}))
|
|
891
|
+
if (
|
|
892
|
+
shouldSkipObject &&
|
|
893
|
+
!hasOnlyEmptyCollections &&
|
|
894
|
+
!hasArraysWithOnlyEmptyElements
|
|
895
|
+
) {
|
|
896
|
+
return null
|
|
897
|
+
}
|
|
1695
898
|
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
|
|
899
|
+
return createObjectBodyPart(
|
|
900
|
+
bodyKey,
|
|
901
|
+
value,
|
|
902
|
+
allTypes,
|
|
903
|
+
fieldLines,
|
|
904
|
+
binaryFields,
|
|
905
|
+
headers,
|
|
906
|
+
sortedBodyKeys
|
|
907
|
+
)
|
|
908
|
+
}
|
|
1699
909
|
|
|
1700
|
-
|
|
1701
|
-
|
|
1702
|
-
|
|
1703
|
-
|
|
910
|
+
// Step 13.2.5: Handle primitive values
|
|
911
|
+
function handlePrimitiveValue(bodyKey, value, headers) {
|
|
912
|
+
const isInline = bodyKey === "body" && headers["inline-body-key"] === "body"
|
|
913
|
+
const lines = []
|
|
1704
914
|
|
|
1705
|
-
|
|
915
|
+
if (isInline) {
|
|
916
|
+
lines.push(`content-disposition: inline`)
|
|
917
|
+
} else {
|
|
918
|
+
lines.push(`content-disposition: form-data;name="${bodyKey}"`)
|
|
919
|
+
}
|
|
1706
920
|
|
|
1707
|
-
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
} else if (fieldLines.length === 0) {
|
|
1719
|
-
bodyParts.push(new Blob([orderedLines.join("\r\n")]))
|
|
1720
|
-
} else {
|
|
1721
|
-
bodyParts.push(new Blob([orderedLines.join("\r\n") + "\r\n"]))
|
|
1722
|
-
}
|
|
1723
|
-
}
|
|
1724
|
-
} else {
|
|
1725
|
-
// Put ao-types first, then content-disposition, then field lines
|
|
1726
|
-
const orderedLines = []
|
|
921
|
+
if (typeof value === "string") {
|
|
922
|
+
lines.push("")
|
|
923
|
+
lines.push(value)
|
|
924
|
+
return new Blob([lines.join("\r\n")])
|
|
925
|
+
} else {
|
|
926
|
+
const content = encodePrimitiveContent(value)
|
|
927
|
+
lines.push("")
|
|
928
|
+
lines.push(content)
|
|
929
|
+
return new Blob([lines.join("\r\n")])
|
|
930
|
+
}
|
|
931
|
+
}
|
|
1727
932
|
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
|
|
933
|
+
// Step 13.2.6: Handle binary values
|
|
934
|
+
function handleBinaryValue(bodyKey, value, headers) {
|
|
935
|
+
const isInline = bodyKey === "body" && headers["inline-body-key"] === "body"
|
|
936
|
+
const lines = []
|
|
1731
937
|
|
|
1732
|
-
|
|
938
|
+
if (isInline) {
|
|
939
|
+
lines.push(`content-disposition: inline`)
|
|
940
|
+
} else {
|
|
941
|
+
lines.push(`content-disposition: form-data;name="${bodyKey}"`)
|
|
942
|
+
}
|
|
1733
943
|
|
|
1734
|
-
|
|
1735
|
-
|
|
944
|
+
const buffer = toBuffer(value)
|
|
945
|
+
const headerText = lines.join("\r\n") + "\r\n\r\n"
|
|
946
|
+
return new Blob([headerText, buffer])
|
|
947
|
+
}
|
|
1736
948
|
|
|
1737
|
-
|
|
1738
|
-
|
|
1739
|
-
|
|
1740
|
-
|
|
949
|
+
// Step 13.3: Handle special data/body case
|
|
950
|
+
function handleSpecialDataBodyCase(obj, hasSpecialDataBody) {
|
|
951
|
+
if (
|
|
952
|
+
hasSpecialDataBody &&
|
|
953
|
+
obj.data &&
|
|
954
|
+
obj.data.body &&
|
|
955
|
+
isBytes(obj.data.body)
|
|
956
|
+
) {
|
|
957
|
+
const buffer = toBuffer(obj.data.body)
|
|
958
|
+
const specialPart = [
|
|
959
|
+
`content-disposition: form-data;name="data/body"`,
|
|
960
|
+
"",
|
|
961
|
+
"",
|
|
962
|
+
].join("\r\n")
|
|
963
|
+
return new Blob([specialPart, buffer])
|
|
964
|
+
}
|
|
965
|
+
return null
|
|
966
|
+
}
|
|
1741
967
|
|
|
1742
|
-
|
|
1743
|
-
|
|
1744
|
-
|
|
1745
|
-
|
|
968
|
+
// Step 13: Build body parts for each body key
|
|
969
|
+
function buildBodyParts(obj, sortedBodyKeys, headers, hasSpecialDataBody) {
|
|
970
|
+
// Step 13.1: Initialize body parts collection
|
|
971
|
+
const bodyParts = []
|
|
1746
972
|
|
|
1747
|
-
|
|
1748
|
-
|
|
1749
|
-
|
|
1750
|
-
|
|
973
|
+
// Step 13.2: Process each body key
|
|
974
|
+
for (const bodyKey of sortedBodyKeys) {
|
|
975
|
+
// Step 13.2.1: Get value for current body key
|
|
976
|
+
const value = getValueByPath(obj, bodyKey)
|
|
977
|
+
const pathParts = bodyKey.split("/")
|
|
1751
978
|
|
|
1752
|
-
|
|
1753
|
-
|
|
1754
|
-
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
|
|
1758
|
-
|
|
1759
|
-
|
|
979
|
+
// Step 13.2.2: Handle empty string in nested path
|
|
980
|
+
const emptyStringPart = handleEmptyStringInNestedPath(
|
|
981
|
+
bodyKey,
|
|
982
|
+
value,
|
|
983
|
+
pathParts
|
|
984
|
+
)
|
|
985
|
+
if (emptyStringPart) {
|
|
986
|
+
bodyParts.push(emptyStringPart)
|
|
987
|
+
continue
|
|
988
|
+
}
|
|
1760
989
|
|
|
1761
|
-
|
|
1762
|
-
|
|
1763
|
-
|
|
1764
|
-
|
|
1765
|
-
|
|
1766
|
-
|
|
1767
|
-
|
|
1768
|
-
|
|
1769
|
-
|
|
1770
|
-
|
|
1771
|
-
|
|
990
|
+
// Step 13.2.3: Handle array values
|
|
991
|
+
if (Array.isArray(value)) {
|
|
992
|
+
const arrayPart = handleArrayValue(
|
|
993
|
+
bodyKey,
|
|
994
|
+
value,
|
|
995
|
+
headers,
|
|
996
|
+
sortedBodyKeys,
|
|
997
|
+
pathParts
|
|
998
|
+
)
|
|
999
|
+
if (arrayPart) {
|
|
1000
|
+
bodyParts.push(arrayPart)
|
|
1772
1001
|
}
|
|
1773
|
-
|
|
1774
1002
|
continue
|
|
1775
1003
|
}
|
|
1776
1004
|
|
|
1777
|
-
|
|
1778
|
-
if (
|
|
1779
|
-
|
|
1780
|
-
|
|
1781
|
-
|
|
1005
|
+
// Step 13.2.4: Handle object values
|
|
1006
|
+
if (isPojo(value)) {
|
|
1007
|
+
const objectPart = handleObjectValue(
|
|
1008
|
+
obj,
|
|
1009
|
+
bodyKey,
|
|
1010
|
+
value,
|
|
1011
|
+
headers,
|
|
1012
|
+
sortedBodyKeys,
|
|
1013
|
+
pathParts,
|
|
1014
|
+
hasSpecialDataBody
|
|
1015
|
+
)
|
|
1016
|
+
if (objectPart) {
|
|
1017
|
+
bodyParts.push(objectPart)
|
|
1018
|
+
}
|
|
1019
|
+
continue
|
|
1782
1020
|
}
|
|
1783
1021
|
|
|
1784
|
-
|
|
1785
|
-
|
|
1786
|
-
|
|
1787
|
-
bodyParts.push(new Blob([headerText, buffer]))
|
|
1788
|
-
} else if (typeof value === "string") {
|
|
1789
|
-
lines.push("")
|
|
1790
|
-
lines.push(value)
|
|
1791
|
-
bodyParts.push(new Blob([lines.join("\r\n")]))
|
|
1792
|
-
} else if (
|
|
1022
|
+
// Step 13.2.5: Handle primitive values
|
|
1023
|
+
if (
|
|
1024
|
+
typeof value === "string" ||
|
|
1793
1025
|
typeof value === "boolean" ||
|
|
1794
1026
|
typeof value === "number" ||
|
|
1795
1027
|
value === null ||
|
|
1796
1028
|
value === undefined ||
|
|
1797
1029
|
typeof value === "symbol"
|
|
1798
1030
|
) {
|
|
1799
|
-
|
|
1800
|
-
|
|
1801
|
-
|
|
1802
|
-
|
|
1803
|
-
content = String(value)
|
|
1804
|
-
} else if (value === null) {
|
|
1805
|
-
content = '"null"'
|
|
1806
|
-
} else if (value === undefined) {
|
|
1807
|
-
content = '"undefined"'
|
|
1808
|
-
} else if (typeof value === "symbol") {
|
|
1809
|
-
content = `"${value.description || "Symbol.for()"}"`
|
|
1810
|
-
}
|
|
1031
|
+
const primitivePart = handlePrimitiveValue(bodyKey, value, headers)
|
|
1032
|
+
bodyParts.push(primitivePart)
|
|
1033
|
+
continue
|
|
1034
|
+
}
|
|
1811
1035
|
|
|
1812
|
-
|
|
1813
|
-
|
|
1814
|
-
|
|
1036
|
+
// Step 13.2.6: Handle binary values
|
|
1037
|
+
if (isBytes(value)) {
|
|
1038
|
+
const binaryPart = handleBinaryValue(bodyKey, value, headers)
|
|
1039
|
+
bodyParts.push(binaryPart)
|
|
1040
|
+
continue
|
|
1815
1041
|
}
|
|
1816
1042
|
}
|
|
1817
1043
|
|
|
1818
|
-
//
|
|
1819
|
-
|
|
1820
|
-
|
|
1821
|
-
|
|
1822
|
-
obj.data.body &&
|
|
1823
|
-
isBytes(obj.data.body)
|
|
1824
|
-
) {
|
|
1825
|
-
const buffer = toBuffer(obj.data.body)
|
|
1826
|
-
const specialPart = [
|
|
1827
|
-
`content-disposition: form-data;name="data/body"`,
|
|
1828
|
-
"",
|
|
1829
|
-
"",
|
|
1830
|
-
].join("\r\n")
|
|
1831
|
-
bodyParts.push(new Blob([specialPart, buffer]))
|
|
1044
|
+
// Step 13.3: Handle special data/body case
|
|
1045
|
+
const specialPart = handleSpecialDataBodyCase(obj, hasSpecialDataBody)
|
|
1046
|
+
if (specialPart) {
|
|
1047
|
+
bodyParts.push(specialPart)
|
|
1832
1048
|
}
|
|
1833
1049
|
|
|
1050
|
+
// Step 13.4: Return body parts
|
|
1051
|
+
return bodyParts
|
|
1052
|
+
}
|
|
1053
|
+
|
|
1054
|
+
// Step 14: Generate multipart boundary
|
|
1055
|
+
async function generateBoundary(bodyParts) {
|
|
1834
1056
|
const partsContent = await Promise.all(bodyParts.map(part => part.text()))
|
|
1835
1057
|
const allContent = partsContent.join("")
|
|
1836
1058
|
const boundaryHash = await sha256(new TextEncoder().encode(allContent))
|
|
1837
1059
|
const boundary = base64url.encode(Buffer.from(boundaryHash))
|
|
1060
|
+
return boundary
|
|
1061
|
+
}
|
|
1838
1062
|
|
|
1063
|
+
// Step 15: Assemble final multipart body
|
|
1064
|
+
function assembleMultipartBody(bodyParts, boundary) {
|
|
1839
1065
|
const finalParts = []
|
|
1840
1066
|
for (let i = 0; i < bodyParts.length; i++) {
|
|
1841
1067
|
if (i === 0) {
|
|
@@ -1847,20 +1073,144 @@ async function encode(obj = {}) {
|
|
|
1847
1073
|
}
|
|
1848
1074
|
finalParts.push(new Blob([`\r\n--${boundary}--`]))
|
|
1849
1075
|
|
|
1850
|
-
|
|
1851
|
-
|
|
1076
|
+
return new Blob(finalParts)
|
|
1077
|
+
}
|
|
1852
1078
|
|
|
1079
|
+
// Step 16: Calculate content digest
|
|
1080
|
+
async function calculateContentDigest(body) {
|
|
1853
1081
|
const finalContent = await body.arrayBuffer()
|
|
1854
1082
|
|
|
1855
1083
|
if (finalContent.byteLength > 0) {
|
|
1856
1084
|
const contentDigest = await sha256(finalContent)
|
|
1857
1085
|
const base64 = base64url.toBase64(base64url.encode(contentDigest))
|
|
1858
|
-
|
|
1086
|
+
return { digest: base64, byteLength: finalContent.byteLength }
|
|
1859
1087
|
}
|
|
1860
1088
|
|
|
1861
|
-
|
|
1089
|
+
return { digest: null, byteLength: finalContent.byteLength }
|
|
1090
|
+
}
|
|
1091
|
+
|
|
1092
|
+
// Step 17: Set final headers (content-type, content-length)
|
|
1093
|
+
function setFinalHeaders(headers, boundary, contentDigest, byteLength) {
|
|
1094
|
+
headers["content-type"] = `multipart/form-data; boundary="${boundary}"`
|
|
1095
|
+
|
|
1096
|
+
if (contentDigest) {
|
|
1097
|
+
headers["content-digest"] = `sha-256=:${contentDigest}:`
|
|
1098
|
+
}
|
|
1099
|
+
|
|
1100
|
+
headers["content-length"] = String(byteLength)
|
|
1101
|
+
}
|
|
1102
|
+
|
|
1103
|
+
async function encode(obj = {}) {
|
|
1104
|
+
// Step 1: Process and normalize input values
|
|
1105
|
+
const processedObj = processInputValues(obj)
|
|
1106
|
+
|
|
1107
|
+
// Step 2: Handle empty object case
|
|
1108
|
+
const emptyResult = handleEmptyObject(processedObj)
|
|
1109
|
+
if (emptyResult) return emptyResult
|
|
1110
|
+
|
|
1111
|
+
// Step 3: Handle single field with empty binary
|
|
1112
|
+
const emptyBinaryResult = handleSingleEmptyBinaryField(processedObj)
|
|
1113
|
+
if (emptyBinaryResult) return emptyBinaryResult
|
|
1114
|
+
|
|
1115
|
+
// Step 4: Handle single field with binary data
|
|
1116
|
+
const singleBinaryResult = await handleSingleBinaryField(processedObj)
|
|
1117
|
+
if (singleBinaryResult) return singleBinaryResult
|
|
1118
|
+
|
|
1119
|
+
// Step 5: Handle single field with primitive value
|
|
1120
|
+
const primitiveResult = await handleSinglePrimitiveField(processedObj)
|
|
1121
|
+
if (primitiveResult) return primitiveResult
|
|
1122
|
+
|
|
1123
|
+
// Step 6a: Handle single field with non-empty binary
|
|
1124
|
+
const nonEmptyBinaryResult =
|
|
1125
|
+
await handleSingleNonEmptyBinaryField(processedObj)
|
|
1126
|
+
if (nonEmptyBinaryResult) return nonEmptyBinaryResult
|
|
1127
|
+
|
|
1128
|
+
// Step 6: Handle single field with non-ASCII string
|
|
1129
|
+
const nonAsciiResult = await handleSingleNonAsciiStringField(processedObj)
|
|
1130
|
+
if (nonAsciiResult) return nonAsciiResult
|
|
1131
|
+
|
|
1132
|
+
// Step 7: Collect all keys that need to go in body
|
|
1133
|
+
const bodyKeys = collectBodyKeysStep(processedObj)
|
|
1134
|
+
|
|
1135
|
+
const objKeys = Object.keys(obj)
|
|
1136
|
+
const headers = {}
|
|
1137
|
+
const headerTypes = []
|
|
1138
|
+
|
|
1139
|
+
// Step 8: Process fields that can go in headers
|
|
1140
|
+
processHeaderFields(obj, bodyKeys, headers, headerTypes)
|
|
1141
|
+
|
|
1142
|
+
// Step 9: Handle case where all body keys are empty binaries
|
|
1143
|
+
const emptyBinaryBodyResult = handleAllEmptyBinaryBodyKeys(
|
|
1144
|
+
obj,
|
|
1145
|
+
bodyKeys,
|
|
1146
|
+
headers,
|
|
1147
|
+
headerTypes
|
|
1148
|
+
)
|
|
1149
|
+
if (emptyBinaryBodyResult) return emptyBinaryBodyResult
|
|
1150
|
+
|
|
1151
|
+
// Step 10: Handle single body key optimization
|
|
1152
|
+
const singleBodyKeyResult = await handleSingleBodyKeyOptimization(
|
|
1153
|
+
obj,
|
|
1154
|
+
bodyKeys,
|
|
1155
|
+
headers,
|
|
1156
|
+
headerTypes
|
|
1157
|
+
)
|
|
1158
|
+
if (singleBodyKeyResult) return singleBodyKeyResult
|
|
1159
|
+
|
|
1160
|
+
// Step 11: Sort body keys
|
|
1161
|
+
const sortedBodyKeys = sortBodyKeys(bodyKeys)
|
|
1162
|
+
|
|
1163
|
+
// Step 12: Check for special data/body case
|
|
1164
|
+
const hasSpecialDataBody = checkSpecialDataBodyCase(obj, sortedBodyKeys)
|
|
1165
|
+
|
|
1166
|
+
// Only add body-keys header if there are actual body keys
|
|
1167
|
+
if (sortedBodyKeys.length > 0) {
|
|
1168
|
+
headers["body-keys"] = sortedBodyKeys.map(k => `"${k}"`).join(", ")
|
|
1169
|
+
}
|
|
1170
|
+
|
|
1171
|
+
// Special case: single body key named "body" containing an object
|
|
1172
|
+
if (
|
|
1173
|
+
!hasSpecialDataBody &&
|
|
1174
|
+
sortedBodyKeys.length === 1 &&
|
|
1175
|
+
sortedBodyKeys[0] === "body"
|
|
1176
|
+
) {
|
|
1177
|
+
const bodyValue = obj.body
|
|
1178
|
+
if (isPojo(bodyValue)) {
|
|
1179
|
+
headers["inline-body-key"] = "body"
|
|
1180
|
+
}
|
|
1181
|
+
}
|
|
1182
|
+
|
|
1183
|
+
if (headerTypes.length > 0) {
|
|
1184
|
+
headers["ao-types"] = headerTypes.sort().join(", ")
|
|
1185
|
+
}
|
|
1186
|
+
|
|
1187
|
+
// Step 13: Build body parts for each body key
|
|
1188
|
+
const bodyParts = buildBodyParts(
|
|
1189
|
+
obj,
|
|
1190
|
+
sortedBodyKeys,
|
|
1191
|
+
headers,
|
|
1192
|
+
hasSpecialDataBody
|
|
1193
|
+
)
|
|
1194
|
+
|
|
1195
|
+
// If no body parts were created, return headers only
|
|
1196
|
+
if (bodyParts.length === 0) {
|
|
1197
|
+
return { headers, body: undefined }
|
|
1198
|
+
}
|
|
1199
|
+
|
|
1200
|
+
// Step 14: Generate multipart boundary
|
|
1201
|
+
const boundary = await generateBoundary(bodyParts)
|
|
1202
|
+
|
|
1203
|
+
// Step 15: Assemble final multipart body
|
|
1204
|
+
const body = assembleMultipartBody(bodyParts, boundary)
|
|
1205
|
+
|
|
1206
|
+
// Step 16: Calculate content digest
|
|
1207
|
+
const { digest: contentDigest, byteLength } =
|
|
1208
|
+
await calculateContentDigest(body)
|
|
1209
|
+
|
|
1210
|
+
// Step 17: Set final headers (content-type, content-length)
|
|
1211
|
+
setFinalHeaders(headers, boundary, contentDigest, byteLength)
|
|
1862
1212
|
|
|
1863
|
-
|
|
1213
|
+
// Step 18: Return result
|
|
1864
1214
|
return { headers, body }
|
|
1865
1215
|
}
|
|
1866
1216
|
|