hbsig 0.0.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.
Files changed (44) hide show
  1. package/cjs/bin_to_str.js +44 -0
  2. package/cjs/collect-body-keys.js +470 -0
  3. package/cjs/encode-array-item.js +110 -0
  4. package/cjs/encode-utils.js +236 -0
  5. package/cjs/encode.js +1318 -0
  6. package/cjs/erl_json.js +317 -0
  7. package/cjs/erl_str.js +1037 -0
  8. package/cjs/flat.js +222 -0
  9. package/cjs/http-message-signatures/httpbis.js +489 -0
  10. package/cjs/http-message-signatures/index.js +25 -0
  11. package/cjs/http-message-signatures/structured-header.js +129 -0
  12. package/cjs/httpsig.js +716 -0
  13. package/cjs/httpsig2.js +1160 -0
  14. package/cjs/id.js +470 -0
  15. package/cjs/index.js +63 -0
  16. package/cjs/send.js +194 -0
  17. package/cjs/signer-utils.js +617 -0
  18. package/cjs/signer.js +606 -0
  19. package/cjs/structured.js +296 -0
  20. package/cjs/test.js +27 -0
  21. package/cjs/utils.js +42 -0
  22. package/esm/bin_to_str.js +46 -0
  23. package/esm/collect-body-keys.js +436 -0
  24. package/esm/encode-array-item.js +112 -0
  25. package/esm/encode-utils.js +185 -0
  26. package/esm/encode.js +1219 -0
  27. package/esm/erl_json.js +289 -0
  28. package/esm/erl_str.js +1139 -0
  29. package/esm/flat.js +196 -0
  30. package/esm/http-message-signatures/httpbis.js +438 -0
  31. package/esm/http-message-signatures/index.js +4 -0
  32. package/esm/http-message-signatures/structured-header.js +105 -0
  33. package/esm/httpsig.js +658 -0
  34. package/esm/httpsig2.js +1097 -0
  35. package/esm/id.js +459 -0
  36. package/esm/index.js +4 -0
  37. package/esm/package.json +3 -0
  38. package/esm/send.js +124 -0
  39. package/esm/signer-utils.js +494 -0
  40. package/esm/signer.js +452 -0
  41. package/esm/structured.js +269 -0
  42. package/esm/test.js +6 -0
  43. package/esm/utils.js +28 -0
  44. package/package.json +28 -0
