wao 0.21.2 → 0.22.0

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/signer2.js ADDED
@@ -0,0 +1,762 @@
1
+ import base64url from "base64url"
2
+ import crypto from "crypto"
3
+ import { httpbis } from "http-message-signatures"
4
+ import { parseItem, serializeList } from "structured-headers"
5
+ const { verifyMessage } = httpbis
6
+ const {
7
+ augmentHeaders,
8
+ createSignatureBase,
9
+ createSigningParameters,
10
+ formatSignatureBase,
11
+ } = httpbis
12
+
13
+ /**
14
+ * Convert value to Buffer
15
+ */
16
+ const toView = value => {
17
+ if (ArrayBuffer.isView(value)) {
18
+ return Buffer.from(value.buffer, value.byteOffset, value.byteLength)
19
+ } else if (typeof value === "string") {
20
+ return base64url.toBuffer(value)
21
+ }
22
+ throw new Error(
23
+ "Value must be Uint8Array, ArrayBuffer, or base64url-encoded string"
24
+ )
25
+ }
26
+
27
+ /**
28
+ * Generate HTTP signature name from address
29
+ */
30
+ const httpSigName = address => {
31
+ const decoded = base64url.toBuffer(address)
32
+ const hexString = [...decoded.subarray(1, 9)]
33
+ .map(byte => byte.toString(16).padStart(2, "0"))
34
+ .join("")
35
+ return `http-sig-${hexString}`
36
+ }
37
+
38
+ /**
39
+ * Join URL parts
40
+ */
41
+ const joinUrl = ({ url, path }) => {
42
+ // If path is already a full URL, return it as-is
43
+ if (path.startsWith("http://") || path.startsWith("https://")) {
44
+ return path
45
+ }
46
+
47
+ // Otherwise, join the base URL with the path
48
+ return url.endsWith("/") ? url.slice(0, -1) + path : url + path
49
+ }
50
+
51
+ /**
52
+ * HyperBEAM Encoding Logic
53
+ */
54
+ const MAX_HEADER_LENGTH = 4096
55
+
56
+ async function hasNewline(value) {
57
+ if (typeof value === "string") return value.includes("\n")
58
+ if (value instanceof Blob) {
59
+ value = await value.text()
60
+ return value.includes("\n")
61
+ }
62
+ if (isBytes(value)) return Buffer.from(value).includes("\n")
63
+ return false
64
+ }
65
+
66
+ async function sha256(data) {
67
+ return crypto.subtle.digest("SHA-256", data)
68
+ }
69
+
70
+ function isBytes(value) {
71
+ return value instanceof ArrayBuffer || ArrayBuffer.isView(value)
72
+ }
73
+
74
+ function isPojo(value) {
75
+ return (
76
+ !isBytes(value) &&
77
+ !Array.isArray(value) &&
78
+ !(value instanceof Blob) &&
79
+ typeof value === "object" &&
80
+ value !== null
81
+ )
82
+ }
83
+
84
+ function hbEncodeValue(value) {
85
+ if (isBytes(value)) {
86
+ if (value.byteLength === 0) return hbEncodeValue("")
87
+ return [undefined, value]
88
+ }
89
+
90
+ if (typeof value === "string") {
91
+ if (value.length === 0) return [undefined, "empty-binary"]
92
+ return [undefined, value]
93
+ }
94
+
95
+ if (Array.isArray(value)) {
96
+ if (value.length === 0) return ["empty-list", undefined]
97
+ // For structured fields, just join the string values
98
+ const encoded = value
99
+ .map(v => {
100
+ if (typeof v === "string") {
101
+ // Escape quotes and backslashes
102
+ const escaped = v.replace(/\\/g, "\\\\").replace(/"/g, '\\"')
103
+ return `"${escaped}"`
104
+ }
105
+ return `"${String(v)}"`
106
+ })
107
+ .join(", ")
108
+ return ["list", encoded]
109
+ }
110
+
111
+ if (typeof value === "number") {
112
+ if (!Number.isInteger(value)) return ["float", `${value}`]
113
+ return ["integer", String(value)]
114
+ }
115
+
116
+ if (typeof value === "symbol") {
117
+ return ["atom", value.description]
118
+ }
119
+
120
+ throw new Error(`Cannot encode value: ${value.toString()}`)
121
+ }
122
+
123
+ function hbEncodeLift(obj, parent = "", top = {}) {
124
+ const [flattened, types] = Object.entries({ ...obj }).reduce(
125
+ (acc, [key, value]) => {
126
+ // For nested paths, preserve casing. For top-level, also preserve casing
127
+ const storageKey = parent ? `${parent}/${key}` : key
128
+
129
+ // skip nullish values
130
+ if (value == null) return acc
131
+
132
+ // list of objects
133
+ if (Array.isArray(value) && value.some(isPojo)) {
134
+ value = value.reduce(
135
+ (indexedObj, v, idx) => Object.assign(indexedObj, { [idx]: v }),
136
+ {}
137
+ )
138
+ }
139
+
140
+ // first/second lift object
141
+ if (isPojo(value)) {
142
+ hbEncodeLift(value, storageKey, top)
143
+ return acc
144
+ }
145
+
146
+ // leaf encode value
147
+ const [type, encoded] = hbEncodeValue(value)
148
+ if (encoded !== undefined) {
149
+ if (Buffer.from(String(encoded)).byteLength > MAX_HEADER_LENGTH) {
150
+ top[storageKey] = String(encoded)
151
+ } else {
152
+ // Preserve the original key casing
153
+ const httpKey = key
154
+ if (type === "integer" && typeof value === "number") {
155
+ acc[0][httpKey] = String(value)
156
+ } else {
157
+ acc[0][httpKey] = encoded
158
+ }
159
+ }
160
+ }
161
+ if (type) {
162
+ // Store type with lowercase key for ao-types dictionary
163
+ acc[1][key.toLowerCase()] = type
164
+ }
165
+ return acc
166
+ },
167
+ [{}, {}]
168
+ )
169
+
170
+ if (Object.keys(flattened).length === 0) return top
171
+
172
+ if (Object.keys(types).length > 0) {
173
+ // Format as structured fields dictionary
174
+ const aoTypeItems = Object.entries(types).map(([key, value]) => {
175
+ const safeKey = key
176
+ .toLowerCase()
177
+ .replace(
178
+ /[^a-z0-9_-]/g,
179
+ c => "%" + c.charCodeAt(0).toString(16).padStart(2, "0")
180
+ )
181
+ return `${safeKey}="${value}"`
182
+ })
183
+ aoTypeItems.sort()
184
+ const aoTypes = aoTypeItems.join(", ")
185
+
186
+ if (Buffer.from(aoTypes).byteLength > MAX_HEADER_LENGTH) {
187
+ const flatK = parent ? `${parent}/ao-types` : "ao-types"
188
+ top[flatK] = aoTypes
189
+ } else {
190
+ flattened["ao-types"] = aoTypes
191
+ }
192
+ }
193
+
194
+ if (parent) {
195
+ top[parent] = flattened
196
+ } else {
197
+ Object.assign(top, flattened)
198
+ }
199
+
200
+ return top
201
+ }
202
+
203
+ function encodePart(name, { headers, body }) {
204
+ const parts = Object.entries(Object.fromEntries(headers)).reduce(
205
+ (acc, [name, value]) => {
206
+ acc.push(`${name}: `, value, "\r\n")
207
+ return acc
208
+ },
209
+ [`content-disposition: form-data;name="${name}"\r\n`]
210
+ )
211
+
212
+ if (body) parts.push("\r\n", body)
213
+
214
+ return new Blob(parts)
215
+ }
216
+
217
+ async function encode(obj = {}) {
218
+ if (Object.keys(obj).length === 0) return
219
+
220
+ // Keep reference to original object for data field
221
+ const originalObj = obj
222
+ const flattened = hbEncodeLift(obj)
223
+
224
+ const bodyKeys = []
225
+ const headerKeys = []
226
+
227
+ // Process all flattened keys
228
+ await Promise.all(
229
+ Object.keys(flattened).map(async key => {
230
+ const value = flattened[key]
231
+
232
+ if (isPojo(value)) {
233
+ const subPart = await encode(value)
234
+ if (!subPart) return
235
+
236
+ bodyKeys.push(key)
237
+ flattened[key] = encodePart(key, subPart)
238
+ return
239
+ }
240
+
241
+ // Check if this should be a body field
242
+ const valueStr = String(value)
243
+ if (
244
+ (await hasNewline(valueStr)) ||
245
+ key.includes("/") ||
246
+ Buffer.from(valueStr).byteLength > MAX_HEADER_LENGTH
247
+ ) {
248
+ bodyKeys.push(key)
249
+ flattened[key] = new Blob([
250
+ `content-disposition: form-data;name="${key}"\r\n\r\n`,
251
+ value,
252
+ ])
253
+ return
254
+ }
255
+
256
+ // It's a header
257
+ headerKeys.push(key)
258
+ })
259
+ )
260
+
261
+ // Build headers object with all header keys
262
+ const headers = {}
263
+ headerKeys.forEach(key => {
264
+ headers[key] = flattened[key]
265
+ })
266
+
267
+ // Special handling for data and body fields
268
+ if ("data" in originalObj && !bodyKeys.includes("data")) {
269
+ bodyKeys.push("data")
270
+ delete headers["data"] // Remove from headers if it was there
271
+ }
272
+
273
+ if ("body" in originalObj && !bodyKeys.includes("body")) {
274
+ bodyKeys.push("body")
275
+ delete headers["body"] // Remove from headers if it was there
276
+ }
277
+
278
+ let body = undefined
279
+ if (bodyKeys.length > 0) {
280
+ if (bodyKeys.length === 1) {
281
+ // If there is only one element, promote it to be the full body
282
+ const bodyKey = bodyKeys[0]
283
+ body = new Blob([originalObj[bodyKey] || flattened[bodyKey]])
284
+ headers["inline-body-key"] = bodyKey
285
+ } else {
286
+ // Multiple body fields - create multipart
287
+ const bodyParts = await Promise.all(
288
+ bodyKeys.map(name => {
289
+ if (flattened[name] instanceof Blob) {
290
+ return flattened[name].arrayBuffer()
291
+ }
292
+ // For raw values, create a blob
293
+ return new Blob([originalObj[name] || ""]).arrayBuffer()
294
+ })
295
+ )
296
+
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())
303
+ const boundary = base64url.encode(Buffer.from(hash))
304
+
305
+ const blobParts = bodyParts.flatMap(p => [`--${boundary}\r\n`, p, "\r\n"])
306
+ blobParts.push(`--${boundary}--`)
307
+
308
+ headers["content-type"] = `multipart/form-data; boundary="${boundary}"`
309
+ body = new Blob(blobParts)
310
+ }
311
+
312
+ if (body) {
313
+ const finalContent = await body.arrayBuffer()
314
+ const contentDigest = await sha256(finalContent)
315
+ const base64 = base64url.toBase64(base64url.encode(contentDigest))
316
+
317
+ // Use lowercase to match what's in the other headers
318
+ headers["content-digest"] = `sha-256=:${base64}:`
319
+ headers["content-length"] = String(finalContent.byteLength)
320
+ }
321
+ }
322
+
323
+ return { headers, body }
324
+ }
325
+
326
+ /**
327
+ * Create HTTP signer wrapper
328
+ */
329
+ const toHttpSigner = signer => {
330
+ const params = ["alg", "keyid"].sort()
331
+
332
+ return async ({ request, fields }) => {
333
+ let signatureBase
334
+ let signatureInput
335
+ let createCalled = false
336
+
337
+ const create = injected => {
338
+ createCalled = true
339
+
340
+ const { publicKey, alg = "rsa-pss-sha512" } = injected
341
+
342
+ const publicKeyBuffer = toView(publicKey)
343
+
344
+ const signingParameters = createSigningParameters({
345
+ params,
346
+ paramValues: {
347
+ keyid: base64url.encode(publicKeyBuffer),
348
+ alg,
349
+ },
350
+ })
351
+
352
+ const signatureBaseArray = createSignatureBase({ fields }, request)
353
+ signatureInput = serializeList([
354
+ [
355
+ signatureBaseArray.map(([item]) => parseItem(item)),
356
+ signingParameters,
357
+ ],
358
+ ])
359
+
360
+ signatureBaseArray.push(['"@signature-params"', [signatureInput]])
361
+ signatureBase = formatSignatureBase(signatureBaseArray)
362
+
363
+ return new TextEncoder().encode(signatureBase)
364
+ }
365
+
366
+ const result = await signer(create, "httpsig")
367
+
368
+ if (!createCalled) {
369
+ throw new Error(
370
+ "create() must be invoked in order to construct the data to sign"
371
+ )
372
+ }
373
+
374
+ if (!result.signature || !result.address) {
375
+ throw new Error("Signer must return signature and address")
376
+ }
377
+
378
+ const signatureBuffer = toView(result.signature)
379
+ const signedHeaders = augmentHeaders(
380
+ request.headers,
381
+ signatureBuffer,
382
+ signatureInput,
383
+ httpSigName(result.address)
384
+ )
385
+
386
+ // Only lowercase the signature headers
387
+ const finalHeaders = {}
388
+ for (const [key, value] of Object.entries(signedHeaders)) {
389
+ if (key === "Signature" || key === "Signature-Input") {
390
+ finalHeaders[key.toLowerCase()] = value
391
+ } else {
392
+ finalHeaders[key] = value
393
+ }
394
+ }
395
+
396
+ return {
397
+ ...request,
398
+ headers: finalHeaders,
399
+ }
400
+ }
401
+ }
402
+
403
+ /**
404
+ * Create the main request function that creates signed messages locally
405
+ *
406
+ * @param {Object} config - Configuration object
407
+ * @param {Function} config.signer - Signer function
408
+ * @param {string} [config.HB_URL='http://relay.ao-hb.xyz'] - Base URL
409
+ * @returns {Function} Request function that takes tags and returns signed message
410
+ */
411
+ export function createRequest(config) {
412
+ const { signer, url = "http://localhost:10001" } = config
413
+
414
+ if (!signer) {
415
+ throw new Error("Signer is required for mainnet mode")
416
+ }
417
+
418
+ /**
419
+ * Create a signed message with tags (no network request)
420
+ *
421
+ * @param {Object} fields - Request fields (tags)
422
+ * @param {string} [fields.path='/relay/process'] - Path to append to base URL
423
+ * @param {string} [fields.method='POST'] - HTTP method
424
+ * @returns {Promise<Object>} Signed message object
425
+ */
426
+ return async function request(fields) {
427
+ const { path = "/relay/process", method = "POST", ...restFields } = fields
428
+
429
+ // Add default AO fields
430
+ const aoFields = { ...restFields }
431
+
432
+ // Use the HyperBEAM encode function
433
+ const encoded = await encode(aoFields)
434
+
435
+ // If no encoding needed, create minimal structure
436
+ const headersObj = encoded ? encoded.headers : {}
437
+ const body = encoded ? encoded.body : undefined
438
+
439
+ const _url = joinUrl({ url, path })
440
+
441
+ // Add Content-Length if body exists
442
+ if (body && !headersObj["content-length"]) {
443
+ const bodySize = body.size || body.byteLength || 0
444
+ if (bodySize > 0) {
445
+ headersObj["content-length"] = String(bodySize)
446
+ }
447
+ }
448
+
449
+ // Get all header keys for signing (might be empty)
450
+ // Convert to lowercase for HTTP signature spec compliance
451
+ const signingFields = Object.keys(headersObj).map(key => key.toLowerCase())
452
+
453
+ // Sign the request (even with no fields)
454
+ const signedRequest = await toHttpSigner(signer)({
455
+ request: { url, method, headers: headersObj },
456
+ fields: signingFields,
457
+ })
458
+
459
+ // Return the signed message
460
+ const result = {
461
+ url: _url,
462
+ method,
463
+ headers: signedRequest.headers,
464
+ }
465
+
466
+ // Only add body if it exists
467
+ if (body) {
468
+ result.body = body
469
+ }
470
+
471
+ return result
472
+ }
473
+ }
474
+
475
+ /**
476
+ * Utility function to extract the message ID from a signed message
477
+ * Based on the original code's hash calculation
478
+ */
479
+ async function getMessageId(signedMessage) {
480
+ // Extract signature from the Signature header
481
+ const signatureHeader =
482
+ signedMessage.headers.Signature || signedMessage.headers.signature
483
+ const match = signatureHeader.match(/Signature:\s*'http-sig-[^:]+:([^']+)'/)
484
+ const signature = match ? match[1] : null
485
+
486
+ if (!signature) {
487
+ throw new Error("Could not extract signature from headers")
488
+ }
489
+
490
+ // Hash the signature to get message ID
491
+ const encoder = new TextEncoder()
492
+ const data = encoder.encode(signature)
493
+ const hashBuffer = await crypto.subtle.digest("SHA-256", data)
494
+ const hashArray = Array.from(new Uint8Array(hashBuffer))
495
+ const hashBase64 = btoa(String.fromCharCode(...hashArray))
496
+
497
+ return hashBase64
498
+ }
499
+
500
+ export async function send(signedMsg, fetchImpl = fetch) {
501
+ console.log("signed....", signedMsg)
502
+ const fetchOptions = {
503
+ method: signedMsg.method,
504
+ headers: signedMsg.headers,
505
+ redirect: "follow",
506
+ }
507
+
508
+ // Only add body if it exists and method supports it
509
+ if (
510
+ signedMsg.body !== undefined &&
511
+ signedMsg.method !== "GET" &&
512
+ signedMsg.method !== "HEAD"
513
+ ) {
514
+ fetchOptions.body = signedMsg.body
515
+ }
516
+ const response = await fetchImpl(signedMsg.url, fetchOptions)
517
+
518
+ if (response.status >= 400) {
519
+ throw new Error(`${response.status}: ${await response.text()}`)
520
+ }
521
+
522
+ return {
523
+ headers: response.headers,
524
+ body: await response.text(),
525
+ status: response.status,
526
+ }
527
+ }
528
+
529
+ /**
530
+ * Convert JWK modulus (n) to PEM format public key
531
+ * @param {Buffer} nBuffer - The modulus buffer
532
+ * @returns {string} PEM formatted public key
533
+ */
534
+ function jwkModulusToPem(nBuffer) {
535
+ // RSA public key with standard exponent
536
+ const rsaPublicKey = crypto.createPublicKey({
537
+ key: {
538
+ kty: "RSA",
539
+ n: base64url.encode(nBuffer),
540
+ e: "AQAB", // Standard exponent 65537
541
+ },
542
+ format: "jwk",
543
+ })
544
+
545
+ return rsaPublicKey.export({ type: "spki", format: "pem" })
546
+ }
547
+
548
+ /**
549
+ * Extract signature name from headers
550
+ * @param {Object} headers - Request headers
551
+ * @returns {string|null} Signature name or null
552
+ */
553
+ function extractSignatureName(headers) {
554
+ const signatureHeader = headers["signature"] || headers["Signature"]
555
+ if (!signatureHeader) return null
556
+
557
+ // Extract signature name (e.g., "http-sig-xxxxxxxx")
558
+ // Handle both "name:" and "name=" formats
559
+ const match = signatureHeader.match(/^([^:=]+)[:=]/)
560
+ return match ? match[1] : null
561
+ }
562
+
563
+ /**
564
+ * Extract public key from signature-input header
565
+ * @param {Object} headers - Request headers
566
+ * @param {string} [signatureName] - Optional signature name to look for
567
+ * @returns {Buffer|null} Public key buffer or null
568
+ */
569
+ function extractPublicKeyFromHeaders(headers, signatureName) {
570
+ const signatureInput =
571
+ headers["signature-input"] || headers["Signature-Input"]
572
+ if (!signatureInput) return null
573
+
574
+ // If we have a signature name, look for its specific keyid
575
+ let keyidMatch
576
+ if (signatureName) {
577
+ // The signature-input format is: signatureName=(...);alg="...";keyid="..."
578
+ // We need to match after the signature name
579
+ const signatureSection = signatureInput.substring(
580
+ signatureInput.indexOf(signatureName)
581
+ )
582
+ keyidMatch = signatureSection.match(/keyid="([^"]+)"/)
583
+ } else {
584
+ // General keyid match
585
+ keyidMatch = signatureInput.match(/keyid="([^"]+)"/)
586
+ }
587
+
588
+ if (!keyidMatch) return null
589
+
590
+ try {
591
+ return base64url.toBuffer(keyidMatch[1])
592
+ } catch (error) {
593
+ return null
594
+ }
595
+ }
596
+
597
+ /**
598
+ * Verify an HTTP signed message using http-message-signatures
599
+ *
600
+ * @param {Object} signedMessage - The signed message to verify
601
+ * @param {string} signedMessage.url - Request URL
602
+ * @param {string} signedMessage.method - HTTP method
603
+ * @param {Object} signedMessage.headers - Headers including signature
604
+ * @param {string} [signedMessage.body] - Request body
605
+ * @param {string|Buffer} [publicKey] - Optional public key (if not provided, extracts from keyid)
606
+ * @returns {Object} Verification result
607
+ */
608
+ export async function verify(signedMessage, publicKey) {
609
+ try {
610
+ const { url, method, headers, body } = signedMessage
611
+
612
+ // Determine which public key to use
613
+ let keyLookup
614
+
615
+ if (publicKey) {
616
+ // Use provided public key
617
+ const pem =
618
+ typeof publicKey === "string" ? publicKey : jwkModulusToPem(publicKey)
619
+
620
+ keyLookup = async keyId => {
621
+ return {
622
+ id: keyId,
623
+ algs: ["rsa-pss-sha512", "rsa-pss-sha256", "rsa-v1_5-sha256"],
624
+ verify: async (data, signature, parameters) => {
625
+ const verifier = crypto.createVerify(
626
+ `RSA-SHA${parameters.alg.includes("512") ? "512" : "256"}`
627
+ )
628
+ verifier.update(data)
629
+
630
+ if (parameters.alg.startsWith("rsa-pss")) {
631
+ return verifier.verify(
632
+ {
633
+ key: pem,
634
+ padding: crypto.constants.RSA_PKCS1_PSS_PADDING,
635
+ saltLength: crypto.constants.RSA_PSS_SALTLEN_DIGEST,
636
+ },
637
+ signature
638
+ )
639
+ } else {
640
+ return verifier.verify(pem, signature)
641
+ }
642
+ },
643
+ }
644
+ }
645
+ } else {
646
+ // Extract public key from keyid
647
+ const signatureName = extractSignatureName(headers)
648
+ const extractedKey = extractPublicKeyFromHeaders(headers, signatureName)
649
+ if (!extractedKey) {
650
+ return {
651
+ valid: false,
652
+ error: "No public key provided and none found in signature",
653
+ }
654
+ }
655
+
656
+ const pem = jwkModulusToPem(extractedKey)
657
+
658
+ keyLookup = async keyId => {
659
+ // The library might pass the keyId in different formats, so be flexible
660
+ return {
661
+ id: keyId,
662
+ algs: ["rsa-pss-sha512", "rsa-pss-sha256", "rsa-v1_5-sha256"],
663
+ verify: async (data, signature, parameters) => {
664
+ try {
665
+ const verifier = crypto.createVerify(
666
+ `RSA-SHA${parameters.alg.includes("512") ? "512" : "256"}`
667
+ )
668
+ verifier.update(data)
669
+
670
+ let verified
671
+ if (parameters.alg.startsWith("rsa-pss")) {
672
+ verified = verifier.verify(
673
+ {
674
+ key: pem,
675
+ padding: crypto.constants.RSA_PKCS1_PSS_PADDING,
676
+ saltLength: crypto.constants.RSA_PSS_SALTLEN_DIGEST,
677
+ },
678
+ signature
679
+ )
680
+ } else {
681
+ verified = verifier.verify(pem, signature)
682
+ }
683
+
684
+ return verified
685
+ } catch (error) {
686
+ console.error("Verification error:", error)
687
+ return false
688
+ }
689
+ },
690
+ }
691
+ }
692
+ }
693
+
694
+ // Create request object for verification
695
+ const request = {
696
+ method,
697
+ url,
698
+ headers: { ...headers },
699
+ }
700
+
701
+ // Extract additional info from headers
702
+ const signatureName = extractSignatureName(headers)
703
+ const extractedPublicKey = extractPublicKeyFromHeaders(
704
+ headers,
705
+ signatureName
706
+ )
707
+
708
+ // Extract algorithm from signature-input
709
+ const signatureInputHeader =
710
+ headers["signature-input"] || headers["Signature-Input"]
711
+ const algMatch = signatureInputHeader?.match(/alg="([^"]+)"/)
712
+ const algorithm = algMatch ? algMatch[1] : undefined
713
+
714
+ // Verify using the library
715
+ let verified = false
716
+ let verificationError = null
717
+
718
+ try {
719
+ const verificationResult = await verifyMessage(
720
+ {
721
+ keyLookup,
722
+ requiredFields: [], // Don't require specific fields
723
+ },
724
+ request
725
+ )
726
+ // If we get here without throwing, verification succeeded
727
+ verified = true
728
+ } catch (verifyError) {
729
+ // Verification failed
730
+ verificationError = verifyError.message
731
+ verified = false
732
+ }
733
+
734
+ return {
735
+ valid: true, // The signature format is valid
736
+ verified, // Whether the cryptographic verification passed
737
+ signatureName,
738
+ keyId: extractedPublicKey
739
+ ? base64url.encode(extractedPublicKey)
740
+ : undefined,
741
+ algorithm,
742
+ publicKeyFromHeader: extractedPublicKey,
743
+ ...(verificationError && { error: verificationError }),
744
+ }
745
+ } catch (error) {
746
+ return {
747
+ valid: false,
748
+ error: error.message,
749
+ }
750
+ }
751
+ }
752
+
753
+ /**
754
+ * Extract public key from a signed message
755
+ *
756
+ * @param {Object} signedMessage - The signed message
757
+ * @returns {Buffer|null} The public key buffer or null
758
+ */
759
+ function extractPublicKeyFromMessage(signedMessage) {
760
+ const signatureName = extractSignatureName(signedMessage.headers)
761
+ return extractPublicKeyFromHeaders(signedMessage.headers, signatureName)
762
+ }