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