myc

Self-custodial remote signer for Radroots apps
git clone https://radroots.dev/git/myc.git
Log | Files | Refs | README | LICENSE

config_v1.rs (45905B)


      1 //! Strict, bounded Myc configuration document v1 admission.
      2 
      3 use std::collections::{BTreeMap, BTreeSet};
      4 use std::error::Error;
      5 use std::fmt;
      6 use std::net::SocketAddr;
      7 
      8 use nostr::PublicKey;
      9 use serde::Serialize;
     10 use serde_json::{Map, Value, json};
     11 use url::Url;
     12 
     13 use crate::provider_contract::MycProviderContract;
     14 
     15 const CONFIG_SCHEMA: &str = include_str!("../contracts/services_hardening/config.v1.schema.json");
     16 
     17 #[cfg(any(target_os = "linux", target_os = "macos"))]
     18 pub(crate) const fn config_schema_document() -> &'static str {
     19     CONFIG_SCHEMA
     20 }
     21 
     22 /// Exact schema identity for the production Myc configuration document.
     23 pub const MYC_CONFIG_SCHEMA: &str = "radroots.myc.config";
     24 
     25 /// Exact supported Myc configuration schema version.
     26 pub const MYC_CONFIG_SCHEMA_VERSION: u32 = 1;
     27 
     28 /// Hard cap applied to original bytes before UTF-8 or TOML parsing.
     29 pub const MYC_CONFIG_DOCUMENT_MAX_UTF8_BYTES: usize = 1_048_576;
     30 
     31 /// Bootstrap-selected network posture used during relay admission.
     32 #[derive(Clone, Copy, Debug, PartialEq, Eq)]
     33 pub enum MycConfigProfile {
     34     /// Production and ordinary service-host configurations require WSS relays.
     35     Production,
     36     /// Explicit repository-local development may also use loopback WS relays.
     37     RepoLocal,
     38 }
     39 
     40 /// Stable source classification for an effective configuration value.
     41 #[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize)]
     42 #[serde(rename_all = "snake_case")]
     43 pub enum MycConfigValueSource {
     44     Document,
     45     RadrootsServiceHost,
     46     RadrootsServiceSqlite,
     47     RadrootsEvent,
     48     RadrootsNostrConnect,
     49     AcceptedServiceAuthority,
     50     EngineeringSafety,
     51 }
     52 
     53 /// Stable source-free classification for configuration admission failures.
     54 #[derive(Clone, Copy, Debug, PartialEq, Eq)]
     55 pub enum MycConfigV1ErrorKind {
     56     TooLarge,
     57     InvalidUtf8,
     58     MalformedToml,
     59     MissingSchema,
     60     InvalidSchema,
     61     SchemaMismatch,
     62     MissingSchemaVersion,
     63     InvalidSchemaVersion,
     64     UnsupportedSchemaVersion,
     65     InvalidDocument,
     66     InvalidRelationship,
     67     Encoding,
     68 }
     69 
     70 impl MycConfigV1ErrorKind {
     71     const fn message(self) -> &'static str {
     72         match self {
     73             Self::TooLarge => "configuration document exceeds its size limit",
     74             Self::InvalidUtf8 => "configuration document is not valid UTF-8",
     75             Self::MalformedToml => "configuration document is not valid TOML",
     76             Self::MissingSchema => "configuration document schema is missing",
     77             Self::InvalidSchema => "configuration document schema is invalid",
     78             Self::SchemaMismatch => "configuration document schema is unsupported",
     79             Self::MissingSchemaVersion => "configuration document schema version is missing",
     80             Self::InvalidSchemaVersion => "configuration document schema version is invalid",
     81             Self::UnsupportedSchemaVersion => {
     82                 "configuration document schema version is unsupported"
     83             }
     84             Self::InvalidDocument => "configuration document fields are invalid",
     85             Self::InvalidRelationship => "configuration document relationships are invalid",
     86             Self::Encoding => "effective configuration could not be encoded",
     87         }
     88     }
     89 }
     90 
     91 /// One source-free configuration admission failure.
     92 #[derive(Clone, Copy, PartialEq, Eq)]
     93 pub struct MycConfigV1Error {
     94     kind: MycConfigV1ErrorKind,
     95 }
     96 
     97 impl MycConfigV1Error {
     98     const fn new(kind: MycConfigV1ErrorKind) -> Self {
     99         Self { kind }
    100     }
    101 
    102     /// Returns the stable failure classification.
    103     #[must_use]
    104     pub const fn kind(self) -> MycConfigV1ErrorKind {
    105         self.kind
    106     }
    107 }
    108 
    109 impl fmt::Debug for MycConfigV1Error {
    110     fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
    111         formatter
    112             .debug_struct("MycConfigV1Error")
    113             .field("kind", &self.kind)
    114             .finish()
    115     }
    116 }
    117 
    118 impl fmt::Display for MycConfigV1Error {
    119     fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
    120         formatter.write_str(self.kind.message())
    121     }
    122 }
    123 
    124 impl Error for MycConfigV1Error {}
    125 
    126 /// Deterministic redacted effective configuration with exact leaf provenance.
    127 #[derive(Clone, PartialEq, Eq)]
    128 pub struct MycEffectiveConfigV1 {
    129     canonical_json: Box<str>,
    130     field_count: usize,
    131 }
    132 
    133 impl MycEffectiveConfigV1 {
    134     /// Returns compact JSON in deterministic path order.
    135     #[must_use]
    136     pub fn canonical_json(&self) -> &str {
    137         &self.canonical_json
    138     }
    139 
    140     /// Returns the number of projected effective leaf values.
    141     #[must_use]
    142     pub const fn field_count(&self) -> usize {
    143         self.field_count
    144     }
    145 }
    146 
    147 impl fmt::Debug for MycEffectiveConfigV1 {
    148     fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
    149         formatter
    150             .debug_struct("MycEffectiveConfigV1")
    151             .field("canonical_json", &"[redacted]")
    152             .field("field_count", &self.field_count)
    153             .finish()
    154     }
    155 }
    156 
    157 /// A validated immutable Myc configuration document v1.
    158 pub struct MycConfigDocumentV1 {
    159     profile: MycConfigProfile,
    160     normalized: Value,
    161     effective: MycEffectiveConfigV1,
    162     provider_contract: MycProviderContract,
    163     runtime_thread_limits: MycRuntimeThreadLimitsV1,
    164 }
    165 
    166 /// Validated thread counts for the sole binary-owned Tokio runtime.
    167 #[derive(Clone, Copy, Debug, PartialEq, Eq)]
    168 pub struct MycRuntimeThreadLimitsV1 {
    169     worker_threads: usize,
    170     blocking_threads: usize,
    171 }
    172 
    173 impl MycRuntimeThreadLimitsV1 {
    174     #[must_use]
    175     pub const fn worker_threads(self) -> usize {
    176         self.worker_threads
    177     }
    178 
    179     #[must_use]
    180     pub const fn blocking_threads(self) -> usize {
    181         self.blocking_threads
    182     }
    183 }
    184 
    185 impl MycConfigDocumentV1 {
    186     /// Returns the exact admitted schema identity.
    187     #[must_use]
    188     pub const fn schema(&self) -> &'static str {
    189         MYC_CONFIG_SCHEMA
    190     }
    191 
    192     /// Returns the exact admitted schema version.
    193     #[must_use]
    194     pub const fn schema_version(&self) -> u32 {
    195         MYC_CONFIG_SCHEMA_VERSION
    196     }
    197 
    198     /// Returns the bootstrap-selected network posture used during admission.
    199     #[must_use]
    200     pub const fn profile(&self) -> MycConfigProfile {
    201         self.profile
    202     }
    203 
    204     /// Returns the deterministic redacted effective configuration projection.
    205     #[must_use]
    206     pub const fn effective(&self) -> &MycEffectiveConfigV1 {
    207         &self.effective
    208     }
    209 
    210     /// Returns the exact number of configured relay bindings.
    211     #[must_use]
    212     pub fn relay_count(&self) -> usize {
    213         self.normalized
    214             .pointer("/relays")
    215             .and_then(Value::as_array)
    216             .map_or(0, Vec::len)
    217     }
    218 
    219     /// Returns the immutable provider assignments derived from this document.
    220     #[must_use]
    221     pub const fn provider_contract(&self) -> &MycProviderContract {
    222         &self.provider_contract
    223     }
    224 
    225     /// Returns the validated limits for the sole binary-owned Tokio runtime.
    226     #[must_use]
    227     pub const fn runtime_thread_limits(&self) -> MycRuntimeThreadLimitsV1 {
    228         self.runtime_thread_limits
    229     }
    230 
    231     pub(crate) const fn normalized(&self) -> &Value {
    232         &self.normalized
    233     }
    234 }
    235 
    236 impl fmt::Debug for MycConfigDocumentV1 {
    237     fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
    238         formatter
    239             .debug_struct("MycConfigDocumentV1")
    240             .field("schema", &MYC_CONFIG_SCHEMA)
    241             .field("schema_version", &MYC_CONFIG_SCHEMA_VERSION)
    242             .field("profile", &self.profile)
    243             .field("effective", &self.effective)
    244             .field("provider_contract", &self.provider_contract)
    245             .finish()
    246     }
    247 }
    248 
    249 /// Parses and semantically validates one complete Myc configuration document.
    250 pub fn parse_myc_config_v1(
    251     bytes: &[u8],
    252     profile: MycConfigProfile,
    253 ) -> Result<MycConfigDocumentV1, MycConfigV1Error> {
    254     if bytes.len() > MYC_CONFIG_DOCUMENT_MAX_UTF8_BYTES {
    255         return Err(error(MycConfigV1ErrorKind::TooLarge));
    256     }
    257     let source =
    258         std::str::from_utf8(bytes).map_err(|_| error(MycConfigV1ErrorKind::InvalidUtf8))?;
    259     let original = source
    260         .parse::<toml::Table>()
    261         .map_err(|_| error(MycConfigV1ErrorKind::MalformedToml))?;
    262     validate_header(&original)?;
    263     let mut normalized = serde_json::to_value(toml::Value::Table(original.clone()))
    264         .map_err(|_| error(MycConfigV1ErrorKind::InvalidDocument))?;
    265     let schema: Value =
    266         serde_json::from_str(CONFIG_SCHEMA).map_err(|_| error(MycConfigV1ErrorKind::Encoding))?;
    267     let validator =
    268         jsonschema::validator_for(&schema).map_err(|_| error(MycConfigV1ErrorKind::Encoding))?;
    269     if !validator.is_valid(&normalized) {
    270         return Err(error(MycConfigV1ErrorKind::InvalidDocument));
    271     }
    272     apply_defaults(&mut normalized)?;
    273     if !validator.is_valid(&normalized) {
    274         return Err(error(MycConfigV1ErrorKind::InvalidDocument));
    275     }
    276     validate_relationships(&normalized, profile)?;
    277     let effective = build_effective(&normalized, &original)?;
    278     let provider_contract = MycProviderContract::from_normalized(&normalized)
    279         .map_err(|_| error(MycConfigV1ErrorKind::InvalidRelationship))?;
    280     let runtime_thread_limits = MycRuntimeThreadLimitsV1 {
    281         worker_threads: usize::try_from(integer(
    282             &normalized,
    283             "/resource_limits/runtime/worker_threads",
    284         )?)
    285         .map_err(|_| error(MycConfigV1ErrorKind::InvalidRelationship))?,
    286         blocking_threads: usize::try_from(integer(
    287             &normalized,
    288             "/resource_limits/runtime/blocking_threads",
    289         )?)
    290         .map_err(|_| error(MycConfigV1ErrorKind::InvalidRelationship))?,
    291     };
    292     Ok(MycConfigDocumentV1 {
    293         profile,
    294         normalized,
    295         effective,
    296         provider_contract,
    297         runtime_thread_limits,
    298     })
    299 }
    300 
    301 fn validate_header(header: &toml::Table) -> Result<(), MycConfigV1Error> {
    302     let schema = header
    303         .get("schema")
    304         .ok_or_else(|| error(MycConfigV1ErrorKind::MissingSchema))?
    305         .as_str()
    306         .ok_or_else(|| error(MycConfigV1ErrorKind::InvalidSchema))?;
    307     if !valid_schema_id(schema) {
    308         return Err(error(MycConfigV1ErrorKind::InvalidSchema));
    309     }
    310     if schema != MYC_CONFIG_SCHEMA {
    311         return Err(error(MycConfigV1ErrorKind::SchemaMismatch));
    312     }
    313     let version = header
    314         .get("schema_version")
    315         .ok_or_else(|| error(MycConfigV1ErrorKind::MissingSchemaVersion))?
    316         .as_integer()
    317         .ok_or_else(|| error(MycConfigV1ErrorKind::InvalidSchemaVersion))?;
    318     let version =
    319         u32::try_from(version).map_err(|_| error(MycConfigV1ErrorKind::InvalidSchemaVersion))?;
    320     if version == 0 {
    321         return Err(error(MycConfigV1ErrorKind::InvalidSchemaVersion));
    322     }
    323     if version != MYC_CONFIG_SCHEMA_VERSION {
    324         return Err(error(MycConfigV1ErrorKind::UnsupportedSchemaVersion));
    325     }
    326     Ok(())
    327 }
    328 
    329 fn valid_schema_id(value: &str) -> bool {
    330     let mut bytes = value.bytes();
    331     !value.is_empty()
    332         && value.len() <= 128
    333         && bytes
    334             .next()
    335             .is_some_and(|byte| byte.is_ascii_alphanumeric())
    336         && bytes
    337             .all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'.' | b'_' | b':' | b'-'))
    338 }
    339 
    340 #[derive(Clone, Copy)]
    341 struct DefaultEntry {
    342     path: &'static str,
    343     value: DefaultValue,
    344     source: MycConfigValueSource,
    345     enabled_pointer: Option<&'static str>,
    346 }
    347 
    348 #[derive(Clone, Copy)]
    349 enum DefaultValue {
    350     Integer(u64),
    351     String(&'static str),
    352 }
    353 
    354 const DEFAULTS: &[DefaultEntry] = &[
    355     default(
    356         "/service/shutdown_grace_ms",
    357         30_000,
    358         MycConfigValueSource::EngineeringSafety,
    359     ),
    360     default_string(
    361         "/logging/level",
    362         "info",
    363         MycConfigValueSource::EngineeringSafety,
    364     ),
    365     default_string(
    366         "/logging/format",
    367         "json",
    368         MycConfigValueSource::AcceptedServiceAuthority,
    369     ),
    370     conditional_default(
    371         "/operations/limits/header_count",
    372         32,
    373         MycConfigValueSource::RadrootsServiceHost,
    374         "/operations/enabled",
    375     ),
    376     conditional_default(
    377         "/operations/limits/header_bytes",
    378         16_384,
    379         MycConfigValueSource::RadrootsServiceHost,
    380         "/operations/enabled",
    381     ),
    382     conditional_default(
    383         "/operations/limits/response_body_utf8_bytes",
    384         1_048_576,
    385         MycConfigValueSource::RadrootsServiceHost,
    386         "/operations/enabled",
    387     ),
    388     conditional_default(
    389         "/operations/limits/concurrent_connections",
    390         32,
    391         MycConfigValueSource::RadrootsServiceHost,
    392         "/operations/enabled",
    393     ),
    394     conditional_default(
    395         "/operations/limits/request_deadline_ms",
    396         15_000,
    397         MycConfigValueSource::RadrootsServiceHost,
    398         "/operations/enabled",
    399     ),
    400     conditional_default(
    401         "/operations/limits/idle_timeout_ms",
    402         30_000,
    403         MycConfigValueSource::RadrootsServiceHost,
    404         "/operations/enabled",
    405     ),
    406     default(
    407         "/database/busy_timeout_ms",
    408         5_000,
    409         MycConfigValueSource::RadrootsServiceSqlite,
    410     ),
    411     default(
    412         "/database/max_connections",
    413         8,
    414         MycConfigValueSource::RadrootsServiceSqlite,
    415     ),
    416     default(
    417         "/transport/connect_deadline_ms",
    418         10_000,
    419         MycConfigValueSource::EngineeringSafety,
    420     ),
    421     default(
    422         "/transport/publish_retry/max_attempts",
    423         5,
    424         MycConfigValueSource::EngineeringSafety,
    425     ),
    426     default(
    427         "/transport/publish_retry/initial_backoff_ms",
    428         250,
    429         MycConfigValueSource::EngineeringSafety,
    430     ),
    431     default(
    432         "/transport/publish_retry/maximum_backoff_ms",
    433         30_000,
    434         MycConfigValueSource::EngineeringSafety,
    435     ),
    436     default(
    437         "/transport/publish_retry/attempt_deadline_ms",
    438         15_000,
    439         MycConfigValueSource::RadrootsServiceHost,
    440     ),
    441     default(
    442         "/resource_limits/admin/header_count",
    443         32,
    444         MycConfigValueSource::RadrootsServiceHost,
    445     ),
    446     default(
    447         "/resource_limits/admin/header_bytes",
    448         16_384,
    449         MycConfigValueSource::RadrootsServiceHost,
    450     ),
    451     default(
    452         "/resource_limits/admin/request_body_utf8_bytes",
    453         65_536,
    454         MycConfigValueSource::RadrootsServiceHost,
    455     ),
    456     default(
    457         "/resource_limits/admin/response_body_utf8_bytes",
    458         1_048_576,
    459         MycConfigValueSource::RadrootsServiceHost,
    460     ),
    461     default(
    462         "/resource_limits/admin/concurrent_connections",
    463         32,
    464         MycConfigValueSource::RadrootsServiceHost,
    465     ),
    466     default(
    467         "/resource_limits/admin/request_deadline_ms",
    468         15_000,
    469         MycConfigValueSource::RadrootsServiceHost,
    470     ),
    471     default(
    472         "/resource_limits/admin/idle_timeout_ms",
    473         30_000,
    474         MycConfigValueSource::RadrootsServiceHost,
    475     ),
    476     default(
    477         "/resource_limits/admin/query_items",
    478         100,
    479         MycConfigValueSource::RadrootsServiceHost,
    480     ),
    481     default(
    482         "/resource_limits/events/wire_bytes",
    483         262_144,
    484         MycConfigValueSource::RadrootsEvent,
    485     ),
    486     default(
    487         "/resource_limits/events/content_bytes",
    488         131_072,
    489         MycConfigValueSource::RadrootsEvent,
    490     ),
    491     default(
    492         "/resource_limits/events/tag_count",
    493         1_024,
    494         MycConfigValueSource::RadrootsEvent,
    495     ),
    496     default(
    497         "/resource_limits/events/tag_total_elements",
    498         4_096,
    499         MycConfigValueSource::RadrootsEvent,
    500     ),
    501     default(
    502         "/resource_limits/events/tag_element_bytes",
    503         4_096,
    504         MycConfigValueSource::RadrootsEvent,
    505     ),
    506     default(
    507         "/resource_limits/events/tag_total_bytes",
    508         131_072,
    509         MycConfigValueSource::RadrootsEvent,
    510     ),
    511     default(
    512         "/resource_limits/events/decrypted_plaintext_bytes",
    513         262_144,
    514         MycConfigValueSource::RadrootsNostrConnect,
    515     ),
    516     default(
    517         "/resource_limits/queues/ingress",
    518         1_024,
    519         MycConfigValueSource::EngineeringSafety,
    520     ),
    521     default(
    522         "/resource_limits/queues/provider",
    523         64,
    524         MycConfigValueSource::EngineeringSafety,
    525     ),
    526     default(
    527         "/resource_limits/queues/outbox",
    528         4_096,
    529         MycConfigValueSource::EngineeringSafety,
    530     ),
    531     default(
    532         "/resource_limits/queues/discovery",
    533         64,
    534         MycConfigValueSource::EngineeringSafety,
    535     ),
    536     default(
    537         "/resource_limits/metrics/descriptors",
    538         64,
    539         MycConfigValueSource::RadrootsServiceHost,
    540     ),
    541     default(
    542         "/resource_limits/metrics/samples",
    543         512,
    544         MycConfigValueSource::RadrootsServiceHost,
    545     ),
    546     default(
    547         "/resource_limits/metrics/labels_per_sample",
    548         8,
    549         MycConfigValueSource::RadrootsServiceHost,
    550     ),
    551     default(
    552         "/resource_limits/metrics/render_utf8_bytes",
    553         1_048_576,
    554         MycConfigValueSource::RadrootsServiceHost,
    555     ),
    556     default(
    557         "/resource_limits/runtime/worker_threads",
    558         4,
    559         MycConfigValueSource::EngineeringSafety,
    560     ),
    561     default(
    562         "/resource_limits/runtime/blocking_threads",
    563         8,
    564         MycConfigValueSource::EngineeringSafety,
    565     ),
    566 ];
    567 
    568 const fn default(path: &'static str, value: u64, source: MycConfigValueSource) -> DefaultEntry {
    569     DefaultEntry {
    570         path,
    571         value: DefaultValue::Integer(value),
    572         source,
    573         enabled_pointer: None,
    574     }
    575 }
    576 
    577 const fn conditional_default(
    578     path: &'static str,
    579     value: u64,
    580     source: MycConfigValueSource,
    581     enabled_pointer: &'static str,
    582 ) -> DefaultEntry {
    583     DefaultEntry {
    584         path,
    585         value: DefaultValue::Integer(value),
    586         source,
    587         enabled_pointer: Some(enabled_pointer),
    588     }
    589 }
    590 
    591 const fn default_string(
    592     path: &'static str,
    593     value: &'static str,
    594     source: MycConfigValueSource,
    595 ) -> DefaultEntry {
    596     DefaultEntry {
    597         path,
    598         value: DefaultValue::String(value),
    599         source,
    600         enabled_pointer: None,
    601     }
    602 }
    603 
    604 fn apply_defaults(document: &mut Value) -> Result<(), MycConfigV1Error> {
    605     for entry in DEFAULTS {
    606         if entry
    607             .enabled_pointer
    608             .is_some_and(|pointer| document.pointer(pointer) != Some(&Value::Bool(true)))
    609             || document.pointer(entry.path).is_some()
    610         {
    611             continue;
    612         }
    613         insert_json_pointer(document, entry.path, entry.value)?;
    614     }
    615     Ok(())
    616 }
    617 
    618 fn insert_json_pointer(
    619     root: &mut Value,
    620     pointer: &str,
    621     value: DefaultValue,
    622 ) -> Result<(), MycConfigV1Error> {
    623     let mut parts = pointer
    624         .split('/')
    625         .filter(|part| !part.is_empty())
    626         .peekable();
    627     let mut current = root;
    628     while let Some(part) = parts.next() {
    629         let object = current
    630             .as_object_mut()
    631             .ok_or_else(|| error(MycConfigV1ErrorKind::InvalidDocument))?;
    632         if parts.peek().is_none() {
    633             object.insert(
    634                 part.to_owned(),
    635                 match value {
    636                     DefaultValue::Integer(value) => Value::Number(value.into()),
    637                     DefaultValue::String(value) => Value::String(value.to_owned()),
    638                 },
    639             );
    640             return Ok(());
    641         }
    642         current = object
    643             .entry(part.to_owned())
    644             .or_insert_with(|| Value::Object(Map::new()));
    645     }
    646     Err(error(MycConfigV1ErrorKind::InvalidDocument))
    647 }
    648 
    649 fn validate_relationships(
    650     document: &Value,
    651     profile: MycConfigProfile,
    652 ) -> Result<(), MycConfigV1Error> {
    653     validate_utf8_byte_limits(document)?;
    654     let relays = array(document, "/relays")?;
    655     let mut ids = BTreeSet::new();
    656     let mut urls = BTreeSet::new();
    657     let mut has_read = false;
    658     let mut has_write = false;
    659     let mut has_required_read = false;
    660     let mut has_required_write = false;
    661     for relay in relays {
    662         let id = string_at(relay, "/id")?;
    663         let raw_url = string_at(relay, "/url")?;
    664         canonical_relay_url(raw_url, profile)?;
    665         let read = bool_at(relay, "/read")?;
    666         let write = bool_at(relay, "/write")?;
    667         let required = bool_at(relay, "/required")?;
    668         if !ids.insert(id) || !urls.insert(raw_url) || (!read && !write) {
    669             return relationship_error();
    670         }
    671         has_read |= read;
    672         has_write |= write;
    673         has_required_read |= required && read;
    674         has_required_write |= required && write;
    675     }
    676     if !(has_read && has_write && has_required_read && has_required_write) {
    677         return relationship_error();
    678     }
    679     validate_unique_role_keys(document)?;
    680     validate_client_policy(document)?;
    681     validate_challenges(document)?;
    682     validate_rate_limits(document)?;
    683     validate_transport(document, relays)?;
    684     validate_discovery(document, relays)?;
    685     validate_operations(document)
    686 }
    687 
    688 fn validate_utf8_byte_limits(document: &Value) -> Result<(), MycConfigV1Error> {
    689     validate_provider_bytes(document, "/identity/transport")?;
    690     validate_provider_bytes(document, "/identity/user")?;
    691     if bool_value(document, "/identity/discovery/enabled")? {
    692         validate_provider_bytes(document, "/identity/discovery/binding")?;
    693     }
    694     for relay in array(document, "/relays")? {
    695         validate_string_bytes(string_at(relay, "/id")?, 1, 64)?;
    696         validate_string_bytes(string_at(relay, "/url")?, 1, 2_048)?;
    697     }
    698     if bool_value(document, "/operations/enabled")? {
    699         validate_string_bytes(string(document, "/operations/listen")?, 1, 128)?;
    700     }
    701     if bool_value(document, "/policy/challenges/enabled")? {
    702         validate_string_bytes(string(document, "/policy/challenges/url")?, 1, 2_048)?;
    703     }
    704     if bool_value(document, "/discovery/enabled")? {
    705         for (pointer, minimum, maximum) in [
    706             ("/discovery/domain", 1, 253),
    707             ("/discovery/handler_identifier", 1, 128),
    708             ("/discovery/nostrconnect_url_template", 1, 2_048),
    709             ("/discovery/metadata/name", 1, 128),
    710             ("/discovery/metadata/display_name", 1, 128),
    711             ("/discovery/metadata/about", 0, 1_024),
    712             ("/discovery/metadata/website", 1, 2_048),
    713             ("/discovery/metadata/picture", 1, 2_048),
    714         ] {
    715             validate_string_bytes(string(document, pointer)?, minimum, maximum)?;
    716         }
    717     }
    718     Ok(())
    719 }
    720 
    721 fn validate_provider_bytes(document: &Value, prefix: &str) -> Result<(), MycConfigV1Error> {
    722     match string(document, &format!("{prefix}/provider"))? {
    723         "encrypted_file" => {
    724             validate_string_bytes(
    725                 string(document, &format!("{prefix}/envelope_path"))?,
    726                 1,
    727                 4_096,
    728             )?;
    729             validate_string_bytes(
    730                 string(document, &format!("{prefix}/credential_reference"))?,
    731                 1,
    732                 128,
    733             )
    734         }
    735         "local_signer" => validate_string_bytes(
    736             string(document, &format!("{prefix}/socket_path"))?,
    737             1,
    738             4_096,
    739         ),
    740         _ => document_error(),
    741     }
    742 }
    743 
    744 fn validate_string_bytes(
    745     value: &str,
    746     minimum: usize,
    747     maximum: usize,
    748 ) -> Result<(), MycConfigV1Error> {
    749     if (minimum..=maximum).contains(&value.len()) {
    750         Ok(())
    751     } else {
    752         document_error()
    753     }
    754 }
    755 
    756 fn validate_unique_role_keys(document: &Value) -> Result<(), MycConfigV1Error> {
    757     let mut keys = vec![
    758         string(document, "/identity/transport/expected_public_key")?,
    759         string(document, "/identity/user/expected_public_key")?,
    760     ];
    761     if bool_value(document, "/identity/discovery/enabled")? {
    762         keys.push(string(
    763             document,
    764             "/identity/discovery/binding/expected_public_key",
    765         )?);
    766     }
    767     if keys.iter().any(|key| !valid_nostr_public_key(key))
    768         || keys.iter().copied().collect::<BTreeSet<_>>().len() != keys.len()
    769     {
    770         return relationship_error();
    771     }
    772     Ok(())
    773 }
    774 
    775 fn validate_client_policy(document: &Value) -> Result<(), MycConfigV1Error> {
    776     let trusted = string_set(document, "/policy/trusted_clients")?;
    777     let denied = string_set(document, "/policy/denied_clients")?;
    778     if trusted
    779         .iter()
    780         .chain(denied.iter())
    781         .any(|key| !valid_nostr_public_key(key))
    782         || !trusted.is_disjoint(&denied)
    783     {
    784         return relationship_error();
    785     }
    786     let permission_kinds = string_set(document, "/policy/permission_ceiling")?
    787         .into_iter()
    788         .filter_map(|permission| permission.strip_prefix("sign_event:kind:"))
    789         .map(|kind| {
    790             kind.parse::<u32>()
    791                 .map_err(|_| error(MycConfigV1ErrorKind::InvalidDocument))
    792         })
    793         .collect::<Result<BTreeSet<_>, _>>()?;
    794     let allowed_kinds = array(document, "/policy/allowed_sign_event_kinds")?
    795         .iter()
    796         .map(|kind| kind.as_u64().and_then(|value| u32::try_from(value).ok()))
    797         .collect::<Option<BTreeSet<_>>>()
    798         .ok_or_else(|| error(MycConfigV1ErrorKind::InvalidDocument))?;
    799     if permission_kinds != allowed_kinds {
    800         return relationship_error();
    801     }
    802     Ok(())
    803 }
    804 
    805 fn valid_nostr_public_key(value: &str) -> bool {
    806     PublicKey::from_hex(value).is_ok_and(|public_key| public_key.xonly().is_ok())
    807 }
    808 
    809 fn validate_challenges(document: &Value) -> Result<(), MycConfigV1Error> {
    810     if bool_value(document, "/policy/challenges/enabled")?
    811         && integer(document, "/policy/challenges/authorized_lifetime_ms")?
    812             < integer(document, "/policy/challenges/pending_lifetime_ms")?
    813     {
    814         return relationship_error();
    815     }
    816     Ok(())
    817 }
    818 
    819 fn validate_rate_limits(document: &Value) -> Result<(), MycConfigV1Error> {
    820     for (name, expected_scope) in [
    821         ("connection_admission", "global_and_relay"),
    822         ("challenge_creation", "connection"),
    823         ("challenge_authorization", "connection"),
    824     ] {
    825         let prefix = format!("/rate_limits/{name}");
    826         if string(document, &format!("{prefix}/scope"))? != expected_scope
    827             || integer(document, &format!("{prefix}/retention_ms"))?
    828                 < integer(document, &format!("{prefix}/window_ms"))?
    829         {
    830             return relationship_error();
    831         }
    832     }
    833     Ok(())
    834 }
    835 
    836 fn validate_transport(document: &Value, relays: &[Value]) -> Result<(), MycConfigV1Error> {
    837     if integer(document, "/transport/publish_retry/initial_backoff_ms")?
    838         > integer(document, "/transport/publish_retry/maximum_backoff_ms")?
    839     {
    840         return relationship_error();
    841     }
    842     if string(document, "/transport/delivery_policy/mode")? == "required_quorum" {
    843         let required = integer(
    844             document,
    845             "/transport/delivery_policy/required_acknowledgements",
    846         )?;
    847         let required_writers = relays
    848             .iter()
    849             .filter(|relay| {
    850                 bool_at(relay, "/required") == Ok(true) && bool_at(relay, "/write") == Ok(true)
    851             })
    852             .count() as u64;
    853         if required > required_writers {
    854             return relationship_error();
    855         }
    856     }
    857     Ok(())
    858 }
    859 
    860 fn validate_discovery(document: &Value, relays: &[Value]) -> Result<(), MycConfigV1Error> {
    861     let enabled = bool_value(document, "/discovery/enabled")?;
    862     if enabled != bool_value(document, "/identity/discovery/enabled")? {
    863         return relationship_error();
    864     }
    865     if !enabled {
    866         return Ok(());
    867     }
    868     for (pointer, capability) in [
    869         ("/discovery/public_relay_ids", "read"),
    870         ("/discovery/publish_relay_ids", "write"),
    871     ] {
    872         for relay_id in string_set(document, pointer)? {
    873             if !relays.iter().any(|relay| {
    874                 string_at(relay, "/id") == Ok(relay_id)
    875                     && bool_at(relay, &format!("/{capability}")) == Ok(true)
    876             }) {
    877                 return relationship_error();
    878             }
    879         }
    880     }
    881     Ok(())
    882 }
    883 
    884 fn validate_operations(document: &Value) -> Result<(), MycConfigV1Error> {
    885     if !bool_value(document, "/operations/enabled")? {
    886         return Ok(());
    887     }
    888     let address = string(document, "/operations/listen")?
    889         .parse::<SocketAddr>()
    890         .map_err(|_| error(MycConfigV1ErrorKind::InvalidDocument))?;
    891     if address.port() == 0
    892         || (string(document, "/operations/bind_policy")? == "loopback_only"
    893             && !address.ip().is_loopback())
    894     {
    895         return relationship_error();
    896     }
    897     Ok(())
    898 }
    899 
    900 fn canonical_relay_url(value: &str, profile: MycConfigProfile) -> Result<Url, MycConfigV1Error> {
    901     let parsed = Url::parse(value).map_err(|_| error(MycConfigV1ErrorKind::InvalidDocument))?;
    902     if parsed.as_str() != value
    903         || !parsed.username().is_empty()
    904         || parsed.password().is_some()
    905         || parsed.fragment().is_some()
    906     {
    907         return document_error();
    908     }
    909     let allowed = match profile {
    910         MycConfigProfile::Production => parsed.scheme() == "wss",
    911         MycConfigProfile::RepoLocal => match parsed.scheme() {
    912             "wss" => true,
    913             "ws" => parsed
    914                 .host_str()
    915                 .is_some_and(|host| matches!(host, "localhost" | "127.0.0.1" | "[::1]" | "::1")),
    916             _ => false,
    917         },
    918     };
    919     if !allowed {
    920         return relationship_error();
    921     }
    922     Ok(parsed)
    923 }
    924 
    925 fn array<'a>(value: &'a Value, pointer: &str) -> Result<&'a [Value], MycConfigV1Error> {
    926     value
    927         .pointer(pointer)
    928         .and_then(Value::as_array)
    929         .map(Vec::as_slice)
    930         .ok_or_else(|| error(MycConfigV1ErrorKind::InvalidDocument))
    931 }
    932 
    933 fn string<'a>(value: &'a Value, pointer: &str) -> Result<&'a str, MycConfigV1Error> {
    934     value
    935         .pointer(pointer)
    936         .and_then(Value::as_str)
    937         .ok_or_else(|| error(MycConfigV1ErrorKind::InvalidDocument))
    938 }
    939 
    940 fn string_at<'a>(value: &'a Value, pointer: &str) -> Result<&'a str, MycConfigV1Error> {
    941     string(value, pointer)
    942 }
    943 
    944 fn string_set<'a>(value: &'a Value, pointer: &str) -> Result<BTreeSet<&'a str>, MycConfigV1Error> {
    945     array(value, pointer)?
    946         .iter()
    947         .map(|entry| {
    948             entry
    949                 .as_str()
    950                 .ok_or_else(|| error(MycConfigV1ErrorKind::InvalidDocument))
    951         })
    952         .collect()
    953 }
    954 
    955 fn bool_value(value: &Value, pointer: &str) -> Result<bool, MycConfigV1Error> {
    956     bool_at(value, pointer)
    957 }
    958 
    959 fn bool_at(value: &Value, pointer: &str) -> Result<bool, MycConfigV1Error> {
    960     value
    961         .pointer(pointer)
    962         .and_then(Value::as_bool)
    963         .ok_or_else(|| error(MycConfigV1ErrorKind::InvalidDocument))
    964 }
    965 
    966 fn integer(value: &Value, pointer: &str) -> Result<u64, MycConfigV1Error> {
    967     value
    968         .pointer(pointer)
    969         .and_then(Value::as_u64)
    970         .ok_or_else(|| error(MycConfigV1ErrorKind::InvalidDocument))
    971 }
    972 
    973 fn document_error<T>() -> Result<T, MycConfigV1Error> {
    974     Err(error(MycConfigV1ErrorKind::InvalidDocument))
    975 }
    976 
    977 fn relationship_error<T>() -> Result<T, MycConfigV1Error> {
    978     Err(error(MycConfigV1ErrorKind::InvalidRelationship))
    979 }
    980 
    981 const fn error(kind: MycConfigV1ErrorKind) -> MycConfigV1Error {
    982     MycConfigV1Error::new(kind)
    983 }
    984 
    985 #[derive(Serialize)]
    986 struct EffectiveProjection {
    987     schema: &'static str,
    988     schema_version: u32,
    989     fields: Vec<EffectiveField>,
    990 }
    991 
    992 #[derive(Serialize)]
    993 struct EffectiveField {
    994     path: String,
    995     source: MycConfigValueSource,
    996     value: Value,
    997 }
    998 
    999 fn build_effective(
   1000     normalized: &Value,
   1001     original: &toml::Table,
   1002 ) -> Result<MycEffectiveConfigV1, MycConfigV1Error> {
   1003     let mut flattened = BTreeMap::new();
   1004     flatten_value("", normalized, &mut flattened);
   1005     let original = toml::Value::Table(original.clone());
   1006     let fields = flattened
   1007         .into_iter()
   1008         .map(|(path, value)| EffectiveField {
   1009             source: default_entry(&path).map_or(MycConfigValueSource::Document, |entry| {
   1010                 if toml_path(&original, &path).is_some() {
   1011                     MycConfigValueSource::Document
   1012                 } else {
   1013                     entry.source
   1014                 }
   1015             }),
   1016             value: redacted_value(&path, value),
   1017             path,
   1018         })
   1019         .collect::<Vec<_>>();
   1020     let field_count = fields.len();
   1021     let canonical_json = serde_json::to_string(&EffectiveProjection {
   1022         schema: "radroots.myc.effective-config",
   1023         schema_version: 1,
   1024         fields,
   1025     })
   1026     .map_err(|_| error(MycConfigV1ErrorKind::Encoding))?
   1027     .into_boxed_str();
   1028     Ok(MycEffectiveConfigV1 {
   1029         canonical_json,
   1030         field_count,
   1031     })
   1032 }
   1033 
   1034 fn flatten_value(path: &str, value: &Value, fields: &mut BTreeMap<String, Value>) {
   1035     match value {
   1036         Value::Object(object) => {
   1037             for (key, child) in object {
   1038                 flatten_value(&format!("{path}/{key}"), child, fields);
   1039             }
   1040         }
   1041         Value::Array(array) if array.iter().all(Value::is_object) => {
   1042             for (index, child) in array.iter().enumerate() {
   1043                 flatten_value(&format!("{path}/{index}"), child, fields);
   1044             }
   1045         }
   1046         _ => {
   1047             fields.insert(path.to_owned(), value.clone());
   1048         }
   1049     }
   1050 }
   1051 
   1052 fn redacted_value(path: &str, value: Value) -> Value {
   1053     let scalar_redaction = if path.ends_with("/envelope_path") || path.ends_with("/socket_path") {
   1054         Some("[redacted-path]")
   1055     } else if path.ends_with("/credential_reference") {
   1056         Some("[redacted-credential-reference]")
   1057     } else if path.ends_with("/expected_public_key") {
   1058         Some("[redacted-public-key]")
   1059     } else if path == "/operations/listen" {
   1060         Some("[redacted-address]")
   1061     } else if path.starts_with("/relays/") && path.ends_with("/id") {
   1062         Some("[redacted-relay-id]")
   1063     } else if (path.starts_with("/relays/") && path.ends_with("/url"))
   1064         || path == "/policy/challenges/url"
   1065     {
   1066         Some("[redacted-url]")
   1067     } else if matches!(
   1068         path,
   1069         "/discovery/domain"
   1070             | "/discovery/handler_identifier"
   1071             | "/discovery/nostrconnect_url_template"
   1072             | "/discovery/metadata/name"
   1073             | "/discovery/metadata/display_name"
   1074             | "/discovery/metadata/about"
   1075             | "/discovery/metadata/website"
   1076             | "/discovery/metadata/picture"
   1077     ) {
   1078         Some("[redacted]")
   1079     } else {
   1080         None
   1081     };
   1082     if let Some(redaction) = scalar_redaction {
   1083         return Value::String(redaction.to_owned());
   1084     }
   1085     if matches!(
   1086         path,
   1087         "/policy/trusted_clients"
   1088             | "/policy/denied_clients"
   1089             | "/discovery/public_relay_ids"
   1090             | "/discovery/publish_relay_ids"
   1091     ) {
   1092         return json!({
   1093             "count": value.as_array().map_or(0, Vec::len),
   1094             "values": "[redacted]"
   1095         });
   1096     }
   1097     value
   1098 }
   1099 
   1100 fn default_entry(path: &str) -> Option<&'static DefaultEntry> {
   1101     DEFAULTS.iter().find(|entry| entry.path == path)
   1102 }
   1103 
   1104 fn toml_path<'a>(root: &'a toml::Value, path: &str) -> Option<&'a toml::Value> {
   1105     path.split('/')
   1106         .filter(|part| !part.is_empty())
   1107         .try_fold(root, |value, part| {
   1108             if let Ok(index) = part.parse::<usize>() {
   1109                 value.as_array()?.get(index)
   1110             } else {
   1111                 value.as_table()?.get(part)
   1112             }
   1113         })
   1114 }
   1115 
   1116 #[cfg(test)]
   1117 mod tests {
   1118     use super::*;
   1119 
   1120     const EXAMPLE: &str = include_str!("../contracts/services_hardening/config.v1.example.toml");
   1121 
   1122     fn parse(source: &str) -> Result<MycConfigDocumentV1, MycConfigV1Error> {
   1123         parse_myc_config_v1(source.as_bytes(), MycConfigProfile::Production)
   1124     }
   1125 
   1126     fn replace(source: &str, old: &str, new: &str) -> String {
   1127         assert!(source.contains(old), "missing fixture fragment: {old}");
   1128         source.replacen(old, new, 1)
   1129     }
   1130 
   1131     #[test]
   1132     fn canonical_example_is_deterministic_and_redacted() {
   1133         let first = parse(EXAMPLE).expect("canonical example");
   1134         let second = parse(EXAMPLE).expect("canonical example again");
   1135         assert_eq!(first.schema(), MYC_CONFIG_SCHEMA);
   1136         assert_eq!(first.schema_version(), MYC_CONFIG_SCHEMA_VERSION);
   1137         assert_eq!(first.profile(), MycConfigProfile::Production);
   1138         assert_eq!(first.relay_count(), 2);
   1139         assert_eq!(first.effective(), second.effective());
   1140         assert!(first.effective().field_count() > 80);
   1141         let output = first.effective().canonical_json();
   1142         assert!(output.starts_with(
   1143             "{\"schema\":\"radroots.myc.effective-config\",\"schema_version\":1,\"fields\":["
   1144         ));
   1145         for forbidden in [
   1146             "/var/lib/radroots",
   1147             "/run/radroots",
   1148             "4444444444444444",
   1149             "7777777777777777",
   1150             "relay-primary.example.test",
   1151             "myc.example.test",
   1152             "transport_wrapping_key",
   1153             "Radroots Myc",
   1154         ] {
   1155             assert!(!output.contains(forbidden), "leaked {forbidden}");
   1156             assert!(!format!("{first:?}").contains(forbidden));
   1157         }
   1158     }
   1159 
   1160     #[test]
   1161     fn exact_document_bound_precedes_parsing() {
   1162         let mut exact = EXAMPLE.as_bytes().to_vec();
   1163         exact.extend_from_slice(b"\n#");
   1164         exact.resize(MYC_CONFIG_DOCUMENT_MAX_UTF8_BYTES, b'a');
   1165         assert!(parse_myc_config_v1(&exact, MycConfigProfile::Production).is_ok());
   1166         exact.push(b'a');
   1167         assert_eq!(
   1168             parse_myc_config_v1(&exact, MycConfigProfile::Production)
   1169                 .unwrap_err()
   1170                 .kind(),
   1171             MycConfigV1ErrorKind::TooLarge
   1172         );
   1173     }
   1174 
   1175     #[test]
   1176     fn invalid_utf8_duplicate_null_unknown_and_malformed_wire_fail() {
   1177         assert_eq!(
   1178             parse_myc_config_v1(&[0xff], MycConfigProfile::Production)
   1179                 .unwrap_err()
   1180                 .kind(),
   1181             MycConfigV1ErrorKind::InvalidUtf8
   1182         );
   1183         for source in [
   1184             format!("schema = \"radroots.myc.config\"\n{EXAMPLE}"),
   1185             replace(
   1186                 EXAMPLE,
   1187                 "shutdown_grace_ms = 30000",
   1188                 "shutdown_grace_ms = null",
   1189             ),
   1190             replace(
   1191                 EXAMPLE,
   1192                 "shutdown_grace_ms = 30000",
   1193                 "shutdown_grace_ms = [null]",
   1194             ),
   1195         ] {
   1196             assert_eq!(
   1197                 parse(&source).unwrap_err().kind(),
   1198                 MycConfigV1ErrorKind::MalformedToml
   1199             );
   1200         }
   1201         let unknown = replace(
   1202             EXAMPLE,
   1203             "shutdown_grace_ms = 30000",
   1204             "shutdown_grace_ms = 30000\nsecret = \"do-not-render\"",
   1205         );
   1206         assert_eq!(
   1207             parse(&unknown).unwrap_err().kind(),
   1208             MycConfigV1ErrorKind::InvalidDocument
   1209         );
   1210         let nested_duplicate = replace(
   1211             EXAMPLE,
   1212             "busy_timeout_ms = 5000",
   1213             "busy_timeout_ms = 5000\nbusy_timeout_ms = 5000",
   1214         );
   1215         assert_eq!(
   1216             parse(&nested_duplicate).unwrap_err().kind(),
   1217             MycConfigV1ErrorKind::MalformedToml
   1218         );
   1219         let mut table = EXAMPLE.parse::<toml::Table>().expect("example table");
   1220         table.remove("resource_limits");
   1221         assert_eq!(
   1222             parse(&toml::to_string(&table).expect("missing-table TOML"))
   1223                 .unwrap_err()
   1224                 .kind(),
   1225             MycConfigV1ErrorKind::InvalidDocument
   1226         );
   1227     }
   1228 
   1229     #[test]
   1230     fn header_failures_are_classified_before_document_admission() {
   1231         for (source, expected) in [
   1232             (
   1233                 EXAMPLE.replace("schema = \"radroots.myc.config\"\n", ""),
   1234                 MycConfigV1ErrorKind::MissingSchema,
   1235             ),
   1236             (
   1237                 replace(EXAMPLE, "schema = \"radroots.myc.config\"", "schema = 1"),
   1238                 MycConfigV1ErrorKind::InvalidSchema,
   1239             ),
   1240             (
   1241                 replace(EXAMPLE, "radroots.myc.config", "radroots.rhi.config"),
   1242                 MycConfigV1ErrorKind::SchemaMismatch,
   1243             ),
   1244             (
   1245                 EXAMPLE.replace("schema_version = 1\n", ""),
   1246                 MycConfigV1ErrorKind::MissingSchemaVersion,
   1247             ),
   1248             (
   1249                 replace(EXAMPLE, "schema_version = 1", "schema_version = \"1\""),
   1250                 MycConfigV1ErrorKind::InvalidSchemaVersion,
   1251             ),
   1252             (
   1253                 replace(EXAMPLE, "schema_version = 1", "schema_version = 2"),
   1254                 MycConfigV1ErrorKind::UnsupportedSchemaVersion,
   1255             ),
   1256         ] {
   1257             assert_eq!(parse(&source).unwrap_err().kind(), expected);
   1258         }
   1259     }
   1260 
   1261     #[test]
   1262     fn production_and_repo_local_relay_postures_are_distinct() {
   1263         let local = replace(
   1264             EXAMPLE,
   1265             "wss://relay-primary.example.test/",
   1266             "ws://127.0.0.1:7777/",
   1267         );
   1268         assert_eq!(
   1269             parse(&local).unwrap_err().kind(),
   1270             MycConfigV1ErrorKind::InvalidRelationship
   1271         );
   1272         assert!(parse_myc_config_v1(local.as_bytes(), MycConfigProfile::RepoLocal).is_ok());
   1273         let remote = replace(&local, "ws://127.0.0.1:7777/", "ws://relay.example.test/");
   1274         assert_eq!(
   1275             parse_myc_config_v1(remote.as_bytes(), MycConfigProfile::RepoLocal)
   1276                 .unwrap_err()
   1277                 .kind(),
   1278             MycConfigV1ErrorKind::InvalidDocument
   1279         );
   1280     }
   1281 
   1282     #[test]
   1283     fn semantic_relationship_failures_are_rejected() {
   1284         let cases = [
   1285             replace(EXAMPLE, "id = \"secondary\"", "id = \"primary\""),
   1286             replace(
   1287                 EXAMPLE,
   1288                 "url = \"wss://relay-secondary.example.test/\"",
   1289                 "url = \"wss://relay-primary.example.test/\"",
   1290             ),
   1291             replace(
   1292                 EXAMPLE,
   1293                 "trusted_clients = [\"7777777777777777777777777777777777777777777777777777777777777777\"]",
   1294                 "trusted_clients = [\"8888888888888888888888888888888888888888888888888888888888888888\"]",
   1295             ),
   1296             replace(
   1297                 EXAMPLE,
   1298                 "expected_public_key = \"3333333333333333333333333333333333333333333333333333333333333333\"",
   1299                 "expected_public_key = \"2222222222222222222222222222222222222222222222222222222222222222\"",
   1300             ),
   1301             replace(
   1302                 EXAMPLE,
   1303                 "allowed_sign_event_kinds = [1]",
   1304                 "allowed_sign_event_kinds = [2]",
   1305             ),
   1306             replace(
   1307                 EXAMPLE,
   1308                 "authorized_lifetime_ms = 3600000",
   1309                 "authorized_lifetime_ms = 1000",
   1310             ),
   1311             replace(EXAMPLE, "retention_ms = 3600000", "retention_ms = 1"),
   1312             replace(
   1313                 EXAMPLE,
   1314                 "initial_backoff_ms = 250",
   1315                 "initial_backoff_ms = 30001",
   1316             ),
   1317             replace(
   1318                 EXAMPLE,
   1319                 "public_relay_ids = [\"primary\", \"secondary\"]",
   1320                 "public_relay_ids = [\"missing\"]",
   1321             ),
   1322             replace(
   1323                 EXAMPLE,
   1324                 "[discovery]\nenabled = true",
   1325                 "[discovery]\nenabled = false",
   1326             ),
   1327         ];
   1328         for source in cases {
   1329             assert!(matches!(
   1330                 parse(&source).unwrap_err().kind(),
   1331                 MycConfigV1ErrorKind::InvalidDocument | MycConfigV1ErrorKind::InvalidRelationship
   1332             ));
   1333         }
   1334     }
   1335 
   1336     #[test]
   1337     fn defaults_have_exact_sources_and_explicit_values_override_them() {
   1338         assert_eq!(DEFAULTS.len(), 41);
   1339         let mut table = EXAMPLE.parse::<toml::Table>().expect("example TOML");
   1340         for entry in DEFAULTS {
   1341             remove_toml_path(&mut table, entry.path);
   1342         }
   1343         let minimal = toml::to_string(&table).expect("minimal TOML");
   1344         let parsed = parse(&minimal).expect("defaults admitted");
   1345         let output = parsed.effective().canonical_json();
   1346         for source in [
   1347             "engineering_safety",
   1348             "accepted_service_authority",
   1349             "radroots_service_sqlite",
   1350             "radroots_service_host",
   1351             "radroots_event",
   1352             "radroots_nostr_connect",
   1353         ] {
   1354             assert!(output.contains(&format!("\"source\":\"{source}\"")));
   1355         }
   1356         let explicit = parse(EXAMPLE).expect("explicit example");
   1357         assert!(
   1358             explicit
   1359                 .effective()
   1360                 .canonical_json()
   1361                 .matches("\"source\":\"document\"")
   1362                 .count()
   1363                 > output.matches("\"source\":\"document\"").count()
   1364         );
   1365     }
   1366 
   1367     #[test]
   1368     fn errors_and_debug_are_source_free() {
   1369         let secret = "credential-secret-value";
   1370         let source = replace(
   1371             EXAMPLE,
   1372             "shutdown_grace_ms = 30000",
   1373             &format!("unknown = \"{secret}\""),
   1374         );
   1375         let failure = parse(&source).unwrap_err();
   1376         let rendered = format!("{failure} {failure:?}");
   1377         assert!(!rendered.contains(secret));
   1378         assert!(Error::source(&failure).is_none());
   1379     }
   1380 
   1381     #[test]
   1382     fn annotated_utf8_limits_are_measured_in_bytes() {
   1383         let exact_name = "é".repeat(64);
   1384         let exact = replace(
   1385             EXAMPLE,
   1386             "name = \"myc\"",
   1387             &format!("name = \"{exact_name}\""),
   1388         );
   1389         assert!(parse(&exact).is_ok());
   1390         let over_name = "é".repeat(65);
   1391         let over = replace(
   1392             EXAMPLE,
   1393             "name = \"myc\"",
   1394             &format!("name = \"{over_name}\""),
   1395         );
   1396         assert_eq!(
   1397             parse(&over).unwrap_err().kind(),
   1398             MycConfigV1ErrorKind::InvalidDocument
   1399         );
   1400     }
   1401 
   1402     fn remove_toml_path(table: &mut toml::Table, pointer: &str) {
   1403         let mut parts = pointer
   1404             .split('/')
   1405             .filter(|part| !part.is_empty())
   1406             .peekable();
   1407         let mut current = table;
   1408         while let Some(part) = parts.next() {
   1409             if parts.peek().is_none() {
   1410                 current.remove(part);
   1411                 return;
   1412             }
   1413             let Some(next) = current.get_mut(part).and_then(toml::Value::as_table_mut) else {
   1414                 return;
   1415             };
   1416             current = next;
   1417         }
   1418     }
   1419 }