clawpowers 2.2.5 → 2.2.7

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 (138) hide show
  1. package/CHANGELOG.md +186 -160
  2. package/COMPATIBILITY.md +48 -13
  3. package/KNOWN_LIMITATIONS.md +20 -19
  4. package/LICENSE +44 -44
  5. package/LICENSING.md +10 -10
  6. package/README.md +486 -462
  7. package/SECURITY.md +52 -52
  8. package/dist/index.d.ts +17 -5
  9. package/dist/index.js +187 -92
  10. package/dist/index.js.map +1 -1
  11. package/native/Cargo.lock +4927 -4927
  12. package/native/Cargo.toml +73 -73
  13. package/native/crates/canonical/Cargo.toml +24 -24
  14. package/native/crates/canonical/src/lib.rs +677 -673
  15. package/native/crates/compression/Cargo.toml +20 -20
  16. package/native/crates/compression/benches/compression_bench.rs +42 -42
  17. package/native/crates/compression/src/lib.rs +393 -393
  18. package/native/crates/evm-eth/Cargo.toml +13 -13
  19. package/native/crates/evm-eth/src/lib.rs +105 -105
  20. package/native/crates/fee/Cargo.toml +15 -15
  21. package/native/crates/fee/src/lib.rs +281 -281
  22. package/native/crates/index/Cargo.toml +16 -16
  23. package/native/crates/index/src/lib.rs +277 -277
  24. package/native/crates/policy/Cargo.toml +17 -17
  25. package/native/crates/policy/src/lib.rs +614 -614
  26. package/native/crates/security/Cargo.toml +22 -22
  27. package/native/crates/security/src/lib.rs +478 -478
  28. package/native/crates/tokens/Cargo.toml +13 -13
  29. package/native/crates/tokens/src/lib.rs +534 -534
  30. package/native/crates/verification/Cargo.toml +23 -23
  31. package/native/crates/verification/src/lib.rs +333 -333
  32. package/native/crates/wallet/Cargo.toml +20 -20
  33. package/native/crates/wallet/src/lib.rs +261 -261
  34. package/native/crates/x402/Cargo.toml +30 -30
  35. package/native/crates/x402/src/lib.rs +423 -423
  36. package/native/ffi/Cargo.toml +34 -34
  37. package/native/ffi/build.rs +4 -4
  38. package/native/ffi/src/lib.rs +352 -352
  39. package/native/ffi/tests/integration.rs +354 -354
  40. package/native/pyo3/Cargo.toml +26 -26
  41. package/native/pyo3/pyproject.toml +16 -16
  42. package/native/pyo3/src/lib.rs +407 -407
  43. package/native/pyo3/tests/test_smoke.py +180 -180
  44. package/native/wasm/Cargo.toml +47 -44
  45. package/native/wasm/pkg/.gitignore +6 -6
  46. package/native/wasm/pkg/clawpowers_wasm.d.ts +208 -208
  47. package/native/wasm/pkg/clawpowers_wasm.js +872 -872
  48. package/native/wasm/pkg/clawpowers_wasm_bg.wasm.d.ts +40 -40
  49. package/native/wasm/pkg/package.json +16 -16
  50. package/native/wasm/pkg-node/clawpowers_wasm.d.ts +143 -143
  51. package/native/wasm/pkg-node/clawpowers_wasm.js +798 -798
  52. package/native/wasm/pkg-node/clawpowers_wasm_bg.wasm.d.ts +40 -40
  53. package/native/wasm/pkg-node/package.json +12 -12
  54. package/native/wasm/src/lib.rs +433 -433
  55. package/package.json +13 -8
  56. package/scripts/build-wasm.mjs +59 -0
  57. package/scripts/generate_hermes_wrappers.py +211 -0
  58. package/scripts/hermes_wrapper_overrides.json +184 -0
  59. package/scripts/run-python-script.mjs +48 -0
  60. package/scripts/verify-consumer-install.mjs +109 -0
  61. package/scripts/verify-wasm-artifacts.mjs +26 -3
  62. package/scripts/verify_hermes_wrappers.py +154 -0
  63. package/skill.json +20 -0
  64. package/skills/1password/SKILL.md +34 -0
  65. package/skills/README.md +44 -0
  66. package/skills/agent-nexus-2/SKILL.md +34 -0
  67. package/skills/apple-notes/SKILL.md +34 -0
  68. package/skills/apple-reminders/SKILL.md +34 -0
  69. package/skills/autoresearch/SKILL.md +43 -0
  70. package/skills/bear-notes/SKILL.md +34 -0
  71. package/skills/blogwatcher/SKILL.md +34 -0
  72. package/skills/blucli/SKILL.md +34 -0
  73. package/skills/bluebubbles/SKILL.md +34 -0
  74. package/skills/business-strategy/SKILL.md +41 -0
  75. package/skills/camsnap/SKILL.md +34 -0
  76. package/skills/canvas/SKILL.md +34 -0
  77. package/skills/clawhub/SKILL.md +34 -0
  78. package/skills/coding-agent/SKILL.md +34 -0
  79. package/skills/coding-discipline.skill/SKILL.md +34 -0
  80. package/skills/content-writer/SKILL.md +41 -0
  81. package/skills/discord/SKILL.md +34 -0
  82. package/skills/eightctl/SKILL.md +34 -0
  83. package/skills/execution-validation.skill/SKILL.md +34 -0
  84. package/skills/gemini/SKILL.md +34 -0
  85. package/skills/gh-issues/SKILL.md +34 -0
  86. package/skills/gifgrep/SKILL.md +34 -0
  87. package/skills/github/SKILL.md +41 -0
  88. package/skills/gog/SKILL.md +34 -0
  89. package/skills/goplaces/SKILL.md +34 -0
  90. package/skills/healthcheck/SKILL.md +34 -0
  91. package/skills/himalaya/SKILL.md +34 -0
  92. package/skills/humanize/SKILL.md +41 -0
  93. package/skills/imsg/SKILL.md +34 -0
  94. package/skills/itp/SKILL.md +112 -0
  95. package/skills/mcporter/SKILL.md +34 -0
  96. package/skills/model-usage/SKILL.md +34 -0
  97. package/skills/nano-pdf/SKILL.md +34 -0
  98. package/skills/node-connect/SKILL.md +34 -0
  99. package/skills/notion/SKILL.md +34 -0
  100. package/skills/obsidian/SKILL.md +34 -0
  101. package/skills/openai-whisper/SKILL.md +34 -0
  102. package/skills/openai-whisper-api/SKILL.md +34 -0
  103. package/skills/openhue/SKILL.md +34 -0
  104. package/skills/oracle/SKILL.md +34 -0
  105. package/skills/ordercli/SKILL.md +34 -0
  106. package/skills/peekaboo/SKILL.md +34 -0
  107. package/skills/polyclaw/SKILL.md +34 -0
  108. package/skills/prospector/SKILL.md +41 -0
  109. package/skills/rsi.skill/SKILL.md +34 -0
  110. package/skills/sag/SKILL.md +34 -0
  111. package/skills/security/SKILL.md +41 -0
  112. package/skills/session-logs/SKILL.md +34 -0
  113. package/skills/sherpa-onnx-tts/SKILL.md +34 -0
  114. package/skills/skill-creator/SKILL.md +34 -0
  115. package/skills/slack/SKILL.md +34 -0
  116. package/skills/songsee/SKILL.md +34 -0
  117. package/skills/sonoscli/SKILL.md +34 -0
  118. package/skills/spotify-player/SKILL.md +34 -0
  119. package/skills/strykr-prism/SKILL.md +41 -0
  120. package/skills/summarize/SKILL.md +34 -0
  121. package/skills/taskbridge/SKILL.md +34 -0
  122. package/skills/things-mac/SKILL.md +34 -0
  123. package/skills/tmux/SKILL.md +34 -0
  124. package/skills/trello/SKILL.md +34 -0
  125. package/skills/validator-agent/SKILL.md +41 -0
  126. package/skills/video-frames/SKILL.md +34 -0
  127. package/skills/voice-call/SKILL.md +34 -0
  128. package/skills/wacli/SKILL.md +34 -0
  129. package/skills/weather/SKILL.md +34 -0
  130. package/skills/webmcp-payments/SKILL.md +41 -0
  131. package/skills/xurl/SKILL.md +34 -0
  132. package/src/skills/catalog.ts +435 -435
  133. package/src/skills/executor.ts +56 -56
  134. package/src/skills/index.ts +3 -3
  135. package/src/skills/itp/SKILL.md +112 -112
  136. package/src/skills/loader.ts +262 -193
  137. package/native/ffi/index.node +0 -0
  138. package/native/wasm/pkg-node/.gitignore +0 -6
