1use peerbadge_protocol as protocol;
2use peerbadge_schemas as schemas;
3use serde::{de::DeserializeOwned, Serialize};
4use wasm_bindgen::prelude::*;
5
6#[wasm_bindgen(typescript_custom_section)]
7const TYPESCRIPT_SURFACE: &'static str = r#"
8/** Any JSON value accepted by credential info and blind-message fields. */
9export type JsonValue =
10 | null
11 | boolean
12 | number
13 | string
14 | JsonValue[]
15 | { readonly [key: string]: JsonValue };
16
17/** Holder-created request sent to an issuer during credential issuance. */
18export interface IssuanceRequest {
19 readonly version: 1;
20 /** Unpadded URL-safe base64 encoded PBRSA blinded message. */
21 readonly blinded_message: string;
22}
23
24/** Issuer-created response containing the blind signature for an issuance request. */
25export interface IssuanceResponse {
26 readonly version: 1;
27 /** Nostr public key identifying the issuer that signed the response. */
28 readonly issuer_id: string;
29 /** Issuer-visible credential information bound into the blind signature. */
30 readonly info: JsonValue;
31 /** Unpadded URL-safe base64 encoded PBRSA blind signature. */
32 readonly blind_signature: string;
33}
34
35/** Exported issuer secret material for application-managed backup or restore. */
36export interface IssuerSecretKeys {
37 /** Nostr issuer identity secret key encoded as hex. */
38 readonly issuer_id_secret_key: string;
39 /** Unpadded URL-safe base64 encoded PBRSA issuance secret key. */
40 readonly issuance_secret_key: string;
41}
42
43/** Final credential presented by a holder to a verifier. */
44export interface SignedCredential {
45 readonly version: 1;
46 readonly credential: Credential;
47 readonly proof: CredentialProof;
48}
49
50/** Holder-signed authorization allowing an auxiliary subject key to use credentials. */
51export interface HolderAuthorization {
52 readonly version: 1;
53 readonly authorization: HolderAuthorizationStatement;
54 readonly proof: SchnorrSignatureProof;
55}
56
57/** Input for `HolderContext.authorizeCredentialUse`. */
58export interface HolderAuthorizationRequest {
59 /** External application's Nostr subject public key. */
60 readonly subject_pubkey: string;
61}
62
63/** Holder statement signed by `HolderContext.authorizeCredentialUse` and returned in `HolderAuthorization`. */
64export interface HolderAuthorizationStatement {
65 /** Nostr public key identifying the holder. */
66 readonly holder_id_pubkey: string;
67 /** External application's Nostr subject public key. */
68 readonly subject_pubkey: string;
69 /** Credential digest this authorization allows the subject to present. */
70 readonly credential_digest: CredentialDigest;
71 /** Unix timestamp in seconds. */
72 readonly issued_at: Timestamp;
73}
74
75/** Unpadded URL-safe base64 encoded canonical credential digest. */
76export type CredentialDigest = string;
77
78/** Unix timestamp in seconds. */
79export type Timestamp = number;
80
81/** Credential payload signed by the issuer's issuance key. */
82export interface Credential {
83 /** Nostr public key identifying the issuer. */
84 readonly issuer_id_pubkey: string;
85 /** Credential information that was visible to the issuer during issuance. */
86 readonly info: JsonValue;
87 /** Holder-controlled value hidden during issuance and disclosed in the final credential. */
88 readonly blind_msg: JsonValue;
89}
90
91/** Proof over a finalized credential payload. */
92export interface CredentialProof {
93 /** Unpadded URL-safe base64 encoded PBRSA credential signature. */
94 readonly signature: string;
95}
96
97/** Signed issuer metadata that verifiers trust before accepting credentials. */
98export interface IssuerAuthority {
99 readonly version: 1;
100 readonly issuer: Issuer;
101 readonly proof: SchnorrSignatureProof;
102}
103
104/** Schnorr signature proof encoded for JSON transport. */
105export interface SchnorrSignatureProof {
106 readonly signature: string;
107}
108
109/** Public issuer metadata bound into an issuer authority. */
110export interface Issuer {
111 /** Nostr public key identifying the issuer. */
112 readonly issuer_id_pubkey: string;
113 /** Unpadded URL-safe base64 encoded PBRSA issuance public key. */
114 readonly issuance_key: string;
115 /** Locations where applications may fetch this issuer's revocations. */
116 readonly revocation: readonly RevocationLocation[];
117}
118
119/** Issuer-signed credential revocation object. */
120export interface SignedRevocation {
121 readonly version: 1;
122 readonly revocation: Revocation;
123 readonly proof: RevocationProof;
124}
125
126/** Issuer identity proof for a signed revocation. */
127export interface RevocationProof {
128 /** Nostr public key identifying the issuer that signed the revocation. */
129 readonly issuer_id_pubkey: string;
130 /** Schnorr signature over the revocation payload. */
131 readonly signature: string;
132}
133
134/** Revocation payload signed by an issuer identity key. */
135export interface Revocation {
136 /** Unpadded URL-safe base64 encoded SHA-256 digest. */
137 readonly credential_digest: CredentialDigest;
138}
139
140/** Application-owned location where issuer revocations may be published. */
141export interface RevocationLocation {
142 /** Transport or publication protocol name, such as "nostr". */
143 readonly protocol: string;
144 /** Protocol-specific location, such as a relay URL. */
145 readonly location: string;
146}
147
148/** Result of creating a holder issuance request. */
149export interface PendingIssuanceResult {
150 /** Request to send to the issuer. */
151 readonly request: IssuanceRequest;
152 /** Local holder state required to finalize the issuer response. */
153 readonly pending: PendingIssuance;
154}
155"#;
156
157fn from_js<T: DeserializeOwned>(value: JsValue) -> Result<T, JsError> {
158 serde_wasm_bindgen::from_value(value).map_err(|error| JsError::new(&error.to_string()))
159}
160
161fn to_js<T: Serialize>(value: &T) -> Result<JsValue, JsError> {
162 value
163 .serialize(&serde_wasm_bindgen::Serializer::json_compatible())
164 .map_err(|error| JsError::new(&error.to_string()))
165}
166
167fn current_unix_timestamp() -> Result<u64, JsError> {
168 let seconds = (js_sys::Date::now() / 1000.0).floor();
169 if !seconds.is_finite() || seconds < 0.0 {
170 return Err(JsError::new("current time must be non-negative"));
171 }
172
173 Ok(seconds as u64)
174}
175
176fn reflect_error(error: JsValue) -> JsError {
177 JsError::new(
178 &error
179 .as_string()
180 .unwrap_or_else(|| "failed to set JS object property".to_owned()),
181 )
182}
183
184#[wasm_bindgen(js_name = initTracing)]
189pub fn init_tracing() -> bool {
190 tracing_wasm::try_set_as_global_default().is_ok()
191}
192
193#[wasm_bindgen]
194#[derive(Clone)]
195pub struct IssuerContext {
197 inner: protocol::IssuerContext,
198}
199
200#[wasm_bindgen]
201impl IssuerContext {
202 #[wasm_bindgen(js_name = generate)]
204 pub fn generate() -> Result<IssuerContext, JsError> {
205 Ok(Self {
206 inner: protocol::IssuerContext::generate()?,
207 })
208 }
209
210 #[wasm_bindgen(js_name = generateWithThreadRng)]
212 pub fn generate_with_thread_rng() -> Result<IssuerContext, JsError> {
213 Ok(Self {
214 inner: protocol::IssuerContext::generate_with_thread_rng()?,
215 })
216 }
217
218 #[wasm_bindgen(js_name = generateWithSystemRng)]
220 pub fn generate_with_system_rng() -> Result<IssuerContext, JsError> {
221 Ok(Self {
222 inner: protocol::IssuerContext::generate_with_system_rng()?,
223 })
224 }
225
226 #[wasm_bindgen(js_name = importSecretKey)]
228 pub fn import_secret_key(
229 #[wasm_bindgen(unchecked_param_type = "IssuerSecretKeys")] secret_key: JsValue,
230 ) -> Result<IssuerContext, JsError> {
231 let secret_key: protocol::IssuerSecretKeys = from_js(secret_key)?;
232 Ok(Self {
233 inner: protocol::IssuerContext::import_secret_key(&secret_key)?,
234 })
235 }
236
237 #[wasm_bindgen(js_name = exportSecretKey, unchecked_return_type = "IssuerSecretKeys")]
239 pub fn export_secret_key(&self) -> Result<JsValue, JsError> {
240 to_js(&self.inner.export_secret_key()?)
241 }
242
243 #[wasm_bindgen(js_name = issueCredential, unchecked_return_type = "IssuanceResponse")]
245 pub fn issue_credential(
246 &self,
247 #[wasm_bindgen(unchecked_param_type = "JsonValue")] info: JsValue,
248 #[wasm_bindgen(unchecked_param_type = "IssuanceRequest")] request: JsValue,
249 ) -> Result<JsValue, JsError> {
250 let info: serde_json::Value = from_js(info)?;
251 let request: protocol::IssuanceRequest = from_js(request)?;
252 to_js(&self.inner.issue_credential(info, &request)?)
253 }
254
255 #[wasm_bindgen(js_name = issuerAuthority, unchecked_return_type = "IssuerAuthority")]
257 pub fn issuer_authority(
258 &self,
259 #[wasm_bindgen(unchecked_param_type = "readonly RevocationLocation[]")] revocation: JsValue,
260 ) -> Result<JsValue, JsError> {
261 let revocation: Vec<protocol::RevocationLocation> = from_js(revocation)?;
262 to_js(&self.inner.issuer_authority(revocation)?)
263 }
264
265 #[wasm_bindgen(js_name = revokeCredential, unchecked_return_type = "SignedRevocation")]
267 pub fn revoke_credential(
268 &self,
269 #[wasm_bindgen(unchecked_param_type = "SignedCredential")] credential: JsValue,
270 ) -> Result<JsValue, JsError> {
271 let credential: protocol::SignedCredential = from_js(credential)?;
272 to_js(&self.inner.revoke_credential(&credential)?)
273 }
274}
275
276#[wasm_bindgen]
277#[derive(Clone)]
278pub struct HolderContext {
280 inner: protocol::HolderContext,
281}
282
283#[wasm_bindgen]
284impl HolderContext {
285 #[wasm_bindgen(js_name = generate)]
287 pub fn generate() -> HolderContext {
288 Self {
289 inner: protocol::HolderContext::generate(),
290 }
291 }
292
293 #[wasm_bindgen(js_name = importSecretKey)]
295 pub fn import_secret_key(secret_key: String) -> Result<HolderContext, JsError> {
296 Ok(Self {
297 inner: protocol::HolderContext::import_secret_key(&secret_key)?,
298 })
299 }
300
301 #[wasm_bindgen(js_name = exportSecretKey)]
303 pub fn export_secret_key(&self) -> String {
304 self.inner.export_secret_key()
305 }
306
307 #[wasm_bindgen(getter, js_name = publicKey)]
309 pub fn public_key(&self) -> String {
310 self.inner.public_key().to_string()
311 }
312
313 #[wasm_bindgen(js_name = authorizeCredentialUse, unchecked_return_type = "HolderAuthorization")]
315 pub fn authorize_credential_use(
316 &self,
317 #[wasm_bindgen(unchecked_param_type = "HolderAuthorizationRequest")] request: JsValue,
318 #[wasm_bindgen(unchecked_param_type = "SignedCredential")] credential: JsValue,
319 ) -> Result<JsValue, JsError> {
320 let request: protocol::HolderAuthorizationRequest = from_js(request)?;
321 let credential: protocol::SignedCredential = from_js(credential)?;
322 to_js(&self.inner.authorize_credential_use_at_time(
323 request,
324 &credential,
325 current_unix_timestamp()?,
326 )?)
327 }
328}
329
330#[wasm_bindgen]
331pub struct PendingIssuance {
333 inner: protocol::PendingIssuance,
334}
335
336#[wasm_bindgen]
337impl PendingIssuance {
338 #[wasm_bindgen(js_name = createRequest, unchecked_return_type = "PendingIssuanceResult")]
340 pub fn create_request(
341 #[wasm_bindgen(unchecked_param_type = "IssuerAuthority")] issuer_authority: JsValue,
342 #[wasm_bindgen(unchecked_param_type = "JsonValue")] info: JsValue,
343 #[wasm_bindgen(unchecked_param_type = "JsonValue")] blind_msg: JsValue,
344 ) -> Result<JsValue, JsError> {
345 let issuer_authority: protocol::IssuerAuthority = from_js(issuer_authority)?;
346 let info: serde_json::Value = from_js(info)?;
347 let blind_msg: serde_json::Value = from_js(blind_msg)?;
348 let (request, pending) = protocol::PendingIssuance::create_request(
349 &issuer_authority.issuer.issuance_key,
350 issuer_authority.issuer.issuer_id_pubkey,
351 info,
352 blind_msg,
353 )?;
354
355 let result = js_sys::Object::new();
356 js_sys::Reflect::set(&result, &JsValue::from_str("request"), &to_js(&request)?)
357 .map_err(reflect_error)?;
358 js_sys::Reflect::set(
359 &result,
360 &JsValue::from_str("pending"),
361 &JsValue::from(PendingIssuance { inner: pending }),
362 )
363 .map_err(reflect_error)?;
364 Ok(result.into())
365 }
366
367 #[wasm_bindgen(js_name = exportState)]
369 pub fn export_state(&self) -> Result<String, JsError> {
370 Ok(self.inner.export_state()?)
371 }
372
373 #[wasm_bindgen(js_name = importState)]
375 pub fn import_state(state: String) -> Result<PendingIssuance, JsError> {
376 Ok(Self {
377 inner: protocol::PendingIssuance::import_state(&state)?,
378 })
379 }
380
381 #[wasm_bindgen(js_name = finalize, unchecked_return_type = "SignedCredential")]
383 pub fn finalize(
384 self,
385 #[wasm_bindgen(unchecked_param_type = "IssuerAuthority")] issuer_authority: JsValue,
386 #[wasm_bindgen(unchecked_param_type = "IssuanceResponse")] response: JsValue,
387 ) -> Result<JsValue, JsError> {
388 let issuer_authority: protocol::IssuerAuthority = from_js(issuer_authority)?;
389 let response: protocol::IssuanceResponse = from_js(response)?;
390 if issuer_authority.issuer.issuer_id_pubkey != response.issuer_id {
391 return Err(protocol::CredentialsError::IssuerIdMismatch.into());
392 }
393 to_js(
394 &self
395 .inner
396 .finalize(&issuer_authority.issuer.issuance_key, &response)?,
397 )
398 }
399}
400
401#[wasm_bindgen]
402pub struct VerificationContext {
404 inner: protocol::VerificationContext,
405}
406
407#[wasm_bindgen]
408impl VerificationContext {
409 #[wasm_bindgen(constructor)]
411 pub fn new() -> VerificationContext {
412 Self {
413 inner: protocol::VerificationContext::new(),
414 }
415 }
416
417 #[wasm_bindgen(js_name = addIssuerAuthority)]
419 pub fn add_issuer_authority(
420 &mut self,
421 #[wasm_bindgen(unchecked_param_type = "IssuerAuthority")] issuer_authority: JsValue,
422 ) -> Result<(), JsError> {
423 let issuer_authority: protocol::IssuerAuthority = from_js(issuer_authority)?;
424 Ok(self.inner.add_issuer_authority(&issuer_authority)?)
425 }
426
427 #[wasm_bindgen(js_name = addRevocation)]
429 pub fn add_revocation(
430 &mut self,
431 #[wasm_bindgen(unchecked_param_type = "SignedRevocation")] revocation: JsValue,
432 ) -> Result<(), JsError> {
433 let revocation: protocol::SignedRevocation = from_js(revocation)?;
434 Ok(self.inner.add_revocation(&revocation)?)
435 }
436
437 #[wasm_bindgen(js_name = verifyCredential)]
439 pub fn verify_credential(
440 &self,
441 #[wasm_bindgen(unchecked_param_type = "SignedCredential")] credential: JsValue,
442 ) -> Result<bool, JsError> {
443 let credential: protocol::SignedCredential = from_js(credential)?;
444 self.inner.verify_credential(&credential)?;
445 Ok(true)
446 }
447
448 #[wasm_bindgen(js_name = verifyCredentialAuthorization)]
450 pub fn verify_credential_authorization(
451 &self,
452 #[wasm_bindgen(unchecked_param_type = "SignedCredential")] credential: JsValue,
453 #[wasm_bindgen(unchecked_param_type = "HolderAuthorization")] authorization: JsValue,
454 ) -> Result<bool, JsError> {
455 let credential: protocol::SignedCredential = from_js(credential)?;
456 let authorization: protocol::HolderAuthorization = from_js(authorization)?;
457
458 self.inner.verify_credential_authorization_at_time(
459 &credential,
460 &authorization,
461 current_unix_timestamp()?,
462 )?;
463 Ok(true)
464 }
465}
466
467#[wasm_bindgen(typescript_custom_section)]
468const TYPESCRIPT_SCHEMAS_SURFACE: &'static str = r#"
469/** Parsed payload of a `fedi-trust-score-v1.0` badge. */
470export interface TrustScoreBadgeV1 {
471 /** Public result of attester-private scoring; acceptability is caller policy. */
472 readonly trust_level: number;
473 /** Holder Nostr public key bound by the revealed `blind_msg`, canonical lowercase hex. */
474 readonly holder_pubkey: string;
475}
476"#;
477
478#[wasm_bindgen(js_name = trustScoreSchemaV1)]
480pub fn trust_score_schema_v1() -> String {
481 schemas::TRUST_SCORE_SCHEMA_V1.to_owned()
482}
483
484#[wasm_bindgen(js_name = trustScoreInfoV1, unchecked_return_type = "JsonValue")]
489pub fn trust_score_info_v1(trust_level: f64) -> Result<JsValue, JsError> {
490 if !trust_level.is_finite() || trust_level.fract() != 0.0 || trust_level < 0.0 {
491 return Err(JsError::new("trust_level must be a non-negative integer"));
492 }
493 to_js(&schemas::trust_score_info_v1(trust_level as u64)?)
494}
495
496#[wasm_bindgen(js_name = trustScoreBlindMsgV1, unchecked_return_type = "JsonValue")]
498pub fn trust_score_blind_msg_v1(holder_pubkey: String) -> Result<JsValue, JsError> {
499 let holder: protocol::HolderId = holder_pubkey
500 .parse()
501 .map_err(|_| JsError::new("holder_pubkey is not a valid Nostr public key"))?;
502 to_js(&schemas::trust_score_blind_msg_v1(&holder.0))
503}
504
505#[wasm_bindgen(js_name = parseTrustScoreBadgeV1, unchecked_return_type = "TrustScoreBadgeV1")]
510pub fn parse_trust_score_badge_v1(
511 #[wasm_bindgen(unchecked_param_type = "SignedCredential")] credential: JsValue,
512) -> Result<JsValue, JsError> {
513 let credential: protocol::SignedCredential = from_js(credential)?;
514 let badge = schemas::parse_trust_score_badge_v1(&credential.credential)?;
515 to_js(&serde_json::json!({
516 "trust_level": badge.trust_level,
517 "holder_pubkey": badge.holder_pubkey.to_string(),
518 }))
519}