hbsig 0.2.8 → 0.3.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/flat.js CHANGED
@@ -90,6 +90,60 @@ function pathToParts(path) {
90
90
  throw new Error("Path must be a string or array");
91
91
  }
92
92
 
93
+ /**
94
+ * Convert a value to string format to match Erlang codec behavior
95
+ * Erlang dev_codec_flat only handles binaries, so all leaf values become strings
96
+ * @param {*} value - Value to convert
97
+ * @returns {string|Object} - String for leaf values, or recursively processed object
98
+ */
99
+ function valueToString(value) {
100
+ if (_typeof(value) === "object" && value !== null && !Array.isArray(value) && !Buffer.isBuffer(value)) {
101
+ // Recursively process nested objects
102
+ var result = {};
103
+ for (var _i2 = 0, _Object$entries2 = Object.entries(value); _i2 < _Object$entries2.length; _i2++) {
104
+ var _Object$entries2$_i = _slicedToArray(_Object$entries2[_i2], 2),
105
+ k = _Object$entries2$_i[0],
106
+ v = _Object$entries2$_i[1];
107
+ result[k] = valueToString(v);
108
+ }
109
+ return result;
110
+ }
111
+ if (typeof value === "string") {
112
+ return value;
113
+ }
114
+ if (typeof value === "number") {
115
+ return String(value);
116
+ }
117
+ if (typeof value === "boolean") {
118
+ return String(value);
119
+ }
120
+ if (value === null) {
121
+ return "null";
122
+ }
123
+ if (Array.isArray(value)) {
124
+ // Convert array elements to strings first, then format as Erlang list
125
+ // Erlang's io_lib:format("~p", [List]) produces binary syntax like [<<"a">>,<<"b">>]
126
+ var elements = value.map(function (v) {
127
+ if (typeof v === "string") {
128
+ return "<<\"".concat(v, "\">>");
129
+ } else if (typeof v === "number") {
130
+ return String(v);
131
+ } else if (typeof v === "boolean") {
132
+ return v ? "true" : "false";
133
+ } else if (v === null) {
134
+ return "null";
135
+ } else {
136
+ return String(v);
137
+ }
138
+ });
139
+ return "[".concat(elements.join(","), "]");
140
+ }
141
+ if (Buffer.isBuffer(value)) {
142
+ return value.toString();
143
+ }
144
+ return String(value);
145
+ }
146
+
93
147
  /**
94
148
  * Helper function to inject a value at a specific path in a nested object
95
149
  * @param {Array} pathParts - Array of path parts
@@ -100,20 +154,23 @@ function injectAtPath(pathParts, value, obj) {
100
154
  if (pathParts.length === 0) {
101
155
  throw new Error("Path cannot be empty");
102
156
  }
157
+
158
+ // Convert value to match Erlang codec behavior
159
+ var convertedValue = valueToString(value);
103
160
  if (pathParts.length === 1) {
104
161
  var _key = pathParts[0];
105
162
  if (_key in obj) {
106
163
  var existing = obj[_key];
107
164
 
108
165
  // If both are objects, merge them
109
- if (_typeof(existing) === "object" && existing !== null && _typeof(value) === "object" && value !== null && !Array.isArray(existing) && !Array.isArray(value)) {
110
- obj[_key] = _objectSpread(_objectSpread({}, existing), value);
166
+ if (_typeof(existing) === "object" && existing !== null && _typeof(convertedValue) === "object" && convertedValue !== null && !Array.isArray(existing) && !Array.isArray(convertedValue)) {
167
+ obj[_key] = _objectSpread(_objectSpread({}, existing), convertedValue);
111
168
  } else {
112
169
  // Path collision
113
- throw new Error("Path collision at key: ".concat(_key, ", existing: ").concat(JSON.stringify(existing), ", value: ").concat(JSON.stringify(value)));
170
+ throw new Error("Path collision at key: ".concat(_key, ", existing: ").concat(JSON.stringify(existing), ", value: ").concat(JSON.stringify(convertedValue)));
114
171
  }
115
172
  } else {
116
- obj[_key] = value;
173
+ obj[_key] = convertedValue;
117
174
  }
118
175
  return;
119
176
  }
@@ -125,7 +182,7 @@ function injectAtPath(pathParts, value, obj) {
125
182
  } else if (_typeof(obj[key]) !== "object" || obj[key] === null) {
126
183
  throw new Error("Cannot create nested path at non-object key: ".concat(key));
127
184
  }
128
- injectAtPath(rest, value, obj[key]);
185
+ injectAtPath(rest, convertedValue, obj[key]);
129
186
  }
130
187
 
131
188
  /**
@@ -137,10 +194,10 @@ function injectAtPath(pathParts, value, obj) {
137
194
  function flattenRecursive(value, currentPath, result) {
138
195
  if (_typeof(value) === "object" && value !== null && !Array.isArray(value)) {
139
196
  // It's an object, recurse into it
140
- for (var _i2 = 0, _Object$entries2 = Object.entries(value); _i2 < _Object$entries2.length; _i2++) {
141
- var _Object$entries2$_i = _slicedToArray(_Object$entries2[_i2], 2),
142
- key = _Object$entries2$_i[0],
143
- subValue = _Object$entries2$_i[1];
197
+ for (var _i3 = 0, _Object$entries3 = Object.entries(value); _i3 < _Object$entries3.length; _i3++) {
198
+ var _Object$entries3$_i = _slicedToArray(_Object$entries3[_i3], 2),
199
+ key = _Object$entries3$_i[0],
200
+ subValue = _Object$entries3$_i[1];
144
201
  var newPath = [].concat(_toConsumableArray(currentPath), [key]);
145
202
  flattenRecursive(subValue, newPath, result);
146
203
  }
@@ -166,10 +223,10 @@ function serialize(map) {
166
223
  try {
167
224
  var flattened = flat_to(map);
168
225
  var lines = [];
169
- for (var _i3 = 0, _Object$entries3 = Object.entries(flattened); _i3 < _Object$entries3.length; _i3++) {
170
- var _Object$entries3$_i = _slicedToArray(_Object$entries3[_i3], 2),
171
- key = _Object$entries3$_i[0],
172
- value = _Object$entries3$_i[1];
226
+ for (var _i4 = 0, _Object$entries4 = Object.entries(flattened); _i4 < _Object$entries4.length; _i4++) {
227
+ var _Object$entries4$_i = _slicedToArray(_Object$entries4[_i4], 2),
228
+ key = _Object$entries4$_i[0],
229
+ value = _Object$entries4$_i[1];
173
230
  lines.push("".concat(key, ": ").concat(value));
174
231
  }
175
232
  return {
package/cjs/httpsig.js CHANGED
@@ -9,10 +9,7 @@ exports.structured_from = structured_from;
9
9
  exports.structured_to = structured_to;
10
10
  var _fastSha = require("fast-sha256");
11
11
  var _flat = require("./flat.js");
12
- function _toConsumableArray(r) { return _arrayWithoutHoles(r) || _iterableToArray(r) || _unsupportedIterableToArray(r) || _nonIterableSpread(); }
13
- function _nonIterableSpread() { throw new TypeError("Invalid attempt to spread non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); }
14
- function _iterableToArray(r) { if ("undefined" != typeof Symbol && null != r[Symbol.iterator] || null != r["@@iterator"]) return Array.from(r); }
15
- function _arrayWithoutHoles(r) { if (Array.isArray(r)) return _arrayLikeToArray(r); }
12
+ var _structured = require("./structured.js");
16
13
  function _typeof(o) { "@babel/helpers - typeof"; return _typeof = "function" == typeof Symbol && "symbol" == typeof Symbol.iterator ? function (o) { return typeof o; } : function (o) { return o && "function" == typeof Symbol && o.constructor === Symbol && o !== Symbol.prototype ? "symbol" : typeof o; }, _typeof(o); }
17
14
  function ownKeys(e, r) { var t = Object.keys(e); if (Object.getOwnPropertySymbols) { var o = Object.getOwnPropertySymbols(e); r && (o = o.filter(function (r) { return Object.getOwnPropertyDescriptor(e, r).enumerable; })), t.push.apply(t, o); } return t; }
18
15
  function _objectSpread(e) { for (var r = 1; r < arguments.length; r++) { var t = null != arguments[r] ? arguments[r] : {}; r % 2 ? ownKeys(Object(t), !0).forEach(function (r) { _defineProperty(e, r, t[r]); }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(e, Object.getOwnPropertyDescriptors(t)) : ownKeys(Object(t)).forEach(function (r) { Object.defineProperty(e, r, Object.getOwnPropertyDescriptor(t, r)); }); } return e; }
@@ -143,18 +140,19 @@ function boundaryFromParts(parts) {
143
140
  return bytesToBase64url(hashBytes);
144
141
  }
145
142
 
146
- // Helper to determine inline key
143
+ // Helper to determine inline key - matches Erlang's inline_key/2
147
144
  function inlineKey(msg) {
148
- var inlineBodyKey = msg["inline-body-key"];
149
- if (inlineBodyKey) {
150
- return [{}, inlineBodyKey];
145
+ // Check for ao-body-key (Erlang uses ao-body-key, not inline-body-key)
146
+ var aoBodyKey = msg["ao-body-key"];
147
+ if (aoBodyKey) {
148
+ return [{}, aoBodyKey];
151
149
  }
152
150
  if ("body" in msg) {
153
151
  return [{}, "body"];
154
152
  }
155
153
  if ("data" in msg) {
156
154
  return [{
157
- "inline-body-key": "data"
155
+ "ao-body-key": "data"
158
156
  }, "data"];
159
157
  }
160
158
  return [{}, "body"];
@@ -213,6 +211,13 @@ function ungroupIds(msg) {
213
211
  return result;
214
212
  }
215
213
 
214
+ // Get the size of a map (matches Erlang's maps:size behavior)
215
+ // This counts ALL keys including ao-types - empty means literally {}
216
+ function mapSize(obj) {
217
+ if (_typeof(obj) !== "object" || obj === null) return 0;
218
+ return Object.keys(obj).length;
219
+ }
220
+
216
221
  // Group maps for body encoding - following Erlang logic exactly
217
222
  function groupMaps(map) {
218
223
  var parent = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : "";
@@ -242,8 +247,19 @@ function groupMaps(map) {
242
247
  var normKey = normalizeKey(key);
243
248
  var flatK = parent ? "".concat(parent, "/").concat(normKey) : normKey;
244
249
  if (_typeof(value) === "object" && value !== null && !Array.isArray(value) && !Buffer.isBuffer(value)) {
245
- // Recursively process nested objects
246
- newTop = groupMaps(value, flatK, newTop);
250
+ // Check size of the nested object (including metadata keys like ao-types)
251
+ // Empty means literally {} - a map with only ao-types is NOT empty
252
+ var size = mapSize(value);
253
+ if (size === 0) {
254
+ // Empty map (no data keys) - add empty-message marker
255
+ // This matches Erlang's group_maps behavior for empty maps
256
+ newTop[flatK] = {
257
+ "ao-types": "empty-message"
258
+ };
259
+ } else {
260
+ // Recursively process nested objects
261
+ newTop = groupMaps(value, flatK, newTop);
262
+ }
247
263
  } else if (typeof value === "string" && value.length > MAX_HEADER_LENGTH) {
248
264
  // Value too large for header, lift to top level
249
265
  newTop[flatK] = value;
@@ -271,124 +287,74 @@ function groupMaps(map) {
271
287
  }
272
288
  }
273
289
 
290
+ // Helper to compute content-digest for a body value
291
+ function computePartDigest(bodyValue) {
292
+ var bodyBytes;
293
+ if (Buffer.isBuffer(bodyValue)) {
294
+ bodyBytes = new Uint8Array(bodyValue);
295
+ } else if (typeof bodyValue === "string") {
296
+ bodyBytes = stringToBytes(bodyValue, "binary");
297
+ } else {
298
+ bodyBytes = stringToBytes(String(bodyValue), "binary");
299
+ }
300
+ var hashBytes = (0, _fastSha.hash)(bodyBytes);
301
+ return "sha-256=:".concat(bytesToBase64(hashBytes), ":");
302
+ }
303
+
274
304
  // Encode multipart body part
305
+ // NOTE: This matches Erlang's encode_body_part/4 which does NOT apply inline_key
306
+ // logic to nested parts. For nested maps, ALL fields become headers (except 'body'
307
+ // which becomes the part body). The inline_key logic is only for top-level messages.
275
308
  function encodeBodyPart(partName, bodyPart, inlineKey) {
276
309
  var disposition = partName === inlineKey ? "inline" : "form-data;name=\"".concat(partName, "\"");
277
- var isInline = partName === inlineKey;
278
310
  if (_typeof(bodyPart) === "object" && bodyPart !== null && !Array.isArray(bodyPart) && !Buffer.isBuffer(bodyPart)) {
279
- // Check if this part has ao-types
280
- var hasAoTypes = "ao-types" in bodyPart;
281
- if (hasAoTypes) {
282
- // For parts WITH ao-types: sort all entries alphabetically
283
- var allEntries = [];
284
-
285
- // Collect all entries except body
286
- for (var _i6 = 0, _Object$entries6 = Object.entries(bodyPart); _i6 < _Object$entries6.length; _i6++) {
287
- var _Object$entries6$_i = _slicedToArray(_Object$entries6[_i6], 2),
288
- key = _Object$entries6$_i[0],
289
- value = _Object$entries6$_i[1];
290
- if (key === "body") continue;
291
- if (key === "ao-types") {
292
- // Keep ao-types as-is (Buffer or string)
293
- var valueStr = value;
294
- if (Buffer.isBuffer(value)) {
295
- valueStr = value.toString("binary");
296
- }
297
- allEntries.push({
298
- key: "ao-types",
299
- line: "ao-types: ".concat(valueStr)
300
- });
301
- } else {
302
- // Handle Buffer values properly
303
- var _valueStr = value;
304
- if (Buffer.isBuffer(value)) {
305
- // Use binary/latin1 encoding to preserve all byte values 0-255
306
- _valueStr = value.toString("binary");
307
- }
308
- allEntries.push({
309
- key: key,
310
- line: "".concat(key, ": ").concat(_valueStr)
311
- });
312
- }
311
+ // Collect all headers (everything except 'body' and 'priv')
312
+ var allEntries = [];
313
+ for (var _i6 = 0, _Object$entries6 = Object.entries(bodyPart); _i6 < _Object$entries6.length; _i6++) {
314
+ var _Object$entries6$_i = _slicedToArray(_Object$entries6[_i6], 2),
315
+ key = _Object$entries6$_i[0],
316
+ value = _Object$entries6$_i[1];
317
+ if (key === "body" || key === "priv") continue;
318
+
319
+ // Handle Buffer values properly
320
+ var valueStr = value;
321
+ if (Buffer.isBuffer(value)) {
322
+ // Use binary/latin1 encoding to preserve all byte values 0-255
323
+ valueStr = value.toString("binary");
313
324
  }
314
-
315
- // Add content-disposition
316
325
  allEntries.push({
317
- key: "content-disposition",
318
- line: "content-disposition: ".concat(disposition)
326
+ key: key,
327
+ line: "".concat(key, ": ").concat(valueStr)
319
328
  });
329
+ }
320
330
 
321
- // Sort alphabetically by key
322
- allEntries.sort(function (a, b) {
323
- return a.key.localeCompare(b.key);
324
- });
331
+ // Add content-disposition
332
+ allEntries.push({
333
+ key: "content-disposition",
334
+ line: "content-disposition: ".concat(disposition)
335
+ });
325
336
 
326
- // Build the lines
327
- var lines = allEntries.map(function (entry) {
328
- return entry.line;
329
- });
337
+ // Sort all entries by key alphabetically - matches Erlang behavior
338
+ allEntries.sort(function (a, b) {
339
+ return a.key.localeCompare(b.key);
340
+ });
330
341
 
331
- // Body handling
332
- var body = bodyPart.body || "";
333
- if (body) {
334
- lines.push(""); // Always add empty line before body
335
- lines.push(body);
336
- }
337
- return lines.join(CRLF);
338
- } else {
339
- // For parts WITHOUT ao-types
340
- var _allEntries = [];
341
- for (var _i7 = 0, _Object$entries7 = Object.entries(bodyPart); _i7 < _Object$entries7.length; _i7++) {
342
- var _Object$entries7$_i = _slicedToArray(_Object$entries7[_i7], 2),
343
- _key = _Object$entries7$_i[0],
344
- _value = _Object$entries7$_i[1];
345
- if (_key === "body") continue;
346
- // Handle Buffer values properly
347
- var _valueStr2 = _value;
348
- if (Buffer.isBuffer(_value)) {
349
- // Use binary/latin1 encoding to preserve all byte values 0-255
350
- _valueStr2 = _value.toString("binary");
351
- }
352
- _allEntries.push({
353
- key: _key,
354
- line: "".concat(_key, ": ").concat(_valueStr2)
355
- });
356
- }
357
- var _lines = [];
358
- if (isInline) {
359
- // Inline parts without ao-types: sort ALL fields alphabetically including content-disposition
360
- _allEntries.push({
361
- key: "content-disposition",
362
- line: "content-disposition: ".concat(disposition)
363
- });
364
-
365
- // Sort by key
366
- _allEntries.sort(function (a, b) {
367
- return a.key.localeCompare(b.key);
368
- });
369
-
370
- // Extract the lines
371
- _lines.push.apply(_lines, _toConsumableArray(_allEntries.map(function (entry) {
372
- return entry.line;
373
- })));
374
- } else {
375
- // Regular parts: content-disposition first, then fields
376
- _lines.push("content-disposition: ".concat(disposition));
377
- _lines.push.apply(_lines, _toConsumableArray(_allEntries.map(function (entry) {
378
- return entry.line;
379
- })));
380
- }
342
+ // Build the lines
343
+ var lines = allEntries.map(function (entry) {
344
+ return entry.line;
345
+ });
381
346
 
382
- // Body handling
383
- var _body = bodyPart.body || "";
384
- if (_body) {
385
- _lines.push(""); // Always add empty line before body
386
- _lines.push(_body);
387
- }
388
- return _lines.join(CRLF);
347
+ // Only the 'body' field (if present) becomes the part body
348
+ var body = bodyPart.body;
349
+ if (body !== "" && body !== undefined && body !== null) {
350
+ lines.push(""); // Always add empty line before body
351
+ lines.push(Buffer.isBuffer(body) ? body.toString("binary") : String(body));
389
352
  }
353
+ return lines.join(CRLF);
390
354
  } else if (typeof bodyPart === "string" || Buffer.isBuffer(bodyPart)) {
391
- return "content-disposition: ".concat(disposition).concat(DOUBLE_CRLF).concat(bodyPart);
355
+ // Use binary/latin1 encoding to preserve byte values 0-255
356
+ var bodyStr = Buffer.isBuffer(bodyPart) ? bodyPart.toString("binary") : bodyPart;
357
+ return "content-disposition: ".concat(disposition).concat(DOUBLE_CRLF).concat(bodyStr);
392
358
  }
393
359
  return "";
394
360
  }
@@ -406,8 +372,8 @@ function isBinaryData(buf) {
406
372
  // Count non-text bytes
407
373
  var controlCount = 0;
408
374
  var highByteCount = 0;
409
- for (var _i8 = 0; _i8 < checkLength; _i8++) {
410
- var _byte = buf[_i8];
375
+ for (var _i7 = 0; _i7 < checkLength; _i7++) {
376
+ var _byte = buf[_i7];
411
377
  // Non-printable chars (except CR/LF/TAB)
412
378
  if (_byte < 32 && _byte !== 9 && _byte !== 10 && _byte !== 13) {
413
379
  controlCount++;
@@ -561,11 +527,11 @@ function parseMultipart(contentType, body) {
561
527
  delete restHeaders["content-disposition"];
562
528
 
563
529
  // Add each header from the inline part to the top level of result
564
- for (var _i9 = 0, _Object$entries8 = Object.entries(restHeaders); _i9 < _Object$entries8.length; _i9++) {
565
- var _Object$entries8$_i = _slicedToArray(_Object$entries8[_i9], 2),
566
- key = _Object$entries8$_i[0],
567
- _value2 = _Object$entries8$_i[1];
568
- result[key] = _value2;
530
+ for (var _i8 = 0, _Object$entries7 = Object.entries(restHeaders); _i8 < _Object$entries7.length; _i8++) {
531
+ var _Object$entries7$_i = _slicedToArray(_Object$entries7[_i8], 2),
532
+ key = _Object$entries7$_i[0],
533
+ _value = _Object$entries7$_i[1];
534
+ result[key] = _value;
569
535
  }
570
536
 
571
537
  // If there's body content in the inline part, add it as 'body'
@@ -670,10 +636,10 @@ function httpsig_from(http) {
670
636
  // Convert flat structure to nested using flat.js
671
637
  var flat = {};
672
638
  var nonFlat = {};
673
- for (var _i0 = 0, _Object$entries9 = Object.entries(withBodyKeys); _i0 < _Object$entries9.length; _i0++) {
674
- var _Object$entries9$_i = _slicedToArray(_Object$entries9[_i0], 2),
675
- key = _Object$entries9$_i[0],
676
- value = _Object$entries9$_i[1];
639
+ for (var _i9 = 0, _Object$entries8 = Object.entries(withBodyKeys); _i9 < _Object$entries8.length; _i9++) {
640
+ var _Object$entries8$_i = _slicedToArray(_Object$entries8[_i9], 2),
641
+ key = _Object$entries8$_i[0],
642
+ value = _Object$entries8$_i[1];
677
643
  if (key.includes("/")) {
678
644
  flat[key] = value;
679
645
  } else {
@@ -706,10 +672,10 @@ function httpsig_from(http) {
706
672
  delete result["content-digest"];
707
673
 
708
674
  // Extract hashpaths if any
709
- for (var _i1 = 0, _Object$keys = Object.keys(result); _i1 < _Object$keys.length; _i1++) {
710
- var _key2 = _Object$keys[_i1];
711
- if (_key2.startsWith("hashpath")) {
712
- delete result[_key2];
675
+ for (var _i0 = 0, _Object$keys = Object.keys(result); _i0 < _Object$keys.length; _i0++) {
676
+ var _key = _Object$keys[_i0];
677
+ if (_key.startsWith("hashpath")) {
678
+ delete result[_key];
713
679
  }
714
680
  }
715
681
  return result;
@@ -717,12 +683,20 @@ function httpsig_from(http) {
717
683
 
718
684
  /**
719
685
  * Convert TABM to HTTP message
686
+ * Implements bundle mode like Erlang's dev_codec_httpsig_conv:to/3 with bundle=true
720
687
  */
