1use crate::types::AuthDecision;
2use crate::utils::parse_resource;
3use core_identity::Identity;
4use core_policy::{Action, Policy};
5use secrecy::ExposeSecret;
6use serde::Deserialize;
7use wasm_bindgen::prelude::*;
8
9#[wasm_bindgen]
14pub struct P47hClient {
15 identity: Identity,
18}
19
20#[wasm_bindgen]
21impl P47hClient {
22 #[wasm_bindgen(constructor)]
28 pub fn generate_new() -> Result<P47hClient, JsValue> {
29 let mut rng = rand::thread_rng();
30 let identity = Identity::generate(&mut rng)
31 .map_err(|e| JsValue::from_str(&format!("Failed to generate identity: {}", e)))?;
32
33 Ok(P47hClient { identity })
34 }
35
36 #[wasm_bindgen]
38 pub fn from_secret(secret_bytes: &[u8]) -> Result<P47hClient, JsValue> {
39 if secret_bytes.len() != 32 {
40 return Err(JsValue::from_str("Secret must be exactly 32 bytes"));
41 }
42
43 let mut key_bytes = [0u8; 32];
44 key_bytes.copy_from_slice(secret_bytes);
45
46 let identity = Identity::from_bytes(&key_bytes)
47 .map_err(|e| JsValue::from_str(&format!("Failed to import identity: {}", e)))?;
48
49 Ok(P47hClient { identity })
50 }
51
52 #[wasm_bindgen]
76 pub fn export_wrapped_secret(&self, session_key: &[u8]) -> Result<Vec<u8>, JsValue> {
77 use chacha20poly1305::{
78 aead::{Aead, KeyInit},
79 ChaCha20Poly1305, Nonce,
80 };
81
82 if session_key.len() != 32 {
83 return Err(JsValue::from_str("Session key must be exactly 32 bytes"));
84 }
85
86 let cipher = ChaCha20Poly1305::new_from_slice(session_key)
87 .map_err(|e| JsValue::from_str(&format!("Invalid session key: {}", e)))?;
88
89 let mut nonce_bytes = [0u8; 12];
91 use rand::RngCore;
92 rand::thread_rng().fill_bytes(&mut nonce_bytes);
93 let nonce = Nonce::from_slice(&nonce_bytes);
94
95 let signing_key = self.identity.signing_key_bytes();
97 let secret = signing_key.expose_secret();
98 let ciphertext = cipher
99 .encrypt(nonce, secret.as_ref())
100 .map_err(|e| JsValue::from_str(&format!("Encryption failed: {}", e)))?;
101
102 let mut result = Vec::with_capacity(12 + ciphertext.len());
104 result.extend_from_slice(&nonce_bytes);
105 result.extend_from_slice(&ciphertext);
106
107 Ok(result)
108 }
109
110 #[wasm_bindgen]
128 pub fn from_wrapped_secret(wrapped: &[u8], session_key: &[u8]) -> Result<P47hClient, JsValue> {
129 use chacha20poly1305::{
130 aead::{Aead, KeyInit},
131 ChaCha20Poly1305, Nonce,
132 };
133
134 if wrapped.len() < 12 + 32 + 16 {
135 return Err(JsValue::from_str(
136 "Invalid wrapped secret: too short (expected at least 60 bytes)",
137 ));
138 }
139
140 if session_key.len() != 32 {
141 return Err(JsValue::from_str("Session key must be exactly 32 bytes"));
142 }
143
144 let cipher = ChaCha20Poly1305::new_from_slice(session_key)
145 .map_err(|e| JsValue::from_str(&format!("Invalid session key: {}", e)))?;
146
147 let nonce = Nonce::from_slice(&wrapped[..12]);
148 let ciphertext = &wrapped[12..];
149
150 let plaintext = cipher.decrypt(nonce, ciphertext).map_err(|_| {
151 JsValue::from_str("Decryption failed: wrong password or corrupted data")
152 })?;
153
154 Self::from_secret(&plaintext)
155 }
156
157 #[wasm_bindgen]
159 pub fn get_did(&self) -> String {
160 let verifying_key = self.identity.verifying_key();
161 let pub_key_bytes = verifying_key.as_bytes();
162 format!("did:p47h:{}", hex::encode(pub_key_bytes))
163 }
164
165 #[wasm_bindgen]
167 pub fn get_public_key(&self) -> Vec<u8> {
168 self.identity.verifying_key().as_bytes().to_vec()
169 }
170
171 #[wasm_bindgen]
173 pub fn sign_challenge(&self, challenge: &[u8]) -> Vec<u8> {
174 self.identity.sign(challenge).to_bytes().to_vec()
175 }
176
177 #[wasm_bindgen]
179 pub fn sign_data(&self, data: &[u8]) -> Vec<u8> {
180 self.identity.sign(data).to_bytes().to_vec()
181 }
182
183 #[wasm_bindgen]
189 pub fn evaluate_request(
190 &self,
191 policy_toml: &str,
192 resource: &str,
193 action: &str,
194 ) -> Result<JsValue, JsValue> {
195 let start = web_sys::window()
196 .and_then(|w| w.performance())
197 .map(|p| p.now())
198 .unwrap_or(0.0);
199
200 let policy: Policy = toml::from_str(policy_toml)
202 .map_err(|e| JsValue::from_str(&format!("Invalid policy TOML: {}", e)))?;
203
204 let did = self.get_did();
206
207 let action_enum = match action.to_lowercase().as_str() {
209 "read" => Action::Read,
210 "write" => Action::Write,
211 "execute" => Action::Execute,
212 "delete" => Action::Delete,
213 "all" | "*" => Action::All,
214 custom => Action::Custom(custom.to_string()),
215 };
216
217 let resource_enum = parse_resource(resource);
219
220 let allowed = policy.is_allowed(&did, &action_enum, &resource_enum);
222
223 let end = web_sys::window()
224 .and_then(|w| w.performance())
225 .map(|p| p.now())
226 .unwrap_or(0.0);
227
228 let evaluation_time_us = ((end - start) * 1000.0) as u64;
231
232 let reasons = if allowed {
234 vec![format!(
235 "ALLOWED: {} access to {} granted by policy",
236 action, resource
237 )]
238 } else {
239 vec![format!(
240 "DENIED: {} access to {} denied by policy",
241 action, resource
242 )]
243 };
244
245 let decision = AuthDecision {
246 allowed,
247 reasons,
248 evaluation_time_us,
249 };
250
251 serde_wasm_bindgen::to_value(&decision)
252 .map_err(|e| JsValue::from_str(&format!("Serialization error: {}", e)))
253 }
254
255 #[wasm_bindgen]
257 pub fn evaluate_batch(
258 &self,
259 policy_toml: &str,
260 requests_json: &str,
261 ) -> Result<JsValue, JsValue> {
262 #[derive(Deserialize)]
263 struct BatchRequest {
264 resource: String,
265 action: String,
266 }
267
268 let start = web_sys::window()
269 .and_then(|w| w.performance())
270 .map(|p| p.now())
271 .unwrap_or(0.0);
272
273 let policy: Policy = toml::from_str(policy_toml)
275 .map_err(|e| JsValue::from_str(&format!("Invalid policy TOML: {}", e)))?;
276
277 let requests: Vec<BatchRequest> = serde_json::from_str(requests_json)
279 .map_err(|e| JsValue::from_str(&format!("Invalid requests JSON: {}", e)))?;
280
281 let did = self.get_did();
282 let mut decisions = Vec::with_capacity(requests.len());
283
284 for req in requests {
286 let action_enum = match req.action.to_lowercase().as_str() {
287 "read" => Action::Read,
288 "write" => Action::Write,
289 "execute" => Action::Execute,
290 "delete" => Action::Delete,
291 "all" | "*" => Action::All,
292 custom => Action::Custom(custom.to_string()),
293 };
294
295 let resource_enum = parse_resource(&req.resource);
296 let allowed = policy.is_allowed(&did, &action_enum, &resource_enum);
297
298 let reasons = if allowed {
299 vec![format!(
300 "ALLOWED: {} access to {} granted",
301 req.action, req.resource
302 )]
303 } else {
304 vec![format!(
305 "DENIED: {} access to {} denied",
306 req.action, req.resource
307 )]
308 };
309
310 decisions.push(AuthDecision {
311 allowed,
312 reasons,
313 evaluation_time_us: 0, });
315 }
316
317 let end = web_sys::window()
318 .and_then(|w| w.performance())
319 .map(|p| p.now())
320 .unwrap_or(0.0);
321
322 let total_time_us = ((end - start) * 1000.0) as u64;
324
325 if let Some(last) = decisions.last_mut() {
327 last.evaluation_time_us = total_time_us;
328 }
329
330 serde_wasm_bindgen::to_value(&decisions)
331 .map_err(|e| JsValue::from_str(&format!("Serialization error: {}", e)))
332 }
333}