@@ -1,433 +1,433 @@
1
- //! WASM bindings for ClawPowers core.
2
- //!
3
- //! Exposes the following modules via `wasm-bindgen`:
4
- //!
5
- //! - **tokens** — Token registry, token amounts, decimal math
6
- //! - **fee** — Fee schedule calculation
7
- //! - **policy** — Spending policy engine
8
- //! - **compression** — TurboQuant vector compression
9
- //! - **index** — In-memory vector index
10
- //! - **canonical** — Canonical record store (in-memory backend for WASM)
11
- //! - **verification** — Record verification pipeline
12
- //! - **security** — Write firewall and audit log
13
- //!
14
- //! **NOT included (falls back to TypeScript):**
15
- //! - **wallet** — Uses `alloy-signer-local` which depends on native crypto
16
- //! primitives that are not fully WASM-compatible without significant patching.
17
- //! The TypeScript fallback uses `ethers.js` / `viem` for equivalent functionality.
18
- //! - **x402** — Uses `reqwest` with `tokio` runtime; in WASM, HTTP is handled
19
- //! natively by `fetch()` in the TypeScript layer.
20
-
21
- use wasm_bindgen::prelude::*;
22
-
23
- // ═══════════════════════════════════════════════════════════════════════════════
24
- // Tokens
25
- // ═══════════════════════════════════════════════════════════════════════════════
26
-
27
- /// Create a TokenAmount from a human-readable f64 value and decimal count.
28
- /// Returns JSON: `{"raw": "...", "decimals": N}`
29
- #[wasm_bindgen(js_name = "tokenAmountFromHuman")]
30
- pub fn token_amount_from_human(human: f64, decimals: u8) -> Result<String, JsError> {
31
- let amount = clawpowers_tokens::TokenAmount::from_human(human, decimals);
32
- Ok(serde_json::to_string(&amount)?)
33
- }
34
-
35
- /// Convert a TokenAmount JSON back to a human-readable f64.
36
- #[wasm_bindgen(js_name = "tokenAmountToHuman")]
37
- pub fn token_amount_to_human(json: &str) -> Result<f64, JsError> {
38
- let amount: clawpowers_tokens::TokenAmount = serde_json::from_str(json)?;
39
- Ok(amount.to_human())
40
- }
41
-
42
- /// Add two TokenAmount JSONs. Returns the sum as JSON.
43
- #[wasm_bindgen(js_name = "tokenAmountAdd")]
44
- pub fn token_amount_add(a_json: &str, b_json: &str) -> Result<String, JsError> {
45
- let a: clawpowers_tokens::TokenAmount = serde_json::from_str(a_json)?;
46
- let b: clawpowers_tokens::TokenAmount = serde_json::from_str(b_json)?;
47
- let result = a.add(&b).map_err(|e| JsError::new(&e.to_string()))?;
48
- Ok(serde_json::to_string(&result)?)
49
- }
50
-
51
- /// Subtract two TokenAmount JSONs. Returns the difference as JSON.
52
- #[wasm_bindgen(js_name = "tokenAmountSub")]
53
- pub fn token_amount_sub(a_json: &str, b_json: &str) -> Result<String, JsError> {
54
- let a: clawpowers_tokens::TokenAmount = serde_json::from_str(a_json)?;
55
- let b: clawpowers_tokens::TokenAmount = serde_json::from_str(b_json)?;
56
- let result = a.sub(&b).map_err(|e| JsError::new(&e.to_string()))?;
57
- Ok(serde_json::to_string(&result)?)
58
- }
59
-
60
- /// Multiply a TokenAmount by basis points. Returns result as JSON.
61
- #[wasm_bindgen(js_name = "tokenAmountMulBps")]
62
- pub fn token_amount_mul_bps(json: &str, bps: u64) -> Result<String, JsError> {
63
- let amount: clawpowers_tokens::TokenAmount = serde_json::from_str(json)?;
64
- let result = amount
65
- .checked_mul_bps(bps)
66
- .ok_or_else(|| JsError::new("overflow in checked_mul_bps"))?;
67
- Ok(serde_json::to_string(&result)?)
68
- }
69
-
70
- /// Get the default token registry as JSON.
71
- #[wasm_bindgen(js_name = "getDefaultTokenRegistry")]
72
- pub fn get_default_token_registry() -> Result<String, JsError> {
73
- let registry = clawpowers_tokens::TokenRegistry::default();
74
- let tokens: Vec<_> = registry.iter().collect();
75
- Ok(serde_json::to_string(&tokens)?)
76
- }
77
-
78
- // ═══════════════════════════════════════════════════════════════════════════════
79
- // Fee
80
- // ═══════════════════════════════════════════════════════════════════════════════
81
-
82
- /// Calculate a transaction fee.
83
- /// Returns JSON: `{"gross_amount": ..., "fee_amount": ..., "net_amount": ...}`
84
- #[wasm_bindgen(js_name = "calculateFee")]
85
- pub fn calculate_fee(
86
- amount_json: &str,
87
- fee_type: &str,
88
- tx_fee_bps: Option<u64>,
89
- swap_fee_bps: Option<u64>,
90
- ) -> Result<String, JsError> {
91
- let amount: clawpowers_tokens::TokenAmount = serde_json::from_str(amount_json)?;
92
-
93
- let fee_type = match fee_type {
94
- "transaction" => clawpowers_fee::FeeType::Transaction,
95
- "swap" => clawpowers_fee::FeeType::Swap,
96
- s if s.starts_with("custom:") => {
97
- let bps: u64 = s[7..]
98
- .parse()
99
- .map_err(|_| JsError::new("invalid custom bps"))?;
100
- clawpowers_fee::FeeType::Custom(bps)
101
- }
102
- _ => return Err(JsError::new("fee_type must be 'transaction', 'swap', or 'custom:N'")),
103
- };
104
-
105
- let schedule = clawpowers_fee::FeeSchedule::new(
106
- tx_fee_bps.unwrap_or(clawpowers_fee::DEFAULT_TX_FEE_BPS),
107
- swap_fee_bps.unwrap_or(clawpowers_fee::DEFAULT_SWAP_FEE_BPS),
108
- alloy_primitives::Address::ZERO,
109
- );
110
-
111
- let calc = schedule
112
- .calculate(amount, fee_type)
113
- .map_err(|e| JsError::new(&e.to_string()))?;
114
-
115
- let result = serde_json::json!({
116
- "gross_amount": calc.gross_amount.to_human(),
117
- "fee_amount": calc.fee_amount.to_human(),
118
- "net_amount": calc.net_amount.to_human(),
119
- });
120
- Ok(serde_json::to_string(&result)?)
121
- }
122
-
123
- // ═══════════════════════════════════════════════════════════════════════════════
124
- // Compression
125
- // ═══════════════════════════════════════════════════════════════════════════════
126
-
127
- /// Compress a float32 vector. Input: JSON array of f32. Output: CompressedVector JSON.
128
- #[wasm_bindgen(js_name = "compressVector")]
129
- pub fn compress_vector(vector_json: &str, dimensions: usize) -> Result<String, JsError> {
130
- let vector: Vec<f32> = serde_json::from_str(vector_json)?;
131
- let config = clawpowers_compression::CompressionConfig {
132
- dimensions,
133
- quantization_bits: 8,
134
- rotation_seed: 0xDEAD_BEEF_CAFE_1234,
135
- };
136
- let compressor = clawpowers_compression::TurboCompressor::new(config);
137
- let compressed = compressor
138
- .compress(&vector)
139
- .map_err(|e| JsError::new(&e.to_string()))?;
140
- Ok(serde_json::to_string(&compressed)?)
141
- }
142
-
143
- /// Decompress a CompressedVector JSON back to a float32 array JSON.
144
- #[wasm_bindgen(js_name = "decompressVector")]
145
- pub fn decompress_vector(compressed_json: &str, dimensions: usize) -> Result<String, JsError> {
146
- let compressed: clawpowers_compression::CompressedVector =
147
- serde_json::from_str(compressed_json)?;
148
- let config = clawpowers_compression::CompressionConfig {
149
- dimensions,
150
- quantization_bits: 8,
151
- rotation_seed: 0xDEAD_BEEF_CAFE_1234,
152
- };
153
- let compressor = clawpowers_compression::TurboCompressor::new(config);
154
- let vector = compressor
155
- .decompress(&compressed)
156
- .map_err(|e| JsError::new(&e.to_string()))?;
157
- Ok(serde_json::to_string(&vector)?)
158
- }
159
-
160
- /// Compute approximate distance between two CompressedVector JSONs.
161
- #[wasm_bindgen(js_name = "approximateDistance")]
162
- pub fn approximate_distance(
163
- a_json: &str,
164
- b_json: &str,
165
- dimensions: usize,
166
- ) -> Result<f32, JsError> {
167
- let a: clawpowers_compression::CompressedVector = serde_json::from_str(a_json)?;
168
- let b: clawpowers_compression::CompressedVector = serde_json::from_str(b_json)?;
169
- let config = clawpowers_compression::CompressionConfig {
170
- dimensions,
171
- quantization_bits: 8,
172
- rotation_seed: 0xDEAD_BEEF_CAFE_1234,
173
- };
174
- let compressor = clawpowers_compression::TurboCompressor::new(config);
175
- let dist = compressor
176
- .approximate_distance(&a, &b)
177
- .map_err(|e| JsError::new(&e.to_string()))?;
178
- Ok(dist)
179
- }
180
-
181
- /// Compute SHA-256 hash of content string.
182
- #[wasm_bindgen(js_name = "computeSha256")]
183
- pub fn compute_sha256(content: &str) -> String {
184
- clawpowers_canonical::compute_sha256(content)
185
- }
186
-
187
- /// Compute Keccak-256 hash of raw bytes (EVM-compatible). Returns `0x` + 64 hex chars.
188
- #[wasm_bindgen(js_name = "computeKeccak256")]
189
- pub fn compute_keccak256(bytes: &[u8]) -> String {
190
- let h = alloy_primitives::keccak256(bytes);
191
- format!("0x{:x}", h)
192
- }
193
-
194
- /// Ethereum address from 32-byte secp256k1 private key (`0x` + 20 bytes, EIP-55 checksum).
195
- #[wasm_bindgen(js_name = "deriveEthereumAddress")]
196
- pub fn derive_ethereum_address_wasm(private_key: &[u8]) -> Result<String, JsError> {
197
- clawpowers_evm_eth::derive_ethereum_address(private_key).map_err(|e| JsError::new(&e))
198
- }
199
-
200
- /// Uncompressed public key: 64 bytes (x || y), no `0x04` prefix.
201
- #[wasm_bindgen(js_name = "derivePublicKey")]
202
- pub fn derive_public_key_wasm(private_key: &[u8]) -> Result<Vec<u8>, JsError> {
203
- clawpowers_evm_eth::derive_public_key(private_key).map_err(|e| JsError::new(&e))
204
- }
205
-
206
- /// ECDSA sign 32-byte message hash → 65 bytes (r || s || recovery_id).
207
- #[wasm_bindgen(js_name = "signEcdsa")]
208
- pub fn sign_ecdsa_wasm(private_key: &[u8], message_hash: &[u8]) -> Result<Vec<u8>, JsError> {
209
- clawpowers_evm_eth::sign_ecdsa(private_key, message_hash).map_err(|e| JsError::new(&e))
210
- }
211
-
212
- #[wasm_bindgen(js_name = "verifyEcdsa")]
213
- pub fn verify_ecdsa_wasm(
214
- public_key: &[u8],
215
- message_hash: &[u8],
216
- signature: &[u8],
217
- ) -> Result<bool, JsError> {
218
- clawpowers_evm_eth::verify_ecdsa(public_key, message_hash, signature).map_err(|e| JsError::new(&e))
219
- }
220
-
221
- // ═══════════════════════════════════════════════════════════════════════════════
222
- // Canonical Store
223
- // ═══════════════════════════════════════════════════════════════════════════════
224
-
225
- /// Opaque handle to an in-memory CanonicalStore.
226
- #[wasm_bindgen]
227
- pub struct WasmCanonicalStore {
228
- inner: clawpowers_canonical::CanonicalStore,
229
- }
230
-
231
- #[wasm_bindgen]
232
- impl WasmCanonicalStore {
233
- /// Create a new in-memory canonical store.
234
- #[wasm_bindgen(constructor)]
235
- pub fn new() -> Result<WasmCanonicalStore, JsError> {
236
- let store = clawpowers_canonical::CanonicalStore::in_memory()
237
- .map_err(|e| JsError::new(&e.to_string()))?;
238
- Ok(Self { inner: store })
239
- }
240
-
241
- /// Insert a record. Input: JSON with {namespace, content, metadata, provenance}.
242
- /// Returns the record UUID as a string.
243
- pub fn insert(&self, json: &str) -> Result<String, JsError> {
244
- #[derive(serde::Deserialize)]
245
- struct InsertInput {
246
- namespace: String,
247
- content: String,
248
- #[serde(default)]
249
- metadata: serde_json::Value,
250
- #[serde(default = "default_provenance")]
251
- provenance: String,
252
- }
253
- fn default_provenance() -> String {
254
- "wasm".to_string()
255
- }
256
-
257
- let input: InsertInput = serde_json::from_str(json)?;
258
- let record = clawpowers_canonical::CanonicalRecord::new(
259
- input.namespace,
260
- input.content,
261
- None,
262
- input.metadata,
263
- input.provenance,
264
- );
265
- let id = self
266
- .inner
267
- .insert(&record)
268
- .map_err(|e| JsError::new(&e.to_string()))?;
269
- Ok(id.to_string())
270
- }
271
-
272
- /// Get a record by UUID. Returns JSON or null.
273
- pub fn get(&self, id: &str) -> Result<Option<String>, JsError> {
274
- let uuid = uuid::Uuid::parse_str(id).map_err(|e| JsError::new(&e.to_string()))?;
275
- let record = self
276
- .inner
277
- .get(&uuid)
278
- .map_err(|e| JsError::new(&e.to_string()))?;
279
- match record {
280
- Some(r) => Ok(Some(serde_json::to_string(&r)?)),
281
- None => Ok(None),
282
- }
283
- }
284
-
285
- /// Get a record by content hash. Returns JSON or null.
286
- #[wasm_bindgen(js_name = "getByHash")]
287
- pub fn get_by_hash(&self, hash: &str) -> Result<Option<String>, JsError> {
288
- let record = self
289
- .inner
290
- .get_by_hash(hash)
291
- .map_err(|e| JsError::new(&e.to_string()))?;
292
- match record {
293
- Some(r) => Ok(Some(serde_json::to_string(&r)?)),
294
- None => Ok(None),
295
- }
296
- }
297
-
298
- /// Query records by namespace. Returns JSON array.
299
- #[wasm_bindgen(js_name = "queryNamespace")]
300
- pub fn query_namespace(&self, namespace: &str, limit: usize) -> Result<String, JsError> {
301
- let records = self
302
- .inner
303
- .query_namespace(namespace, limit)
304
- .map_err(|e| JsError::new(&e.to_string()))?;
305
- Ok(serde_json::to_string(&records)?)
306
- }
307
-
308
- /// Soft-delete a record. Returns true if the record existed.
309
- #[wasm_bindgen(js_name = "softDelete")]
310
- pub fn soft_delete(&self, id: &str) -> Result<bool, JsError> {
311
- let uuid = uuid::Uuid::parse_str(id).map_err(|e| JsError::new(&e.to_string()))?;
312
- self.inner
313
- .soft_delete(&uuid)
314
- .map_err(|e| JsError::new(&e.to_string()))
315
- }
316
-
317
- /// Verify record integrity. Returns true if hash matches.
318
- #[wasm_bindgen(js_name = "verifyIntegrity")]
319
- pub fn verify_integrity(&self, id: &str) -> Result<bool, JsError> {
320
- let uuid = uuid::Uuid::parse_str(id).map_err(|e| JsError::new(&e.to_string()))?;
321
- self.inner
322
- .verify_integrity(&uuid)
323
- .map_err(|e| JsError::new(&e.to_string()))
324
- }
325
-
326
- /// Export all records as JSON for IndexedDB persistence.
327
- #[wasm_bindgen(js_name = "exportJson")]
328
- pub fn export_json(&self) -> Result<String, JsError> {
329
- self.inner
330
- .export_json()
331
- .map_err(|e| JsError::new(&e.to_string()))
332
- }
333
-
334
- /// Import records from JSON (e.g., loaded from IndexedDB).
335
- #[wasm_bindgen(js_name = "importJson")]
336
- pub fn import_json(&self, json: &str) -> Result<usize, JsError> {
337
- self.inner
338
- .import_json(json)
339
- .map_err(|e| JsError::new(&e.to_string()))
340
- }
341
- }
342
-
343
- // ═══════════════════════════════════════════════════════════════════════════════
344
- // Security — Write Firewall
345
- // ═══════════════════════════════════════════════════════════════════════════════
346
-
347
- /// Evaluate a write request against a firewall.
348
- /// Input JSON: {namespace, content, trust_level, source, allowed_namespaces?, blocked_patterns?, max_content_length?}
349
- /// Returns JSON: {"decision": "allow"|"deny"|"sanitize", "reason"?: ..., "sanitized"?: ...}
350
- #[wasm_bindgen(js_name = "evaluateWriteFirewall")]
351
- pub fn evaluate_write_firewall(json: &str) -> Result<String, JsError> {
352
- #[derive(serde::Deserialize)]
353
- struct Input {
354
- namespace: String,
355
- content: String,
356
- trust_level: String,
357
- source: String,
358
- #[serde(default)]
359
- allowed_namespaces: Vec<String>,
360
- #[serde(default)]
361
- blocked_patterns: Vec<String>,
362
- max_content_length: Option<usize>,
363
- }
364
-
365
- let input: Input = serde_json::from_str(json)?;
366
-
367
- let trust = match input.trust_level.as_str() {
368
- "system" => clawpowers_security::TrustLevel::System,
369
- "agent" => clawpowers_security::TrustLevel::Agent,
370
- "external" => clawpowers_security::TrustLevel::External,
371
- _ => clawpowers_security::TrustLevel::Untrusted,
372
- };
373
-
374
- let mut firewall = clawpowers_security::WriteFirewall::new(input.allowed_namespaces);
375
- firewall.blocked_patterns = input.blocked_patterns;
376
- if let Some(max) = input.max_content_length {
377
- firewall.max_content_length = max;
378
- }
379
-
380
- let request = clawpowers_security::WriteRequest {
381
- namespace: input.namespace,
382
- content: input.content,
383
- trust_level: trust,
384
- source: input.source,
385
- };
386
-
387
- let decision = firewall.evaluate(&request);
388
- let result = match decision {
389
- clawpowers_security::FirewallDecision::Allow => {
390
- serde_json::json!({"decision": "allow"})
391
- }
392
- clawpowers_security::FirewallDecision::Deny(reason) => {
393
- serde_json::json!({"decision": "deny", "reason": reason})
394
- }
395
- clawpowers_security::FirewallDecision::Sanitize(original, sanitized) => {
396
- serde_json::json!({"decision": "sanitize", "original": original, "sanitized": sanitized})
397
- }
398
- };
399
- Ok(serde_json::to_string(&result)?)
400
- }
401
-
402
- // ═══════════════════════════════════════════════════════════════════════════════
403
- // Version info
404
- // ═══════════════════════════════════════════════════════════════════════════════
405
-
406
- /// Returns the version and build info.
407
- #[wasm_bindgen(js_name = "getVersion")]
408
- pub fn get_version() -> String {
409
- format!(
410
- "clawpowers-wasm v{} ({})",
411
- env!("CARGO_PKG_VERSION"),
412
- if cfg!(debug_assertions) {
413
- "debug"
414
- } else {
415
- "release"
416
- }
417
- )
418
- }
419
-
420
- /// Returns a list of modules available in this WASM build.
421
- #[wasm_bindgen(js_name = "getAvailableModules")]
422
- pub fn get_available_modules() -> String {
423
- serde_json::to_string(&vec![
424
- "tokens",
425
- "fee",
426
- "compression",
427
- "canonical",
428
- "verification",
429
- "security",
430
- "index",
431
- ])
432
- .unwrap_or_default()
433
- }
1
+ //! WASM bindings for ClawPowers core.
2
+ //!
3
+ //! Exposes the following modules via `wasm-bindgen`:
4
+ //!
5
+ //! - **tokens** — Token registry, token amounts, decimal math
6
+ //! - **fee** — Fee schedule calculation
7
+ //! - **policy** — Spending policy engine
8
+ //! - **compression** — TurboQuant vector compression
9
+ //! - **index** — In-memory vector index
10
+ //! - **canonical** — Canonical record store (in-memory backend for WASM)
11
+ //! - **verification** — Record verification pipeline
12
+ //! - **security** — Write firewall and audit log
13
+ //!
14
+ //! **NOT included (falls back to TypeScript):**
15
+ //! - **wallet** — Uses `alloy-signer-local` which depends on native crypto
16
+ //! primitives that are not fully WASM-compatible without significant patching.
17
+ //! The TypeScript fallback uses `ethers.js` / `viem` for equivalent functionality.
18
+ //! - **x402** — Uses `reqwest` with `tokio` runtime; in WASM, HTTP is handled
19
+ //! natively by `fetch()` in the TypeScript layer.
20
+
21
+ use wasm_bindgen::prelude::*;
22
+
23
+ // ═══════════════════════════════════════════════════════════════════════════════
24
+ // Tokens
25
+ // ═══════════════════════════════════════════════════════════════════════════════
26
+
27
+ /// Create a TokenAmount from a human-readable f64 value and decimal count.
28
+ /// Returns JSON: `{"raw": "...", "decimals": N}`
29
+ #[wasm_bindgen(js_name = "tokenAmountFromHuman")]
30
+ pub fn token_amount_from_human(human: f64, decimals: u8) -> Result<String, JsError> {
31
+ let amount = clawpowers_tokens::TokenAmount::from_human(human, decimals);
32
+ Ok(serde_json::to_string(&amount)?)
33
+ }
34
+
35
+ /// Convert a TokenAmount JSON back to a human-readable f64.
36
+ #[wasm_bindgen(js_name = "tokenAmountToHuman")]
37
+ pub fn token_amount_to_human(json: &str) -> Result<f64, JsError> {
38
+ let amount: clawpowers_tokens::TokenAmount = serde_json::from_str(json)?;
39
+ Ok(amount.to_human())
40
+ }
41
+
42
+ /// Add two TokenAmount JSONs. Returns the sum as JSON.
43
+ #[wasm_bindgen(js_name = "tokenAmountAdd")]
44
+ pub fn token_amount_add(a_json: &str, b_json: &str) -> Result<String, JsError> {
45
+ let a: clawpowers_tokens::TokenAmount = serde_json::from_str(a_json)?;
46
+ let b: clawpowers_tokens::TokenAmount = serde_json::from_str(b_json)?;
47
+ let result = a.add(&b).map_err(|e| JsError::new(&e.to_string()))?;
48
+ Ok(serde_json::to_string(&result)?)
49
+ }
50
+
51
+ /// Subtract two TokenAmount JSONs. Returns the difference as JSON.
52
+ #[wasm_bindgen(js_name = "tokenAmountSub")]
53
+ pub fn token_amount_sub(a_json: &str, b_json: &str) -> Result<String, JsError> {
54
+ let a: clawpowers_tokens::TokenAmount = serde_json::from_str(a_json)?;
55
+ let b: clawpowers_tokens::TokenAmount = serde_json::from_str(b_json)?;
56
+ let result = a.sub(&b).map_err(|e| JsError::new(&e.to_string()))?;
57
+ Ok(serde_json::to_string(&result)?)
58
+ }
59
+
60
+ /// Multiply a TokenAmount by basis points. Returns result as JSON.
61
+ #[wasm_bindgen(js_name = "tokenAmountMulBps")]
62
+ pub fn token_amount_mul_bps(json: &str, bps: u64) -> Result<String, JsError> {
63
+ let amount: clawpowers_tokens::TokenAmount = serde_json::from_str(json)?;
64
+ let result = amount
65
+ .checked_mul_bps(bps)
66
+ .ok_or_else(|| JsError::new("overflow in checked_mul_bps"))?;
67
+ Ok(serde_json::to_string(&result)?)
68
+ }
69
+
70
+ /// Get the default token registry as JSON.
71
+ #[wasm_bindgen(js_name = "getDefaultTokenRegistry")]
72
+ pub fn get_default_token_registry() -> Result<String, JsError> {
73
+ let registry = clawpowers_tokens::TokenRegistry::default();
74
+ let tokens: Vec<_> = registry.iter().collect();
75
+ Ok(serde_json::to_string(&tokens)?)
76
+ }
77
+
78
+ // ═══════════════════════════════════════════════════════════════════════════════
79
+ // Fee
80
+ // ═══════════════════════════════════════════════════════════════════════════════
81
+
82
+ /// Calculate a transaction fee.
83
+ /// Returns JSON: `{"gross_amount": ..., "fee_amount": ..., "net_amount": ...}`
84
+ #[wasm_bindgen(js_name = "calculateFee")]
85
+ pub fn calculate_fee(
86
+ amount_json: &str,
87
+ fee_type: &str,
88
+ tx_fee_bps: Option<u64>,
89
+ swap_fee_bps: Option<u64>,
90
+ ) -> Result<String, JsError> {
91
+ let amount: clawpowers_tokens::TokenAmount = serde_json::from_str(amount_json)?;
92
+
93
+ let fee_type = match fee_type {
94
+ "transaction" => clawpowers_fee::FeeType::Transaction,
95
+ "swap" => clawpowers_fee::FeeType::Swap,
96
+ s if s.starts_with("custom:") => {
97
+ let bps: u64 = s[7..]
98
+ .parse()
99
+ .map_err(|_| JsError::new("invalid custom bps"))?;
100
+ clawpowers_fee::FeeType::Custom(bps)
101
+ }
102
+ _ => return Err(JsError::new("fee_type must be 'transaction', 'swap', or 'custom:N'")),
103
+ };
104
+
105
+ let schedule = clawpowers_fee::FeeSchedule::new(
106
+ tx_fee_bps.unwrap_or(clawpowers_fee::DEFAULT_TX_FEE_BPS),
107
+ swap_fee_bps.unwrap_or(clawpowers_fee::DEFAULT_SWAP_FEE_BPS),
108
+ alloy_primitives::Address::ZERO,
109
+ );
110
+
111
+ let calc = schedule
112
+ .calculate(amount, fee_type)
113
+ .map_err(|e| JsError::new(&e.to_string()))?;
114
+
115
+ let result = serde_json::json!({
116
+ "gross_amount": calc.gross_amount.to_human(),
117
+ "fee_amount": calc.fee_amount.to_human(),
118
+ "net_amount": calc.net_amount.to_human(),
119
+ });
120
+ Ok(serde_json::to_string(&result)?)
121
+ }
122
+
123
+ // ═══════════════════════════════════════════════════════════════════════════════
124
+ // Compression
125
+ // ═══════════════════════════════════════════════════════════════════════════════
126
+
127
+ /// Compress a float32 vector. Input: JSON array of f32. Output: CompressedVector JSON.
128
+ #[wasm_bindgen(js_name = "compressVector")]
129
+ pub fn compress_vector(vector_json: &str, dimensions: usize) -> Result<String, JsError> {
130
+ let vector: Vec<f32> = serde_json::from_str(vector_json)?;
131
+ let config = clawpowers_compression::CompressionConfig {
132
+ dimensions,
133
+ quantization_bits: 8,
134
+ rotation_seed: 0xDEAD_BEEF_CAFE_1234,
135
+ };
136
+ let compressor = clawpowers_compression::TurboCompressor::new(config);
137
+ let compressed = compressor
138
+ .compress(&vector)
139
+ .map_err(|e| JsError::new(&e.to_string()))?;
140
+ Ok(serde_json::to_string(&compressed)?)
141
+ }
142
+
143
+ /// Decompress a CompressedVector JSON back to a float32 array JSON.
144
+ #[wasm_bindgen(js_name = "decompressVector")]
145
+ pub fn decompress_vector(compressed_json: &str, dimensions: usize) -> Result<String, JsError> {
146
+ let compressed: clawpowers_compression::CompressedVector =
147
+ serde_json::from_str(compressed_json)?;
148
+ let config = clawpowers_compression::CompressionConfig {
149
+ dimensions,
150
+ quantization_bits: 8,
151
+ rotation_seed: 0xDEAD_BEEF_CAFE_1234,
152
+ };
153
+ let compressor = clawpowers_compression::TurboCompressor::new(config);
154
+ let vector = compressor
155
+ .decompress(&compressed)
156
+ .map_err(|e| JsError::new(&e.to_string()))?;
157
+ Ok(serde_json::to_string(&vector)?)
158
+ }
159
+
160
+ /// Compute approximate distance between two CompressedVector JSONs.
161
+ #[wasm_bindgen(js_name = "approximateDistance")]
162
+ pub fn approximate_distance(
163
+ a_json: &str,
164
+ b_json: &str,
165
+ dimensions: usize,
166
+ ) -> Result<f32, JsError> {
167
+ let a: clawpowers_compression::CompressedVector = serde_json::from_str(a_json)?;
168
+ let b: clawpowers_compression::CompressedVector = serde_json::from_str(b_json)?;
169
+ let config = clawpowers_compression::CompressionConfig {
170
+ dimensions,
171
+ quantization_bits: 8,
172
+ rotation_seed: 0xDEAD_BEEF_CAFE_1234,
173
+ };
174
+ let compressor = clawpowers_compression::TurboCompressor::new(config);
175
+ let dist = compressor
176
+ .approximate_distance(&a, &b)
177
+ .map_err(|e| JsError::new(&e.to_string()))?;
178
+ Ok(dist)
179
+ }
180
+
181
+ /// Compute SHA-256 hash of content string.
182
+ #[wasm_bindgen(js_name = "computeSha256")]
183
+ pub fn compute_sha256(content: &str) -> String {
184
+ clawpowers_canonical::compute_sha256(content)
185
+ }
186
+
187
+ /// Compute Keccak-256 hash of raw bytes (EVM-compatible). Returns `0x` + 64 hex chars.
188
+ #[wasm_bindgen(js_name = "computeKeccak256")]
189
+ pub fn compute_keccak256(bytes: &[u8]) -> String {
190
+ let h = alloy_primitives::keccak256(bytes);
191
+ format!("0x{:x}", h)
192
+ }
193
+
194
+ /// Ethereum address from 32-byte secp256k1 private key (`0x` + 20 bytes, EIP-55 checksum).
195
+ #[wasm_bindgen(js_name = "deriveEthereumAddress")]
196
+ pub fn derive_ethereum_address_wasm(private_key: &[u8]) -> Result<String, JsError> {
197
+ clawpowers_evm_eth::derive_ethereum_address(private_key).map_err(|e| JsError::new(&e))
198
+ }
199
+
200
+ /// Uncompressed public key: 64 bytes (x || y), no `0x04` prefix.
201
+ #[wasm_bindgen(js_name = "derivePublicKey")]
202
+ pub fn derive_public_key_wasm(private_key: &[u8]) -> Result<Vec<u8>, JsError> {
203
+ clawpowers_evm_eth::derive_public_key(private_key).map_err(|e| JsError::new(&e))
204
+ }
205
+
206
+ /// ECDSA sign 32-byte message hash → 65 bytes (r || s || recovery_id).
207
+ #[wasm_bindgen(js_name = "signEcdsa")]
208
+ pub fn sign_ecdsa_wasm(private_key: &[u8], message_hash: &[u8]) -> Result<Vec<u8>, JsError> {
209
+ clawpowers_evm_eth::sign_ecdsa(private_key, message_hash).map_err(|e| JsError::new(&e))
210
+ }
211
+
212
+ #[wasm_bindgen(js_name = "verifyEcdsa")]
213
+ pub fn verify_ecdsa_wasm(
214
+ public_key: &[u8],
215
+ message_hash: &[u8],
216
+ signature: &[u8],
217
+ ) -> Result<bool, JsError> {
218
+ clawpowers_evm_eth::verify_ecdsa(public_key, message_hash, signature).map_err(|e| JsError::new(&e))
219
+ }
220
+
221
+ // ═══════════════════════════════════════════════════════════════════════════════
222
+ // Canonical Store
223
+ // ═══════════════════════════════════════════════════════════════════════════════
224
+
225
+ /// Opaque handle to an in-memory CanonicalStore.
226
+ #[wasm_bindgen]
227
+ pub struct WasmCanonicalStore {
228
+ inner: clawpowers_canonical::CanonicalStore,
229
+ }
230
+
231
+ #[wasm_bindgen]
232
+ impl WasmCanonicalStore {
233
+ /// Create a new in-memory canonical store.
234
+ #[wasm_bindgen(constructor)]
235
+ pub fn new() -> Result<WasmCanonicalStore, JsError> {
236
+ let store = clawpowers_canonical::CanonicalStore::in_memory()
237
+ .map_err(|e| JsError::new(&e.to_string()))?;
238
+ Ok(Self { inner: store })
239
+ }
240
+
241
+ /// Insert a record. Input: JSON with {namespace, content, metadata, provenance}.
242
+ /// Returns the record UUID as a string.
243
+ pub fn insert(&self, json: &str) -> Result<String, JsError> {
244
+ #[derive(serde::Deserialize)]
245
+ struct InsertInput {
246
+ namespace: String,
247
+ content: String,
248
+ #[serde(default)]
249
+ metadata: serde_json::Value,
250
+ #[serde(default = "default_provenance")]
251
+ provenance: String,
252
+ }
253
+ fn default_provenance() -> String {
254
+ "wasm".to_string()
255
+ }
256
+
257
+ let input: InsertInput = serde_json::from_str(json)?;
258
+ let record = clawpowers_canonical::CanonicalRecord::new(
259
+ input.namespace,
260
+ input.content,
261
+ None,
262
+ input.metadata,
263
+ input.provenance,
264
+ );
265
+ let id = self
266
+ .inner
267
+ .insert(&record)
268
+ .map_err(|e| JsError::new(&e.to_string()))?;
269
+ Ok(id.to_string())
270
+ }
271
+
272
+ /// Get a record by UUID. Returns JSON or null.
273
+ pub fn get(&self, id: &str) -> Result<Option<String>, JsError> {
274
+ let uuid = uuid::Uuid::parse_str(id).map_err(|e| JsError::new(&e.to_string()))?;
275
+ let record = self
276
+ .inner
277
+ .get(&uuid)
278
+ .map_err(|e| JsError::new(&e.to_string()))?;
279
+ match record {
280
+ Some(r) => Ok(Some(serde_json::to_string(&r)?)),
281
+ None => Ok(None),
282
+ }
283
+ }
284
+
285
+ /// Get a record by content hash. Returns JSON or null.
286
+ #[wasm_bindgen(js_name = "getByHash")]
287
+ pub fn get_by_hash(&self, hash: &str) -> Result<Option<String>, JsError> {
288
+ let record = self
289
+ .inner
290
+ .get_by_hash(hash)
291
+ .map_err(|e| JsError::new(&e.to_string()))?;
292
+ match record {
293
+ Some(r) => Ok(Some(serde_json::to_string(&r)?)),
294
+ None => Ok(None),
295
+ }
296
+ }
297
+
298
+ /// Query records by namespace. Returns JSON array.
299
+ #[wasm_bindgen(js_name = "queryNamespace")]
300
+ pub fn query_namespace(&self, namespace: &str, limit: usize) -> Result<String, JsError> {
301
+ let records = self
302
+ .inner
303
+ .query_namespace(namespace, limit)
304
+ .map_err(|e| JsError::new(&e.to_string()))?;
305
+ Ok(serde_json::to_string(&records)?)
306
+ }
307
+
308
+ /// Soft-delete a record. Returns true if the record existed.
309
+ #[wasm_bindgen(js_name = "softDelete")]
310
+ pub fn soft_delete(&self, id: &str) -> Result<bool, JsError> {
311
+ let uuid = uuid::Uuid::parse_str(id).map_err(|e| JsError::new(&e.to_string()))?;
312
+ self.inner
313
+ .soft_delete(&uuid)
314
+ .map_err(|e| JsError::new(&e.to_string()))
315
+ }
316
+
317
+ /// Verify record integrity. Returns true if hash matches.
318
+ #[wasm_bindgen(js_name = "verifyIntegrity")]
319
+ pub fn verify_integrity(&self, id: &str) -> Result<bool, JsError> {
320
+ let uuid = uuid::Uuid::parse_str(id).map_err(|e| JsError::new(&e.to_string()))?;
321
+ self.inner
322
+ .verify_integrity(&uuid)
323
+ .map_err(|e| JsError::new(&e.to_string()))
324
+ }
325
+
326
+ /// Export all records as JSON for IndexedDB persistence.
327
+ #[wasm_bindgen(js_name = "exportJson")]
328
+ pub fn export_json(&self) -> Result<String, JsError> {
329
+ self.inner
330
+ .export_json()
331
+ .map_err(|e| JsError::new(&e.to_string()))
332
+ }
333
+
334
+ /// Import records from JSON (e.g., loaded from IndexedDB).
335
+ #[wasm_bindgen(js_name = "importJson")]
336
+ pub fn import_json(&self, json: &str) -> Result<usize, JsError> {
337
+ self.inner
338
+ .import_json(json)
339
+ .map_err(|e| JsError::new(&e.to_string()))
340
+ }
341
+ }
342
+
343
+ // ═══════════════════════════════════════════════════════════════════════════════
344
+ // Security — Write Firewall
345
+ // ═══════════════════════════════════════════════════════════════════════════════
346
+
347
+ /// Evaluate a write request against a firewall.
348
+ /// Input JSON: {namespace, content, trust_level, source, allowed_namespaces?, blocked_patterns?, max_content_length?}
349
+ /// Returns JSON: {"decision": "allow"|"deny"|"sanitize", "reason"?: ..., "sanitized"?: ...}
350
+ #[wasm_bindgen(js_name = "evaluateWriteFirewall")]
351
+ pub fn evaluate_write_firewall(json: &str) -> Result<String, JsError> {
352
+ #[derive(serde::Deserialize)]
353
+ struct Input {
354
+ namespace: String,
355
+ content: String,
356
+ trust_level: String,
357
+ source: String,
358
+ #[serde(default)]
359
+ allowed_namespaces: Vec<String>,
360
+ #[serde(default)]
361
+ blocked_patterns: Vec<String>,
362
+ max_content_length: Option<usize>,
363
+ }
364
+
365
+ let input: Input = serde_json::from_str(json)?;
366
+
367
+ let trust = match input.trust_level.as_str() {
368
+ "system" => clawpowers_security::TrustLevel::System,
369
+ "agent" => clawpowers_security::TrustLevel::Agent,
370
+ "external" => clawpowers_security::TrustLevel::External,
371
+ _ => clawpowers_security::TrustLevel::Untrusted,
372
+ };
373
+
374
+ let mut firewall = clawpowers_security::WriteFirewall::new(input.allowed_namespaces);
375
+ firewall.blocked_patterns = input.blocked_patterns;
376
+ if let Some(max) = input.max_content_length {
377
+ firewall.max_content_length = max;
378
+ }
379
+
380
+ let request = clawpowers_security::WriteRequest {
381
+ namespace: input.namespace,
382
+ content: input.content,
383
+ trust_level: trust,
384
+ source: input.source,
385
+ };
386
+
387
+ let decision = firewall.evaluate(&request);
388
+ let result = match decision {
389
+ clawpowers_security::FirewallDecision::Allow => {
390
+ serde_json::json!({"decision": "allow"})
391
+ }
392
+ clawpowers_security::FirewallDecision::Deny(reason) => {
393
+ serde_json::json!({"decision": "deny", "reason": reason})
394
+ }
395
+ clawpowers_security::FirewallDecision::Sanitize(original, sanitized) => {
396
+ serde_json::json!({"decision": "sanitize", "original": original, "sanitized": sanitized})
397
+ }
398
+ };
399
+ Ok(serde_json::to_string(&result)?)
400
+ }
401
+
402
+ // ═══════════════════════════════════════════════════════════════════════════════
403
+ // Version info
404
+ // ═══════════════════════════════════════════════════════════════════════════════
405
+
406
+ /// Returns the version and build info.
407
+ #[wasm_bindgen(js_name = "getVersion")]
408
+ pub fn get_version() -> String {
409
+ format!(
410
+ "clawpowers-wasm v{} ({})",
411
+ env!("CARGO_PKG_VERSION"),
412
+ if cfg!(debug_assertions) {
413
+ "debug"
414
+ } else {
415
+ "release"
416
+ }
417
+ )
418
+ }
419
+
420
+ /// Returns a list of modules available in this WASM build.
421
+ #[wasm_bindgen(js_name = "getAvailableModules")]
422
+ pub fn get_available_modules() -> String {
423
+ serde_json::to_string(&vec![
424
+ "tokens",
425
+ "fee",
426
+ "compression",
427
+ "canonical",
428
+ "verification",
429
+ "security",
430
+ "index",
431
+ ])
432
+ .unwrap_or_default()
433
+ }