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
+1
View File
@@ -1,2 +1,3 @@
.claude .claude
CLAUDE.md CLAUDE.md
target
Generated
+96
View File
@@ -45,6 +45,12 @@ dependencies = [
"generic-array", "generic-array",
] ]
[[package]]
name = "bumpalo"
version = "3.20.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb"
[[package]] [[package]]
name = "cc" name = "cc"
version = "1.2.59" version = "1.2.59"
@@ -158,6 +164,18 @@ dependencies = [
"pulldown-cmark", "pulldown-cmark",
] ]
[[package]]
name = "coracle-wasm"
version = "0.1.0"
dependencies = [
"coracle-lib",
"hex",
"secp256k1",
"serde-wasm-bindgen",
"serde_json",
"wasm-bindgen",
]
[[package]] [[package]]
name = "cpufeatures" name = "cpufeatures"
version = "0.2.17" version = "0.2.17"
@@ -264,6 +282,16 @@ version = "1.0.18"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"
[[package]]
name = "js-sys"
version = "0.3.95"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2964e92d1d9dc3364cae4d718d93f227e3abb088e747d92e0395bfdedf1c12ca"
dependencies = [
"once_cell",
"wasm-bindgen",
]
[[package]] [[package]]
name = "libc" name = "libc"
version = "0.2.184" version = "0.2.184"
@@ -276,6 +304,12 @@ version = "2.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79"
[[package]]
name = "once_cell"
version = "1.21.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
[[package]] [[package]]
name = "opaque-debug" name = "opaque-debug"
version = "0.3.1" version = "0.3.1"
@@ -390,6 +424,12 @@ dependencies = [
"getrandom", "getrandom",
] ]
[[package]]
name = "rustversion"
version = "1.0.22"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d"
[[package]] [[package]]
name = "salsa20" name = "salsa20"
version = "0.10.2" version = "0.10.2"
@@ -441,6 +481,17 @@ dependencies = [
"serde_derive", "serde_derive",
] ]
[[package]]
name = "serde-wasm-bindgen"
version = "0.6.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8302e169f0eddcc139c70f139d19d6467353af16f9fce27e8c30158036a1e16b"
dependencies = [
"js-sys",
"serde",
"wasm-bindgen",
]
[[package]] [[package]]
name = "serde_core" name = "serde_core"
version = "1.0.228" version = "1.0.228"
@@ -578,6 +629,51 @@ version = "0.11.1+wasi-snapshot-preview1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b"
[[package]]
name = "wasm-bindgen"
version = "0.2.118"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0bf938a0bacb0469e83c1e148908bd7d5a6010354cf4fb73279b7447422e3a89"
dependencies = [
"cfg-if",
"once_cell",
"rustversion",
"wasm-bindgen-macro",
"wasm-bindgen-shared",
]
[[package]]
name = "wasm-bindgen-macro"
version = "0.2.118"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eeff24f84126c0ec2db7a449f0c2ec963c6a49efe0698c4242929da037ca28ed"
dependencies = [
"quote",
"wasm-bindgen-macro-support",
]
[[package]]
name = "wasm-bindgen-macro-support"
version = "0.2.118"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9d08065faf983b2b80a79fd87d8254c409281cf7de75fc4b773019824196c904"
dependencies = [
"bumpalo",
"proc-macro2",
"quote",
"syn",
"wasm-bindgen-shared",
]
[[package]]
name = "wasm-bindgen-shared"
version = "0.2.118"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5fd04d9e306f1907bd13c6361b5c6bfc7b3b3c095ed3f8a9246390f8dbdee129"
dependencies = [
"unicode-ident",
]
[[package]] [[package]]
name = "zerocopy" name = "zerocopy"
version = "0.8.48" version = "0.8.48"
+1
View File
@@ -8,4 +8,5 @@ members = [
"coracle-domain", "coracle-domain",
"coracle-content", "coracle-content",
"coracle-storage", "coracle-storage",
"coracle-wasm",
] ]
+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()
}
}