lib

Core libraries for Radroots
git clone https://radroots.dev/git/lib.git
Log | Files | Refs | README | LICENSE

commit 8c01bc55dc0471535284a1ab7d13c4cb5b44ddb8
parent d021c4a305969ef3b9b95ce32e4afa376bdafa7d
Author: triesap <tyson@radroots.org>
Date:   Sun,  4 Jan 2026 22:11:52 +0000

events: add account claim kind and username helpers


- Add KIND_ACCOUNT_CLAIM constant and wire it into kinds exports
- Introduce RadrootsAccountClaim event types with ts-rs bindings
- Add identity username validation/normalization with exported constants
- Extend TS bindings to include account claim types and radrootsd profile type

Diffstat:
MCargo.lock | 1+
Mevents/bindings/ts/src/kinds.ts | 1+
Mevents/bindings/ts/src/types.ts | 8+++++++-
Aevents/src/account.rs | 40++++++++++++++++++++++++++++++++++++++++
Mevents/src/kinds.rs | 2++
Mevents/src/lib.rs | 1+
Midentity/Cargo.toml | 3+++
Aidentity/bindings/ts/src/constants.ts | 3+++
Aidentity/build.rs | 3+++
Midentity/src/lib.rs | 5+++++
Aidentity/src/username.rs | 111+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
11 files changed, 177 insertions(+), 1 deletion(-)