721
688
  function httpsig_to(tabm) {
722
689
  if (typeof tabm === "string") return tabm;
723
690
 
691
+ // Bundle logic: TABM → structured → TABM
692
+ // This matches Erlang's behavior when bundle=true:
693
+ // 1. Convert TABM to structured@1.0 (interprets ao-types, decodes to native types)
694
+ // 2. Convert back to TABM (re-encodes with ao-types)
695
+ var structured = (0, _structured.structured_to)(tabm);
696
+ var bundledTabm = (0, _structured.structured_from)(structured);
697
+
724
698
  // Group IDs
725
- var withGroupedIds = groupIds(tabm);
699
+ var withGroupedIds = groupIds(bundledTabm);
726
700
 
727
701
  // Remove private and signature-related keys
728
702
  var stripped = _objectSpread({}, withGroupedIds);
@@ -736,24 +710,25 @@ function httpsig_to(tabm) {
736
710
  inlineKeyVal = _inlineKey4[1];
737
711
 
738
712
  // Check if this is a flat structure that should stay as headers
739
- // A flat structure has no nested objects (maps)
713
+ // A flat structure has no nested objects (maps), excluding:
714
+ // - Arrays (JS arrays, not numbered maps)
715
+ // - Buffers
716
+ // Note: List-encoded maps (numbered maps with .="list") ARE nested maps
717
+ // and should trigger multipart encoding, matching Erlang's behavior
740
718
  var hasNestedMaps = Object.values(stripped).some(function (value) {
741
- return _typeof(value) === "object" && value !== null && !Array.isArray(value) && !Buffer.isBuffer(value);
719
+ // Not an object
720
+ if (_typeof(value) !== "object" || value === null) return false;
721
+ // Arrays and Buffers are not nested maps
722
+ if (Array.isArray(value) || Buffer.isBuffer(value)) return false;
723
+ // Any other object (including list-encoded maps) is a nested map
724
+ return true;
742
725
  });
743
726
 
744
727
  // If it's just a flat map with strings/primitives, keep as headers
745
728
  // This matches Erlang's behavior where flat maps don't become multipart
746
729
  if (!hasNestedMaps) {
747
730
  // For flat structures, just return with normalized keys
748
- // This matches Erlang which returns the map unchanged
749
- var result = _objectSpread({}, inlineFieldHdrs);
750
- for (var _i10 = 0, _Object$entries0 = Object.entries(stripped); _i10 < _Object$entries0.length; _i10++) {
751
- var _Object$entries0$_i = _slicedToArray(_Object$entries0[_i10], 2),
752
- key = _Object$entries0$_i[0],
753
- value = _Object$entries0$_i[1];
754
- // Keep Buffers as Buffers - don't convert to strings
755
- result[key] = value;
756
- }
731
+ var result = _objectSpread(_objectSpread({}, inlineFieldHdrs), stripped);
757
732
 
758
733
  // Handle inline body key - move data from inline key to body
759
734
  if (inlineKeyVal && inlineKeyVal !== "body" && result[inlineKeyVal]) {
@@ -761,9 +736,22 @@ function httpsig_to(tabm) {
761
736
  delete result[inlineKeyVal];
762
737
  }
763
738
 
764
- // If there's a body, add content-digest
739
+ // If the only field is ao-types (no actual data), return empty object
740
+ // This matches Erlang's behavior where ao-types-only messages become empty
741
+ var dataKeys = Object.keys(result).filter(function (k) {
742
+ return k !== "ao-types" && k !== "ao-ids" && k !== "inline-body-key" && k !== "ao-body-key";
743
+ });
744
+ if (dataKeys.length === 0) {
745
+ return {};
746
+ }
747
+
748
+ // If there's a non-empty body, add content-digest
749
+ // Erlang doesn't add content-digest for empty bodies (<<>>)
765
750
  if (result.body) {
766
- return addContentDigest(result);
751
+ var bodyIsEmpty = Buffer.isBuffer(result.body) ? result.body.length === 0 : typeof result.body === "string" && result.body.length === 0;
752
+ if (!bodyIsEmpty) {
753
+ return addContentDigest(result);
754
+ }
767
755
  }
768
756
  return result;
769
757
  }
@@ -773,26 +761,26 @@ function httpsig_to(tabm) {
773
761
  var headers = _objectSpread({}, inlineFieldHdrs);
774
762
 
775
763
  // Process each field - ao-types at top level should go to headers
776
- for (var _i11 = 0, _Object$entries1 = Object.entries(stripped); _i11 < _Object$entries1.length; _i11++) {
777
- var _Object$entries1$_i = _slicedToArray(_Object$entries1[_i11], 2),
778
- _key3 = _Object$entries1$_i[0],
779
- _value3 = _Object$entries1$_i[1];
780
- if (_key3 === "ao-types") {
764
+ for (var _i1 = 0, _Object$entries9 = Object.entries(stripped); _i1 < _Object$entries9.length; _i1++) {
765
+ var _Object$entries9$_i = _slicedToArray(_Object$entries9[_i1], 2),
766
+ key = _Object$entries9$_i[0],
767
+ value = _Object$entries9$_i[1];
768
+ if (key === "ao-types") {
781
769
  // Top-level ao-types goes to headers only
782
770
  // Keep as Buffer if it's a Buffer, otherwise use as-is
783
- headers[_key3] = _value3;
784
- } else if (_key3 === "body" || _key3 === inlineKeyVal) {
785
- bodyMap[_key3 === inlineKeyVal ? inlineKeyVal : "body"] = _value3;
786
- } else if (_typeof(_value3) === "object" && _value3 !== null && !Array.isArray(_value3) && !Buffer.isBuffer(_value3)) {
787
- bodyMap[_key3] = _value3;
788
- } else if (typeof _value3 === "string" && _value3.length <= MAX_HEADER_LENGTH && _key3 !== "ao-types") {
789
- headers[normalizeKey(_key3)] = _value3;
790
- } else if (Buffer.isBuffer(_value3) && _value3.length <= MAX_HEADER_LENGTH && _key3 !== "ao-types") {
771
+ headers[key] = value;
772
+ } else if (key === "body" || key === inlineKeyVal) {
773
+ bodyMap[key === inlineKeyVal ? inlineKeyVal : "body"] = value;
774
+ } else if (_typeof(value) === "object" && value !== null && !Array.isArray(value) && !Buffer.isBuffer(value)) {
775
+ bodyMap[key] = value;
776
+ } else if (typeof value === "string" && value.length <= MAX_HEADER_LENGTH && key !== "ao-types") {
777
+ headers[normalizeKey(key)] = value;
778
+ } else if (Buffer.isBuffer(value) && value.length <= MAX_HEADER_LENGTH && key !== "ao-types") {
791
779
  // Keep buffers as buffers for headers
792
- headers[normalizeKey(_key3)] = _value3;
793
- } else if (_key3 !== "ao-types") {
780
+ headers[normalizeKey(key)] = value;
781
+ } else if (key !== "ao-types") {
794
782
  // Only add to bodyMap if it's not ao-types
795
- bodyMap[_key3] = _value3;
783
+ bodyMap[key] = value;
796
784
  }
797
785
  }
798
786
 
@@ -821,22 +809,22 @@ function httpsig_to(tabm) {
821
809
  try {
822
810
  for (_iterator4.s(); !(_step4 = _iterator4.n()).done;) {
823
811
  var _step4$value = _slicedToArray(_step4.value, 2),
824
- _key4 = _step4$value[0],
825
- _value4 = _step4$value[1];
826
- if (_typeof(_value4) === "object" && _value4 !== null && Object.keys(_value4).length === 1 && "body" in _value4) {
827
- var encoded = encodeBodyPart("".concat(_key4, "/body"), _value4, "body");
812
+ _key2 = _step4$value[0],
813
+ _value2 = _step4$value[1];
814
+ if (_typeof(_value2) === "object" && _value2 !== null && Object.keys(_value2).length === 1 && "body" in _value2) {
815
+ var encoded = encodeBodyPart("".concat(_key2, "/body"), _value2, "body");
828
816
  parts.push({
829
- name: "".concat(_key4, "/body"),
817
+ name: "".concat(_key2, "/body"),
830
818
  body: encoded
831
819
  });
832
- bodyKeysList.push(_key4);
820
+ bodyKeysList.push(_key2);
833
821
  } else {
834
- var _encoded = encodeBodyPart(_key4, _value4, inlineKeyVal);
822
+ var _encoded = encodeBodyPart(_key2, _value2, inlineKeyVal);
835
823
  parts.push({
836
- name: _key4,
824
+ name: _key2,
837
825
  body: _encoded
838
826
  });
839
- bodyKeysList.push(_key4);
827
+ bodyKeysList.push(_key2);
840
828
  }
841
829
  }
842
830
  } catch (err) {
@@ -850,9 +838,7 @@ function httpsig_to(tabm) {
850
838
  });
851
839
  var finalBody = bodyParts.join(CRLF) + "".concat(CRLF, "--").concat(boundary, "--");
852
840
  var _result2 = _objectSpread(_objectSpread({}, headers), {}, {
853
- "body-keys": bodyKeysList.map(function (k) {
854
- return "\"".concat(k, "\"");
855
- }).join(", "),
841
+ // Note: body-keys is NOT included in httpsig output - it's only used for parsing
856
842
  "content-type": "multipart/form-data; boundary=\"".concat(boundary, "\""),
857
843
  body: finalBody
858
844
  });