clawpowers 2.0.0 → 2.2.1

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.
Files changed (63) hide show
  1. package/CHANGELOG.md +32 -0
  2. package/COMPATIBILITY.md +13 -0
  3. package/KNOWN_LIMITATIONS.md +19 -0
  4. package/LICENSING.md +10 -0
  5. package/README.md +201 -9
  6. package/SECURITY.md +33 -53
  7. package/dist/index.d.ts +638 -5
  8. package/dist/index.js +986 -58
  9. package/dist/index.js.map +1 -1
  10. package/native/Cargo.lock +4863 -0
  11. package/native/Cargo.toml +73 -0
  12. package/native/crates/canonical/Cargo.toml +24 -0
  13. package/native/crates/canonical/src/lib.rs +673 -0
  14. package/native/crates/compression/Cargo.toml +20 -0
  15. package/native/crates/compression/benches/compression_bench.rs +42 -0
  16. package/native/crates/compression/src/lib.rs +393 -0
  17. package/native/crates/evm-eth/Cargo.toml +13 -0
  18. package/native/crates/evm-eth/src/lib.rs +105 -0
  19. package/native/crates/fee/Cargo.toml +15 -0
  20. package/native/crates/fee/src/lib.rs +281 -0
  21. package/native/crates/index/Cargo.toml +16 -0
  22. package/native/crates/index/src/lib.rs +277 -0
  23. package/native/crates/policy/Cargo.toml +17 -0
  24. package/native/crates/policy/src/lib.rs +614 -0
  25. package/native/crates/security/Cargo.toml +22 -0
  26. package/native/crates/security/src/lib.rs +478 -0
  27. package/native/crates/tokens/Cargo.toml +13 -0
  28. package/native/crates/tokens/src/lib.rs +534 -0
  29. package/native/crates/verification/Cargo.toml +23 -0
  30. package/native/crates/verification/src/lib.rs +333 -0
  31. package/native/crates/wallet/Cargo.toml +20 -0
  32. package/native/crates/wallet/src/lib.rs +261 -0
  33. package/native/crates/x402/Cargo.toml +30 -0
  34. package/native/crates/x402/src/lib.rs +423 -0
  35. package/native/ffi/Cargo.toml +34 -0
  36. package/native/ffi/build.rs +4 -0
  37. package/native/ffi/index.node +0 -0
  38. package/native/ffi/src/lib.rs +352 -0
  39. package/native/ffi/tests/integration.rs +354 -0
  40. package/native/pyo3/Cargo.toml +26 -0
  41. package/native/pyo3/pyproject.toml +16 -0
  42. package/native/pyo3/src/lib.rs +407 -0
  43. package/native/pyo3/tests/test_smoke.py +180 -0
  44. package/native/wasm/Cargo.toml +44 -0
  45. package/native/wasm/pkg/.gitignore +6 -0
  46. package/native/wasm/pkg/clawpowers_wasm.d.ts +208 -0
  47. package/native/wasm/pkg/clawpowers_wasm.js +872 -0
  48. package/native/wasm/pkg/clawpowers_wasm_bg.wasm +0 -0
  49. package/native/wasm/pkg/clawpowers_wasm_bg.wasm.d.ts +40 -0
  50. package/native/wasm/pkg/package.json +17 -0
  51. package/native/wasm/pkg-node/.gitignore +6 -0
  52. package/native/wasm/pkg-node/clawpowers_wasm.d.ts +143 -0
  53. package/native/wasm/pkg-node/clawpowers_wasm.js +798 -0
  54. package/native/wasm/pkg-node/clawpowers_wasm_bg.wasm +0 -0
  55. package/native/wasm/pkg-node/clawpowers_wasm_bg.wasm.d.ts +40 -0
  56. package/native/wasm/pkg-node/package.json +13 -0
  57. package/native/wasm/src/lib.rs +433 -0
  58. package/package.json +24 -3
  59. package/src/skills/catalog.ts +435 -0
  60. package/src/skills/executor.ts +56 -0
  61. package/src/skills/index.ts +3 -0
  62. package/src/skills/itp/SKILL.md +112 -0
  63. package/src/skills/loader.ts +193 -0