diff --git a/Cargo.lock b/Cargo.lock @@ -1769,6 +1769,7 @@ dependencies = [ "tempfile", "thiserror 1.0.69", "tracing", + "ts-rs", ] [[package]] diff --git a/events/bindings/ts/src/kinds.ts b/events/bindings/ts/src/kinds.ts @@ -44,6 +44,7 @@ export const KIND_COOP = 30360; export const KIND_DOCUMENT = 30361; export const KIND_RESOURCE_AREA = 30370; export const KIND_RESOURCE_HARVEST_CAP = 30371; +export const KIND_ACCOUNT_CLAIM = 30380; export const KIND_APP_DATA = 30078; export const KIND_LISTING = 30402; export const KIND_APPLICATION_HANDLER = 31990; diff --git a/events/bindings/ts/src/types.ts b/events/bindings/ts/src/types.ts @@ -8,6 +8,12 @@ export type JobInputType = "url" | "event" | "job" | "text"; export type JobPaymentRequest = { amount_sat: number, bolt11?: string | null, }; +export type RadrootsAccountClaim = { username: string, pubkey: string, nip05?: string | null, }; + +export type RadrootsAccountClaimEventIndex = { event: RadrootsNostrEvent, metadata: RadrootsAccountClaimEventMetadata, }; + +export type RadrootsAccountClaimEventMetadata = { id: string, author: string, published_at: number, kind: number, account: RadrootsAccountClaim, }; + export type RadrootsAppData = { d_tag: string, content: string, }; export type RadrootsAppDataEventIndex = { event: RadrootsNostrEvent, metadata: RadrootsAppDataEventMetadata, }; @@ -176,7 +182,7 @@ export type RadrootsProfileEventIndex = { event: RadrootsNostrEvent, metadata: R export type RadrootsProfileEventMetadata = { id: string, author: string, published_at: number, kind: number, profile_type?: RadrootsProfileType | null, profile: RadrootsProfile, }; -export type RadrootsProfileType = "individual" | "farm" | "coop" | "any"; +export type RadrootsProfileType = "individual" | "farm" | "coop" | "any" | "radrootsd"; export type RadrootsReaction = { root: RadrootsNostrEventRef, content: string, }; diff --git a/events/src/account.rs b/events/src/account.rs @@ -0,0 +1,40 @@ +use crate::{kinds::KIND_ACCOUNT_CLAIM as KIND_ACCOUNT_CLAIM_EVENT, RadrootsNostrEvent}; +#[cfg(feature = "ts-rs")] +use ts_rs::TS; + +#[cfg(not(feature = "std"))] +use alloc::string::String; + +pub const KIND_ACCOUNT_CLAIM: u32 = KIND_ACCOUNT_CLAIM_EVENT; + +#[cfg_attr(feature = "ts-rs", derive(TS))] +#[cfg_attr(feature = "ts-rs", ts(export, export_to = "types.ts"))] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[derive(Clone, Debug)] +pub struct RadrootsAccountClaimEventIndex { + pub event: RadrootsNostrEvent, + pub metadata: RadrootsAccountClaimEventMetadata, +} + +#[cfg_attr(feature = "ts-rs", derive(TS))] +#[cfg_attr(feature = "ts-rs", ts(export, export_to = "types.ts"))] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[derive(Clone, Debug)] +pub struct RadrootsAccountClaimEventMetadata { + pub id: String, + pub author: String, + pub published_at: u32, + pub kind: u32, + pub account: RadrootsAccountClaim, +} + +#[cfg_attr(feature = "ts-rs", derive(TS))] +#[cfg_attr(feature = "ts-rs", ts(export, export_to = "types.ts"))] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[derive(Clone, Debug)] +pub struct RadrootsAccountClaim { + pub username: String, + pub pubkey: String, + #[cfg_attr(feature = "ts-rs", ts(optional, type = "string | null"))] + pub nip05: Option<String>, +} diff --git a/events/src/kinds.rs b/events/src/kinds.rs @@ -44,6 +44,7 @@ pub const KIND_COOP: u32 = 30360; pub const KIND_DOCUMENT: u32 = 30361; pub const KIND_RESOURCE_AREA: u32 = 30370; pub const KIND_RESOURCE_HARVEST_CAP: u32 = 30371; +pub const KIND_ACCOUNT_CLAIM: u32 = 30380; pub const KIND_APP_DATA: u32 = 30078; pub const KIND_LISTING: u32 = 30402; pub const KIND_APPLICATION_HANDLER: u32 = 31990; @@ -175,6 +176,7 @@ mod kinds_constants_tests { ("KIND_DOCUMENT", KIND_DOCUMENT), ("KIND_RESOURCE_AREA", KIND_RESOURCE_AREA), ("KIND_RESOURCE_HARVEST_CAP", KIND_RESOURCE_HARVEST_CAP), + ("KIND_ACCOUNT_CLAIM", KIND_ACCOUNT_CLAIM), ("KIND_APP_DATA", KIND_APP_DATA), ("KIND_LISTING", KIND_LISTING), ("KIND_APPLICATION_HANDLER", KIND_APPLICATION_HANDLER), diff --git a/events/src/lib.rs b/events/src/lib.rs @@ -10,6 +10,7 @@ use ts_rs::TS; use alloc::{string::String, vec::Vec}; pub mod comment; +pub mod account; pub mod follow; pub mod gift_wrap; pub mod job; diff --git a/identity/Cargo.toml b/identity/Cargo.toml @@ -5,10 +5,12 @@ edition.workspace = true authors = ["Radroots Authors"] rust-version.workspace = true license.workspace = true +build = "build.rs" [features] default = ["std"] std = ["dep:radroots-runtime"] +ts-rs = ["dep:ts-rs"] [dependencies] radroots-runtime = { workspace = true, optional = true } @@ -18,6 +20,7 @@ serde = { workspace = true } serde_json = { workspace = true } thiserror = { workspace = true } tracing = { workspace = true } +ts-rs = { workspace = true, optional = true } [dev-dependencies] tempfile = { workspace = true } diff --git a/identity/bindings/ts/src/constants.ts b/identity/bindings/ts/src/constants.ts @@ -0,0 +1,3 @@ +export const RADROOTS_USERNAME_MIN_LEN = 3; +export const RADROOTS_USERNAME_MAX_LEN = 30; +export const RADROOTS_USERNAME_REGEX = "^(?!.*\.\.)(?!\.)(?!.*\.$)[a-z0-9._-]{3,30}$"; diff --git a/identity/build.rs b/identity/build.rs @@ -0,0 +1,3 @@ +fn main() { + println!("cargo:rustc-env=TS_RS_EXPORT_DIR=./bindings/ts/src"); +} diff --git a/identity/src/lib.rs b/identity/src/lib.rs @@ -6,9 +6,14 @@ extern crate alloc; pub mod error; pub mod identity; +pub mod username; pub use error::IdentityError; pub use identity::{ RadrootsIdentity, RadrootsIdentityFile, RadrootsIdentityProfile, RadrootsIdentitySecretKeyFormat, DEFAULT_IDENTITY_PATH, }; +pub use username::{ + radroots_username_is_valid, radroots_username_normalize, RADROOTS_USERNAME_MAX_LEN, + RADROOTS_USERNAME_MIN_LEN, RADROOTS_USERNAME_REGEX, +}; diff --git a/identity/src/username.rs b/identity/src/username.rs @@ -0,0 +1,111 @@ +#![forbid(unsafe_code)] + +#[cfg(not(feature = "std"))] +use alloc::string::String; + +pub const RADROOTS_USERNAME_MIN_LEN: usize = 3; +pub const RADROOTS_USERNAME_MAX_LEN: usize = 30; +pub const RADROOTS_USERNAME_REGEX: &str = + r"^(?!.*\.\.)(?!\.)(?!.*\.$)[a-z0-9._-]{3,30}$"; + +pub fn radroots_username_is_valid(username: &str) -> bool { + if !username.is_ascii() { + return false; + } + let len = username.len(); + if len < RADROOTS_USERNAME_MIN_LEN || len > RADROOTS_USERNAME_MAX_LEN { + return false; + } + let bytes = username.as_bytes(); + if bytes.first() == Some(&b'.') || bytes.last() == Some(&b'.') { + return false; + } + let mut prev_dot = false; + for &byte in bytes { + if byte == b'.' { + if prev_dot { + return false; + } + prev_dot = true; + continue; + } + prev_dot = false; + let is_alpha = byte.is_ascii_lowercase(); + let is_digit = byte.is_ascii_digit(); + let is_allowed = is_alpha || is_digit || byte == b'_' || byte == b'-'; + if !is_allowed { + return false; + } + } + true +} + +pub fn radroots_username_normalize(input: &str) -> Option<String> { + let trimmed = input.trim(); + if trimmed.is_empty() { + return None; + } + let normalized = trimmed.to_ascii_lowercase(); + if radroots_username_is_valid(&normalized) { + Some(normalized) + } else { + None + } +} + +#[cfg(all(test, feature = "std"))] +mod tests { + use super::*; + + #[test] + fn valid_usernames() { + for name in ["radroots", "radroots_1", "radroots.test", "rr-01"] { + assert!(radroots_username_is_valid(name)); + } + } + + #[test] + fn invalid_usernames() { + for name in [ + "ra", + ".radroots", + "radroots.", + "radroots..test", + "radroots!", + "RADROOTS", + ] { + assert!(!radroots_username_is_valid(name)); + } + } + + #[test] + fn normalize_usernames() { + assert_eq!( + radroots_username_normalize(" RadRoots "), + Some("radroots".to_string()) + ); + assert_eq!(radroots_username_normalize("ra"), None); + } +} + +#[cfg(all(test, feature = "ts-rs", feature = "std"))] +mod constants_tests { + use super::*; + use std::{env, fs, path::Path}; + + #[test] + fn export_username_constants() { + let out_dir = env::var("TS_RS_EXPORT_DIR").unwrap_or_else(|_| "./bindings".to_string()); + let path = Path::new(&out_dir).join("constants.ts"); + if let Some(parent) = path.parent() { + fs::create_dir_all(parent).expect("create ts export dir"); + } + let content = format!( + "export const RADROOTS_USERNAME_MIN_LEN = {min_len};\nexport const RADROOTS_USERNAME_MAX_LEN = {max_len};\nexport const RADROOTS_USERNAME_REGEX = \"{regex}\";\n", + min_len = RADROOTS_USERNAME_MIN_LEN, + max_len = RADROOTS_USERNAME_MAX_LEN, + regex = RADROOTS_USERNAME_REGEX + ); + fs::write(&path, content).expect("write constants"); + } +}