wao 0.24.2 → 0.25.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/cjs/hb.js +5 -1
- package/cjs/signer-utils.js +47 -5
- package/cjs/signer.js +318 -616
- package/esm/hb.js +5 -1
- package/esm/signer-utils.js +25 -0
- package/esm/signer.js +269 -482
- package/package.json +1 -1
package/esm/signer.js
CHANGED
|
@@ -10,23 +10,79 @@ const {
|
|
|
10
10
|
formatSignatureBase,
|
|
11
11
|
} = httpbis
|
|
12
12
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
13
|
+
export function hbEncodeValue(value) {
|
|
14
|
+
if (isBytes(value)) {
|
|
15
|
+
if (value.byteLength === 0) return hbEncodeValue("")
|
|
16
|
+
return [undefined, value]
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
if (typeof value === "string") {
|
|
20
|
+
if (value.length === 0) return ["empty-binary", undefined]
|
|
21
|
+
return [undefined, value]
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
if (Array.isArray(value)) {
|
|
25
|
+
if (value.length === 0) return ["empty-list", undefined]
|
|
26
|
+
if (value.some(isPojo)) {
|
|
27
|
+
throw new Error(
|
|
28
|
+
`Array with objects should have been lifted: ${JSON.stringify(value)}`
|
|
29
|
+
)
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const encoded = value
|
|
33
|
+
.map(v => {
|
|
34
|
+
if (typeof v === "string") {
|
|
35
|
+
if (v === "") return `"(ao-type-empty-binary) "`
|
|
36
|
+
const escaped = v.replace(/\\/g, "\\\\").replace(/"/g, '\\"')
|
|
37
|
+
return `"${escaped}"`
|
|
38
|
+
} else if (typeof v === "number") return String(v)
|
|
39
|
+
else if (typeof v === "boolean") return v ? "?1" : "?0"
|
|
40
|
+
else if (typeof v === "symbol") {
|
|
41
|
+
const desc = v.description || "symbol"
|
|
42
|
+
const escaped = desc.replace(/\\/g, "\\\\").replace(/"/g, '\\"')
|
|
43
|
+
return `"(ao-type-atom) ${escaped}"`
|
|
44
|
+
} else if (v === null) return `"(ao-type-atom) null"`
|
|
45
|
+
else if (v === undefined) return `"(ao-type-atom) undefined"`
|
|
46
|
+
else if (Array.isArray(v) && v.length === 0) {
|
|
47
|
+
return `"(ao-type-empty-list) "`
|
|
48
|
+
}
|
|
49
|
+
return `"${String(v)}"`
|
|
50
|
+
})
|
|
51
|
+
.join(", ")
|
|
52
|
+
return ["list", encoded]
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (typeof value === "number") {
|
|
56
|
+
if (!Number.isInteger(value)) return ["float", `${value}`]
|
|
57
|
+
return ["integer", String(value)]
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if (typeof value === "boolean") {
|
|
61
|
+
return ["atom", `"${value ? "true" : "false"}"`]
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (typeof value === "symbol") {
|
|
65
|
+
const desc = value.description || "symbol"
|
|
66
|
+
return ["atom", `"${desc}"`]
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (value === null) return ["atom", `"null"`]
|
|
70
|
+
|
|
71
|
+
if (value === undefined) return ["atom", `"undefined"`]
|
|
72
|
+
|
|
73
|
+
throw new Error(`Cannot encode value: ${String(value)}`)
|
|
74
|
+
}
|
|
75
|
+
|
|
16
76
|
const toView = value => {
|
|
17
77
|
if (ArrayBuffer.isView(value)) {
|
|
18
78
|
return Buffer.from(value.buffer, value.byteOffset, value.byteLength)
|
|
19
|
-
} else if (typeof value === "string")
|
|
20
|
-
|
|
21
|
-
}
|
|
79
|
+
} else if (typeof value === "string") return base64url.toBuffer(value)
|
|
80
|
+
|
|
22
81
|
throw new Error(
|
|
23
82
|
"Value must be Uint8Array, ArrayBuffer, or base64url-encoded string"
|
|
24
83
|
)
|
|
25
84
|
}
|
|
26
85
|
|
|
27
|
-
/**
|
|
28
|
-
* Generate HTTP signature name from address
|
|
29
|
-
*/
|
|
30
86
|
const httpSigName = address => {
|
|
31
87
|
const decoded = base64url.toBuffer(address)
|
|
32
88
|
const hexString = [...decoded.subarray(1, 9)]
|
|
@@ -35,24 +91,22 @@ const httpSigName = address => {
|
|
|
35
91
|
return `http-sig-${hexString}`
|
|
36
92
|
}
|
|
37
93
|
|
|
38
|
-
/**
|
|
39
|
-
* Join URL parts
|
|
40
|
-
*/
|
|
41
94
|
const joinUrl = ({ url, path }) => {
|
|
42
|
-
// If path is already a full URL, return it as-is
|
|
43
95
|
if (path.startsWith("http://") || path.startsWith("https://")) {
|
|
44
96
|
return path
|
|
45
97
|
}
|
|
46
|
-
|
|
47
|
-
// Otherwise, join the base URL with the path
|
|
48
98
|
return url.endsWith("/") ? url.slice(0, -1) + path : url + path
|
|
49
99
|
}
|
|
50
100
|
|
|
51
|
-
/**
|
|
52
|
-
* HyperBEAM Encoding Logic
|
|
53
|
-
*/
|
|
54
101
|
const MAX_HEADER_LENGTH = 4096
|
|
55
102
|
|
|
103
|
+
function encode_body_keys(bodyKeys) {
|
|
104
|
+
if (!bodyKeys || bodyKeys.length === 0) return ""
|
|
105
|
+
const items = bodyKeys.map(key => `"${key}"`)
|
|
106
|
+
const result = items.join(", ")
|
|
107
|
+
return result
|
|
108
|
+
}
|
|
109
|
+
|
|
56
110
|
async function hasNewline(value) {
|
|
57
111
|
if (typeof value === "string") return value.includes("\n")
|
|
58
112
|
if (value instanceof Blob) {
|
|
@@ -81,171 +135,171 @@ function isPojo(value) {
|
|
|
81
135
|
)
|
|
82
136
|
}
|
|
83
137
|
|
|
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
|
-
} 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"
|
|
110
|
-
}
|
|
111
|
-
return `"${String(v)}"`
|
|
112
|
-
})
|
|
113
|
-
.join(", ")
|
|
114
|
-
return ["list", encoded]
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
if (typeof value === "number") {
|
|
118
|
-
if (!Number.isInteger(value)) return ["float", `${value}`]
|
|
119
|
-
return ["integer", String(value)]
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
if (typeof value === "symbol") {
|
|
123
|
-
return ["atom", value.description]
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
throw new Error(`Cannot encode value: ${value.toString()}`)
|
|
127
|
-
}
|
|
128
|
-
|
|
129
138
|
function hbEncodeLift(obj, parent = "", top = {}) {
|
|
130
139
|
const [flattened, types] = Object.entries({ ...obj }).reduce(
|
|
131
140
|
(acc, [key, value]) => {
|
|
132
|
-
// For nested paths, preserve casing. For top-level, also preserve casing
|
|
133
141
|
const storageKey = parent ? `${parent}/${key}` : key
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
)
|
|
142
|
+
if (value == null) {
|
|
143
|
+
const [type, encoded] = hbEncodeValue(value)
|
|
144
|
+
if (encoded !== undefined) acc[0][key] = encoded
|
|
145
|
+
if (type) acc[1][key.toLowerCase()] = type
|
|
146
|
+
return acc
|
|
147
|
+
}
|
|
148
|
+
if (Array.isArray(value)) {
|
|
149
|
+
const hasObjects = value.some(isPojo)
|
|
150
|
+
const hasBinary = value.some(isBytes)
|
|
151
|
+
if (hasObjects || hasBinary) {
|
|
152
|
+
const indexedObj = value.reduce(
|
|
153
|
+
(obj, v, idx) => Object.assign(obj, { [idx]: v }),
|
|
154
|
+
{}
|
|
155
|
+
)
|
|
156
|
+
acc[1][key.toLowerCase()] = "list"
|
|
157
|
+
hbEncodeLift(indexedObj, storageKey, top)
|
|
158
|
+
return acc
|
|
159
|
+
} else {
|
|
160
|
+
const [type, encoded] = hbEncodeValue(value)
|
|
161
|
+
if (type) acc[1][key.toLowerCase()] = type
|
|
162
|
+
if (encoded !== undefined) acc[0][key] = encoded
|
|
163
|
+
return acc
|
|
164
|
+
}
|
|
144
165
|
}
|
|
145
166
|
|
|
146
|
-
// Store the original value for reference
|
|
147
167
|
const originalValue = value
|
|
148
168
|
|
|
149
|
-
// first/second lift object - handle nested objects
|
|
150
169
|
if (isPojo(value)) {
|
|
151
|
-
|
|
170
|
+
if (Object.keys(value).length === 0) {
|
|
171
|
+
acc[1][key.toLowerCase()] = "empty-message"
|
|
172
|
+
return acc
|
|
173
|
+
}
|
|
174
|
+
|
|
152
175
|
const hasComplexValues = Object.values(value).some(
|
|
153
176
|
v => isPojo(v) || (Array.isArray(v) && v.some(item => isPojo(item)))
|
|
154
177
|
)
|
|
155
178
|
|
|
156
179
|
if (!hasComplexValues) {
|
|
157
|
-
// Simple flat object - can be encoded as structured field dictionary
|
|
158
180
|
const items = []
|
|
181
|
+
const hasAnyNonEmptyValues = Object.values(value).some(v => {
|
|
182
|
+
return !(
|
|
183
|
+
v === null ||
|
|
184
|
+
v === undefined ||
|
|
185
|
+
v === "" ||
|
|
186
|
+
(Array.isArray(v) && v.length === 0) ||
|
|
187
|
+
(isPojo(v) && Object.keys(v).length === 0)
|
|
188
|
+
)
|
|
189
|
+
})
|
|
159
190
|
|
|
160
191
|
Object.entries(value).forEach(([k, v]) => {
|
|
161
192
|
const subKey = k.toLowerCase()
|
|
162
193
|
|
|
163
|
-
if (
|
|
164
|
-
|
|
165
|
-
|
|
194
|
+
if (v === null) {
|
|
195
|
+
items.push(`${subKey}="null"`)
|
|
196
|
+
acc[1][`${key.toLowerCase()}%2f${subKey}`] = "atom"
|
|
197
|
+
} else if (v === undefined) {
|
|
198
|
+
items.push(`${subKey}="undefined"`)
|
|
199
|
+
acc[1][`${key.toLowerCase()}%2f${subKey}`] = "atom"
|
|
200
|
+
} else if (typeof v === "string") {
|
|
201
|
+
if (v === "") {
|
|
202
|
+
acc[1][`${key.toLowerCase()}%2f${subKey}`] = "empty-binary"
|
|
203
|
+
} else {
|
|
204
|
+
const escaped = v.replace(/\\/g, "\\\\").replace(/"/g, '\\"')
|
|
205
|
+
items.push(`${subKey}="${escaped}"`)
|
|
206
|
+
}
|
|
166
207
|
} else if (typeof v === "number") {
|
|
167
208
|
items.push(`${subKey}=${v}`)
|
|
168
209
|
if (Number.isInteger(v)) {
|
|
169
|
-
// Use URL-encoded forward slash separator
|
|
170
210
|
acc[1][`${key.toLowerCase()}%2f${subKey}`] = "integer"
|
|
171
211
|
} else {
|
|
172
212
|
acc[1][`${key.toLowerCase()}%2f${subKey}`] = "float"
|
|
173
213
|
}
|
|
174
214
|
} else if (typeof v === "boolean") {
|
|
175
215
|
items.push(`${subKey}=${v ? "?1" : "?0"}`)
|
|
216
|
+
acc[1][`${key.toLowerCase()}%2f${subKey}`] = "boolean"
|
|
217
|
+
} else if (typeof v === "symbol") {
|
|
218
|
+
const desc = v.description || "symbol"
|
|
219
|
+
const escaped = desc.replace(/\\/g, "\\\\").replace(/"/g, '\\"')
|
|
220
|
+
items.push(`${subKey}="${escaped}"`)
|
|
221
|
+
acc[1][`${key.toLowerCase()}%2f${subKey}`] = "atom"
|
|
176
222
|
} else if (Array.isArray(v) && !v.some(item => isPojo(item))) {
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
223
|
+
if (v.length === 0) {
|
|
224
|
+
items.push(`${subKey}=()`)
|
|
225
|
+
acc[1][`${key.toLowerCase()}%2f${subKey}`] = "empty-list"
|
|
226
|
+
} else {
|
|
227
|
+
const listItems = v.map(item => {
|
|
228
|
+
if (typeof item === "string") {
|
|
229
|
+
return `"${item.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`
|
|
230
|
+
} else if (typeof item === "number") {
|
|
231
|
+
return String(item)
|
|
232
|
+
} else if (typeof item === "boolean") {
|
|
233
|
+
return item ? "?1" : "?0"
|
|
234
|
+
} else if (typeof item === "symbol") {
|
|
235
|
+
const desc = item.description || "symbol"
|
|
236
|
+
return `"${desc.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`
|
|
237
|
+
} else if (item === null) {
|
|
238
|
+
return `"null"`
|
|
239
|
+
} else if (item === undefined) {
|
|
240
|
+
return `"undefined"`
|
|
241
|
+
} else {
|
|
242
|
+
return `"${String(item)}"`
|
|
243
|
+
}
|
|
244
|
+
})
|
|
245
|
+
items.push(`${subKey}=(${listItems.join(" ")})`)
|
|
246
|
+
}
|
|
247
|
+
} else if (isPojo(v) && Object.keys(v).length === 0) {
|
|
248
|
+
items.push(`${subKey}`)
|
|
249
|
+
acc[1][`${key.toLowerCase()}%2f${subKey}`] = "empty-message"
|
|
190
250
|
}
|
|
191
251
|
})
|
|
192
252
|
|
|
193
253
|
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
254
|
|
|
255
|
+
const hasOnlyEmptyValues = Object.entries(value).every(([k, v]) => {
|
|
256
|
+
return (
|
|
257
|
+
v === null ||
|
|
258
|
+
v === undefined ||
|
|
259
|
+
v === "" ||
|
|
260
|
+
(Array.isArray(v) && v.length === 0) ||
|
|
261
|
+
(isPojo(v) && Object.keys(v).length === 0)
|
|
262
|
+
)
|
|
263
|
+
})
|
|
264
|
+
|
|
265
|
+
if (!hasAnyNonEmptyValues) {
|
|
266
|
+
acc[1][key.toLowerCase()] = "map"
|
|
267
|
+
} else if (encodedValue === "") {
|
|
268
|
+
acc[1][key.toLowerCase()] = "empty-message"
|
|
269
|
+
} else {
|
|
270
|
+
acc[0][key] = encodedValue
|
|
271
|
+
acc[1][key.toLowerCase()] = "map"
|
|
272
|
+
}
|
|
273
|
+
} else hbEncodeLift(value, storageKey, top)
|
|
203
274
|
return acc
|
|
204
275
|
}
|
|
205
276
|
|
|
206
|
-
// leaf encode value
|
|
207
277
|
const [type, encoded] = hbEncodeValue(value)
|
|
278
|
+
|
|
208
279
|
if (encoded !== undefined) {
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
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)
|
|
217
|
-
} else {
|
|
218
|
-
// Preserve the original key casing
|
|
219
|
-
const httpKey = key
|
|
220
|
-
if (type === "integer" && typeof value === "number") {
|
|
221
|
-
acc[0][httpKey] = String(value)
|
|
222
|
-
} else {
|
|
223
|
-
acc[0][httpKey] = encoded
|
|
224
|
-
}
|
|
280
|
+
if (isBytes(encoded)) top[storageKey] = encoded
|
|
281
|
+
else {
|
|
282
|
+
acc[0][key] = encoded
|
|
283
|
+
if (type) acc[1][key.toLowerCase()] = type
|
|
225
284
|
}
|
|
226
|
-
}
|
|
227
|
-
if (type) {
|
|
228
|
-
// Store type with lowercase key for ao-types dictionary
|
|
229
|
-
acc[1][key.toLowerCase()] = type
|
|
230
|
-
}
|
|
285
|
+
} else if (type) acc[1][key.toLowerCase()] = type
|
|
231
286
|
return acc
|
|
232
287
|
},
|
|
233
288
|
[{}, {}]
|
|
234
289
|
)
|
|
235
290
|
|
|
236
|
-
if (Object.keys(flattened).length === 0)
|
|
291
|
+
if (Object.keys(flattened).length === 0 && Object.keys(types).length === 0)
|
|
292
|
+
return top
|
|
237
293
|
|
|
238
294
|
if (Object.keys(types).length > 0) {
|
|
239
|
-
// Format as structured fields dictionary
|
|
240
295
|
const aoTypeItems = Object.entries(types).map(([key, value]) => {
|
|
241
|
-
// The Erlang side expects keys with %2f for forward slashes
|
|
242
296
|
const safeKey = key
|
|
243
297
|
.toLowerCase()
|
|
244
298
|
.replace(
|
|
245
299
|
/[^a-z0-9_\-.*\/]/g,
|
|
246
300
|
c => "%" + c.charCodeAt(0).toString(16).padStart(2, "0")
|
|
247
301
|
)
|
|
248
|
-
.replace(/\//g, "%2f")
|
|
302
|
+
.replace(/\//g, "%2f")
|
|
249
303
|
return `${safeKey}="${value}"`
|
|
250
304
|
})
|
|
251
305
|
aoTypeItems.sort()
|
|
@@ -254,22 +308,15 @@ function hbEncodeLift(obj, parent = "", top = {}) {
|
|
|
254
308
|
if (Buffer.from(aoTypes).byteLength > MAX_HEADER_LENGTH) {
|
|
255
309
|
const flatK = parent ? `${parent}/ao-types` : "ao-types"
|
|
256
310
|
top[flatK] = aoTypes
|
|
257
|
-
} else
|
|
258
|
-
flattened["ao-types"] = aoTypes
|
|
259
|
-
}
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
if (parent) {
|
|
263
|
-
top[parent] = flattened
|
|
264
|
-
} else {
|
|
265
|
-
Object.assign(top, flattened)
|
|
311
|
+
} else flattened["ao-types"] = aoTypes
|
|
266
312
|
}
|
|
267
313
|
|
|
314
|
+
if (parent) top[parent] = flattened
|
|
315
|
+
else Object.assign(top, flattened)
|
|
268
316
|
return top
|
|
269
317
|
}
|
|
270
318
|
|
|
271
319
|
function encodePart(name, { headers = {}, body }) {
|
|
272
|
-
// Convert headers to a plain object if it's a Headers instance
|
|
273
320
|
const headerEntries =
|
|
274
321
|
headers instanceof Headers
|
|
275
322
|
? Array.from(headers.entries())
|
|
@@ -290,15 +337,12 @@ function encodePart(name, { headers = {}, body }) {
|
|
|
290
337
|
|
|
291
338
|
async function encode(obj = {}) {
|
|
292
339
|
if (Object.keys(obj).length === 0) return { headers: {}, body: undefined }
|
|
293
|
-
|
|
294
|
-
// Keep reference to original object for data field
|
|
295
340
|
const originalObj = obj
|
|
296
341
|
const flattened = hbEncodeLift(obj)
|
|
297
342
|
|
|
298
343
|
const bodyKeys = []
|
|
299
344
|
const headerKeys = []
|
|
300
345
|
|
|
301
|
-
// Process all flattened keys
|
|
302
346
|
await Promise.all(
|
|
303
347
|
Object.keys(flattened).map(async key => {
|
|
304
348
|
const value = flattened[key]
|
|
@@ -312,13 +356,23 @@ async function encode(obj = {}) {
|
|
|
312
356
|
return
|
|
313
357
|
}
|
|
314
358
|
|
|
315
|
-
// Check if this should be a body field
|
|
316
359
|
if (isBytes(value)) {
|
|
317
|
-
// Binary data should always go to body
|
|
318
360
|
bodyKeys.push(key)
|
|
361
|
+
const uint8Array =
|
|
362
|
+
value instanceof Uint8Array
|
|
363
|
+
? value
|
|
364
|
+
: value instanceof ArrayBuffer
|
|
365
|
+
? new Uint8Array(value)
|
|
366
|
+
: Buffer.isBuffer(value)
|
|
367
|
+
? new Uint8Array(value.buffer, value.byteOffset, value.length)
|
|
368
|
+
: new Uint8Array(
|
|
369
|
+
value.buffer,
|
|
370
|
+
value.byteOffset,
|
|
371
|
+
value.byteLength
|
|
372
|
+
)
|
|
319
373
|
flattened[key] = new Blob([
|
|
320
374
|
`content-disposition: form-data;name="${key}"\r\n\r\n`,
|
|
321
|
-
|
|
375
|
+
uint8Array,
|
|
322
376
|
])
|
|
323
377
|
return
|
|
324
378
|
}
|
|
@@ -328,7 +382,7 @@ async function encode(obj = {}) {
|
|
|
328
382
|
(await hasNewline(valueStr)) ||
|
|
329
383
|
key.includes("/") ||
|
|
330
384
|
Buffer.from(valueStr).byteLength > MAX_HEADER_LENGTH ||
|
|
331
|
-
(isPojo(value) && valueStr === "[object Object]")
|
|
385
|
+
(isPojo(value) && valueStr === "[object Object]")
|
|
332
386
|
) {
|
|
333
387
|
bodyKeys.push(key)
|
|
334
388
|
flattened[key] = new Blob([
|
|
@@ -338,71 +392,65 @@ async function encode(obj = {}) {
|
|
|
338
392
|
return
|
|
339
393
|
}
|
|
340
394
|
|
|
341
|
-
// It's a header
|
|
342
395
|
headerKeys.push(key)
|
|
343
396
|
})
|
|
344
397
|
)
|
|
345
398
|
|
|
346
|
-
// Build headers object with all header keys
|
|
347
399
|
const headers = {}
|
|
348
400
|
headerKeys.forEach(key => {
|
|
349
401
|
headers[key] = flattened[key]
|
|
350
402
|
})
|
|
351
403
|
|
|
352
|
-
// Special handling for data and body fields
|
|
353
404
|
if ("data" in originalObj && !bodyKeys.includes("data")) {
|
|
354
405
|
bodyKeys.push("data")
|
|
355
|
-
delete headers["data"]
|
|
406
|
+
delete headers["data"]
|
|
356
407
|
}
|
|
357
408
|
|
|
358
409
|
if ("body" in originalObj && !bodyKeys.includes("body")) {
|
|
359
410
|
bodyKeys.push("body")
|
|
360
|
-
delete headers["body"]
|
|
411
|
+
delete headers["body"]
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
if (bodyKeys.length > 0) {
|
|
415
|
+
headers["body-keys"] = encode_body_keys(bodyKeys)
|
|
361
416
|
}
|
|
362
417
|
|
|
363
418
|
let body = undefined
|
|
364
419
|
let promoteToBody = true
|
|
365
420
|
if (bodyKeys.length > 0) {
|
|
366
421
|
if (bodyKeys.length === 1) {
|
|
367
|
-
// If there is only one element, promote it to be the full body
|
|
368
422
|
const bodyKey = bodyKeys[0]
|
|
369
423
|
const originalValue = originalObj[bodyKey]
|
|
370
424
|
const flattenedValue = flattened[bodyKey]
|
|
371
425
|
|
|
372
|
-
// Only promote if it's not a complex object
|
|
373
426
|
if (
|
|
374
427
|
!isPojo(originalValue) ||
|
|
375
428
|
(isPojo(originalValue) && typeof flattenedValue === "string")
|
|
376
429
|
) {
|
|
377
|
-
// For objects that were encoded as structured fields, use the encoded value
|
|
378
430
|
if (
|
|
379
431
|
(bodyKey === "body" || bodyKey === "data") &&
|
|
380
432
|
isPojo(originalValue) &&
|
|
381
433
|
typeof flattenedValue === "string"
|
|
382
434
|
) {
|
|
383
435
|
body = new Blob([flattenedValue])
|
|
384
|
-
} else {
|
|
385
|
-
|
|
386
|
-
|
|
436
|
+
} else if (Array.isArray(originalValue)) {
|
|
437
|
+
const hasSymbols = originalValue.some(
|
|
438
|
+
item => typeof item === "symbol"
|
|
439
|
+
)
|
|
440
|
+
if (hasSymbols) {
|
|
441
|
+
const [type, encoded] = hbEncodeValue(originalValue)
|
|
442
|
+
body = new Blob([encoded || originalValue.toString()])
|
|
443
|
+
} else body = new Blob([originalValue.toString()])
|
|
444
|
+
} else body = new Blob([originalValue || flattenedValue])
|
|
387
445
|
headers["inline-body-key"] = bodyKey
|
|
388
|
-
} else
|
|
389
|
-
// Complex object - don't promote, create multipart
|
|
390
|
-
promoteToBody = false
|
|
391
|
-
}
|
|
446
|
+
} else promoteToBody = false
|
|
392
447
|
}
|
|
393
448
|
|
|
394
449
|
if (!promoteToBody || bodyKeys.length > 1) {
|
|
395
|
-
// Multiple body fields - create multipart
|
|
396
450
|
const bodyParts = await Promise.all(
|
|
397
451
|
bodyKeys.map(async name => {
|
|
398
|
-
if (flattened[name] instanceof Blob)
|
|
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
|
|
452
|
+
if (flattened[name] instanceof Blob) return flattened[name]
|
|
403
453
|
const value = originalObj[name] || flattened[name] || ""
|
|
404
|
-
|
|
405
|
-
// Special case: if this is a structured field encoded value, use the flattened value
|
|
406
454
|
if (
|
|
407
455
|
name === "body" &&
|
|
408
456
|
isPojo(originalObj[name]) &&
|
|
@@ -415,20 +463,50 @@ async function encode(obj = {}) {
|
|
|
415
463
|
return partBlob
|
|
416
464
|
}
|
|
417
465
|
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
value
|
|
421
|
-
|
|
466
|
+
let valueToEncode = value
|
|
467
|
+
if (Array.isArray(value)) {
|
|
468
|
+
const hasSymbols = value.some(item => typeof item === "symbol")
|
|
469
|
+
if (hasSymbols) {
|
|
470
|
+
const [type, encoded] = hbEncodeValue(value)
|
|
471
|
+
valueToEncode = encoded || value.toString()
|
|
472
|
+
}
|
|
473
|
+
} else if (isBytes(value)) valueToEncode = value
|
|
474
|
+
let partBlob
|
|
475
|
+
if (isBytes(valueToEncode)) {
|
|
476
|
+
const uint8Array =
|
|
477
|
+
valueToEncode instanceof Uint8Array
|
|
478
|
+
? valueToEncode
|
|
479
|
+
: valueToEncode instanceof ArrayBuffer
|
|
480
|
+
? new Uint8Array(valueToEncode)
|
|
481
|
+
: Buffer.isBuffer(valueToEncode)
|
|
482
|
+
? new Uint8Array(
|
|
483
|
+
valueToEncode.buffer,
|
|
484
|
+
valueToEncode.byteOffset,
|
|
485
|
+
valueToEncode.length
|
|
486
|
+
)
|
|
487
|
+
: new Uint8Array(
|
|
488
|
+
valueToEncode.buffer,
|
|
489
|
+
valueToEncode.byteOffset,
|
|
490
|
+
valueToEncode.byteLength
|
|
491
|
+
)
|
|
492
|
+
partBlob = new Blob([
|
|
493
|
+
`content-disposition: form-data;name="${name}"\r\n\r\n`,
|
|
494
|
+
uint8Array,
|
|
495
|
+
])
|
|
496
|
+
} else {
|
|
497
|
+
partBlob = new Blob([
|
|
498
|
+
`content-disposition: form-data;name="${name}"\r\n\r\n`,
|
|
499
|
+
valueToEncode,
|
|
500
|
+
])
|
|
501
|
+
}
|
|
422
502
|
return partBlob
|
|
423
503
|
})
|
|
424
504
|
)
|
|
425
505
|
|
|
426
|
-
// Calculate boundary from the content
|
|
427
506
|
const allPartsBuffer = await new Blob(bodyParts).arrayBuffer()
|
|
428
507
|
const hash = await sha256(allPartsBuffer)
|
|
429
508
|
const boundary = base64url.encode(Buffer.from(hash))
|
|
430
509
|
|
|
431
|
-
// Build the multipart body with proper boundaries
|
|
432
510
|
const finalParts = []
|
|
433
511
|
for (const part of bodyParts) {
|
|
434
512
|
finalParts.push(`--${boundary}\r\n`)
|
|
@@ -445,8 +523,6 @@ async function encode(obj = {}) {
|
|
|
445
523
|
const finalContent = await body.arrayBuffer()
|
|
446
524
|
const contentDigest = await sha256(finalContent)
|
|
447
525
|
const base64 = base64url.toBase64(base64url.encode(contentDigest))
|
|
448
|
-
|
|
449
|
-
// Use lowercase to match what's in the other headers
|
|
450
526
|
headers["content-digest"] = `sha-256=:${base64}:`
|
|
451
527
|
headers["content-length"] = String(finalContent.byteLength)
|
|
452
528
|
}
|
|
@@ -455,9 +531,6 @@ async function encode(obj = {}) {
|
|
|
455
531
|
return { headers, body }
|
|
456
532
|
}
|
|
457
533
|
|
|
458
|
-
/**
|
|
459
|
-
* Create HTTP signer wrapper
|
|
460
|
-
*/
|
|
461
534
|
const toHttpSigner = signer => {
|
|
462
535
|
const params = ["alg", "keyid"].sort()
|
|
463
536
|
|
|
@@ -515,31 +588,17 @@ const toHttpSigner = signer => {
|
|
|
515
588
|
httpSigName(result.address)
|
|
516
589
|
)
|
|
517
590
|
|
|
518
|
-
// Only lowercase the signature headers
|
|
519
591
|
const finalHeaders = {}
|
|
520
592
|
for (const [key, value] of Object.entries(signedHeaders)) {
|
|
521
593
|
if (key === "Signature" || key === "Signature-Input") {
|
|
522
594
|
finalHeaders[key.toLowerCase()] = value
|
|
523
|
-
} else
|
|
524
|
-
finalHeaders[key] = value
|
|
525
|
-
}
|
|
595
|
+
} else finalHeaders[key] = value
|
|
526
596
|
}
|
|
527
597
|
|
|
528
|
-
return {
|
|
529
|
-
...request,
|
|
530
|
-
headers: finalHeaders,
|
|
531
|
-
}
|
|
598
|
+
return { ...request, headers: finalHeaders }
|
|
532
599
|
}
|
|
533
600
|
}
|
|
534
601
|
|
|
535
|
-
/**
|
|
536
|
-
* Create the main request function that creates signed messages locally
|
|
537
|
-
*
|
|
538
|
-
* @param {Object} config - Configuration object
|
|
539
|
-
* @param {Function} config.signer - Signer function
|
|
540
|
-
* @param {string} [config.HB_URL='http://relay.ao-hb.xyz'] - Base URL
|
|
541
|
-
* @returns {Function} Request function that takes tags and returns signed message
|
|
542
|
-
*/
|
|
543
602
|
export function createRequest(config) {
|
|
544
603
|
const { signer, url = "http://localhost:10001" } = config
|
|
545
604
|
|
|
@@ -549,20 +608,20 @@ export function createRequest(config) {
|
|
|
549
608
|
|
|
550
609
|
return async function request(fields) {
|
|
551
610
|
const { path = "/relay/process", method = "POST", ...restFields } = fields
|
|
552
|
-
|
|
553
|
-
// Add default AO fields
|
|
554
611
|
const aoFields = { ...restFields }
|
|
612
|
+
const rootKeys = Object.keys(aoFields)
|
|
613
|
+
const binaryKeys = rootKeys.filter(key => isBytes(aoFields[key]))
|
|
555
614
|
|
|
556
|
-
|
|
557
|
-
|
|
615
|
+
if (binaryKeys.length > 1 && !aoFields.body && !aoFields.data) {
|
|
616
|
+
aoFields.body = "1984"
|
|
617
|
+
}
|
|
558
618
|
|
|
559
|
-
|
|
619
|
+
const encoded = await encode(aoFields)
|
|
560
620
|
const headersObj = encoded ? encoded.headers : {}
|
|
561
621
|
const body = encoded ? encoded.body : undefined
|
|
562
622
|
|
|
563
623
|
const _url = joinUrl({ url, path })
|
|
564
624
|
|
|
565
|
-
// Add Content-Length if body exists
|
|
566
625
|
if (body && !headersObj["content-length"]) {
|
|
567
626
|
const bodySize = body.size || body.byteLength || 0
|
|
568
627
|
if (bodySize > 0) {
|
|
@@ -570,87 +629,51 @@ export function createRequest(config) {
|
|
|
570
629
|
}
|
|
571
630
|
}
|
|
572
631
|
|
|
573
|
-
// Create lowercase headers for signing
|
|
574
632
|
const lowercaseHeaders = {}
|
|
575
633
|
for (const [key, value] of Object.entries(headersObj)) {
|
|
576
634
|
lowercaseHeaders[key.toLowerCase()] = value
|
|
577
635
|
}
|
|
578
636
|
|
|
579
|
-
|
|
580
|
-
|
|
637
|
+
const signingFields = Object.keys(lowercaseHeaders).filter(
|
|
638
|
+
key => key !== "body-keys"
|
|
639
|
+
)
|
|
581
640
|
|
|
582
|
-
// If there are no fields to sign, add at least the content-length
|
|
583
641
|
if (signingFields.length === 0 && !body) {
|
|
584
642
|
lowercaseHeaders["content-length"] = "0"
|
|
585
643
|
signingFields.push("content-length")
|
|
586
644
|
}
|
|
587
645
|
|
|
588
|
-
// Sign the request with lowercase headers
|
|
589
646
|
const signedRequest = await toHttpSigner(signer)({
|
|
590
647
|
request: { url: _url, method, headers: lowercaseHeaders },
|
|
591
648
|
fields: signingFields,
|
|
592
649
|
})
|
|
593
650
|
|
|
594
|
-
// Build final headers: use original casing for all headers except signature headers
|
|
595
651
|
const finalHeaders = {}
|
|
596
652
|
|
|
597
|
-
// First, add all original headers with their original casing
|
|
598
653
|
for (const [key, value] of Object.entries(headersObj)) {
|
|
599
654
|
finalHeaders[key] = value
|
|
600
655
|
}
|
|
601
656
|
|
|
602
|
-
// Then add the signature headers (which should be lowercase)
|
|
603
657
|
finalHeaders["signature"] = signedRequest.headers["signature"]
|
|
604
658
|
finalHeaders["signature-input"] = signedRequest.headers["signature-input"]
|
|
605
659
|
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
url: _url,
|
|
609
|
-
method,
|
|
610
|
-
headers: finalHeaders,
|
|
660
|
+
if (headersObj["body-keys"]) {
|
|
661
|
+
finalHeaders["body-keys"] = headersObj["body-keys"]
|
|
611
662
|
}
|
|
612
663
|
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
}
|
|
664
|
+
const result = { url: _url, method, headers: finalHeaders }
|
|
665
|
+
|
|
666
|
+
if (body) result.body = body
|
|
617
667
|
|
|
618
668
|
return result
|
|
619
669
|
}
|
|
620
670
|
}
|
|
621
|
-
/**
|
|
622
|
-
* Utility function to extract the message ID from a signed message
|
|
623
|
-
* Based on the original code's hash calculation
|
|
624
|
-
*/
|
|
625
|
-
async function getMessageId(signedMessage) {
|
|
626
|
-
// Extract signature from the Signature header
|
|
627
|
-
const signatureHeader =
|
|
628
|
-
signedMessage.headers.Signature || signedMessage.headers.signature
|
|
629
|
-
const match = signatureHeader.match(/Signature:\s*'http-sig-[^:]+:([^']+)'/)
|
|
630
|
-
const signature = match ? match[1] : null
|
|
631
|
-
|
|
632
|
-
if (!signature) {
|
|
633
|
-
throw new Error("Could not extract signature from headers")
|
|
634
|
-
}
|
|
635
|
-
|
|
636
|
-
// Hash the signature to get message ID
|
|
637
|
-
const encoder = new TextEncoder()
|
|
638
|
-
const data = encoder.encode(signature)
|
|
639
|
-
const hashBuffer = await crypto.subtle.digest("SHA-256", data)
|
|
640
|
-
const hashArray = Array.from(new Uint8Array(hashBuffer))
|
|
641
|
-
const hashBase64 = btoa(String.fromCharCode(...hashArray))
|
|
642
|
-
|
|
643
|
-
return hashBase64
|
|
644
|
-
}
|
|
645
|
-
|
|
646
671
|
export async function send(signedMsg, fetchImpl = fetch) {
|
|
647
672
|
const fetchOptions = {
|
|
648
673
|
method: signedMsg.method,
|
|
649
674
|
headers: signedMsg.headers,
|
|
650
675
|
redirect: "follow",
|
|
651
676
|
}
|
|
652
|
-
|
|
653
|
-
// Only add body if it exists and method supports it
|
|
654
677
|
if (
|
|
655
678
|
signedMsg.body !== undefined &&
|
|
656
679
|
signedMsg.method !== "GET" &&
|
|
@@ -664,7 +687,6 @@ export async function send(signedMsg, fetchImpl = fetch) {
|
|
|
664
687
|
throw new Error(`${response.status}: ${await response.text()}`)
|
|
665
688
|
}
|
|
666
689
|
|
|
667
|
-
// Convert Headers object to plain object
|
|
668
690
|
let headers = {}
|
|
669
691
|
if (response.headers && typeof response.headers.forEach === "function") {
|
|
670
692
|
response.headers.forEach((v, k) => (headers[k] = v))
|
|
@@ -677,238 +699,3 @@ export async function send(signedMsg, fetchImpl = fetch) {
|
|
|
677
699
|
status: response.status,
|
|
678
700
|
}
|
|
679
701
|
}
|
|
680
|
-
|
|
681
|
-
/**
|
|
682
|
-
* Convert JWK modulus (n) to PEM format public key
|
|
683
|
-
* @param {Buffer} nBuffer - The modulus buffer
|
|
684
|
-
* @returns {string} PEM formatted public key
|
|
685
|
-
*/
|
|
686
|
-
function jwkModulusToPem(nBuffer) {
|
|
687
|
-
// RSA public key with standard exponent
|
|
688
|
-
const rsaPublicKey = crypto.createPublicKey({
|
|
689
|
-
key: {
|
|
690
|
-
kty: "RSA",
|
|
691
|
-
n: base64url.encode(nBuffer),
|
|
692
|
-
e: "AQAB", // Standard exponent 65537
|
|
693
|
-
},
|
|
694
|
-
format: "jwk",
|
|
695
|
-
})
|
|
696
|
-
|
|
697
|
-
return rsaPublicKey.export({ type: "spki", format: "pem" })
|
|
698
|
-
}
|
|
699
|
-
|
|
700
|
-
/**
|
|
701
|
-
* Extract signature name from headers
|
|
702
|
-
* @param {Object} headers - Request headers
|
|
703
|
-
* @returns {string|null} Signature name or null
|
|
704
|
-
*/
|
|
705
|
-
function extractSignatureName(headers) {
|
|
706
|
-
const signatureHeader = headers["signature"] || headers["Signature"]
|
|
707
|
-
if (!signatureHeader) return null
|
|
708
|
-
|
|
709
|
-
// Extract signature name (e.g., "http-sig-xxxxxxxx")
|
|
710
|
-
// Handle both "name:" and "name=" formats
|
|
711
|
-
const match = signatureHeader.match(/^([^:=]+)[:=]/)
|
|
712
|
-
return match ? match[1] : null
|
|
713
|
-
}
|
|
714
|
-
|
|
715
|
-
/**
|
|
716
|
-
* Extract public key from signature-input header
|
|
717
|
-
* @param {Object} headers - Request headers
|
|
718
|
-
* @param {string} [signatureName] - Optional signature name to look for
|
|
719
|
-
* @returns {Buffer|null} Public key buffer or null
|
|
720
|
-
*/
|
|
721
|
-
export function extractPublicKeyFromHeaders(headers, signatureName) {
|
|
722
|
-
const signatureInput =
|
|
723
|
-
headers["signature-input"] || headers["Signature-Input"]
|
|
724
|
-
if (!signatureInput) return null
|
|
725
|
-
|
|
726
|
-
// If we have a signature name, look for its specific keyid
|
|
727
|
-
let keyidMatch
|
|
728
|
-
if (signatureName) {
|
|
729
|
-
// The signature-input format is: signatureName=(...);alg="...";keyid="..."
|
|
730
|
-
// We need to match after the signature name
|
|
731
|
-
const signatureSection = signatureInput.substring(
|
|
732
|
-
signatureInput.indexOf(signatureName)
|
|
733
|
-
)
|
|
734
|
-
keyidMatch = signatureSection.match(/keyid="([^"]+)"/)
|
|
735
|
-
} else {
|
|
736
|
-
// General keyid match
|
|
737
|
-
keyidMatch = signatureInput.match(/keyid="([^"]+)"/)
|
|
738
|
-
}
|
|
739
|
-
|
|
740
|
-
if (!keyidMatch) return null
|
|
741
|
-
|
|
742
|
-
try {
|
|
743
|
-
return base64url.toBuffer(keyidMatch[1])
|
|
744
|
-
} catch (error) {
|
|
745
|
-
return null
|
|
746
|
-
}
|
|
747
|
-
}
|
|
748
|
-
|
|
749
|
-
/**
|
|
750
|
-
* Verify an HTTP signed message using http-message-signatures
|
|
751
|
-
*
|
|
752
|
-
* @param {Object} signedMessage - The signed message to verify
|
|
753
|
-
* @param {string} signedMessage.url - Request URL
|
|
754
|
-
* @param {string} signedMessage.method - HTTP method
|
|
755
|
-
* @param {Object} signedMessage.headers - Headers including signature
|
|
756
|
-
* @param {string} [signedMessage.body] - Request body
|
|
757
|
-
* @param {string|Buffer} [publicKey] - Optional public key (if not provided, extracts from keyid)
|
|
758
|
-
* @returns {Object} Verification result
|
|
759
|
-
*/
|
|
760
|
-
export async function verify(signedMessage, publicKey) {
|
|
761
|
-
try {
|
|
762
|
-
const { url, method, headers, body } = signedMessage
|
|
763
|
-
|
|
764
|
-
// Determine which public key to use
|
|
765
|
-
let keyLookup
|
|
766
|
-
|
|
767
|
-
if (publicKey) {
|
|
768
|
-
// Use provided public key
|
|
769
|
-
const pem =
|
|
770
|
-
typeof publicKey === "string" ? publicKey : jwkModulusToPem(publicKey)
|
|
771
|
-
|
|
772
|
-
keyLookup = async keyId => {
|
|
773
|
-
return {
|
|
774
|
-
id: keyId,
|
|
775
|
-
algs: ["rsa-pss-sha512", "rsa-pss-sha256", "rsa-v1_5-sha256"],
|
|
776
|
-
verify: async (data, signature, parameters) => {
|
|
777
|
-
const verifier = crypto.createVerify(
|
|
778
|
-
`RSA-SHA${parameters.alg.includes("512") ? "512" : "256"}`
|
|
779
|
-
)
|
|
780
|
-
verifier.update(data)
|
|
781
|
-
|
|
782
|
-
if (parameters.alg.startsWith("rsa-pss")) {
|
|
783
|
-
return verifier.verify(
|
|
784
|
-
{
|
|
785
|
-
key: pem,
|
|
786
|
-
padding: crypto.constants.RSA_PKCS1_PSS_PADDING,
|
|
787
|
-
saltLength: crypto.constants.RSA_PSS_SALTLEN_DIGEST,
|
|
788
|
-
},
|
|
789
|
-
signature
|
|
790
|
-
)
|
|
791
|
-
} else {
|
|
792
|
-
return verifier.verify(pem, signature)
|
|
793
|
-
}
|
|
794
|
-
},
|
|
795
|
-
}
|
|
796
|
-
}
|
|
797
|
-
} else {
|
|
798
|
-
// Extract public key from keyid
|
|
799
|
-
const signatureName = extractSignatureName(headers)
|
|
800
|
-
const extractedKey = extractPublicKeyFromHeaders(headers, signatureName)
|
|
801
|
-
if (!extractedKey) {
|
|
802
|
-
return {
|
|
803
|
-
valid: false,
|
|
804
|
-
error: "No public key provided and none found in signature",
|
|
805
|
-
}
|
|
806
|
-
}
|
|
807
|
-
|
|
808
|
-
const pem = jwkModulusToPem(extractedKey)
|
|
809
|
-
|
|
810
|
-
keyLookup = async keyId => {
|
|
811
|
-
// The library might pass the keyId in different formats, so be flexible
|
|
812
|
-
return {
|
|
813
|
-
id: keyId,
|
|
814
|
-
algs: ["rsa-pss-sha512", "rsa-pss-sha256", "rsa-v1_5-sha256"],
|
|
815
|
-
verify: async (data, signature, parameters) => {
|
|
816
|
-
try {
|
|
817
|
-
const verifier = crypto.createVerify(
|
|
818
|
-
`RSA-SHA${parameters.alg.includes("512") ? "512" : "256"}`
|
|
819
|
-
)
|
|
820
|
-
verifier.update(data)
|
|
821
|
-
|
|
822
|
-
let verified
|
|
823
|
-
if (parameters.alg.startsWith("rsa-pss")) {
|
|
824
|
-
verified = verifier.verify(
|
|
825
|
-
{
|
|
826
|
-
key: pem,
|
|
827
|
-
padding: crypto.constants.RSA_PKCS1_PSS_PADDING,
|
|
828
|
-
saltLength: crypto.constants.RSA_PSS_SALTLEN_DIGEST,
|
|
829
|
-
},
|
|
830
|
-
signature
|
|
831
|
-
)
|
|
832
|
-
} else {
|
|
833
|
-
verified = verifier.verify(pem, signature)
|
|
834
|
-
}
|
|
835
|
-
|
|
836
|
-
return verified
|
|
837
|
-
} catch (error) {
|
|
838
|
-
console.error("Verification error:", error)
|
|
839
|
-
return false
|
|
840
|
-
}
|
|
841
|
-
},
|
|
842
|
-
}
|
|
843
|
-
}
|
|
844
|
-
}
|
|
845
|
-
|
|
846
|
-
// Create request object for verification
|
|
847
|
-
const request = {
|
|
848
|
-
method,
|
|
849
|
-
url,
|
|
850
|
-
headers: { ...headers },
|
|
851
|
-
}
|
|
852
|
-
|
|
853
|
-
// Extract additional info from headers
|
|
854
|
-
const signatureName = extractSignatureName(headers)
|
|
855
|
-
const extractedPublicKey = extractPublicKeyFromHeaders(
|
|
856
|
-
headers,
|
|
857
|
-
signatureName
|
|
858
|
-
)
|
|
859
|
-
|
|
860
|
-
// Extract algorithm from signature-input
|
|
861
|
-
const signatureInputHeader =
|
|
862
|
-
headers["signature-input"] || headers["Signature-Input"]
|
|
863
|
-
const algMatch = signatureInputHeader?.match(/alg="([^"]+)"/)
|
|
864
|
-
const algorithm = algMatch ? algMatch[1] : undefined
|
|
865
|
-
|
|
866
|
-
// Verify using the library
|
|
867
|
-
let verified = false
|
|
868
|
-
let verificationError = null
|
|
869
|
-
|
|
870
|
-
try {
|
|
871
|
-
const verificationResult = await verifyMessage(
|
|
872
|
-
{
|
|
873
|
-
keyLookup,
|
|
874
|
-
requiredFields: [], // Don't require specific fields
|
|
875
|
-
},
|
|
876
|
-
request
|
|
877
|
-
)
|
|
878
|
-
// If we get here without throwing, verification succeeded
|
|
879
|
-
verified = true
|
|
880
|
-
} catch (verifyError) {
|
|
881
|
-
// Verification failed
|
|
882
|
-
verificationError = verifyError.message
|
|
883
|
-
verified = false
|
|
884
|
-
}
|
|
885
|
-
|
|
886
|
-
return {
|
|
887
|
-
valid: true, // The signature format is valid
|
|
888
|
-
verified, // Whether the cryptographic verification passed
|
|
889
|
-
signatureName,
|
|
890
|
-
keyId: extractedPublicKey
|
|
891
|
-
? base64url.encode(extractedPublicKey)
|
|
892
|
-
: undefined,
|
|
893
|
-
algorithm,
|
|
894
|
-
publicKeyFromHeader: extractedPublicKey,
|
|
895
|
-
...(verificationError && { error: verificationError }),
|
|
896
|
-
}
|
|
897
|
-
} catch (error) {
|
|
898
|
-
return {
|
|
899
|
-
valid: false,
|
|
900
|
-
error: error.message,
|
|
901
|
-
}
|
|
902
|
-
}
|
|
903
|
-
}
|
|
904
|
-
|
|
905
|
-
/**
|
|
906
|
-
* Extract public key from a signed message
|
|
907
|
-
*
|
|
908
|
-
* @param {Object} signedMessage - The signed message
|
|
909
|
-
* @returns {Buffer|null} The public key buffer or null
|
|
910
|
-
*/
|
|
911
|
-
function extractPublicKeyFromMessage(signedMessage) {
|
|
912
|
-
const signatureName = extractSignatureName(signedMessage.headers)
|
|
913
|
-
return extractPublicKeyFromHeaders(signedMessage.headers, signatureName)
|
|
914
|
-
}
|