wao 0.24.0 → 0.24.2
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/hb.js +493 -326
- package/cjs/hyperbeam.js +32 -2
- package/cjs/signer.js +352 -196
- package/cjs/utils.js +1 -0
- package/esm/hb.js +121 -81
- package/esm/hyperbeam.js +21 -4
- package/esm/signer.js +172 -27
- package/esm/utils.js +1 -0
- package/package.json +1 -1
package/esm/signer.js
CHANGED
|
@@ -101,6 +101,12 @@ function hbEncodeValue(value) {
|
|
|
101
101
|
// Escape quotes and backslashes
|
|
102
102
|
const escaped = v.replace(/\\/g, "\\\\").replace(/"/g, '\\"')
|
|
103
103
|
return `"${escaped}"`
|
|
104
|
+
} else if (typeof v === "number") {
|
|
105
|
+
// Numbers should be encoded as bare items, not strings
|
|
106
|
+
return String(v)
|
|
107
|
+
} else if (typeof v === "boolean") {
|
|
108
|
+
// Booleans as structured field tokens
|
|
109
|
+
return v ? "?1" : "?0"
|
|
104
110
|
}
|
|
105
111
|
return `"${String(v)}"`
|
|
106
112
|
})
|
|
@@ -137,17 +143,77 @@ function hbEncodeLift(obj, parent = "", top = {}) {
|
|
|
137
143
|
)
|
|
138
144
|
}
|
|
139
145
|
|
|
140
|
-
//
|
|
146
|
+
// Store the original value for reference
|
|
147
|
+
const originalValue = value
|
|
148
|
+
|
|
149
|
+
// first/second lift object - handle nested objects
|
|
141
150
|
if (isPojo(value)) {
|
|
142
|
-
|
|
151
|
+
// Check if this object has any nested objects or arrays with objects
|
|
152
|
+
const hasComplexValues = Object.values(value).some(
|
|
153
|
+
v => isPojo(v) || (Array.isArray(v) && v.some(item => isPojo(item)))
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
if (!hasComplexValues) {
|
|
157
|
+
// Simple flat object - can be encoded as structured field dictionary
|
|
158
|
+
const items = []
|
|
159
|
+
|
|
160
|
+
Object.entries(value).forEach(([k, v]) => {
|
|
161
|
+
const subKey = k.toLowerCase()
|
|
162
|
+
|
|
163
|
+
if (typeof v === "string") {
|
|
164
|
+
const escaped = v.replace(/\\/g, "\\\\").replace(/"/g, '\\"')
|
|
165
|
+
items.push(`${subKey}="${escaped}"`)
|
|
166
|
+
} else if (typeof v === "number") {
|
|
167
|
+
items.push(`${subKey}=${v}`)
|
|
168
|
+
if (Number.isInteger(v)) {
|
|
169
|
+
// Use URL-encoded forward slash separator
|
|
170
|
+
acc[1][`${key.toLowerCase()}%2f${subKey}`] = "integer"
|
|
171
|
+
} else {
|
|
172
|
+
acc[1][`${key.toLowerCase()}%2f${subKey}`] = "float"
|
|
173
|
+
}
|
|
174
|
+
} else if (typeof v === "boolean") {
|
|
175
|
+
items.push(`${subKey}=${v ? "?1" : "?0"}`)
|
|
176
|
+
} else if (Array.isArray(v) && !v.some(item => isPojo(item))) {
|
|
177
|
+
// Simple array (no objects) - encode as structured field inner list
|
|
178
|
+
const listItems = v.map(item => {
|
|
179
|
+
if (typeof item === "string") {
|
|
180
|
+
return `"${item.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`
|
|
181
|
+
} else if (typeof item === "number") {
|
|
182
|
+
return String(item)
|
|
183
|
+
} else if (typeof item === "boolean") {
|
|
184
|
+
return item ? "?1" : "?0"
|
|
185
|
+
} else {
|
|
186
|
+
return `"${String(item)}"`
|
|
187
|
+
}
|
|
188
|
+
})
|
|
189
|
+
items.push(`${subKey}=(${listItems.join(" ")})`)
|
|
190
|
+
}
|
|
191
|
+
})
|
|
192
|
+
|
|
193
|
+
const encodedValue = items.join(", ")
|
|
194
|
+
acc[0][key] = encodedValue
|
|
195
|
+
acc[1][key.toLowerCase()] = "map"
|
|
196
|
+
} else {
|
|
197
|
+
// Has nested objects - needs multipart encoding
|
|
198
|
+
hbEncodeLift(value, storageKey, top)
|
|
199
|
+
// Add the original object to flattened so it can be processed
|
|
200
|
+
acc[0][key] = value
|
|
201
|
+
}
|
|
202
|
+
|
|
143
203
|
return acc
|
|
144
204
|
}
|
|
145
205
|
|
|
146
206
|
// leaf encode value
|
|
147
207
|
const [type, encoded] = hbEncodeValue(value)
|
|
148
208
|
if (encoded !== undefined) {
|
|
149
|
-
|
|
150
|
-
|
|
209
|
+
// For binary data, check the byte length directly without converting to string
|
|
210
|
+
const byteLength = isBytes(encoded)
|
|
211
|
+
? encoded.byteLength
|
|
212
|
+
: Buffer.from(String(encoded)).byteLength
|
|
213
|
+
|
|
214
|
+
if (byteLength > MAX_HEADER_LENGTH) {
|
|
215
|
+
// Store large values, but preserve binary data as-is
|
|
216
|
+
top[storageKey] = isBytes(encoded) ? encoded : String(encoded)
|
|
151
217
|
} else {
|
|
152
218
|
// Preserve the original key casing
|
|
153
219
|
const httpKey = key
|
|
@@ -172,12 +238,14 @@ function hbEncodeLift(obj, parent = "", top = {}) {
|
|
|
172
238
|
if (Object.keys(types).length > 0) {
|
|
173
239
|
// Format as structured fields dictionary
|
|
174
240
|
const aoTypeItems = Object.entries(types).map(([key, value]) => {
|
|
241
|
+
// The Erlang side expects keys with %2f for forward slashes
|
|
175
242
|
const safeKey = key
|
|
176
243
|
.toLowerCase()
|
|
177
244
|
.replace(
|
|
178
|
-
/[^a-z0-9_
|
|
245
|
+
/[^a-z0-9_\-.*\/]/g,
|
|
179
246
|
c => "%" + c.charCodeAt(0).toString(16).padStart(2, "0")
|
|
180
247
|
)
|
|
248
|
+
.replace(/\//g, "%2f") // Replace forward slashes AFTER other encoding
|
|
181
249
|
return `${safeKey}="${value}"`
|
|
182
250
|
})
|
|
183
251
|
aoTypeItems.sort()
|
|
@@ -200,8 +268,14 @@ function hbEncodeLift(obj, parent = "", top = {}) {
|
|
|
200
268
|
return top
|
|
201
269
|
}
|
|
202
270
|
|
|
203
|
-
function encodePart(name, { headers, body }) {
|
|
204
|
-
|
|
271
|
+
function encodePart(name, { headers = {}, body }) {
|
|
272
|
+
// Convert headers to a plain object if it's a Headers instance
|
|
273
|
+
const headerEntries =
|
|
274
|
+
headers instanceof Headers
|
|
275
|
+
? Array.from(headers.entries())
|
|
276
|
+
: Object.entries(headers || {})
|
|
277
|
+
|
|
278
|
+
const parts = headerEntries.reduce(
|
|
205
279
|
(acc, [name, value]) => {
|
|
206
280
|
acc.push(`${name}: `, value, "\r\n")
|
|
207
281
|
return acc
|
|
@@ -215,7 +289,7 @@ function encodePart(name, { headers, body }) {
|
|
|
215
289
|
}
|
|
216
290
|
|
|
217
291
|
async function encode(obj = {}) {
|
|
218
|
-
if (Object.keys(obj).length === 0) return
|
|
292
|
+
if (Object.keys(obj).length === 0) return { headers: {}, body: undefined }
|
|
219
293
|
|
|
220
294
|
// Keep reference to original object for data field
|
|
221
295
|
const originalObj = obj
|
|
@@ -239,11 +313,22 @@ async function encode(obj = {}) {
|
|
|
239
313
|
}
|
|
240
314
|
|
|
241
315
|
// Check if this should be a body field
|
|
316
|
+
if (isBytes(value)) {
|
|
317
|
+
// Binary data should always go to body
|
|
318
|
+
bodyKeys.push(key)
|
|
319
|
+
flattened[key] = new Blob([
|
|
320
|
+
`content-disposition: form-data;name="${key}"\r\n\r\n`,
|
|
321
|
+
new Uint8Array(value.buffer || value),
|
|
322
|
+
])
|
|
323
|
+
return
|
|
324
|
+
}
|
|
325
|
+
|
|
242
326
|
const valueStr = String(value)
|
|
243
327
|
if (
|
|
244
328
|
(await hasNewline(valueStr)) ||
|
|
245
329
|
key.includes("/") ||
|
|
246
|
-
Buffer.from(valueStr).byteLength > MAX_HEADER_LENGTH
|
|
330
|
+
Buffer.from(valueStr).byteLength > MAX_HEADER_LENGTH ||
|
|
331
|
+
(isPojo(value) && valueStr === "[object Object]") // Catch unencoded objects
|
|
247
332
|
) {
|
|
248
333
|
bodyKeys.push(key)
|
|
249
334
|
flattened[key] = new Blob([
|
|
@@ -276,37 +361,84 @@ async function encode(obj = {}) {
|
|
|
276
361
|
}
|
|
277
362
|
|
|
278
363
|
let body = undefined
|
|
364
|
+
let promoteToBody = true
|
|
279
365
|
if (bodyKeys.length > 0) {
|
|
280
366
|
if (bodyKeys.length === 1) {
|
|
281
367
|
// If there is only one element, promote it to be the full body
|
|
282
368
|
const bodyKey = bodyKeys[0]
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
369
|
+
const originalValue = originalObj[bodyKey]
|
|
370
|
+
const flattenedValue = flattened[bodyKey]
|
|
371
|
+
|
|
372
|
+
// Only promote if it's not a complex object
|
|
373
|
+
if (
|
|
374
|
+
!isPojo(originalValue) ||
|
|
375
|
+
(isPojo(originalValue) && typeof flattenedValue === "string")
|
|
376
|
+
) {
|
|
377
|
+
// For objects that were encoded as structured fields, use the encoded value
|
|
378
|
+
if (
|
|
379
|
+
(bodyKey === "body" || bodyKey === "data") &&
|
|
380
|
+
isPojo(originalValue) &&
|
|
381
|
+
typeof flattenedValue === "string"
|
|
382
|
+
) {
|
|
383
|
+
body = new Blob([flattenedValue])
|
|
384
|
+
} else {
|
|
385
|
+
body = new Blob([originalValue || flattenedValue])
|
|
386
|
+
}
|
|
387
|
+
headers["inline-body-key"] = bodyKey
|
|
388
|
+
} else {
|
|
389
|
+
// Complex object - don't promote, create multipart
|
|
390
|
+
promoteToBody = false
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
if (!promoteToBody || bodyKeys.length > 1) {
|
|
286
395
|
// Multiple body fields - create multipart
|
|
287
396
|
const bodyParts = await Promise.all(
|
|
288
|
-
bodyKeys.map(name => {
|
|
397
|
+
bodyKeys.map(async name => {
|
|
289
398
|
if (flattened[name] instanceof Blob) {
|
|
290
|
-
|
|
399
|
+
// The blob already has the content-disposition header
|
|
400
|
+
return flattened[name]
|
|
401
|
+
}
|
|
402
|
+
// For raw values, we need to create a proper multipart part
|
|
403
|
+
const value = originalObj[name] || flattened[name] || ""
|
|
404
|
+
|
|
405
|
+
// Special case: if this is a structured field encoded value, use the flattened value
|
|
406
|
+
if (
|
|
407
|
+
name === "body" &&
|
|
408
|
+
isPojo(originalObj[name]) &&
|
|
409
|
+
typeof flattened[name] === "string"
|
|
410
|
+
) {
|
|
411
|
+
const partBlob = new Blob([
|
|
412
|
+
`content-disposition: form-data;name="${name}"\r\n\r\n`,
|
|
413
|
+
flattened[name],
|
|
414
|
+
])
|
|
415
|
+
return partBlob
|
|
291
416
|
}
|
|
292
|
-
|
|
293
|
-
|
|
417
|
+
|
|
418
|
+
const partBlob = new Blob([
|
|
419
|
+
`content-disposition: form-data;name="${name}"\r\n\r\n`,
|
|
420
|
+
value,
|
|
421
|
+
])
|
|
422
|
+
return partBlob
|
|
294
423
|
})
|
|
295
424
|
)
|
|
296
425
|
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
)
|
|
301
|
-
)
|
|
302
|
-
const hash = await sha256(await base.arrayBuffer())
|
|
426
|
+
// Calculate boundary from the content
|
|
427
|
+
const allPartsBuffer = await new Blob(bodyParts).arrayBuffer()
|
|
428
|
+
const hash = await sha256(allPartsBuffer)
|
|
303
429
|
const boundary = base64url.encode(Buffer.from(hash))
|
|
304
430
|
|
|
305
|
-
|
|
306
|
-
|
|
431
|
+
// Build the multipart body with proper boundaries
|
|
432
|
+
const finalParts = []
|
|
433
|
+
for (const part of bodyParts) {
|
|
434
|
+
finalParts.push(`--${boundary}\r\n`)
|
|
435
|
+
finalParts.push(part)
|
|
436
|
+
finalParts.push("\r\n")
|
|
437
|
+
}
|
|
438
|
+
finalParts.push(`--${boundary}--`)
|
|
307
439
|
|
|
308
440
|
headers["content-type"] = `multipart/form-data; boundary="${boundary}"`
|
|
309
|
-
body = new Blob(
|
|
441
|
+
body = new Blob(finalParts)
|
|
310
442
|
}
|
|
311
443
|
|
|
312
444
|
if (body) {
|
|
@@ -447,6 +579,12 @@ export function createRequest(config) {
|
|
|
447
579
|
// Get all header keys for signing (lowercase)
|
|
448
580
|
const signingFields = Object.keys(lowercaseHeaders)
|
|
449
581
|
|
|
582
|
+
// If there are no fields to sign, add at least the content-length
|
|
583
|
+
if (signingFields.length === 0 && !body) {
|
|
584
|
+
lowercaseHeaders["content-length"] = "0"
|
|
585
|
+
signingFields.push("content-length")
|
|
586
|
+
}
|
|
587
|
+
|
|
450
588
|
// Sign the request with lowercase headers
|
|
451
589
|
const signedRequest = await toHttpSigner(signer)({
|
|
452
590
|
request: { url: _url, method, headers: lowercaseHeaders },
|
|
@@ -526,8 +664,15 @@ export async function send(signedMsg, fetchImpl = fetch) {
|
|
|
526
664
|
throw new Error(`${response.status}: ${await response.text()}`)
|
|
527
665
|
}
|
|
528
666
|
|
|
667
|
+
// Convert Headers object to plain object
|
|
668
|
+
let headers = {}
|
|
669
|
+
if (response.headers && typeof response.headers.forEach === "function") {
|
|
670
|
+
response.headers.forEach((v, k) => (headers[k] = v))
|
|
671
|
+
} else headers = response.headers
|
|
672
|
+
|
|
529
673
|
return {
|
|
530
|
-
|
|
674
|
+
response,
|
|
675
|
+
headers,
|
|
531
676
|
body: await response.text(),
|
|
532
677
|
status: response.status,
|
|
533
678
|
}
|
|
@@ -573,7 +718,7 @@ function extractSignatureName(headers) {
|
|
|
573
718
|
* @param {string} [signatureName] - Optional signature name to look for
|
|
574
719
|
* @returns {Buffer|null} Public key buffer or null
|
|
575
720
|
*/
|
|
576
|
-
function extractPublicKeyFromHeaders(headers, signatureName) {
|
|
721
|
+
export function extractPublicKeyFromHeaders(headers, signatureName) {
|
|
577
722
|
const signatureInput =
|
|
578
723
|
headers["signature-input"] || headers["Signature-Input"]
|
|
579
724
|
if (!signatureInput) return null
|
package/esm/utils.js
CHANGED
|
@@ -547,6 +547,7 @@ const toGraphObj = ({ query, variables }) => {
|
|
|
547
547
|
if (fields) args.fields = fields
|
|
548
548
|
if (args.sort && args.sort === "HEIGHT_ASC") args.asc = true
|
|
549
549
|
delete args.sort
|
|
550
|
+
if (!Array.isArray(args.tags)) args.tags = [args.tags]
|
|
550
551
|
if (args.tags) {
|
|
551
552
|
let _tags = {}
|
|
552
553
|
for (const v of args.tags) _tags[v.name] = v.values
|