wao 0.27.3 → 0.28.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/collect-body-keys.js +470 -0
- package/cjs/encode-array-item.js +110 -0
- package/cjs/encode-utils.js +241 -0
- package/cjs/encode.js +1191 -1921
- package/cjs/id.js +234 -0
- package/cjs/send.js +4 -0
- package/cjs/signer-utils.js +1 -0
- package/cjs/signer.js +59 -10
- package/cjs/utils.js +15 -1
- package/esm/collect-body-keys.js +436 -0
- package/esm/encode-array-item.js +112 -0
- package/esm/encode-utils.js +185 -0
- package/esm/encode.js +881 -1531
- package/esm/id.js +222 -0
- package/esm/send.js +4 -3
- package/esm/signer-utils.js +1 -1
- package/esm/signer.js +33 -8
- package/esm/utils.js +7 -0
- package/package.json +1 -1
package/esm/id.js
ADDED
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
import { hash, hmac } from "fast-sha256"
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Parse structured field dictionary format
|
|
5
|
+
* Handles both complex format: name=(components);params
|
|
6
|
+
* and simple format: name=:value:
|
|
7
|
+
*/
|
|
8
|
+
function parseStructuredFieldDictionary(input) {
|
|
9
|
+
// Try complex format first
|
|
10
|
+
const match = input.match(/([^=]+)=\((.*?)\);(.*)$/)
|
|
11
|
+
if (match) {
|
|
12
|
+
const name = match[1]
|
|
13
|
+
const components = match[2].split(" ")
|
|
14
|
+
const params = {}
|
|
15
|
+
|
|
16
|
+
const paramPairs = match[3].split(";").filter(p => p)
|
|
17
|
+
paramPairs.forEach(pair => {
|
|
18
|
+
const [key, value] = pair.split("=")
|
|
19
|
+
if (key && value) {
|
|
20
|
+
params[key] = value.replace(/"/g, "")
|
|
21
|
+
}
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
return { name, components, params }
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// Try simple format
|
|
28
|
+
const simpleMatch = input.match(/([^=]+)=:([^:]+):/)
|
|
29
|
+
if (simpleMatch) {
|
|
30
|
+
return { name: simpleMatch[1], value: simpleMatch[2] }
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
return null
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Convert base64url string to base64
|
|
38
|
+
*/
|
|
39
|
+
function base64urlToBase64(str) {
|
|
40
|
+
return str.replace(/-/g, "+").replace(/_/g, "/")
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Convert Uint8Array to base64url string
|
|
45
|
+
*/
|
|
46
|
+
function uint8ArrayToBase64url(bytes) {
|
|
47
|
+
// Convert to base64
|
|
48
|
+
let binary = ""
|
|
49
|
+
for (let i = 0; i < bytes.length; i++) {
|
|
50
|
+
binary += String.fromCharCode(bytes[i])
|
|
51
|
+
}
|
|
52
|
+
const base64 = btoa(binary)
|
|
53
|
+
|
|
54
|
+
// Convert to base64url
|
|
55
|
+
return base64.replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, "")
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Generate commitment ID for RSA-PSS and ECDSA signatures
|
|
60
|
+
* The ID is the SHA256 hash of the raw signature bytes
|
|
61
|
+
*
|
|
62
|
+
* @param {Object} commitment - The commitment object containing signature
|
|
63
|
+
* @returns {string} The commitment ID in base64url format
|
|
64
|
+
*/
|
|
65
|
+
function generateRsaCommitmentId(commitment) {
|
|
66
|
+
// Extract the base64 signature from structured field format
|
|
67
|
+
// Format: "signature-name=:BASE64_SIGNATURE:"
|
|
68
|
+
const match = commitment.signature.match(/^[^=]+=:([^:]+):/)
|
|
69
|
+
if (!match) {
|
|
70
|
+
throw new Error("Invalid signature format")
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const signatureBase64 = match[1]
|
|
74
|
+
// Convert base64 to Uint8Array
|
|
75
|
+
const signatureBinary = Uint8Array.from(atob(signatureBase64), c =>
|
|
76
|
+
c.charCodeAt(0)
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
// SHA256 hash of the raw signature
|
|
80
|
+
const hashResult = hash(signatureBinary)
|
|
81
|
+
const id = uint8ArrayToBase64url(hashResult)
|
|
82
|
+
|
|
83
|
+
return id
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Generate HMAC commitment ID for HyperBEAM messages
|
|
88
|
+
* The ID is deterministic based on message content only
|
|
89
|
+
*
|
|
90
|
+
* The Erlang implementation sorts components WITH @ prefix included,
|
|
91
|
+
* then removes @ from derived components in the signature base.
|
|
92
|
+
*
|
|
93
|
+
* @param {Object} message - The message with signature and signature-input
|
|
94
|
+
* @returns {string} The commitment ID in base64url format
|
|
95
|
+
*/
|
|
96
|
+
function generateHmacCommitmentId(message) {
|
|
97
|
+
// Parse signature-input to get components
|
|
98
|
+
const parsed = parseStructuredFieldDictionary(message["signature-input"])
|
|
99
|
+
if (!parsed || !parsed.components) {
|
|
100
|
+
throw new Error("Failed to parse signature-input")
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Sort components AS-IS (with quotes and @ prefix)
|
|
104
|
+
const sortedComponents = [...parsed.components].sort()
|
|
105
|
+
|
|
106
|
+
// Build signature base in sorted order
|
|
107
|
+
const lines = []
|
|
108
|
+
|
|
109
|
+
sortedComponents.forEach(component => {
|
|
110
|
+
const cleanComponent = component.replace(/"/g, "")
|
|
111
|
+
let fieldName = cleanComponent
|
|
112
|
+
let value
|
|
113
|
+
|
|
114
|
+
// For derived components (starting with @), remove @ in the signature base
|
|
115
|
+
if (cleanComponent.startsWith("@")) {
|
|
116
|
+
fieldName = cleanComponent.substring(1)
|
|
117
|
+
value = message[fieldName]
|
|
118
|
+
} else {
|
|
119
|
+
value = message[cleanComponent]
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
if (value === undefined || value === null) {
|
|
123
|
+
value = ""
|
|
124
|
+
} else if (typeof value === "number") {
|
|
125
|
+
value = value.toString()
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
lines.push(`"${fieldName}": ${value}`)
|
|
129
|
+
})
|
|
130
|
+
|
|
131
|
+
// Add signature-params line with sorted components (keeping @ prefix)
|
|
132
|
+
const paramsComponents = sortedComponents.join(" ")
|
|
133
|
+
lines.push(
|
|
134
|
+
`"@signature-params": (${paramsComponents});alg="hmac-sha256";keyid="ao"`
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
const signatureBase = lines.join("\n")
|
|
138
|
+
|
|
139
|
+
// Generate HMAC with key "ao"
|
|
140
|
+
// Convert string to Uint8Array
|
|
141
|
+
const messageBytes = new TextEncoder().encode(signatureBase)
|
|
142
|
+
const keyBytes = new TextEncoder().encode("ao")
|
|
143
|
+
|
|
144
|
+
const hmacResult = hmac(keyBytes, messageBytes)
|
|
145
|
+
return uint8ArrayToBase64url(hmacResult)
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Generate commitment ID based on the algorithm type
|
|
150
|
+
*
|
|
151
|
+
* @param {Object} commitment - The commitment object containing alg, signature, etc.
|
|
152
|
+
* @param {Object} fullMessage - The full message (required for HMAC)
|
|
153
|
+
* @returns {string} The commitment ID in base64url format
|
|
154
|
+
*/
|
|
155
|
+
function generateCommitmentId(commitment, fullMessage = null) {
|
|
156
|
+
switch (commitment.alg) {
|
|
157
|
+
case "rsa-pss-sha512":
|
|
158
|
+
case "ecdsa-p256-sha256":
|
|
159
|
+
return generateRsaCommitmentId(commitment)
|
|
160
|
+
|
|
161
|
+
case "hmac-sha256":
|
|
162
|
+
if (!fullMessage) {
|
|
163
|
+
throw new Error("HMAC commitment IDs require full message context")
|
|
164
|
+
}
|
|
165
|
+
return generateHmacCommitmentId(fullMessage)
|
|
166
|
+
|
|
167
|
+
default:
|
|
168
|
+
throw new Error(`Unsupported algorithm: ${commitment.alg}`)
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Extract all commitment IDs from a HyperBEAM message
|
|
174
|
+
*
|
|
175
|
+
* @param {Object} message - The message with commitments
|
|
176
|
+
* @returns {Object} Map of commitment IDs to their types
|
|
177
|
+
*/
|
|
178
|
+
function extractCommitmentIds(message) {
|
|
179
|
+
const ids = {}
|
|
180
|
+
|
|
181
|
+
if (!message.commitments) {
|
|
182
|
+
return ids
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
for (const [id, commitment] of Object.entries(message.commitments)) {
|
|
186
|
+
ids[id] = {
|
|
187
|
+
alg: commitment.alg,
|
|
188
|
+
committer: commitment.committer,
|
|
189
|
+
device: commitment["commitment-device"],
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
return ids
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Verify a commitment ID matches the expected value
|
|
198
|
+
*
|
|
199
|
+
* @param {Object} commitment - The commitment object
|
|
200
|
+
* @param {string} expectedId - The expected commitment ID
|
|
201
|
+
* @param {Object} fullMessage - The full message (required for HMAC)
|
|
202
|
+
* @returns {boolean} True if the ID matches
|
|
203
|
+
*/
|
|
204
|
+
function verifyCommitmentId(commitment, expectedId, fullMessage = null) {
|
|
205
|
+
try {
|
|
206
|
+
const calculatedId = generateCommitmentId(commitment, fullMessage)
|
|
207
|
+
return calculatedId === expectedId
|
|
208
|
+
} catch (error) {
|
|
209
|
+
console.error("Error verifying commitment ID:", error)
|
|
210
|
+
return false
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// Export all functions
|
|
215
|
+
export {
|
|
216
|
+
generateCommitmentId,
|
|
217
|
+
generateRsaCommitmentId,
|
|
218
|
+
generateHmacCommitmentId,
|
|
219
|
+
extractCommitmentIds,
|
|
220
|
+
verifyCommitmentId,
|
|
221
|
+
parseStructuredFieldDictionary,
|
|
222
|
+
}
|
package/esm/send.js
CHANGED
|
@@ -9,6 +9,8 @@ const {
|
|
|
9
9
|
} = httpbis
|
|
10
10
|
|
|
11
11
|
export async function send(signedMsg, fetchImpl = fetch) {
|
|
12
|
+
// IMPORTANT: Use the URL from signedMsg.url, NOT from any path header
|
|
13
|
+
// This ensures we send to the correct URL even if path header is different
|
|
12
14
|
const fetchOptions = {
|
|
13
15
|
method: signedMsg.method,
|
|
14
16
|
headers: signedMsg.headers,
|
|
@@ -21,8 +23,9 @@ export async function send(signedMsg, fetchImpl = fetch) {
|
|
|
21
23
|
) {
|
|
22
24
|
fetchOptions.body = signedMsg.body
|
|
23
25
|
}
|
|
24
|
-
const response = await fetchImpl(signedMsg.url, fetchOptions)
|
|
25
26
|
|
|
27
|
+
// Use the URL as provided, ignoring any path header
|
|
28
|
+
const response = await fetchImpl(signedMsg.url, fetchOptions)
|
|
26
29
|
if (response.status >= 400) {
|
|
27
30
|
throw new Error(`${response.status}: ${await response.text()}`)
|
|
28
31
|
}
|
|
@@ -90,10 +93,8 @@ export const toHttpSigner = signer => {
|
|
|
90
93
|
|
|
91
94
|
signatureBaseArray.push(['"@signature-params"', [signatureInput]])
|
|
92
95
|
signatureBase = formatSignatureBase(signatureBaseArray)
|
|
93
|
-
|
|
94
96
|
return new TextEncoder().encode(signatureBase)
|
|
95
97
|
}
|
|
96
|
-
|
|
97
98
|
const result = await signer(create, "httpsig")
|
|
98
99
|
|
|
99
100
|
if (!createCalled) {
|
package/esm/signer-utils.js
CHANGED
|
@@ -207,7 +207,7 @@ export async function verify(signedMessage, publicKey) {
|
|
|
207
207
|
* @param {string} [signatureName] - Optional signature name to look for
|
|
208
208
|
* @returns {Buffer|null} Public key buffer or null
|
|
209
209
|
*/
|
|
210
|
-
function extractPublicKeyFromHeaders(headers, signatureName) {
|
|
210
|
+
export function extractPublicKeyFromHeaders(headers, signatureName) {
|
|
211
211
|
const signatureInput =
|
|
212
212
|
headers["signature-input"] || headers["Signature-Input"]
|
|
213
213
|
if (!signatureInput) return null
|
package/esm/signer.js
CHANGED
|
@@ -5,7 +5,11 @@ const joinUrl = ({ url, path }) => {
|
|
|
5
5
|
if (path.startsWith("http://") || path.startsWith("https://")) {
|
|
6
6
|
return path
|
|
7
7
|
}
|
|
8
|
-
|
|
8
|
+
// Ensure path starts with /
|
|
9
|
+
const normalizedPath = path.startsWith("/") ? path : "/" + path
|
|
10
|
+
return url.endsWith("/")
|
|
11
|
+
? url.slice(0, -1) + normalizedPath
|
|
12
|
+
: url + normalizedPath
|
|
9
13
|
}
|
|
10
14
|
|
|
11
15
|
export function signer(config) {
|
|
@@ -15,7 +19,7 @@ export function signer(config) {
|
|
|
15
19
|
throw new Error("Signer is required for mainnet mode")
|
|
16
20
|
}
|
|
17
21
|
|
|
18
|
-
return async function sign(fields) {
|
|
22
|
+
return async function sign(fields, { path: _path = false } = {}) {
|
|
19
23
|
const { path = "/relay/process", method = "POST", ...restFields } = fields
|
|
20
24
|
const aoFields = { ...restFields }
|
|
21
25
|
const encoded = await enc(aoFields)
|
|
@@ -24,6 +28,8 @@ export function signer(config) {
|
|
|
24
28
|
|
|
25
29
|
const _url = joinUrl({ url, path })
|
|
26
30
|
|
|
31
|
+
headersObj["path"] = path
|
|
32
|
+
|
|
27
33
|
if (body && !headersObj["content-length"]) {
|
|
28
34
|
const bodySize = body.size || body.byteLength || 0
|
|
29
35
|
if (bodySize > 0) {
|
|
@@ -35,21 +41,39 @@ export function signer(config) {
|
|
|
35
41
|
for (const [key, value] of Object.entries(headersObj)) {
|
|
36
42
|
lowercaseHeaders[key.toLowerCase()] = value
|
|
37
43
|
}
|
|
38
|
-
|
|
44
|
+
if (lowercaseHeaders.path && /^\//.test(lowercaseHeaders.path)) {
|
|
45
|
+
//const sp = lowercaseHeaders.path.split("/")
|
|
46
|
+
//lowercaseHeaders.path = sp.slice(sp.length - 1).join("/")
|
|
47
|
+
}
|
|
48
|
+
// Parse body-keys if present
|
|
49
|
+
const bodyKeys = headersObj["body-keys"]
|
|
50
|
+
? headersObj["body-keys"]
|
|
51
|
+
.replace(/"/g, "")
|
|
52
|
+
.split(",")
|
|
53
|
+
.map(k => k.trim())
|
|
54
|
+
: []
|
|
55
|
+
|
|
56
|
+
// Collect fields to sign from headers
|
|
39
57
|
const signingFields = Object.keys(lowercaseHeaders).filter(
|
|
40
|
-
key => key !== "body-keys"
|
|
58
|
+
key => key !== "body-keys" && key !== "path" && !bodyKeys.includes(key) // Also exclude fields that are in body-keys
|
|
41
59
|
)
|
|
42
|
-
|
|
60
|
+
// Always include @path in the signature
|
|
61
|
+
//if (!signingFields.includes("path")) signingFields.push("path")
|
|
62
|
+
if (_path) signingFields.push("path")
|
|
63
|
+
// Ensure we have at least one field to sign
|
|
43
64
|
if (signingFields.length === 0 && !body) {
|
|
44
65
|
lowercaseHeaders["content-length"] = "0"
|
|
45
66
|
signingFields.push("content-length")
|
|
46
67
|
}
|
|
47
68
|
|
|
48
69
|
const signedRequest = await toHttpSigner(signer)({
|
|
49
|
-
request: {
|
|
70
|
+
request: {
|
|
71
|
+
url: _url,
|
|
72
|
+
method,
|
|
73
|
+
headers: lowercaseHeaders,
|
|
74
|
+
},
|
|
50
75
|
fields: signingFields,
|
|
51
76
|
})
|
|
52
|
-
|
|
53
77
|
const finalHeaders = {}
|
|
54
78
|
|
|
55
79
|
for (const [key, value] of Object.entries(headersObj)) {
|
|
@@ -62,7 +86,6 @@ export function signer(config) {
|
|
|
62
86
|
if (headersObj["body-keys"]) {
|
|
63
87
|
finalHeaders["body-keys"] = headersObj["body-keys"]
|
|
64
88
|
}
|
|
65
|
-
|
|
66
89
|
const result = { url: _url, method, headers: finalHeaders }
|
|
67
90
|
|
|
68
91
|
if (body) result.body = body
|
|
@@ -70,3 +93,5 @@ export function signer(config) {
|
|
|
70
93
|
return result
|
|
71
94
|
}
|
|
72
95
|
}
|
|
96
|
+
|
|
97
|
+
// stack / simple-pay / patch / hyperbeam / hyperbeam-aos / scheduler / cron
|
package/esm/utils.js
CHANGED
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
import { graphql, parse, validate, buildSchema } from "graphql"
|
|
2
2
|
import sha256 from "fast-sha256"
|
|
3
|
+
import {
|
|
4
|
+
generateCommitmentId,
|
|
5
|
+
generateRsaCommitmentId,
|
|
6
|
+
generateHmacCommitmentId,
|
|
7
|
+
verifyCommitmentId,
|
|
8
|
+
} from "./id.js"
|
|
9
|
+
export { generateRsaCommitmentId as rsaid, generateHmacCommitmentId as hmacid }
|
|
3
10
|
import {
|
|
4
11
|
clone,
|
|
5
12
|
is,
|