hbsig 0.2.1 → 0.2.3
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/httpsig.js +163 -122
- package/cjs/send-utils.js +3 -2
- package/esm/httpsig.js +149 -91
- package/esm/send-utils.js +3 -1
- package/package.json +1 -1
package/cjs/httpsig.js
CHANGED
|
@@ -348,18 +348,48 @@ function encodeBodyPart(partName, bodyPart, inlineKey) {
|
|
|
348
348
|
return "";
|
|
349
349
|
}
|
|
350
350
|
|
|
351
|
-
//
|
|
352
|
-
function isBinaryData(
|
|
353
|
-
|
|
354
|
-
|
|
351
|
+
// Improved helper to detect if data is likely binary
|
|
352
|
+
function isBinaryData(data) {
|
|
353
|
+
var buf;
|
|
354
|
+
if (typeof data === "string") {
|
|
355
|
+
// Convert string to buffer using binary encoding to check byte values
|
|
356
|
+
buf = Buffer.from(data, "binary");
|
|
357
|
+
} else if (Buffer.isBuffer(data)) {
|
|
358
|
+
buf = data;
|
|
359
|
+
} else {
|
|
360
|
+
return false;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
// Check a larger sample for better detection
|
|
364
|
+
var sampleSize = Math.min(buf.length, 512);
|
|
365
|
+
var nullCount = 0;
|
|
366
|
+
var controlCount = 0;
|
|
367
|
+
var highByteCount = 0;
|
|
368
|
+
for (var i = 0; i < sampleSize; i++) {
|
|
355
369
|
var _byte = buf[i];
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
370
|
+
|
|
371
|
+
// Null bytes are a strong indicator of binary
|
|
372
|
+
if (_byte === 0) {
|
|
373
|
+
nullCount++;
|
|
374
|
+
if (nullCount > 0) return true; // Even one null byte indicates binary
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
// Control characters (except common text ones: TAB, LF, CR)
|
|
378
|
+
if (_byte < 32 && _byte !== 9 && _byte !== 10 && _byte !== 13) {
|
|
379
|
+
controlCount++;
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
// High byte values that aren't valid UTF-8 continuation bytes
|
|
383
|
+
if (_byte > 127) {
|
|
384
|
+
highByteCount++;
|
|
385
|
+
}
|
|
362
386
|
}
|
|
387
|
+
|
|
388
|
+
// If more than 10% of bytes are control chars, likely binary
|
|
389
|
+
if (controlCount / sampleSize > 0.1) return true;
|
|
390
|
+
|
|
391
|
+
// If more than 30% are high bytes without valid UTF-8 sequences, likely binary
|
|
392
|
+
if (highByteCount / sampleSize > 0.3) return true;
|
|
363
393
|
return false;
|
|
364
394
|
}
|
|
365
395
|
|
|
@@ -404,73 +434,83 @@ function parseMultipart(contentType, body) {
|
|
|
404
434
|
// Parse headers more carefully to handle binary data
|
|
405
435
|
var currentPos = 0;
|
|
406
436
|
while (currentPos < headerBlock.length) {
|
|
407
|
-
// Find next
|
|
408
|
-
var
|
|
409
|
-
if (
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
if (colonIndex > 0) {
|
|
414
|
-
var name = line.substring(0, colonIndex).toLowerCase();
|
|
415
|
-
var value = line.substring(colonIndex + 2);
|
|
416
|
-
|
|
417
|
-
// Special handling for known binary fields or detected binary data
|
|
418
|
-
if (name === "owner" || name === "signature") {
|
|
419
|
-
// These fields contain binary data that may have embedded newlines
|
|
420
|
-
// We need to read until we find the next header or end of headers
|
|
421
|
-
var valueStart = currentPos + colonIndex + 2;
|
|
422
|
-
|
|
423
|
-
// Look ahead to find where this field really ends
|
|
424
|
-
// The next header will start with a valid header name followed by ": "
|
|
425
|
-
var searchPos = valueStart;
|
|
426
|
-
var valueEnd = headerBlock.length;
|
|
427
|
-
|
|
428
|
-
// Look for the next valid header pattern
|
|
429
|
-
while (searchPos < headerBlock.length) {
|
|
430
|
-
var nextNewline = headerBlock.indexOf("\n", searchPos);
|
|
431
|
-
if (nextNewline === -1) break;
|
|
432
|
-
|
|
433
|
-
// Check if what follows looks like a header
|
|
434
|
-
var nextLineStart = nextNewline + 1;
|
|
435
|
-
var nextColon = headerBlock.indexOf(":", nextLineStart);
|
|
436
|
-
|
|
437
|
-
// Valid header should have colon relatively close to line start
|
|
438
|
-
if (nextColon > nextLineStart && nextColon < nextLineStart + 50) {
|
|
439
|
-
// Check if the text before colon looks like a header name (ASCII text)
|
|
440
|
-
var possibleHeaderName = headerBlock.substring(nextLineStart, nextColon);
|
|
441
|
-
var looksLikeHeader = /^[a-zA-Z0-9-]+$/.test(possibleHeaderName);
|
|
442
|
-
if (looksLikeHeader) {
|
|
443
|
-
// Found the next header, value ends at the newline before it
|
|
444
|
-
valueEnd = nextNewline;
|
|
445
|
-
break;
|
|
446
|
-
}
|
|
447
|
-
}
|
|
448
|
-
searchPos = nextNewline + 1;
|
|
449
|
-
}
|
|
437
|
+
// Find the next colon to identify a potential header
|
|
438
|
+
var colonPos = headerBlock.indexOf(": ", currentPos);
|
|
439
|
+
if (colonPos === -1) {
|
|
440
|
+
// No more headers
|
|
441
|
+
break;
|
|
442
|
+
}
|
|
450
443
|
|
|
451
|
-
|
|
452
|
-
|
|
444
|
+
// Look backwards from colon to find the start of this line
|
|
445
|
+
var lineStart = currentPos;
|
|
446
|
+
var searchBack = colonPos - 1;
|
|
447
|
+
while (searchBack >= currentPos) {
|
|
448
|
+
if (headerBlock[searchBack] === "\n") {
|
|
449
|
+
lineStart = searchBack + 1;
|
|
450
|
+
break;
|
|
451
|
+
}
|
|
452
|
+
searchBack--;
|
|
453
|
+
}
|
|
453
454
|
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
value = value.substring(0, value.length - 1);
|
|
457
|
-
}
|
|
455
|
+
// Extract the header name
|
|
456
|
+
var name = headerBlock.substring(lineStart, colonPos).trim().toLowerCase();
|
|
458
457
|
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
458
|
+
// Check if this looks like a valid header name
|
|
459
|
+
if (!/^[a-zA-Z0-9-]+$/.test(name) || name.length > 50) {
|
|
460
|
+
// Not a valid header, skip past this colon
|
|
461
|
+
currentPos = colonPos + 2;
|
|
462
|
+
continue;
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
// Start of value is after ": "
|
|
466
|
+
var valueStart = colonPos + 2;
|
|
467
|
+
|
|
468
|
+
// Find the end of this header's value by looking for the next valid header
|
|
469
|
+
var valueEnd = headerBlock.length;
|
|
470
|
+
var searchPos = valueStart;
|
|
471
|
+
while (searchPos < headerBlock.length) {
|
|
472
|
+
var nextNewline = headerBlock.indexOf("\n", searchPos);
|
|
473
|
+
if (nextNewline === -1) break;
|
|
474
|
+
var nextLineStart = nextNewline + 1;
|
|
475
|
+
if (nextLineStart >= headerBlock.length) break;
|
|
476
|
+
|
|
477
|
+
// Check if next line starts with a header pattern
|
|
478
|
+
var nextColon = headerBlock.indexOf(": ", nextLineStart);
|
|
479
|
+
if (nextColon > nextLineStart && nextColon < nextLineStart + 50) {
|
|
480
|
+
var possibleHeaderName = headerBlock.substring(nextLineStart, nextColon).trim();
|
|
481
|
+
|
|
482
|
+
// Must be valid header name format
|
|
483
|
+
if (/^[a-zA-Z0-9-]+$/.test(possibleHeaderName)) {
|
|
484
|
+
valueEnd = nextNewline;
|
|
485
|
+
break;
|
|
486
|
+
}
|
|
470
487
|
}
|
|
488
|
+
searchPos = nextNewline + 1;
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
// Extract the value
|
|
492
|
+
var value = headerBlock.substring(valueStart, valueEnd);
|
|
493
|
+
|
|
494
|
+
// Trim trailing CRLF or LF
|
|
495
|
+
if (value.endsWith("\r\n")) {
|
|
496
|
+
value = value.substring(0, value.length - 2);
|
|
497
|
+
} else if (value.endsWith("\n")) {
|
|
498
|
+
value = value.substring(0, value.length - 1);
|
|
499
|
+
} else if (value.endsWith("\r")) {
|
|
500
|
+
value = value.substring(0, value.length - 1);
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
// Determine if this is binary data
|
|
504
|
+
if (value.length > 0 && isBinaryData(value)) {
|
|
505
|
+
headers[name] = Buffer.from(value, "binary");
|
|
471
506
|
} else {
|
|
472
|
-
|
|
473
|
-
|
|
507
|
+
headers[name] = value;
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
// Move to the end of this header's value
|
|
511
|
+
currentPos = valueEnd + 1;
|
|
512
|
+
if (headerBlock[valueEnd] === "\r") {
|
|
513
|
+
currentPos++;
|
|
474
514
|
}
|
|
475
515
|
}
|
|
476
516
|
var disposition = headers["content-disposition"];
|
|
@@ -495,8 +535,8 @@ function parseMultipart(contentType, body) {
|
|
|
495
535
|
|
|
496
536
|
// If there's body content in the inline part, add it as 'body'
|
|
497
537
|
if (partBody) {
|
|
498
|
-
//
|
|
499
|
-
result[partName] = Buffer.from(partBody, "binary");
|
|
538
|
+
// Keep as Buffer if it's binary data
|
|
539
|
+
result[partName] = isBinaryData(partBody) ? Buffer.from(partBody, "binary") : partBody;
|
|
500
540
|
}
|
|
501
541
|
} else {
|
|
502
542
|
// Handle named form-data parts
|
|
@@ -510,14 +550,15 @@ function parseMultipart(contentType, body) {
|
|
|
510
550
|
var _restHeaders = _objectSpread({}, headers);
|
|
511
551
|
delete _restHeaders["content-disposition"];
|
|
512
552
|
if (Object.keys(_restHeaders).length === 0) {
|
|
513
|
-
//
|
|
514
|
-
result[partName] = Buffer.from(partBody, "binary");
|
|
553
|
+
// Keep as Buffer if it's binary data
|
|
554
|
+
result[partName] = isBinaryData(partBody) ? Buffer.from(partBody, "binary") : partBody;
|
|
515
555
|
} else if (!partBody) {
|
|
556
|
+
// ao-types should stay with this part, not be extracted
|
|
516
557
|
result[partName] = _restHeaders;
|
|
517
558
|
} else {
|
|
518
|
-
//
|
|
559
|
+
// Keep as Buffer if it's binary data
|
|
519
560
|
result[partName] = _objectSpread(_objectSpread({}, _restHeaders), {}, {
|
|
520
|
-
body: Buffer.from(partBody, "binary")
|
|
561
|
+
body: isBinaryData(partBody) ? Buffer.from(partBody, "binary") : partBody
|
|
521
562
|
});
|
|
522
563
|
}
|
|
523
564
|
}
|
|
@@ -593,27 +634,27 @@ function httpsig_from(http) {
|
|
|
593
634
|
|
|
594
635
|
// Convert flat structure to nested using flat.js
|
|
595
636
|
var flat = {};
|
|
637
|
+
var nonFlat = {};
|
|
596
638
|
for (var _i9 = 0, _Object$entries9 = Object.entries(withBodyKeys); _i9 < _Object$entries9.length; _i9++) {
|
|
597
639
|
var _Object$entries9$_i = _slicedToArray(_Object$entries9[_i9], 2),
|
|
598
640
|
key = _Object$entries9$_i[0],
|
|
599
641
|
value = _Object$entries9$_i[1];
|
|
600
642
|
if (key.includes("/")) {
|
|
601
643
|
flat[key] = value;
|
|
644
|
+
} else {
|
|
645
|
+
nonFlat[key] = value;
|
|
602
646
|
}
|
|
603
647
|
}
|
|
604
648
|
if (Object.keys(flat).length > 0) {
|
|
649
|
+
// Merge non-flat keys into flat for processing
|
|
650
|
+
// This ensures flat_from can see existing objects like results: { "ao-types": "..." }
|
|
651
|
+
var combined = _objectSpread(_objectSpread({}, nonFlat), flat);
|
|
652
|
+
|
|
605
653
|
// Use flat_from to convert flat structure to nested
|
|
606
|
-
var nested = (0, _flat.flat_from)(
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
_value3 = _Object$entries0$_i[1];
|
|
611
|
-
withBodyKeys[_key2] = _value3;
|
|
612
|
-
}
|
|
613
|
-
for (var _i1 = 0, _Object$keys = Object.keys(flat); _i1 < _Object$keys.length; _i1++) {
|
|
614
|
-
var _key3 = _Object$keys[_i1];
|
|
615
|
-
delete withBodyKeys[_key3];
|
|
616
|
-
}
|
|
654
|
+
var nested = (0, _flat.flat_from)(combined);
|
|
655
|
+
|
|
656
|
+
// The nested result already has everything merged
|
|
657
|
+
withBodyKeys = nested;
|
|
617
658
|
}
|
|
618
659
|
} else if (body) {
|
|
619
660
|
withBodyKeys[inlinedKey] = body;
|
|
@@ -630,10 +671,10 @@ function httpsig_from(http) {
|
|
|
630
671
|
delete result["content-digest"];
|
|
631
672
|
|
|
632
673
|
// Extract hashpaths if any
|
|
633
|
-
for (var
|
|
634
|
-
var
|
|
635
|
-
if (
|
|
636
|
-
delete result[
|
|
674
|
+
for (var _i0 = 0, _Object$keys = Object.keys(result); _i0 < _Object$keys.length; _i0++) {
|
|
675
|
+
var _key2 = _Object$keys[_i0];
|
|
676
|
+
if (_key2.startsWith("hashpath")) {
|
|
677
|
+
delete result[_key2];
|
|
637
678
|
}
|
|
638
679
|
}
|
|
639
680
|
return result;
|
|
@@ -671,10 +712,10 @@ function httpsig_to(tabm) {
|
|
|
671
712
|
// For flat structures, just return with normalized keys
|
|
672
713
|
// This matches Erlang which returns the map unchanged
|
|
673
714
|
var result = _objectSpread({}, inlineFieldHdrs);
|
|
674
|
-
for (var
|
|
675
|
-
var _Object$
|
|
676
|
-
key = _Object$
|
|
677
|
-
value = _Object$
|
|
715
|
+
for (var _i1 = 0, _Object$entries0 = Object.entries(stripped); _i1 < _Object$entries0.length; _i1++) {
|
|
716
|
+
var _Object$entries0$_i = _slicedToArray(_Object$entries0[_i1], 2),
|
|
717
|
+
key = _Object$entries0$_i[0],
|
|
718
|
+
value = _Object$entries0$_i[1];
|
|
678
719
|
// Keep Buffers as Buffers - don't convert to strings
|
|
679
720
|
result[key] = value;
|
|
680
721
|
}
|
|
@@ -697,26 +738,26 @@ function httpsig_to(tabm) {
|
|
|
697
738
|
var headers = _objectSpread({}, inlineFieldHdrs);
|
|
698
739
|
|
|
699
740
|
// Process each field - ao-types at top level should go to headers
|
|
700
|
-
for (var
|
|
701
|
-
var _Object$
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
if (
|
|
741
|
+
for (var _i10 = 0, _Object$entries1 = Object.entries(stripped); _i10 < _Object$entries1.length; _i10++) {
|
|
742
|
+
var _Object$entries1$_i = _slicedToArray(_Object$entries1[_i10], 2),
|
|
743
|
+
_key3 = _Object$entries1$_i[0],
|
|
744
|
+
_value3 = _Object$entries1$_i[1];
|
|
745
|
+
if (_key3 === "ao-types") {
|
|
705
746
|
// Top-level ao-types goes to headers only
|
|
706
747
|
// Keep as Buffer if it's a Buffer, otherwise use as-is
|
|
707
|
-
headers[
|
|
708
|
-
} else if (
|
|
709
|
-
bodyMap[
|
|
710
|
-
} else if (_typeof(
|
|
711
|
-
bodyMap[
|
|
712
|
-
} else if (typeof
|
|
713
|
-
headers[normalizeKey(
|
|
714
|
-
} else if (Buffer.isBuffer(
|
|
748
|
+
headers[_key3] = _value3;
|
|
749
|
+
} else if (_key3 === "body" || _key3 === inlineKeyVal) {
|
|
750
|
+
bodyMap[_key3 === inlineKeyVal ? inlineKeyVal : "body"] = _value3;
|
|
751
|
+
} else if (_typeof(_value3) === "object" && _value3 !== null && !Array.isArray(_value3) && !Buffer.isBuffer(_value3)) {
|
|
752
|
+
bodyMap[_key3] = _value3;
|
|
753
|
+
} else if (typeof _value3 === "string" && _value3.length <= MAX_HEADER_LENGTH && _key3 !== "ao-types") {
|
|
754
|
+
headers[normalizeKey(_key3)] = _value3;
|
|
755
|
+
} else if (Buffer.isBuffer(_value3) && _value3.length <= MAX_HEADER_LENGTH && _key3 !== "ao-types") {
|
|
715
756
|
// Keep buffers as buffers for headers
|
|
716
|
-
headers[normalizeKey(
|
|
717
|
-
} else if (
|
|
757
|
+
headers[normalizeKey(_key3)] = _value3;
|
|
758
|
+
} else if (_key3 !== "ao-types") {
|
|
718
759
|
// Only add to bodyMap if it's not ao-types
|
|
719
|
-
bodyMap[
|
|
760
|
+
bodyMap[_key3] = _value3;
|
|
720
761
|
}
|
|
721
762
|
}
|
|
722
763
|
|
|
@@ -745,22 +786,22 @@ function httpsig_to(tabm) {
|
|
|
745
786
|
try {
|
|
746
787
|
for (_iterator4.s(); !(_step4 = _iterator4.n()).done;) {
|
|
747
788
|
var _step4$value = _slicedToArray(_step4.value, 2),
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
if (_typeof(
|
|
751
|
-
var encoded = encodeBodyPart("".concat(
|
|
789
|
+
_key4 = _step4$value[0],
|
|
790
|
+
_value4 = _step4$value[1];
|
|
791
|
+
if (_typeof(_value4) === "object" && _value4 !== null && Object.keys(_value4).length === 1 && "body" in _value4) {
|
|
792
|
+
var encoded = encodeBodyPart("".concat(_key4, "/body"), _value4, "body");
|
|
752
793
|
parts.push({
|
|
753
|
-
name: "".concat(
|
|
794
|
+
name: "".concat(_key4, "/body"),
|
|
754
795
|
body: encoded
|
|
755
796
|
});
|
|
756
|
-
bodyKeysList.push(
|
|
797
|
+
bodyKeysList.push(_key4);
|
|
757
798
|
} else {
|
|
758
|
-
var _encoded = encodeBodyPart(
|
|
799
|
+
var _encoded = encodeBodyPart(_key4, _value4, inlineKeyVal);
|
|
759
800
|
parts.push({
|
|
760
|
-
name:
|
|
801
|
+
name: _key4,
|
|
761
802
|
body: _encoded
|
|
762
803
|
});
|
|
763
|
-
bodyKeysList.push(
|
|
804
|
+
bodyKeysList.push(_key4);
|
|
764
805
|
}
|
|
765
806
|
}
|
|
766
807
|
} catch (err) {
|
package/cjs/send-utils.js
CHANGED
|
@@ -951,7 +951,7 @@ var toMsg = /*#__PURE__*/function () {
|
|
|
951
951
|
var result = exports.result = /*#__PURE__*/function () {
|
|
952
952
|
var _ref2 = _asyncToGenerator(/*#__PURE__*/_regenerator().m(function _callee2(response) {
|
|
953
953
|
var _from$signer, _from$hashpath;
|
|
954
|
-
var headers, msg, out, body, http, _from;
|
|
954
|
+
var headers, msg, tabm, out, body, http, _from;
|
|
955
955
|
return _regenerator().w(function (_context2) {
|
|
956
956
|
while (1) switch (_context2.n) {
|
|
957
957
|
case 0:
|
|
@@ -963,7 +963,8 @@ var result = exports.result = /*#__PURE__*/function () {
|
|
|
963
963
|
return toMsg(response);
|
|
964
964
|
case 1:
|
|
965
965
|
msg = _context2.v;
|
|
966
|
-
|
|
966
|
+
tabm = (0, _httpsig.httpsig_from)(msg); //console.log("TABM:", JSON.stringify(tabm))
|
|
967
|
+
out = (0, _structured.structured_to)(tabm);
|
|
967
968
|
body = Buffer.from(msg.body).toString();
|
|
968
969
|
http = {
|
|
969
970
|
headers: headers,
|
package/esm/httpsig.js
CHANGED
|
@@ -291,18 +291,51 @@ function encodeBodyPart(partName, bodyPart, inlineKey) {
|
|
|
291
291
|
return ""
|
|
292
292
|
}
|
|
293
293
|
|
|
294
|
-
//
|
|
295
|
-
function isBinaryData(
|
|
296
|
-
|
|
297
|
-
|
|
294
|
+
// Improved helper to detect if data is likely binary
|
|
295
|
+
function isBinaryData(data) {
|
|
296
|
+
let buf
|
|
297
|
+
|
|
298
|
+
if (typeof data === "string") {
|
|
299
|
+
// Convert string to buffer using binary encoding to check byte values
|
|
300
|
+
buf = Buffer.from(data, "binary")
|
|
301
|
+
} else if (Buffer.isBuffer(data)) {
|
|
302
|
+
buf = data
|
|
303
|
+
} else {
|
|
304
|
+
return false
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
// Check a larger sample for better detection
|
|
308
|
+
const sampleSize = Math.min(buf.length, 512)
|
|
309
|
+
let nullCount = 0
|
|
310
|
+
let controlCount = 0
|
|
311
|
+
let highByteCount = 0
|
|
312
|
+
|
|
313
|
+
for (let i = 0; i < sampleSize; i++) {
|
|
298
314
|
const byte = buf[i]
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
315
|
+
|
|
316
|
+
// Null bytes are a strong indicator of binary
|
|
317
|
+
if (byte === 0) {
|
|
318
|
+
nullCount++
|
|
319
|
+
if (nullCount > 0) return true // Even one null byte indicates binary
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
// Control characters (except common text ones: TAB, LF, CR)
|
|
323
|
+
if (byte < 32 && byte !== 9 && byte !== 10 && byte !== 13) {
|
|
324
|
+
controlCount++
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
// High byte values that aren't valid UTF-8 continuation bytes
|
|
328
|
+
if (byte > 127) {
|
|
329
|
+
highByteCount++
|
|
330
|
+
}
|
|
305
331
|
}
|
|
332
|
+
|
|
333
|
+
// If more than 10% of bytes are control chars, likely binary
|
|
334
|
+
if (controlCount / sampleSize > 0.1) return true
|
|
335
|
+
|
|
336
|
+
// If more than 30% are high bytes without valid UTF-8 sequences, likely binary
|
|
337
|
+
if (highByteCount / sampleSize > 0.3) return true
|
|
338
|
+
|
|
306
339
|
return false
|
|
307
340
|
}
|
|
308
341
|
|
|
@@ -348,79 +381,93 @@ function parseMultipart(contentType, body) {
|
|
|
348
381
|
// Parse headers more carefully to handle binary data
|
|
349
382
|
let currentPos = 0
|
|
350
383
|
while (currentPos < headerBlock.length) {
|
|
351
|
-
// Find next
|
|
352
|
-
let
|
|
353
|
-
if (lineEnd === -1) lineEnd = headerBlock.indexOf("\n", currentPos)
|
|
354
|
-
if (lineEnd === -1) lineEnd = headerBlock.length
|
|
355
|
-
|
|
356
|
-
const line = headerBlock.substring(currentPos, lineEnd)
|
|
357
|
-
const colonIndex = line.indexOf(": ")
|
|
358
|
-
|
|
359
|
-
if (colonIndex > 0) {
|
|
360
|
-
const name = line.substring(0, colonIndex).toLowerCase()
|
|
361
|
-
let value = line.substring(colonIndex + 2)
|
|
362
|
-
|
|
363
|
-
// Special handling for known binary fields or detected binary data
|
|
364
|
-
if (name === "owner" || name === "signature") {
|
|
365
|
-
// These fields contain binary data that may have embedded newlines
|
|
366
|
-
// We need to read until we find the next header or end of headers
|
|
367
|
-
let valueStart = currentPos + colonIndex + 2
|
|
368
|
-
|
|
369
|
-
// Look ahead to find where this field really ends
|
|
370
|
-
// The next header will start with a valid header name followed by ": "
|
|
371
|
-
let searchPos = valueStart
|
|
372
|
-
let valueEnd = headerBlock.length
|
|
373
|
-
|
|
374
|
-
// Look for the next valid header pattern
|
|
375
|
-
while (searchPos < headerBlock.length) {
|
|
376
|
-
let nextNewline = headerBlock.indexOf("\n", searchPos)
|
|
377
|
-
if (nextNewline === -1) break
|
|
378
|
-
|
|
379
|
-
// Check if what follows looks like a header
|
|
380
|
-
let nextLineStart = nextNewline + 1
|
|
381
|
-
let nextColon = headerBlock.indexOf(":", nextLineStart)
|
|
382
|
-
|
|
383
|
-
// Valid header should have colon relatively close to line start
|
|
384
|
-
if (nextColon > nextLineStart && nextColon < nextLineStart + 50) {
|
|
385
|
-
// Check if the text before colon looks like a header name (ASCII text)
|
|
386
|
-
let possibleHeaderName = headerBlock.substring(
|
|
387
|
-
nextLineStart,
|
|
388
|
-
nextColon
|
|
389
|
-
)
|
|
390
|
-
let looksLikeHeader = /^[a-zA-Z0-9-]+$/.test(possibleHeaderName)
|
|
391
|
-
|
|
392
|
-
if (looksLikeHeader) {
|
|
393
|
-
// Found the next header, value ends at the newline before it
|
|
394
|
-
valueEnd = nextNewline
|
|
395
|
-
break
|
|
396
|
-
}
|
|
397
|
-
}
|
|
398
|
-
searchPos = nextNewline + 1
|
|
399
|
-
}
|
|
384
|
+
// Find the next colon to identify a potential header
|
|
385
|
+
let colonPos = headerBlock.indexOf(": ", currentPos)
|
|
400
386
|
|
|
401
|
-
|
|
402
|
-
|
|
387
|
+
if (colonPos === -1) {
|
|
388
|
+
// No more headers
|
|
389
|
+
break
|
|
390
|
+
}
|
|
403
391
|
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
392
|
+
// Look backwards from colon to find the start of this line
|
|
393
|
+
let lineStart = currentPos
|
|
394
|
+
let searchBack = colonPos - 1
|
|
395
|
+
while (searchBack >= currentPos) {
|
|
396
|
+
if (headerBlock[searchBack] === "\n") {
|
|
397
|
+
lineStart = searchBack + 1
|
|
398
|
+
break
|
|
399
|
+
}
|
|
400
|
+
searchBack--
|
|
401
|
+
}
|
|
408
402
|
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
403
|
+
// Extract the header name
|
|
404
|
+
const name = headerBlock
|
|
405
|
+
.substring(lineStart, colonPos)
|
|
406
|
+
.trim()
|
|
407
|
+
.toLowerCase()
|
|
408
|
+
|
|
409
|
+
// Check if this looks like a valid header name
|
|
410
|
+
if (!/^[a-zA-Z0-9-]+$/.test(name) || name.length > 50) {
|
|
411
|
+
// Not a valid header, skip past this colon
|
|
412
|
+
currentPos = colonPos + 2
|
|
413
|
+
continue
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
// Start of value is after ": "
|
|
417
|
+
let valueStart = colonPos + 2
|
|
418
|
+
|
|
419
|
+
// Find the end of this header's value by looking for the next valid header
|
|
420
|
+
let valueEnd = headerBlock.length
|
|
421
|
+
let searchPos = valueStart
|
|
422
|
+
|
|
423
|
+
while (searchPos < headerBlock.length) {
|
|
424
|
+
let nextNewline = headerBlock.indexOf("\n", searchPos)
|
|
425
|
+
if (nextNewline === -1) break
|
|
426
|
+
|
|
427
|
+
let nextLineStart = nextNewline + 1
|
|
428
|
+
if (nextLineStart >= headerBlock.length) break
|
|
429
|
+
|
|
430
|
+
// Check if next line starts with a header pattern
|
|
431
|
+
let nextColon = headerBlock.indexOf(": ", nextLineStart)
|
|
432
|
+
|
|
433
|
+
if (nextColon > nextLineStart && nextColon < nextLineStart + 50) {
|
|
434
|
+
let possibleHeaderName = headerBlock
|
|
435
|
+
.substring(nextLineStart, nextColon)
|
|
436
|
+
.trim()
|
|
437
|
+
|
|
438
|
+
// Must be valid header name format
|
|
439
|
+
if (/^[a-zA-Z0-9-]+$/.test(possibleHeaderName)) {
|
|
440
|
+
valueEnd = nextNewline
|
|
441
|
+
break
|
|
442
|
+
}
|
|
420
443
|
}
|
|
444
|
+
|
|
445
|
+
searchPos = nextNewline + 1
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
// Extract the value
|
|
449
|
+
let value = headerBlock.substring(valueStart, valueEnd)
|
|
450
|
+
|
|
451
|
+
// Trim trailing CRLF or LF
|
|
452
|
+
if (value.endsWith("\r\n")) {
|
|
453
|
+
value = value.substring(0, value.length - 2)
|
|
454
|
+
} else if (value.endsWith("\n")) {
|
|
455
|
+
value = value.substring(0, value.length - 1)
|
|
456
|
+
} else if (value.endsWith("\r")) {
|
|
457
|
+
value = value.substring(0, value.length - 1)
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
// Determine if this is binary data
|
|
461
|
+
if (value.length > 0 && isBinaryData(value)) {
|
|
462
|
+
headers[name] = Buffer.from(value, "binary")
|
|
421
463
|
} else {
|
|
422
|
-
|
|
423
|
-
|
|
464
|
+
headers[name] = value
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
// Move to the end of this header's value
|
|
468
|
+
currentPos = valueEnd + 1
|
|
469
|
+
if (headerBlock[valueEnd] === "\r") {
|
|
470
|
+
currentPos++
|
|
424
471
|
}
|
|
425
472
|
}
|
|
426
473
|
|
|
@@ -444,8 +491,10 @@ function parseMultipart(contentType, body) {
|
|
|
444
491
|
|
|
445
492
|
// If there's body content in the inline part, add it as 'body'
|
|
446
493
|
if (partBody) {
|
|
447
|
-
//
|
|
448
|
-
result[partName] =
|
|
494
|
+
// Keep as Buffer if it's binary data
|
|
495
|
+
result[partName] = isBinaryData(partBody)
|
|
496
|
+
? Buffer.from(partBody, "binary")
|
|
497
|
+
: partBody
|
|
449
498
|
}
|
|
450
499
|
} else {
|
|
451
500
|
// Handle named form-data parts
|
|
@@ -462,15 +511,20 @@ function parseMultipart(contentType, body) {
|
|
|
462
511
|
delete restHeaders["content-disposition"]
|
|
463
512
|
|
|
464
513
|
if (Object.keys(restHeaders).length === 0) {
|
|
465
|
-
//
|
|
466
|
-
result[partName] =
|
|
514
|
+
// Keep as Buffer if it's binary data
|
|
515
|
+
result[partName] = isBinaryData(partBody)
|
|
516
|
+
? Buffer.from(partBody, "binary")
|
|
517
|
+
: partBody
|
|
467
518
|
} else if (!partBody) {
|
|
519
|
+
// ao-types should stay with this part, not be extracted
|
|
468
520
|
result[partName] = restHeaders
|
|
469
521
|
} else {
|
|
470
|
-
//
|
|
522
|
+
// Keep as Buffer if it's binary data
|
|
471
523
|
result[partName] = {
|
|
472
524
|
...restHeaders,
|
|
473
|
-
body:
|
|
525
|
+
body: isBinaryData(partBody)
|
|
526
|
+
? Buffer.from(partBody, "binary")
|
|
527
|
+
: partBody,
|
|
474
528
|
}
|
|
475
529
|
}
|
|
476
530
|
}
|
|
@@ -546,21 +600,25 @@ export function httpsig_from(http) {
|
|
|
546
600
|
|
|
547
601
|
// Convert flat structure to nested using flat.js
|
|
548
602
|
const flat = {}
|
|
603
|
+
const nonFlat = {}
|
|
549
604
|
for (const [key, value] of Object.entries(withBodyKeys)) {
|
|
550
605
|
if (key.includes("/")) {
|
|
551
606
|
flat[key] = value
|
|
607
|
+
} else {
|
|
608
|
+
nonFlat[key] = value
|
|
552
609
|
}
|
|
553
610
|
}
|
|
554
611
|
|
|
555
612
|
if (Object.keys(flat).length > 0) {
|
|
613
|
+
// Merge non-flat keys into flat for processing
|
|
614
|
+
// This ensures flat_from can see existing objects like results: { "ao-types": "..." }
|
|
615
|
+
const combined = { ...nonFlat, ...flat }
|
|
616
|
+
|
|
556
617
|
// Use flat_from to convert flat structure to nested
|
|
557
|
-
const nested = flat_from(
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
for (const key of Object.keys(flat)) {
|
|
562
|
-
delete withBodyKeys[key]
|
|
563
|
-
}
|
|
618
|
+
const nested = flat_from(combined)
|
|
619
|
+
|
|
620
|
+
// The nested result already has everything merged
|
|
621
|
+
withBodyKeys = nested
|
|
564
622
|
}
|
|
565
623
|
} else if (body) {
|
|
566
624
|
withBodyKeys[inlinedKey] = body
|
package/esm/send-utils.js
CHANGED
|
@@ -878,7 +878,9 @@ export const result = async response => {
|
|
|
878
878
|
let headers = {}
|
|
879
879
|
response.headers.forEach((v, k) => (headers[k] = v))
|
|
880
880
|
const msg = await toMsg(response)
|
|
881
|
-
const
|
|
881
|
+
const tabm = httpsig_from(msg)
|
|
882
|
+
//console.log("TABM:", JSON.stringify(tabm))
|
|
883
|
+
const out = structured_to(tabm)
|
|
882
884
|
const body = Buffer.from(msg.body).toString()
|
|
883
885
|
const http = { headers, body }
|
|
884
886
|
const _from = from(http)
|