Skip to content

Commit 391d38a

Browse files
committed
Pip: publish accepted build
Pip-Source-Commit: f109f3452f6e339b2c4dc0ca76ca35a27ad693aa Pip-Source-Tree: a336ec2
1 parent c1289ee commit 391d38a

15 files changed

Lines changed: 1035 additions & 217 deletions

File tree

crates/marmot-app/src/ids.rs

Lines changed: 152 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
use cgka_traits::MemberId;
2+
use nostr::FromBech32;
23
use nostr::ToBech32;
34
use nostr::nips::nip19::Nip19Profile;
45
use nostr::prelude::RelayUrl;
@@ -78,12 +79,22 @@ pub fn nprofile_for_account_id(
7879
.map_err(|_| AppError::InvalidPublicKey)
7980
}
8081

81-
/// Normalize any public-key reference (npub bech32 or hex) into canonical
82-
/// hex account id. Public so embedders can resolve scanned/typed npubs.
82+
/// Normalize a public identity reference into a canonical hex account id.
83+
///
84+
/// Accepts hex, `npub`, one lowercase `nostr:npub` prefix, bare `nprofile`,
85+
/// and one lowercase `nostr:nprofile` prefix. Existing hex/npub/NIP-21
86+
/// behavior stays on [`PublicKey::parse`]; nprofile is a fallback that
87+
/// returns only `profile.public_key` hex and discards every relay hint.
88+
/// This helper does not trim whitespace. Failures map to
89+
/// [`AppError::InvalidPublicKey`] without echoing the input.
8390
pub fn account_id_hex_from_ref(reference: &str) -> Result<String, AppError> {
84-
Ok(PublicKey::parse(reference)
85-
.map_err(|_| AppError::InvalidPublicKey)?
86-
.to_hex())
91+
if let Ok(pubkey) = PublicKey::parse(reference) {
92+
return Ok(pubkey.to_hex());
93+
}
94+
let profile_ref = reference.strip_prefix("nostr:").unwrap_or(reference);
95+
Nip19Profile::from_bech32(profile_ref)
96+
.map(|profile| profile.public_key.to_hex())
97+
.map_err(|_| AppError::InvalidPublicKey)
8798
}
8899

