wao 0.32.0 → 0.32.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/cjs/hb.js +536 -477
- package/cjs/http-message-signatures/httpbis.js +497 -0
- package/cjs/http-message-signatures/index.js +26 -0
- package/cjs/http-message-signatures/structured-header.js +129 -0
- package/cjs/send.js +11 -7
- package/cjs/signer-utils.js +6 -6
- package/cjs/signer.js +12 -15
- package/cjs/workspace/package.json +1 -1
- package/esm/hb.js +93 -124
- package/esm/http-message-signatures/httpbis.js +438 -0
- package/esm/http-message-signatures/index.js +4 -0
- package/esm/http-message-signatures/structured-header.js +105 -0
- package/esm/send.js +9 -5
- package/esm/signer-utils.js +1 -1
- package/esm/signer.js +11 -16
- package/esm/workspace/package.json +1 -1
- package/package.json +2 -1
|
@@ -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,105 @@
|
|
|
1
|
+
import {
|
|
2
|
+
isInnerList,
|
|
3
|
+
parseDictionary,
|
|
4
|
+
parseItem,
|
|
5
|
+
parseList,
|
|
6
|
+
serializeDictionary,
|
|
7
|
+
serializeInnerList,
|
|
8
|
+
serializeItem,
|
|
9
|
+
serializeList,
|
|
10
|
+
} from "structured-headers"
|
|
11
|
+
|
|
12
|
+
export class Dictionary {
|
|
13
|
+
constructor(input) {
|
|
14
|
+
this.raw = input
|
|
15
|
+
this.parsed = parseDictionary(input)
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
toString() {
|
|
19
|
+
return this.serialize()
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
serialize() {
|
|
23
|
+
return serializeDictionary(this.parsed)
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
has(key) {
|
|
27
|
+
return this.parsed.has(key)
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
get(key) {
|
|
31
|
+
const value = this.parsed.get(key)
|
|
32
|
+
if (!value) {
|
|
33
|
+
return value
|
|
34
|
+
}
|
|
35
|
+
if (isInnerList(value)) {
|
|
36
|
+
return serializeInnerList(value)
|
|
37
|
+
}
|
|
38
|
+
return serializeItem(value)
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export class List {
|
|
43
|
+
constructor(input) {
|
|
44
|
+
this.raw = input
|
|
45
|
+
this.parsed = parseList(input)
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
toString() {
|
|
49
|
+
return this.serialize()
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
serialize() {
|
|
53
|
+
return serializeList(this.parsed)
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export class Item {
|
|
58
|
+
constructor(input) {
|
|
59
|
+
this.raw = input
|
|
60
|
+
this.parsed = parseItem(input)
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
toString() {
|
|
64
|
+
return this.serialize()
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
serialize() {
|
|
68
|
+
return serializeItem(this.parsed)
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export function parseHeader(header) {
|
|
73
|
+
const classes = [List, Dictionary, Item]
|
|
74
|
+
for (let i = 0; i < classes.length; i++) {
|
|
75
|
+
try {
|
|
76
|
+
return new classes[i](header)
|
|
77
|
+
} catch (e) {
|
|
78
|
+
// noop
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
throw new Error("Unable to parse header as structured field")
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* This allows consumers of the library to supply field specifications that aren't
|
|
86
|
+
* strictly "structured fields". Really a string must start with a `"` but that won't
|
|
87
|
+
* tend to happen in our configs.
|
|
88
|
+
*
|
|
89
|
+
* @param {string} input
|
|
90
|
+
* @returns {string}
|
|
91
|
+
*/
|
|
92
|
+
export function quoteString(input) {
|
|
93
|
+
// if it's not quoted, attempt to quote
|
|
94
|
+
if (!input.startsWith('"')) {
|
|
95
|
+
// try to split the structured field
|
|
96
|
+
const [name, ...rest] = input.split(";")
|
|
97
|
+
// no params, just quote the whole thing
|
|
98
|
+
if (!rest.length) {
|
|
99
|
+
return `"${name}"`
|
|
100
|
+
}
|
|
101
|
+
// quote the first part and put the rest back as it was
|
|
102
|
+
return `"${name}";${rest.join(";")}`
|
|
103
|
+
}
|
|
104
|
+
return input
|
|
105
|
+
}
|
package/esm/send.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import base64url from "base64url"
|
|
2
|
-
import { httpbis } from "http-message-signatures"
|
|
2
|
+
import { httpbis } from "./http-message-signatures/index.js"
|
|
3
3
|
import { parseItem, serializeList } from "structured-headers"
|
|
4
4
|
const {
|
|
5
5
|
augmentHeaders,
|
|
@@ -36,7 +36,7 @@ export async function send(signedMsg, fetchImpl = fetch) {
|
|
|
36
36
|
return { ...from(http), ...http }
|
|
37
37
|
}
|
|
38
38
|
|
|
39
|
-
const httpSigName = address => {
|
|
39
|
+
export const httpSigName = address => {
|
|
40
40
|
const decoded = base64url.toBuffer(address)
|
|
41
41
|
const hexString = [...decoded.subarray(1, 9)]
|
|
42
42
|
.map(byte => byte.toString(16).padStart(2, "0"))
|
|
@@ -76,7 +76,13 @@ export const toHttpSigner = signer => {
|
|
|
76
76
|
},
|
|
77
77
|
})
|
|
78
78
|
|
|
79
|
-
|
|
79
|
+
// SORT THE FIELDS HERE to match Erlang's lists:sort(maps:keys(Enc))
|
|
80
|
+
const sortedFields = [...fields].sort()
|
|
81
|
+
|
|
82
|
+
const signatureBaseArray = createSignatureBase(
|
|
83
|
+
{ fields: sortedFields },
|
|
84
|
+
request
|
|
85
|
+
)
|
|
80
86
|
signatureInput = serializeList([
|
|
81
87
|
[
|
|
82
88
|
signatureBaseArray.map(([item]) => parseItem(item)),
|
|
@@ -89,7 +95,6 @@ export const toHttpSigner = signer => {
|
|
|
89
95
|
return new TextEncoder().encode(signatureBase)
|
|
90
96
|
}
|
|
91
97
|
const result = await signer(create, "httpsig")
|
|
92
|
-
|
|
93
98
|
if (!createCalled) {
|
|
94
99
|
throw new Error(
|
|
95
100
|
"create() must be invoked in order to construct the data to sign"
|
|
@@ -107,7 +112,6 @@ export const toHttpSigner = signer => {
|
|
|
107
112
|
signatureInput,
|
|
108
113
|
httpSigName(result.address)
|
|
109
114
|
)
|
|
110
|
-
|
|
111
115
|
const finalHeaders = {}
|
|
112
116
|
for (const [key, value] of Object.entries(signedHeaders)) {
|
|
113
117
|
if (key === "Signature" || key === "Signature-Input") {
|
package/esm/signer-utils.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import base64url from "base64url"
|
|
2
2
|
import crypto from "crypto"
|
|
3
|
-
import { httpbis } from "http-message-signatures"
|
|
3
|
+
import { httpbis } from "./http-message-signatures/index.js"
|
|
4
4
|
import { parseItem, serializeList } from "structured-headers"
|
|
5
5
|
const {
|
|
6
6
|
augmentHeaders,
|
package/esm/signer.js
CHANGED
|
@@ -11,7 +11,7 @@ const joinUrl = ({ url, path }) => {
|
|
|
11
11
|
: url + normalizedPath
|
|
12
12
|
}
|
|
13
13
|
|
|
14
|
-
export async function sign({ url, path, msg: encoded, jwk, signPath =
|
|
14
|
+
export async function sign({ url, path, msg: encoded, jwk, signPath = true }) {
|
|
15
15
|
const signer = createSigner(jwk, url)
|
|
16
16
|
const { body = null, ...headers } = encoded
|
|
17
17
|
let _enc = { headers }
|
|
@@ -25,11 +25,12 @@ async function _sign({
|
|
|
25
25
|
path,
|
|
26
26
|
url,
|
|
27
27
|
method = "POST",
|
|
28
|
-
_path =
|
|
28
|
+
_path = true,
|
|
29
29
|
}) {
|
|
30
30
|
const headersObj = encoded ? encoded.headers : {}
|
|
31
31
|
const body = encoded ? encoded.body : undefined
|
|
32
|
-
|
|
32
|
+
let url_path = typeof _path === "string" ? _path : path
|
|
33
|
+
const _url = joinUrl({ url, path: url_path })
|
|
33
34
|
headersObj["path"] = path
|
|
34
35
|
if (body && !headersObj["content-length"]) {
|
|
35
36
|
const bodySize = body.size || body.byteLength || 0
|
|
@@ -45,18 +46,12 @@ async function _sign({
|
|
|
45
46
|
.split(",")
|
|
46
47
|
.map(k => k.trim())
|
|
47
48
|
: []
|
|
48
|
-
|
|
49
|
-
const signingFields = Object.keys(lowercaseHeaders).filter(
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
if (_path) signingFields.push("path")
|
|
54
|
-
/*
|
|
55
|
-
if (signingFields.length === 0 && !body) {
|
|
56
|
-
//lowercaseHeaders["content-length"] = "0"
|
|
57
|
-
//signingFields.push("content-length")
|
|
58
|
-
}
|
|
59
|
-
*/
|
|
49
|
+
let isPath = false
|
|
50
|
+
const signingFields = Object.keys(lowercaseHeaders).filter(key => {
|
|
51
|
+
if (key === "path") isPath = true
|
|
52
|
+
return key !== "body-keys" && key !== "path" && !bodyKeys.includes(key)
|
|
53
|
+
})
|
|
54
|
+
if (_path !== false && isPath) signingFields.push("@path")
|
|
60
55
|
const signedRequest = await toHttpSigner(signer)({
|
|
61
56
|
request: { url: _url, method, headers: lowercaseHeaders },
|
|
62
57
|
fields: signingFields,
|
|
@@ -85,7 +80,7 @@ export function signer(config) {
|
|
|
85
80
|
if (!signer) throw new Error("Signer is required for mainnet mode")
|
|
86
81
|
return async (
|
|
87
82
|
fields,
|
|
88
|
-
{ encoded: _encoded = false, path: _path =
|
|
83
|
+
{ encoded: _encoded = false, path: _path = true } = {}
|
|
89
84
|
) => {
|
|
90
85
|
const { path = "/relay/process", method = "POST", ...aoFields } = fields
|
|
91
86
|
const encoded = _encoded ? aoFields : await enc(aoFields)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "wao",
|
|
3
|
-
"version": "0.32.
|
|
3
|
+
"version": "0.32.2",
|
|
4
4
|
"bin": {
|
|
5
5
|
"wao": "./cjs/cli.js",
|
|
6
6
|
"wao-esm": "./esm/cli.js"
|
|
@@ -56,6 +56,7 @@
|
|
|
56
56
|
"md5": "^2.3.0",
|
|
57
57
|
"pm2": "^5.4.3",
|
|
58
58
|
"ramda": "^0.30.1",
|
|
59
|
+
"structured-headers": "1.0.1",
|
|
59
60
|
"warp-arbundles": "^1.0.4",
|
|
60
61
|
"wasm-brotli": "^2.0.2",
|
|
61
62
|
"yargs": "^17.7.2"
|