1use std::{
4 collections::{BTreeMap, HashMap},
5 fmt,
6};
7
8use serde::{de, Deserialize, Serialize};
9
10use crate::{
11 error::map_error, pubky_routing, validation::invalid_data, PaykitAppId, PaykitError,
12 PaymentEndpointIdentifier, PublicKey, Result,
13};
14
15const APP_REGISTRY_KIND: &str = "paykit.app_registry";
16const APP_REGISTRY_VERSION: u8 = 1;
17const APP_DISPLAY_NAME_MAX_LEN: usize = 128;
18pub const INITIAL_PAYKIT_KEY_GENERATION: u64 = 1;
20pub const PAYKIT_APP_REGISTRY_MAX_BYTES: usize = 64 * 1024;
22pub const PAYKIT_APP_REGISTRY_MAX_APPS: usize = 64;
24pub const PAYKIT_APP_REGISTRY_MAX_ENDPOINT_DEFAULTS: usize = 256;
26
27#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
29#[serde(deny_unknown_fields)]
30pub struct PaykitAppCapabilities {
31 pub private_payments: bool,
33 pub payment_requests: bool,
35 pub receipts: bool,
37 pub outgoing_payments: bool,
42}
43
44#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
46#[serde(try_from = "PaykitAppWire", into = "PaykitAppWire")]
47pub struct PaykitApp {
48 display_name: String,
49 capabilities: PaykitAppCapabilities,
50}
51
52impl PaykitApp {
53 pub fn new(
55 display_name: impl Into<String>,
56 capabilities: PaykitAppCapabilities,
57 ) -> Result<Self> {
58 let display_name = display_name.into();
59 validate_display_name(&display_name)?;
60 Ok(Self {
61 display_name,
62 capabilities,
63 })
64 }
65
66 pub fn display_name(&self) -> &str {
68 &self.display_name
69 }
70
71 pub fn capabilities(&self) -> PaykitAppCapabilities {
73 self.capabilities
74 }
75}
76
77#[derive(Clone, PartialEq, Eq)]
79pub struct PaykitAppRegistry {
80 key_generation: u64,
81 noise_public_key: Option<PublicKey>,
82 apps: HashMap<PaykitAppId, PaykitApp>,
83 default_app_id: Option<PaykitAppId>,
84 default_apps_by_endpoint: HashMap<PaymentEndpointIdentifier, PaykitAppId>,
85}
86
87impl fmt::Debug for PaykitAppRegistry {
88 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
89 f.debug_struct("PaykitAppRegistry")
90 .field("key_generation", &self.key_generation)
91 .field("noise_public_key", &self.noise_public_key)
92 .field("apps", &self.apps)
93 .field("default_app_id", &self.default_app_id)
94 .field("default_apps_by_endpoint", &self.default_apps_by_endpoint)
95 .finish()
96 }
97}
98
99impl PaykitAppRegistry {
100 pub fn new(noise_public_key: Option<PublicKey>) -> Self {
105 Self {
106 key_generation: INITIAL_PAYKIT_KEY_GENERATION,
107 noise_public_key,
108 apps: HashMap::new(),
109 default_app_id: None,
110 default_apps_by_endpoint: HashMap::new(),
111 }
112 }
113
114 pub fn key_generation(&self) -> u64 {
116 self.key_generation
117 }
118
119 pub fn noise_public_key(&self) -> Option<&PublicKey> {
122 self.noise_public_key.as_ref()
123 }
124
125 pub fn set_noise_public_key(
127 &mut self,
128 noise_public_key: PublicKey,
129 key_generation: u64,
130 ) -> Result<()> {
131 if key_generation == 0 {
132 return Err(PaykitError::Validation(
133 "Paykit key generation must be positive".into(),
134 ));
135 }
136 if self.noise_public_key.as_ref().is_some_and(|existing| {
137 existing != &noise_public_key || self.key_generation != key_generation
138 }) {
139 return Err(PaykitError::Validation(
140 "Paykit App Registry Noise public key is already initialized to a different key"
141 .into(),
142 ));
143 }
144 self.noise_public_key = Some(noise_public_key);
145 self.key_generation = key_generation;
146 Ok(())
147 }
148
149 pub fn rotate_noise_public_key(
151 &mut self,
152 noise_public_key: PublicKey,
153 key_generation: u64,
154 ) -> Result<()> {
155 let expected_generation = self.key_generation.checked_add(1).ok_or_else(|| {
156 PaykitError::Validation("Paykit App Registry key generation is exhausted".into())
157 })?;
158 if key_generation != expected_generation {
159 return Err(PaykitError::Validation(format!(
160 "Paykit App Registry key generation must advance from {} to {expected_generation}",
161 self.key_generation
162 )));
163 }
164 self.noise_public_key = Some(noise_public_key);
165 self.key_generation = key_generation;
166 Ok(())
167 }
168
169 pub fn apps(&self) -> &HashMap<PaykitAppId, PaykitApp> {
171 &self.apps
172 }
173
174 pub fn default_app_id(&self) -> Option<&PaykitAppId> {
176 self.default_app_id.as_ref()
177 }
178
179 pub fn default_apps_by_endpoint(&self) -> &HashMap<PaymentEndpointIdentifier, PaykitAppId> {
181 &self.default_apps_by_endpoint
182 }
183
184 pub fn preferred_app_for_endpoint(
189 &self,
190 identifier: &PaymentEndpointIdentifier,
191 ) -> Option<&PaykitAppId> {
192 self.default_apps_by_endpoint
193 .get(identifier)
194 .or(self.default_app_id.as_ref())
195 }
196
197 pub fn register_app(&mut self, app_id: PaykitAppId, app: PaykitApp) -> Result<()> {
199 if self.noise_public_key.is_none() && app_uses_private_protocol(app.capabilities()) {
200 return Err(PaykitError::Validation(
201 "Paykit App Registry must initialize its Noise public key before registering private capabilities"
202 .into(),
203 ));
204 }
205 if !self.apps.contains_key(&app_id) && self.apps.len() >= PAYKIT_APP_REGISTRY_MAX_APPS {
206 return Err(PaykitError::Validation(format!(
207 "Paykit App Registry must not contain more than {PAYKIT_APP_REGISTRY_MAX_APPS} applications"
208 )));
209 }
210 self.apps.insert(app_id, app);
211 Ok(())
212 }
213
214 pub fn remove_app(&mut self, app_id: &PaykitAppId) -> Option<PaykitApp> {
216 let removed = self.apps.remove(app_id)?;
217 if self.default_app_id.as_ref() == Some(app_id) {
218 self.default_app_id = None;
219 }
220 self.default_apps_by_endpoint
221 .retain(|_, default_app_id| default_app_id != app_id);
222 Some(removed)
223 }
224
225 pub fn set_default_app(&mut self, app_id: Option<PaykitAppId>) -> Result<()> {
227 if let Some(app_id) = &app_id {
228 self.require_registered_app(app_id)?;
229 }
230 self.default_app_id = app_id;
231 Ok(())
232 }
233
234 pub fn set_default_app_for_endpoint(
236 &mut self,
237 identifier: PaymentEndpointIdentifier,
238 app_id: PaykitAppId,
239 ) -> Result<()> {
240 self.require_registered_app(&app_id)?;
241 if !self.default_apps_by_endpoint.contains_key(&identifier)
242 && self.default_apps_by_endpoint.len() >= PAYKIT_APP_REGISTRY_MAX_ENDPOINT_DEFAULTS
243 {
244 return Err(PaykitError::Validation(format!(
245 "Paykit App Registry must not contain more than {PAYKIT_APP_REGISTRY_MAX_ENDPOINT_DEFAULTS} endpoint defaults"
246 )));
247 }
248 self.default_apps_by_endpoint.insert(identifier, app_id);
249 Ok(())
250 }
251
252 pub fn clear_default_app_for_endpoint(
254 &mut self,
255 identifier: &PaymentEndpointIdentifier,
256 ) -> Option<PaykitAppId> {
257 self.default_apps_by_endpoint.remove(identifier)
258 }
259
260 fn require_registered_app(&self, app_id: &PaykitAppId) -> Result<()> {
261 if self.apps.contains_key(app_id) {
262 return Ok(());
263 }
264 Err(PaykitError::Validation(format!(
265 "Paykit App ID '{app_id}' is not registered"
266 )))
267 }
268}
269
270#[derive(Clone, Serialize, Deserialize)]
271#[serde(deny_unknown_fields)]
272struct PaykitAppWire {
273 display_name: String,
274 capabilities: PaykitAppCapabilities,
275}
276
277impl TryFrom<PaykitAppWire> for PaykitApp {
278 type Error = PaykitError;
279
280 fn try_from(wire: PaykitAppWire) -> Result<Self> {
281 Self::new(wire.display_name, wire.capabilities)
282 }
283}
284
285impl From<PaykitApp> for PaykitAppWire {
286 fn from(app: PaykitApp) -> Self {
287 Self {
288 display_name: app.display_name,
289 capabilities: app.capabilities,
290 }
291 }
292}
293
294#[derive(Deserialize)]
295#[serde(deny_unknown_fields)]
296struct AppRegistryWire {
297 version: u8,
298 kind: String,
299 key_generation: u64,
300 noise_public_key: Option<String>,
301 #[serde(deserialize_with = "deserialize_unique_map")]
302 apps: HashMap<String, PaykitAppWire>,
303 default_app_id: Option<String>,
304 #[serde(deserialize_with = "deserialize_unique_map")]
305 default_apps_by_endpoint: HashMap<String, String>,
306}
307
308#[derive(Serialize)]
309struct AppRegistryWireRef<'a> {
310 version: u8,
311 kind: &'static str,
312 key_generation: u64,
313 noise_public_key: Option<String>,
314 apps: BTreeMap<&'a str, &'a PaykitApp>,
315 default_app_id: Option<&'a str>,
316 default_apps_by_endpoint: BTreeMap<&'a str, &'a str>,
317}
318
319pub fn serialize_paykit_app_registry(registry: &PaykitAppRegistry) -> Result<String> {
321 validate_registry_limits(registry).map_err(PaykitError::Validation)?;
322 let apps = registry
323 .apps
324 .iter()
325 .map(|(app_id, app)| (app_id.as_str(), app))
326 .collect();
327 let default_apps_by_endpoint = registry
328 .default_apps_by_endpoint
329 .iter()
330 .map(|(identifier, app_id)| (identifier.as_str(), app_id.as_str()))
331 .collect();
332 let raw_json = serde_json::to_string(&AppRegistryWireRef {
333 version: APP_REGISTRY_VERSION,
334 kind: APP_REGISTRY_KIND,
335 key_generation: registry.key_generation,
336 noise_public_key: registry.noise_public_key.as_ref().map(PublicKey::z32),
337 apps,
338 default_app_id: registry.default_app_id.as_ref().map(PaykitAppId::as_str),
339 default_apps_by_endpoint,
340 })
341 .map_err(|err| {
342 PaykitError::Validation(format!("failed to serialize Paykit App Registry: {err}"))
343 })?;
344 if raw_json.len() > PAYKIT_APP_REGISTRY_MAX_BYTES {
345 return Err(PaykitError::Validation(format!(
346 "Paykit App Registry must not exceed {PAYKIT_APP_REGISTRY_MAX_BYTES} bytes"
347 )));
348 }
349 Ok(raw_json)
350}
351
352pub fn parse_paykit_app_registry_json(raw_json: &str) -> Result<PaykitAppRegistry> {
354 if raw_json.len() > PAYKIT_APP_REGISTRY_MAX_BYTES {
360 return Err(invalid_data(
361 format!("Paykit App Registry exceeds {PAYKIT_APP_REGISTRY_MAX_BYTES} bytes"),
362 None,
363 ));
364 }
365 let wire = serde_json::from_str::<AppRegistryWire>(raw_json)
366 .map_err(|_| invalid_data("Paykit App Registry JSON is invalid", None))?;
367 if wire.version != APP_REGISTRY_VERSION || wire.kind != APP_REGISTRY_KIND {
368 return Err(invalid_data(
369 "unsupported Paykit App Registry version/kind",
370 None,
371 ));
372 }
373 if wire.key_generation == 0 {
374 return Err(invalid_data(
375 "Paykit App Registry key generation must be greater than zero",
376 None,
377 ));
378 }
379 if wire.apps.len() > PAYKIT_APP_REGISTRY_MAX_APPS {
380 return Err(invalid_data(
381 format!(
382 "Paykit App Registry contains more than {PAYKIT_APP_REGISTRY_MAX_APPS} applications"
383 ),
384 None,
385 ));
386 }
387 if wire.default_apps_by_endpoint.len() > PAYKIT_APP_REGISTRY_MAX_ENDPOINT_DEFAULTS {
388 return Err(invalid_data(
389 format!(
390 "Paykit App Registry contains more than {PAYKIT_APP_REGISTRY_MAX_ENDPOINT_DEFAULTS} endpoint defaults"
391 ),
392 None,
393 ));
394 }
395
396 let noise_public_key = wire
397 .noise_public_key
398 .map(|value| {
399 PublicKey::try_from_z32(&value).map_err(|err| {
400 invalid_data(
401 format!("Paykit App Registry Noise public key is invalid: {err}"),
402 Some(err.into()),
403 )
404 })
405 })
406 .transpose()?;
407 let mut registry = PaykitAppRegistry::new(noise_public_key);
408 registry.key_generation = wire.key_generation;
409 for (raw_app_id, raw_app) in wire.apps {
410 let app_id = parse_remote_app_id(raw_app_id)?;
411 let app = PaykitApp::try_from(raw_app).map_err(|err| {
412 invalid_data(
413 format!("Paykit App Registry contains an invalid app: {err}"),
414 Some(err.into()),
415 )
416 })?;
417 registry.register_app(app_id, app).map_err(|err| {
418 invalid_data(
419 format!("Paykit App Registry contains an invalid application: {err}"),
420 Some(err.into()),
421 )
422 })?;
423 }
424
425 registry.default_app_id = wire.default_app_id.map(parse_remote_app_id).transpose()?;
426 for (raw_identifier, raw_app_id) in wire.default_apps_by_endpoint {
427 let identifier = PaymentEndpointIdentifier::new(raw_identifier).map_err(|_| {
428 invalid_data(
429 "Paykit App Registry contains an invalid Payment Endpoint Identifier",
430 None,
431 )
432 })?;
433 registry
434 .default_apps_by_endpoint
435 .insert(identifier, parse_remote_app_id(raw_app_id)?);
436 }
437 validate_registry_defaults(®istry)?;
438 Ok(registry)
439}
440
441pub async fn create_paykit_app_registry(
454 session: &pubky::PubkySession,
455 registry: &PaykitAppRegistry,
456) -> Result<()> {
457 pubky_routing::create_paykit_app_registry(session, registry)
458 .await
459 .map_err(|err| map_error("create_paykit_app_registry", err))
460}
461
462pub async fn update_paykit_app_registry(
476 session: &pubky::PubkySession,
477 registry: &PaykitAppRegistry,
478 revision: &str,
479) -> Result<()> {
480 pubky_routing::update_paykit_app_registry(session, registry, revision)
481 .await
482 .map_err(|err| map_error("update_paykit_app_registry", err))
483}
484
485pub async fn get_paykit_app_registry(
497 storage: &pubky::PublicStorage,
498 owner: &PublicKey,
499) -> Result<Option<PaykitAppRegistry>> {
500 pubky_routing::fetch_paykit_app_registry(storage, owner)
501 .await
502 .map_err(|err| map_error("get_paykit_app_registry", err))
503}
504
505pub async fn get_paykit_app_registry_with_revision(
515 storage: &pubky::PublicStorage,
516 owner: &PublicKey,
517) -> Result<Option<(PaykitAppRegistry, String)>> {
518 pubky_routing::fetch_paykit_app_registry_with_revision(storage, owner)
519 .await
520 .map_err(|err| map_error("get_paykit_app_registry_with_revision", err))
521}
522
523fn parse_remote_app_id(value: String) -> Result<PaykitAppId> {
524 PaykitAppId::new(value)
525 .map_err(|_| invalid_data("Paykit App Registry contains an invalid App ID", None))
526}
527
528fn validate_registry_defaults(registry: &PaykitAppRegistry) -> Result<()> {
529 if let Some(app_id) = registry.default_app_id() {
530 validate_registered_default(registry, app_id, "identity default")?;
531 }
532 for app_id in registry.default_apps_by_endpoint().values() {
533 validate_registered_default(registry, app_id, "endpoint default")?;
534 }
535 Ok(())
536}
537
538fn validate_registry_limits(registry: &PaykitAppRegistry) -> std::result::Result<(), String> {
539 if registry.apps.len() > PAYKIT_APP_REGISTRY_MAX_APPS {
540 return Err(format!(
541 "Paykit App Registry must not contain more than {PAYKIT_APP_REGISTRY_MAX_APPS} applications"
542 ));
543 }
544 if registry.default_apps_by_endpoint.len() > PAYKIT_APP_REGISTRY_MAX_ENDPOINT_DEFAULTS {
545 return Err(format!(
546 "Paykit App Registry must not contain more than {PAYKIT_APP_REGISTRY_MAX_ENDPOINT_DEFAULTS} endpoint defaults"
547 ));
548 }
549 if registry.noise_public_key.is_none()
550 && registry
551 .apps
552 .values()
553 .any(|app| app_uses_private_protocol(app.capabilities()))
554 {
555 return Err(
556 "Paykit App Registry must initialize its Noise public key before advertising private capabilities"
557 .into(),
558 );
559 }
560 Ok(())
561}
562
563fn app_uses_private_protocol(capabilities: PaykitAppCapabilities) -> bool {
564 capabilities.private_payments || capabilities.payment_requests || capabilities.receipts
565}
566
567fn validate_registered_default(
568 registry: &PaykitAppRegistry,
569 app_id: &PaykitAppId,
570 label: &'static str,
571) -> Result<()> {
572 if registry.apps.contains_key(app_id) {
573 return Ok(());
574 }
575 Err(invalid_data(
576 format!("Paykit App Registry {label} refers to an unregistered App ID"),
577 None,
578 ))
579}
580
581fn validate_display_name(value: &str) -> Result<()> {
582 if value.trim().is_empty() {
583 return Err(PaykitError::Validation(
584 "Paykit app display name must not be empty".into(),
585 ));
586 }
587 if value.len() > APP_DISPLAY_NAME_MAX_LEN {
588 return Err(PaykitError::Validation(format!(
589 "Paykit app display name must not exceed {APP_DISPLAY_NAME_MAX_LEN} bytes"
590 )));
591 }
592 if value.chars().any(char::is_control) {
593 return Err(PaykitError::Validation(
594 "Paykit app display name must not contain control characters".into(),
595 ));
596 }
597 Ok(())
598}
599
600fn deserialize_unique_map<'de, D, V>(
601 deserializer: D,
602) -> std::result::Result<HashMap<String, V>, D::Error>
603where
604 D: de::Deserializer<'de>,
605 V: Deserialize<'de>,
606{
607 struct UniqueMapVisitor<V>(std::marker::PhantomData<V>);
608
609 impl<'de, V> de::Visitor<'de> for UniqueMapVisitor<V>
610 where
611 V: Deserialize<'de>,
612 {
613 type Value = HashMap<String, V>;
614
615 fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
616 formatter.write_str("an object without duplicate keys")
617 }
618
619 fn visit_map<A>(self, mut map: A) -> std::result::Result<Self::Value, A::Error>
620 where
621 A: de::MapAccess<'de>,
622 {
623 let mut values = HashMap::new();
624 while let Some((key, value)) = map.next_entry::<String, V>()? {
625 if values.insert(key.clone(), value).is_some() {
626 return Err(de::Error::custom(format!("duplicate key '{key}'")));
627 }
628 }
629 Ok(values)
630 }
631 }
632
633 deserializer.deserialize_map(UniqueMapVisitor(std::marker::PhantomData))
634}
635
636#[cfg(test)]
637mod tests;