Add coracle-wasm bindings
This commit is contained in:
@@ -0,0 +1,16 @@
|
||||
[package]
|
||||
name = "coracle-wasm"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
description = "Web bindings for coracle-lib via wasm-bindgen"
|
||||
|
||||
[lib]
|
||||
crate-type = ["cdylib", "rlib"]
|
||||
|
||||
[dependencies]
|
||||
coracle-lib = { path = "../coracle-lib" }
|
||||
wasm-bindgen = "0.2"
|
||||
serde-wasm-bindgen = "0.6"
|
||||
serde_json = "1"
|
||||
hex = "0.4"
|
||||
secp256k1 = "0.29"
|
||||
@@ -0,0 +1,265 @@
|
||||
//! Web bindings for `coracle-lib` via `wasm-bindgen`.
|
||||
//!
|
||||
//! Wraps the core nostr types — keys, NIP-44 encryption, events — in
|
||||
//! `#[wasm_bindgen]` shims so they can be consumed from JavaScript. The
|
||||
//! wrappers hold the underlying `coracle-lib` value by composition and
|
||||
//! translate its errors into `JsError`.
|
||||
|
||||
use wasm_bindgen::prelude::*;
|
||||
|
||||
use coracle_lib::encryption as ce;
|
||||
use coracle_lib::event as cev;
|
||||
use coracle_lib::keys as ck;
|
||||
|
||||
fn js_err<E: std::fmt::Display>(e: E) -> JsError {
|
||||
JsError::new(&e.to_string())
|
||||
}
|
||||
|
||||
// ---------- Keys ----------
|
||||
|
||||
/// A nostr public key: the x-only 32-byte secp256k1 coordinate.
|
||||
#[wasm_bindgen]
|
||||
pub struct PublicKey(ck::PublicKey);
|
||||
|
||||
#[wasm_bindgen]
|
||||
impl PublicKey {
|
||||
#[wasm_bindgen(js_name = fromHex)]
|
||||
pub fn from_hex(s: &str) -> Result<PublicKey, JsError> {
|
||||
ck::PublicKey::from_hex(s).map(PublicKey).map_err(js_err)
|
||||
}
|
||||
|
||||
#[wasm_bindgen(js_name = fromNpub)]
|
||||
pub fn from_npub(s: &str) -> Result<PublicKey, JsError> {
|
||||
ck::PublicKey::from_npub(s).map(PublicKey).map_err(js_err)
|
||||
}
|
||||
|
||||
#[wasm_bindgen(js_name = toHex)]
|
||||
pub fn to_hex(&self) -> String {
|
||||
self.0.to_hex()
|
||||
}
|
||||
|
||||
#[wasm_bindgen(js_name = toNpub)]
|
||||
pub fn to_npub(&self) -> String {
|
||||
self.0.to_npub()
|
||||
}
|
||||
|
||||
#[wasm_bindgen(js_name = toBytes)]
|
||||
pub fn to_bytes(&self) -> Vec<u8> {
|
||||
self.0.as_bytes().to_vec()
|
||||
}
|
||||
}
|
||||
|
||||
/// A nostr secret key. Exporting the raw bytes is explicit via `toHex` /
|
||||
/// `toNsec` — there is no implicit string conversion.
|
||||
#[wasm_bindgen]
|
||||
pub struct SecretKey(ck::SecretKey);
|
||||
|
||||
#[wasm_bindgen]
|
||||
impl SecretKey {
|
||||
/// Generate a new secret key from the OS RNG.
|
||||
#[wasm_bindgen]
|
||||
pub fn generate() -> SecretKey {
|
||||
SecretKey(ck::SecretKey::generate())
|
||||
}
|
||||
|
||||
#[wasm_bindgen(js_name = fromHex)]
|
||||
pub fn from_hex(s: &str) -> Result<SecretKey, JsError> {
|
||||
ck::SecretKey::from_hex(s).map(SecretKey).map_err(js_err)
|
||||
}
|
||||
|
||||
#[wasm_bindgen(js_name = fromNsec)]
|
||||
pub fn from_nsec(s: &str) -> Result<SecretKey, JsError> {
|
||||
ck::SecretKey::from_nsec(s).map(SecretKey).map_err(js_err)
|
||||
}
|
||||
|
||||
/// Decrypt a NIP-49 `ncryptsec1…` string.
|
||||
#[wasm_bindgen(js_name = fromNcryptsec)]
|
||||
pub fn from_ncryptsec(s: &str, password: &str) -> Result<SecretKey, JsError> {
|
||||
ck::SecretKey::from_ncryptsec(s, password)
|
||||
.map(SecretKey)
|
||||
.map_err(js_err)
|
||||
}
|
||||
|
||||
#[wasm_bindgen(js_name = toHex)]
|
||||
pub fn to_hex(&self) -> String {
|
||||
self.0.to_hex()
|
||||
}
|
||||
|
||||
#[wasm_bindgen(js_name = toNsec)]
|
||||
pub fn to_nsec(&self) -> String {
|
||||
self.0.to_nsec()
|
||||
}
|
||||
|
||||
/// Encrypt this key as a NIP-49 `ncryptsec1…` string.
|
||||
#[wasm_bindgen(js_name = toNcryptsec)]
|
||||
pub fn to_ncryptsec(
|
||||
&self,
|
||||
password: &str,
|
||||
log_n: u8,
|
||||
security_byte: u8,
|
||||
) -> Result<String, JsError> {
|
||||
self.0
|
||||
.to_ncryptsec(password, log_n, security_byte)
|
||||
.map_err(js_err)
|
||||
}
|
||||
|
||||
#[wasm_bindgen(js_name = publicKey)]
|
||||
pub fn public_key(&self) -> PublicKey {
|
||||
PublicKey(self.0.public_key())
|
||||
}
|
||||
|
||||
#[wasm_bindgen(js_name = nip44Encrypt)]
|
||||
pub fn nip44_encrypt(&self, pk: &PublicKey, plaintext: &str) -> Result<String, JsError> {
|
||||
self.0.nip44_encrypt(&pk.0, plaintext).map_err(js_err)
|
||||
}
|
||||
|
||||
#[wasm_bindgen(js_name = nip44Decrypt)]
|
||||
pub fn nip44_decrypt(&self, pk: &PublicKey, payload: &str) -> Result<String, JsError> {
|
||||
self.0.nip44_decrypt(&pk.0, payload).map_err(js_err)
|
||||
}
|
||||
}
|
||||
|
||||
// ---------- Encryption ----------
|
||||
|
||||
/// Raw 32-byte ECDH shared secret between a secret and a public key.
|
||||
#[wasm_bindgen(js_name = sharedSecret)]
|
||||
pub fn shared_secret(sk: &SecretKey, pk: &PublicKey) -> Vec<u8> {
|
||||
ce::shared_secret(&sk.0, &pk.0).to_vec()
|
||||
}
|
||||
|
||||
/// A reusable NIP-44 conversation key. Derive once per `(sk, pk)` pair and
|
||||
/// hold it when sending many messages to the same counterparty.
|
||||
#[wasm_bindgen]
|
||||
pub struct ConversationKey(ce::nip44::ConversationKey);
|
||||
|
||||
#[wasm_bindgen]
|
||||
impl ConversationKey {
|
||||
#[wasm_bindgen]
|
||||
pub fn derive(sk: &SecretKey, pk: &PublicKey) -> ConversationKey {
|
||||
ConversationKey(ce::nip44::ConversationKey::derive(&sk.0, &pk.0))
|
||||
}
|
||||
|
||||
#[wasm_bindgen(js_name = toBytes)]
|
||||
pub fn to_bytes(&self) -> Vec<u8> {
|
||||
self.0.as_bytes().to_vec()
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
pub fn encrypt(&self, plaintext: &str) -> Result<String, JsError> {
|
||||
self.0.encrypt(plaintext).map_err(js_err)
|
||||
}
|
||||
|
||||
#[wasm_bindgen(js_name = encryptWithNonce)]
|
||||
pub fn encrypt_with_nonce(&self, plaintext: &str, nonce: &[u8]) -> Result<String, JsError> {
|
||||
let nonce: &[u8; 32] = nonce
|
||||
.try_into()
|
||||
.map_err(|_| JsError::new("nonce must be exactly 32 bytes"))?;
|
||||
self.0.encrypt_with_nonce(plaintext, nonce).map_err(js_err)
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
pub fn decrypt(&self, payload: &str) -> Result<String, JsError> {
|
||||
self.0.decrypt(payload).map_err(js_err)
|
||||
}
|
||||
}
|
||||
|
||||
// ---------- Events ----------
|
||||
|
||||
/// A signed nostr event.
|
||||
#[wasm_bindgen]
|
||||
pub struct Event(cev::Event);
|
||||
|
||||
#[wasm_bindgen]
|
||||
impl Event {
|
||||
/// Build and sign a new event under `sk`. `tags` must be a JS array of
|
||||
/// string arrays (`string[][]`).
|
||||
#[wasm_bindgen(js_name = signNew)]
|
||||
pub fn sign_new(
|
||||
kind: u16,
|
||||
content: &str,
|
||||
tags: JsValue,
|
||||
created_at: u64,
|
||||
sk: &SecretKey,
|
||||
) -> Result<Event, JsError> {
|
||||
let tags: Vec<Vec<String>> =
|
||||
serde_wasm_bindgen::from_value(tags).map_err(|e| JsError::new(&e.to_string()))?;
|
||||
// coracle-lib's SecretKey hides the inner secp type behind a
|
||||
// crate-private accessor, so round-trip through hex to hand
|
||||
// Event::new the low-level key it wants.
|
||||
let bytes = hex::decode(sk.0.to_hex()).map_err(js_err)?;
|
||||
let secp_sk = secp256k1::SecretKey::from_slice(&bytes).map_err(js_err)?;
|
||||
Ok(Event(cev::Event::new(
|
||||
kind, content, tags, created_at, &secp_sk,
|
||||
)))
|
||||
}
|
||||
|
||||
/// Parse an event from its JSON representation.
|
||||
#[wasm_bindgen(js_name = fromJson)]
|
||||
pub fn from_json(s: &str) -> Result<Event, JsError> {
|
||||
serde_json::from_str::<cev::Event>(s)
|
||||
.map(Event)
|
||||
.map_err(js_err)
|
||||
}
|
||||
|
||||
/// Serialize the event to JSON.
|
||||
#[wasm_bindgen(js_name = toJson)]
|
||||
pub fn to_json(&self) -> Result<String, JsError> {
|
||||
serde_json::to_string(&self.0).map_err(js_err)
|
||||
}
|
||||
|
||||
#[wasm_bindgen(getter)]
|
||||
pub fn id(&self) -> String {
|
||||
self.0.id.clone()
|
||||
}
|
||||
|
||||
#[wasm_bindgen(getter)]
|
||||
pub fn pubkey(&self) -> String {
|
||||
self.0.pubkey.clone()
|
||||
}
|
||||
|
||||
#[wasm_bindgen(getter, js_name = createdAt)]
|
||||
pub fn created_at(&self) -> u64 {
|
||||
self.0.created_at
|
||||
}
|
||||
|
||||
#[wasm_bindgen(getter)]
|
||||
pub fn kind(&self) -> u16 {
|
||||
self.0.kind
|
||||
}
|
||||
|
||||
#[wasm_bindgen(getter)]
|
||||
pub fn content(&self) -> String {
|
||||
self.0.content.clone()
|
||||
}
|
||||
|
||||
#[wasm_bindgen(getter)]
|
||||
pub fn sig(&self) -> String {
|
||||
self.0.sig.clone()
|
||||
}
|
||||
|
||||
#[wasm_bindgen(getter)]
|
||||
pub fn tags(&self) -> Result<JsValue, JsError> {
|
||||
serde_wasm_bindgen::to_value(&self.0.tags).map_err(|e| JsError::new(&e.to_string()))
|
||||
}
|
||||
|
||||
/// Canonical `[0, pubkey, created_at, kind, tags, content]` serialization.
|
||||
#[wasm_bindgen]
|
||||
pub fn serialize(&self) -> String {
|
||||
self.0.serialize()
|
||||
}
|
||||
|
||||
#[wasm_bindgen(js_name = computeId)]
|
||||
pub fn compute_id(&self) -> String {
|
||||
self.0.compute_id()
|
||||
}
|
||||
|
||||
#[wasm_bindgen(js_name = idIsValid)]
|
||||
pub fn id_is_valid(&self) -> bool {
|
||||
self.0.id_is_valid()
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
pub fn verify(&self) -> bool {
|
||||
self.0.verify()
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user