hbsig 0.2.8 → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/cjs/commit.js +263 -43
- package/cjs/encode-utils.js +10 -4
- package/cjs/encode.js +66 -19
- package/cjs/erl_json.js +12 -3
- package/cjs/erl_str.js +5 -0
- package/cjs/flat.js +70 -13
- package/cjs/httpsig.js +159 -173
- package/cjs/parser.js +30 -6
- package/cjs/send.js +11 -7
- package/cjs/signer-utils.js +14 -12
- package/cjs/signer.js +140 -281
- package/cjs/structured.js +140 -146
- package/cjs/test.js +0 -15
- package/esm/commit.js +174 -19
- package/esm/encode-utils.js +10 -4
- package/esm/encode.js +52 -15
- package/esm/erl_json.js +4 -1
- package/esm/erl_str.js +5 -0
- package/esm/flat.js +61 -7
- package/esm/httpsig.js +118 -113
- package/esm/parser.js +26 -8
- package/esm/send.js +8 -3
- package/esm/signer-utils.js +5 -1
- package/esm/signer.js +66 -174
- package/esm/structured.js +97 -98
- package/esm/test.js +2 -6
- package/package.json +2 -2
package/esm/structured.js
CHANGED
|
@@ -73,7 +73,7 @@ function to(tabm) {
|
|
|
73
73
|
// Build result with empty values first
|
|
74
74
|
const result = {}
|
|
75
75
|
|
|
76
|
-
// Add empty values based on their types
|
|
76
|
+
// Add empty values based on their types (these may be overwritten if data exists)
|
|
77
77
|
for (const [key, type] of Object.entries(types)) {
|
|
78
78
|
if (type === "empty-binary") {
|
|
79
79
|
result[key] = ""
|
|
@@ -111,11 +111,19 @@ function to(tabm) {
|
|
|
111
111
|
value !== null &&
|
|
112
112
|
!Array.isArray(value)
|
|
113
113
|
) {
|
|
114
|
+
// Check if the child object itself indicates it's a list via .="list" in its ao-types
|
|
115
|
+
const childAoTypes = value["ao-types"] || ""
|
|
116
|
+
const childTypes = parseAoTypes(childAoTypes)
|
|
117
|
+
const isChildList = childTypes["."] === "list"
|
|
118
|
+
|
|
114
119
|
// Recursively decode child TABM
|
|
115
120
|
const childDecoded = to(value)
|
|
116
|
-
const type = types[normalizedKey]
|
|
117
121
|
|
|
118
|
-
if
|
|
122
|
+
// Only convert numbered map to array if the child object itself
|
|
123
|
+
// declares it's a list via .="list" in its ao-types.
|
|
124
|
+
// This preserves original keys when the parent declares the type
|
|
125
|
+
// but the child doesn't have the list marker.
|
|
126
|
+
if (isChildList) {
|
|
119
127
|
// Convert numbered map back to ordered list
|
|
120
128
|
result[rawKey] = messageToOrderedList(childDecoded)
|
|
121
129
|
} else {
|
|
@@ -206,9 +214,23 @@ function decodeValue(type, value) {
|
|
|
206
214
|
case "float":
|
|
207
215
|
return parseFloat(value)
|
|
208
216
|
|
|
217
|
+
case "boolean":
|
|
218
|
+
// SF boolean format: ?1 = true, ?0 = false
|
|
219
|
+
// Convert to native boolean, will be encoded as "atom" type
|
|
220
|
+
if (value === "?1") return true
|
|
221
|
+
if (value === "?0") return false
|
|
222
|
+
// Fallback for other formats
|
|
223
|
+
return value === "true" || value === "1"
|
|
224
|
+
|
|
209
225
|
case "atom":
|
|
210
226
|
const atomItem = parseStructuredItem(value)
|
|
211
|
-
|
|
227
|
+
const atomName = atomItem.replace(/^"|"$/g, "") // Remove quotes
|
|
228
|
+
// Convert to Symbol to preserve atom type through round-trip
|
|
229
|
+
// Special cases for common atoms that JS has native types for
|
|
230
|
+
if (atomName === "true") return true
|
|
231
|
+
if (atomName === "false") return false
|
|
232
|
+
if (atomName === "null") return null
|
|
233
|
+
return Symbol.for(atomName)
|
|
212
234
|
|
|
213
235
|
case "list":
|
|
214
236
|
return parseStructuredList(value).map(item => {
|
|
@@ -236,7 +258,8 @@ function decodeValue(type, value) {
|
|
|
236
258
|
* @returns {*} - Parsed value
|
|
237
259
|
*/
|
|
238
260
|
function parseStructuredItem(value) {
|
|
239
|
-
//
|
|
261
|
+
// Handle non-string values (e.g., numbers from HyperBEAM responses)
|
|
262
|
+
if (typeof value !== "string") return String(value)
|
|
240
263
|
if (value.startsWith('"') && value.endsWith('"')) {
|
|
241
264
|
return value.slice(1, -1) // Remove quotes
|
|
242
265
|
}
|
|
@@ -249,10 +272,14 @@ function parseStructuredItem(value) {
|
|
|
249
272
|
* @returns {Array} - Parsed list
|
|
250
273
|
*/
|
|
251
274
|
function parseStructuredList(value) {
|
|
252
|
-
|
|
275
|
+
if (typeof value !== "string") return [value]
|
|
253
276
|
return value.split(", ").map(item => {
|
|
254
277
|
if (item.startsWith('"') && item.endsWith('"')) {
|
|
255
|
-
|
|
278
|
+
// Remove quotes and unescape SF string escapes
|
|
279
|
+
// In SF strings: \" = " and \\ = \
|
|
280
|
+
return item.slice(1, -1)
|
|
281
|
+
.replace(/\\"/g, '"') // \" -> "
|
|
282
|
+
.replace(/\\\\/g, '\\') // \\ -> \
|
|
256
283
|
}
|
|
257
284
|
return item
|
|
258
285
|
})
|
|
@@ -264,44 +291,72 @@ function parseStructuredList(value) {
|
|
|
264
291
|
* @returns {object} - TABM
|
|
265
292
|
*/
|
|
266
293
|
function from(msg) {
|
|
267
|
-
// Handle
|
|
268
|
-
if (
|
|
269
|
-
msg
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
) {
|
|
294
|
+
// Handle binary input - passthrough
|
|
295
|
+
if (msg instanceof Buffer || msg instanceof Uint8Array) {
|
|
296
|
+
return msg
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
// Handle non-object values - passthrough
|
|
300
|
+
if (typeof msg !== "object" || msg === null) {
|
|
274
301
|
return msg
|
|
275
302
|
}
|
|
276
303
|
|
|
277
|
-
//
|
|
278
|
-
|
|
304
|
+
// Handle arrays - convert to numbered map with .="list" in ao-types
|
|
305
|
+
// Mirrors Erlang: from(List, Req, Opts) when is_list(List)
|
|
306
|
+
if (Array.isArray(msg)) {
|
|
307
|
+
// Convert to numbered map (1-based indexing like Erlang)
|
|
308
|
+
const numberedMap = {}
|
|
309
|
+
msg.forEach((item, idx) => {
|
|
310
|
+
numberedMap[(idx + 1).toString()] = item
|
|
311
|
+
})
|
|
312
|
+
|
|
313
|
+
// Recursively process the numbered map
|
|
314
|
+
const result = from(numberedMap)
|
|
315
|
+
|
|
316
|
+
// Add .="list" to ao-types to indicate this message is a list
|
|
317
|
+
const existingAoTypes = result["ao-types"] || ""
|
|
318
|
+
if (existingAoTypes) {
|
|
319
|
+
result["ao-types"] = '.="list", ' + existingAoTypes
|
|
320
|
+
} else {
|
|
321
|
+
result["ao-types"] = '.="list"'
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
return result
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
// Process keys - preserve original case to match Erlang behavior
|
|
328
|
+
// HTTP headers are case-insensitive but JSON/map keys preserve case
|
|
329
|
+
const keysMap = {}
|
|
279
330
|
for (const [key, value] of Object.entries(msg)) {
|
|
280
|
-
|
|
281
|
-
normalizedMap[normKey] = value
|
|
331
|
+
keysMap[key] = value
|
|
282
332
|
}
|
|
283
333
|
|
|
284
|
-
// Get sorted keys (
|
|
285
|
-
const sortedKeys = Object.keys(
|
|
334
|
+
// Get sorted keys (preserving case)
|
|
335
|
+
const sortedKeys = Object.keys(keysMap).sort()
|
|
286
336
|
|
|
287
337
|
const types = []
|
|
288
338
|
const values = []
|
|
289
339
|
|
|
290
340
|
// Process each key in sorted order
|
|
291
|
-
for (const
|
|
292
|
-
const value =
|
|
341
|
+
for (const key of sortedKeys) {
|
|
342
|
+
const value = keysMap[key]
|
|
293
343
|
|
|
294
|
-
// Handle empty
|
|
344
|
+
// Handle empty binaries/strings - just include as-is, no type annotation
|
|
345
|
+
// (Erlang doesn't add empty-binary type, it just keeps the empty binary)
|
|
295
346
|
if (value === "" || (value instanceof Buffer && value.length === 0)) {
|
|
296
|
-
|
|
347
|
+
values.push([key, value])
|
|
297
348
|
continue
|
|
298
349
|
}
|
|
299
350
|
|
|
351
|
+
// Empty arrays - convert to numbered map with .="list" in ao-types
|
|
352
|
+
// (Erlang doesn't add empty-list type, just the list marker)
|
|
300
353
|
if (Array.isArray(value) && value.length === 0) {
|
|
301
|
-
|
|
354
|
+
values.push([key, from(value)])
|
|
302
355
|
continue
|
|
303
356
|
}
|
|
304
357
|
|
|
358
|
+
// Empty objects - just include as-is, no type annotation
|
|
359
|
+
// (Erlang doesn't add empty-message type, it just keeps the empty map)
|
|
305
360
|
if (
|
|
306
361
|
typeof value === "object" &&
|
|
307
362
|
value !== null &&
|
|
@@ -309,43 +364,30 @@ function from(msg) {
|
|
|
309
364
|
!(value instanceof Buffer) &&
|
|
310
365
|
Object.keys(value).length === 0
|
|
311
366
|
) {
|
|
312
|
-
|
|
367
|
+
values.push([key, value])
|
|
313
368
|
continue
|
|
314
369
|
}
|
|
315
370
|
|
|
316
371
|
// Handle binary/string values
|
|
317
372
|
if (value instanceof Buffer || value instanceof Uint8Array) {
|
|
318
|
-
values.push([
|
|
373
|
+
values.push([key, value])
|
|
319
374
|
continue
|
|
320
375
|
}
|
|
321
376
|
|
|
322
377
|
if (typeof value === "string") {
|
|
323
|
-
values.push([
|
|
378
|
+
values.push([key, value])
|
|
324
379
|
continue
|
|
325
380
|
}
|
|
326
381
|
|
|
327
382
|
// Handle nested maps
|
|
328
383
|
if (typeof value === "object" && !Array.isArray(value) && value !== null) {
|
|
329
|
-
values.push([
|
|
384
|
+
values.push([key, from(value)])
|
|
330
385
|
continue
|
|
331
386
|
}
|
|
332
387
|
|
|
333
|
-
// Handle arrays
|
|
388
|
+
// Handle arrays - from() converts to numbered map with .="list" in ao-types
|
|
334
389
|
if (Array.isArray(value) && value.length > 0) {
|
|
335
|
-
|
|
336
|
-
// Convert to numbered map (1-based indexing)
|
|
337
|
-
const numberedMap = {}
|
|
338
|
-
value.forEach((item, idx) => {
|
|
339
|
-
numberedMap[(idx + 1).toString()] = item
|
|
340
|
-
})
|
|
341
|
-
types.push([normKey, "list"])
|
|
342
|
-
values.push([normKey, from(numberedMap)])
|
|
343
|
-
} else {
|
|
344
|
-
// Encode as list string
|
|
345
|
-
const [type, encoded] = encodeValue(value)
|
|
346
|
-
types.push([normKey, type])
|
|
347
|
-
values.push([normKey, encoded])
|
|
348
|
-
}
|
|
390
|
+
values.push([key, from(value)])
|
|
349
391
|
continue
|
|
350
392
|
}
|
|
351
393
|
|
|
@@ -353,13 +395,12 @@ function from(msg) {
|
|
|
353
395
|
if (
|
|
354
396
|
typeof value === "symbol" ||
|
|
355
397
|
typeof value === "number" ||
|
|
356
|
-
Array.isArray(value) ||
|
|
357
398
|
typeof value === "boolean" ||
|
|
358
399
|
value === null
|
|
359
400
|
) {
|
|
360
401
|
const [type, encoded] = encodeValue(value)
|
|
361
|
-
types.push([
|
|
362
|
-
values.push([
|
|
402
|
+
types.push([key, type])
|
|
403
|
+
values.push([key, encoded])
|
|
363
404
|
continue
|
|
364
405
|
}
|
|
365
406
|
}
|
|
@@ -380,54 +421,13 @@ function from(msg) {
|
|
|
380
421
|
return result
|
|
381
422
|
}
|
|
382
423
|
|
|
383
|
-
/**
|
|
384
|
-
* Check if an array should be converted to numbered map
|
|
385
|
-
* Rules based on Erlang behavior:
|
|
386
|
-
* 1. Contains any objects/maps → convert
|
|
387
|
-
* 2. Contains empty arrays (but NOT empty buffers) → convert
|
|
388
|
-
* 3. All items are arrays (array of arrays) → convert
|
|
389
|
-
* 4. Otherwise → encode as string
|
|
390
|
-
*/
|
|
391
|
-
function shouldConvertToNumberedMap(arr) {
|
|
392
|
-
let allArrays = true
|
|
393
|
-
let hasObjects = false
|
|
394
|
-
let hasEmptyArrays = false
|
|
395
|
-
|
|
396
|
-
for (const item of arr) {
|
|
397
|
-
// Check for objects (not arrays or buffers)
|
|
398
|
-
if (
|
|
399
|
-
typeof item === "object" &&
|
|
400
|
-
item !== null &&
|
|
401
|
-
!Array.isArray(item) &&
|
|
402
|
-
!Buffer.isBuffer(item)
|
|
403
|
-
) {
|
|
404
|
-
hasObjects = true
|
|
405
|
-
}
|
|
406
|
-
// Check for empty arrays only (NOT empty buffers)
|
|
407
|
-
else if (Array.isArray(item) && item.length === 0) {
|
|
408
|
-
hasEmptyArrays = true
|
|
409
|
-
}
|
|
410
|
-
// Track if all items are arrays
|
|
411
|
-
else if (!Array.isArray(item)) {
|
|
412
|
-
allArrays = false
|
|
413
|
-
}
|
|
414
|
-
}
|
|
415
|
-
|
|
416
|
-
// Convert if: has objects, has empty arrays, or all items are non-empty arrays
|
|
417
|
-
return (
|
|
418
|
-
hasObjects ||
|
|
419
|
-
hasEmptyArrays ||
|
|
420
|
-
(allArrays && arr.length > 0 && arr.every(item => Array.isArray(item)))
|
|
421
|
-
)
|
|
422
|
-
}
|
|
423
|
-
|
|
424
424
|
/**
|
|
425
425
|
* Encode a value with its type
|
|
426
426
|
*/
|
|
427
427
|
function encodeValue(value) {
|
|
428
|
-
// Null (as atom)
|
|
428
|
+
// Null (as atom) - use token format (unquoted)
|
|
429
429
|
if (value === null) {
|
|
430
|
-
return ["atom",
|
|
430
|
+
return ["atom", "null"]
|
|
431
431
|
}
|
|
432
432
|
|
|
433
433
|
// Integer
|
|
@@ -437,24 +437,23 @@ function encodeValue(value) {
|
|
|
437
437
|
|
|
438
438
|
// Float
|
|
439
439
|
if (typeof value === "number") {
|
|
440
|
-
// Format like Erlang
|
|
440
|
+
// Format like Erlang's float_to_binary - scientific notation with full precision
|
|
441
|
+
// Erlang's float_to_binary/1 uses ~20 decimal digits and keeps trailing zeros
|
|
441
442
|
let str = value.toExponential(20)
|
|
442
|
-
//
|
|
443
|
-
str = str.replace(/(\.\d*?)0+e/, "$1e").replace(/\.e/, ".0e")
|
|
444
|
-
// Ensure 2-digit exponent
|
|
443
|
+
// Ensure 2-digit exponent with sign
|
|
445
444
|
str = str.replace(/e([+-])(\d)$/, "e$10$2")
|
|
446
445
|
return ["float", str]
|
|
447
446
|
}
|
|
448
447
|
|
|
449
|
-
// Boolean (as atom)
|
|
448
|
+
// Boolean (as atom) - use token format (unquoted)
|
|
450
449
|
if (typeof value === "boolean") {
|
|
451
|
-
return ["atom",
|
|
450
|
+
return ["atom", value.toString()]
|
|
452
451
|
}
|
|
453
452
|
|
|
454
|
-
// Symbol (as atom)
|
|
453
|
+
// Symbol (as atom) - use token format (unquoted)
|
|
455
454
|
if (typeof value === "symbol") {
|
|
456
455
|
const name = Symbol.keyFor(value) || value.description || ""
|
|
457
|
-
return ["atom",
|
|
456
|
+
return ["atom", name]
|
|
458
457
|
}
|
|
459
458
|
|
|
460
459
|
// List
|
package/esm/test.js
CHANGED
package/package.json
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "hbsig",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.1",
|
|
4
4
|
"main": "cjs/index.js",
|
|
5
|
+
"module": "esm/index.js",
|
|
5
6
|
"license": "MIT",
|
|
6
7
|
"exports": {
|
|
7
8
|
".": {
|
|
@@ -28,7 +29,6 @@
|
|
|
28
29
|
"ramda": "^0.31.3",
|
|
29
30
|
"structured-headers": "1.0.1"
|
|
30
31
|
},
|
|
31
|
-
"module": "esm/index.js",
|
|
32
32
|
"bin": {
|
|
33
33
|
"wao": "./cjs/cli.js",
|
|
34
34
|
"wao-esm": "./esm/cli.js"
|