@@ -0,0 +1,423 @@
1
+ //! clawpowers-x402 — HTTP 402 Payment Required client.
2
+ //!
3
+ //! Implements the [x402 payment protocol](https://x402.org) for automatically
4
+ //! handling `402 Payment Required` responses from API endpoints.
5
+ //!
6
+ //! # Protocol flow
7
+ //!
8
+ //! 1. Client makes a request; server responds `402` with `X-Payment-*` headers.
9
+ //! 2. Client parses the payment requirements via
10
+ //! [`X402Client::parse_402_response`].
11
+ //! 3. Wallet signs the payment descriptor off-chain.
12
+ //! 4. Client retries with the `X-Payment` header set via
13
+ //! [`X402Client::create_payment_header`].
14
+ //!
15
+ //! # Example
16
+ //!
17
+ //! ```rust,ignore
18
+ //! let client = X402Client::new();
19
+ //! let payment = client.parse_402_response(&response.headers())?;
20
+ //! let sig = wallet.sign(&payment).await?;
21
+ //! let header = client.create_payment_header(&payment, &sig);
22
+ //! let resp = client.request_with_payment(url, "GET", &header).await?;
23
+ //! ```
24
+
25
+ use reqwest::{
26
+ Client, Method,
27
+ header::{HeaderMap, HeaderValue},
28
+ };
29
+ use std::str::FromStr;
30
+ use thiserror::Error;
31
+
32
+ // ---------------------------------------------------------------------------
33
+ // Error type
34
+ // ---------------------------------------------------------------------------
35
+
36
+ /// Errors produced by the x402 payment client.
37
+ #[derive(Debug, Error)]
38
+ pub enum X402Error {
39
+ /// An underlying HTTP transport error.
40
+ #[error("HTTP error: {0}")]
41
+ HttpError(#[from] reqwest::Error),
42
+
43
+ /// A required x402 header was absent from the `402` response.
44
+ #[error("missing required x402 header: {0}")]
45
+ MissingHeader(String),
46
+
47
+ /// A header value could not be parsed into the expected type.
48
+ #[error("invalid payment parameter: {0}")]
49
+ InvalidPayment(String),
50
+
51
+ /// The server rejected the payment (non-2xx after retry).
52
+ #[error("payment failed: {0}")]
53
+ PaymentFailed(String),
54
+ }
55
+
56
+ // ---------------------------------------------------------------------------
57
+ // X402PaymentRequired
58
+ // ---------------------------------------------------------------------------
59
+
60
+ /// Payment requirements parsed from a `402 Payment Required` response.
61
+ #[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
62
+ pub struct X402PaymentRequired {
63
+ /// URL the client should use to submit payment or retrieve further info.
64
+ pub payment_url: String,
65
+ /// Human-readable amount string (e.g. `"1.00"` — interpreted relative to
66
+ /// the token's decimal precision).
67
+ pub amount: String,
68
+ /// Token symbol or contract address accepted for payment.
69
+ pub token: String,
70
+ /// EVM chain identifier.
71
+ pub chain_id: u64,
72
+ /// Address of the recipient that must receive the payment.
73
+ pub recipient: String,
74
+ /// Optional human-readable memo attached to the payment.
75
+ pub memo: Option<String>,
76
+ }
77
+
78
+ // ---------------------------------------------------------------------------
79
+ // X402Client
80
+ // ---------------------------------------------------------------------------
81
+
82
+ /// HTTP client that handles the x402 payment flow.
83
+ ///
84
+ /// Wraps [`reqwest::Client`] and adds helpers for parsing `402` responses and
85
+ /// attaching payment proofs to retry requests.
86
+ #[derive(Debug, Clone)]
87
+ pub struct X402Client {
88
+ inner: Client,
89
+ }
90
+
91
+ impl X402Client {
92
+ /// Creates a new [`X402Client`] with default [`reqwest`] settings.
93
+ pub fn new() -> Self {
94
+ Self {
95
+ inner: Client::new(),
96
+ }
97
+ }
98
+
99
+ /// Parses x402 payment requirements from a `HeaderMap`.
100
+ ///
101
+ /// Required headers:
102
+ /// - `X-Payment-URL`
103
+ /// - `X-Payment-Amount`
104
+ /// - `X-Payment-Token`
105
+ /// - `X-Payment-ChainId`
106
+ /// - `X-Payment-Recipient`
107
+ ///
108
+ /// Optional headers:
109
+ /// - `X-Payment-Memo`
110
+ ///
111
+ /// # Errors
112
+ ///
113
+ /// Returns [`X402Error::MissingHeader`] if any required header is absent,
114
+ /// or [`X402Error::InvalidPayment`] if a header value is malformed.
115
+ pub fn parse_402_response(headers: &HeaderMap) -> Result<X402PaymentRequired, X402Error> {
116
+ let payment_url = Self::required_str(headers, "x-payment-url")?;
117
+ let amount = Self::required_str(headers, "x-payment-amount")?;
118
+ let token = Self::required_str(headers, "x-payment-token")?;
119
+ let chain_id_str = Self::required_str(headers, "x-payment-chainid")?;
120
+ let recipient = Self::required_str(headers, "x-payment-recipient")?;
121
+ let memo = Self::optional_str(headers, "x-payment-memo")?;
122
+
123
+ let chain_id = chain_id_str
124
+ .parse::<u64>()
125
+ .map_err(|_| X402Error::InvalidPayment(format!("invalid chain_id: {chain_id_str}")))?;
126
+
127
+ Ok(X402PaymentRequired {
128
+ payment_url,
129
+ amount,
130
+ token,
131
+ chain_id,
132
+ recipient,
133
+ memo,
134
+ })
135
+ }
136
+
137
+ /// Formats the `X-Payment` header value for the follow-up request.
138
+ ///
139
+ /// The format is:
140
+ /// `<recipient>:<amount>:<token>:<chain_id>:<signature>[:<memo>]`
141
+ pub fn create_payment_header(payment: &X402PaymentRequired, signature: &str) -> String {
142
+ let mut parts = vec![
143
+ payment.recipient.clone(),
144
+ payment.amount.clone(),
145
+ payment.token.clone(),
146
+ payment.chain_id.to_string(),
147
+ signature.to_string(),
148
+ ];
149
+ if let Some(memo) = &payment.memo {
150
+ parts.push(memo.clone());
151
+ }
152
+ parts.join(":")
153
+ }
154
+
155
+ /// Makes an authenticated request carrying a pre-built `X-Payment` header.
156
+ ///
157
+ /// # Errors
158
+ ///
159
+ /// Returns [`X402Error::HttpError`] on transport failure, or
160
+ /// [`X402Error::PaymentFailed`] if the server responds with a non-2xx
161
+ /// status code.
162
+ pub async fn request_with_payment(
163
+ &self,
164
+ url: &str,
165
+ method: &str,
166
+ payment_header: &str,
167
+ ) -> Result<reqwest::Response, X402Error> {
168
+ let method = Method::from_str(method)
169
+ .map_err(|_| X402Error::InvalidPayment(format!("unknown HTTP method: {method}")))?;
170
+
171
+ let header_value = HeaderValue::from_str(payment_header).map_err(|_| {
172
+ X402Error::InvalidPayment("payment header contains invalid characters".to_string())
173
+ })?;
174
+
175
+ let response = self
176
+ .inner
177
+ .request(method, url)
178
+ .header("x-payment", header_value)
179
+ .send()
180
+ .await?;
181
+
182
+ if response.status().is_success() {
183
+ Ok(response)
184
+ } else {
185
+ Err(X402Error::PaymentFailed(format!(
186
+ "server returned {}",
187
+ response.status()
188
+ )))
189
+ }
190
+ }
191
+
192
+ /// Executes the full x402 flow: initial request → parse 402 → sign →
193
+ /// retry with payment.
194
+ ///
195
+ /// `sign_fn` is a synchronous callback that receives a reference to the
196
+ /// parsed [`X402PaymentRequired`] and returns the hex-encoded signature
197
+ /// string to include in the `X-Payment` header.
198
+ ///
199
+ /// # Errors
200
+ ///
201
+ /// - [`X402Error::HttpError`] — transport failure on either leg.
202
+ /// - [`X402Error::MissingHeader`] / [`X402Error::InvalidPayment`] — bad 402 headers.
203
+ /// - [`X402Error::PaymentFailed`] — server rejects the payment on retry.
204
+ pub async fn execute_with_payment(
205
+ &self,
206
+ url: &str,
207
+ method: &str,
208
+ sign_fn: impl Fn(&X402PaymentRequired) -> String,
209
+ ) -> Result<reqwest::Response, X402Error> {
210
+ let method_obj = Method::from_str(method)
211
+ .map_err(|_| X402Error::InvalidPayment(format!("unknown HTTP method: {method}")))?;
212
+
213
+ // First attempt — expect a 402.
214
+ let initial = self.inner.request(method_obj, url).send().await?;
215
+
216
+ if initial.status() != reqwest::StatusCode::PAYMENT_REQUIRED {
217
+ if initial.status().is_success() {
218
+ return Ok(initial);
219
+ }
220
+ return Err(X402Error::PaymentFailed(format!(
221
+ "unexpected status: {}",
222
+ initial.status()
223
+ )));
224
+ }
225
+
226
+ let payment = Self::parse_402_response(initial.headers())?;
227
+ let signature = sign_fn(&payment);
228
+ let payment_header = Self::create_payment_header(&payment, &signature);
229
+
230
+ self.request_with_payment(url, method, &payment_header)
231
+ .await
232
+ }
233
+
234
+ // -------------------------------------------------------------------
235
+ // Internal helpers
236
+ // -------------------------------------------------------------------
237
+
238
+ fn required_str(headers: &HeaderMap, name: &str) -> Result<String, X402Error> {
239
+ headers
240
+ .get(name)
241
+ .ok_or_else(|| X402Error::MissingHeader(name.to_string()))
242
+ .and_then(|v| {
243
+ v.to_str()
244
+ .map(str::to_owned)
245
+ .map_err(|_| X402Error::InvalidPayment(format!("non-UTF8 value in {name}")))
246
+ })
247
+ }
248
+
249
+ fn optional_str(headers: &HeaderMap, name: &str) -> Result<Option<String>, X402Error> {
250
+ match headers.get(name) {
251
+ None => Ok(None),
252
+ Some(v) => v
253
+ .to_str()
254
+ .map(|s| Some(s.to_owned()))
255
+ .map_err(|_| X402Error::InvalidPayment(format!("non-UTF8 value in {name}"))),
256
+ }
257
+ }
258
+ }
259
+
260
+ impl Default for X402Client {
261
+ fn default() -> Self {
262
+ Self::new()
263
+ }
264
+ }
265
+
266
+ // ---------------------------------------------------------------------------
267
+ // Tests
268
+ // ---------------------------------------------------------------------------
269
+
270
+ #[cfg(test)]
271
+ mod tests {
272
+ use super::*;
273
+ use reqwest::header::{HeaderMap, HeaderName, HeaderValue};
274
+
275
+ // Helper: build a HeaderMap from key-value pairs.
276
+ fn headers(pairs: &[(&str, &str)]) -> HeaderMap {
277
+ let mut map = HeaderMap::new();
278
+ for (k, v) in pairs {
279
+ map.insert(
280
+ HeaderName::from_bytes(k.as_bytes()).unwrap(),
281
+ HeaderValue::from_str(v).unwrap(),
282
+ );
283
+ }
284
+ map
285
+ }
286
+
287
+ fn full_402_headers() -> HeaderMap {
288
+ headers(&[
289
+ ("x-payment-url", "https://pay.example.com/pay"),
290
+ ("x-payment-amount", "1.00"),
291
+ ("x-payment-token", "USDC"),
292
+ ("x-payment-chainid", "8453"),
293
+ (
294
+ "x-payment-recipient",
295
+ "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045",
296
+ ),
297
+ ])
298
+ }
299
+
300
+ // --- parse_402_response ---
301
+
302
+ #[test]
303
+ fn parse_full_402_headers() {
304
+ let hdrs = full_402_headers();
305
+ let payment = X402Client::parse_402_response(&hdrs).unwrap();
306
+ assert_eq!(payment.payment_url, "https://pay.example.com/pay");
307
+ assert_eq!(payment.amount, "1.00");
308
+ assert_eq!(payment.token, "USDC");
309
+ assert_eq!(payment.chain_id, 8453);
310
+ assert_eq!(
311
+ payment.recipient,
312
+ "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045"
313
+ );
314
+ assert_eq!(payment.memo, None);
315
+ }
316
+
317
+ #[test]
318
+ fn parse_402_with_memo() {
319
+ let mut hdrs = full_402_headers();
320
+ hdrs.insert(
321
+ HeaderName::from_static("x-payment-memo"),
322
+ HeaderValue::from_static("invoice-42"),
323
+ );
324
+ let payment = X402Client::parse_402_response(&hdrs).unwrap();
325
+ assert_eq!(payment.memo, Some("invoice-42".to_string()));
326
+ }
327
+
328
+ #[test]
329
+ fn parse_missing_url_header() {
330
+ let hdrs = headers(&[
331
+ ("x-payment-amount", "1.00"),
332
+ ("x-payment-token", "USDC"),
333
+ ("x-payment-chainid", "1"),
334
+ ("x-payment-recipient", "0xabc"),
335
+ ]);
336
+ let err = X402Client::parse_402_response(&hdrs).unwrap_err();
337
+ assert!(matches!(err, X402Error::MissingHeader(h) if h == "x-payment-url"));
338
+ }
339
+
340
+ #[test]
341
+ fn parse_missing_amount_header() {
342
+ let hdrs = headers(&[
343
+ ("x-payment-url", "https://pay.example.com/pay"),
344
+ ("x-payment-token", "USDC"),
345
+ ("x-payment-chainid", "1"),
346
+ ("x-payment-recipient", "0xabc"),
347
+ ]);
348
+ let err = X402Client::parse_402_response(&hdrs).unwrap_err();
349
+ assert!(matches!(err, X402Error::MissingHeader(h) if h == "x-payment-amount"));
350
+ }
351
+
352
+ #[test]
353
+ fn parse_invalid_chain_id() {
354
+ let hdrs = headers(&[
355
+ ("x-payment-url", "https://pay.example.com/pay"),
356
+ ("x-payment-amount", "1.00"),
357
+ ("x-payment-token", "USDC"),
358
+ ("x-payment-chainid", "not-a-number"),
359
+ ("x-payment-recipient", "0xabc"),
360
+ ]);
361
+ let err = X402Client::parse_402_response(&hdrs).unwrap_err();
362
+ assert!(matches!(err, X402Error::InvalidPayment(_)));
363
+ }
364
+
365
+ // --- create_payment_header ---
366
+
367
+ #[test]
368
+ fn create_payment_header_without_memo() {
369
+ let payment = X402PaymentRequired {
370
+ payment_url: "https://pay.example.com/pay".to_string(),
371
+ amount: "1.00".to_string(),
372
+ token: "USDC".to_string(),
373
+ chain_id: 8453,
374
+ recipient: "0xrecipient".to_string(),
375
+ memo: None,
376
+ };
377
+ let header = X402Client::create_payment_header(&payment, "0xsig");
378
+ assert_eq!(header, "0xrecipient:1.00:USDC:8453:0xsig");
379
+ }
380
+
381
+ #[test]
382
+ fn create_payment_header_with_memo() {
383
+ let payment = X402PaymentRequired {
384
+ payment_url: "https://pay.example.com/pay".to_string(),
385
+ amount: "2.50".to_string(),
386
+ token: "ETH".to_string(),
387
+ chain_id: 1,
388
+ recipient: "0xrecipient".to_string(),
389
+ memo: Some("purchase-99".to_string()),
390
+ };
391
+ let header = X402Client::create_payment_header(&payment, "0xsig");
392
+ assert_eq!(header, "0xrecipient:2.50:ETH:1:0xsig:purchase-99");
393
+ }
394
+
395
+ // --- X402Client construction ---
396
+
397
+ #[test]
398
+ fn client_new_and_default_are_equivalent() {
399
+ // Both constructors should produce a usable client (structural test).
400
+ let _a = X402Client::new();
401
+ let _b = X402Client::default();
402
+ }
403
+
404
+ // --- Error display ---
405
+
406
+ #[test]
407
+ fn missing_header_error_display() {
408
+ let err = X402Error::MissingHeader("x-payment-url".to_string());
409
+ assert!(err.to_string().contains("x-payment-url"));
410
+ }
411
+
412
+ #[test]
413
+ fn invalid_payment_error_display() {
414
+ let err = X402Error::InvalidPayment("bad chain_id".to_string());
415
+ assert!(err.to_string().contains("bad chain_id"));
416
+ }
417
+
418
+ #[test]
419
+ fn payment_failed_error_display() {
420
+ let err = X402Error::PaymentFailed("server returned 402".to_string());
421
+ assert!(err.to_string().contains("402"));
422
+ }
423
+ }
@@ -0,0 +1,34 @@
1
+ [package]
2
+ name = "clawpowers-ffi"
3
+ version.workspace = true
4
+ edition.workspace = true
5
+ license.workspace = true
6
+
7
+ [lib]
8
+ crate-type = ["cdylib"]
9
+
10
+ [dependencies]
11
+ napi = { workspace = true }
12
+ napi-derive = { workspace = true }
13
+ serde = { workspace = true }
14
+ serde_json = { workspace = true }
15
+ tokio = { workspace = true }
16
+ alloy-primitives = { workspace = true }
17
+ k256 = { version = "0.13", features = ["ecdsa", "sha2"] }
18
+ clawpowers-evm-eth = { path = "../crates/evm-eth" }
19
+ uuid = { workspace = true }
20
+ clawpowers-wallet = { path = "../crates/wallet" }
21
+ clawpowers-policy = { path = "../crates/policy" }
22
+ clawpowers-tokens = { path = "../crates/tokens" }
23
+ clawpowers-fee = { path = "../crates/fee" }
24
+ clawpowers-x402 = { path = "../crates/x402" }
25
+ clawpowers-canonical = { path = "../crates/canonical" }
26
+ clawpowers-compression = { path = "../crates/compression" }
27
+ clawpowers-index = { path = "../crates/index" }
28
+ clawpowers-verification = { path = "../crates/verification" }
29
+ clawpowers-security = { path = "../crates/security" }
30
+
31
+ [build-dependencies]
32
+ napi-build = "2"
33
+
34
+ [dev-dependencies]
@@ -0,0 +1,4 @@
1
+ extern crate napi_build;
2
+ fn main() {
3
+ napi_build::setup();
4
+ }
Binary file