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/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) {
@@ -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
- return url.endsWith("/") ? url.slice(0, -1) + path : url + path
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: { url: _url, method, headers: lowercaseHeaders },
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,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wao",
3
- "version": "0.27.3",
3
+ "version": "0.28.0",
4
4
  "bin": {
5
5
  "wao": "./cjs/cli.js",
6
6
  "wao-esm": "./esm/cli.js"