wao 0.27.4 → 0.28.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.
@@ -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
- this.processRootArray(key, value);
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
- this.keys.push(fullPath);
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, otherFieldsAreEmpty, bodyBuffer, bodyArrayBuffer, contentDigest, base64;
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 = 15;
373
+ _context5.next = 14;
374
374
  break;
375
375
  }
376
376
  singleKey = bodyKeys[0];
377
- value = (0, _encodeUtils.getValueByPath)(obj, singleKey);
378
- otherFieldsAreEmpty = Object.entries(obj).every(function (_ref11) {
379
- var _ref12 = _slicedToArray(_ref11, 2),
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
- bodyBuffer = (0, _encodeUtils.toBuffer)(value);
390
- bodyArrayBuffer = bodyBuffer.buffer.slice(bodyBuffer.byteOffset, bodyBuffer.byteOffset + bodyBuffer.byteLength);
391
- _context5.next = 9;
392
- return (0, _encodeUtils.sha256)(bodyArrayBuffer);
393
- case 9:
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: value
407
+ body: bodyContent
406
408
  });
407
- case 15:
409
+ case 14:
408
410
  return _context5.abrupt("return", null);
409
- case 16:
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
- var isLastBodyPart = sortedBodyKeys.indexOf(bodyKey) === sortedBodyKeys.length - 1;
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
- headers["body-keys"] = sortedBodyKeys.map(function (k) {
1289
- return "\"".concat(k, "\"");
1290
- }).join(", ");
1291
- if (!hasSpecialDataBody) {
1292
- if (sortedBodyKeys.includes("body") && sortedBodyKeys.length === 1) {
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); // Step 14: Generate multipart boundary
1302
- _context8.next = 49;
1303
- return generateBoundary(bodyParts);
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 = 53;
1317
+ _context8.next = 55;
1309
1318
  return calculateContentDigest(body);
1310
- case 53:
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 58:
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:
@@ -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
- return url.endsWith("/") ? url.slice(0, -1) + path : url + path;
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 _fields$path, path, _fields$method, method, restFields, aoFields, encoded, headersObj, body, _url, bodySize, lowercaseHeaders, _i, _Object$entries, _Object$entries$_i, key, value, signingFields, signedRequest, finalHeaders, _i2, _Object$entries2, _Object$entries2$_i, _key, _value, result;
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 = 4;
83
+ _context.next = 5;
51
84
  return (0, _encode.enc)(aoFields);
52
- case 4:
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 = 15;
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 15:
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 24:
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.isData = exports.isCheckComplete = exports.getTagVal = exports.getTag = exports.dirname = exports.checkTag = exports.buildTags = exports.allChecked = exports.action = void 0;
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(); }
@@ -8,6 +8,6 @@
8
8
  "deploy": "node scripts/deploy.js"
9
9
  },
10
10
  "dependencies": {
11
- "wao": "^0.26.1"
11
+ "wao": "^0.28.0"
12
12
  }
13
13
  }
@@ -5,7 +5,7 @@ import { HB } from "wao"
5
5
  import { resolve } from "path"
6
6
  import { readFileSync } from "fs"
7
7
 
8
- const cwd = "../dev/wao/HyperBEAM"
8
+ const cwd = "../../HyperBEAM"
9
9
  const wallet = ".wallet.json"
10
10
 
11
11
  describe("HyperBEAM", function () {
@@ -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
- this.processRootArray(key, value)
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
- this.keys.push(fullPath)
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
- const otherFieldsAreEmpty = Object.entries(obj).every(([key, val]) => {
297
- if (key === singleKey) return true
298
- return isEmpty(val)
299
- })
300
-
301
- if (otherFieldsAreEmpty && isBytes(value) && value.length > 0) {
302
- const bodyBuffer = toBuffer(value)
303
- const bodyArrayBuffer = bodyBuffer.buffer.slice(
304
- bodyBuffer.byteOffset,
305
- bodyBuffer.byteOffset + bodyBuffer.byteLength
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(bodyArrayBuffer)
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: value }
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
- const isLastBodyPart =
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
- headers["body-keys"] = sortedBodyKeys.map(k => `"${k}"`).join(", ")
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
- if (!hasSpecialDataBody) {
1168
- if (sortedBodyKeys.includes("body") && sortedBodyKeys.length === 1) {
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) {
@@ -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
- return url.endsWith("/") ? url.slice(0, -1) + path : url + path
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: { url: _url, method, headers: lowercaseHeaders },
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,
@@ -8,6 +8,6 @@
8
8
  "deploy": "node scripts/deploy.js"
9
9
  },
10
10
  "dependencies": {
11
- "wao": "^0.26.1"
11
+ "wao": "^0.28.0"
12
12
  }
13
13
  }
@@ -5,7 +5,7 @@ import { HB } from "wao"
5
5
  import { resolve } from "path"
6
6
  import { readFileSync } from "fs"
7
7
 
8
- const cwd = "../dev/wao/HyperBEAM"
8
+ const cwd = "../../HyperBEAM"
9
9
  const wallet = ".wallet.json"
10
10
 
11
11
  describe("HyperBEAM", function () {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wao",
3
- "version": "0.27.4",
3
+ "version": "0.28.1",
4
4
  "bin": {
5
5
  "wao": "./cjs/cli.js",
6
6
  "wao-esm": "./esm/cli.js"