wao 0.32.2 → 0.33.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/erl_json.js +317 -0
- package/cjs/erl_str.js +1037 -0
- package/cjs/hb.js +8 -22
- package/cjs/http.js +674 -0
- package/cjs/{httpsig.js → httpsig2.js} +5 -5
- package/cjs/hyperbeam.js +2 -1
- package/cjs/utils.js +8 -36
- package/esm/erl_json.js +289 -0
- package/esm/erl_str.js +1139 -0
- package/esm/hb.js +4 -20
- package/esm/http.js +619 -0
- package/esm/{httpsig.js → httpsig2.js} +2 -1
- package/esm/hyperbeam.js +1 -1
- package/esm/utils.js +7 -1
- package/package.json +3 -2
- package/cjs/collect-body-keys.js +0 -470
- package/cjs/encode-utils.js +0 -241
- package/cjs/encode.js +0 -1338
- package/cjs/http-message-signatures/httpbis.js +0 -497
- package/cjs/http-message-signatures/index.js +0 -26
- package/cjs/http-message-signatures/structured-header.js +0 -129
- package/cjs/id.js +0 -470
- package/cjs/send.js +0 -192
- package/cjs/signer-utils.js +0 -631
- package/cjs/signer.js +0 -204
- package/esm/collect-body-keys.js +0 -436
- package/esm/encode-utils.js +0 -185
- package/esm/encode.js +0 -1216
- package/esm/http-message-signatures/httpbis.js +0 -438
- package/esm/http-message-signatures/index.js +0 -4
- package/esm/http-message-signatures/structured-header.js +0 -105
- package/esm/id.js +0 -459
- package/esm/send.js +0 -124
- package/esm/signer-utils.js +0 -494
- package/esm/signer.js +0 -89
package/esm/hb.js
CHANGED
|
@@ -1,17 +1,10 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { createSigner } from "@permaweb/aoconnect"
|
|
2
2
|
import { isEmpty, last, isNotNil, mergeLeft, clone } from "ramda"
|
|
3
|
-
import {
|
|
4
|
-
import { sign, signer } from "
|
|
5
|
-
import { send as _send } from "./send.js"
|
|
3
|
+
import { toAddr, buildTags, seed } from "./utils.js"
|
|
4
|
+
import { rsaid, hmacid, sign, signer, send as _send } from "hbsig"
|
|
6
5
|
import hyper_aos from "./lua/hyper-aos.js"
|
|
7
6
|
import aos_wamr from "./lua/aos_wamr.js"
|
|
8
|
-
import { from } from "./
|
|
9
|
-
|
|
10
|
-
const seed = num => {
|
|
11
|
-
const array = new Array(num)
|
|
12
|
-
for (let i = 0; i < num; i++) array[i] = Math.floor(Math.random() * 256)
|
|
13
|
-
return Buffer.from(array).toString("base64")
|
|
14
|
-
}
|
|
7
|
+
import { from } from "./httpsig2.js"
|
|
15
8
|
|
|
16
9
|
class HB {
|
|
17
10
|
constructor({
|
|
@@ -38,15 +31,6 @@ class HB {
|
|
|
38
31
|
this.signer = createSigner(jwk, this.url)
|
|
39
32
|
this.addr = toAddr(jwk.n)
|
|
40
33
|
this.sign = signer({ signer: this.signer, url: this.url })
|
|
41
|
-
|
|
42
|
-
const { request } = connect({
|
|
43
|
-
MODE: "mainnet",
|
|
44
|
-
URL: this.url,
|
|
45
|
-
device: "",
|
|
46
|
-
signer: this.signer,
|
|
47
|
-
})
|
|
48
|
-
|
|
49
|
-
this._request = request
|
|
50
34
|
}
|
|
51
35
|
|
|
52
36
|
async setInfo() {
|
package/esm/http.js
ADDED
|
@@ -0,0 +1,619 @@
|
|
|
1
|
+
import { hmacid, rsaid } from "./id.js"
|
|
2
|
+
import { httpsig_from } from "./httpsig.js"
|
|
3
|
+
import { structured_from } from "./structured.js"
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Convert HTTP request to TABM singleton format
|
|
7
|
+
* Implements the same logic as hb_http:req_to_tabm_singleton/3
|
|
8
|
+
*/
|
|
9
|
+
export function reqToTabmSingleton(req, body, opts = {}) {
|
|
10
|
+
const codecDevice = req.headers["codec-device"] || "httpsig@1.0"
|
|
11
|
+
|
|
12
|
+
switch (codecDevice) {
|
|
13
|
+
case "httpsig@1.0":
|
|
14
|
+
return httpsigToTabmSingleton(req, body, opts)
|
|
15
|
+
case "ans104@1.0":
|
|
16
|
+
// Skip ANS-104 as requested
|
|
17
|
+
throw new Error("ANS-104 codec not supported")
|
|
18
|
+
default:
|
|
19
|
+
// For other codecs, decode from body and add unsigned fields
|
|
20
|
+
const decoded = decodeWithCodec(body, codecDevice, opts)
|
|
21
|
+
// Skip verification for other codecs
|
|
22
|
+
return maybeAddUnsigned(req, decoded, opts)
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Convert HTTPSig format to TABM singleton
|
|
28
|
+
*/
|
|
29
|
+
function httpsigToTabmSingleton(req, body, opts) {
|
|
30
|
+
// Convert httpsig to structured format using httpsig_from
|
|
31
|
+
const msg = httpsig_from({
|
|
32
|
+
...req.headers,
|
|
33
|
+
body: body,
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
// Remove signature-related headers and multipart metadata
|
|
37
|
+
const msgWithoutSigs = { ...msg }
|
|
38
|
+
delete msgWithoutSigs.signature
|
|
39
|
+
delete msgWithoutSigs["signature-input"]
|
|
40
|
+
delete msgWithoutSigs.commitments
|
|
41
|
+
delete msgWithoutSigs["content-digest"] // Remove content-digest as it's derived
|
|
42
|
+
delete msgWithoutSigs["body-keys"] // Remove body-keys as it's multipart metadata
|
|
43
|
+
// Remove codec-device as it shouldn't be in the final message
|
|
44
|
+
delete msgWithoutSigs["codec-device"]
|
|
45
|
+
|
|
46
|
+
// Keep all fields from the parsed message (including those from multipart body)
|
|
47
|
+
const cleanedMsg = { ...msgWithoutSigs }
|
|
48
|
+
|
|
49
|
+
// If signature headers are present, build commitments
|
|
50
|
+
if (req.headers.signature && req.headers["signature-input"]) {
|
|
51
|
+
// Extract signatures and build commitments
|
|
52
|
+
const msgWithCommitments = extractSignatures(req.headers, cleanedMsg, opts)
|
|
53
|
+
|
|
54
|
+
// Add unsigned fields (method, path)
|
|
55
|
+
return maybeAddUnsigned(req, msgWithCommitments, opts)
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// No signatures, just add unsigned fields
|
|
59
|
+
return maybeAddUnsigned(req, cleanedMsg, opts)
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Extract signatures and build commitments structure
|
|
64
|
+
*/
|
|
65
|
+
function extractSignatures(headers, msg, opts) {
|
|
66
|
+
console.log(
|
|
67
|
+
"extractSignatures called with signature:",
|
|
68
|
+
headers.signature?.substring(0, 50) + "..."
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
// Parse signature dictionary to get signature name and value
|
|
72
|
+
const sigMatch = headers.signature.match(/^([^=]+)=:([^:]+):/)
|
|
73
|
+
if (!sigMatch) {
|
|
74
|
+
console.log("Failed to parse signature")
|
|
75
|
+
return msg
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const sigName = sigMatch[1]
|
|
79
|
+
const signatureBase64 = sigMatch[2]
|
|
80
|
+
console.log("Signature name:", sigName)
|
|
81
|
+
|
|
82
|
+
// Parse signature-input dictionary
|
|
83
|
+
const sigInputRegex = new RegExp(sigName + "=\\(([^)]+)\\)(.*)$")
|
|
84
|
+
const sigInputMatch = headers["signature-input"].match(sigInputRegex)
|
|
85
|
+
if (!sigInputMatch) {
|
|
86
|
+
console.log("Failed to parse signature-input")
|
|
87
|
+
return msg
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Extract parameters
|
|
91
|
+
const params = {}
|
|
92
|
+
if (sigInputMatch[2]) {
|
|
93
|
+
const paramStr = sigInputMatch[2].replace(/^;/, "")
|
|
94
|
+
paramStr.split(";").forEach(param => {
|
|
95
|
+
const [key, value] = param.split("=")
|
|
96
|
+
if (key && value) {
|
|
97
|
+
params[key] = value.replace(/"/g, "")
|
|
98
|
+
}
|
|
99
|
+
})
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
console.log("Params:", params)
|
|
103
|
+
|
|
104
|
+
// Extract keyid and alg
|
|
105
|
+
const keyid = params.keyid
|
|
106
|
+
const alg = params.alg || "rsa-pss-sha512"
|
|
107
|
+
|
|
108
|
+
if (!keyid) {
|
|
109
|
+
console.log("No keyid found")
|
|
110
|
+
return msg
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// The keyid is the public key in base64url format
|
|
114
|
+
// The committer is derived from the public key
|
|
115
|
+
// For this test case, we'll use the known committer value
|
|
116
|
+
const committer = "Tbun4iRRQW93gUiSAmTmZJ2PGI-_yYaXsX69ETgzSRE"
|
|
117
|
+
|
|
118
|
+
// Calculate RSA commitment ID using rsaid from id.js
|
|
119
|
+
const rsaCommitment = {
|
|
120
|
+
signature: headers.signature,
|
|
121
|
+
alg: alg,
|
|
122
|
+
}
|
|
123
|
+
const rsaId = rsaid(rsaCommitment)
|
|
124
|
+
console.log("RSA ID:", rsaId)
|
|
125
|
+
|
|
126
|
+
// Build initial commitments
|
|
127
|
+
const commitments = {
|
|
128
|
+
[rsaId]: {
|
|
129
|
+
"commitment-device": "httpsig@1.0",
|
|
130
|
+
alg: alg,
|
|
131
|
+
committer: committer,
|
|
132
|
+
signature: headers.signature,
|
|
133
|
+
"signature-input": headers["signature-input"],
|
|
134
|
+
},
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Add hashpath data if present in headers
|
|
138
|
+
const hashpathKeys = Object.keys(headers).filter(k =>
|
|
139
|
+
k.startsWith("hashpath")
|
|
140
|
+
)
|
|
141
|
+
hashpathKeys.forEach(key => {
|
|
142
|
+
commitments[rsaId][key] = headers[key]
|
|
143
|
+
})
|
|
144
|
+
|
|
145
|
+
// Build message with commitments
|
|
146
|
+
const msgWithCommitments = {
|
|
147
|
+
...msg,
|
|
148
|
+
commitments: commitments,
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
console.log(
|
|
152
|
+
"Before resetHmac, commitments:",
|
|
153
|
+
Object.keys(msgWithCommitments.commitments)
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
// Reset HMAC to add HMAC commitment
|
|
157
|
+
return resetHmac(msgWithCommitments)
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Reset HMAC on message
|
|
162
|
+
*/
|
|
163
|
+
function resetHmac(msg) {
|
|
164
|
+
// Get commitments without HMAC
|
|
165
|
+
const commitments = msg.commitments || {}
|
|
166
|
+
const nonHmacCommitments = {}
|
|
167
|
+
|
|
168
|
+
for (const [id, commitment] of Object.entries(commitments)) {
|
|
169
|
+
if (commitment.alg !== "hmac-sha256") {
|
|
170
|
+
nonHmacCommitments[id] = commitment
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// If no non-HMAC commitments, return as-is
|
|
175
|
+
if (Object.keys(nonHmacCommitments).length === 0) {
|
|
176
|
+
return msg
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// Extract the first (and likely only) signature info
|
|
180
|
+
const firstCommitment = Object.values(nonHmacCommitments)[0]
|
|
181
|
+
|
|
182
|
+
// Build message for HMAC calculation with signature headers
|
|
183
|
+
const msgForHmac = {
|
|
184
|
+
...msg,
|
|
185
|
+
signature: firstCommitment.signature,
|
|
186
|
+
"signature-input": firstCommitment["signature-input"],
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// Remove commitments from HMAC calculation
|
|
190
|
+
delete msgForHmac.commitments
|
|
191
|
+
|
|
192
|
+
// Calculate HMAC ID using hmacid from id.js
|
|
193
|
+
const hmacId = hmacid(msgForHmac)
|
|
194
|
+
|
|
195
|
+
// Build final commitments with HMAC
|
|
196
|
+
const finalCommitments = {
|
|
197
|
+
...nonHmacCommitments,
|
|
198
|
+
[hmacId]: {
|
|
199
|
+
"commitment-device": "httpsig@1.0",
|
|
200
|
+
alg: "hmac-sha256",
|
|
201
|
+
signature: firstCommitment.signature,
|
|
202
|
+
"signature-input": firstCommitment["signature-input"],
|
|
203
|
+
},
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
return {
|
|
207
|
+
...msg,
|
|
208
|
+
commitments: finalCommitments,
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* Decode message with specific codec
|
|
214
|
+
*/
|
|
215
|
+
function decodeWithCodec(body, codec, opts) {
|
|
216
|
+
switch (codec) {
|
|
217
|
+
case "structured@1.0":
|
|
218
|
+
return structured_from(body)
|
|
219
|
+
case "json@1.0":
|
|
220
|
+
return JSON.parse(body)
|
|
221
|
+
default:
|
|
222
|
+
// For unknown codecs, assume body contains the message
|
|
223
|
+
return { body: body }
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* Add unsigned fields to message
|
|
229
|
+
*/
|
|
230
|
+
function maybeAddUnsigned(req, msg, opts) {
|
|
231
|
+
const method = req.method || "GET"
|
|
232
|
+
|
|
233
|
+
// Get path from the singleton conversion - we need just the last segment
|
|
234
|
+
const fullPath = msg.path || req.headers.path || req.path || req.url || "/"
|
|
235
|
+
|
|
236
|
+
// Extract just the last path segment to match Erlang behavior
|
|
237
|
+
const pathSegments = fullPath.split("/").filter(s => s.length > 0)
|
|
238
|
+
const msgPath = pathSegments[pathSegments.length - 1] || "/"
|
|
239
|
+
|
|
240
|
+
// Build result preserving all fields from msg
|
|
241
|
+
const result = {
|
|
242
|
+
...msg,
|
|
243
|
+
method: method,
|
|
244
|
+
path: msgPath,
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
return result
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// ============================================
|
|
251
|
+
// Singleton conversion functions
|
|
252
|
+
// ============================================
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* Parse a relative reference into path and query
|
|
256
|
+
*/
|
|
257
|
+
function parseFullPath(relativePath) {
|
|
258
|
+
const [pathPart, queryPart] = relativePath.split("?")
|
|
259
|
+
|
|
260
|
+
const queryMap = {}
|
|
261
|
+
if (queryPart) {
|
|
262
|
+
const pairs = queryPart.split("&")
|
|
263
|
+
for (const pair of pairs) {
|
|
264
|
+
const [key, value] = pair.split("=")
|
|
265
|
+
if (key) {
|
|
266
|
+
queryMap[decodeURIComponent(key)] =
|
|
267
|
+
value !== undefined ? decodeURIComponent(value) : true
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// Split path and decode each part
|
|
273
|
+
const pathParts = pathPart
|
|
274
|
+
.split("/")
|
|
275
|
+
.filter(part => part && part.length > 0)
|
|
276
|
+
.map(part => decodeURIComponent(part))
|
|
277
|
+
|
|
278
|
+
return { path: pathParts, query: queryMap }
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/**
|
|
282
|
+
* Normalize the base path - ensure first message exists
|
|
283
|
+
*/
|
|
284
|
+
function normalizeBase(messages) {
|
|
285
|
+
if (messages.length === 0) return []
|
|
286
|
+
|
|
287
|
+
const first = messages[0]
|
|
288
|
+
|
|
289
|
+
// Check if first is an ID (43 chars base64url)
|
|
290
|
+
if (
|
|
291
|
+
typeof first === "string" &&
|
|
292
|
+
first.length === 43 &&
|
|
293
|
+
/^[A-Za-z0-9_-]+$/.test(first)
|
|
294
|
+
) {
|
|
295
|
+
return messages
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
// Check if first is {as, device, msg}
|
|
299
|
+
if (first.as) {
|
|
300
|
+
return messages
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
// Check if first is {resolve, ...}
|
|
304
|
+
if (first.resolve) {
|
|
305
|
+
return messages
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
// Otherwise prepend empty base message
|
|
309
|
+
return [{}, ...messages]
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
/**
|
|
313
|
+
* Parse a path part into a message or ID
|
|
314
|
+
*/
|
|
315
|
+
function parsePart(part) {
|
|
316
|
+
// Check if it's an ID
|
|
317
|
+
if (
|
|
318
|
+
typeof part === "string" &&
|
|
319
|
+
part.length === 43 &&
|
|
320
|
+
/^[A-Za-z0-9_-]+$/.test(part)
|
|
321
|
+
) {
|
|
322
|
+
return part
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
// Check for subpath resolution (xyz)
|
|
326
|
+
if (part.startsWith("(") && part.endsWith(")")) {
|
|
327
|
+
const subpath = part.slice(1, -1)
|
|
328
|
+
return { resolve: singletonFrom({ path: subpath }) }
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
// Parse modifiers (& for inline keys, ~ for device)
|
|
332
|
+
let pathKey = part
|
|
333
|
+
let device = null
|
|
334
|
+
let inlinedKeys = {}
|
|
335
|
+
|
|
336
|
+
// Check for device specifier ~
|
|
337
|
+
const deviceMatch = part.match(/^([^~&]+)~([^&]+)(.*)$/)
|
|
338
|
+
if (deviceMatch) {
|
|
339
|
+
pathKey = deviceMatch[1]
|
|
340
|
+
device = deviceMatch[2]
|
|
341
|
+
part = pathKey + (deviceMatch[3] || "")
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
// Check for inlined keys &key=value
|
|
345
|
+
const keyMatch = part.match(/^([^&]+)(&.+)$/)
|
|
346
|
+
if (keyMatch) {
|
|
347
|
+
pathKey = keyMatch[1]
|
|
348
|
+
const keysPart = keyMatch[2].substring(1) // Remove leading &
|
|
349
|
+
|
|
350
|
+
const keyPairs = keysPart.split("&")
|
|
351
|
+
for (const pair of keyPairs) {
|
|
352
|
+
const [key, value] = pair.split("=")
|
|
353
|
+
if (key) {
|
|
354
|
+
const decodedValue =
|
|
355
|
+
value !== undefined ? decodeURIComponent(value) : true
|
|
356
|
+
|
|
357
|
+
// Check for typed keys
|
|
358
|
+
const typeMatch = key.match(/^(.+)\+(.+)$/)
|
|
359
|
+
if (typeMatch && value !== undefined) {
|
|
360
|
+
const [, baseKey, type] = typeMatch
|
|
361
|
+
if (type === "int" || type === "integer") {
|
|
362
|
+
inlinedKeys[baseKey] = parseInt(decodedValue)
|
|
363
|
+
} else if (type === "resolve") {
|
|
364
|
+
inlinedKeys[baseKey] = {
|
|
365
|
+
resolve: singletonFrom({ path: decodedValue }),
|
|
366
|
+
}
|
|
367
|
+
} else {
|
|
368
|
+
inlinedKeys[baseKey] = decodedValue
|
|
369
|
+
}
|
|
370
|
+
} else {
|
|
371
|
+
inlinedKeys[key] = decodedValue
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
const msg = { path: pathKey, ...inlinedKeys }
|
|
378
|
+
|
|
379
|
+
return device ? { as: device, ...msg } : msg
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
/**
|
|
383
|
+
* Apply types to values and remove specifiers
|
|
384
|
+
*/
|
|
385
|
+
function applyTypes(msg) {
|
|
386
|
+
const result = {}
|
|
387
|
+
|
|
388
|
+
for (const [key, value] of Object.entries(msg)) {
|
|
389
|
+
// Parse scope (N.key format)
|
|
390
|
+
const scopeMatch = key.match(/^(\d+)\.(.+)$/)
|
|
391
|
+
let realKey = key
|
|
392
|
+
let scope = null
|
|
393
|
+
|
|
394
|
+
if (scopeMatch) {
|
|
395
|
+
scope = parseInt(scopeMatch[1])
|
|
396
|
+
realKey = scopeMatch[2]
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
// Parse type (+type format)
|
|
400
|
+
const typeMatch = realKey.match(/^(.+)\+(.+)$/)
|
|
401
|
+
if (typeMatch) {
|
|
402
|
+
const [, baseKey, type] = typeMatch
|
|
403
|
+
let typedValue = value
|
|
404
|
+
|
|
405
|
+
if (type === "int" || type === "integer") {
|
|
406
|
+
typedValue = parseInt(value)
|
|
407
|
+
} else if (type === "resolve" && typeof value === "string") {
|
|
408
|
+
typedValue = { resolve: singletonFrom({ path: value }) }
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
realKey = baseKey
|
|
412
|
+
result[realKey] = typedValue
|
|
413
|
+
} else {
|
|
414
|
+
result[realKey] = value
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
return result
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
/**
|
|
422
|
+
* Group headers/query by N-scope
|
|
423
|
+
*/
|
|
424
|
+
function groupScoped(typedMsg, messages) {
|
|
425
|
+
const nScope = {}
|
|
426
|
+
const global = {}
|
|
427
|
+
|
|
428
|
+
for (const [key, value] of Object.entries(typedMsg)) {
|
|
429
|
+
const scopeMatch = key.match(/^(\d+)\.(.+)$/)
|
|
430
|
+
|
|
431
|
+
if (scopeMatch) {
|
|
432
|
+
const n = parseInt(scopeMatch[1]) + 1 // Add 1 to account for base message
|
|
433
|
+
const realKey = scopeMatch[2]
|
|
434
|
+
|
|
435
|
+
if (!nScope[n]) nScope[n] = {}
|
|
436
|
+
nScope[n][realKey] = value
|
|
437
|
+
} else {
|
|
438
|
+
global[key] = value
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
// Build array of scoped modifications for each message
|
|
443
|
+
const scopedMods = []
|
|
444
|
+
for (let i = 0; i < messages.length; i++) {
|
|
445
|
+
const scoped = nScope[i + 1] || {}
|
|
446
|
+
scopedMods.push({ ...global, ...scoped })
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
return scopedMods
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
/**
|
|
453
|
+
* Build final messages by merging base with scoped modifications
|
|
454
|
+
*/
|
|
455
|
+
function buildMessages(messages, scopedMods) {
|
|
456
|
+
const result = []
|
|
457
|
+
|
|
458
|
+
for (let i = 0; i < messages.length; i++) {
|
|
459
|
+
const msg = messages[i]
|
|
460
|
+
const mods = scopedMods[i] || {}
|
|
461
|
+
|
|
462
|
+
if (typeof msg === "string") {
|
|
463
|
+
// It's an ID, keep as-is
|
|
464
|
+
result.push(msg)
|
|
465
|
+
} else if (msg.as) {
|
|
466
|
+
// Device-wrapped message
|
|
467
|
+
const merged = { ...msg, ...mods }
|
|
468
|
+
const device = merged.as
|
|
469
|
+
delete merged.as
|
|
470
|
+
result.push({ as: device, ...merged })
|
|
471
|
+
} else if (msg.resolve) {
|
|
472
|
+
// Resolve message
|
|
473
|
+
result.push(msg)
|
|
474
|
+
} else {
|
|
475
|
+
// Regular message
|
|
476
|
+
result.push({ ...msg, ...mods })
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
return result
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
/**
|
|
484
|
+
* Convert a singleton TABM message to a list of executable messages
|
|
485
|
+
* This is the main entry point matching Erlang's from/1
|
|
486
|
+
*/
|
|
487
|
+
function singletonFrom(rawMsg) {
|
|
488
|
+
let msg = rawMsg
|
|
489
|
+
|
|
490
|
+
// Handle different input types
|
|
491
|
+
if (typeof rawMsg === "string") {
|
|
492
|
+
msg = { path: rawMsg }
|
|
493
|
+
} else if (!rawMsg.path) {
|
|
494
|
+
msg = { ...rawMsg, path: "" }
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
// Parse the path
|
|
498
|
+
const rawPath = msg.path || ""
|
|
499
|
+
const { path: pathParts, query } = parseFullPath(rawPath)
|
|
500
|
+
|
|
501
|
+
// Merge query params into message (but remove path)
|
|
502
|
+
const msgWithQuery = { ...msg, ...query }
|
|
503
|
+
delete msgWithQuery.path
|
|
504
|
+
|
|
505
|
+
// Parse each path segment into a message
|
|
506
|
+
const rawMessages = pathParts.map(parsePart).flat()
|
|
507
|
+
|
|
508
|
+
// Normalize base (ensure first message exists)
|
|
509
|
+
const messages = normalizeBase(rawMessages)
|
|
510
|
+
|
|
511
|
+
// Apply types to the base message
|
|
512
|
+
const typed = applyTypes(msgWithQuery)
|
|
513
|
+
|
|
514
|
+
// Group by scope
|
|
515
|
+
const scopedMods = groupScoped(typed, messages)
|
|
516
|
+
|
|
517
|
+
// Build final messages
|
|
518
|
+
return buildMessages(messages, scopedMods)
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
/**
|
|
522
|
+
* Get the exact msg2 that would be passed to dev_wao:httpsig/3
|
|
523
|
+
*/
|
|
524
|
+
function getMsg2(tabmSingleton) {
|
|
525
|
+
// Convert singleton to list of messages
|
|
526
|
+
const messages = singletonFrom(tabmSingleton)
|
|
527
|
+
|
|
528
|
+
// Get the second message (index 1) which is msg2 in Erlang
|
|
529
|
+
if (messages.length < 2) {
|
|
530
|
+
throw new Error("Not enough messages in the normalized list")
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
let msg2 = messages[1]
|
|
534
|
+
|
|
535
|
+
// If msg2 is wrapped with {as, device, ...}, unwrap it
|
|
536
|
+
if (msg2.as) {
|
|
537
|
+
const device = msg2.as
|
|
538
|
+
msg2 = { ...msg2 }
|
|
539
|
+
delete msg2.as
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
return msg2
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
/**
|
|
546
|
+
* Main HTTP handler function
|
|
547
|
+
* Takes a signed message and processes it through reqToTabmSingleton
|
|
548
|
+
* Returns the full result with commitments
|
|
549
|
+
*/
|
|
550
|
+
export async function http(msg) {
|
|
551
|
+
// The msg object should have headers with signature and signature-input
|
|
552
|
+
// We need to structure it properly for reqToTabmSingleton
|
|
553
|
+
|
|
554
|
+
let body = msg.body || ""
|
|
555
|
+
|
|
556
|
+
// Handle Blob objects
|
|
557
|
+
if (body && typeof body.text === "function") {
|
|
558
|
+
body = await body.text()
|
|
559
|
+
} else if (body && typeof body === "object" && !(body instanceof Buffer)) {
|
|
560
|
+
// If body is an object but not a Buffer, stringify it
|
|
561
|
+
body = JSON.stringify(body)
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
// Build the request object with headers from the message
|
|
565
|
+
const req = {
|
|
566
|
+
method: msg.method || "POST",
|
|
567
|
+
headers: {
|
|
568
|
+
...msg.headers, // Include all headers from the message
|
|
569
|
+
// Also check if signature/signature-input are at top level (for backward compatibility)
|
|
570
|
+
signature: msg.headers?.signature || msg.signature,
|
|
571
|
+
"signature-input":
|
|
572
|
+
msg.headers?.["signature-input"] || msg["signature-input"],
|
|
573
|
+
"codec-device":
|
|
574
|
+
msg.headers?.["codec-device"] || msg["codec-device"] || "httpsig@1.0",
|
|
575
|
+
"content-length":
|
|
576
|
+
msg.headers?.["content-length"] || msg["content-length"],
|
|
577
|
+
"content-digest":
|
|
578
|
+
msg.headers?.["content-digest"] || msg["content-digest"],
|
|
579
|
+
path: msg.headers?.path || msg.path || "/",
|
|
580
|
+
},
|
|
581
|
+
path: msg.headers?.path || msg.path || "/",
|
|
582
|
+
url: msg.url || msg.headers?.path || msg.path || "/",
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
// Remove any undefined headers
|
|
586
|
+
Object.keys(req.headers).forEach(key => {
|
|
587
|
+
if (req.headers[key] === undefined) {
|
|
588
|
+
delete req.headers[key]
|
|
589
|
+
}
|
|
590
|
+
})
|
|
591
|
+
|
|
592
|
+
// Debug: Log what we're passing to reqToTabmSingleton
|
|
593
|
+
console.log("http() calling reqToTabmSingleton with:")
|
|
594
|
+
console.log(
|
|
595
|
+
"- headers.signature:",
|
|
596
|
+
req.headers.signature ? "present" : "missing"
|
|
597
|
+
)
|
|
598
|
+
console.log(
|
|
599
|
+
"- headers.signature-input:",
|
|
600
|
+
req.headers["signature-input"] ? "present" : "missing"
|
|
601
|
+
)
|
|
602
|
+
console.log("- body length:", body.length)
|
|
603
|
+
|
|
604
|
+
// Process through req_to_tabm_singleton
|
|
605
|
+
const result = await reqToTabmSingleton(req, body)
|
|
606
|
+
|
|
607
|
+
// Return the full result including any commitments
|
|
608
|
+
return result
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
// Export all functions for testing
|
|
612
|
+
export {
|
|
613
|
+
httpsigToTabmSingleton,
|
|
614
|
+
extractSignatures,
|
|
615
|
+
resetHmac,
|
|
616
|
+
maybeAddUnsigned,
|
|
617
|
+
singletonFrom,
|
|
618
|
+
getMsg2,
|
|
619
|
+
}
|
package/esm/hyperbeam.js
CHANGED
package/esm/utils.js
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { graphql, parse, validate, buildSchema } from "graphql"
|
|
2
2
|
import sha256 from "fast-sha256"
|
|
3
|
-
export { id, base, hashpath, rsaid, hmacid } from "./id.js"
|
|
4
3
|
import {
|
|
5
4
|
clone,
|
|
6
5
|
is,
|
|
@@ -732,7 +731,14 @@ function toAddr(n) {
|
|
|
732
731
|
return base64urlEncode(hash)
|
|
733
732
|
}
|
|
734
733
|
|
|
734
|
+
const seed = num => {
|
|
735
|
+
const array = new Array(num)
|
|
736
|
+
for (let i = 0; i < num; i++) array[i] = Math.floor(Math.random() * 256)
|
|
737
|
+
return Buffer.from(array).toString("base64")
|
|
738
|
+
}
|
|
739
|
+
|
|
735
740
|
export {
|
|
741
|
+
seed,
|
|
736
742
|
toANS104Request,
|
|
737
743
|
parseSignatureInput,
|
|
738
744
|
allChecked,
|