hbsig 0.0.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/bin_to_str.js +44 -0
- package/cjs/collect-body-keys.js +470 -0
- package/cjs/encode-array-item.js +110 -0
- package/cjs/encode-utils.js +236 -0
- package/cjs/encode.js +1318 -0
- package/cjs/erl_json.js +317 -0
- package/cjs/erl_str.js +1037 -0
- package/cjs/flat.js +222 -0
- package/cjs/http-message-signatures/httpbis.js +489 -0
- package/cjs/http-message-signatures/index.js +25 -0
- package/cjs/http-message-signatures/structured-header.js +129 -0
- package/cjs/httpsig.js +716 -0
- package/cjs/httpsig2.js +1160 -0
- package/cjs/id.js +470 -0
- package/cjs/index.js +63 -0
- package/cjs/send.js +194 -0
- package/cjs/signer-utils.js +617 -0
- package/cjs/signer.js +606 -0
- package/cjs/structured.js +296 -0
- package/cjs/test.js +27 -0
- package/cjs/utils.js +42 -0
- package/esm/bin_to_str.js +46 -0
- 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 +1219 -0
- package/esm/erl_json.js +289 -0
- package/esm/erl_str.js +1139 -0
- package/esm/flat.js +196 -0
- package/esm/http-message-signatures/httpbis.js +438 -0
- package/esm/http-message-signatures/index.js +4 -0
- package/esm/http-message-signatures/structured-header.js +105 -0
- package/esm/httpsig.js +658 -0
- package/esm/httpsig2.js +1097 -0
- package/esm/id.js +459 -0
- package/esm/index.js +4 -0
- package/esm/package.json +3 -0
- package/esm/send.js +124 -0
- package/esm/signer-utils.js +494 -0
- package/esm/signer.js +452 -0
- package/esm/structured.js +269 -0
- package/esm/test.js +6 -0
- package/esm/utils.js +28 -0
- package/package.json +28 -0
package/esm/encode.js
ADDED
|
@@ -0,0 +1,1219 @@
|
|
|
1
|
+
import base64url from "base64url"
|
|
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
|
+
|
|
18
|
+
import encodeArrayItem from "./encode-array-item.js"
|
|
19
|
+
import collectBodyKeys from "./collect-body-keys.js"
|
|
20
|
+
const MAX_HEADER_LENGTH = 4096
|
|
21
|
+
|
|
22
|
+
// Step 1: Process and normalize input values (handle symbols, nested objects/arrays)
|
|
23
|
+
function processInputValues(obj) {
|
|
24
|
+
// Currently this is a no-op, but will be used for input validation/normalization
|
|
25
|
+
return obj
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// Step 2: Handle empty object case
|
|
29
|
+
function handleEmptyObject(obj) {
|
|
30
|
+
if (Object.keys(obj).length === 0) {
|
|
31
|
+
return { headers: {}, body: undefined }
|
|
32
|
+
}
|
|
33
|
+
return null
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Step 3: Handle single field with empty binary
|
|
37
|
+
function handleSingleEmptyBinaryField(obj) {
|
|
38
|
+
const objKeys = Object.keys(obj)
|
|
39
|
+
|
|
40
|
+
if (objKeys.length === 1) {
|
|
41
|
+
const fieldName = objKeys[0]
|
|
42
|
+
const fieldValue = obj[fieldName]
|
|
43
|
+
|
|
44
|
+
if (
|
|
45
|
+
isBytes(fieldValue) &&
|
|
46
|
+
(fieldValue.length === 0 || fieldValue.byteLength === 0)
|
|
47
|
+
) {
|
|
48
|
+
const headers = {}
|
|
49
|
+
headers["ao-types"] = `${fieldName.toLowerCase()}="empty-binary"`
|
|
50
|
+
return { headers, body: undefined }
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return null
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Step 4: Handle single field with binary data
|
|
58
|
+
async function handleSingleBinaryField(obj) {
|
|
59
|
+
const hasBodyBinary = obj.body && isBytes(obj.body)
|
|
60
|
+
const otherFields = Object.keys(obj).filter(k => k !== "body")
|
|
61
|
+
|
|
62
|
+
if (hasBodyBinary && otherFields.length === 0) {
|
|
63
|
+
const headers = {}
|
|
64
|
+
const bodyBuffer = toBuffer(obj.body)
|
|
65
|
+
const bodyArrayBuffer = bodyBuffer.buffer.slice(
|
|
66
|
+
bodyBuffer.byteOffset,
|
|
67
|
+
bodyBuffer.byteOffset + bodyBuffer.byteLength
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
const contentDigest = await sha256(bodyArrayBuffer)
|
|
71
|
+
const base64 = base64url.toBase64(base64url.encode(contentDigest))
|
|
72
|
+
headers["content-digest"] = `sha-256=:${base64}:`
|
|
73
|
+
|
|
74
|
+
return { headers, body: obj.body }
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return null
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Step 5: Handle single field with primitive value (string/number/boolean/null/undefined/symbol)
|
|
81
|
+
async function handleSinglePrimitiveField(obj) {
|
|
82
|
+
const objKeys = Object.keys(obj)
|
|
83
|
+
|
|
84
|
+
if (objKeys.length === 1) {
|
|
85
|
+
const fieldName = objKeys[0]
|
|
86
|
+
const fieldValue = obj[fieldName]
|
|
87
|
+
|
|
88
|
+
if (
|
|
89
|
+
(fieldName === "data" || fieldName === "body") &&
|
|
90
|
+
(typeof fieldValue === "string" ||
|
|
91
|
+
typeof fieldValue === "boolean" ||
|
|
92
|
+
typeof fieldValue === "number" ||
|
|
93
|
+
fieldValue === null ||
|
|
94
|
+
fieldValue === undefined ||
|
|
95
|
+
typeof fieldValue === "symbol")
|
|
96
|
+
) {
|
|
97
|
+
const headers = {}
|
|
98
|
+
const bodyContent = encodePrimitiveContent(fieldValue)
|
|
99
|
+
|
|
100
|
+
const encoder = new TextEncoder()
|
|
101
|
+
const encoded = encoder.encode(bodyContent)
|
|
102
|
+
const contentDigest = await sha256(encoded.buffer)
|
|
103
|
+
const base64 = base64url.toBase64(base64url.encode(contentDigest))
|
|
104
|
+
headers["content-digest"] = `sha-256=:${base64}:`
|
|
105
|
+
|
|
106
|
+
const aoType = getAoType(fieldValue)
|
|
107
|
+
if (aoType === "atom" || aoType === "integer" || aoType === "float") {
|
|
108
|
+
headers["ao-types"] = `${fieldName.toLowerCase()}="${aoType}"`
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
if (fieldName !== "body") {
|
|
112
|
+
headers["inline-body-key"] = fieldName
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
return { headers, body: bodyContent }
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
return null
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Step 6a: Handle single field with non-empty binary (not body field)
|
|
123
|
+
async function handleSingleNonEmptyBinaryField(obj) {
|
|
124
|
+
const objKeys = Object.keys(obj)
|
|
125
|
+
|
|
126
|
+
if (objKeys.length === 1) {
|
|
127
|
+
const fieldName = objKeys[0]
|
|
128
|
+
const fieldValue = obj[fieldName]
|
|
129
|
+
|
|
130
|
+
if (isBytes(fieldValue) && fieldValue.length > 0) {
|
|
131
|
+
const headers = {}
|
|
132
|
+
const bodyBuffer = toBuffer(fieldValue)
|
|
133
|
+
const bodyArrayBuffer = bodyBuffer.buffer.slice(
|
|
134
|
+
bodyBuffer.byteOffset,
|
|
135
|
+
bodyBuffer.byteOffset + bodyBuffer.byteLength
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
const contentDigest = await sha256(bodyArrayBuffer)
|
|
139
|
+
const base64 = base64url.toBase64(base64url.encode(contentDigest))
|
|
140
|
+
headers["content-digest"] = `sha-256=:${base64}:`
|
|
141
|
+
|
|
142
|
+
if (fieldName !== "body") {
|
|
143
|
+
headers["inline-body-key"] = fieldName
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
return { headers, body: fieldValue }
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
return null
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// Step 6: Handle single field with non-ASCII string
|
|
154
|
+
async function handleSingleNonAsciiStringField(obj) {
|
|
155
|
+
const objKeys = Object.keys(obj)
|
|
156
|
+
|
|
157
|
+
if (objKeys.length === 1) {
|
|
158
|
+
const fieldName = objKeys[0]
|
|
159
|
+
const fieldValue = obj[fieldName]
|
|
160
|
+
|
|
161
|
+
if (typeof fieldValue === "string" && hasNonAscii(fieldValue)) {
|
|
162
|
+
const headers = {}
|
|
163
|
+
const encoder = new TextEncoder()
|
|
164
|
+
const encoded = encoder.encode(fieldValue)
|
|
165
|
+
const contentDigest = await sha256(encoded.buffer)
|
|
166
|
+
const base64 = base64url.toBase64(base64url.encode(contentDigest))
|
|
167
|
+
headers["content-digest"] = `sha-256=:${base64}:`
|
|
168
|
+
|
|
169
|
+
if (fieldName !== "body") {
|
|
170
|
+
headers["inline-body-key"] = fieldName
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
return { headers, body: fieldValue }
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
return null
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// Step 7: Collect all keys that need to go in body
|
|
181
|
+
function collectBodyKeysStep(obj) {
|
|
182
|
+
return collectBodyKeys(obj)
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// Step 8: Process fields that can go in headers
|
|
186
|
+
function processHeaderFields(obj, bodyKeys, headers, headerTypes) {
|
|
187
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
188
|
+
const needsBody =
|
|
189
|
+
bodyKeys.includes(key) || bodyKeys.some(k => k.startsWith(`${key}/`))
|
|
190
|
+
|
|
191
|
+
if (!needsBody) {
|
|
192
|
+
if (value === null) {
|
|
193
|
+
headers[key] = '"null"'
|
|
194
|
+
headerTypes.push(`${key.toLowerCase()}="atom"`)
|
|
195
|
+
} else if (value === undefined) {
|
|
196
|
+
headers[key] = '"undefined"'
|
|
197
|
+
headerTypes.push(`${key.toLowerCase()}="atom"`)
|
|
198
|
+
} else if (typeof value === "boolean") {
|
|
199
|
+
headers[key] = `"${value}"`
|
|
200
|
+
headerTypes.push(`${key.toLowerCase()}="atom"`)
|
|
201
|
+
} else if (typeof value === "symbol") {
|
|
202
|
+
headers[key] = `"${value.description || "Symbol.for()"}"`
|
|
203
|
+
headerTypes.push(`${key.toLowerCase()}="atom"`)
|
|
204
|
+
} else if (typeof value === "number") {
|
|
205
|
+
headers[key] = String(value)
|
|
206
|
+
headerTypes.push(
|
|
207
|
+
`${key.toLowerCase()}="${Number.isInteger(value) ? "integer" : "float"}"`
|
|
208
|
+
)
|
|
209
|
+
} else if (typeof value === "string") {
|
|
210
|
+
if (value.length === 0) {
|
|
211
|
+
headerTypes.push(`${key.toLowerCase()}="empty-binary"`)
|
|
212
|
+
} else if (hasNonAscii(value)) {
|
|
213
|
+
continue
|
|
214
|
+
} else {
|
|
215
|
+
headers[key] = value
|
|
216
|
+
}
|
|
217
|
+
} else if (Array.isArray(value) && value.length === 0) {
|
|
218
|
+
headerTypes.push(`${key.toLowerCase()}="empty-list"`)
|
|
219
|
+
} else if (Array.isArray(value) && !value.some(item => isPojo(item))) {
|
|
220
|
+
const hasNonAsciiItems = value.some(
|
|
221
|
+
item => typeof item === "string" && hasNonAscii(item)
|
|
222
|
+
)
|
|
223
|
+
if (!hasNonAsciiItems) {
|
|
224
|
+
const encodedItems = value
|
|
225
|
+
.map(item => encodeArrayItem(item))
|
|
226
|
+
.join(", ")
|
|
227
|
+
headers[key] = encodedItems
|
|
228
|
+
headerTypes.push(`${key.toLowerCase()}="list"`)
|
|
229
|
+
}
|
|
230
|
+
} else if (
|
|
231
|
+
isBytes(value) &&
|
|
232
|
+
(value.length === 0 || value.byteLength === 0)
|
|
233
|
+
) {
|
|
234
|
+
headerTypes.push(`${key.toLowerCase()}="empty-binary"`)
|
|
235
|
+
} else if (isPojo(value) && Object.keys(value).length === 0) {
|
|
236
|
+
headerTypes.push(`${key.toLowerCase()}="empty-message"`)
|
|
237
|
+
}
|
|
238
|
+
} else {
|
|
239
|
+
// Fields that need body still get type annotations
|
|
240
|
+
const aoType = getAoType(value)
|
|
241
|
+
if (aoType) {
|
|
242
|
+
headerTypes.push(`${key.toLowerCase()}="${aoType}"`)
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// Second pass for array type annotations
|
|
248
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
249
|
+
if (Array.isArray(value)) {
|
|
250
|
+
if (
|
|
251
|
+
bodyKeys.includes(key) ||
|
|
252
|
+
bodyKeys.some(k => k.startsWith(`${key}/`))
|
|
253
|
+
) {
|
|
254
|
+
if (!headerTypes.some(t => t.startsWith(`${key.toLowerCase()}=`))) {
|
|
255
|
+
headerTypes.push(`${key.toLowerCase()}="list"`)
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// Step 9: Handle case where all body keys are empty binaries
|
|
263
|
+
function handleAllEmptyBinaryBodyKeys(obj, bodyKeys, headers, headerTypes) {
|
|
264
|
+
if (bodyKeys.length === 0) {
|
|
265
|
+
if (headerTypes.length > 0) {
|
|
266
|
+
headers["ao-types"] = headerTypes.sort().join(", ")
|
|
267
|
+
}
|
|
268
|
+
return { headers, body: undefined }
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
const allBodyKeysAreEmptyBinaries = bodyKeys.every(key => {
|
|
272
|
+
const value = getValueByPath(obj, key)
|
|
273
|
+
return isBytes(value) && (value.length === 0 || value.byteLength === 0)
|
|
274
|
+
})
|
|
275
|
+
|
|
276
|
+
if (allBodyKeysAreEmptyBinaries) {
|
|
277
|
+
if (headerTypes.length > 0) {
|
|
278
|
+
headers["ao-types"] = headerTypes.sort().join(", ")
|
|
279
|
+
}
|
|
280
|
+
return { headers, body: undefined }
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
return null
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// Step 10: Handle single body key optimization
|
|
287
|
+
async function handleSingleBodyKeyOptimization(
|
|
288
|
+
obj,
|
|
289
|
+
bodyKeys,
|
|
290
|
+
headers,
|
|
291
|
+
headerTypes
|
|
292
|
+
) {
|
|
293
|
+
if (bodyKeys.length === 1) {
|
|
294
|
+
const singleKey = bodyKeys[0]
|
|
295
|
+
const value = getValueByPath(obj, singleKey)
|
|
296
|
+
|
|
297
|
+
// Apply optimization for binary data OR strings with newlines
|
|
298
|
+
if (
|
|
299
|
+
(isBytes(value) && value.length > 0) ||
|
|
300
|
+
(typeof value === "string" && value.includes("\n"))
|
|
301
|
+
) {
|
|
302
|
+
let contentToHash
|
|
303
|
+
let bodyContent = value
|
|
304
|
+
|
|
305
|
+
if (isBytes(value)) {
|
|
306
|
+
const bodyBuffer = toBuffer(value)
|
|
307
|
+
contentToHash = bodyBuffer.buffer.slice(
|
|
308
|
+
bodyBuffer.byteOffset,
|
|
309
|
+
bodyBuffer.byteOffset + bodyBuffer.byteLength
|
|
310
|
+
)
|
|
311
|
+
} else {
|
|
312
|
+
// For strings, encode to UTF-8 for hashing
|
|
313
|
+
const encoder = new TextEncoder()
|
|
314
|
+
const encoded = encoder.encode(value)
|
|
315
|
+
contentToHash = encoded.buffer
|
|
316
|
+
bodyContent = value
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
const contentDigest = await sha256(contentToHash)
|
|
320
|
+
const base64 = base64url.toBase64(base64url.encode(contentDigest))
|
|
321
|
+
headers["content-digest"] = `sha-256=:${base64}:`
|
|
322
|
+
|
|
323
|
+
if (singleKey !== "body") {
|
|
324
|
+
headers["inline-body-key"] = singleKey
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
if (headerTypes.length > 0) {
|
|
328
|
+
headers["ao-types"] = headerTypes.sort().join(", ")
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
return { headers, body: bodyContent }
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
return null
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
// Step 11: Sort body keys
|
|
339
|
+
function sortBodyKeys(bodyKeys) {
|
|
340
|
+
return bodyKeys.sort((a, b) => {
|
|
341
|
+
const aIsArrayElement = /\/\d+$/.test(a)
|
|
342
|
+
const bIsArrayElement = /\/\d+$/.test(b)
|
|
343
|
+
const aBase = a.split("/")[0]
|
|
344
|
+
const bBase = b.split("/")[0]
|
|
345
|
+
if (aBase === bBase) {
|
|
346
|
+
if (!aIsArrayElement && bIsArrayElement) {
|
|
347
|
+
return -1
|
|
348
|
+
}
|
|
349
|
+
if (aIsArrayElement && !bIsArrayElement) {
|
|
350
|
+
return 1
|
|
351
|
+
}
|
|
352
|
+
if (aIsArrayElement && bIsArrayElement) {
|
|
353
|
+
const aIndex = parseInt(a.split("/")[1])
|
|
354
|
+
const bIndex = parseInt(b.split("/")[1])
|
|
355
|
+
return aIndex - bIndex
|
|
356
|
+
}
|
|
357
|
+
return a.localeCompare(b)
|
|
358
|
+
}
|
|
359
|
+
return a.localeCompare(b)
|
|
360
|
+
})
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
// Step 12: Check for special data/body case
|
|
364
|
+
function checkSpecialDataBodyCase(obj, sortedBodyKeys) {
|
|
365
|
+
return (
|
|
366
|
+
sortedBodyKeys.includes("data") &&
|
|
367
|
+
sortedBodyKeys.includes("body") &&
|
|
368
|
+
obj.data &&
|
|
369
|
+
obj.data.body &&
|
|
370
|
+
isBytes(obj.data.body) &&
|
|
371
|
+
obj.body &&
|
|
372
|
+
obj.body.data &&
|
|
373
|
+
isBytes(obj.body.data)
|
|
374
|
+
)
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
// Step 13.2.2: Handle empty string in nested path
|
|
378
|
+
function handleEmptyStringInNestedPath(bodyKey, value, pathParts) {
|
|
379
|
+
if (typeof value === "string" && value === "" && pathParts.length > 1) {
|
|
380
|
+
const lines = []
|
|
381
|
+
lines.push(`content-disposition: form-data;name="${bodyKey}"`)
|
|
382
|
+
lines.push("")
|
|
383
|
+
lines.push("")
|
|
384
|
+
return new Blob([lines.join("\r\n")])
|
|
385
|
+
}
|
|
386
|
+
return null
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
// Step 13.2.3.2: Handle arrays with only empty elements
|
|
390
|
+
function handleArrayWithOnlyEmptyElements(
|
|
391
|
+
bodyKey,
|
|
392
|
+
value,
|
|
393
|
+
headers,
|
|
394
|
+
sortedBodyKeys
|
|
395
|
+
) {
|
|
396
|
+
const fieldLines = []
|
|
397
|
+
const partTypes = []
|
|
398
|
+
|
|
399
|
+
value.forEach((item, idx) => {
|
|
400
|
+
const index = idx + 1
|
|
401
|
+
const itemType = getAoType(item)
|
|
402
|
+
if (itemType) {
|
|
403
|
+
partTypes.push(`${index}="${itemType}"`)
|
|
404
|
+
}
|
|
405
|
+
})
|
|
406
|
+
|
|
407
|
+
const isInline = bodyKey === "body" && headers["inline-body-key"] === "body"
|
|
408
|
+
|
|
409
|
+
if (isInline) {
|
|
410
|
+
const orderedLines = []
|
|
411
|
+
if (partTypes.length > 0) {
|
|
412
|
+
orderedLines.push(
|
|
413
|
+
`ao-types: ${sortTypeAnnotations(partTypes).join(", ")}`
|
|
414
|
+
)
|
|
415
|
+
}
|
|
416
|
+
orderedLines.push("content-disposition: inline")
|
|
417
|
+
orderedLines.push("")
|
|
418
|
+
return new Blob([orderedLines.join("\r\n")])
|
|
419
|
+
} else {
|
|
420
|
+
const orderedLines = []
|
|
421
|
+
if (partTypes.length > 0) {
|
|
422
|
+
orderedLines.push(
|
|
423
|
+
`ao-types: ${sortTypeAnnotations(partTypes).join(", ")}`
|
|
424
|
+
)
|
|
425
|
+
}
|
|
426
|
+
orderedLines.push(`content-disposition: form-data;name="${bodyKey}"`)
|
|
427
|
+
|
|
428
|
+
const isLastBodyPart =
|
|
429
|
+
sortedBodyKeys.indexOf(bodyKey) === sortedBodyKeys.length - 1
|
|
430
|
+
const hasOnlyTypes = partTypes.length > 0 && fieldLines.length === 0
|
|
431
|
+
|
|
432
|
+
if (isLastBodyPart && hasOnlyTypes) {
|
|
433
|
+
return new Blob([orderedLines.join("\r\n")])
|
|
434
|
+
} else {
|
|
435
|
+
orderedLines.push("")
|
|
436
|
+
return new Blob([orderedLines.join("\r\n")])
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
// Step 13.2.3.3: Build indices with own parts
|
|
442
|
+
function buildIndicesWithOwnParts(bodyKey, sortedBodyKeys) {
|
|
443
|
+
const indicesWithOwnParts = new Set()
|
|
444
|
+
sortedBodyKeys.forEach(key => {
|
|
445
|
+
if (key.startsWith(bodyKey + "/")) {
|
|
446
|
+
const subPath = key.substring(bodyKey.length + 1)
|
|
447
|
+
const match = subPath.match(/^(\d+)/)
|
|
448
|
+
if (match) {
|
|
449
|
+
indicesWithOwnParts.add(parseInt(match[1]))
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
})
|
|
453
|
+
return indicesWithOwnParts
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
// Step 13.2.3.4: Process array items
|
|
457
|
+
function processArrayItems(
|
|
458
|
+
value,
|
|
459
|
+
indicesWithOwnParts,
|
|
460
|
+
hasNestedObjectParts,
|
|
461
|
+
pathParts
|
|
462
|
+
) {
|
|
463
|
+
const fieldLines = []
|
|
464
|
+
const partTypes = []
|
|
465
|
+
|
|
466
|
+
if (hasNestedObjectParts) {
|
|
467
|
+
value.forEach((item, idx) => {
|
|
468
|
+
const index = idx + 1
|
|
469
|
+
if (Array.isArray(item)) {
|
|
470
|
+
partTypes.push(`${index}="list"`)
|
|
471
|
+
}
|
|
472
|
+
})
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
value.forEach((item, idx) => {
|
|
476
|
+
const index = idx + 1
|
|
477
|
+
|
|
478
|
+
if (indicesWithOwnParts.has(index)) {
|
|
479
|
+
// This item has its own part - skip it here
|
|
480
|
+
// Don't add type annotation for items that have their own parts
|
|
481
|
+
return
|
|
482
|
+
}
|
|
483
|
+
if (
|
|
484
|
+
hasNestedObjectParts &&
|
|
485
|
+
Array.isArray(item) &&
|
|
486
|
+
item.some(subItem => isPojo(subItem))
|
|
487
|
+
) {
|
|
488
|
+
return
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
if (typeof item === "string" && item === "") {
|
|
492
|
+
partTypes.push(`${index}="empty-binary"`)
|
|
493
|
+
} else if (isPojo(item) && Object.keys(item).length === 0) {
|
|
494
|
+
partTypes.push(`${index}="empty-message"`)
|
|
495
|
+
} else if (isPojo(item)) {
|
|
496
|
+
// Non-empty objects are handled elsewhere
|
|
497
|
+
} else if (Array.isArray(item)) {
|
|
498
|
+
if (item.length === 0) {
|
|
499
|
+
partTypes.push(`${index}="empty-list"`)
|
|
500
|
+
} else {
|
|
501
|
+
partTypes.push(`${index}="list"`)
|
|
502
|
+
const encodedItems = item
|
|
503
|
+
.map(subItem => {
|
|
504
|
+
if (typeof subItem === "number") {
|
|
505
|
+
if (Number.isInteger(subItem)) {
|
|
506
|
+
return `"(ao-type-integer) ${subItem}"`
|
|
507
|
+
} else {
|
|
508
|
+
return `"(ao-type-float) ${formatFloat(subItem)}"`
|
|
509
|
+
}
|
|
510
|
+
} else if (typeof subItem === "string") {
|
|
511
|
+
return `"${subItem}"`
|
|
512
|
+
} else if (subItem === null) {
|
|
513
|
+
return `"(ao-type-atom) \\"null\\""`
|
|
514
|
+
} else if (subItem === undefined) {
|
|
515
|
+
return `"(ao-type-atom) \\"undefined\\""`
|
|
516
|
+
} else if (typeof subItem === "symbol") {
|
|
517
|
+
const desc = subItem.description || "Symbol.for()"
|
|
518
|
+
return `"(ao-type-atom) \\"${desc}\\""`
|
|
519
|
+
} else if (typeof subItem === "boolean") {
|
|
520
|
+
return `"(ao-type-atom) \\"${subItem}\\""`
|
|
521
|
+
} else if (Array.isArray(subItem)) {
|
|
522
|
+
return encodeArrayItem(subItem)
|
|
523
|
+
} else if (isBytes(subItem)) {
|
|
524
|
+
const buffer = toBuffer(subItem)
|
|
525
|
+
if (buffer.length === 0 || buffer.byteLength === 0) {
|
|
526
|
+
return `""`
|
|
527
|
+
}
|
|
528
|
+
return `"(ao-type-binary)"`
|
|
529
|
+
} else if (isPojo(subItem)) {
|
|
530
|
+
const json = JSON.stringify(subItem)
|
|
531
|
+
const escaped = json.replace(/\\/g, "\\\\").replace(/"/g, '\\"')
|
|
532
|
+
return `"(ao-type-map) ${escaped}"`
|
|
533
|
+
} else {
|
|
534
|
+
return `"${String(subItem)}"`
|
|
535
|
+
}
|
|
536
|
+
})
|
|
537
|
+
.join(", ")
|
|
538
|
+
fieldLines.push(`${index}: ${encodedItems}`)
|
|
539
|
+
}
|
|
540
|
+
} else if (typeof item === "number") {
|
|
541
|
+
if (Number.isInteger(item)) {
|
|
542
|
+
partTypes.push(`${index}="integer"`)
|
|
543
|
+
fieldLines.push(`${index}: ${item}`)
|
|
544
|
+
} else {
|
|
545
|
+
partTypes.push(`${index}="float"`)
|
|
546
|
+
fieldLines.push(`${index}: ${formatFloat(item)}`)
|
|
547
|
+
}
|
|
548
|
+
} else if (typeof item === "string") {
|
|
549
|
+
fieldLines.push(`${index}: ${item}`)
|
|
550
|
+
} else if (
|
|
551
|
+
item === null ||
|
|
552
|
+
item === undefined ||
|
|
553
|
+
typeof item === "symbol" ||
|
|
554
|
+
typeof item === "boolean"
|
|
555
|
+
) {
|
|
556
|
+
partTypes.push(`${index}="atom"`)
|
|
557
|
+
if (item === null) {
|
|
558
|
+
fieldLines.push(`${index}: null`)
|
|
559
|
+
} else if (item === undefined) {
|
|
560
|
+
fieldLines.push(`${index}: undefined`)
|
|
561
|
+
} else if (typeof item === "symbol") {
|
|
562
|
+
const desc = item.description || "Symbol.for()"
|
|
563
|
+
fieldLines.push(`${index}: ${desc}`)
|
|
564
|
+
} else {
|
|
565
|
+
fieldLines.push(`${index}: ${item}`)
|
|
566
|
+
}
|
|
567
|
+
} else if (isBytes(item)) {
|
|
568
|
+
const buffer = toBuffer(item)
|
|
569
|
+
if (buffer.length === 0) {
|
|
570
|
+
partTypes.push(`${index}="empty-binary"`)
|
|
571
|
+
} else {
|
|
572
|
+
partTypes.push(`${index}="binary"`)
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
})
|
|
576
|
+
|
|
577
|
+
return { fieldLines, partTypes }
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
// Step 13.2.3.5: Create array body part
|
|
581
|
+
function createArrayBodyPart(bodyKey, fieldLines, partTypes, headers) {
|
|
582
|
+
const isInline = bodyKey === "body" && headers["inline-body-key"] === "body"
|
|
583
|
+
|
|
584
|
+
if (isInline) {
|
|
585
|
+
const orderedLines = []
|
|
586
|
+
if (partTypes.length > 0) {
|
|
587
|
+
orderedLines.push(
|
|
588
|
+
`ao-types: ${sortTypeAnnotations(partTypes).join(", ")}`
|
|
589
|
+
)
|
|
590
|
+
}
|
|
591
|
+
orderedLines.push("content-disposition: inline")
|
|
592
|
+
if (fieldLines.length > 0) {
|
|
593
|
+
orderedLines.push("")
|
|
594
|
+
for (const line of fieldLines) {
|
|
595
|
+
orderedLines.push(line)
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
return new Blob([orderedLines.join("\r\n") + "\r\n"])
|
|
599
|
+
} else {
|
|
600
|
+
const orderedLines = []
|
|
601
|
+
if (partTypes.length > 0) {
|
|
602
|
+
orderedLines.push(
|
|
603
|
+
`ao-types: ${sortTypeAnnotations(partTypes).join(", ")}`
|
|
604
|
+
)
|
|
605
|
+
}
|
|
606
|
+
orderedLines.push(`content-disposition: form-data;name="${bodyKey}"`)
|
|
607
|
+
for (const line of fieldLines) {
|
|
608
|
+
orderedLines.push(line)
|
|
609
|
+
}
|
|
610
|
+
if (fieldLines.length > 0) {
|
|
611
|
+
return new Blob([orderedLines.join("\r\n") + "\r\n"])
|
|
612
|
+
} else {
|
|
613
|
+
return new Blob([orderedLines.join("\r\n")])
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
// Step 13.2.3: Handle array values
|
|
619
|
+
function handleArrayValue(bodyKey, value, headers, sortedBodyKeys, pathParts) {
|
|
620
|
+
const arrayInfo = analyzeArray(value)
|
|
621
|
+
|
|
622
|
+
if (arrayInfo.hasOnlyNonEmptyObjects) {
|
|
623
|
+
return null
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
if (arrayInfo.hasOnlyEmptyElements) {
|
|
627
|
+
return handleArrayWithOnlyEmptyElements(
|
|
628
|
+
bodyKey,
|
|
629
|
+
value,
|
|
630
|
+
headers,
|
|
631
|
+
sortedBodyKeys
|
|
632
|
+
)
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
const indicesWithOwnParts = buildIndicesWithOwnParts(bodyKey, sortedBodyKeys)
|
|
636
|
+
const hasNestedObjectParts = sortedBodyKeys.some(
|
|
637
|
+
key =>
|
|
638
|
+
key.startsWith(bodyKey + "/") &&
|
|
639
|
+
key.split("/").length > pathParts.length + 1
|
|
640
|
+
)
|
|
641
|
+
|
|
642
|
+
const { fieldLines, partTypes } = processArrayItems(
|
|
643
|
+
value,
|
|
644
|
+
indicesWithOwnParts,
|
|
645
|
+
hasNestedObjectParts,
|
|
646
|
+
pathParts
|
|
647
|
+
)
|
|
648
|
+
return createArrayBodyPart(bodyKey, fieldLines, partTypes, headers)
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
// Step 13.2.4.3: Process object fields
|
|
652
|
+
function processObjectFields(value, bodyKey, sortedBodyKeys) {
|
|
653
|
+
const objectTypes = []
|
|
654
|
+
const fieldLines = []
|
|
655
|
+
const binaryFields = []
|
|
656
|
+
const arrayTypes = []
|
|
657
|
+
|
|
658
|
+
// First collect array types
|
|
659
|
+
for (const [k, v] of Object.entries(value)) {
|
|
660
|
+
if (Array.isArray(v)) {
|
|
661
|
+
arrayTypes.push(
|
|
662
|
+
`${k.toLowerCase()}="${v.length === 0 ? "empty-list" : "list"}"`
|
|
663
|
+
)
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
// Then process other fields
|
|
668
|
+
for (const [k, v] of Object.entries(value)) {
|
|
669
|
+
const childPath = `${bodyKey}/${k}`
|
|
670
|
+
|
|
671
|
+
if (sortedBodyKeys.includes(childPath)) {
|
|
672
|
+
continue
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
if (Array.isArray(v) && v.some(item => isPojo(item))) {
|
|
676
|
+
const hasOnlyEmpty = v.every(item => isEmpty(item))
|
|
677
|
+
if (hasOnlyEmpty) {
|
|
678
|
+
continue
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
if (Array.isArray(v)) {
|
|
683
|
+
// Type already added in arrayTypes
|
|
684
|
+
} else if (
|
|
685
|
+
v === null ||
|
|
686
|
+
v === undefined ||
|
|
687
|
+
typeof v === "symbol" ||
|
|
688
|
+
typeof v === "boolean"
|
|
689
|
+
) {
|
|
690
|
+
objectTypes.push(`${k.toLowerCase()}="atom"`)
|
|
691
|
+
} else if (typeof v === "number") {
|
|
692
|
+
objectTypes.push(
|
|
693
|
+
`${k.toLowerCase()}="${Number.isInteger(v) ? "integer" : "float"}"`
|
|
694
|
+
)
|
|
695
|
+
} else if (typeof v === "string" && v.length === 0) {
|
|
696
|
+
objectTypes.push(`${k.toLowerCase()}="empty-binary"`)
|
|
697
|
+
} else if (isBytes(v) && (v.length === 0 || v.byteLength === 0)) {
|
|
698
|
+
objectTypes.push(`${k.toLowerCase()}="empty-binary"`)
|
|
699
|
+
} else if (isPojo(v) && Object.keys(v).length === 0) {
|
|
700
|
+
objectTypes.push(`${k.toLowerCase()}="empty-message"`)
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
if (typeof v === "string") {
|
|
704
|
+
if (v.length === 0) {
|
|
705
|
+
fieldLines.push(`${k}: `)
|
|
706
|
+
} else {
|
|
707
|
+
fieldLines.push(`${k}: ${v}`)
|
|
708
|
+
}
|
|
709
|
+
} else if (typeof v === "number") {
|
|
710
|
+
fieldLines.push(`${k}: ${v}`)
|
|
711
|
+
} else if (typeof v === "boolean") {
|
|
712
|
+
fieldLines.push(`${k}: "${v}"`)
|
|
713
|
+
} else if (v === null) {
|
|
714
|
+
fieldLines.push(`${k}: "null"`)
|
|
715
|
+
} else if (v === undefined) {
|
|
716
|
+
fieldLines.push(`${k}: "undefined"`)
|
|
717
|
+
} else if (typeof v === "symbol") {
|
|
718
|
+
const desc = v.description || "Symbol.for()"
|
|
719
|
+
fieldLines.push(`${k}: "${desc}"`)
|
|
720
|
+
} else if (isBytes(v)) {
|
|
721
|
+
const buffer = toBuffer(v)
|
|
722
|
+
binaryFields.push({ key: k, buffer })
|
|
723
|
+
} else if (Array.isArray(v) && v.length > 0) {
|
|
724
|
+
const childPath = `${bodyKey}/${k}`
|
|
725
|
+
if (!sortedBodyKeys.includes(childPath)) {
|
|
726
|
+
const hasObjects = v.some(item => isPojo(item))
|
|
727
|
+
if (!hasObjects) {
|
|
728
|
+
const encodedItems = v.map(item => encodeArrayItem(item)).join(", ")
|
|
729
|
+
fieldLines.push(`${k}: ${encodedItems}`)
|
|
730
|
+
}
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
const allTypes = [...arrayTypes, ...objectTypes]
|
|
736
|
+
return { allTypes, fieldLines, binaryFields }
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
// Step 13.2.4.5: Create object body part
|
|
740
|
+
function createObjectBodyPart(
|
|
741
|
+
bodyKey,
|
|
742
|
+
value,
|
|
743
|
+
allTypes,
|
|
744
|
+
fieldLines,
|
|
745
|
+
binaryFields,
|
|
746
|
+
headers,
|
|
747
|
+
sortedBodyKeys
|
|
748
|
+
) {
|
|
749
|
+
const isInline = bodyKey === "body" && headers["inline-body-key"] === "body"
|
|
750
|
+
const lines = []
|
|
751
|
+
|
|
752
|
+
if (isInline) {
|
|
753
|
+
const orderedLines = []
|
|
754
|
+
|
|
755
|
+
// For inline mode: fields first, then headers
|
|
756
|
+
for (const line of fieldLines) {
|
|
757
|
+
orderedLines.push(line)
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
if (allTypes.length > 0) {
|
|
761
|
+
orderedLines.push(`ao-types: ${allTypes.sort().join(", ")}`)
|
|
762
|
+
}
|
|
763
|
+
orderedLines.push("content-disposition: inline")
|
|
764
|
+
|
|
765
|
+
const binaryFieldsForInline = Object.entries(value)
|
|
766
|
+
.filter(
|
|
767
|
+
([k, v]) => isBytes(v) && !sortedBodyKeys.includes(`${bodyKey}/${k}`)
|
|
768
|
+
)
|
|
769
|
+
.map(([k, v]) => ({
|
|
770
|
+
key: k,
|
|
771
|
+
buffer: toBuffer(v),
|
|
772
|
+
}))
|
|
773
|
+
|
|
774
|
+
if (binaryFieldsForInline.length > 0) {
|
|
775
|
+
const parts = []
|
|
776
|
+
// Join all text lines first
|
|
777
|
+
parts.push(Buffer.from(orderedLines.join("\r\n")))
|
|
778
|
+
// Then add binary fields
|
|
779
|
+
for (const { key, buffer } of binaryFieldsForInline) {
|
|
780
|
+
parts.push(Buffer.from(`\r\n${key}: `))
|
|
781
|
+
parts.push(buffer)
|
|
782
|
+
}
|
|
783
|
+
parts.push(Buffer.from("\r\n"))
|
|
784
|
+
const fullBody = Buffer.concat(parts)
|
|
785
|
+
return new Blob([fullBody])
|
|
786
|
+
} else {
|
|
787
|
+
return new Blob([orderedLines.join("\r\n") + "\r\n"])
|
|
788
|
+
}
|
|
789
|
+
} else {
|
|
790
|
+
// Non-inline mode remains the same
|
|
791
|
+
const orderedLines = []
|
|
792
|
+
if (allTypes.length > 0) {
|
|
793
|
+
orderedLines.push(`ao-types: ${allTypes.sort().join(", ")}`)
|
|
794
|
+
}
|
|
795
|
+
orderedLines.push(`content-disposition: form-data;name="${bodyKey}"`)
|
|
796
|
+
|
|
797
|
+
const hasBinaryFields = binaryFields && binaryFields.length > 0
|
|
798
|
+
if (hasBinaryFields || fieldLines.length === 0) {
|
|
799
|
+
orderedLines.push("")
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
for (const line of fieldLines) {
|
|
803
|
+
orderedLines.push(line)
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
if (binaryFields && binaryFields.length > 0) {
|
|
807
|
+
const parts = []
|
|
808
|
+
const headerText = orderedLines.join("\r\n")
|
|
809
|
+
parts.push(Buffer.from(headerText))
|
|
810
|
+
for (let i = 0; i < binaryFields.length; i++) {
|
|
811
|
+
const { key, buffer } = binaryFields[i]
|
|
812
|
+
if (i > 0) {
|
|
813
|
+
parts.push(Buffer.from("\r\n"))
|
|
814
|
+
}
|
|
815
|
+
parts.push(Buffer.from(`${key}: `))
|
|
816
|
+
parts.push(buffer)
|
|
817
|
+
}
|
|
818
|
+
parts.push(Buffer.from("\r\n"))
|
|
819
|
+
const fullBody = Buffer.concat(parts)
|
|
820
|
+
return new Blob([fullBody])
|
|
821
|
+
} else {
|
|
822
|
+
if (fieldLines.length > 0) {
|
|
823
|
+
return new Blob([orderedLines.join("\r\n") + "\r\n"])
|
|
824
|
+
} else {
|
|
825
|
+
return new Blob([orderedLines.join("\r\n")])
|
|
826
|
+
}
|
|
827
|
+
}
|
|
828
|
+
}
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
// Step 13.2.4: Handle object values
|
|
832
|
+
function handleObjectValue(
|
|
833
|
+
obj,
|
|
834
|
+
bodyKey,
|
|
835
|
+
value,
|
|
836
|
+
headers,
|
|
837
|
+
sortedBodyKeys,
|
|
838
|
+
pathParts,
|
|
839
|
+
hasSpecialDataBody
|
|
840
|
+
) {
|
|
841
|
+
if (Object.keys(value).length === 0) {
|
|
842
|
+
// Skip empty objects in certain contexts
|
|
843
|
+
const parentPath = pathParts.slice(0, -1).join("/")
|
|
844
|
+
const parentValue = parentPath ? getValueByPath(obj, parentPath) : obj
|
|
845
|
+
|
|
846
|
+
if (Array.isArray(parentValue)) {
|
|
847
|
+
const parentArrayInfo = analyzeArray(parentValue)
|
|
848
|
+
if (
|
|
849
|
+
parentArrayInfo.hasObjects &&
|
|
850
|
+
(parentArrayInfo.hasEmptyStrings || parentArrayInfo.hasEmptyObjects)
|
|
851
|
+
) {
|
|
852
|
+
return null
|
|
853
|
+
}
|
|
854
|
+
}
|
|
855
|
+
return null
|
|
856
|
+
}
|
|
857
|
+
|
|
858
|
+
// Skip special data/body case
|
|
859
|
+
if (
|
|
860
|
+
hasSpecialDataBody &&
|
|
861
|
+
bodyKey === "data" &&
|
|
862
|
+
Object.keys(value).length === 1 &&
|
|
863
|
+
value.body &&
|
|
864
|
+
isBytes(value.body)
|
|
865
|
+
) {
|
|
866
|
+
return null
|
|
867
|
+
}
|
|
868
|
+
|
|
869
|
+
const { allTypes, fieldLines, binaryFields } = processObjectFields(
|
|
870
|
+
value,
|
|
871
|
+
bodyKey,
|
|
872
|
+
sortedBodyKeys
|
|
873
|
+
)
|
|
874
|
+
|
|
875
|
+
// Check if object should be skipped
|
|
876
|
+
const hasOnlyEmptyCollections = Object.entries(value).every(([k, v]) =>
|
|
877
|
+
isEmpty(v)
|
|
878
|
+
)
|
|
879
|
+
const hasArraysWithOnlyEmptyElements = Object.entries(value).some(
|
|
880
|
+
([k, v]) =>
|
|
881
|
+
Array.isArray(v) && v.length > 0 && v.every(item => isEmpty(item))
|
|
882
|
+
)
|
|
883
|
+
|
|
884
|
+
const shouldSkipObject = Object.entries(value).every(([k, v]) => {
|
|
885
|
+
const childPath = `${bodyKey}/${k}`
|
|
886
|
+
if (sortedBodyKeys.includes(childPath)) return true
|
|
887
|
+
if (Array.isArray(v) && v.some(item => isPojo(item))) {
|
|
888
|
+
const hasOnlyEmpty = v.every(item => isEmpty(item))
|
|
889
|
+
return hasOnlyEmpty || sortedBodyKeys.includes(childPath)
|
|
890
|
+
}
|
|
891
|
+
return false
|
|
892
|
+
})
|
|
893
|
+
|
|
894
|
+
if (
|
|
895
|
+
shouldSkipObject &&
|
|
896
|
+
!hasOnlyEmptyCollections &&
|
|
897
|
+
!hasArraysWithOnlyEmptyElements
|
|
898
|
+
) {
|
|
899
|
+
return null
|
|
900
|
+
}
|
|
901
|
+
|
|
902
|
+
return createObjectBodyPart(
|
|
903
|
+
bodyKey,
|
|
904
|
+
value,
|
|
905
|
+
allTypes,
|
|
906
|
+
fieldLines,
|
|
907
|
+
binaryFields,
|
|
908
|
+
headers,
|
|
909
|
+
sortedBodyKeys
|
|
910
|
+
)
|
|
911
|
+
}
|
|
912
|
+
|
|
913
|
+
// Step 13.2.5: Handle primitive values
|
|
914
|
+
function handlePrimitiveValue(bodyKey, value, headers) {
|
|
915
|
+
const isInline = bodyKey === "body" && headers["inline-body-key"] === "body"
|
|
916
|
+
const lines = []
|
|
917
|
+
|
|
918
|
+
if (isInline) {
|
|
919
|
+
lines.push(`content-disposition: inline`)
|
|
920
|
+
} else {
|
|
921
|
+
lines.push(`content-disposition: form-data;name="${bodyKey}"`)
|
|
922
|
+
}
|
|
923
|
+
|
|
924
|
+
if (typeof value === "string") {
|
|
925
|
+
lines.push("")
|
|
926
|
+
lines.push(value)
|
|
927
|
+
return new Blob([lines.join("\r\n")])
|
|
928
|
+
} else {
|
|
929
|
+
const content = encodePrimitiveContent(value)
|
|
930
|
+
lines.push("")
|
|
931
|
+
lines.push(content)
|
|
932
|
+
return new Blob([lines.join("\r\n")])
|
|
933
|
+
}
|
|
934
|
+
}
|
|
935
|
+
|
|
936
|
+
// Step 13.2.6: Handle binary values
|
|
937
|
+
function handleBinaryValue(bodyKey, value, headers) {
|
|
938
|
+
const isInline = bodyKey === "body" && headers["inline-body-key"] === "body"
|
|
939
|
+
const lines = []
|
|
940
|
+
|
|
941
|
+
if (isInline) {
|
|
942
|
+
lines.push(`content-disposition: inline`)
|
|
943
|
+
} else {
|
|
944
|
+
lines.push(`content-disposition: form-data;name="${bodyKey}"`)
|
|
945
|
+
}
|
|
946
|
+
|
|
947
|
+
const buffer = toBuffer(value)
|
|
948
|
+
// Always keep binary data as raw binary, regardless of whether it's in an array
|
|
949
|
+
const headerText = lines.join("\r\n") + "\r\n\r\n"
|
|
950
|
+
return new Blob([headerText, buffer])
|
|
951
|
+
}
|
|
952
|
+
|
|
953
|
+
// Step 13.3: Handle special data/body case
|
|
954
|
+
function handleSpecialDataBodyCase(obj, hasSpecialDataBody) {
|
|
955
|
+
if (
|
|
956
|
+
hasSpecialDataBody &&
|
|
957
|
+
obj.data &&
|
|
958
|
+
obj.data.body &&
|
|
959
|
+
isBytes(obj.data.body)
|
|
960
|
+
) {
|
|
961
|
+
const buffer = toBuffer(obj.data.body)
|
|
962
|
+
const specialPart = [
|
|
963
|
+
`content-disposition: form-data;name="data/body"`,
|
|
964
|
+
"",
|
|
965
|
+
"",
|
|
966
|
+
].join("\r\n")
|
|
967
|
+
return new Blob([specialPart, buffer])
|
|
968
|
+
}
|
|
969
|
+
return null
|
|
970
|
+
}
|
|
971
|
+
|
|
972
|
+
// Step 13: Build body parts for each body key
|
|
973
|
+
function buildBodyParts(obj, sortedBodyKeys, headers, hasSpecialDataBody) {
|
|
974
|
+
// Step 13.1: Initialize body parts collection
|
|
975
|
+
const bodyParts = []
|
|
976
|
+
|
|
977
|
+
// Step 13.2: Process each body key
|
|
978
|
+
for (const bodyKey of sortedBodyKeys) {
|
|
979
|
+
// Step 13.2.1: Get value for current body key
|
|
980
|
+
const value = getValueByPath(obj, bodyKey)
|
|
981
|
+
const pathParts = bodyKey.split("/")
|
|
982
|
+
|
|
983
|
+
// Step 13.2.2: Handle empty string in nested path
|
|
984
|
+
const emptyStringPart = handleEmptyStringInNestedPath(
|
|
985
|
+
bodyKey,
|
|
986
|
+
value,
|
|
987
|
+
pathParts
|
|
988
|
+
)
|
|
989
|
+
if (emptyStringPart) {
|
|
990
|
+
bodyParts.push(emptyStringPart)
|
|
991
|
+
continue
|
|
992
|
+
}
|
|
993
|
+
|
|
994
|
+
// Step 13.2.3: Handle array values
|
|
995
|
+
if (Array.isArray(value)) {
|
|
996
|
+
const arrayPart = handleArrayValue(
|
|
997
|
+
bodyKey,
|
|
998
|
+
value,
|
|
999
|
+
headers,
|
|
1000
|
+
sortedBodyKeys,
|
|
1001
|
+
pathParts
|
|
1002
|
+
)
|
|
1003
|
+
if (arrayPart) {
|
|
1004
|
+
bodyParts.push(arrayPart)
|
|
1005
|
+
}
|
|
1006
|
+
continue
|
|
1007
|
+
}
|
|
1008
|
+
|
|
1009
|
+
// Step 13.2.4: Handle object values
|
|
1010
|
+
if (isPojo(value)) {
|
|
1011
|
+
const objectPart = handleObjectValue(
|
|
1012
|
+
obj,
|
|
1013
|
+
bodyKey,
|
|
1014
|
+
value,
|
|
1015
|
+
headers,
|
|
1016
|
+
sortedBodyKeys,
|
|
1017
|
+
pathParts,
|
|
1018
|
+
hasSpecialDataBody
|
|
1019
|
+
)
|
|
1020
|
+
if (objectPart) {
|
|
1021
|
+
bodyParts.push(objectPart)
|
|
1022
|
+
}
|
|
1023
|
+
continue
|
|
1024
|
+
}
|
|
1025
|
+
|
|
1026
|
+
// Step 13.2.5: Handle primitive values
|
|
1027
|
+
if (
|
|
1028
|
+
typeof value === "string" ||
|
|
1029
|
+
typeof value === "boolean" ||
|
|
1030
|
+
typeof value === "number" ||
|
|
1031
|
+
value === null ||
|
|
1032
|
+
value === undefined ||
|
|
1033
|
+
typeof value === "symbol"
|
|
1034
|
+
) {
|
|
1035
|
+
const primitivePart = handlePrimitiveValue(bodyKey, value, headers)
|
|
1036
|
+
bodyParts.push(primitivePart)
|
|
1037
|
+
continue
|
|
1038
|
+
}
|
|
1039
|
+
|
|
1040
|
+
// Step 13.2.6: Handle binary values
|
|
1041
|
+
if (isBytes(value)) {
|
|
1042
|
+
const binaryPart = handleBinaryValue(bodyKey, value, headers)
|
|
1043
|
+
bodyParts.push(binaryPart)
|
|
1044
|
+
continue
|
|
1045
|
+
}
|
|
1046
|
+
}
|
|
1047
|
+
|
|
1048
|
+
// Step 13.3: Handle special data/body case
|
|
1049
|
+
const specialPart = handleSpecialDataBodyCase(obj, hasSpecialDataBody)
|
|
1050
|
+
if (specialPart) {
|
|
1051
|
+
bodyParts.push(specialPart)
|
|
1052
|
+
}
|
|
1053
|
+
|
|
1054
|
+
// Step 13.4: Return body parts
|
|
1055
|
+
return bodyParts
|
|
1056
|
+
}
|
|
1057
|
+
|
|
1058
|
+
// Step 14: Generate multipart boundary
|
|
1059
|
+
async function generateBoundary(bodyParts) {
|
|
1060
|
+
const partsContent = await Promise.all(bodyParts.map(part => part.text()))
|
|
1061
|
+
const allContent = partsContent.join("")
|
|
1062
|
+
const boundaryHash = await sha256(new TextEncoder().encode(allContent))
|
|
1063
|
+
const boundary = base64url.encode(Buffer.from(boundaryHash))
|
|
1064
|
+
return boundary
|
|
1065
|
+
}
|
|
1066
|
+
|
|
1067
|
+
// Step 15: Assemble final multipart body
|
|
1068
|
+
function assembleMultipartBody(bodyParts, boundary) {
|
|
1069
|
+
const finalParts = []
|
|
1070
|
+
for (let i = 0; i < bodyParts.length; i++) {
|
|
1071
|
+
if (i === 0) {
|
|
1072
|
+
finalParts.push(new Blob([`--${boundary}\r\n`]))
|
|
1073
|
+
} else {
|
|
1074
|
+
finalParts.push(new Blob([`\r\n--${boundary}\r\n`]))
|
|
1075
|
+
}
|
|
1076
|
+
finalParts.push(bodyParts[i])
|
|
1077
|
+
}
|
|
1078
|
+
finalParts.push(new Blob([`\r\n--${boundary}--`]))
|
|
1079
|
+
|
|
1080
|
+
return new Blob(finalParts)
|
|
1081
|
+
}
|
|
1082
|
+
|
|
1083
|
+
// Step 16: Calculate content digest
|
|
1084
|
+
async function calculateContentDigest(body) {
|
|
1085
|
+
const finalContent = await body.arrayBuffer()
|
|
1086
|
+
|
|
1087
|
+
if (finalContent.byteLength > 0) {
|
|
1088
|
+
const contentDigest = await sha256(finalContent)
|
|
1089
|
+
const base64 = base64url.toBase64(base64url.encode(contentDigest))
|
|
1090
|
+
return { digest: base64, byteLength: finalContent.byteLength }
|
|
1091
|
+
}
|
|
1092
|
+
|
|
1093
|
+
return { digest: null, byteLength: finalContent.byteLength }
|
|
1094
|
+
}
|
|
1095
|
+
|
|
1096
|
+
// Step 17: Set final headers (content-type, content-length)
|
|
1097
|
+
function setFinalHeaders(headers, boundary, contentDigest, byteLength) {
|
|
1098
|
+
headers["content-type"] = `multipart/form-data; boundary="${boundary}"`
|
|
1099
|
+
|
|
1100
|
+
if (contentDigest) {
|
|
1101
|
+
headers["content-digest"] = `sha-256=:${contentDigest}:`
|
|
1102
|
+
}
|
|
1103
|
+
|
|
1104
|
+
headers["content-length"] = String(byteLength)
|
|
1105
|
+
}
|
|
1106
|
+
|
|
1107
|
+
export async function enc(obj = {}) {
|
|
1108
|
+
// Step 1: Process and normalize input values
|
|
1109
|
+
const processedObj = processInputValues(obj)
|
|
1110
|
+
|
|
1111
|
+
// Step 2: Handle empty object case
|
|
1112
|
+
const emptyResult = handleEmptyObject(processedObj)
|
|
1113
|
+
if (emptyResult) return emptyResult
|
|
1114
|
+
|
|
1115
|
+
// Step 3: Handle single field with empty binary
|
|
1116
|
+
const emptyBinaryResult = handleSingleEmptyBinaryField(processedObj)
|
|
1117
|
+
if (emptyBinaryResult) return emptyBinaryResult
|
|
1118
|
+
|
|
1119
|
+
// Step 4: Handle single field with binary data
|
|
1120
|
+
const singleBinaryResult = await handleSingleBinaryField(processedObj)
|
|
1121
|
+
if (singleBinaryResult) return singleBinaryResult
|
|
1122
|
+
|
|
1123
|
+
// Step 5: Handle single field with primitive value
|
|
1124
|
+
const primitiveResult = await handleSinglePrimitiveField(processedObj)
|
|
1125
|
+
if (primitiveResult) return primitiveResult
|
|
1126
|
+
|
|
1127
|
+
// Step 6a: Handle single field with non-empty binary
|
|
1128
|
+
const nonEmptyBinaryResult =
|
|
1129
|
+
await handleSingleNonEmptyBinaryField(processedObj)
|
|
1130
|
+
if (nonEmptyBinaryResult) return nonEmptyBinaryResult
|
|
1131
|
+
|
|
1132
|
+
// Step 6: Handle single field with non-ASCII string
|
|
1133
|
+
const nonAsciiResult = await handleSingleNonAsciiStringField(processedObj)
|
|
1134
|
+
if (nonAsciiResult) return nonAsciiResult
|
|
1135
|
+
|
|
1136
|
+
// Step 7: Collect all keys that need to go in body
|
|
1137
|
+
const bodyKeys = collectBodyKeysStep(processedObj)
|
|
1138
|
+
|
|
1139
|
+
const objKeys = Object.keys(obj)
|
|
1140
|
+
const headers = {}
|
|
1141
|
+
const headerTypes = []
|
|
1142
|
+
|
|
1143
|
+
// Step 8: Process fields that can go in headers
|
|
1144
|
+
processHeaderFields(obj, bodyKeys, headers, headerTypes)
|
|
1145
|
+
|
|
1146
|
+
// Step 9: Handle case where all body keys are empty binaries
|
|
1147
|
+
const emptyBinaryBodyResult = handleAllEmptyBinaryBodyKeys(
|
|
1148
|
+
obj,
|
|
1149
|
+
bodyKeys,
|
|
1150
|
+
headers,
|
|
1151
|
+
headerTypes
|
|
1152
|
+
)
|
|
1153
|
+
if (emptyBinaryBodyResult) return emptyBinaryBodyResult
|
|
1154
|
+
|
|
1155
|
+
// Step 10: Handle single body key optimization
|
|
1156
|
+
const singleBodyKeyResult = await handleSingleBodyKeyOptimization(
|
|
1157
|
+
obj,
|
|
1158
|
+
bodyKeys,
|
|
1159
|
+
headers,
|
|
1160
|
+
headerTypes
|
|
1161
|
+
)
|
|
1162
|
+
if (singleBodyKeyResult) return singleBodyKeyResult
|
|
1163
|
+
|
|
1164
|
+
// Step 11: Sort body keys
|
|
1165
|
+
const sortedBodyKeys = sortBodyKeys(bodyKeys)
|
|
1166
|
+
|
|
1167
|
+
// Step 12: Check for special data/body case
|
|
1168
|
+
const hasSpecialDataBody = checkSpecialDataBodyCase(obj, sortedBodyKeys)
|
|
1169
|
+
|
|
1170
|
+
// Only add body-keys header if there are actual body keys
|
|
1171
|
+
if (sortedBodyKeys.length > 0) {
|
|
1172
|
+
headers["body-keys"] = sortedBodyKeys.map(k => `"${k}"`).join(", ")
|
|
1173
|
+
}
|
|
1174
|
+
|
|
1175
|
+
// Special case: single body key named "body" containing an object
|
|
1176
|
+
if (
|
|
1177
|
+
!hasSpecialDataBody &&
|
|
1178
|
+
sortedBodyKeys.length === 1 &&
|
|
1179
|
+
sortedBodyKeys[0] === "body"
|
|
1180
|
+
) {
|
|
1181
|
+
const bodyValue = obj.body
|
|
1182
|
+
if (isPojo(bodyValue)) {
|
|
1183
|
+
headers["inline-body-key"] = "body"
|
|
1184
|
+
}
|
|
1185
|
+
}
|
|
1186
|
+
|
|
1187
|
+
if (headerTypes.length > 0) {
|
|
1188
|
+
headers["ao-types"] = headerTypes.sort().join(", ")
|
|
1189
|
+
}
|
|
1190
|
+
|
|
1191
|
+
// Step 13: Build body parts for each body key
|
|
1192
|
+
const bodyParts = buildBodyParts(
|
|
1193
|
+
obj,
|
|
1194
|
+
sortedBodyKeys,
|
|
1195
|
+
headers,
|
|
1196
|
+
hasSpecialDataBody
|
|
1197
|
+
)
|
|
1198
|
+
|
|
1199
|
+
// If no body parts were created, return headers only
|
|
1200
|
+
if (bodyParts.length === 0) {
|
|
1201
|
+
return { headers, body: undefined }
|
|
1202
|
+
}
|
|
1203
|
+
|
|
1204
|
+
// Step 14: Generate multipart boundary
|
|
1205
|
+
const boundary = await generateBoundary(bodyParts)
|
|
1206
|
+
|
|
1207
|
+
// Step 15: Assemble final multipart body
|
|
1208
|
+
const body = assembleMultipartBody(bodyParts, boundary)
|
|
1209
|
+
|
|
1210
|
+
// Step 16: Calculate content digest
|
|
1211
|
+
const { digest: contentDigest, byteLength } =
|
|
1212
|
+
await calculateContentDigest(body)
|
|
1213
|
+
|
|
1214
|
+
// Step 17: Set final headers (content-type, content-length)
|
|
1215
|
+
setFinalHeaders(headers, boundary, contentDigest, byteLength)
|
|
1216
|
+
|
|
1217
|
+
// Step 18: Return result
|
|
1218
|
+
return { headers, body }
|
|
1219
|
+
}
|