89100
pub(crate) fn npub_for_account_id_lossy(account_id_hex: &str) -> String {
@@ -92,17 +103,54 @@ pub(crate) fn npub_for_account_id_lossy(account_id_hex: &str) -> String {
92103

93104
#[cfg(test)]
94105
mod tests {
95-
use super::{nprofile_for_account_id, npub_for_account_id};
106+
use super::{
107+
account_id_hex_from_ref, nprofile_for_account_id, npub_for_account_id, parse_account_id_hex,
108+
};
109+
use crate::AppError;
110+
use nostr::nips::nip01::Coordinate;
111+
use nostr::nips::nip19::{Nip19Coordinate, Nip19Event};
112+
use nostr::{EventId, Kind, SecretKey, ToBech32};
113+
use nostr_sdk::prelude::PublicKey;
96114

97115
const ACCOUNT_ID: &str = "aa4fc8665f5696e33db7e1a572e3b0f5b3d615837b0f362dcb1c8068b098c7b4";
116+
const NPUB: &str = "npub14f8usejl26twx0dhuxjh9cas7keav9vr0v8nvtwtrjqx3vycc76qqh9nsy";
117+
const BOOTSTRAP_NPROFILE: &str = "nprofile1qqs25n7gve04d9hr8km7rftjuwc0tv7kzkphkrek9h93eqrgkzvv0dqpremhxue69uhhyetvv9uju\
118+
et49emks6t5v4hx76tnv5hxx6rpwsq3uamnwvaz7tmjv4kxz7fww4ejuamgd96x2mn0d9ek2tnrdpshggcu28s";
119+
const NO_RELAY_NPROFILE: &str =
120+
"nprofile1qqs25n7gve04d9hr8km7rftjuwc0tv7kzkphkrek9h93eqrgkzvv0dq7r0nz9";
121+
const UNKNOWN_TLV_NPROFILE: &str =
122+
"nprofile1qqs25n7gve04d9hr8km7rftjuwc0tv7kzkphkrek9h93eqrgkzvv0drrq3skycmyxne9kf";
123+
const INVALID_RELAY_NPROFILE: &str =
124+
"nprofile1qqs25n7gve04d9hr8km7rftjuwc0tv7kzkphkrek9h93eqrgkzvv0dqpp9hx7apqvys82unvuca0cf";
125+
const DUPLICATE_TYPE0_NPROFILE: &str = "nprofile1qqs25n7gve04d9hr8km7rftjuwc0tv7kzkphkrek9h93eqrgkzvv0dqqyzamhwamhwamhwamhwamhwamhwamhwamhwamhwamhwamhwamhwamkq4uk7z";
126+
const MISSING_TYPE0_NPROFILE: &str = "nprofile1qy2hwumn8ghj7etcv9khqmr99e5kuanpd35kghsdudn";
127+
const SHORT_TYPE0_NPROFILE: &str = "nprofile1qqg25n7gve04d9hr8km7rftjuwc028ngcqe";
128+
const TRUNCATED_HEADER_NPROFILE: &str = "nprofile1qqqsnhxh";
129+
const TRUNCATED_VALUE_NPROFILE: &str = "nprofile1qqs25n7gve04d9hrdfl42d";
130+
const WRONG_HRP_NOTE: &str =
131+
"note1qqs25n7gve04d9hr8km7rftjuwc0tv7kzkphkrek9h93eqrgkzvv0dq4ueyyt";
132+
133+
fn assert_account_id(reference: &str) {
134+
assert_eq!(
135+
account_id_hex_from_ref(reference).expect("valid identity reference"),
136+
ACCOUNT_ID
137+
);
138+
}
139+
140+
fn assert_invalid(reference: &str) {
141+
assert!(
142+
matches!(
143+
account_id_hex_from_ref(reference),
144+
Err(AppError::InvalidPublicKey)
145+
),
146+
"expected InvalidPublicKey"
147+
);
148+
}
98149

99150
#[test]
100151
fn npub_and_nprofile_match_bootstrap_vectors() {
101152
let npub = npub_for_account_id(ACCOUNT_ID).unwrap();
102-
assert_eq!(
103-
npub,
104-
"npub14f8usejl26twx0dhuxjh9cas7keav9vr0v8nvtwtrjqx3vycc76qqh9nsy"
105-
);
153+
assert_eq!(npub, NPUB);
106154

107155
let nprofile = nprofile_for_account_id(
108156
ACCOUNT_ID,
@@ -112,10 +160,100 @@ mod tests {
112160
],
113161
)
114162
.unwrap();
115-
assert_eq!(
116-
nprofile,
117-
"nprofile1qqs25n7gve04d9hr8km7rftjuwc0tv7kzkphkrek9h93eqrgkzvv0dqpremhxue69uhhyetvv9uju\
118-
et49emks6t5v4hx76tnv5hxx6rpwsq3uamnwvaz7tmjv4kxz7fww4ejuamgd96x2mn0d9ek2tnrdpshggcu28s"
163+
assert_eq!(nprofile, BOOTSTRAP_NPROFILE);
164+
}
165+
166+
#[test]
167+
fn account_id_hex_from_ref_accepts_hex_npub_and_nprofile_spellings() {
168+
let no_relay = nprofile_for_account_id(ACCOUNT_ID, &[]).unwrap();
169+
assert_eq!(no_relay, NO_RELAY_NPROFILE);
170+
let other_relays =
171+
nprofile_for_account_id(ACCOUNT_ID, &["wss://relay.example.invalid".to_owned()])
172+
.unwrap();
173+
assert_ne!(other_relays, BOOTSTRAP_NPROFILE);
174+
175+
for reference in [
176+
ACCOUNT_ID,
177+
&ACCOUNT_ID.to_ascii_uppercase(),
178+
NPUB,
179+
&format!("nostr:{NPUB}"),
180+
BOOTSTRAP_NPROFILE,
181+
&format!("nostr:{BOOTSTRAP_NPROFILE}"),
182+
NO_RELAY_NPROFILE,
183+
&format!("nostr:{NO_RELAY_NPROFILE}"),
184+
&other_relays,
185+
UNKNOWN_TLV_NPROFILE,
186+
INVALID_RELAY_NPROFILE,
187+
DUPLICATE_TYPE0_NPROFILE,
188+
] {
189+
assert_account_id(reference);
190+
}
191+
}
192+
193+
#[test]
194+
fn account_id_hex_from_ref_keeps_untrimmed_whitespace_behavior() {
195+
assert_invalid(&format!(" {ACCOUNT_ID}"));
196+
assert_invalid(&format!("{NPUB} "));
197+
assert_invalid(&format!(" {BOOTSTRAP_NPROFILE}"));
198+
}
199+
200+
#[test]
201+
fn account_id_hex_from_ref_rejects_checksum_damage_and_wrong_hrp() {
202+
let mut damaged_npub = NPUB.to_owned();
203+
damaged_npub.replace_range(damaged_npub.len() - 1.., "x");
204+
assert_ne!(damaged_npub, NPUB);
205+
assert_invalid(&damaged_npub);
206+
207+
let mut damaged_nprofile = NO_RELAY_NPROFILE.to_owned();
208+
damaged_nprofile.replace_range(damaged_nprofile.len() - 1.., "x");
209+
assert_ne!(damaged_nprofile, NO_RELAY_NPROFILE);
210+
assert_invalid(&damaged_nprofile);
211+
212+
assert_invalid(WRONG_HRP_NOTE);
213+
assert_invalid(
214+
"NOSTR:nprofile1qqs25n7gve04d9hr8km7rftjuwc0tv7kzkphkrek9h93eqrgkzvv0dq7r0nz9",
119215
);
216+
assert_invalid(&format!("nostr:nostr:{NO_RELAY_NPROFILE}"));
217+
}
218+
219+
#[test]
220+
fn account_id_hex_from_ref_rejects_event_and_secret_hrps() {
221+
let public_key = PublicKey::parse(ACCOUNT_ID).unwrap();
222+
let event_id = EventId::from_slice(&[0x22; 32]).unwrap();
223+
let note = event_id.to_bech32().unwrap();
224+
let nevent = Nip19Event::new(event_id)
225+
.author(public_key)
226+
.to_bech32()
227+
.unwrap();
228+
let naddr = Nip19Coordinate::new(Coordinate::new(Kind::Metadata, public_key), [])
229+
.to_bech32()
230+
.unwrap();
231+
let nsec = SecretKey::from_slice(&[0x11; 32])
232+
.unwrap()
233+
.to_bech32()
234+
.unwrap();
235+
236+
assert_invalid(&note);
237+
assert_invalid(&nevent);
238+
assert_invalid(&naddr);
239+
assert_invalid(&nsec);
240+
}
241+
242+
#[test]
243+
fn account_id_hex_from_ref_rejects_truncated_and_missing_type0() {
244+
assert_invalid(MISSING_TYPE0_NPROFILE);
245+
assert_invalid(SHORT_TYPE0_NPROFILE);
246+
assert_invalid(TRUNCATED_HEADER_NPROFILE);
247+
assert_invalid(TRUNCATED_VALUE_NPROFILE);
248+
}
249+
250+
#[test]
251+
fn parse_account_id_hex_does_not_gain_nprofile() {
252+
assert_eq!(parse_account_id_hex(ACCOUNT_ID).unwrap(), ACCOUNT_ID);
253+
assert_eq!(parse_account_id_hex(NPUB).unwrap(), ACCOUNT_ID);
254+
assert!(matches!(
255+
parse_account_id_hex(NO_RELAY_NPROFILE),
256+
Err(AppError::InvalidPublicKey)
257+
));
120258
}
121259
}

crates/marmot-app/src/lib.rs

Lines changed: 2 additions & 159 deletions
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,6 @@ use rand::RngCore;
5959
use rand::rngs::OsRng;
6060
use rusqlite::Connection;
6161
use serde::{Deserialize, Serialize};
62-
use sha2::{Digest, Sha256};
6362
use storage_sqlite::{
6463
SecurePruneAppEventsResult, SqliteAccountStorage, SqliteSharedStorage, StoredAppMessageQuery,
6564
TimelineProjectionUpdate,
@@ -93,6 +92,7 @@ mod key_package_records;
9392
#[cfg(test)]
9493
mod local_open_test_gate;
9594
mod media;
95+
mod profile_pseudonyms;
9696
#[cfg(feature = "media-benchmarks")]
9797
#[doc(hidden)]
9898
pub use media::MediaDownloadBenchmarkTransport;
@@ -213,6 +213,7 @@ pub use notifications::{
213213
build_notification_gift_wrap, build_notification_rumor_content, encrypted_push_token,
214214
parse_provider_token, push_token_fingerprint,
215215
};
216+
pub use profile_pseudonyms::{default_profile_pseudonym, random_profile_pseudonym};
216217
pub use relay_plane::{
217218
EngineReorgMetrics, MarmotRelayPlane, MarmotRelayPlaneAccountAdapter,
218219
RelayEndpointClassification, RelayEndpointPolicy, RelayPlaneHealth, RelayRollupEntry,
@@ -423,152 +424,6 @@ const DIRECTORY_FUTURE_CREATED_AT_CLEANUP_MARKER: &str =
423424
pub(crate) const MAX_SEEN_EVENT_IDS: usize = 16_384;
424425
const KIND_NOSTR_METADATA: u64 = 0;
425426
const KIND_NOSTR_CONTACT_LIST: u64 = 3;
426-
const DEFAULT_PROFILE_ADJECTIVES: &[&str] = &[
427-
"Agile", "Amber", "Angry", "Balanced", "Bold", "Brave", "Breezy", "Bright", "Brisk", "Bubbly",
428-
"Calm", "Caring", "Cheerful", "Clear", "Clever", "Coral", "Cosmic", "Cozy", "Crimson", "Crisp",
429-
"Curious", "Daring", "Dawn", "Deep", "Diamond", "Dreamy", "Eager", "Earnest", "Easy",
430-
"Electric", "Emerald", "Festive", "Fiery", "Fleet", "Forest", "Fresh", "Frosty", "Gentle",
431-
"Glad", "Golden", "Graceful", "Grand", "Grateful", "Green", "Happy", "Hardy", "Hearty",
432-
"Hidden", "Honest", "Hopeful", "Humble", "Indigo", "Ivory", "Jade", "Jolly", "Kind", "Lively",
433-
"Loyal", "Lucky", "Majestic", "Maple", "Mellow", "Merry", "Mighty", "Mindful", "Misty",
434-
"Modest", "Mossy", "Neat", "Nifty", "Nimble", "Noble", "Olive", "Open", "Patient", "Peaceful",
435-
"Plum", "Polar", "Proud", "Quiet", "Radiant", "Rapid", "Ready", "Restful", "Rosy", "Ruby",
436-
"Rustic", "Sage", "Scarlet", "Serene", "Sharp", "Shiny", "Silver", "Sincere", "Sky", "Smooth",
437-
"Solar", "Solid", "Spirited", "Spry", "Steady", "Stellar", "Stormy", "Sturdy", "Sunlit",
438-
"Sunny", "Swift", "Tame", "Tangy", "Tender", "Tidy", "Topaz", "Tranquil", "Trusty", "Twilight",
439-
"Upbeat", "Valiant", "Verdant", "Vivid", "Warm", "Willing", "Winsome", "Wise", "Witty",
440-
"Wondrous", "Woodland", "Young", "Zesty",
441-
];
442-
const DEFAULT_PROFILE_NOUNS: &[&str] = &[
443-
"Albatross",
444-
"Alpaca",
445-
"Ant",
446-
"Antelope",
447-
"Armadillo",
448-
"Badger",
449-
"Bat",
450-
"Bear",
451-
"Beaver",
452-
"Bee",
453-
"Bison",
454-
"Bluebird",
455-
"Bobcat",
456-
"Bullfrog",
457-
"Bumblebee",
458-
"Butterfly",
459-
"Camel",
460-
"Caribou",
461-
"Cat",
462-
"Caterpillar",
463-
"Cheetah",
464-
"Chickadee",
465-
"Chinchilla",
466-
"Chipmunk",
467-
"Cobra",
468-
"Condor",
469-
"Cougar",
470-
"Crab",
471-
"Crane",
472-
"Cricket",
473-
"Crow",
474-
"Deer",
475-
"Dingo",
476-
"Dolphin",
477-
"Dove",
478-
"Dragonfly",
479-
"Duck",
480-
"Eagle",
481-
"Egret",
482-
"Elephant",
483-
"Elk",
484-
"Falcon",
485-
"Fawn",
486-
"Ferret",
487-
"Finch",
488-
"Firefly",
489-
"Flamingo",
490-
"Flounder",
491-
"Fox",
492-
"Gazelle",
493-
"Gecko",
494-
"Giraffe",
495-
"Goat",
496-
"Goose",
497-
"Gopher",
498-
"Grouse",
499-
"Hare",
500-
"Hawk",
501-
"Hedgehog",
502-
"Heron",
503-
"Hippo",
504-
"Hornet",
505-
"Horse",
506-
"Hummingbird",
507-
"Ibex",
508-
"Iguana",
509-
"Jackal",
510-
"Jaguar",
511-
"Jay",
512-
"Kestrel",
513-
"Kingfisher",
514-
"Kiwi",
515-
"Koala",
516-
"Ladybug",
517-
"Lark",
518-
"Leopard",
519-
"Lion",
520-
"Llama",
521-
"Lynx",
522-
"Macaw",
523-
"Magpie",
524-
"Mallard",
525-
"Manatee",
526-
"Marmot",
527-
"Meerkat",
528-
"Mink",
529-
"Mole",
530-
"Mongoose",
531-
"Monkey",
532-
"Moose",
533-
"Mouse",
534-
"Narwhal",
535-
"Newt",
536-
"Nightingale",
537-
"Octopus",
538-
"Opossum",
539-
"Orca",
540-
"Oriole",
541-
"Ostrich",
542-
"Otter",
543-
"Owl",
544-
"Panda",
545-
"Parrot",
546-
"Peacock",
547-
"Pelican",
548-
"Penguin",
549-
"Pheasant",
550-
"Pigeon",
551-
"Pony",
552-
"Porcupine",
553-
"Puffin",
554-
"Quail",
555-
"Rabbit",
556-
"Raccoon",
557-
"Ram",
558-
"Raven",
559-
"Reindeer",
560-
"Rhino",
561-
"Roadrunner",
562-
"Robin",
563-
"Salamander",
564-
"Salmon",
565-
"Seal",
566-
"Swan",
567-
"Tiger",
568-
"Turtle",
569-
"Wolf",
570-
"Yak",
571-
];
572427

573428
type AppRuntime = AccountDeviceRuntime<
574429
MarmotRelayPlaneAccountAdapter,
@@ -6425,18 +6280,6 @@ fn empty_key_package_lifecycle(stable_slot_id: String) -> cgka_traits::KeyPackag
64256280
cgka_traits::KeyPackageLifecycleState::slot_only(stable_slot_id)
64266281
}
64276282

6428-
fn default_profile_pseudonym(account_id_hex: &str) -> String {
6429-
let digest = Sha256::digest(account_id_hex.as_bytes());
6430-
let adjective_index =
6431-
u16::from_be_bytes([digest[0], digest[1]]) as usize % DEFAULT_PROFILE_ADJECTIVES.len();
6432-
let noun_index =
6433-
u16::from_be_bytes([digest[2], digest[3]]) as usize % DEFAULT_PROFILE_NOUNS.len();
6434-
format!(
6435-
"{} {}",
6436-
DEFAULT_PROFILE_ADJECTIVES[adjective_index], DEFAULT_PROFILE_NOUNS[noun_index]
6437-
)
6438-
}
6439-
64406283
fn unix_now_seconds() -> u64 {
64416284
SystemTime::now()
64426285
.duration_since(UNIX_EPOCH)

0 commit comments

Comments
 (0)