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/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
- // first/second lift object
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
- hbEncodeLift(value, storageKey, top)
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
- if (Buffer.from(String(encoded)).byteLength > MAX_HEADER_LENGTH) {
150
- top[storageKey] = String(encoded)
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_-]/g,
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
- const parts = Object.entries(Object.fromEntries(headers)).reduce(
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
- body = new Blob([originalObj[bodyKey] || flattened[bodyKey]])
284
- headers["inline-body-key"] = bodyKey
285
- } else {
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
- return flattened[name].arrayBuffer()
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
- // For raw values, create a blob
293
- return new Blob([originalObj[name] || ""]).arrayBuffer()
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
- const base = new Blob(
298
- bodyParts.flatMap((p, i, arr) =>
299
- i < arr.length - 1 ? [p, "\r\n"] : [p]
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
- const blobParts = bodyParts.flatMap(p => [`--${boundary}\r\n`, p, "\r\n"])
306
- blobParts.push(`--${boundary}--`)
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(blobParts)
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
- headers: response.headers,
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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wao",
3
- "version": "0.24.0",
3
+ "version": "0.24.2",
4
4
  "bin": {
5
5
  "wao": "./cjs/cli.js",
6
6
  "wao-esm": "./esm/cli.js"