wao 0.26.2 → 0.27.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/esm/encode.js ADDED
@@ -0,0 +1,701 @@
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
+ )
10
+ }
11
+
12
+ function isPojo(value) {
13
+ return (
14
+ !isBytes(value) &&
15
+ !Array.isArray(value) &&
16
+ !(value instanceof Blob) &&
17
+ typeof value === "object" &&
18
+ value !== null
19
+ )
20
+ }
21
+
22
+ function hbEncodeValue(value) {
23
+ if (isBytes(value)) {
24
+ const length = value.byteLength || value.length || 0
25
+ if (length === 0) return ["empty-binary", ""]
26
+ return [undefined, value]
27
+ }
28
+
29
+ if (typeof value === "string") {
30
+ if (value.length === 0) return ["empty-string", ""]
31
+ return [undefined, value]
32
+ }
33
+
34
+ if (Array.isArray(value)) {
35
+ if (value.length === 0) return ["empty-list", "[]"]
36
+
37
+ const hasObjects = value.some(item => isPojo(item))
38
+ if (hasObjects) {
39
+ return ["list_with_objects", value]
40
+ }
41
+
42
+ const items = value.map(item => {
43
+ if (typeof item === "string") {
44
+ return `"${item}"`
45
+ } else if (typeof item === "number") {
46
+ if (Number.isInteger(item)) {
47
+ return `"(ao-type-integer) ${item}"`
48
+ } else {
49
+ return `"(ao-type-float) ${item.toExponential(20).replace("e+0", "e+00")}"`
50
+ }
51
+ } else if (typeof item === "boolean") {
52
+ return `"(ao-type-atom) \\"${item ? "true" : "false"}\\""`
53
+ } else if (typeof item === "symbol") {
54
+ const desc = item.description || "symbol"
55
+ return `"(ao-type-atom) \\"${desc}\\""`
56
+ } else if (item === null) {
57
+ return `"(ao-type-atom) \\"null\\""`
58
+ } else if (item === undefined) {
59
+ return `"(ao-type-atom) \\"undefined\\""`
60
+ } else if (isBytes(item)) {
61
+ const length = item.byteLength || item.length || 0
62
+ if (length === 0) {
63
+ return `""`
64
+ } else {
65
+ const base64 = base64url.encode(Buffer.from(item))
66
+ return `"(ao-type-binary) ${base64}"`
67
+ }
68
+ } else if (Array.isArray(item)) {
69
+ const [, encoded] = hbEncodeValue(item)
70
+ const escapedEncoded = encoded.replace(/"/g, '\\"')
71
+ return `"(ao-type-list) ${escapedEncoded}"`
72
+ } else {
73
+ return `"${String(item)}"`
74
+ }
75
+ })
76
+
77
+ return ["list", items.join(", ")]
78
+ }
79
+
80
+ if (typeof value === "number") {
81
+ if (!Number.isInteger(value)) return ["float", `${value}`]
82
+ return ["integer", String(value)]
83
+ }
84
+
85
+ if (typeof value === "boolean") {
86
+ return ["atom", value ? "true" : "false"]
87
+ }
88
+
89
+ if (typeof value === "symbol") {
90
+ const desc = value.description || "symbol"
91
+ return ["atom", desc]
92
+ }
93
+
94
+ if (value === null) return ["atom", "null"]
95
+ if (value === undefined) return ["atom", "undefined"]
96
+
97
+ if (isPojo(value)) {
98
+ throw new Error("Objects must be lifted")
99
+ }
100
+
101
+ throw new Error(`Cannot encode value: ${String(value)}`)
102
+ }
103
+
104
+ const MAX_HEADER_LENGTH = 4096
105
+
106
+ function encode_body_keys(bodyKeys) {
107
+ if (!bodyKeys || bodyKeys.length === 0) return ""
108
+ const items = bodyKeys.map(key => {
109
+ const escaped = key.replace(/"/g, '\\"')
110
+ return `"${escaped}"`
111
+ })
112
+ return items.join(", ")
113
+ }
114
+
115
+ async function hasNewline(value) {
116
+ if (typeof value === "string") return value.includes("\n")
117
+ if (value instanceof Blob) {
118
+ value = await value.text()
119
+ return value.includes("\n")
120
+ }
121
+ if (isBytes(value)) return Buffer.from(value).includes("\n")
122
+ return false
123
+ }
124
+
125
+ async function sha256(data) {
126
+ let uint8Array
127
+ if (data instanceof ArrayBuffer) {
128
+ uint8Array = new Uint8Array(data)
129
+ } else if (data instanceof Uint8Array) {
130
+ uint8Array = data
131
+ } else if (ArrayBuffer.isView(data)) {
132
+ uint8Array = new Uint8Array(data.buffer, data.byteOffset, data.byteLength)
133
+ } else {
134
+ throw new Error("sha256 expects ArrayBuffer or ArrayBufferView")
135
+ }
136
+
137
+ const hashResult = hash(uint8Array)
138
+ return hashResult.buffer.slice(
139
+ hashResult.byteOffset,
140
+ hashResult.byteOffset + hashResult.byteLength
141
+ )
142
+ }
143
+
144
+ function collectParts(obj, path = "", parts = {}, types = {}) {
145
+ for (const [key, value] of Object.entries(obj)) {
146
+ const currentPath = path ? `${path}/${key}` : key
147
+
148
+ if (value === null || value === undefined) {
149
+ if (!parts[path]) parts[path] = {}
150
+ parts[path][key] = value === null ? '"null"' : '"undefined"'
151
+ types[currentPath] = "atom"
152
+ } else if (isBytes(value)) {
153
+ if (!parts[path]) parts[path] = {}
154
+ parts[path][key] = value
155
+
156
+ const length = value.byteLength || value.length || 0
157
+ if (length === 0) {
158
+ types[currentPath] = "empty-binary"
159
+ }
160
+ } else if (typeof value === "string") {
161
+ if (value.length === 0) {
162
+ types[currentPath] = "empty-binary"
163
+ if (!parts[path]) parts[path] = {}
164
+ parts[path][key] = ""
165
+ } else {
166
+ if (!parts[path]) parts[path] = {}
167
+ parts[path][key] = value
168
+ }
169
+ } else if (Array.isArray(value)) {
170
+ if (value.length === 0) {
171
+ types[currentPath] = "empty-list"
172
+ if (!parts[path]) parts[path] = {}
173
+ parts[path][key] = "[]"
174
+ } else {
175
+ const hasObjects = value.some(item => isPojo(item))
176
+
177
+ if (hasObjects) {
178
+ // Arrays with objects: create separate parts for each indexed item
179
+ types[currentPath] = "list"
180
+
181
+ value.forEach((item, index) => {
182
+ const indexKey = String(index + 1)
183
+ const indexPath = `${currentPath}/${indexKey}`
184
+
185
+ if (isPojo(item)) {
186
+ // Each object in the array becomes a separate part
187
+ if (!parts[indexPath]) parts[indexPath] = {}
188
+
189
+ for (const [objKey, objValue] of Object.entries(item)) {
190
+ parts[indexPath][objKey] = objValue
191
+
192
+ // Set types for object fields
193
+ const fieldPath = `${indexPath}/${objKey}`
194
+ if (typeof objValue === "number") {
195
+ types[fieldPath] = Number.isInteger(objValue)
196
+ ? "integer"
197
+ : "float"
198
+ } else if (typeof objValue === "boolean") {
199
+ types[fieldPath] = "atom"
200
+ } else if (typeof objValue === "symbol") {
201
+ types[fieldPath] = "atom"
202
+ // Store the symbol's description for later use
203
+ parts[indexPath][objKey] = objValue
204
+ } else if (
205
+ typeof objValue === "string" &&
206
+ objValue.length === 0
207
+ ) {
208
+ types[fieldPath] = "empty-binary"
209
+ } else if (Array.isArray(objValue) && objValue.length === 0) {
210
+ types[fieldPath] = "empty-list"
211
+ } else if (
212
+ isPojo(objValue) &&
213
+ Object.keys(objValue).length === 0
214
+ ) {
215
+ types[fieldPath] = "empty-message"
216
+ }
217
+ }
218
+ } else {
219
+ // Non-object items in the array
220
+ if (!parts[currentPath]) parts[currentPath] = {}
221
+ parts[currentPath][indexKey] = item
222
+
223
+ if (typeof item === "number") {
224
+ types[`${currentPath}/${indexKey}`] = Number.isInteger(item)
225
+ ? "integer"
226
+ : "float"
227
+ } else if (typeof item === "boolean") {
228
+ types[`${currentPath}/${indexKey}`] = "atom"
229
+ }
230
+ }
231
+ })
232
+ } else {
233
+ // Simple arrays without objects
234
+ const [type, encoded] = hbEncodeValue(value)
235
+ if (!parts[path]) parts[path] = {}
236
+ parts[path][key] = encoded
237
+ types[currentPath] = type || "list"
238
+ }
239
+ }
240
+ } else if (isPojo(value)) {
241
+ if (Object.keys(value).length === 0) {
242
+ types[currentPath] = "empty-message"
243
+ if (!parts[path]) parts[path] = {}
244
+ parts[path][key] = "{}"
245
+ } else {
246
+ const hasOnlyEmptyChildren = Object.entries(value).every(([k, v]) => {
247
+ return (
248
+ (Array.isArray(v) && v.length === 0) ||
249
+ (isPojo(v) && Object.keys(v).length === 0)
250
+ )
251
+ })
252
+
253
+ if (hasOnlyEmptyChildren) {
254
+ if (!parts[currentPath]) parts[currentPath] = {}
255
+ }
256
+
257
+ collectParts(value, currentPath, parts, types)
258
+ }
259
+ } else if (typeof value === "symbol") {
260
+ if (!parts[path]) parts[path] = {}
261
+ parts[path][key] = value.description || "symbol"
262
+ types[currentPath] = "atom"
263
+ } else if (typeof value === "boolean") {
264
+ if (!parts[path]) parts[path] = {}
265
+ parts[path][key] = value
266
+ types[currentPath] = "atom"
267
+ } else {
268
+ if (!parts[path]) parts[path] = {}
269
+ parts[path][key] = value
270
+ if (typeof value === "number") {
271
+ types[currentPath] = Number.isInteger(value) ? "integer" : "float"
272
+ }
273
+ }
274
+ }
275
+
276
+ return { parts, types }
277
+ }
278
+
279
+ async function encode(obj = {}) {
280
+ console.log("[encode] START with obj:", JSON.stringify(obj))
281
+
282
+ if (Object.keys(obj).length === 0) return { headers: {}, body: undefined }
283
+
284
+ // Check if we have a simple binary field
285
+ const objKeys = Object.keys(obj)
286
+ if (objKeys.length === 1 && isBytes(obj[objKeys[0]])) {
287
+ // Single binary field - return it directly
288
+ const fieldName = objKeys[0]
289
+ const binaryData = obj[fieldName]
290
+
291
+ const headers = {}
292
+ const bodyBuffer = Buffer.isBuffer(binaryData)
293
+ ? binaryData
294
+ : Buffer.from(binaryData)
295
+ const bodyArrayBuffer = bodyBuffer.buffer.slice(
296
+ bodyBuffer.byteOffset,
297
+ bodyBuffer.byteOffset + bodyBuffer.byteLength
298
+ )
299
+
300
+ const contentDigest = await sha256(bodyArrayBuffer)
301
+ const base64 = base64url.toBase64(base64url.encode(contentDigest))
302
+ headers["content-digest"] = `sha-256=:${base64}:`
303
+
304
+ console.log(
305
+ "[encode] FINAL (simple binary field) - headers:",
306
+ headers,
307
+ "body:",
308
+ binaryData
309
+ )
310
+ return { headers, body: binaryData }
311
+ }
312
+
313
+ if ("body" in obj && isBytes(obj.body)) {
314
+ const headers = {}
315
+ const types = []
316
+ let needsMultipart = false
317
+
318
+ for (const [key, value] of Object.entries(obj)) {
319
+ if (key === "body") continue
320
+
321
+ if (value === null) {
322
+ headers[key] = "null"
323
+ types.push(`${key}="atom"`)
324
+ } else if (value === undefined) {
325
+ headers[key] = "undefined"
326
+ types.push(`${key}="atom"`)
327
+ } else if (typeof value === "string" && value.length === 0) {
328
+ types.push(`${key}="empty-binary"`)
329
+ } else if (Array.isArray(value)) {
330
+ if (value.length === 0) {
331
+ types.push(`${key}="empty-list"`)
332
+ } else if (value.some(item => isPojo(item))) {
333
+ // Arrays with objects need multipart
334
+ types.push(`${key}="list"`)
335
+ needsMultipart = true
336
+ break
337
+ } else {
338
+ const [type, encoded] = hbEncodeValue(value)
339
+ headers[key] = encoded
340
+ types.push(`${key}="${type}"`)
341
+ }
342
+ } else if (isBytes(value)) {
343
+ if (value.length === 0 || value.byteLength === 0) {
344
+ types.push(`${key}="empty-binary"`)
345
+ } else {
346
+ needsMultipart = true
347
+ break
348
+ }
349
+ } else if (typeof value === "boolean") {
350
+ headers[key] = `"${value}"`
351
+ types.push(`${key}="atom"`)
352
+ } else if (typeof value === "symbol") {
353
+ headers[key] = value.description || "symbol"
354
+ types.push(`${key}="atom"`)
355
+ } else if (typeof value === "number") {
356
+ headers[key] = String(value)
357
+ types.push(`${key}="${Number.isInteger(value) ? "integer" : "float"}"`)
358
+ } else if (typeof value === "string") {
359
+ headers[key] = value
360
+ } else if (isPojo(value) && Object.keys(value).length === 0) {
361
+ types.push(`${key}="empty-message"`)
362
+ } else if (isPojo(value)) {
363
+ needsMultipart = true
364
+ break
365
+ }
366
+ }
367
+
368
+ if (needsMultipart) {
369
+ return encodeMultipart(obj)
370
+ }
371
+
372
+ if (types.length > 0) {
373
+ headers["ao-types"] = types.sort().join(", ")
374
+ }
375
+
376
+ const body = obj.body
377
+ const bodyBuffer = Buffer.isBuffer(body) ? body : Buffer.from(body)
378
+ const bodyArrayBuffer = bodyBuffer.buffer.slice(
379
+ bodyBuffer.byteOffset,
380
+ bodyBuffer.byteOffset + bodyBuffer.byteLength
381
+ )
382
+
383
+ const contentDigest = await sha256(bodyArrayBuffer)
384
+ const base64 = base64url.toBase64(base64url.encode(contentDigest))
385
+ headers["content-digest"] = `sha-256=:${base64}:`
386
+
387
+ console.log(
388
+ "[encode] FINAL (simple body encoding) - headers:",
389
+ headers,
390
+ "body:",
391
+ body
392
+ )
393
+ return { headers, body }
394
+ }
395
+
396
+ return encodeMultipart(obj)
397
+ }
398
+
399
+ async function encodeMultipart(obj) {
400
+ const { parts, types } = collectParts(obj)
401
+ console.log("[encode] Parts:", parts, "Types:", types)
402
+
403
+ const headers = {}
404
+ const bodyParts = []
405
+ const bodyKeys = []
406
+
407
+ // Collect header types for arrays with objects
408
+ const headerTypes = []
409
+ for (const [key, value] of Object.entries(obj)) {
410
+ if (Array.isArray(value) && value.some(item => isPojo(item))) {
411
+ headerTypes.push(`${key}="list"`)
412
+ }
413
+ }
414
+
415
+ for (const [path, content] of Object.entries(parts)) {
416
+ if (path === "") {
417
+ for (const [key, value] of Object.entries(content)) {
418
+ if (isBytes(value)) {
419
+ const length = value.byteLength || value.length || 0
420
+ if (length === 0) {
421
+ // Empty binaries stay in headers
422
+ console.log(`[encode] Empty binary field ${key} staying in headers`)
423
+ continue
424
+ } else {
425
+ console.log(`[encode] Binary field ${key} going to body`)
426
+ bodyKeys.push(key)
427
+ }
428
+ } else {
429
+ const valueStr = String(value)
430
+ const type = types[key]
431
+
432
+ if (type === "empty-binary" && valueStr === "") {
433
+ continue
434
+ }
435
+
436
+ if (
437
+ !(await hasNewline(valueStr)) &&
438
+ !key.includes("/") &&
439
+ Buffer.from(valueStr).byteLength <= MAX_HEADER_LENGTH &&
440
+ !isPojo(value) &&
441
+ !(key === "data" || key === "body") // Don't put inline keys in headers
442
+ ) {
443
+ if (typeof value === "number") {
444
+ headers[key] = String(value)
445
+ } else if (typeof value === "boolean") {
446
+ headers[key] = `"${value}"`
447
+ } else if (value === "[]" && type === "empty_list") {
448
+ headers[key] = "[]"
449
+ } else if (value === '"null"' || value === '"undefined"') {
450
+ headers[key] = value
451
+ } else {
452
+ headers[key] = value
453
+ }
454
+ } else {
455
+ bodyKeys.push(key)
456
+ }
457
+ }
458
+ }
459
+ } else {
460
+ bodyKeys.push(path)
461
+ }
462
+ }
463
+
464
+ console.log(
465
+ "[encode] Headers before filtering:",
466
+ headers,
467
+ "BodyKeys:",
468
+ bodyKeys
469
+ )
470
+
471
+ bodyKeys.sort()
472
+
473
+ // Add types for values in headers
474
+ for (const [key, value] of Object.entries(headers)) {
475
+ const type = types[key]
476
+ if (type) {
477
+ headerTypes.push(`${key}="${type}"`)
478
+ }
479
+ }
480
+
481
+ // Add types for empty values not in headers
482
+ for (const [key, type] of Object.entries(types)) {
483
+ if (!key.includes("/") && type.startsWith("empty") && !headers[key]) {
484
+ headerTypes.push(`${key}="${type.replace("empty_", "empty-")}"`)
485
+ }
486
+ }
487
+
488
+ if (headerTypes.length > 0) {
489
+ headers["ao-types"] = headerTypes.sort().join(", ")
490
+ }
491
+
492
+ if (bodyKeys.length > 0) {
493
+ headers["body-keys"] = encode_body_keys(bodyKeys)
494
+
495
+ if (bodyKeys.includes("data") || bodyKeys.includes("body")) {
496
+ const inlineKey = bodyKeys.find(k => k === "data" || k === "body")
497
+ headers["inline-body-key"] = inlineKey
498
+ }
499
+
500
+ // Create multipart body parts
501
+ for (const path of bodyKeys) {
502
+ console.log(`[encode] Processing bodyKey: ${path}`)
503
+
504
+ // Handle root-level fields
505
+ if (!path.includes("/")) {
506
+ // This is a root-level field
507
+ const fieldValue = parts[""] && parts[""][path]
508
+ console.log(`[encode] Root field ${path} value:`, fieldValue)
509
+
510
+ if (fieldValue !== undefined) {
511
+ if (isBytes(fieldValue)) {
512
+ // Binary field - create a simple multipart section
513
+ // The format should be:
514
+ // content-disposition: form-data;name="fieldname"
515
+ // [empty line]
516
+ // [binary data]
517
+ const headerStr = `content-disposition: form-data;name="${path}"\r\n\r\n`
518
+ const headerBlob = new Blob([headerStr])
519
+ const dataBlob = new Blob([fieldValue])
520
+ bodyParts.push(new Blob([headerBlob, dataBlob]))
521
+ console.log(`[encode] Added binary field ${path} to bodyParts`)
522
+ continue
523
+ } else {
524
+ // Non-binary root field that needs to go in body
525
+ const lines = []
526
+
527
+ // Check if this is an inline key
528
+ const isInlineKey = path === "data" || path === "body"
529
+
530
+ if (isInlineKey) {
531
+ lines.push(`content-disposition: inline`)
532
+ } else {
533
+ lines.push(`content-disposition: form-data;name="${path}"`)
534
+ }
535
+
536
+ // Add the field type if available
537
+ const type = types[path]
538
+ if (type) {
539
+ lines.push(`ao-types: ${path}="${type}"`)
540
+ }
541
+
542
+ // Add empty line before content
543
+ lines.push("")
544
+
545
+ // Add the actual value
546
+ lines.push(String(fieldValue))
547
+
548
+ bodyParts.push(new Blob([lines.join("\r\n")]))
549
+ console.log(`[encode] Added non-binary field ${path} to bodyParts`)
550
+ continue
551
+ }
552
+ }
553
+ }
554
+
555
+ // Handle nested paths
556
+ const content = parts[path]
557
+ if (!content) {
558
+ console.log(`[encode] No content found for path: ${path}`)
559
+ continue
560
+ }
561
+
562
+ const lines = []
563
+ const binaryParts = []
564
+ const isInlineKey =
565
+ (path === "data" || path === "body") && !path.includes("/")
566
+
567
+ const sortedKeys = Object.keys(content).sort()
568
+
569
+ // Collect ao-types for this part
570
+ const partTypes = []
571
+ for (const key of sortedKeys) {
572
+ const fullPath = path ? `${path}/${key}` : key
573
+ const type = types[fullPath]
574
+ if (type) {
575
+ partTypes.push(`${key}="${type}"`)
576
+ }
577
+ }
578
+
579
+ if (partTypes.length > 0) {
580
+ lines.push(`ao-types: ${partTypes.sort().join(", ")}`)
581
+ }
582
+
583
+ if (isInlineKey) {
584
+ lines.push(`content-disposition: inline`)
585
+ } else {
586
+ lines.push(`content-disposition: form-data;name="${path}"`)
587
+ }
588
+
589
+ // Add content fields
590
+ for (const key of sortedKeys) {
591
+ const value = content[key]
592
+ const fullPath = path ? `${path}/${key}` : key
593
+ const type = types[fullPath]
594
+
595
+ if (
596
+ type === "empty-message" ||
597
+ type === "empty-list" ||
598
+ type === "empty-binary"
599
+ ) {
600
+ continue
601
+ }
602
+
603
+ if (
604
+ (value === "[]" || value === "{}" || value === "") &&
605
+ type &&
606
+ type.startsWith("empty")
607
+ ) {
608
+ continue
609
+ }
610
+
611
+ if (isBytes(value)) {
612
+ binaryParts.push({ key, value })
613
+ } else if (type === "atom" && typeof value === "boolean") {
614
+ lines.push(`${key}: "${value}"`)
615
+ } else if (typeof value === "symbol") {
616
+ // Handle Symbol values
617
+ const symbolValue = value.description || "symbol"
618
+ lines.push(`${key}: ${symbolValue}`)
619
+ } else {
620
+ lines.push(`${key}: ${value}`)
621
+ }
622
+ }
623
+
624
+ if (lines.length >= 1 || binaryParts.length > 0) {
625
+ if (binaryParts.length > 0) {
626
+ const allParts = []
627
+
628
+ // Add text headers first
629
+ if (lines.length > 0) {
630
+ allParts.push(lines.join("\r\n"))
631
+ allParts.push("\r\n")
632
+ }
633
+
634
+ // Add binary data
635
+ for (const binaryPart of binaryParts) {
636
+ allParts.push(`${binaryPart.key}: `)
637
+ allParts.push(binaryPart.value)
638
+ if (binaryParts.indexOf(binaryPart) < binaryParts.length - 1) {
639
+ allParts.push("\r\n")
640
+ }
641
+ }
642
+
643
+ bodyParts.push(new Blob(allParts))
644
+ } else {
645
+ bodyParts.push(new Blob([lines.join("\r\n")]))
646
+ }
647
+ }
648
+ }
649
+
650
+ console.log(`[encode] Total bodyParts: ${bodyParts.length}`)
651
+
652
+ // Create boundary based on parts content
653
+ const partsForBoundary = []
654
+ for (const part of bodyParts) {
655
+ const partContent = await part.text()
656
+ partsForBoundary.push(partContent)
657
+ }
658
+ const allPartsContent = partsForBoundary.join("")
659
+ console.log(`[encode] All parts content length: ${allPartsContent.length}`)
660
+
661
+ const allPartsBuffer = Buffer.from(allPartsContent)
662
+ const hashResult = await sha256(
663
+ allPartsBuffer.buffer.slice(
664
+ allPartsBuffer.byteOffset,
665
+ allPartsBuffer.byteOffset + allPartsBuffer.byteLength
666
+ )
667
+ )
668
+ const boundary = base64url.encode(Buffer.from(hashResult))
669
+
670
+ // Create final multipart body
671
+ const finalParts = []
672
+ for (let i = 0; i < bodyParts.length; i++) {
673
+ finalParts.push(`--${boundary}`)
674
+ finalParts.push(`\r\n`)
675
+ finalParts.push(bodyParts[i])
676
+ if (i < bodyParts.length - 1) {
677
+ finalParts.push(`\r\n`)
678
+ }
679
+ }
680
+ finalParts.push(`\r\n--${boundary}--`)
681
+
682
+ headers["content-type"] = `multipart/form-data; boundary="${boundary}"`
683
+ const body = new Blob(finalParts)
684
+
685
+ const finalContent = await body.arrayBuffer()
686
+ const contentDigest = await sha256(finalContent)
687
+ const base64 = base64url.toBase64(base64url.encode(contentDigest))
688
+ headers["content-digest"] = `sha-256=:${base64}:`
689
+ headers["content-length"] = String(finalContent.byteLength)
690
+
691
+ console.log("[encode] FINAL - headers:", headers, "body:", body)
692
+ return { headers, body }
693
+ }
694
+
695
+ console.log("[encode] FINAL - headers:", headers, "body:", undefined)
696
+ return { headers, body: undefined }
697
+ }
698
+
699
+ export async function enc(fields) {
700
+ return await encode(fields)
701
+ }