Add coracle-wasm bindings

This commit is contained in:
Jon Staab
2026-04-16 16:50:18 -07:00
parent fe05d540d4
commit e42d7efd6e
5 changed files with 379 additions and 0 deletions
+16
View File
@@ -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"
+265
View File
@@ -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()
}
}