wao 0.27.4 → 0.28.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/cjs/collect-body-keys.js +42 -2
- package/cjs/encode.js +55 -46
- package/cjs/id.js +234 -0
- package/cjs/send.js +4 -0
- package/cjs/signer-utils.js +1 -0
- package/cjs/signer.js +59 -10
- package/cjs/utils.js +15 -1
- package/esm/collect-body-keys.js +37 -2
- package/esm/encode.js +47 -32
- package/esm/id.js +222 -0
- package/esm/send.js +4 -3
- package/esm/signer-utils.js +1 -1
- package/esm/signer.js +33 -8
- package/esm/utils.js +7 -0
- package/package.json +1 -1
package/cjs/collect-body-keys.js
CHANGED
|
@@ -60,6 +60,40 @@ var getValueByPath = function getValueByPath(obj, path) {
|
|
|
60
60
|
}
|
|
61
61
|
return value;
|
|
62
62
|
};
|
|
63
|
+
var canArrayBeInHeader = function canArrayBeInHeader(array) {
|
|
64
|
+
// Empty arrays can be in headers
|
|
65
|
+
if (array.length === 0) return true;
|
|
66
|
+
|
|
67
|
+
// Arrays with objects must go to body
|
|
68
|
+
if (array.some(function (item) {
|
|
69
|
+
return (0, _encodeUtils.isPojo)(item);
|
|
70
|
+
})) return false;
|
|
71
|
+
|
|
72
|
+
// Arrays with binary data must go to body
|
|
73
|
+
if (array.some(function (item) {
|
|
74
|
+
return (0, _encodeUtils.isBytes)(item) && item.length > 0;
|
|
75
|
+
})) return false;
|
|
76
|
+
|
|
77
|
+
// Arrays with non-ASCII strings must go to body
|
|
78
|
+
if (array.some(function (item) {
|
|
79
|
+
return typeof item === "string" && (0, _encodeUtils.hasNonAscii)(item);
|
|
80
|
+
})) return false;
|
|
81
|
+
|
|
82
|
+
// Arrays with nested arrays must go to body (to match original behavior)
|
|
83
|
+
if (array.some(function (item) {
|
|
84
|
+
return Array.isArray(item);
|
|
85
|
+
})) return false;
|
|
86
|
+
|
|
87
|
+
// Arrays with nested arrays that have objects must go to body
|
|
88
|
+
if (array.some(function (item) {
|
|
89
|
+
return Array.isArray(item) && item.some(function (subItem) {
|
|
90
|
+
return (0, _encodeUtils.isPojo)(subItem);
|
|
91
|
+
});
|
|
92
|
+
})) return false;
|
|
93
|
+
|
|
94
|
+
// Simple arrays of primitives can stay in headers
|
|
95
|
+
return true;
|
|
96
|
+
};
|
|
63
97
|
|
|
64
98
|
// Array analysis helper
|
|
65
99
|
var analyzeArray = function analyzeArray(array) {
|
|
@@ -140,7 +174,10 @@ var BodyKeyCollector = /*#__PURE__*/function () {
|
|
|
140
174
|
if (this.isSpecialDataBodyField(key, value, objKeys)) {
|
|
141
175
|
this.keys.push(key);
|
|
142
176
|
} else if (Array.isArray(value) && value.length > 0) {
|
|
143
|
-
|
|
177
|
+
// Check if array can stay in header
|
|
178
|
+
if (!canArrayBeInHeader(value)) {
|
|
179
|
+
this.processRootArray(key, value);
|
|
180
|
+
}
|
|
144
181
|
} else if ((0, _encodeUtils.isPojo)(value)) {
|
|
145
182
|
this.processRootNestedObject(key, value);
|
|
146
183
|
} else if (this.needsBodyKey(key, value)) {
|
|
@@ -329,7 +366,10 @@ var BodyKeyCollector = /*#__PURE__*/function () {
|
|
|
329
366
|
this.processArrayInTraversal(value, fullPath, nestedPaths, analysis);
|
|
330
367
|
if (analysis.hasNonObjects) {
|
|
331
368
|
hasSimpleFields = true;
|
|
332
|
-
|
|
369
|
+
// Only add to keys if array can't be in header
|
|
370
|
+
if (!canArrayBeInHeader(value)) {
|
|
371
|
+
this.keys.push(fullPath);
|
|
372
|
+
}
|
|
333
373
|
}
|
|
334
374
|
} else {
|
|
335
375
|
hasSimpleFields = true;
|
package/cjs/encode.js
CHANGED
|
@@ -365,32 +365,34 @@ function handleSingleBodyKeyOptimization(_x5, _x6, _x7, _x8) {
|
|
|
365
365
|
} // Step 11: Sort body keys
|
|
366
366
|
function _handleSingleBodyKeyOptimization() {
|
|
367
367
|
_handleSingleBodyKeyOptimization = _asyncToGenerator(/*#__PURE__*/_regeneratorRuntime().mark(function _callee5(obj, bodyKeys, headers, headerTypes) {
|
|
368
|
-
var singleKey, value,
|
|
368
|
+
var singleKey, value, contentToHash, bodyContent, bodyBuffer, encoder, encoded, contentDigest, base64;
|
|
369
369
|
return _regeneratorRuntime().wrap(function _callee5$(_context5) {
|
|
370
370
|
while (1) switch (_context5.prev = _context5.next) {
|
|
371
371
|
case 0:
|
|
372
372
|
if (!(bodyKeys.length === 1)) {
|
|
373
|
-
_context5.next =
|
|
373
|
+
_context5.next = 14;
|
|
374
374
|
break;
|
|
375
375
|
}
|
|
376
376
|
singleKey = bodyKeys[0];
|
|
377
|
-
value = (0, _encodeUtils.getValueByPath)(obj, singleKey);
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
key = _ref12[0],
|
|
381
|
-
val = _ref12[1];
|
|
382
|
-
if (key === singleKey) return true;
|
|
383
|
-
return (0, _encodeUtils.isEmpty)(val);
|
|
384
|
-
});
|
|
385
|
-
if (!(otherFieldsAreEmpty && (0, _encodeUtils.isBytes)(value) && value.length > 0)) {
|
|
386
|
-
_context5.next = 15;
|
|
377
|
+
value = (0, _encodeUtils.getValueByPath)(obj, singleKey); // Apply optimization for binary data OR strings with newlines
|
|
378
|
+
if (!((0, _encodeUtils.isBytes)(value) && value.length > 0 || typeof value === "string" && value.includes("\n"))) {
|
|
379
|
+
_context5.next = 14;
|
|
387
380
|
break;
|
|
388
381
|
}
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
382
|
+
bodyContent = value;
|
|
383
|
+
if ((0, _encodeUtils.isBytes)(value)) {
|
|
384
|
+
bodyBuffer = (0, _encodeUtils.toBuffer)(value);
|
|
385
|
+
contentToHash = bodyBuffer.buffer.slice(bodyBuffer.byteOffset, bodyBuffer.byteOffset + bodyBuffer.byteLength);
|
|
386
|
+
} else {
|
|
387
|
+
// For strings, encode to UTF-8 for hashing
|
|
388
|
+
encoder = new TextEncoder();
|
|
389
|
+
encoded = encoder.encode(value);
|
|
390
|
+
contentToHash = encoded.buffer;
|
|
391
|
+
bodyContent = value;
|
|
392
|
+
}
|
|
393
|
+
_context5.next = 8;
|
|
394
|
+
return (0, _encodeUtils.sha256)(contentToHash);
|
|
395
|
+
case 8:
|
|
394
396
|
contentDigest = _context5.sent;
|
|
395
397
|
base64 = _base64url["default"].toBase64(_base64url["default"].encode(contentDigest));
|
|
396
398
|
headers["content-digest"] = "sha-256=:".concat(base64, ":");
|
|
@@ -402,11 +404,11 @@ function _handleSingleBodyKeyOptimization() {
|
|
|
402
404
|
}
|
|
403
405
|
return _context5.abrupt("return", {
|
|
404
406
|
headers: headers,
|
|
405
|
-
body:
|
|
407
|
+
body: bodyContent
|
|
406
408
|
});
|
|
407
|
-
case
|
|
409
|
+
case 14:
|
|
408
410
|
return _context5.abrupt("return", null);
|
|
409
|
-
case
|
|
411
|
+
case 15:
|
|
410
412
|
case "end":
|
|
411
413
|
return _context5.stop();
|
|
412
414
|
}
|
|
@@ -782,13 +784,10 @@ function processObjectFields(value, bodyKey, sortedBodyKeys) {
|
|
|
782
784
|
function createObjectBodyPart(bodyKey, value, allTypes, fieldLines, binaryFields, headers, sortedBodyKeys) {
|
|
783
785
|
var isInline = bodyKey === "body" && headers["inline-body-key"] === "body";
|
|
784
786
|
var lines = [];
|
|
785
|
-
if (isInline) {
|
|
786
|
-
lines.push("content-disposition: inline");
|
|
787
|
-
} else {
|
|
788
|
-
lines.push("content-disposition: form-data;name=\"".concat(bodyKey, "\""));
|
|
789
|
-
}
|
|
790
787
|
if (isInline) {
|
|
791
788
|
var orderedLines = [];
|
|
789
|
+
|
|
790
|
+
// For inline mode: fields first, then headers
|
|
792
791
|
var _iterator3 = _createForOfIteratorHelper(fieldLines),
|
|
793
792
|
_step3;
|
|
794
793
|
try {
|
|
@@ -821,7 +820,9 @@ function createObjectBodyPart(bodyKey, value, allTypes, fieldLines, binaryFields
|
|
|
821
820
|
});
|
|
822
821
|
if (binaryFieldsForInline.length > 0) {
|
|
823
822
|
var parts = [];
|
|
823
|
+
// Join all text lines first
|
|
824
824
|
parts.push(Buffer.from(orderedLines.join("\r\n")));
|
|
825
|
+
// Then add binary fields
|
|
825
826
|
var _iterator4 = _createForOfIteratorHelper(binaryFieldsForInline),
|
|
826
827
|
_step4;
|
|
827
828
|
try {
|
|
@@ -841,17 +842,10 @@ function createObjectBodyPart(bodyKey, value, allTypes, fieldLines, binaryFields
|
|
|
841
842
|
var fullBody = Buffer.concat(parts);
|
|
842
843
|
return new Blob([fullBody]);
|
|
843
844
|
} else {
|
|
844
|
-
|
|
845
|
-
var hasOnlyTypes = allTypes.length > 0 && fieldLines.length === 0;
|
|
846
|
-
if (isLastBodyPart && hasOnlyTypes) {
|
|
847
|
-
return new Blob([orderedLines.join("\r\n")]);
|
|
848
|
-
} else if (fieldLines.length === 0) {
|
|
849
|
-
return new Blob([orderedLines.join("\r\n")]);
|
|
850
|
-
} else {
|
|
851
|
-
return new Blob([orderedLines.join("\r\n") + "\r\n"]);
|
|
852
|
-
}
|
|
845
|
+
return new Blob([orderedLines.join("\r\n") + "\r\n"]);
|
|
853
846
|
}
|
|
854
847
|
} else {
|
|
848
|
+
// Non-inline mode remains the same
|
|
855
849
|
var _orderedLines3 = [];
|
|
856
850
|
if (allTypes.length > 0) {
|
|
857
851
|
_orderedLines3.push("ao-types: ".concat(allTypes.sort().join(", ")));
|
|
@@ -1189,6 +1183,7 @@ function _encode() {
|
|
|
1189
1183
|
singleBodyKeyResult,
|
|
1190
1184
|
sortedBodyKeys,
|
|
1191
1185
|
hasSpecialDataBody,
|
|
1186
|
+
bodyValue,
|
|
1192
1187
|
bodyParts,
|
|
1193
1188
|
boundary,
|
|
1194
1189
|
body,
|
|
@@ -1284,12 +1279,17 @@ function _encode() {
|
|
|
1284
1279
|
case 41:
|
|
1285
1280
|
// Step 11: Sort body keys
|
|
1286
1281
|
sortedBodyKeys = sortBodyKeys(bodyKeys); // Step 12: Check for special data/body case
|
|
1287
|
-
hasSpecialDataBody = checkSpecialDataBodyCase(obj, sortedBodyKeys);
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1282
|
+
hasSpecialDataBody = checkSpecialDataBodyCase(obj, sortedBodyKeys); // Only add body-keys header if there are actual body keys
|
|
1283
|
+
if (sortedBodyKeys.length > 0) {
|
|
1284
|
+
headers["body-keys"] = sortedBodyKeys.map(function (k) {
|
|
1285
|
+
return "\"".concat(k, "\"");
|
|
1286
|
+
}).join(", ");
|
|
1287
|
+
}
|
|
1288
|
+
|
|
1289
|
+
// Special case: single body key named "body" containing an object
|
|
1290
|
+
if (!hasSpecialDataBody && sortedBodyKeys.length === 1 && sortedBodyKeys[0] === "body") {
|
|
1291
|
+
bodyValue = obj.body;
|
|
1292
|
+
if ((0, _encodeUtils.isPojo)(bodyValue)) {
|
|
1293
1293
|
headers["inline-body-key"] = "body";
|
|
1294
1294
|
}
|
|
1295
1295
|
}
|
|
@@ -1298,16 +1298,25 @@ function _encode() {
|
|
|
1298
1298
|
}
|
|
1299
1299
|
|
|
1300
1300
|
// Step 13: Build body parts for each body key
|
|
1301
|
-
bodyParts = buildBodyParts(obj, sortedBodyKeys, headers, hasSpecialDataBody); //
|
|
1302
|
-
|
|
1303
|
-
|
|
1301
|
+
bodyParts = buildBodyParts(obj, sortedBodyKeys, headers, hasSpecialDataBody); // If no body parts were created, return headers only
|
|
1302
|
+
if (!(bodyParts.length === 0)) {
|
|
1303
|
+
_context8.next = 49;
|
|
1304
|
+
break;
|
|
1305
|
+
}
|
|
1306
|
+
return _context8.abrupt("return", {
|
|
1307
|
+
headers: headers,
|
|
1308
|
+
body: undefined
|
|
1309
|
+
});
|
|
1304
1310
|
case 49:
|
|
1311
|
+
_context8.next = 51;
|
|
1312
|
+
return generateBoundary(bodyParts);
|
|
1313
|
+
case 51:
|
|
1305
1314
|
boundary = _context8.sent;
|
|
1306
1315
|
// Step 15: Assemble final multipart body
|
|
1307
1316
|
body = assembleMultipartBody(bodyParts, boundary); // Step 16: Calculate content digest
|
|
1308
|
-
_context8.next =
|
|
1317
|
+
_context8.next = 55;
|
|
1309
1318
|
return calculateContentDigest(body);
|
|
1310
|
-
case
|
|
1319
|
+
case 55:
|
|
1311
1320
|
_yield$calculateConte = _context8.sent;
|
|
1312
1321
|
contentDigest = _yield$calculateConte.digest;
|
|
1313
1322
|
byteLength = _yield$calculateConte.byteLength;
|
|
@@ -1319,7 +1328,7 @@ function _encode() {
|
|
|
1319
1328
|
headers: headers,
|
|
1320
1329
|
body: body
|
|
1321
1330
|
});
|
|
1322
|
-
case
|
|
1331
|
+
case 60:
|
|
1323
1332
|
case "end":
|
|
1324
1333
|
return _context8.stop();
|
|
1325
1334
|
}
|
package/cjs/id.js
ADDED
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
Object.defineProperty(exports, "__esModule", {
|
|
4
|
+
value: true
|
|
5
|
+
});
|
|
6
|
+
exports.extractCommitmentIds = extractCommitmentIds;
|
|
7
|
+
exports.generateCommitmentId = generateCommitmentId;
|
|
8
|
+
exports.generateHmacCommitmentId = generateHmacCommitmentId;
|
|
9
|
+
exports.generateRsaCommitmentId = generateRsaCommitmentId;
|
|
10
|
+
exports.parseStructuredFieldDictionary = parseStructuredFieldDictionary;
|
|
11
|
+
exports.verifyCommitmentId = verifyCommitmentId;
|
|
12
|
+
var _fastSha = require("fast-sha256");
|
|
13
|
+
function _toConsumableArray(r) { return _arrayWithoutHoles(r) || _iterableToArray(r) || _unsupportedIterableToArray(r) || _nonIterableSpread(); }
|
|
14
|
+
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."); }
|
|
15
|
+
function _iterableToArray(r) { if ("undefined" != typeof Symbol && null != r[Symbol.iterator] || null != r["@@iterator"]) return Array.from(r); }
|
|
16
|
+
function _arrayWithoutHoles(r) { if (Array.isArray(r)) return _arrayLikeToArray(r); }
|
|
17
|
+
function _slicedToArray(r, e) { return _arrayWithHoles(r) || _iterableToArrayLimit(r, e) || _unsupportedIterableToArray(r, e) || _nonIterableRest(); }
|
|
18
|
+
function _nonIterableRest() { throw new TypeError("Invalid attempt to destructure non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); }
|
|
19
|
+
function _unsupportedIterableToArray(r, a) { if (r) { if ("string" == typeof r) return _arrayLikeToArray(r, a); var t = {}.toString.call(r).slice(8, -1); return "Object" === t && r.constructor && (t = r.constructor.name), "Map" === t || "Set" === t ? Array.from(r) : "Arguments" === t || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(t) ? _arrayLikeToArray(r, a) : void 0; } }
|
|
20
|
+
function _arrayLikeToArray(r, a) { (null == a || a > r.length) && (a = r.length); for (var e = 0, n = Array(a); e < a; e++) n[e] = r[e]; return n; }
|
|
21
|
+
function _iterableToArrayLimit(r, l) { var t = null == r ? null : "undefined" != typeof Symbol && r[Symbol.iterator] || r["@@iterator"]; if (null != t) { var e, n, i, u, a = [], f = !0, o = !1; try { if (i = (t = t.call(r)).next, 0 === l) { if (Object(t) !== t) return; f = !1; } else for (; !(f = (e = i.call(t)).done) && (a.push(e.value), a.length !== l); f = !0); } catch (r) { o = !0, n = r; } finally { try { if (!f && null != t["return"] && (u = t["return"](), Object(u) !== u)) return; } finally { if (o) throw n; } } return a; } }
|
|
22
|
+
function _arrayWithHoles(r) { if (Array.isArray(r)) return r; }
|
|
23
|
+
/**
|
|
24
|
+
* Parse structured field dictionary format
|
|
25
|
+
* Handles both complex format: name=(components);params
|
|
26
|
+
* and simple format: name=:value:
|
|
27
|
+
*/
|
|
28
|
+
function parseStructuredFieldDictionary(input) {
|
|
29
|
+
// Try complex format first
|
|
30
|
+
var match = input.match(/([^=]+)=\((.*?)\);(.*)$/);
|
|
31
|
+
if (match) {
|
|
32
|
+
var name = match[1];
|
|
33
|
+
var components = match[2].split(" ");
|
|
34
|
+
var params = {};
|
|
35
|
+
var paramPairs = match[3].split(";").filter(function (p) {
|
|
36
|
+
return p;
|
|
37
|
+
});
|
|
38
|
+
paramPairs.forEach(function (pair) {
|
|
39
|
+
var _pair$split = pair.split("="),
|
|
40
|
+
_pair$split2 = _slicedToArray(_pair$split, 2),
|
|
41
|
+
key = _pair$split2[0],
|
|
42
|
+
value = _pair$split2[1];
|
|
43
|
+
if (key && value) {
|
|
44
|
+
params[key] = value.replace(/"/g, "");
|
|
45
|
+
}
|
|
46
|
+
});
|
|
47
|
+
return {
|
|
48
|
+
name: name,
|
|
49
|
+
components: components,
|
|
50
|
+
params: params
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Try simple format
|
|
55
|
+
var simpleMatch = input.match(/([^=]+)=:([^:]+):/);
|
|
56
|
+
if (simpleMatch) {
|
|
57
|
+
return {
|
|
58
|
+
name: simpleMatch[1],
|
|
59
|
+
value: simpleMatch[2]
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Convert base64url string to base64
|
|
67
|
+
*/
|
|
68
|
+
function base64urlToBase64(str) {
|
|
69
|
+
return str.replace(/-/g, "+").replace(/_/g, "/");
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Convert Uint8Array to base64url string
|
|
74
|
+
*/
|
|
75
|
+
function uint8ArrayToBase64url(bytes) {
|
|
76
|
+
// Convert to base64
|
|
77
|
+
var binary = "";
|
|
78
|
+
for (var i = 0; i < bytes.length; i++) {
|
|
79
|
+
binary += String.fromCharCode(bytes[i]);
|
|
80
|
+
}
|
|
81
|
+
var base64 = btoa(binary);
|
|
82
|
+
|
|
83
|
+
// Convert to base64url
|
|
84
|
+
return base64.replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, "");
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Generate commitment ID for RSA-PSS and ECDSA signatures
|
|
89
|
+
* The ID is the SHA256 hash of the raw signature bytes
|
|
90
|
+
*
|
|
91
|
+
* @param {Object} commitment - The commitment object containing signature
|
|
92
|
+
* @returns {string} The commitment ID in base64url format
|
|
93
|
+
*/
|
|
94
|
+
function generateRsaCommitmentId(commitment) {
|
|
95
|
+
// Extract the base64 signature from structured field format
|
|
96
|
+
// Format: "signature-name=:BASE64_SIGNATURE:"
|
|
97
|
+
var match = commitment.signature.match(/^[^=]+=:([^:]+):/);
|
|
98
|
+
if (!match) {
|
|
99
|
+
throw new Error("Invalid signature format");
|
|
100
|
+
}
|
|
101
|
+
var signatureBase64 = match[1];
|
|
102
|
+
// Convert base64 to Uint8Array
|
|
103
|
+
var signatureBinary = Uint8Array.from(atob(signatureBase64), function (c) {
|
|
104
|
+
return c.charCodeAt(0);
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
// SHA256 hash of the raw signature
|
|
108
|
+
var hashResult = (0, _fastSha.hash)(signatureBinary);
|
|
109
|
+
var id = uint8ArrayToBase64url(hashResult);
|
|
110
|
+
return id;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Generate HMAC commitment ID for HyperBEAM messages
|
|
115
|
+
* The ID is deterministic based on message content only
|
|
116
|
+
*
|
|
117
|
+
* The Erlang implementation sorts components WITH @ prefix included,
|
|
118
|
+
* then removes @ from derived components in the signature base.
|
|
119
|
+
*
|
|
120
|
+
* @param {Object} message - The message with signature and signature-input
|
|
121
|
+
* @returns {string} The commitment ID in base64url format
|
|
122
|
+
*/
|
|
123
|
+
function generateHmacCommitmentId(message) {
|
|
124
|
+
// Parse signature-input to get components
|
|
125
|
+
var parsed = parseStructuredFieldDictionary(message["signature-input"]);
|
|
126
|
+
if (!parsed || !parsed.components) {
|
|
127
|
+
throw new Error("Failed to parse signature-input");
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Sort components AS-IS (with quotes and @ prefix)
|
|
131
|
+
var sortedComponents = _toConsumableArray(parsed.components).sort();
|
|
132
|
+
|
|
133
|
+
// Build signature base in sorted order
|
|
134
|
+
var lines = [];
|
|
135
|
+
sortedComponents.forEach(function (component) {
|
|
136
|
+
var cleanComponent = component.replace(/"/g, "");
|
|
137
|
+
var fieldName = cleanComponent;
|
|
138
|
+
var value;
|
|
139
|
+
|
|
140
|
+
// For derived components (starting with @), remove @ in the signature base
|
|
141
|
+
if (cleanComponent.startsWith("@")) {
|
|
142
|
+
fieldName = cleanComponent.substring(1);
|
|
143
|
+
value = message[fieldName];
|
|
144
|
+
} else {
|
|
145
|
+
value = message[cleanComponent];
|
|
146
|
+
}
|
|
147
|
+
if (value === undefined || value === null) {
|
|
148
|
+
value = "";
|
|
149
|
+
} else if (typeof value === "number") {
|
|
150
|
+
value = value.toString();
|
|
151
|
+
}
|
|
152
|
+
lines.push("\"".concat(fieldName, "\": ").concat(value));
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
// Add signature-params line with sorted components (keeping @ prefix)
|
|
156
|
+
var paramsComponents = sortedComponents.join(" ");
|
|
157
|
+
lines.push("\"@signature-params\": (".concat(paramsComponents, ");alg=\"hmac-sha256\";keyid=\"ao\""));
|
|
158
|
+
var signatureBase = lines.join("\n");
|
|
159
|
+
|
|
160
|
+
// Generate HMAC with key "ao"
|
|
161
|
+
// Convert string to Uint8Array
|
|
162
|
+
var messageBytes = new TextEncoder().encode(signatureBase);
|
|
163
|
+
var keyBytes = new TextEncoder().encode("ao");
|
|
164
|
+
var hmacResult = (0, _fastSha.hmac)(keyBytes, messageBytes);
|
|
165
|
+
return uint8ArrayToBase64url(hmacResult);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Generate commitment ID based on the algorithm type
|
|
170
|
+
*
|
|
171
|
+
* @param {Object} commitment - The commitment object containing alg, signature, etc.
|
|
172
|
+
* @param {Object} fullMessage - The full message (required for HMAC)
|
|
173
|
+
* @returns {string} The commitment ID in base64url format
|
|
174
|
+
*/
|
|
175
|
+
function generateCommitmentId(commitment) {
|
|
176
|
+
var fullMessage = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : null;
|
|
177
|
+
switch (commitment.alg) {
|
|
178
|
+
case "rsa-pss-sha512":
|
|
179
|
+
case "ecdsa-p256-sha256":
|
|
180
|
+
return generateRsaCommitmentId(commitment);
|
|
181
|
+
case "hmac-sha256":
|
|
182
|
+
if (!fullMessage) {
|
|
183
|
+
throw new Error("HMAC commitment IDs require full message context");
|
|
184
|
+
}
|
|
185
|
+
return generateHmacCommitmentId(fullMessage);
|
|
186
|
+
default:
|
|
187
|
+
throw new Error("Unsupported algorithm: ".concat(commitment.alg));
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Extract all commitment IDs from a HyperBEAM message
|
|
193
|
+
*
|
|
194
|
+
* @param {Object} message - The message with commitments
|
|
195
|
+
* @returns {Object} Map of commitment IDs to their types
|
|
196
|
+
*/
|
|
197
|
+
function extractCommitmentIds(message) {
|
|
198
|
+
var ids = {};
|
|
199
|
+
if (!message.commitments) {
|
|
200
|
+
return ids;
|
|
201
|
+
}
|
|
202
|
+
for (var _i = 0, _Object$entries = Object.entries(message.commitments); _i < _Object$entries.length; _i++) {
|
|
203
|
+
var _Object$entries$_i = _slicedToArray(_Object$entries[_i], 2),
|
|
204
|
+
id = _Object$entries$_i[0],
|
|
205
|
+
commitment = _Object$entries$_i[1];
|
|
206
|
+
ids[id] = {
|
|
207
|
+
alg: commitment.alg,
|
|
208
|
+
committer: commitment.committer,
|
|
209
|
+
device: commitment["commitment-device"]
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
return ids;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* Verify a commitment ID matches the expected value
|
|
217
|
+
*
|
|
218
|
+
* @param {Object} commitment - The commitment object
|
|
219
|
+
* @param {string} expectedId - The expected commitment ID
|
|
220
|
+
* @param {Object} fullMessage - The full message (required for HMAC)
|
|
221
|
+
* @returns {boolean} True if the ID matches
|
|
222
|
+
*/
|
|
223
|
+
function verifyCommitmentId(commitment, expectedId) {
|
|
224
|
+
var fullMessage = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : null;
|
|
225
|
+
try {
|
|
226
|
+
var calculatedId = generateCommitmentId(commitment, fullMessage);
|
|
227
|
+
return calculatedId === expectedId;
|
|
228
|
+
} catch (error) {
|
|
229
|
+
console.error("Error verifying commitment ID:", error);
|
|
230
|
+
return false;
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// Export all functions
|
package/cjs/send.js
CHANGED
|
@@ -46,6 +46,8 @@ function _send() {
|
|
|
46
46
|
while (1) switch (_context2.prev = _context2.next) {
|
|
47
47
|
case 0:
|
|
48
48
|
fetchImpl = _args2.length > 1 && _args2[1] !== undefined ? _args2[1] : fetch;
|
|
49
|
+
// IMPORTANT: Use the URL from signedMsg.url, NOT from any path header
|
|
50
|
+
// This ensures we send to the correct URL even if path header is different
|
|
49
51
|
fetchOptions = {
|
|
50
52
|
method: signedMsg.method,
|
|
51
53
|
headers: signedMsg.headers,
|
|
@@ -54,6 +56,8 @@ function _send() {
|
|
|
54
56
|
if (signedMsg.body !== undefined && signedMsg.method !== "GET" && signedMsg.method !== "HEAD") {
|
|
55
57
|
fetchOptions.body = signedMsg.body;
|
|
56
58
|
}
|
|
59
|
+
|
|
60
|
+
// Use the URL as provided, ignoring any path header
|
|
57
61
|
_context2.next = 5;
|
|
58
62
|
return fetchImpl(signedMsg.url, fetchOptions);
|
|
59
63
|
case 5:
|
package/cjs/signer-utils.js
CHANGED
|
@@ -4,6 +4,7 @@ function _typeof(o) { "@babel/helpers - typeof"; return _typeof = "function" ==
|
|
|
4
4
|
Object.defineProperty(exports, "__esModule", {
|
|
5
5
|
value: true
|
|
6
6
|
});
|
|
7
|
+
exports.extractPublicKeyFromHeaders = extractPublicKeyFromHeaders;
|
|
7
8
|
exports.send = send;
|
|
8
9
|
exports.toHttpSigner = void 0;
|
|
9
10
|
exports.verify = verify;
|
package/cjs/signer.js
CHANGED
|
@@ -30,7 +30,9 @@ var joinUrl = function joinUrl(_ref) {
|
|
|
30
30
|
if (path.startsWith("http://") || path.startsWith("https://")) {
|
|
31
31
|
return path;
|
|
32
32
|
}
|
|
33
|
-
|
|
33
|
+
// Ensure path starts with /
|
|
34
|
+
var normalizedPath = path.startsWith("/") ? path : "/" + path;
|
|
35
|
+
return url.endsWith("/") ? url.slice(0, -1) + normalizedPath : url + normalizedPath;
|
|
34
36
|
};
|
|
35
37
|
function signer(config) {
|
|
36
38
|
var signer = config.signer,
|
|
@@ -41,15 +43,46 @@ function signer(config) {
|
|
|
41
43
|
}
|
|
42
44
|
return /*#__PURE__*/function () {
|
|
43
45
|
var _sign = _asyncToGenerator(/*#__PURE__*/_regeneratorRuntime().mark(function _callee(fields) {
|
|
44
|
-
var
|
|
46
|
+
var _ref2,
|
|
47
|
+
_ref2$path,
|
|
48
|
+
_path,
|
|
49
|
+
_fields$path,
|
|
50
|
+
path,
|
|
51
|
+
_fields$method,
|
|
52
|
+
method,
|
|
53
|
+
restFields,
|
|
54
|
+
aoFields,
|
|
55
|
+
encoded,
|
|
56
|
+
headersObj,
|
|
57
|
+
body,
|
|
58
|
+
_url,
|
|
59
|
+
bodySize,
|
|
60
|
+
lowercaseHeaders,
|
|
61
|
+
_i,
|
|
62
|
+
_Object$entries,
|
|
63
|
+
_Object$entries$_i,
|
|
64
|
+
key,
|
|
65
|
+
value,
|
|
66
|
+
bodyKeys,
|
|
67
|
+
signingFields,
|
|
68
|
+
signedRequest,
|
|
69
|
+
finalHeaders,
|
|
70
|
+
_i2,
|
|
71
|
+
_Object$entries2,
|
|
72
|
+
_Object$entries2$_i,
|
|
73
|
+
_key,
|
|
74
|
+
_value,
|
|
75
|
+
result,
|
|
76
|
+
_args = arguments;
|
|
45
77
|
return _regeneratorRuntime().wrap(function _callee$(_context) {
|
|
46
78
|
while (1) switch (_context.prev = _context.next) {
|
|
47
79
|
case 0:
|
|
80
|
+
_ref2 = _args.length > 1 && _args[1] !== undefined ? _args[1] : {}, _ref2$path = _ref2.path, _path = _ref2$path === void 0 ? false : _ref2$path;
|
|
48
81
|
_fields$path = fields.path, path = _fields$path === void 0 ? "/relay/process" : _fields$path, _fields$method = fields.method, method = _fields$method === void 0 ? "POST" : _fields$method, restFields = _objectWithoutProperties(fields, _excluded);
|
|
49
82
|
aoFields = _objectSpread({}, restFields);
|
|
50
|
-
_context.next =
|
|
83
|
+
_context.next = 5;
|
|
51
84
|
return (0, _encode.enc)(aoFields);
|
|
52
|
-
case
|
|
85
|
+
case 5:
|
|
53
86
|
encoded = _context.sent;
|
|
54
87
|
headersObj = encoded ? encoded.headers : {};
|
|
55
88
|
body = encoded ? encoded.body : undefined;
|
|
@@ -57,6 +90,7 @@ function signer(config) {
|
|
|
57
90
|
url: url,
|
|
58
91
|
path: path
|
|
59
92
|
});
|
|
93
|
+
headersObj["path"] = path;
|
|
60
94
|
if (body && !headersObj["content-length"]) {
|
|
61
95
|
bodySize = body.size || body.byteLength || 0;
|
|
62
96
|
if (bodySize > 0) {
|
|
@@ -68,14 +102,27 @@ function signer(config) {
|
|
|
68
102
|
_Object$entries$_i = _slicedToArray(_Object$entries[_i], 2), key = _Object$entries$_i[0], value = _Object$entries$_i[1];
|
|
69
103
|
lowercaseHeaders[key.toLowerCase()] = value;
|
|
70
104
|
}
|
|
105
|
+
if (lowercaseHeaders.path && /^\//.test(lowercaseHeaders.path)) {
|
|
106
|
+
//const sp = lowercaseHeaders.path.split("/")
|
|
107
|
+
//lowercaseHeaders.path = sp.slice(sp.length - 1).join("/")
|
|
108
|
+
}
|
|
109
|
+
// Parse body-keys if present
|
|
110
|
+
bodyKeys = headersObj["body-keys"] ? headersObj["body-keys"].replace(/"/g, "").split(",").map(function (k) {
|
|
111
|
+
return k.trim();
|
|
112
|
+
}) : []; // Collect fields to sign from headers
|
|
71
113
|
signingFields = Object.keys(lowercaseHeaders).filter(function (key) {
|
|
72
|
-
return key !== "body-keys";
|
|
73
|
-
}
|
|
114
|
+
return key !== "body-keys" && key !== "path" && !bodyKeys.includes(key);
|
|
115
|
+
} // Also exclude fields that are in body-keys
|
|
116
|
+
); // Always include @path in the signature
|
|
117
|
+
//if (!signingFields.includes("path")) signingFields.push("path")
|
|
118
|
+
|
|
119
|
+
if (_path) signingFields.push("path");
|
|
120
|
+
// Ensure we have at least one field to sign
|
|
74
121
|
if (signingFields.length === 0 && !body) {
|
|
75
122
|
lowercaseHeaders["content-length"] = "0";
|
|
76
123
|
signingFields.push("content-length");
|
|
77
124
|
}
|
|
78
|
-
_context.next =
|
|
125
|
+
_context.next = 20;
|
|
79
126
|
return (0, _send.toHttpSigner)(signer)({
|
|
80
127
|
request: {
|
|
81
128
|
url: _url,
|
|
@@ -84,7 +131,7 @@ function signer(config) {
|
|
|
84
131
|
},
|
|
85
132
|
fields: signingFields
|
|
86
133
|
});
|
|
87
|
-
case
|
|
134
|
+
case 20:
|
|
88
135
|
signedRequest = _context.sent;
|
|
89
136
|
finalHeaders = {};
|
|
90
137
|
for (_i2 = 0, _Object$entries2 = Object.entries(headersObj); _i2 < _Object$entries2.length; _i2++) {
|
|
@@ -103,7 +150,7 @@ function signer(config) {
|
|
|
103
150
|
};
|
|
104
151
|
if (body) result.body = body;
|
|
105
152
|
return _context.abrupt("return", result);
|
|
106
|
-
case
|
|
153
|
+
case 29:
|
|
107
154
|
case "end":
|
|
108
155
|
return _context.stop();
|
|
109
156
|
}
|
|
@@ -114,4 +161,6 @@ function signer(config) {
|
|
|
114
161
|
}
|
|
115
162
|
return sign;
|
|
116
163
|
}();
|
|
117
|
-
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// stack / simple-pay / patch / hyperbeam / hyperbeam-aos / scheduler / cron
|
package/cjs/utils.js
CHANGED
|
@@ -3,16 +3,30 @@
|
|
|
3
3
|
Object.defineProperty(exports, "__esModule", {
|
|
4
4
|
value: true
|
|
5
5
|
});
|
|
6
|
-
exports.
|
|
6
|
+
exports.getTagVal = exports.getTag = exports.dirname = exports.checkTag = exports.buildTags = exports.allChecked = exports.action = void 0;
|
|
7
|
+
Object.defineProperty(exports, "hmacid", {
|
|
8
|
+
enumerable: true,
|
|
9
|
+
get: function get() {
|
|
10
|
+
return _id.generateHmacCommitmentId;
|
|
11
|
+
}
|
|
12
|
+
});
|
|
13
|
+
exports.isData = exports.isCheckComplete = void 0;
|
|
7
14
|
exports.isJSON = isJSON;
|
|
8
15
|
exports.optServer = exports.optAO = exports.mergeOut = exports.mergeChecks = exports.ltags = exports.jsonToStr = exports.isRegExp = exports.isOutComplete = exports.isLocalhost = void 0;
|
|
9
16
|
exports.parseSignatureInput = parseSignatureInput;
|
|
17
|
+
Object.defineProperty(exports, "rsaid", {
|
|
18
|
+
enumerable: true,
|
|
19
|
+
get: function get() {
|
|
20
|
+
return _id.generateRsaCommitmentId;
|
|
21
|
+
}
|
|
22
|
+
});
|
|
10
23
|
exports.tags = exports.tagEq = exports.tag = exports.srcs = exports.searchTag = void 0;
|
|
11
24
|
exports.toANS104Request = toANS104Request;
|
|
12
25
|
exports.toAddr = toAddr;
|
|
13
26
|
exports.wait = exports.validAddress = exports.udl = exports.toGraphObj = void 0;
|
|
14
27
|
var _graphql = require("graphql");
|
|
15
28
|
var _fastSha = _interopRequireDefault(require("fast-sha256"));
|
|
29
|
+
var _id = require("./id.js");
|
|
16
30
|
var _ramda = require("ramda");
|
|
17
31
|
function _interopRequireDefault(e) { return e && e.__esModule ? e : { "default": e }; }
|
|
18
32
|
function _slicedToArray(r, e) { return _arrayWithHoles(r) || _iterableToArrayLimit(r, e) || _unsupportedIterableToArray(r, e) || _nonIterableRest(); }
|
package/esm/collect-body-keys.js
CHANGED
|
@@ -49,6 +49,35 @@ const getValueByPath = (obj, path) => {
|
|
|
49
49
|
return value
|
|
50
50
|
}
|
|
51
51
|
|
|
52
|
+
const canArrayBeInHeader = array => {
|
|
53
|
+
// Empty arrays can be in headers
|
|
54
|
+
if (array.length === 0) return true
|
|
55
|
+
|
|
56
|
+
// Arrays with objects must go to body
|
|
57
|
+
if (array.some(item => isPojo(item))) return false
|
|
58
|
+
|
|
59
|
+
// Arrays with binary data must go to body
|
|
60
|
+
if (array.some(item => isBytes(item) && item.length > 0)) return false
|
|
61
|
+
|
|
62
|
+
// Arrays with non-ASCII strings must go to body
|
|
63
|
+
if (array.some(item => typeof item === "string" && hasNonAscii(item)))
|
|
64
|
+
return false
|
|
65
|
+
|
|
66
|
+
// Arrays with nested arrays must go to body (to match original behavior)
|
|
67
|
+
if (array.some(item => Array.isArray(item))) return false
|
|
68
|
+
|
|
69
|
+
// Arrays with nested arrays that have objects must go to body
|
|
70
|
+
if (
|
|
71
|
+
array.some(
|
|
72
|
+
item => Array.isArray(item) && item.some(subItem => isPojo(subItem))
|
|
73
|
+
)
|
|
74
|
+
)
|
|
75
|
+
return false
|
|
76
|
+
|
|
77
|
+
// Simple arrays of primitives can stay in headers
|
|
78
|
+
return true
|
|
79
|
+
}
|
|
80
|
+
|
|
52
81
|
// Array analysis helper
|
|
53
82
|
const analyzeArray = array => {
|
|
54
83
|
const analysis = {
|
|
@@ -115,7 +144,10 @@ class BodyKeyCollector {
|
|
|
115
144
|
if (this.isSpecialDataBodyField(key, value, objKeys)) {
|
|
116
145
|
this.keys.push(key)
|
|
117
146
|
} else if (Array.isArray(value) && value.length > 0) {
|
|
118
|
-
|
|
147
|
+
// Check if array can stay in header
|
|
148
|
+
if (!canArrayBeInHeader(value)) {
|
|
149
|
+
this.processRootArray(key, value)
|
|
150
|
+
}
|
|
119
151
|
} else if (isPojo(value)) {
|
|
120
152
|
this.processRootNestedObject(key, value)
|
|
121
153
|
} else if (this.needsBodyKey(key, value)) {
|
|
@@ -302,7 +334,10 @@ class BodyKeyCollector {
|
|
|
302
334
|
|
|
303
335
|
if (analysis.hasNonObjects) {
|
|
304
336
|
hasSimpleFields = true
|
|
305
|
-
|
|
337
|
+
// Only add to keys if array can't be in header
|
|
338
|
+
if (!canArrayBeInHeader(value)) {
|
|
339
|
+
this.keys.push(fullPath)
|
|
340
|
+
}
|
|
306
341
|
}
|
|
307
342
|
} else {
|
|
308
343
|
hasSimpleFields = true
|
package/esm/encode.js
CHANGED
|
@@ -293,19 +293,29 @@ async function handleSingleBodyKeyOptimization(
|
|
|
293
293
|
const singleKey = bodyKeys[0]
|
|
294
294
|
const value = getValueByPath(obj, singleKey)
|
|
295
295
|
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
bodyBuffer
|
|
306
|
-
|
|
296
|
+
// Apply optimization for binary data OR strings with newlines
|
|
297
|
+
if (
|
|
298
|
+
(isBytes(value) && value.length > 0) ||
|
|
299
|
+
(typeof value === "string" && value.includes("\n"))
|
|
300
|
+
) {
|
|
301
|
+
let contentToHash
|
|
302
|
+
let bodyContent = value
|
|
303
|
+
|
|
304
|
+
if (isBytes(value)) {
|
|
305
|
+
const bodyBuffer = toBuffer(value)
|
|
306
|
+
contentToHash = bodyBuffer.buffer.slice(
|
|
307
|
+
bodyBuffer.byteOffset,
|
|
308
|
+
bodyBuffer.byteOffset + bodyBuffer.byteLength
|
|
309
|
+
)
|
|
310
|
+
} else {
|
|
311
|
+
// For strings, encode to UTF-8 for hashing
|
|
312
|
+
const encoder = new TextEncoder()
|
|
313
|
+
const encoded = encoder.encode(value)
|
|
314
|
+
contentToHash = encoded.buffer
|
|
315
|
+
bodyContent = value
|
|
316
|
+
}
|
|
307
317
|
|
|
308
|
-
const contentDigest = await sha256(
|
|
318
|
+
const contentDigest = await sha256(contentToHash)
|
|
309
319
|
const base64 = base64url.toBase64(base64url.encode(contentDigest))
|
|
310
320
|
headers["content-digest"] = `sha-256=:${base64}:`
|
|
311
321
|
|
|
@@ -317,7 +327,7 @@ async function handleSingleBodyKeyOptimization(
|
|
|
317
327
|
headers["ao-types"] = headerTypes.sort().join(", ")
|
|
318
328
|
}
|
|
319
329
|
|
|
320
|
-
return { headers, body:
|
|
330
|
+
return { headers, body: bodyContent }
|
|
321
331
|
}
|
|
322
332
|
}
|
|
323
333
|
|
|
@@ -736,17 +746,14 @@ function createObjectBodyPart(
|
|
|
736
746
|
const isInline = bodyKey === "body" && headers["inline-body-key"] === "body"
|
|
737
747
|
const lines = []
|
|
738
748
|
|
|
739
|
-
if (isInline) {
|
|
740
|
-
lines.push(`content-disposition: inline`)
|
|
741
|
-
} else {
|
|
742
|
-
lines.push(`content-disposition: form-data;name="${bodyKey}"`)
|
|
743
|
-
}
|
|
744
|
-
|
|
745
749
|
if (isInline) {
|
|
746
750
|
const orderedLines = []
|
|
751
|
+
|
|
752
|
+
// For inline mode: fields first, then headers
|
|
747
753
|
for (const line of fieldLines) {
|
|
748
754
|
orderedLines.push(line)
|
|
749
755
|
}
|
|
756
|
+
|
|
750
757
|
if (allTypes.length > 0) {
|
|
751
758
|
orderedLines.push(`ao-types: ${allTypes.sort().join(", ")}`)
|
|
752
759
|
}
|
|
@@ -763,7 +770,9 @@ function createObjectBodyPart(
|
|
|
763
770
|
|
|
764
771
|
if (binaryFieldsForInline.length > 0) {
|
|
765
772
|
const parts = []
|
|
773
|
+
// Join all text lines first
|
|
766
774
|
parts.push(Buffer.from(orderedLines.join("\r\n")))
|
|
775
|
+
// Then add binary fields
|
|
767
776
|
for (const { key, buffer } of binaryFieldsForInline) {
|
|
768
777
|
parts.push(Buffer.from(`\r\n${key}: `))
|
|
769
778
|
parts.push(buffer)
|
|
@@ -772,18 +781,10 @@ function createObjectBodyPart(
|
|
|
772
781
|
const fullBody = Buffer.concat(parts)
|
|
773
782
|
return new Blob([fullBody])
|
|
774
783
|
} else {
|
|
775
|
-
|
|
776
|
-
sortedBodyKeys.indexOf(bodyKey) === sortedBodyKeys.length - 1
|
|
777
|
-
const hasOnlyTypes = allTypes.length > 0 && fieldLines.length === 0
|
|
778
|
-
if (isLastBodyPart && hasOnlyTypes) {
|
|
779
|
-
return new Blob([orderedLines.join("\r\n")])
|
|
780
|
-
} else if (fieldLines.length === 0) {
|
|
781
|
-
return new Blob([orderedLines.join("\r\n")])
|
|
782
|
-
} else {
|
|
783
|
-
return new Blob([orderedLines.join("\r\n") + "\r\n"])
|
|
784
|
-
}
|
|
784
|
+
return new Blob([orderedLines.join("\r\n") + "\r\n"])
|
|
785
785
|
}
|
|
786
786
|
} else {
|
|
787
|
+
// Non-inline mode remains the same
|
|
787
788
|
const orderedLines = []
|
|
788
789
|
if (allTypes.length > 0) {
|
|
789
790
|
orderedLines.push(`ao-types: ${allTypes.sort().join(", ")}`)
|
|
@@ -1162,10 +1163,19 @@ async function encode(obj = {}) {
|
|
|
1162
1163
|
// Step 12: Check for special data/body case
|
|
1163
1164
|
const hasSpecialDataBody = checkSpecialDataBodyCase(obj, sortedBodyKeys)
|
|
1164
1165
|
|
|
1165
|
-
|
|
1166
|
+
// Only add body-keys header if there are actual body keys
|
|
1167
|
+
if (sortedBodyKeys.length > 0) {
|
|
1168
|
+
headers["body-keys"] = sortedBodyKeys.map(k => `"${k}"`).join(", ")
|
|
1169
|
+
}
|
|
1166
1170
|
|
|
1167
|
-
|
|
1168
|
-
|
|
1171
|
+
// Special case: single body key named "body" containing an object
|
|
1172
|
+
if (
|
|
1173
|
+
!hasSpecialDataBody &&
|
|
1174
|
+
sortedBodyKeys.length === 1 &&
|
|
1175
|
+
sortedBodyKeys[0] === "body"
|
|
1176
|
+
) {
|
|
1177
|
+
const bodyValue = obj.body
|
|
1178
|
+
if (isPojo(bodyValue)) {
|
|
1169
1179
|
headers["inline-body-key"] = "body"
|
|
1170
1180
|
}
|
|
1171
1181
|
}
|
|
@@ -1182,6 +1192,11 @@ async function encode(obj = {}) {
|
|
|
1182
1192
|
hasSpecialDataBody
|
|
1183
1193
|
)
|
|
1184
1194
|
|
|
1195
|
+
// If no body parts were created, return headers only
|
|
1196
|
+
if (bodyParts.length === 0) {
|
|
1197
|
+
return { headers, body: undefined }
|
|
1198
|
+
}
|
|
1199
|
+
|
|
1185
1200
|
// Step 14: Generate multipart boundary
|
|
1186
1201
|
const boundary = await generateBoundary(bodyParts)
|
|
1187
1202
|
|
package/esm/id.js
ADDED
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
import { hash, hmac } from "fast-sha256"
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Parse structured field dictionary format
|
|
5
|
+
* Handles both complex format: name=(components);params
|
|
6
|
+
* and simple format: name=:value:
|
|
7
|
+
*/
|
|
8
|
+
function parseStructuredFieldDictionary(input) {
|
|
9
|
+
// Try complex format first
|
|
10
|
+
const match = input.match(/([^=]+)=\((.*?)\);(.*)$/)
|
|
11
|
+
if (match) {
|
|
12
|
+
const name = match[1]
|
|
13
|
+
const components = match[2].split(" ")
|
|
14
|
+
const params = {}
|
|
15
|
+
|
|
16
|
+
const paramPairs = match[3].split(";").filter(p => p)
|
|
17
|
+
paramPairs.forEach(pair => {
|
|
18
|
+
const [key, value] = pair.split("=")
|
|
19
|
+
if (key && value) {
|
|
20
|
+
params[key] = value.replace(/"/g, "")
|
|
21
|
+
}
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
return { name, components, params }
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// Try simple format
|
|
28
|
+
const simpleMatch = input.match(/([^=]+)=:([^:]+):/)
|
|
29
|
+
if (simpleMatch) {
|
|
30
|
+
return { name: simpleMatch[1], value: simpleMatch[2] }
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
return null
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Convert base64url string to base64
|
|
38
|
+
*/
|
|
39
|
+
function base64urlToBase64(str) {
|
|
40
|
+
return str.replace(/-/g, "+").replace(/_/g, "/")
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Convert Uint8Array to base64url string
|
|
45
|
+
*/
|
|
46
|
+
function uint8ArrayToBase64url(bytes) {
|
|
47
|
+
// Convert to base64
|
|
48
|
+
let binary = ""
|
|
49
|
+
for (let i = 0; i < bytes.length; i++) {
|
|
50
|
+
binary += String.fromCharCode(bytes[i])
|
|
51
|
+
}
|
|
52
|
+
const base64 = btoa(binary)
|
|
53
|
+
|
|
54
|
+
// Convert to base64url
|
|
55
|
+
return base64.replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, "")
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Generate commitment ID for RSA-PSS and ECDSA signatures
|
|
60
|
+
* The ID is the SHA256 hash of the raw signature bytes
|
|
61
|
+
*
|
|
62
|
+
* @param {Object} commitment - The commitment object containing signature
|
|
63
|
+
* @returns {string} The commitment ID in base64url format
|
|
64
|
+
*/
|
|
65
|
+
function generateRsaCommitmentId(commitment) {
|
|
66
|
+
// Extract the base64 signature from structured field format
|
|
67
|
+
// Format: "signature-name=:BASE64_SIGNATURE:"
|
|
68
|
+
const match = commitment.signature.match(/^[^=]+=:([^:]+):/)
|
|
69
|
+
if (!match) {
|
|
70
|
+
throw new Error("Invalid signature format")
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const signatureBase64 = match[1]
|
|
74
|
+
// Convert base64 to Uint8Array
|
|
75
|
+
const signatureBinary = Uint8Array.from(atob(signatureBase64), c =>
|
|
76
|
+
c.charCodeAt(0)
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
// SHA256 hash of the raw signature
|
|
80
|
+
const hashResult = hash(signatureBinary)
|
|
81
|
+
const id = uint8ArrayToBase64url(hashResult)
|
|
82
|
+
|
|
83
|
+
return id
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Generate HMAC commitment ID for HyperBEAM messages
|
|
88
|
+
* The ID is deterministic based on message content only
|
|
89
|
+
*
|
|
90
|
+
* The Erlang implementation sorts components WITH @ prefix included,
|
|
91
|
+
* then removes @ from derived components in the signature base.
|
|
92
|
+
*
|
|
93
|
+
* @param {Object} message - The message with signature and signature-input
|
|
94
|
+
* @returns {string} The commitment ID in base64url format
|
|
95
|
+
*/
|
|
96
|
+
function generateHmacCommitmentId(message) {
|
|
97
|
+
// Parse signature-input to get components
|
|
98
|
+
const parsed = parseStructuredFieldDictionary(message["signature-input"])
|
|
99
|
+
if (!parsed || !parsed.components) {
|
|
100
|
+
throw new Error("Failed to parse signature-input")
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Sort components AS-IS (with quotes and @ prefix)
|
|
104
|
+
const sortedComponents = [...parsed.components].sort()
|
|
105
|
+
|
|
106
|
+
// Build signature base in sorted order
|
|
107
|
+
const lines = []
|
|
108
|
+
|
|
109
|
+
sortedComponents.forEach(component => {
|
|
110
|
+
const cleanComponent = component.replace(/"/g, "")
|
|
111
|
+
let fieldName = cleanComponent
|
|
112
|
+
let value
|
|
113
|
+
|
|
114
|
+
// For derived components (starting with @), remove @ in the signature base
|
|
115
|
+
if (cleanComponent.startsWith("@")) {
|
|
116
|
+
fieldName = cleanComponent.substring(1)
|
|
117
|
+
value = message[fieldName]
|
|
118
|
+
} else {
|
|
119
|
+
value = message[cleanComponent]
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
if (value === undefined || value === null) {
|
|
123
|
+
value = ""
|
|
124
|
+
} else if (typeof value === "number") {
|
|
125
|
+
value = value.toString()
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
lines.push(`"${fieldName}": ${value}`)
|
|
129
|
+
})
|
|
130
|
+
|
|
131
|
+
// Add signature-params line with sorted components (keeping @ prefix)
|
|
132
|
+
const paramsComponents = sortedComponents.join(" ")
|
|
133
|
+
lines.push(
|
|
134
|
+
`"@signature-params": (${paramsComponents});alg="hmac-sha256";keyid="ao"`
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
const signatureBase = lines.join("\n")
|
|
138
|
+
|
|
139
|
+
// Generate HMAC with key "ao"
|
|
140
|
+
// Convert string to Uint8Array
|
|
141
|
+
const messageBytes = new TextEncoder().encode(signatureBase)
|
|
142
|
+
const keyBytes = new TextEncoder().encode("ao")
|
|
143
|
+
|
|
144
|
+
const hmacResult = hmac(keyBytes, messageBytes)
|
|
145
|
+
return uint8ArrayToBase64url(hmacResult)
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Generate commitment ID based on the algorithm type
|
|
150
|
+
*
|
|
151
|
+
* @param {Object} commitment - The commitment object containing alg, signature, etc.
|
|
152
|
+
* @param {Object} fullMessage - The full message (required for HMAC)
|
|
153
|
+
* @returns {string} The commitment ID in base64url format
|
|
154
|
+
*/
|
|
155
|
+
function generateCommitmentId(commitment, fullMessage = null) {
|
|
156
|
+
switch (commitment.alg) {
|
|
157
|
+
case "rsa-pss-sha512":
|
|
158
|
+
case "ecdsa-p256-sha256":
|
|
159
|
+
return generateRsaCommitmentId(commitment)
|
|
160
|
+
|
|
161
|
+
case "hmac-sha256":
|
|
162
|
+
if (!fullMessage) {
|
|
163
|
+
throw new Error("HMAC commitment IDs require full message context")
|
|
164
|
+
}
|
|
165
|
+
return generateHmacCommitmentId(fullMessage)
|
|
166
|
+
|
|
167
|
+
default:
|
|
168
|
+
throw new Error(`Unsupported algorithm: ${commitment.alg}`)
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Extract all commitment IDs from a HyperBEAM message
|
|
174
|
+
*
|
|
175
|
+
* @param {Object} message - The message with commitments
|
|
176
|
+
* @returns {Object} Map of commitment IDs to their types
|
|
177
|
+
*/
|
|
178
|
+
function extractCommitmentIds(message) {
|
|
179
|
+
const ids = {}
|
|
180
|
+
|
|
181
|
+
if (!message.commitments) {
|
|
182
|
+
return ids
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
for (const [id, commitment] of Object.entries(message.commitments)) {
|
|
186
|
+
ids[id] = {
|
|
187
|
+
alg: commitment.alg,
|
|
188
|
+
committer: commitment.committer,
|
|
189
|
+
device: commitment["commitment-device"],
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
return ids
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Verify a commitment ID matches the expected value
|
|
198
|
+
*
|
|
199
|
+
* @param {Object} commitment - The commitment object
|
|
200
|
+
* @param {string} expectedId - The expected commitment ID
|
|
201
|
+
* @param {Object} fullMessage - The full message (required for HMAC)
|
|
202
|
+
* @returns {boolean} True if the ID matches
|
|
203
|
+
*/
|
|
204
|
+
function verifyCommitmentId(commitment, expectedId, fullMessage = null) {
|
|
205
|
+
try {
|
|
206
|
+
const calculatedId = generateCommitmentId(commitment, fullMessage)
|
|
207
|
+
return calculatedId === expectedId
|
|
208
|
+
} catch (error) {
|
|
209
|
+
console.error("Error verifying commitment ID:", error)
|
|
210
|
+
return false
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// Export all functions
|
|
215
|
+
export {
|
|
216
|
+
generateCommitmentId,
|
|
217
|
+
generateRsaCommitmentId,
|
|
218
|
+
generateHmacCommitmentId,
|
|
219
|
+
extractCommitmentIds,
|
|
220
|
+
verifyCommitmentId,
|
|
221
|
+
parseStructuredFieldDictionary,
|
|
222
|
+
}
|
package/esm/send.js
CHANGED
|
@@ -9,6 +9,8 @@ const {
|
|
|
9
9
|
} = httpbis
|
|
10
10
|
|
|
11
11
|
export async function send(signedMsg, fetchImpl = fetch) {
|
|
12
|
+
// IMPORTANT: Use the URL from signedMsg.url, NOT from any path header
|
|
13
|
+
// This ensures we send to the correct URL even if path header is different
|
|
12
14
|
const fetchOptions = {
|
|
13
15
|
method: signedMsg.method,
|
|
14
16
|
headers: signedMsg.headers,
|
|
@@ -21,8 +23,9 @@ export async function send(signedMsg, fetchImpl = fetch) {
|
|
|
21
23
|
) {
|
|
22
24
|
fetchOptions.body = signedMsg.body
|
|
23
25
|
}
|
|
24
|
-
const response = await fetchImpl(signedMsg.url, fetchOptions)
|
|
25
26
|
|
|
27
|
+
// Use the URL as provided, ignoring any path header
|
|
28
|
+
const response = await fetchImpl(signedMsg.url, fetchOptions)
|
|
26
29
|
if (response.status >= 400) {
|
|
27
30
|
throw new Error(`${response.status}: ${await response.text()}`)
|
|
28
31
|
}
|
|
@@ -90,10 +93,8 @@ export const toHttpSigner = signer => {
|
|
|
90
93
|
|
|
91
94
|
signatureBaseArray.push(['"@signature-params"', [signatureInput]])
|
|
92
95
|
signatureBase = formatSignatureBase(signatureBaseArray)
|
|
93
|
-
|
|
94
96
|
return new TextEncoder().encode(signatureBase)
|
|
95
97
|
}
|
|
96
|
-
|
|
97
98
|
const result = await signer(create, "httpsig")
|
|
98
99
|
|
|
99
100
|
if (!createCalled) {
|
package/esm/signer-utils.js
CHANGED
|
@@ -207,7 +207,7 @@ export async function verify(signedMessage, publicKey) {
|
|
|
207
207
|
* @param {string} [signatureName] - Optional signature name to look for
|
|
208
208
|
* @returns {Buffer|null} Public key buffer or null
|
|
209
209
|
*/
|
|
210
|
-
function extractPublicKeyFromHeaders(headers, signatureName) {
|
|
210
|
+
export function extractPublicKeyFromHeaders(headers, signatureName) {
|
|
211
211
|
const signatureInput =
|
|
212
212
|
headers["signature-input"] || headers["Signature-Input"]
|
|
213
213
|
if (!signatureInput) return null
|
package/esm/signer.js
CHANGED
|
@@ -5,7 +5,11 @@ const joinUrl = ({ url, path }) => {
|
|
|
5
5
|
if (path.startsWith("http://") || path.startsWith("https://")) {
|
|
6
6
|
return path
|
|
7
7
|
}
|
|
8
|
-
|
|
8
|
+
// Ensure path starts with /
|
|
9
|
+
const normalizedPath = path.startsWith("/") ? path : "/" + path
|
|
10
|
+
return url.endsWith("/")
|
|
11
|
+
? url.slice(0, -1) + normalizedPath
|
|
12
|
+
: url + normalizedPath
|
|
9
13
|
}
|
|
10
14
|
|
|
11
15
|
export function signer(config) {
|
|
@@ -15,7 +19,7 @@ export function signer(config) {
|
|
|
15
19
|
throw new Error("Signer is required for mainnet mode")
|
|
16
20
|
}
|
|
17
21
|
|
|
18
|
-
return async function sign(fields) {
|
|
22
|
+
return async function sign(fields, { path: _path = false } = {}) {
|
|
19
23
|
const { path = "/relay/process", method = "POST", ...restFields } = fields
|
|
20
24
|
const aoFields = { ...restFields }
|
|
21
25
|
const encoded = await enc(aoFields)
|
|
@@ -24,6 +28,8 @@ export function signer(config) {
|
|
|
24
28
|
|
|
25
29
|
const _url = joinUrl({ url, path })
|
|
26
30
|
|
|
31
|
+
headersObj["path"] = path
|
|
32
|
+
|
|
27
33
|
if (body && !headersObj["content-length"]) {
|
|
28
34
|
const bodySize = body.size || body.byteLength || 0
|
|
29
35
|
if (bodySize > 0) {
|
|
@@ -35,21 +41,39 @@ export function signer(config) {
|
|
|
35
41
|
for (const [key, value] of Object.entries(headersObj)) {
|
|
36
42
|
lowercaseHeaders[key.toLowerCase()] = value
|
|
37
43
|
}
|
|
38
|
-
|
|
44
|
+
if (lowercaseHeaders.path && /^\//.test(lowercaseHeaders.path)) {
|
|
45
|
+
//const sp = lowercaseHeaders.path.split("/")
|
|
46
|
+
//lowercaseHeaders.path = sp.slice(sp.length - 1).join("/")
|
|
47
|
+
}
|
|
48
|
+
// Parse body-keys if present
|
|
49
|
+
const bodyKeys = headersObj["body-keys"]
|
|
50
|
+
? headersObj["body-keys"]
|
|
51
|
+
.replace(/"/g, "")
|
|
52
|
+
.split(",")
|
|
53
|
+
.map(k => k.trim())
|
|
54
|
+
: []
|
|
55
|
+
|
|
56
|
+
// Collect fields to sign from headers
|
|
39
57
|
const signingFields = Object.keys(lowercaseHeaders).filter(
|
|
40
|
-
key => key !== "body-keys"
|
|
58
|
+
key => key !== "body-keys" && key !== "path" && !bodyKeys.includes(key) // Also exclude fields that are in body-keys
|
|
41
59
|
)
|
|
42
|
-
|
|
60
|
+
// Always include @path in the signature
|
|
61
|
+
//if (!signingFields.includes("path")) signingFields.push("path")
|
|
62
|
+
if (_path) signingFields.push("path")
|
|
63
|
+
// Ensure we have at least one field to sign
|
|
43
64
|
if (signingFields.length === 0 && !body) {
|
|
44
65
|
lowercaseHeaders["content-length"] = "0"
|
|
45
66
|
signingFields.push("content-length")
|
|
46
67
|
}
|
|
47
68
|
|
|
48
69
|
const signedRequest = await toHttpSigner(signer)({
|
|
49
|
-
request: {
|
|
70
|
+
request: {
|
|
71
|
+
url: _url,
|
|
72
|
+
method,
|
|
73
|
+
headers: lowercaseHeaders,
|
|
74
|
+
},
|
|
50
75
|
fields: signingFields,
|
|
51
76
|
})
|
|
52
|
-
|
|
53
77
|
const finalHeaders = {}
|
|
54
78
|
|
|
55
79
|
for (const [key, value] of Object.entries(headersObj)) {
|
|
@@ -62,7 +86,6 @@ export function signer(config) {
|
|
|
62
86
|
if (headersObj["body-keys"]) {
|
|
63
87
|
finalHeaders["body-keys"] = headersObj["body-keys"]
|
|
64
88
|
}
|
|
65
|
-
|
|
66
89
|
const result = { url: _url, method, headers: finalHeaders }
|
|
67
90
|
|
|
68
91
|
if (body) result.body = body
|
|
@@ -70,3 +93,5 @@ export function signer(config) {
|
|
|
70
93
|
return result
|
|
71
94
|
}
|
|
72
95
|
}
|
|
96
|
+
|
|
97
|
+
// stack / simple-pay / patch / hyperbeam / hyperbeam-aos / scheduler / cron
|
package/esm/utils.js
CHANGED
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
import { graphql, parse, validate, buildSchema } from "graphql"
|
|
2
2
|
import sha256 from "fast-sha256"
|
|
3
|
+
import {
|
|
4
|
+
generateCommitmentId,
|
|
5
|
+
generateRsaCommitmentId,
|
|
6
|
+
generateHmacCommitmentId,
|
|
7
|
+
verifyCommitmentId,
|
|
8
|
+
} from "./id.js"
|
|
9
|
+
export { generateRsaCommitmentId as rsaid, generateHmacCommitmentId as hmacid }
|
|
3
10
|
import {
|
|
4
11
|
clone,
|
|
5
12
|
is,
|