package/esm/flat.js ADDED
@@ -0,0 +1,196 @@
1
+ /**
2
+ * A codec for turning nested objects into/from flat objects that have
3
+ * (potentially multi-layer) paths as their keys, and values as their values.
4
+ */
5
+
6
+ /**
7
+ * Convert a flat map to a nested object.
8
+ * @param {Object|string} input - Either a flat object or a serialized string
9
+ * @returns {Object} - Nested object
10
+ */
11
+ export function flat_from(input) {
12
+ if (typeof input === "string" || input instanceof Buffer) {
13
+ // If input is binary/string, deserialize it first
14
+ return deserialize(input).ok
15
+ }
16
+
17
+ if (typeof input !== "object" || input === null) {
18
+ throw new Error("Input must be an object or string")
19
+ }
20
+
21
+ const result = {}
22
+
23
+ for (const [pathKey, value] of Object.entries(input)) {
24
+ const pathParts = pathToParts(pathKey)
25
+ injectAtPath(pathParts, value, result)
26
+ }
27
+
28
+ return result
29
+ }
30
+
31
+ /**
32
+ * Convert a nested object to a flat map.
33
+ * @param {Object|string} input - Either a nested object or a binary string (passthrough)
34
+ * @returns {Object|string} - Flat object or passthrough string
35
+ */
36
+ export function flat_to(input) {
37
+ if (typeof input === "string" || input instanceof Buffer) {
38
+ // Binary passthrough
39
+ return input
40
+ }
41
+
42
+ if (typeof input !== "object" || input === null) {
43
+ throw new Error("Input must be an object or string")
44
+ }
45
+
46
+ const result = {}
47
+ flattenRecursive(input, [], result)
48
+ return result
49
+ }
50
+
51
+ /**
52
+ * Helper function to convert a path string to path parts
53
+ * @param {string|Array} path - Path string like "a/b/c" or array of parts
54
+ * @returns {Array} - Array of path parts
55
+ */
56
+ function pathToParts(path) {
57
+ if (Array.isArray(path)) {
58
+ // Handle array paths
59
+ if (path.length === 1 && Array.isArray(path[0])) {
60
+ return path[0]
61
+ }
62
+ return path
63
+ }
64
+
65
+ if (typeof path === "string") {
66
+ // Split by '/' but handle edge cases
67
+ return path.split("/")
68
+ }
69
+
70
+ throw new Error("Path must be a string or array")
71
+ }
72
+
73
+ /**
74
+ * Helper function to inject a value at a specific path in a nested object
75
+ * @param {Array} pathParts - Array of path parts
76
+ * @param {*} value - Value to inject
77
+ * @param {Object} obj - Object to inject into
78
+ */
79
+ function injectAtPath(pathParts, value, obj) {
80
+ if (pathParts.length === 0) {
81
+ throw new Error("Path cannot be empty")
82
+ }
83
+
84
+ if (pathParts.length === 1) {
85
+ const key = pathParts[0]
86
+
87
+ if (key in obj) {
88
+ const existing = obj[key]
89
+
90
+ // If both are objects, merge them
91
+ if (
92
+ typeof existing === "object" &&
93
+ existing !== null &&
94
+ typeof value === "object" &&
95
+ value !== null &&
96
+ !Array.isArray(existing) &&
97
+ !Array.isArray(value)
98
+ ) {
99
+ obj[key] = { ...existing, ...value }
100
+ } else {
101
+ // Path collision
102
+ throw new Error(
103
+ `Path collision at key: ${key}, existing: ${JSON.stringify(existing)}, value: ${JSON.stringify(value)}`
104
+ )
105
+ }
106
+ } else {
107
+ obj[key] = value
108
+ }
109
+ return
110
+ }
111
+
112
+ const [key, ...rest] = pathParts
113
+
114
+ if (!(key in obj)) {
115
+ obj[key] = {}
116
+ } else if (typeof obj[key] !== "object" || obj[key] === null) {
117
+ throw new Error(`Cannot create nested path at non-object key: ${key}`)
118
+ }
119
+
120
+ injectAtPath(rest, value, obj[key])
121
+ }
122
+
123
+ /**
124
+ * Helper function to recursively flatten an object
125
+ * @param {*} value - Current value
126
+ * @param {Array} currentPath - Current path parts
127
+ * @param {Object} result - Result object to populate
128
+ */
129
+ function flattenRecursive(value, currentPath, result) {
130
+ if (typeof value === "object" && value !== null && !Array.isArray(value)) {
131
+ // It's an object, recurse into it
132
+ for (const [key, subValue] of Object.entries(value)) {
133
+ const newPath = [...currentPath, key]
134
+ flattenRecursive(subValue, newPath, result)
135
+ }
136
+ } else if (typeof value === "string") {
137
+ // It's a string leaf value (matching Erlang binary type)
138
+ if (currentPath.length === 0) {
139
+ throw new Error("Cannot flatten a non-object at root level")
140
+ }
141
+
142
+ const pathKey = currentPath.join("/")
143
+ result[pathKey] = value
144
+ } else {
145
+ // Non-string leaf values cause errors in Erlang codec
146
+ throw new Error(
147
+ `Value type ${typeof value} not supported. Only strings and objects are allowed.`
148
+ )
149
+ }
150
+ }
151
+
152
+ /**
153
+ * Serialize a map to a string format
154
+ * @param {Object} map - The map to serialize
155
+ * @returns {Object} - {ok: string} or {error: string}
156
+ */
157
+ function serialize(map) {
158
+ try {
159
+ const flattened = flat_to(map)
160
+ const lines = []
161
+
162
+ for (const [key, value] of Object.entries(flattened)) {
163
+ lines.push(`${key}: ${value}`)
164
+ }
165
+
166
+ return { ok: lines.join("\n") + (lines.length > 0 ? "\n" : "") }
167
+ } catch (error) {
168
+ return { error: error.message }
169
+ }
170
+ }
171
+
172
+ /**
173
+ * Deserialize a string to a map
174
+ * @param {string} input - The string to deserialize
175
+ * @returns {Object} - {ok: Object} or {error: string}
176
+ */
177
+ function deserialize(input) {
178
+ try {
179
+ const str = input.toString()
180
+ const lines = str.split("\n").filter(line => line.trim())
181
+ const flat = {}
182
+
183
+ for (const line of lines) {
184
+ const colonIndex = line.indexOf(": ")
185
+ if (colonIndex !== -1) {
186
+ const key = line.substring(0, colonIndex)
187
+ const value = line.substring(colonIndex + 2)
188
+ flat[key] = value
189
+ }
190
+ }
191
+
192
+ return { ok: flat_from(flat) }
193
+ } catch (error) {
194
+ return { error: error.message }
195
+ }
196
+ }
@@ -0,0 +1,438 @@
1
+ import {
2
+ parseDictionary,
3
+ parseItem,
4
+ serializeItem,
5
+ serializeList,
6
+ ByteSequence,
7
+ serializeDictionary,
8
+ parseList,
9
+ isInnerList,
10
+ isByteSequence,
11
+ Token,
12
+ } from "structured-headers"
13
+ import { Dictionary, parseHeader, quoteString } from "./structured-header.js"
14
+
15
+ // Default params if not provided
16
+ const defaultParams = ["created", "keyid", "alg", "expires"]
17
+
18
+ // Helper to check if message is a request
19
+ const isRequest = message => {
20
+ return message.method !== undefined
21
+ }
22
+
23
+ /**
24
+ * Components can be derived from requests or responses (which can also be bound to their request).
25
+ * The signature is essentially (component, params, signingSubject, supplementaryData)
26
+ *
27
+ * MODIFIED: This implementation matches Erlang behavior where @ components are treated as field lookups
28
+ */
29
+ export function deriveComponent(component, params, message, req) {
30
+ // MODIFIED: Match Erlang behavior - all @ components become field lookups
31
+ // The Erlang strips @ and calls extract_field for ALL derived components
32
+
33
+ // Remove @ prefix to match Erlang behavior
34
+ const fieldName = component.startsWith("@") ? component.slice(1) : component
35
+
36
+ // For all @ components, do a field lookup instead of deriving
37
+ // This matches the Erlang identifier_to_component behavior
38
+ if (component.startsWith("@")) {
39
+ try {
40
+ if (params.has("req") && req) {
41
+ return extractHeader(fieldName, params, req, undefined)
42
+ }
43
+ return extractHeader(fieldName, params, message, req)
44
+ } catch (e) {
45
+ // If field not found, return empty or throw based on component type
46
+ throw new Error(
47
+ `No field "${fieldName}" found for component "${component}"`
48
+ )
49
+ }
50
+ }
51
+
52
+ // This code path should not be reached for @ components anymore
53
+ // but keeping it for non-@ components
54
+ throw new Error(`Unsupported component "${component}"`)
55
+ }
56
+
57
+ export function extractHeader(header, params, messageOrHeaders, req) {
58
+ const headers = messageOrHeaders.headers || messageOrHeaders
59
+ const context = params.has("req") ? req?.headers : headers
60
+ if (!context) {
61
+ throw new Error("Missing request in request-response bound component")
62
+ }
63
+ const headerTuple = Object.entries(context).find(
64
+ ([name]) => name.toLowerCase() === header
65
+ )
66
+ if (!headerTuple) {
67
+ throw new Error(`No header "${header}" found in headers`)
68
+ }
69
+ const values = Array.isArray(headerTuple[1])
70
+ ? headerTuple[1]
71
+ : [headerTuple[1]]
72
+ if (params.has("bs") && (params.has("sf") || params.has("key"))) {
73
+ throw new Error("Cannot have both `bs` and (implicit) `sf` parameters")
74
+ }
75
+ if (params.has("sf") || params.has("key")) {
76
+ // strict encoding of field
77
+ const value = values.join(", ")
78
+ const parsed = parseHeader(value)
79
+ if (params.has("key") && !(parsed instanceof Dictionary)) {
80
+ throw new Error("Unable to parse header as dictionary")
81
+ }
82
+ if (params.has("key")) {
83
+ const key = params.get("key").toString()
84
+ if (!parsed.has(key)) {
85
+ throw new Error(`Unable to find key "${key}" in structured field`)
86
+ }
87
+ return [parsed.get(key)]
88
+ }
89
+ return [parsed.toString()]
90
+ }
91
+ if (params.has("bs")) {
92
+ return [
93
+ values
94
+ .map(val => {
95
+ const encoded = Buffer.from(val.trim().replace(/\n\s*/gm, " "))
96
+ return `:${encoded.toString("base64")}:`
97
+ })
98
+ .join(", "),
99
+ ]
100
+ }
101
+ // raw encoding
102
+ return [values.map(val => val.trim().replace(/\n\s*/gm, " ")).join(", ")]
103
+ }
104
+
105
+ function normaliseParams(params) {
106
+ const map = new Map()
107
+ params.forEach((value, key) => {
108
+ if (value instanceof ByteSequence) {
109
+ map.set(key, value.toBase64())
110
+ } else if (value instanceof Token) {
111
+ map.set(key, value.toString())
112
+ } else {
113
+ map.set(key, value)
114
+ }
115
+ })
116
+ return map
117
+ }
118
+
119
+ export function createSignatureBase(config, res, req) {
120
+ return config.fields.reduce((base, fieldName) => {
121
+ const [field, params] = parseItem(quoteString(fieldName))
122
+ const fieldParams = normaliseParams(params)
123
+ const lcFieldName = field.toLowerCase()
124
+ if (lcFieldName !== "@signature-params") {
125
+ let value = null
126
+ if (config.componentParser) {
127
+ value =
128
+ config.componentParser(lcFieldName, fieldParams, res, req) ?? null
129
+ }
130
+ if (value === null) {
131
+ value = field.startsWith("@")
132
+ ? deriveComponent(lcFieldName, fieldParams, res, req)
133
+ : extractHeader(lcFieldName, fieldParams, res, req)
134
+ }
135
+ base.push([serializeItem([field, params]), value])
136
+ }
137
+ return base
138
+ }, [])
139
+ }
140
+
141
+ export function formatSignatureBase(base) {
142
+ return base
143
+ .map(([key, value]) => {
144
+ const quotedKey = serializeItem(parseItem(quoteString(key)))
145
+
146
+ // MODIFIED: Special handling to match Erlang behavior
147
+ // If the key is "@path", format it as "path" (without @) in the signature base
148
+ let formattedKey = quotedKey
149
+ if (quotedKey === '"@path"') {
150
+ formattedKey = '"path"'
151
+ }
152
+
153
+ return value.map(val => `${formattedKey}: ${val}`).join("\n")
154
+ })
155
+ .join("\n")
156
+ }
157
+
158
+ export function createSigningParameters(config) {
159
+ const now = new Date()
160
+ return (config.params ?? defaultParams).reduce((params, paramName) => {
161
+ let value = ""
162
+ switch (paramName.toLowerCase()) {
163
+ case "created":
164
+ // created is optional but recommended. If created is supplied but is null, that's an explicit
165
+ // instruction to *not* include the created parameter
166
+ if (config.paramValues?.created !== null) {
167
+ const created = config.paramValues?.created ?? now
168
+ value = Math.floor(created.getTime() / 1000)
169
+ }
170
+ break
171
+ case "expires":
172
+ // attempt to obtain an explicit expires time, otherwise create one that is 300 seconds after
173
+ // creation. Don't add an expires time if there is no created time
174
+ if (
175
+ config.paramValues?.expires ||
176
+ config.paramValues?.created !== null
177
+ ) {
178
+ const expires =
179
+ config.paramValues?.expires ??
180
+ new Date((config.paramValues?.created ?? now).getTime() + 300000)
181
+ value = Math.floor(expires.getTime() / 1000)
182
+ }
183
+ break
184
+ case "keyid": {
185
+ // attempt to obtain the keyid omit if missing
186
+ const kid = config.paramValues?.keyid ?? config.key.id ?? null
187
+ if (kid) {
188
+ value = kid.toString()
189
+ }
190
+ break
191
+ }
192
+ case "alg": {
193
+ // if there is no alg, but it's listed as a required parameter, we should probably
194
+ // throw an error - the problem is that if it's in the default set of params, do we
195
+ // really want to throw if there's no keyid?
196
+ const alg = config.paramValues?.alg ?? config.key.alg ?? null
197
+ if (alg) {
198
+ value = alg.toString()
199
+ }
200
+ break
201
+ }
202
+ default:
203
+ if (config.paramValues?.[paramName] instanceof Date) {
204
+ value = Math.floor(config.paramValues[paramName].getTime() / 1000)
205
+ } else if (config.paramValues?.[paramName]) {
206
+ value = config.paramValues[paramName]
207
+ }
208
+ }
209
+ if (value) {
210
+ params.set(paramName, value)
211
+ }
212
+ return params
213
+ }, new Map())
214
+ }
215
+
216
+ export function augmentHeaders(headers, signature, signatureInput, name) {
217
+ let signatureHeaderName = "Signature"
218
+ let signatureInputHeaderName = "Signature-Input"
219
+ let signatureHeader = new Map()
220
+ let inputHeader = new Map()
221
+ // check to see if there are already signature/signature-input headers
222
+ // if there are we want to store the current (case-sensitive) name of the header
223
+ // and we want to parse out the current values so we can append our new signature
224
+ for (const header in headers) {
225
+ switch (header.toLowerCase()) {
226
+ case "signature": {
227
+ signatureHeaderName = header
228
+ signatureHeader = parseDictionary(
229
+ Array.isArray(headers[header])
230
+ ? headers[header].join(", ")
231
+ : headers[header]
232
+ )
233
+ break
234
+ }
235
+ case "signature-input":
236
+ signatureInputHeaderName = header
237
+ inputHeader = parseDictionary(
238
+ Array.isArray(headers[header])
239
+ ? headers[header].join(", ")
240
+ : headers[header]
241
+ )
242
+ break
243
+ }
244
+ }
245
+ // find a unique signature name for the header. Check if any existing headers already use
246
+ // the name we intend to use, if there are, add incrementing numbers to the signature name
247
+ // until we have a unique name to use
248
+ let signatureName = name ?? "sig"
249
+ if (signatureHeader.has(signatureName) || inputHeader.has(signatureName)) {
250
+ let count = 0
251
+ while (
252
+ signatureHeader.has(`${signatureName}${count}`) ||
253
+ inputHeader.has(`${signatureName}${count}`)
254
+ ) {
255
+ count++
256
+ }
257
+ signatureName += count.toString()
258
+ }
259
+ // append our signature and signature-inputs to the headers and return
260
+ signatureHeader.set(signatureName, [
261
+ new ByteSequence(signature.toString("base64")),
262
+ new Map(),
263
+ ])
264
+ inputHeader.set(signatureName, parseList(signatureInput)[0])
265
+ return {
266
+ ...headers,
267
+ [signatureHeaderName]: serializeDictionary(signatureHeader),
268
+ [signatureInputHeaderName]: serializeDictionary(inputHeader),
269
+ }
270
+ }
271
+
272
+ export async function signMessage(config, message, req) {
273
+ const signingParameters = createSigningParameters(config)
274
+ const signatureBase = createSignatureBase(
275
+ {
276
+ fields: config.fields ?? [],
277
+ componentParser: config.componentParser,
278
+ },
279
+ message,
280
+ req
281
+ )
282
+ const signatureInput = serializeList([
283
+ [signatureBase.map(([item]) => parseItem(item)), signingParameters],
284
+ ])
285
+ signatureBase.push(['"@signature-params"', [signatureInput]])
286
+ const base = formatSignatureBase(signatureBase)
287
+ // call sign
288
+ const signature = await config.key.sign(Buffer.from(base))
289
+ return {
290
+ ...message,
291
+ headers: augmentHeaders(
292
+ { ...message.headers },
293
+ signature,
294
+ signatureInput,
295
+ config.name
296
+ ),
297
+ }
298
+ }
299
+
300
+ export async function verifyMessage(config, message, req) {
301
+ const { signatures, signatureInputs } = Object.entries(
302
+ message.headers
303
+ ).reduce((accum, [name, value]) => {
304
+ switch (name.toLowerCase()) {
305
+ case "signature":
306
+ return Object.assign(accum, {
307
+ signatures: parseDictionary(
308
+ Array.isArray(value) ? value.join(", ") : value
309
+ ),
310
+ })
311
+ case "signature-input":
312
+ return Object.assign(accum, {
313
+ signatureInputs: parseDictionary(
314
+ Array.isArray(value) ? value.join(", ") : value
315
+ ),
316
+ })
317
+ default:
318
+ return accum
319
+ }
320
+ }, {})
321
+ // no signatures means an indeterminate result
322
+ if (!signatures?.size && !signatureInputs?.size) {
323
+ return null
324
+ }
325
+ // a missing header means we can't verify the signatures
326
+ if (!signatures?.size || !signatureInputs?.size) {
327
+ throw new Error("Incomplete signature headers")
328
+ }
329
+ const now = Math.floor(Date.now() / 1000)
330
+ const tolerance = config.tolerance ?? 0
331
+ const notAfter =
332
+ config.notAfter instanceof Date
333
+ ? Math.floor(config.notAfter.getTime() / 1000)
334
+ : (config.notAfter ?? now)
335
+ const maxAge = config.maxAge ?? null
336
+ const requiredParams = config.requiredParams ?? []
337
+ const requiredFields = config.requiredFields ?? []
338
+ return Array.from(signatureInputs.entries()).reduce(
339
+ async (prev, [name, input]) => {
340
+ const signatureParams = Array.from(input[1].entries()).reduce(
341
+ (params, [key, value]) => {
342
+ if (value instanceof ByteSequence) {
343
+ Object.assign(params, {
344
+ [key]: value.toBase64(),
345
+ })
346
+ } else if (value instanceof Token) {
347
+ Object.assign(params, {
348
+ [key]: value.toString(),
349
+ })
350
+ } else if (key === "created" || key === "expired") {
351
+ Object.assign(params, {
352
+ [key]: new Date(value * 1000),
353
+ })
354
+ } else {
355
+ Object.assign(params, {
356
+ [key]: value,
357
+ })
358
+ }
359
+ return params
360
+ },
361
+ {}
362
+ )
363
+ const [result, key] = await Promise.all([
364
+ prev.catch(e => e),
365
+ config.keyLookup(signatureParams),
366
+ ])
367
+ // @todo - confirm this is all working as expected
368
+ if (config.all && !key) {
369
+ throw new Error("Unknown key")
370
+ }
371
+ if (!key) {
372
+ if (result instanceof Error) {
373
+ throw result
374
+ }
375
+ return result
376
+ }
377
+ if (
378
+ input[1].has("alg") &&
379
+ key.algs?.includes(input[1].get("alg")) === false
380
+ ) {
381
+ throw new Error("Unsupported key algorithm")
382
+ }
383
+ if (!isInnerList(input)) {
384
+ throw new Error("Malformed signature input")
385
+ }
386
+ const hasRequiredParams = requiredParams.every(param =>
387
+ input[1].has(param)
388
+ )
389
+ if (!hasRequiredParams) {
390
+ throw new Error("Missing required signature parameters")
391
+ }
392
+ // this could be tricky, what if we say "@method" but there is "@method;req"
393
+ const hasRequiredFields = requiredFields.every(field =>
394
+ input[0].some(([fieldName]) => fieldName === field)
395
+ )
396
+ if (!hasRequiredFields) {
397
+ throw new Error("Missing required signed fields")
398
+ }
399
+ if (input[1].has("created")) {
400
+ const created = input[1].get("created") - tolerance
401
+ // maxAge overrides expires.
402
+ // signature is older than maxAge
403
+ if ((maxAge && now - created > maxAge) || created > notAfter) {
404
+ throw new Error("Signature is too old")
405
+ }
406
+ }
407
+ if (input[1].has("expires")) {
408
+ const expires = input[1].get("expires") + tolerance
409
+ // expired signature
410
+ if (now > expires) {
411
+ throw new Error("Signature has expired")
412
+ }
413
+ }
414
+ // now look to verify the signature! Build the expected "signing base" and verify it!
415
+ const fields = input[0].map(item => serializeItem(item))
416
+ const signingBase = createSignatureBase(
417
+ { fields, componentParser: config.componentParser },
418
+ message,
419
+ req
420
+ )
421
+ signingBase.push(['"@signature-params"', [serializeList([input])]])
422
+ const base = formatSignatureBase(signingBase)
423
+ const signature = signatures.get(name)
424
+ if (!signature) {
425
+ throw new Error("No corresponding signature for input")
426
+ }
427
+ if (!isByteSequence(signature[0])) {
428
+ throw new Error("Malformed signature")
429
+ }
430
+ return key.verify(
431
+ Buffer.from(base),
432
+ Buffer.from(signature[0].toBase64(), "base64"),
433
+ signatureParams
434
+ )
435
+ },
436
+ Promise.resolve(null)
437
+ )
438
+ }
@@ -0,0 +1,4 @@
1
+ export * from "./httpbis.js"
2
+ import * as httpbis from "./httpbis.js"
3
+ export { httpbis }
4
+ export { httpbis as default }