1#![allow(
4 unreachable_pub,
5 reason = "UniFFI adapter records and functions stay inside this private core module"
6)]
7
8use bech32::{Bech32, Hrp};
9
10use crate::{NostrEventId, NostrEventKind, NostrPublicKey, SoftchatError};
11
12pub const MAX_NIP19_IDENTIFIER_CHARS: usize = 5_000;
16
17pub const MAX_NIP19_RELAYS: usize = 8;
19
20const MAX_RELAY_HINT_BYTES: usize = 255;
21const TLV_SPECIAL: u8 = 0;
22const TLV_RELAY: u8 = 1;
23const TLV_AUTHOR: u8 = 2;
24const TLV_KIND: u8 = 3;
25
26#[derive(Clone, Copy, Debug, Eq, PartialEq)]
28#[cfg_attr(feature = "native-bindings", derive(uniffi::Enum))]
29pub enum Nip19IdentifierKind {
30 PublicKey,
32 EventId,
34 Profile,
36 Event,
38}
39
40#[derive(Clone, Debug, Eq, PartialEq)]
42#[cfg_attr(feature = "native-bindings", derive(uniffi::Record))]
43pub struct Nip19Tlv {
44 pub type_id: u32,
46 pub value: Vec<u8>,
48}
49
50#[derive(Clone, Debug, Eq, PartialEq)]
52pub struct Nip19Identifier {
53 kind: Nip19IdentifierKind,
54 primary_hex: String,
55 relays: Vec<String>,
56 author_hex: Option<String>,
57 event_kind: Option<NostrEventKind>,
58 unknown_tlvs: Vec<Nip19Tlv>,
59}
60
61impl Nip19Identifier {
62 pub fn parse(encoded: &str) -> Result<Self, SoftchatError> {
72 validate_encoded_shape(encoded)?;
73 let (hrp, data) =
74 bech32::decode(encoded).map_err(|_| SoftchatError::InvalidNip19Identifier)?;
75 match hrp.as_str() {
76 "npub" => parse_bare(Nip19IdentifierKind::PublicKey, &data),
77 "note" => parse_bare(Nip19IdentifierKind::EventId, &data),
78 "nprofile" => parse_tlv(Nip19IdentifierKind::Profile, &data),
79 "nevent" => parse_tlv(Nip19IdentifierKind::Event, &data),
80 _ => Err(SoftchatError::InvalidNip19Identifier),
81 }
82 }
83
84 pub fn encode(&self) -> Result<String, SoftchatError> {
91 let (hrp, bytes) = match self.kind {
92 Nip19IdentifierKind::PublicKey => {
93 let key = NostrPublicKey::from_hex(&self.primary_hex)
94 .map_err(|_| SoftchatError::InvalidNip19Identifier)?;
95 ("npub", key.as_inner().to_bytes().to_vec())
96 }
97 Nip19IdentifierKind::EventId => {
98 let id = NostrEventId::from_hex(&self.primary_hex)
99 .map_err(|_| SoftchatError::InvalidNip19Identifier)?;
100 ("note", id.as_inner().as_bytes().to_vec())
101 }
102 Nip19IdentifierKind::Profile | Nip19IdentifierKind::Event => {
103 let bytes = self.encode_tlvs()?;
104 let hrp = if self.kind == Nip19IdentifierKind::Profile {
105 "nprofile"
106 } else {
107 "nevent"
108 };
109 (hrp, bytes)
110 }
111 };
112 let hrp = Hrp::parse(hrp).map_err(|_| SoftchatError::InvalidNip19Identifier)?;
113 let encoded = bech32::encode::<Bech32>(hrp, &bytes)
114 .map_err(|_| SoftchatError::InvalidNip19Identifier)?;
115 validate_encoded_shape(&encoded)?;
116 Ok(encoded)
117 }
118
119 #[must_use]
121 pub const fn kind(&self) -> Nip19IdentifierKind {
122 self.kind
123 }
124
125 #[must_use]
127 pub fn primary_hex(&self) -> &str {
128 &self.primary_hex
129 }
130
131 #[must_use]
133 pub fn relays(&self) -> &[String] {
134 &self.relays
135 }
136
137 #[must_use]
139 pub fn author_hex(&self) -> Option<&str> {
140 self.author_hex.as_deref()
141 }
142
143 #[must_use]
145 pub const fn event_kind(&self) -> Option<NostrEventKind> {
146 self.event_kind
147 }
148
149 #[must_use]
151 pub fn unknown_tlvs(&self) -> &[Nip19Tlv] {
152 &self.unknown_tlvs
153 }
154
155 fn encode_tlvs(&self) -> Result<Vec<u8>, SoftchatError> {
156 let mut output = Vec::new();
157 let special = match self.kind {
158 Nip19IdentifierKind::Profile => NostrPublicKey::from_hex(&self.primary_hex)
159 .map(|key| key.as_inner().to_bytes().to_vec()),
160 Nip19IdentifierKind::Event => NostrEventId::from_hex(&self.primary_hex)
161 .map(|id| id.as_inner().as_bytes().to_vec()),
162 Nip19IdentifierKind::PublicKey | Nip19IdentifierKind::EventId => {
163 return Err(SoftchatError::InvalidNip19Identifier);
164 }
165 }
166 .map_err(|_| SoftchatError::InvalidNip19Identifier)?;
167 push_tlv(&mut output, TLV_SPECIAL, &special)?;
168 validate_relays(&self.relays)?;
169 for relay in &self.relays {
170 push_tlv(&mut output, TLV_RELAY, relay.as_bytes())?;
171 }
172 if self.kind == Nip19IdentifierKind::Profile
173 && (self.author_hex.is_some() || self.event_kind.is_some())
174 {
175 return Err(SoftchatError::InvalidNip19Identifier);
176 }
177 if let Some(author) = &self.author_hex {
178 let author = NostrPublicKey::from_hex(author)
179 .map_err(|_| SoftchatError::InvalidNip19Identifier)?;
180 push_tlv(&mut output, TLV_AUTHOR, &author.as_inner().to_bytes())?;
181 }
182 if let Some(kind) = self.event_kind {
183 push_tlv(
184 &mut output,
185 TLV_KIND,
186 &u32::from(kind.as_u16()).to_be_bytes(),
187 )?;
188 }
189 for unknown in &self.unknown_tlvs {
190 let type_id =
191 u8::try_from(unknown.type_id).map_err(|_| SoftchatError::InvalidNip19Identifier)?;
192 if matches!(type_id, TLV_SPECIAL | TLV_RELAY | TLV_AUTHOR | TLV_KIND) {
193 return Err(SoftchatError::InvalidNip19Identifier);
194 }
195 push_tlv(&mut output, type_id, &unknown.value)?;
196 }
197 Ok(output)
198 }
199}
200
201#[derive(Clone, Debug, Eq, PartialEq)]
203#[cfg_attr(feature = "native-bindings", derive(uniffi::Record))]
204pub struct Nip19Value {
205 pub kind: Nip19IdentifierKind,
207 pub primary_hex: String,
209 pub relays: Vec<String>,
211 pub author_hex: Option<String>,
213 pub event_kind: Option<u32>,
215 pub unknown_tlvs: Vec<Nip19Tlv>,
217}
218
219impl From<&Nip19Identifier> for Nip19Value {
220 fn from(value: &Nip19Identifier) -> Self {
221 Self {
222 kind: value.kind,
223 primary_hex: value.primary_hex.clone(),
224 relays: value.relays.clone(),
225 author_hex: value.author_hex.clone(),
226 event_kind: value.event_kind.map(|kind| u32::from(kind.as_u16())),
227 unknown_tlvs: value.unknown_tlvs.clone(),
228 }
229 }
230}
231
232impl TryFrom<Nip19Value> for Nip19Identifier {
233 type Error = SoftchatError;
234
235 fn try_from(value: Nip19Value) -> Result<Self, Self::Error> {
236 let event_kind = value
237 .event_kind
238 .map(NostrEventKind::try_from_u32)
239 .transpose()
240 .map_err(|_| SoftchatError::InvalidNip19Identifier)?;
241 let value = Self {
242 kind: value.kind,
243 primary_hex: value.primary_hex,
244 relays: value.relays,
245 author_hex: value.author_hex,
246 event_kind,
247 unknown_tlvs: value.unknown_tlvs,
248 };
249 value.encode()?;
250 Ok(value)
251 }
252}
253
254#[cfg(any(
256 feature = "native-bindings",
257 all(feature = "javascript-bindings", target_arch = "wasm32"),
258 test
259))]
260#[cfg_attr(feature = "native-bindings", uniffi::export)]
261pub fn parse_nip19_identifier(encoded: String) -> Result<Nip19Value, SoftchatError> {
262 Nip19Identifier::parse(&encoded).map(|value| Nip19Value::from(&value))
263}
264
265#[cfg(any(
267 feature = "native-bindings",
268 all(feature = "javascript-bindings", target_arch = "wasm32"),
269 test
270))]
271#[cfg_attr(feature = "native-bindings", uniffi::export)]
272pub fn encode_nip19_identifier(value: Nip19Value) -> Result<String, SoftchatError> {
273 Nip19Identifier::try_from(value)?.encode()
274}
275
276fn validate_encoded_shape(encoded: &str) -> Result<(), SoftchatError> {
277 if encoded.is_empty() || encoded.len() > MAX_NIP19_IDENTIFIER_CHARS {
278 return Err(SoftchatError::InvalidNip19Identifier);
279 }
280 let has_lower = encoded.bytes().any(|byte| byte.is_ascii_lowercase());
281 let has_upper = encoded.bytes().any(|byte| byte.is_ascii_uppercase());
282 if has_lower && has_upper {
283 return Err(SoftchatError::InvalidNip19Identifier);
284 }
285 Ok(())
286}
287
288fn parse_bare(kind: Nip19IdentifierKind, data: &[u8]) -> Result<Nip19Identifier, SoftchatError> {
289 if data.len() != 32 {
290 return Err(SoftchatError::InvalidNip19Identifier);
291 }
292 let primary_hex = match kind {
293 Nip19IdentifierKind::PublicKey => {
294 NostrPublicKey::from_hex(&hex::encode(data)).map(|key| key.to_hex())
295 }
296 Nip19IdentifierKind::EventId => {
297 NostrEventId::from_hex(&hex::encode(data)).map(NostrEventId::to_hex)
298 }
299 Nip19IdentifierKind::Profile | Nip19IdentifierKind::Event => {
300 return Err(SoftchatError::InvalidNip19Identifier);
301 }
302 }
303 .map_err(|_| SoftchatError::InvalidNip19Identifier)?;
304 Ok(Nip19Identifier {
305 kind,
306 primary_hex,
307 relays: Vec::new(),
308 author_hex: None,
309 event_kind: None,
310 unknown_tlvs: Vec::new(),
311 })
312}
313
314fn parse_tlv(kind: Nip19IdentifierKind, data: &[u8]) -> Result<Nip19Identifier, SoftchatError> {
315 let mut cursor = data;
316 let mut primary_hex = None;
317 let mut relays = Vec::new();
318 let mut author_hex = None;
319 let mut event_kind = None;
320 let mut unknown_tlvs = Vec::new();
321 while !cursor.is_empty() {
322 let type_id = *cursor
323 .first()
324 .ok_or(SoftchatError::InvalidNip19Identifier)?;
325 let length = usize::from(*cursor.get(1).ok_or(SoftchatError::InvalidNip19Identifier)?);
326 let value = cursor
327 .get(2..2 + length)
328 .ok_or(SoftchatError::InvalidNip19Identifier)?;
329 cursor = cursor
330 .get(2 + length..)
331 .ok_or(SoftchatError::InvalidNip19Identifier)?;
332 match type_id {
333 TLV_SPECIAL => {
334 if primary_hex.is_some() || value.len() != 32 {
335 return Err(SoftchatError::InvalidNip19Identifier);
336 }
337 let hex = hex::encode(value);
338 primary_hex = Some(match kind {
339 Nip19IdentifierKind::Profile => NostrPublicKey::from_hex(&hex)
340 .map(|key| key.to_hex())
341 .map_err(|_| SoftchatError::InvalidNip19Identifier)?,
342 Nip19IdentifierKind::Event => NostrEventId::from_hex(&hex)
343 .map(NostrEventId::to_hex)
344 .map_err(|_| SoftchatError::InvalidNip19Identifier)?,
345 Nip19IdentifierKind::PublicKey | Nip19IdentifierKind::EventId => {
346 return Err(SoftchatError::InvalidNip19Identifier);
347 }
348 });
349 }
350 TLV_RELAY => {
351 let relay = std::str::from_utf8(value)
352 .map_err(|_| SoftchatError::InvalidNip19Identifier)?
353 .to_owned();
354 validate_relay(&relay)?;
355 relays.push(relay);
356 if relays.len() > MAX_NIP19_RELAYS {
357 return Err(SoftchatError::InvalidNip19Identifier);
358 }
359 }
360 TLV_AUTHOR if kind == Nip19IdentifierKind::Event => {
361 if author_hex.is_some() || value.len() != 32 {
362 return Err(SoftchatError::InvalidNip19Identifier);
363 }
364 author_hex = Some(
365 NostrPublicKey::from_hex(&hex::encode(value))
366 .map(|key| key.to_hex())
367 .map_err(|_| SoftchatError::InvalidNip19Identifier)?,
368 );
369 }
370 TLV_KIND if kind == Nip19IdentifierKind::Event => {
371 if event_kind.is_some() || value.len() != 4 {
372 return Err(SoftchatError::InvalidNip19Identifier);
373 }
374 let bytes: [u8; 4] = value
375 .try_into()
376 .map_err(|_| SoftchatError::InvalidNip19Identifier)?;
377 event_kind = Some(
378 NostrEventKind::try_from_u32(u32::from_be_bytes(bytes))
379 .map_err(|_| SoftchatError::InvalidNip19Identifier)?,
380 );
381 }
382 _ => unknown_tlvs.push(Nip19Tlv {
383 type_id: u32::from(type_id),
384 value: value.to_vec(),
385 }),
386 }
387 }
388 validate_relays(&relays)?;
389 Ok(Nip19Identifier {
390 kind,
391 primary_hex: primary_hex.ok_or(SoftchatError::InvalidNip19Identifier)?,
392 relays,
393 author_hex,
394 event_kind,
395 unknown_tlvs,
396 })
397}
398
399fn validate_relays(relays: &[String]) -> Result<(), SoftchatError> {
400 if relays.len() > MAX_NIP19_RELAYS {
401 return Err(SoftchatError::InvalidNip19Identifier);
402 }
403 for relay in relays {
404 validate_relay(relay)?;
405 }
406 Ok(())
407}
408
409fn validate_relay(relay: &str) -> Result<(), SoftchatError> {
410 if relay.is_empty()
411 || relay.len() > MAX_RELAY_HINT_BYTES
412 || (!relay.starts_with("ws://") && !relay.starts_with("wss://"))
413 || nostr::RelayUrl::parse(relay).is_err()
414 {
415 return Err(SoftchatError::InvalidNip19Identifier);
416 }
417 Ok(())
418}
419
420fn push_tlv(output: &mut Vec<u8>, type_id: u8, value: &[u8]) -> Result<(), SoftchatError> {
421 let length = u8::try_from(value.len()).map_err(|_| SoftchatError::InvalidNip19Identifier)?;
422 output.push(type_id);
423 output.push(length);
424 output.extend_from_slice(value);
425 Ok(())
426}
427
428#[cfg(test)]
429mod tests {
430 use serde::Deserialize;
431
432 use super::*;
433
434 #[derive(Deserialize)]
435 #[serde(rename_all = "camelCase")]
436 struct BareExample {
437 encoded: String,
438 public_key: String,
439 }
440
441 #[derive(Deserialize)]
442 #[serde(rename_all = "camelCase")]
443 struct ProfileExample {
444 encoded: String,
445 public_key: String,
446 relays: Vec<String>,
447 }
448
449 #[derive(Deserialize)]
450 struct Examples {
451 npub: BareExample,
452 nprofile: ProfileExample,
453 }
454
455 #[derive(Deserialize)]
456 struct Fixture {
457 examples: Examples,
458 }
459
460 fn official_fixture() -> Result<Fixture, serde_json::Error> {
461 serde_json::from_str(include_str!(
462 "../../../fixtures/nostr/nip19/nip19-official.json"
463 ))
464 }
465
466 #[test]
467 fn parses_official_public_key_and_profile_examples() -> Result<(), SoftchatError> {
468 let fixture = official_fixture().map_err(|_| SoftchatError::InvalidNip19Identifier)?;
469 let public_key = Nip19Identifier::parse(&fixture.examples.npub.encoded)?;
470 assert_eq!(public_key.kind(), Nip19IdentifierKind::PublicKey);
471 assert_eq!(public_key.primary_hex(), fixture.examples.npub.public_key);
472 assert_eq!(public_key.encode()?, fixture.examples.npub.encoded);
473
474 let profile = Nip19Identifier::parse(&fixture.examples.nprofile.encoded)?;
475 assert_eq!(profile.kind(), Nip19IdentifierKind::Profile);
476 assert_eq!(profile.primary_hex(), fixture.examples.nprofile.public_key);
477 assert_eq!(profile.relays(), fixture.examples.nprofile.relays);
478 assert_eq!(profile.encode()?, fixture.examples.nprofile.encoded);
479 Ok(())
480 }
481
482 #[test]
483 fn round_trips_nevent_unknown_tlv_and_rejects_mixed_case() -> Result<(), SoftchatError> {
484 let value = Nip19Value {
485 kind: Nip19IdentifierKind::Event,
486 primary_hex: "44900586091b284416a0c001f677f9c49f7639a55c3f1e2ec130a8e1a7998e1b"
487 .to_owned(),
488 relays: vec!["wss://relay.example".to_owned()],
489 author_hex: Some(
490 "918e2da906df4ccd12c8ac672d8335add131a4cf9d27ce42b3bb3625755f0788".to_owned(),
491 ),
492 event_kind: Some(14),
493 unknown_tlvs: vec![Nip19Tlv {
494 type_id: 99,
495 value: vec![0, 1, 2],
496 }],
497 };
498 let encoded = encode_nip19_identifier(value.clone())?;
499 assert_eq!(parse_nip19_identifier(encoded.clone())?, value);
500
501 let mut mixed = encoded;
502 mixed.replace_range(0..1, "N");
503 assert!(matches!(
504 Nip19Identifier::parse(&mixed),
505 Err(SoftchatError::InvalidNip19Identifier)
506 ));
507 Ok(())
508 }
509}