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/commit.js +263 -43
- package/cjs/encode-utils.js +10 -4
- package/cjs/encode.js +66 -19
- package/cjs/erl_json.js +12 -3
- package/cjs/erl_str.js +5 -0
- package/cjs/flat.js +70 -13
- package/cjs/httpsig.js +159 -173
- package/cjs/parser.js +30 -6
- package/cjs/send.js +11 -7
- package/cjs/signer-utils.js +14 -12
- package/cjs/signer.js +140 -281
- package/cjs/structured.js +140 -146
- package/cjs/test.js +0 -15
- package/esm/commit.js +174 -19
- package/esm/encode-utils.js +10 -4
- package/esm/encode.js +52 -15
- package/esm/erl_json.js +4 -1
- package/esm/erl_str.js +5 -0
- package/esm/flat.js +61 -7
- package/esm/httpsig.js +118 -113
- package/esm/parser.js +26 -8
- package/esm/send.js +8 -3
- package/esm/signer-utils.js +5 -1
- package/esm/signer.js +66 -174
- package/esm/structured.js +97 -98
- package/esm/test.js +2 -6
- package/package.json +2 -2
package/cjs/structured.js
CHANGED
|
@@ -77,7 +77,7 @@ function to(tabm) {
|
|
|
77
77
|
// Build result with empty values first
|
|
78
78
|
var result = {};
|
|
79
79
|
|
|
80
|
-
// Add empty values based on their types
|
|
80
|
+
// Add empty values based on their types (these may be overwritten if data exists)
|
|
81
81
|
for (var _i = 0, _Object$entries = Object.entries(types); _i < _Object$entries.length; _i++) {
|
|
82
82
|
var _Object$entries$_i = _slicedToArray(_Object$entries[_i], 2),
|
|
83
83
|
key = _Object$entries$_i[0],
|
|
@@ -111,10 +111,19 @@ function to(tabm) {
|
|
|
111
111
|
result[rawKey] = value;
|
|
112
112
|
}
|
|
113
113
|
} else if (_typeof(value) === "object" && value !== null && !Array.isArray(value)) {
|
|
114
|
+
// Check if the child object itself indicates it's a list via .="list" in its ao-types
|
|
115
|
+
var childAoTypes = value["ao-types"] || "";
|
|
116
|
+
var childTypes = parseAoTypes(childAoTypes);
|
|
117
|
+
var isChildList = childTypes["."] === "list";
|
|
118
|
+
|
|
114
119
|
// Recursively decode child TABM
|
|
115
120
|
var childDecoded = to(value);
|
|
116
|
-
|
|
117
|
-
if
|
|
121
|
+
|
|
122
|
+
// Only convert numbered map to array if the child object itself
|
|
123
|
+
// declares it's a list via .="list" in its ao-types.
|
|
124
|
+
// This preserves original keys when the parent declares the type
|
|
125
|
+
// but the child doesn't have the list marker.
|
|
126
|
+
if (isChildList) {
|
|
118
127
|
// Convert numbered map back to ordered list
|
|
119
128
|
result[rawKey] = messageToOrderedList(childDecoded);
|
|
120
129
|
} else {
|
|
@@ -214,11 +223,22 @@ function decodeValue(type, value) {
|
|
|
214
223
|
return parseInt(parseStructuredItem(value), 10);
|
|
215
224
|
case "float":
|
|
216
225
|
return parseFloat(value);
|
|
226
|
+
case "boolean":
|
|
227
|
+
// SF boolean format: ?1 = true, ?0 = false
|
|
228
|
+
// Convert to native boolean, will be encoded as "atom" type
|
|
229
|
+
if (value === "?1") return true;
|
|
230
|
+
if (value === "?0") return false;
|
|
231
|
+
// Fallback for other formats
|
|
232
|
+
return value === "true" || value === "1";
|
|
217
233
|
case "atom":
|
|
218
234
|
var atomItem = parseStructuredItem(value);
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
235
|
+
var atomName = atomItem.replace(/^"|"$/g, ""); // Remove quotes
|
|
236
|
+
// Convert to Symbol to preserve atom type through round-trip
|
|
237
|
+
// Special cases for common atoms that JS has native types for
|
|
238
|
+
if (atomName === "true") return true;
|
|
239
|
+
if (atomName === "false") return false;
|
|
240
|
+
if (atomName === "null") return null;
|
|
241
|
+
return Symbol["for"](atomName);
|
|
222
242
|
case "list":
|
|
223
243
|
return parseStructuredList(value).map(function (item) {
|
|
224
244
|
if (typeof item === "string" && item.startsWith("(ao-type-")) {
|
|
@@ -245,7 +265,8 @@ function decodeValue(type, value) {
|
|
|
245
265
|
* @returns {*} - Parsed value
|
|
246
266
|
*/
|
|
247
267
|
function parseStructuredItem(value) {
|
|
248
|
-
//
|
|
268
|
+
// Handle non-string values (e.g., numbers from HyperBEAM responses)
|
|
269
|
+
if (typeof value !== "string") return String(value);
|
|
249
270
|
if (value.startsWith('"') && value.endsWith('"')) {
|
|
250
271
|
return value.slice(1, -1); // Remove quotes
|
|
251
272
|
}
|
|
@@ -258,10 +279,13 @@ function parseStructuredItem(value) {
|
|
|
258
279
|
* @returns {Array} - Parsed list
|
|
259
280
|
*/
|
|
260
281
|
function parseStructuredList(value) {
|
|
261
|
-
|
|
282
|
+
if (typeof value !== "string") return [value];
|
|
262
283
|
return value.split(", ").map(function (item) {
|
|
263
284
|
if (item.startsWith('"') && item.endsWith('"')) {
|
|
264
|
-
|
|
285
|
+
// Remove quotes and unescape SF string escapes
|
|
286
|
+
// In SF strings: \" = " and \\ = \
|
|
287
|
+
return item.slice(1, -1).replace(/\\"/g, '"') // \" -> "
|
|
288
|
+
.replace(/\\\\/g, '\\'); // \\ -> \
|
|
265
289
|
}
|
|
266
290
|
return item;
|
|
267
291
|
});
|
|
@@ -273,23 +297,50 @@ function parseStructuredList(value) {
|
|
|
273
297
|
* @returns {object} - TABM
|
|
274
298
|
*/
|
|
275
299
|
function from(msg) {
|
|
276
|
-
// Handle
|
|
277
|
-
if (msg instanceof Buffer ||
|
|
300
|
+
// Handle binary input - passthrough
|
|
301
|
+
if (msg instanceof Buffer || msg instanceof Uint8Array) {
|
|
302
|
+
return msg;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
// Handle non-object values - passthrough
|
|
306
|
+
if (_typeof(msg) !== "object" || msg === null) {
|
|
278
307
|
return msg;
|
|
279
308
|
}
|
|
280
309
|
|
|
281
|
-
//
|
|
282
|
-
|
|
310
|
+
// Handle arrays - convert to numbered map with .="list" in ao-types
|
|
311
|
+
// Mirrors Erlang: from(List, Req, Opts) when is_list(List)
|
|
312
|
+
if (Array.isArray(msg)) {
|
|
313
|
+
// Convert to numbered map (1-based indexing like Erlang)
|
|
314
|
+
var numberedMap = {};
|
|
315
|
+
msg.forEach(function (item, idx) {
|
|
316
|
+
numberedMap[(idx + 1).toString()] = item;
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
// Recursively process the numbered map
|
|
320
|
+
var _result = from(numberedMap);
|
|
321
|
+
|
|
322
|
+
// Add .="list" to ao-types to indicate this message is a list
|
|
323
|
+
var existingAoTypes = _result["ao-types"] || "";
|
|
324
|
+
if (existingAoTypes) {
|
|
325
|
+
_result["ao-types"] = '.="list", ' + existingAoTypes;
|
|
326
|
+
} else {
|
|
327
|
+
_result["ao-types"] = '.="list"';
|
|
328
|
+
}
|
|
329
|
+
return _result;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
// Process keys - preserve original case to match Erlang behavior
|
|
333
|
+
// HTTP headers are case-insensitive but JSON/map keys preserve case
|
|
334
|
+
var keysMap = {};
|
|
283
335
|
for (var _i3 = 0, _Object$entries3 = Object.entries(msg); _i3 < _Object$entries3.length; _i3++) {
|
|
284
336
|
var _Object$entries3$_i = _slicedToArray(_Object$entries3[_i3], 2),
|
|
285
337
|
key = _Object$entries3$_i[0],
|
|
286
338
|
value = _Object$entries3$_i[1];
|
|
287
|
-
|
|
288
|
-
normalizedMap[normKey] = value;
|
|
339
|
+
keysMap[key] = value;
|
|
289
340
|
}
|
|
290
341
|
|
|
291
|
-
// Get sorted keys (
|
|
292
|
-
var sortedKeys = Object.keys(
|
|
342
|
+
// Get sorted keys (preserving case)
|
|
343
|
+
var sortedKeys = Object.keys(keysMap).sort();
|
|
293
344
|
var types = [];
|
|
294
345
|
var values = [];
|
|
295
346
|
|
|
@@ -297,77 +348,63 @@ function from(msg) {
|
|
|
297
348
|
var _iterator2 = _createForOfIteratorHelper(sortedKeys),
|
|
298
349
|
_step2;
|
|
299
350
|
try {
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
types.push([normKey, "empty-list"]);
|
|
311
|
-
return 0; // continue
|
|
312
|
-
}
|
|
313
|
-
if (_typeof(value) === "object" && value !== null && !Array.isArray(value) && !(value instanceof Buffer) && Object.keys(value).length === 0) {
|
|
314
|
-
types.push([normKey, "empty-message"]);
|
|
315
|
-
return 0; // continue
|
|
316
|
-
}
|
|
351
|
+
for (_iterator2.s(); !(_step2 = _iterator2.n()).done;) {
|
|
352
|
+
var _key = _step2.value;
|
|
353
|
+
var _value = keysMap[_key];
|
|
354
|
+
|
|
355
|
+
// Handle empty binaries/strings - just include as-is, no type annotation
|
|
356
|
+
// (Erlang doesn't add empty-binary type, it just keeps the empty binary)
|
|
357
|
+
if (_value === "" || _value instanceof Buffer && _value.length === 0) {
|
|
358
|
+
values.push([_key, _value]);
|
|
359
|
+
continue;
|
|
360
|
+
}
|
|
317
361
|
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
values.push([normKey, value]);
|
|
325
|
-
return 0; // continue
|
|
326
|
-
}
|
|
362
|
+
// Empty arrays - convert to numbered map with .="list" in ao-types
|
|
363
|
+
// (Erlang doesn't add empty-list type, just the list marker)
|
|
364
|
+
if (Array.isArray(_value) && _value.length === 0) {
|
|
365
|
+
values.push([_key, from(_value)]);
|
|
366
|
+
continue;
|
|
367
|
+
}
|
|
327
368
|
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
369
|
+
// Empty objects - just include as-is, no type annotation
|
|
370
|
+
// (Erlang doesn't add empty-message type, it just keeps the empty map)
|
|
371
|
+
if (_typeof(_value) === "object" && _value !== null && !Array.isArray(_value) && !(_value instanceof Buffer) && Object.keys(_value).length === 0) {
|
|
372
|
+
values.push([_key, _value]);
|
|
373
|
+
continue;
|
|
374
|
+
}
|
|
333
375
|
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
values.push([normKey, from(numberedMap)]);
|
|
344
|
-
} else {
|
|
345
|
-
// Encode as list string
|
|
346
|
-
var _encodeValue = encodeValue(value),
|
|
347
|
-
_encodeValue2 = _slicedToArray(_encodeValue, 2),
|
|
348
|
-
type = _encodeValue2[0],
|
|
349
|
-
encoded = _encodeValue2[1];
|
|
350
|
-
types.push([normKey, type]);
|
|
351
|
-
values.push([normKey, encoded]);
|
|
352
|
-
}
|
|
353
|
-
return 0; // continue
|
|
354
|
-
}
|
|
376
|
+
// Handle binary/string values
|
|
377
|
+
if (_value instanceof Buffer || _value instanceof Uint8Array) {
|
|
378
|
+
values.push([_key, _value]);
|
|
379
|
+
continue;
|
|
380
|
+
}
|
|
381
|
+
if (typeof _value === "string") {
|
|
382
|
+
values.push([_key, _value]);
|
|
383
|
+
continue;
|
|
384
|
+
}
|
|
355
385
|
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
}
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
386
|
+
// Handle nested maps
|
|
387
|
+
if (_typeof(_value) === "object" && !Array.isArray(_value) && _value !== null) {
|
|
388
|
+
values.push([_key, from(_value)]);
|
|
389
|
+
continue;
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
// Handle arrays - from() converts to numbered map with .="list" in ao-types
|
|
393
|
+
if (Array.isArray(_value) && _value.length > 0) {
|
|
394
|
+
values.push([_key, from(_value)]);
|
|
395
|
+
continue;
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
// Handle typed values (need encoding)
|
|
399
|
+
if (_typeof(_value) === "symbol" || typeof _value === "number" || typeof _value === "boolean" || _value === null) {
|
|
400
|
+
var _encodeValue = encodeValue(_value),
|
|
401
|
+
_encodeValue2 = _slicedToArray(_encodeValue, 2),
|
|
402
|
+
type = _encodeValue2[0],
|
|
403
|
+
encoded = _encodeValue2[1];
|
|
404
|
+
types.push([_key, type]);
|
|
405
|
+
values.push([_key, encoded]);
|
|
406
|
+
continue;
|
|
407
|
+
}
|
|
371
408
|
}
|
|
372
409
|
|
|
373
410
|
// Build result
|
|
@@ -398,55 +435,13 @@ function from(msg) {
|
|
|
398
435
|
return result;
|
|
399
436
|
}
|
|
400
437
|
|
|
401
|
-
/**
|
|
402
|
-
* Check if an array should be converted to numbered map
|
|
403
|
-
* Rules based on Erlang behavior:
|
|
404
|
-
* 1. Contains any objects/maps → convert
|
|
405
|
-
* 2. Contains empty arrays (but NOT empty buffers) → convert
|
|
406
|
-
* 3. All items are arrays (array of arrays) → convert
|
|
407
|
-
* 4. Otherwise → encode as string
|
|
408
|
-
*/
|
|
409
|
-
function shouldConvertToNumberedMap(arr) {
|
|
410
|
-
var allArrays = true;
|
|
411
|
-
var hasObjects = false;
|
|
412
|
-
var hasEmptyArrays = false;
|
|
413
|
-
var _iterator3 = _createForOfIteratorHelper(arr),
|
|
414
|
-
_step3;
|
|
415
|
-
try {
|
|
416
|
-
for (_iterator3.s(); !(_step3 = _iterator3.n()).done;) {
|
|
417
|
-
var item = _step3.value;
|
|
418
|
-
// Check for objects (not arrays or buffers)
|
|
419
|
-
if (_typeof(item) === "object" && item !== null && !Array.isArray(item) && !Buffer.isBuffer(item)) {
|
|
420
|
-
hasObjects = true;
|
|
421
|
-
}
|
|
422
|
-
// Check for empty arrays only (NOT empty buffers)
|
|
423
|
-
else if (Array.isArray(item) && item.length === 0) {
|
|
424
|
-
hasEmptyArrays = true;
|
|
425
|
-
}
|
|
426
|
-
// Track if all items are arrays
|
|
427
|
-
else if (!Array.isArray(item)) {
|
|
428
|
-
allArrays = false;
|
|
429
|
-
}
|
|
430
|
-
}
|
|
431
|
-
|
|
432
|
-
// Convert if: has objects, has empty arrays, or all items are non-empty arrays
|
|
433
|
-
} catch (err) {
|
|
434
|
-
_iterator3.e(err);
|
|
435
|
-
} finally {
|
|
436
|
-
_iterator3.f();
|
|
437
|
-
}
|
|
438
|
-
return hasObjects || hasEmptyArrays || allArrays && arr.length > 0 && arr.every(function (item) {
|
|
439
|
-
return Array.isArray(item);
|
|
440
|
-
});
|
|
441
|
-
}
|
|
442
|
-
|
|
443
438
|
/**
|
|
444
439
|
* Encode a value with its type
|
|
445
440
|
*/
|
|
446
441
|
function encodeValue(value) {
|
|
447
|
-
// Null (as atom)
|
|
442
|
+
// Null (as atom) - use token format (unquoted)
|
|
448
443
|
if (value === null) {
|
|
449
|
-
return ["atom",
|
|
444
|
+
return ["atom", "null"];
|
|
450
445
|
}
|
|
451
446
|
|
|
452
447
|
// Integer
|
|
@@ -456,44 +451,43 @@ function encodeValue(value) {
|
|
|
456
451
|
|
|
457
452
|
// Float
|
|
458
453
|
if (typeof value === "number") {
|
|
459
|
-
// Format like Erlang
|
|
454
|
+
// Format like Erlang's float_to_binary - scientific notation with full precision
|
|
455
|
+
// Erlang's float_to_binary/1 uses ~20 decimal digits and keeps trailing zeros
|
|
460
456
|
var str = value.toExponential(20);
|
|
461
|
-
//
|
|
462
|
-
str = str.replace(/(\.\d*?)0+e/, "$1e").replace(/\.e/, ".0e");
|
|
463
|
-
// Ensure 2-digit exponent
|
|
457
|
+
// Ensure 2-digit exponent with sign
|
|
464
458
|
str = str.replace(/e([+-])(\d)$/, "e$10$2");
|
|
465
459
|
return ["float", str];
|
|
466
460
|
}
|
|
467
461
|
|
|
468
|
-
// Boolean (as atom)
|
|
462
|
+
// Boolean (as atom) - use token format (unquoted)
|
|
469
463
|
if (typeof value === "boolean") {
|
|
470
|
-
return ["atom",
|
|
464
|
+
return ["atom", value.toString()];
|
|
471
465
|
}
|
|
472
466
|
|
|
473
|
-
// Symbol (as atom)
|
|
467
|
+
// Symbol (as atom) - use token format (unquoted)
|
|
474
468
|
if (_typeof(value) === "symbol") {
|
|
475
469
|
var name = Symbol.keyFor(value) || value.description || "";
|
|
476
|
-
return ["atom",
|
|
470
|
+
return ["atom", name];
|
|
477
471
|
}
|
|
478
472
|
|
|
479
473
|
// List
|
|
480
474
|
if (Array.isArray(value)) {
|
|
481
475
|
var parts = [];
|
|
482
|
-
var
|
|
483
|
-
|
|
476
|
+
var _iterator3 = _createForOfIteratorHelper(value),
|
|
477
|
+
_step3;
|
|
484
478
|
try {
|
|
485
|
-
for (
|
|
486
|
-
var item =
|
|
479
|
+
for (_iterator3.s(); !(_step3 = _iterator3.n()).done;) {
|
|
480
|
+
var item = _step3.value;
|
|
487
481
|
if (item instanceof Buffer) {
|
|
488
482
|
// Empty buffer => empty string
|
|
489
483
|
parts.push(item.length === 0 ? '""' : "\"".concat(item.toString(), "\""));
|
|
490
484
|
} else if (typeof item === "string") {
|
|
491
485
|
parts.push("\"".concat(item, "\""));
|
|
492
486
|
} else {
|
|
493
|
-
var
|
|
494
|
-
|
|
495
|
-
itemType =
|
|
496
|
-
itemEncoded =
|
|
487
|
+
var _encodeValue3 = encodeValue(item),
|
|
488
|
+
_encodeValue4 = _slicedToArray(_encodeValue3, 2),
|
|
489
|
+
itemType = _encodeValue4[0],
|
|
490
|
+
itemEncoded = _encodeValue4[1];
|
|
497
491
|
if (itemType === "list") {
|
|
498
492
|
// Escape nested list quotes
|
|
499
493
|
var escaped = itemEncoded.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
|
|
@@ -508,9 +502,9 @@ function encodeValue(value) {
|
|
|
508
502
|
}
|
|
509
503
|
}
|
|
510
504
|
} catch (err) {
|
|
511
|
-
|
|
505
|
+
_iterator3.e(err);
|
|
512
506
|
} finally {
|
|
513
|
-
|
|
507
|
+
_iterator3.f();
|
|
514
508
|
}
|
|
515
509
|
return ["list", parts.join(", ")];
|
|
516
510
|
}
|
package/cjs/test.js
CHANGED
|
@@ -3,25 +3,10 @@
|
|
|
3
3
|
Object.defineProperty(exports, "__esModule", {
|
|
4
4
|
value: true
|
|
5
5
|
});
|
|
6
|
-
Object.defineProperty(exports, "acc", {
|
|
7
|
-
enumerable: true,
|
|
8
|
-
get: function get() {
|
|
9
|
-
return _accounts.acc;
|
|
10
|
-
}
|
|
11
|
-
});
|
|
12
6
|
Object.defineProperty(exports, "toAddr", {
|
|
13
7
|
enumerable: true,
|
|
14
8
|
get: function get() {
|
|
15
9
|
return _utils.toAddr;
|
|
16
10
|
}
|
|
17
11
|
});
|
|
18
|
-
Object.defineProperty(exports, "wait", {
|
|
19
|
-
enumerable: true,
|
|
20
|
-
get: function get() {
|
|
21
|
-
return _utils.wait;
|
|
22
|
-
}
|
|
23
|
-
});
|
|
24
|
-
var _fs = require("fs");
|
|
25
|
-
var _path = require("path");
|
|
26
|
-
var _accounts = require("./accounts.js");
|
|
27
12
|
var _utils = require("./utils.js");
|
package/esm/commit.js
CHANGED
|
@@ -1,7 +1,42 @@
|
|
|
1
|
-
import { id, base, hashpath, rsaid
|
|
1
|
+
import { id, base, hashpath, rsaid } from "./id.js"
|
|
2
2
|
import { toAddr } from "./utils.js"
|
|
3
3
|
import { extractPubKey } from "./signer-utils.js"
|
|
4
4
|
import { verify } from "./signer-utils.js"
|
|
5
|
+
import crypto from "crypto"
|
|
6
|
+
|
|
7
|
+
// Helper to compute SHA-256 content-digest in RFC 9530 format
|
|
8
|
+
const computeContentDigest = (body) => {
|
|
9
|
+
let bodyBuffer
|
|
10
|
+
if (Buffer.isBuffer(body)) {
|
|
11
|
+
bodyBuffer = body
|
|
12
|
+
} else if (body instanceof Blob) {
|
|
13
|
+
return null // Can't compute synchronously for Blob
|
|
14
|
+
} else if (typeof body === "string") {
|
|
15
|
+
bodyBuffer = Buffer.from(body, "binary")
|
|
16
|
+
} else {
|
|
17
|
+
bodyBuffer = Buffer.from(String(body), "binary")
|
|
18
|
+
}
|
|
19
|
+
const hash = crypto.createHash("sha256").update(bodyBuffer).digest("base64")
|
|
20
|
+
return `sha-256=:${hash}:`
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// Helper to build ao-types string from an object
|
|
24
|
+
const buildAoTypes = (obj) => {
|
|
25
|
+
const types = []
|
|
26
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
27
|
+
if (typeof value === "number") {
|
|
28
|
+
types.push(`${key}="${Number.isInteger(value) ? "integer" : "float"}"`)
|
|
29
|
+
} else if (typeof value === "boolean") {
|
|
30
|
+
types.push(`${key}="atom"`)
|
|
31
|
+
} else if (value === null) {
|
|
32
|
+
types.push(`${key}="atom"`)
|
|
33
|
+
} else if (typeof value === "symbol") {
|
|
34
|
+
// Symbols are Erlang atoms
|
|
35
|
+
types.push(`${key}="atom"`)
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
return types.length > 0 ? types.join(", ") : null
|
|
39
|
+
}
|
|
5
40
|
|
|
6
41
|
// todo: handle @
|
|
7
42
|
export const commit = async (obj, opts) => {
|
|
@@ -12,19 +47,63 @@ export const commit = async (obj, opts) => {
|
|
|
12
47
|
|
|
13
48
|
let body = {}
|
|
14
49
|
|
|
15
|
-
// Check for inline-body-key
|
|
16
|
-
const inlineBodyKey = msg.headers["inline-body-key"]
|
|
50
|
+
// Check for inline-body-key (indicates a field was moved to HTTP body during encoding)
|
|
51
|
+
const inlineBodyKey = msg.headers["inline-body-key"] || msg.headers["ao-body-key"]
|
|
52
|
+
|
|
53
|
+
// Body field names that HyperBEAM's inline_key() recognizes natively.
|
|
54
|
+
// For these, normalize_for_encoding() re-derives ao-body-key automatically.
|
|
55
|
+
// For custom names (e.g., "json"), we must keep ao-body-key in committed list
|
|
56
|
+
// so HyperBEAM knows which field to inline during verification.
|
|
57
|
+
const NATIVE_BODY_KEYS = new Set(["body", "data"])
|
|
58
|
+
const isNativeBodyKey = !inlineBodyKey || NATIVE_BODY_KEYS.has(inlineBodyKey)
|
|
59
|
+
|
|
60
|
+
// Build body from committed components.
|
|
61
|
+
// IMPORTANT: Use original typed values from obj (preserves integers, booleans, etc.)
|
|
62
|
+
// instead of string values from headers. This is critical because:
|
|
63
|
+
// 1. HTTP headers are always strings (e.g., quantity: 100 → "100")
|
|
64
|
+
// 2. We include ao-types to tell HyperBEAM the original types
|
|
65
|
+
// 3. HyperBEAM applies ao-types BEFORE signature verification
|
|
66
|
+
// 4. If we send strings, HyperBEAM converts to integers, breaking signature
|
|
67
|
+
// 5. If we send original integers, HyperBEAM re-encodes them as strings for verification
|
|
68
|
+
//
|
|
69
|
+
// Always skip content-digest and inline-body-key (transport artifacts re-derived by HyperBEAM).
|
|
70
|
+
// Skip ao-body-key only for native body keys (HyperBEAM re-derives it).
|
|
71
|
+
// Keep ao-body-key for custom body keys (HyperBEAM needs it to find the body field).
|
|
72
|
+
|
|
73
|
+
// Create case-insensitive lookup maps
|
|
74
|
+
const headerLookup = new Map()
|
|
75
|
+
for (const [k, v] of Object.entries(msg.headers)) {
|
|
76
|
+
headerLookup.set(k.toLowerCase(), v)
|
|
77
|
+
}
|
|
78
|
+
// Case-insensitive lookup for original object values (preserves types)
|
|
79
|
+
const objLookup = new Map()
|
|
80
|
+
for (const [k, v] of Object.entries(obj)) {
|
|
81
|
+
objLookup.set(k.toLowerCase(), v)
|
|
82
|
+
}
|
|
17
83
|
|
|
18
|
-
// Build body from components
|
|
19
84
|
for (const v of components) {
|
|
20
85
|
const key = v === "@path" ? "path" : v
|
|
21
|
-
|
|
86
|
+
if (key === "content-length") continue
|
|
87
|
+
if (key === "content-digest") continue
|
|
88
|
+
if (key === "inline-body-key") continue
|
|
89
|
+
if (isNativeBodyKey && key === "ao-body-key") continue
|
|
90
|
+
|
|
91
|
+
// Prefer original value from input object (preserves integer/boolean/etc. types)
|
|
92
|
+
// Fall back to header string value if not found in original object
|
|
93
|
+
const originalValue = objLookup.get(key.toLowerCase())
|
|
94
|
+
if (originalValue !== undefined) {
|
|
95
|
+
body[key] = originalValue
|
|
96
|
+
} else {
|
|
97
|
+
const headerValue = headerLookup.get(key)
|
|
98
|
+
if (headerValue !== undefined) {
|
|
99
|
+
body[key] = headerValue
|
|
100
|
+
}
|
|
101
|
+
}
|
|
22
102
|
}
|
|
23
103
|
|
|
24
|
-
// Handle body resolution
|
|
104
|
+
// Handle body resolution - restore the inlined field to its AO-Core name
|
|
105
|
+
let bodyContent = null
|
|
25
106
|
if (msg.body) {
|
|
26
|
-
let bodyContent
|
|
27
|
-
|
|
28
107
|
if (msg.body instanceof Blob) {
|
|
29
108
|
const arrayBuffer = await msg.body.arrayBuffer()
|
|
30
109
|
bodyContent = Buffer.from(arrayBuffer)
|
|
@@ -32,31 +111,107 @@ export const commit = async (obj, opts) => {
|
|
|
32
111
|
bodyContent = msg.body
|
|
33
112
|
}
|
|
34
113
|
|
|
35
|
-
//
|
|
36
|
-
if (inlineBodyKey
|
|
37
|
-
body
|
|
114
|
+
// Put body content under the original field name (e.g., "data", "json")
|
|
115
|
+
if (inlineBodyKey) {
|
|
116
|
+
body[inlineBodyKey] = bodyContent
|
|
38
117
|
} else {
|
|
39
118
|
body.body = bodyContent
|
|
40
119
|
}
|
|
41
120
|
}
|
|
42
121
|
|
|
43
|
-
//
|
|
44
|
-
|
|
122
|
+
// Include non-committed fields from the original object as JSON values.
|
|
123
|
+
// This covers: (1) body-key fields (arrays/objects encoded as multipart,
|
|
124
|
+
// which can't be included as raw multipart in JSON), and (2) any other
|
|
125
|
+
// fields excluded from signing. These fields are unsigned but present
|
|
126
|
+
// so HyperBEAM can parse them directly as JSON types.
|
|
127
|
+
// Use case-insensitive matching: signing normalizes keys to lowercase,
|
|
128
|
+
// but the original object may use mixed case (e.g., "To" vs "to").
|
|
129
|
+
const committedSetLower = new Set(components.map(v => (v === "@path" ? "path" : v).toLowerCase()))
|
|
130
|
+
// Also track lowercase keys already in body to prevent duplicates
|
|
131
|
+
const bodyKeysLower = new Set(Object.keys(body).map(k => k.toLowerCase()))
|
|
132
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
133
|
+
// Skip HTTP method (not a data field)
|
|
134
|
+
if (key === "method") continue
|
|
135
|
+
// Skip if already in committed set (was signed)
|
|
136
|
+
if (committedSetLower.has(key.toLowerCase())) continue
|
|
137
|
+
// Skip if already in body (duplicate prevention)
|
|
138
|
+
if (bodyKeysLower.has(key.toLowerCase())) continue
|
|
139
|
+
if (value === undefined) continue
|
|
140
|
+
body[key] = value
|
|
141
|
+
bodyKeysLower.add(key.toLowerCase())
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// Include ao-types so HyperBEAM knows how to convert string values to proper types.
|
|
145
|
+
// First check if it was set by the signer, otherwise compute it from the original object
|
|
146
|
+
let aoTypes = msg.headers["ao-types"]
|
|
147
|
+
if (!aoTypes) {
|
|
148
|
+
// Compute ao-types from the original typed values in obj
|
|
149
|
+
aoTypes = buildAoTypes(obj)
|
|
150
|
+
}
|
|
151
|
+
if (aoTypes) {
|
|
152
|
+
body["ao-types"] = aoTypes
|
|
153
|
+
}
|
|
45
154
|
|
|
46
|
-
const hmacId = hmacid(msg.headers)
|
|
47
155
|
const rsaId = rsaid(msg.headers)
|
|
48
156
|
const pub = extractPubKey(msg.headers)
|
|
49
|
-
const
|
|
50
|
-
const
|
|
51
|
-
|
|
157
|
+
const pubKeyBase64 = pub.toString("base64")
|
|
158
|
+
const committer = toAddr(pubKeyBase64)
|
|
159
|
+
|
|
160
|
+
// Extract keyid from signature-input to ensure it matches what was signed
|
|
161
|
+
// Format: sig-xxx=("field1"...);alg="...";keyid="..."
|
|
162
|
+
// The keyid may already have a scheme prefix (e.g., "publickey:base64data")
|
|
163
|
+
const extractKeyidFromSigInput = (sigInput) => {
|
|
164
|
+
if (!sigInput) return null
|
|
165
|
+
const match = sigInput.match(/keyid="([^"]+)"/)
|
|
166
|
+
if (!match) return null
|
|
167
|
+
// Return as-is - the keyid already includes the prefix from signing
|
|
168
|
+
return match[1]
|
|
169
|
+
}
|
|
170
|
+
const keyid = extractKeyidFromSigInput(msg.headers["signature-input"]) || `publickey:${pubKeyBase64}`
|
|
171
|
+
|
|
172
|
+
// Build the list of committed fields.
|
|
173
|
+
// Always transform HTTPSig transport keys to AO-Core keys:
|
|
174
|
+
// - Replace content-digest with the body field name (HyperBEAM re-derives content-digest)
|
|
175
|
+
// - For native body keys ("body", "data"): also remove ao-body-key (HyperBEAM re-derives it)
|
|
176
|
+
// - For custom body keys (e.g., "json"): keep ao-body-key (HyperBEAM needs it to find the field)
|
|
177
|
+
let committedFields = components.map(v => v === "@path" ? "path" : v)
|
|
178
|
+
if (committedFields.includes("content-digest") && (inlineBodyKey || msg.body)) {
|
|
179
|
+
const bodyFieldName = inlineBodyKey || "body"
|
|
180
|
+
committedFields = committedFields.filter(k => k !== "content-digest")
|
|
181
|
+
if (!committedFields.includes(bodyFieldName)) {
|
|
182
|
+
committedFields.push(bodyFieldName)
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
if (isNativeBodyKey) {
|
|
186
|
+
// For native body keys, ao-body-key is a transport artifact - remove it
|
|
187
|
+
committedFields = committedFields.filter(k => k !== "ao-body-key")
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// Extract just the base64 signature data from the header format "sig-xxx=:base64data:"
|
|
191
|
+
// HyperBEAM expects raw base64 without colons (uses b64fast:encode/decode)
|
|
192
|
+
const extractSignature = (sigHeader) => {
|
|
193
|
+
if (!sigHeader) return sigHeader
|
|
194
|
+
// Match the base64 data between colons after the label
|
|
195
|
+
const match = sigHeader.match(/=:([^:]+):/)
|
|
196
|
+
return match ? match[1] : sigHeader
|
|
197
|
+
}
|
|
198
|
+
const rawSignature = extractSignature(msg.headers.signature)
|
|
199
|
+
const meta = {
|
|
200
|
+
type: "rsa-pss-sha512",
|
|
201
|
+
alg: "rsa-pss-sha512",
|
|
202
|
+
"commitment-device": "httpsig@1.0",
|
|
203
|
+
keyid: keyid,
|
|
204
|
+
committed: committedFields
|
|
205
|
+
}
|
|
52
206
|
const sigs = {
|
|
53
|
-
signature:
|
|
207
|
+
signature: rawSignature,
|
|
54
208
|
"signature-input": msg.headers["signature-input"],
|
|
55
209
|
}
|
|
210
|
+
// Only include the RSA commitment - HMAC commitments are created by HyperBEAM
|
|
211
|
+
// when needed and require the server's HMAC key which we don't have
|
|
56
212
|
const committed = {
|
|
57
213
|
commitments: {
|
|
58
214
|
[rsaId]: { ...meta, committer, ...sigs },
|
|
59
|
-
[hmacId]: { ...meta2, ...sigs },
|
|
60
215
|
},
|
|
61
216
|
...body,
|
|
62
217
|
}
|