commit 875099514f23d10593f7c36969237be44afeda69
parent 34c71f38970de687e1da749c6a88d59ee7cbbd60
Author: triesap <tyson@radroots.org>
Date: Fri, 21 Aug 2026 11:34:02 +0000
contracts: freeze Myc v1 configuration
- bind the complete strict schema, safety defaults, and semantic relationships\n- validate canonical production and repo-local examples plus fail-closed vectors\n- keep Nix and OCI qualification deferred through RCLD-RSHR-170
Diffstat:
8 files changed, 1854 insertions(+), 27 deletions(-)
diff --git a/AGENTS.md b/AGENTS.md
@@ -181,10 +181,11 @@ all-target checking and testing, warnings-denied all-target Clippy, rustdoc with
warnings denied, and diff hygiene. Run any gate not yet covered by the current
release script explicitly; do not describe the script as sufficient until it
enforces the complete contract. Run additional SQLx freshness, source-lock,
-independently produced OCI, systemd, package, SBOM, checksum, notice, and
-fresh-install gates when their surfaces change. Checked-in Nix material remains
-deferred source data through RCLD-RSHR-170 and is not a verification gate. Use
-narrower checked-in commands only for iteration, and never claim a command
+systemd, package, SBOM, checksum, notice, and fresh-install gates when their
+surfaces change. Checked-in Nix material remains deferred source data through
+RCLD-RSHR-170 and is not a verification gate. Every OCI production or
+qualification path is likewise deferred and unclaimed through RCLD-RSHR-170.
+Use narrower checked-in commands only for iteration, and never claim a command
passed unless it ran successfully.
## 10. Commits and irreversible actions
diff --git a/Cargo.lock b/Cargo.lock
@@ -24,6 +24,20 @@ dependencies = [
]
[[package]]
+name = "ahash"
+version = "0.8.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75"
+dependencies = [
+ "cfg-if",
+ "getrandom 0.3.4",
+ "once_cell",
+ "serde",
+ "version_check",
+ "zerocopy",
+]
+
+[[package]]
name = "aho-corasick"
version = "1.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -242,6 +256,21 @@ dependencies = [
]
[[package]]
+name = "bit-set"
+version = "0.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "08807e080ed7f9d5433fa9b275196cfc35414f66a0c79d864dc51a0d825231a3"
+dependencies = [
+ "bit-vec",
+]
+
+[[package]]
+name = "bit-vec"
+version = "0.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7"
+
+[[package]]
name = "bitcoin-io"
version = "0.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -283,12 +312,24 @@ dependencies = [
]
[[package]]
+name = "borrow-or-share"
+version = "0.2.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dc0b364ead1874514c8c2855ab558056ebfeb775653e7ae45ff72f28f8f3166c"
+
+[[package]]
name = "bumpalo"
version = "3.20.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb"
[[package]]
+name = "bytecount"
+version = "0.6.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "175812e0be2bccb6abe50bb8d566126198344f707e304f45c648fd8f2cc0365e"
+
+[[package]]
name = "byteorder"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -391,7 +432,7 @@ dependencies = [
"heck",
"proc-macro2",
"quote",
- "syn",
+ "syn 2.0.117",
]
[[package]]
@@ -584,7 +625,7 @@ checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0"
dependencies = [
"proc-macro2",
"quote",
- "syn",
+ "syn 2.0.117",
]
[[package]]
@@ -620,6 +661,15 @@ dependencies = [
]
[[package]]
+name = "email_address"
+version = "0.2.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e079f19b08ca6239f47f8ba8509c11cf3ea30095831f7fed61441475edd8c449"
+dependencies = [
+ "serde",
+]
+
+[[package]]
name = "equivalent"
version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -647,6 +697,17 @@ dependencies = [
]
[[package]]
+name = "fancy-regex"
+version = "0.18.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e1e1dacd0d2082dfcf1351c4bdd566bbe89a2b263235a2b50058f1e130a47277"
+dependencies = [
+ "bit-set",
+ "regex-automata",
+ "regex-syntax",
+]
+
+[[package]]
name = "fastrand"
version = "2.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -669,6 +730,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582"
[[package]]
+name = "fluent-uri"
+version = "0.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bc74ac4d8359ae70623506d512209619e5cf8f347124910440dbc221714b328e"
+dependencies = [
+ "borrow-or-share",
+ "ref-cast",
+ "serde",
+]
+
+[[package]]
name = "flume"
version = "0.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -716,6 +788,16 @@ dependencies = [
]
[[package]]
+name = "fraction"
+version = "0.15.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e076045bb43dac435333ed5f04caf35c7463631d0dae2deb2638d94dd0a5b872"
+dependencies = [
+ "lazy_static",
+ "num",
+]
+
+[[package]]
name = "futures"
version = "0.3.32"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -781,7 +863,7 @@ checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b"
dependencies = [
"proc-macro2",
"quote",
- "syn",
+ "syn 2.0.117",
]
[[package]]
@@ -844,9 +926,11 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd"
dependencies = [
"cfg-if",
+ "js-sys",
"libc",
"r-efi 5.3.0",
"wasip2",
+ "wasm-bindgen",
]
[[package]]
@@ -906,6 +990,17 @@ dependencies = [
]
[[package]]
+name = "hashbrown"
+version = "0.17.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a"
+dependencies = [
+ "allocator-api2",
+ "equivalent",
+ "foldhash 0.2.0",
+]
+
+[[package]]
name = "hashlink"
version = "0.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1208,6 +1303,58 @@ dependencies = [
]
[[package]]
+name = "jsonschema"
+version = "0.48.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f05a6cff806294a7699d48761d4ae2bdebab7fa2eef05e4a0b83320f0ff46901"
+dependencies = [
+ "ahash",
+ "bytecount",
+ "data-encoding",
+ "email_address",
+ "fancy-regex",
+ "fraction",
+ "getrandom 0.3.4",
+ "idna 1.1.0",
+ "itoa",
+ "jsonschema-regex",
+ "jsonschema-value",
+ "num-cmp",
+ "num-traits",
+ "percent-encoding",
+ "referencing",
+ "regex",
+ "serde",
+ "serde_json",
+ "strum",
+ "unicode-general-category",
+ "uuid-simd",
+]
+
+[[package]]
+name = "jsonschema-regex"
+version = "0.48.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "84708dd3f6c5327478a96f37378adb27deed6a626cf84d19de780d9e8ae84b17"
+dependencies = [
+ "regex-syntax",
+]
+
+[[package]]
+name = "jsonschema-value"
+version = "0.48.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fd8c04ab4ce60e42c4e302d3bd9d184652bb56f212495d2eea23573bcbf72556"
+dependencies = [
+ "ahash",
+ "bytecount",
+ "fraction",
+ "num-cmp",
+ "num-traits",
+ "serde_json",
+]
+
+[[package]]
name = "k256"
version = "0.13.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1344,6 +1491,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79"
[[package]]
+name = "micromap"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c2a86d3146ed3995b5913c414f6664344b9617457320782e64f0bb44afd49d74"
+
+[[package]]
name = "mime"
version = "0.3.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1370,6 +1523,7 @@ dependencies = [
"futures-executor",
"futures-util",
"hex",
+ "jsonschema",
"keyring",
"nostr",
"nostr-sdk",
@@ -1389,6 +1543,7 @@ dependencies = [
"thiserror 2.0.18",
"tokio",
"tokio-tungstenite",
+ "toml",
"tracing",
"tracing-appender",
"tracing-subscriber",
@@ -1492,12 +1647,81 @@ dependencies = [
]
[[package]]
+name = "num"
+version = "0.4.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "35bd024e8b2ff75562e5f34e7f4905839deb4b22955ef5e73d2fea1b9813cb23"
+dependencies = [
+ "num-bigint",
+ "num-complex",
+ "num-integer",
+ "num-iter",
+ "num-rational",
+ "num-traits",
+]
+
+[[package]]
+name = "num-bigint"
+version = "0.4.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c89e69e7e0f03bea5ef08013795c25018e101932225a656383bd384495ecc367"
+dependencies = [
+ "num-integer",
+ "num-traits",
+]
+
+[[package]]
+name = "num-cmp"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "63335b2e2c34fae2fb0aa2cecfd9f0832a1e24b3b32ecec612c3426d46dc8aaa"
+
+[[package]]
+name = "num-complex"
+version = "0.4.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "73f88a1307638156682bada9d7604135552957b7818057dcef22705b4d509495"
+dependencies = [
+ "num-traits",
+]
+
+[[package]]
name = "num-conv"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c6673768db2d862beb9b39a78fdcb1a69439615d5794a1be50caa9bc92c81967"
[[package]]
+name = "num-integer"
+version = "0.1.47"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7ce2d95d4b3734dc35aa2f45e1aa22cd416814592a4f9d9205e11affd5b8e10b"
+dependencies = [
+ "num-traits",
+]
+
+[[package]]
+name = "num-iter"
+version = "0.1.46"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c92800bd69a1eac91786bcfe9da64a897eb72911b8dc3095decbd07429e8048b"
+dependencies = [
+ "num-integer",
+ "num-traits",
+]
+
+[[package]]
+name = "num-rational"
+version = "0.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f83d14da390562dca69fc84082e73e548e1ad308d24accdedd2720017cb37824"
+dependencies = [
+ "num-bigint",
+ "num-integer",
+ "num-traits",
+]
+
+[[package]]
name = "num-traits"
version = "0.2.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1547,7 +1771,7 @@ checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c"
dependencies = [
"proc-macro2",
"quote",
- "syn",
+ "syn 2.0.117",
]
[[package]]
@@ -1573,6 +1797,12 @@ dependencies = [
]
[[package]]
+name = "outref"
+version = "0.5.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1a80800c0488c3a21695ea981a54918fbb37abf04f4d0720c453632255e2ff0e"
+
+[[package]]
name = "parking"
version = "2.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1682,7 +1912,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b"
dependencies = [
"proc-macro2",
- "syn",
+ "syn 2.0.117",
]
[[package]]
@@ -1915,6 +2145,55 @@ dependencies = [
]
[[package]]
+name = "ref-cast"
+version = "1.0.27"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7e440fb4e4b4147295338efb76001ab9e4efc0e5839df2c47fc5ac2381d365c3"
+dependencies = [
+ "ref-cast-impl",
+]
+
+[[package]]
+name = "ref-cast-impl"
+version = "1.0.27"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "92ecd8964f8453721699a1ed72037b0db49ce2f5a5138486ee89bed6f67cdf3a"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 3.0.3",
+]
+
+[[package]]
+name = "referencing"
+version = "0.48.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "980e77f5cfffc592c0e7dfb6d20770a0b96a6ab94d403de7454ed9799505e4ba"
+dependencies = [
+ "ahash",
+ "fluent-uri",
+ "getrandom 0.3.4",
+ "hashbrown 0.17.1",
+ "itoa",
+ "micromap",
+ "parking_lot",
+ "percent-encoding",
+ "serde_json",
+]
+
+[[package]]
+name = "regex"
+version = "1.12.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276"
+dependencies = [
+ "aho-corasick",
+ "memchr",
+ "regex-automata",
+ "regex-syntax",
+]
+
+[[package]]
name = "regex-automata"
version = "0.4.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -2139,7 +2418,7 @@ checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79"
dependencies = [
"proc-macro2",
"quote",
- "syn",
+ "syn 2.0.117",
]
[[package]]
@@ -2167,6 +2446,15 @@ dependencies = [
]
[[package]]
+name = "serde_spanned"
+version = "0.6.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3"
+dependencies = [
+ "serde",
+]
+
+[[package]]
name = "serial_test"
version = "3.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -2188,7 +2476,7 @@ checksum = "94e153fc76e1c6a068703d6d29c508a0b15c061c4b7e43da59cc097bc342673c"
dependencies = [
"proc-macro2",
"quote",
- "syn",
+ "syn 2.0.117",
]
[[package]]
@@ -2321,7 +2609,7 @@ dependencies = [
"quote",
"sqlx-core",
"sqlx-macros-core",
- "syn",
+ "syn 2.0.117",
]
[[package]]
@@ -2342,7 +2630,7 @@ dependencies = [
"sha2",
"sqlx-core",
"sqlx-sqlite",
- "syn",
+ "syn 2.0.117",
"url",
]
@@ -2383,6 +2671,27 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
[[package]]
+name = "strum"
+version = "0.28.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9628de9b8791db39ceda2b119bbe13134770b56c138ec1d3af810d045c04f9bd"
+dependencies = [
+ "strum_macros",
+]
+
+[[package]]
+name = "strum_macros"
+version = "0.28.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ab85eea0270ee17587ed4156089e10b9e6880ee688791d45a905f5b1ca36f664"
+dependencies = [
+ "heck",
+ "proc-macro2",
+ "quote",
+ "syn 2.0.117",
+]
+
+[[package]]
name = "subtle"
version = "2.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -2400,6 +2709,17 @@ dependencies = [
]
[[package]]
+name = "syn"
+version = "3.0.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "53e9bae58849f64dfa4f5d5ae372c8341f7305f82a3868709269343628b659a3"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "unicode-ident",
+]
+
+[[package]]
name = "sync_wrapper"
version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -2413,7 +2733,7 @@ checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2"
dependencies = [
"proc-macro2",
"quote",
- "syn",
+ "syn 2.0.117",
]
[[package]]
@@ -2455,7 +2775,7 @@ checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1"
dependencies = [
"proc-macro2",
"quote",
- "syn",
+ "syn 2.0.117",
]
[[package]]
@@ -2466,7 +2786,7 @@ checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5"
dependencies = [
"proc-macro2",
"quote",
- "syn",
+ "syn 2.0.117",
]
[[package]]
@@ -2558,7 +2878,7 @@ checksum = "385a6cb71ab9ab790c5fe8d67f1645e6c450a7ce006a33de03daa956cf70a496"
dependencies = [
"proc-macro2",
"quote",
- "syn",
+ "syn 2.0.117",
]
[[package]]
@@ -2600,6 +2920,47 @@ dependencies = [
]
[[package]]
+name = "toml"
+version = "0.8.23"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dc1beb996b9d83529a9e75c17a1686767d148d70663143c7854d8b4a09ced362"
+dependencies = [
+ "serde",
+ "serde_spanned",
+ "toml_datetime",
+ "toml_edit",
+]
+
+[[package]]
+name = "toml_datetime"
+version = "0.6.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c"
+dependencies = [
+ "serde",
+]
+
+[[package]]
+name = "toml_edit"
+version = "0.22.27"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a"
+dependencies = [
+ "indexmap",
+ "serde",
+ "serde_spanned",
+ "toml_datetime",
+ "toml_write",
+ "winnow",
+]
+
+[[package]]
+name = "toml_write"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801"
+
+[[package]]
name = "tower"
version = "0.5.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -2658,7 +3019,7 @@ checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da"
dependencies = [
"proc-macro2",
"quote",
- "syn",
+ "syn 2.0.117",
]
[[package]]
@@ -2830,6 +3191,16 @@ dependencies = [
]
[[package]]
+name = "uuid-simd"
+version = "0.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "23b082222b4f6619906941c17eb2297fff4c2fb96cb60164170522942a200bd8"
+dependencies = [
+ "outref",
+ "vsimd",
+]
+
+[[package]]
name = "valuable"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -2848,6 +3219,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
[[package]]
+name = "vsimd"
+version = "0.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5c3082ca00d5a5ef149bb8b555a72ae84c9c59f7250f013ac822ac2e49b19c64"
+
+[[package]]
name = "wasi"
version = "0.11.1+wasi-snapshot-preview1"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -2914,7 +3291,7 @@ dependencies = [
"bumpalo",
"proc-macro2",
"quote",
- "syn",
+ "syn 2.0.117",
"wasm-bindgen-shared",
]
@@ -3161,6 +3538,15 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650"
[[package]]
+name = "winnow"
+version = "0.7.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "df79d97927682d2fd8adb29682d1140b343be4ac0f08fd68b7765d9c059d3945"
+dependencies = [
+ "memchr",
+]
+
+[[package]]
name = "wit-bindgen"
version = "0.51.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -3190,7 +3576,7 @@ dependencies = [
"heck",
"indexmap",
"prettyplease",
- "syn",
+ "syn 2.0.117",
"wasm-metadata",
"wit-bindgen-core",
"wit-component",
@@ -3206,7 +3592,7 @@ dependencies = [
"prettyplease",
"proc-macro2",
"quote",
- "syn",
+ "syn 2.0.117",
"wit-bindgen-core",
"wit-bindgen-rust",
]
@@ -3273,7 +3659,7 @@ checksum = "de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e"
dependencies = [
"proc-macro2",
"quote",
- "syn",
+ "syn 2.0.117",
"synstructure",
]
@@ -3294,7 +3680,7 @@ checksum = "70e3cd084b1788766f53af483dd21f93881ff30d7320490ec3ef7526d203bad4"
dependencies = [
"proc-macro2",
"quote",
- "syn",
+ "syn 2.0.117",
]
[[package]]
@@ -3314,7 +3700,7 @@ checksum = "11532158c46691caf0f2593ea8358fed6bbf68a0315e80aae9bd41fbade684a1"
dependencies = [
"proc-macro2",
"quote",
- "syn",
+ "syn 2.0.117",
"synstructure",
]
@@ -3335,7 +3721,7 @@ checksum = "85a5b4158499876c763cb03bc4e49185d3cccbabb15b33c627f7884f43db852e"
dependencies = [
"proc-macro2",
"quote",
- "syn",
+ "syn 2.0.117",
]
[[package]]
@@ -3368,7 +3754,7 @@ checksum = "625dc425cab0dca6dc3c3319506e6593dcb08a9f387ea3b284dbd52a92c40555"
dependencies = [
"proc-macro2",
"quote",
- "syn",
+ "syn 2.0.117",
]
[[package]]
diff --git a/Cargo.toml b/Cargo.toml
@@ -61,5 +61,7 @@ zeroize = "1.8"
[dev-dependencies]
futures-util = "0.3.32"
+jsonschema = { version = "0.48.1", default-features = false }
serial_test = "3"
tokio-tungstenite = "0.26.2"
+toml = "0.8"
diff --git a/README b/README
@@ -3,6 +3,20 @@
This is the README for `myc` which provides a Nostr remote signer for
standalone and application-embedded clients.
+## Hardened v1 configuration contract
+
+The target service configuration is frozen by
+`contracts/services_hardening/config.v1.schema.json` and the canonical
+non-secret `config.v1.example.toml`. It is one strict, immutable TOML document
+with explicit identity, relay, authorization, rate, and discovery authority.
+Only reviewed bounded operational leaves have defaults. Bootstrap profile,
+instance, repo-local root, and config-path selection are CLI concerns and are
+not document fields.
+
+The current runtime loader remains transitional until its ordered replacement
+steps are complete. Existing environment variables and `.env.example` are
+prototype evidence, not authority to change or weaken the v1 contract.
+
## NIP-46 runtime contract
Myc listens for encrypted kind-24133 requests on the ordered relay set in
diff --git a/contracts/services_hardening/config.v1.example.toml b/contracts/services_hardening/config.v1.example.toml
@@ -0,0 +1,156 @@
+schema = "radroots.myc.config"
+schema_version = 1
+
+[service]
+shutdown_grace_ms = 30000
+
+[logging]
+level = "info"
+format = "json"
+
+[operations]
+enabled = false
+
+[database]
+busy_timeout_ms = 5000
+max_connections = 8
+minimum_free_bytes = 268435456
+
+[identity.transport]
+provider = "encrypted_file"
+envelope_path = "/var/lib/radroots/services/myc/primary/secrets/transport.identity.ncrypt"
+credential_reference = "transport_wrapping_key"
+expected_public_key = "1111111111111111111111111111111111111111111111111111111111111111"
+
+[identity.user]
+provider = "local_signer"
+socket_path = "/run/radroots/services/myc/primary/user-signer.sock"
+request_deadline_ms = 15000
+request_max_bytes = 65536
+response_max_bytes = 1048576
+concurrency = 32
+expected_public_key = "2222222222222222222222222222222222222222222222222222222222222222"
+
+[identity.discovery]
+enabled = true
+
+[identity.discovery.binding]
+provider = "encrypted_file"
+envelope_path = "/var/lib/radroots/services/myc/primary/secrets/discovery.identity.ncrypt"
+credential_reference = "discovery_wrapping_key"
+expected_public_key = "3333333333333333333333333333333333333333333333333333333333333333"
+
+[[relays]]
+id = "primary"
+url = "wss://relay-primary.example.test/"
+read = true
+write = true
+required = true
+authentication = "required"
+
+[[relays]]
+id = "secondary"
+url = "wss://relay-secondary.example.test/"
+read = true
+write = true
+required = true
+authentication = "required"
+
+[transport]
+connect_deadline_ms = 10000
+
+[transport.delivery_policy]
+mode = "all_required"
+
+[transport.publish_retry]
+max_attempts = 5
+initial_backoff_ms = 250
+maximum_backoff_ms = 30000
+attempt_deadline_ms = 15000
+
+[policy]
+connection_approval = "explicit_user"
+trusted_clients = ["aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"]
+denied_clients = ["bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"]
+permission_ceiling = ["nip04_decrypt", "nip04_encrypt", "nip44_decrypt", "nip44_encrypt", "sign_event:kind:1"]
+allowed_sign_event_kinds = [1]
+
+[policy.challenges]
+enabled = true
+url = "https://myc.example.test/auth/challenge"
+pending_lifetime_ms = 900000
+authorized_lifetime_ms = 3600000
+
+[policy.retention]
+terminal_connections_ms = 604800000
+terminal_challenges_ms = 86400000
+request_dedup_ms = 604800000
+audit_ms = 2592000000
+completed_outbox_ms = 604800000
+
+[rate_limits.connection_admission]
+scope = "global_and_relay"
+window_ms = 60000
+max_attempts = 10
+retention_ms = 3600000
+maximum_tracked_subjects = 4096
+
+[rate_limits.challenge_creation]
+scope = "connection"
+window_ms = 120000
+max_attempts = 5
+retention_ms = 86400000
+maximum_tracked_subjects = 16384
+
+[rate_limits.challenge_authorization]
+scope = "connection"
+window_ms = 120000
+max_attempts = 5
+retention_ms = 86400000
+maximum_tracked_subjects = 16384
+
+[resource_limits.admin]
+header_count = 32
+header_bytes = 16384
+request_body_utf8_bytes = 65536
+response_body_utf8_bytes = 1048576
+concurrent_connections = 32
+request_deadline_ms = 15000
+idle_timeout_ms = 30000
+query_items = 100
+
+[resource_limits.events]
+wire_bytes = 262144
+content_bytes = 131072
+tag_count = 1024
+tag_total_elements = 4096
+tag_element_bytes = 4096
+tag_total_bytes = 131072
+decrypted_plaintext_bytes = 262144
+
+[resource_limits.queues]
+ingress = 1024
+provider = 64
+outbox = 4096
+discovery = 64
+
+[resource_limits.metrics]
+descriptors = 64
+samples = 512
+labels_per_sample = 8
+render_utf8_bytes = 1048576
+
+[discovery]
+enabled = true
+domain = "myc.example.test"
+handler_identifier = "myc"
+public_relay_ids = ["primary", "secondary"]
+publish_relay_ids = ["primary", "secondary"]
+nostrconnect_url_template = "https://myc.example.test/connect?uri=<nostrconnect>"
+
+[discovery.metadata]
+name = "myc"
+display_name = "Radroots Myc"
+about = "NIP-46 signer"
+website = "https://myc.example.test/"
+picture = "https://myc.example.test/myc.png"
diff --git a/contracts/services_hardening/config.v1.schema.json b/contracts/services_hardening/config.v1.schema.json
@@ -0,0 +1,508 @@
+{
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
+ "$id": "https://github.com/radrootslabs/myc/contracts/services_hardening/config.v1.schema.json",
+ "title": "Radroots Myc configuration v1",
+ "description": "Strict, immutable, non-secret Myc service configuration.",
+ "type": "object",
+ "additionalProperties": false,
+ "required": [
+ "schema",
+ "schema_version",
+ "service",
+ "logging",
+ "operations",
+ "database",
+ "identity",
+ "relays",
+ "transport",
+ "policy",
+ "rate_limits",
+ "resource_limits",
+ "discovery"
+ ],
+ "properties": {
+ "schema": { "const": "radroots.myc.config" },
+ "schema_version": { "const": 1 },
+ "service": { "$ref": "#/$defs/service" },
+ "logging": { "$ref": "#/$defs/logging" },
+ "operations": { "$ref": "#/$defs/operations" },
+ "database": { "$ref": "#/$defs/database" },
+ "identity": { "$ref": "#/$defs/identity" },
+ "relays": {
+ "type": "array",
+ "minItems": 1,
+ "maxItems": 32,
+ "items": { "$ref": "#/$defs/relay" }
+ },
+ "transport": { "$ref": "#/$defs/transport" },
+ "policy": { "$ref": "#/$defs/policy" },
+ "rate_limits": { "$ref": "#/$defs/rate_limits" },
+ "resource_limits": { "$ref": "#/$defs/resource_limits" },
+ "discovery": { "$ref": "#/$defs/discovery" }
+ },
+ "$defs": {
+ "absolute_path": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 4096,
+ "x-radroots-maximum-utf8-bytes": 4096,
+ "pattern": "^/"
+ },
+ "bounded_id": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 128,
+ "x-radroots-maximum-utf8-bytes": 128,
+ "pattern": "^[a-z][a-z0-9]*(?:_[a-z0-9]+)*$"
+ },
+ "relay_id": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 64,
+ "x-radroots-maximum-utf8-bytes": 64,
+ "pattern": "^[a-z][a-z0-9]*(?:_[a-z0-9]+)*$"
+ },
+ "credential_reference": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 128,
+ "x-radroots-maximum-utf8-bytes": 128,
+ "pattern": "^[a-z0-9](?:[a-z0-9._-]{0,126}[a-z0-9])?$"
+ },
+ "public_key": {
+ "type": "string",
+ "pattern": "^[0-9a-f]{64}$"
+ },
+ "relay_url": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 2048,
+ "x-radroots-maximum-utf8-bytes": 2048,
+ "pattern": "^(wss://|ws://(localhost|127\\.0\\.0\\.1|\\[::1\\])(?::[1-9][0-9]{0,4})?(?:/|$))"
+ },
+ "https_url": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 2048,
+ "x-radroots-maximum-utf8-bytes": 2048,
+ "pattern": "^https://"
+ },
+ "nostrconnect_url_template": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 2048,
+ "x-radroots-maximum-utf8-bytes": 2048,
+ "pattern": "^https://[^ ]*<nostrconnect>[^ ]*$"
+ },
+ "service": {
+ "type": "object",
+ "additionalProperties": false,
+ "properties": {
+ "shutdown_grace_ms": {
+ "type": "integer",
+ "minimum": 1,
+ "maximum": 300000,
+ "default": 30000,
+ "x-radroots-default-source": "engineering_safety"
+ }
+ }
+ },
+ "logging": {
+ "type": "object",
+ "additionalProperties": false,
+ "properties": {
+ "level": {
+ "type": "string",
+ "enum": ["trace", "debug", "info", "warn", "error"],
+ "default": "info",
+ "x-radroots-default-source": "engineering_safety"
+ },
+ "format": {
+ "const": "json",
+ "default": "json",
+ "x-radroots-default-source": "accepted_service_authority"
+ }
+ }
+ },
+ "operations_limits": {
+ "type": "object",
+ "additionalProperties": false,
+ "properties": {
+ "header_count": { "type": "integer", "minimum": 1, "maximum": 64, "default": 32, "x-radroots-default-source": "radroots_service_host" },
+ "header_bytes": { "type": "integer", "minimum": 1, "maximum": 32768, "default": 16384, "x-radroots-default-source": "radroots_service_host" },
+ "response_body_utf8_bytes": { "type": "integer", "minimum": 1, "maximum": 1048576, "default": 1048576, "x-radroots-default-source": "radroots_service_host" },
+ "concurrent_connections": { "type": "integer", "minimum": 1, "maximum": 64, "default": 32, "x-radroots-default-source": "radroots_service_host" },
+ "request_deadline_ms": { "type": "integer", "minimum": 1, "maximum": 30000, "default": 15000, "x-radroots-default-source": "radroots_service_host" },
+ "idle_timeout_ms": { "type": "integer", "minimum": 1, "maximum": 60000, "default": 30000, "x-radroots-default-source": "radroots_service_host" }
+ }
+ },
+ "operations": {
+ "oneOf": [
+ {
+ "type": "object",
+ "additionalProperties": false,
+ "required": ["enabled"],
+ "properties": { "enabled": { "const": false } }
+ },
+ {
+ "type": "object",
+ "additionalProperties": false,
+ "required": ["enabled", "listen", "bind_policy"],
+ "properties": {
+ "enabled": { "const": true },
+ "listen": { "type": "string", "minLength": 1, "maxLength": 128, "x-radroots-maximum-utf8-bytes": 128 },
+ "bind_policy": { "type": "string", "enum": ["loopback_only", "public"] },
+ "limits": { "$ref": "#/$defs/operations_limits" }
+ }
+ }
+ ]
+ },
+ "database": {
+ "type": "object",
+ "additionalProperties": false,
+ "required": ["minimum_free_bytes"],
+ "properties": {
+ "busy_timeout_ms": { "type": "integer", "minimum": 1, "maximum": 60000, "default": 5000, "x-radroots-default-source": "radroots_service_sqlite" },
+ "max_connections": { "type": "integer", "minimum": 1, "maximum": 8, "default": 8, "x-radroots-default-source": "radroots_service_sqlite" },
+ "minimum_free_bytes": { "type": "integer", "minimum": 1, "maximum": 9223372036854775807 }
+ }
+ },
+ "encrypted_file_provider": {
+ "type": "object",
+ "additionalProperties": false,
+ "required": ["provider", "envelope_path", "credential_reference", "expected_public_key"],
+ "properties": {
+ "provider": { "const": "encrypted_file" },
+ "envelope_path": { "$ref": "#/$defs/absolute_path" },
+ "credential_reference": { "$ref": "#/$defs/credential_reference" },
+ "expected_public_key": { "$ref": "#/$defs/public_key" }
+ }
+ },
+ "local_signer_provider": {
+ "type": "object",
+ "additionalProperties": false,
+ "required": ["provider", "socket_path", "request_deadline_ms", "request_max_bytes", "response_max_bytes", "concurrency", "expected_public_key"],
+ "properties": {
+ "provider": { "const": "local_signer" },
+ "socket_path": { "$ref": "#/$defs/absolute_path" },
+ "request_deadline_ms": { "type": "integer", "minimum": 1, "maximum": 30000 },
+ "request_max_bytes": { "type": "integer", "minimum": 1, "maximum": 65536 },
+ "response_max_bytes": { "type": "integer", "minimum": 1, "maximum": 1048576 },
+ "concurrency": { "type": "integer", "minimum": 1, "maximum": 64 },
+ "expected_public_key": { "$ref": "#/$defs/public_key" }
+ }
+ },
+ "provider": {
+ "oneOf": [
+ { "$ref": "#/$defs/encrypted_file_provider" },
+ { "$ref": "#/$defs/local_signer_provider" }
+ ]
+ },
+ "discovery_identity": {
+ "oneOf": [
+ {
+ "type": "object",
+ "additionalProperties": false,
+ "required": ["enabled"],
+ "properties": { "enabled": { "const": false } }
+ },
+ {
+ "type": "object",
+ "additionalProperties": false,
+ "required": ["enabled", "binding"],
+ "properties": {
+ "enabled": { "const": true },
+ "binding": { "$ref": "#/$defs/provider" }
+ }
+ }
+ ]
+ },
+ "identity": {
+ "type": "object",
+ "additionalProperties": false,
+ "required": ["transport", "user", "discovery"],
+ "properties": {
+ "transport": { "$ref": "#/$defs/provider" },
+ "user": { "$ref": "#/$defs/provider" },
+ "discovery": { "$ref": "#/$defs/discovery_identity" }
+ }
+ },
+ "relay": {
+ "type": "object",
+ "additionalProperties": false,
+ "required": ["id", "url", "read", "write", "required", "authentication"],
+ "properties": {
+ "id": { "$ref": "#/$defs/relay_id" },
+ "url": { "$ref": "#/$defs/relay_url" },
+ "read": { "type": "boolean" },
+ "write": { "type": "boolean" },
+ "required": { "type": "boolean" },
+ "authentication": { "type": "string", "enum": ["disabled", "required"] }
+ }
+ },
+ "delivery_policy": {
+ "oneOf": [
+ {
+ "type": "object",
+ "additionalProperties": false,
+ "required": ["mode"],
+ "properties": { "mode": { "enum": ["all_required", "at_least_one_required"] } }
+ },
+ {
+ "type": "object",
+ "additionalProperties": false,
+ "required": ["mode", "required_acknowledgements"],
+ "properties": {
+ "mode": { "const": "required_quorum" },
+ "required_acknowledgements": { "type": "integer", "minimum": 1, "maximum": 32 }
+ }
+ }
+ ]
+ },
+ "publish_retry": {
+ "type": "object",
+ "additionalProperties": false,
+ "properties": {
+ "max_attempts": { "type": "integer", "minimum": 1, "maximum": 32, "default": 5, "x-radroots-default-source": "engineering_safety" },
+ "initial_backoff_ms": { "type": "integer", "minimum": 1, "maximum": 30000, "default": 250, "x-radroots-default-source": "engineering_safety" },
+ "maximum_backoff_ms": { "type": "integer", "minimum": 1, "maximum": 300000, "default": 30000, "x-radroots-default-source": "engineering_safety" },
+ "attempt_deadline_ms": { "type": "integer", "minimum": 1, "maximum": 30000, "default": 15000, "x-radroots-default-source": "radroots_service_host" }
+ }
+ },
+ "transport": {
+ "type": "object",
+ "additionalProperties": false,
+ "required": ["delivery_policy"],
+ "properties": {
+ "connect_deadline_ms": { "type": "integer", "minimum": 1, "maximum": 30000, "default": 10000, "x-radroots-default-source": "engineering_safety" },
+ "delivery_policy": { "$ref": "#/$defs/delivery_policy" },
+ "publish_retry": { "$ref": "#/$defs/publish_retry" }
+ }
+ },
+ "permission": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 64,
+ "x-radroots-maximum-utf8-bytes": 64,
+ "pattern": "^(nip04_decrypt|nip04_encrypt|nip44_decrypt|nip44_encrypt|switch_relays|sign_event:kind:(0|[1-9][0-9]{0,9}))$"
+ },
+ "challenges": {
+ "oneOf": [
+ {
+ "type": "object",
+ "additionalProperties": false,
+ "required": ["enabled"],
+ "properties": { "enabled": { "const": false } }
+ },
+ {
+ "type": "object",
+ "additionalProperties": false,
+ "required": ["enabled", "url", "pending_lifetime_ms", "authorized_lifetime_ms"],
+ "properties": {
+ "enabled": { "const": true },
+ "url": { "$ref": "#/$defs/https_url" },
+ "pending_lifetime_ms": { "type": "integer", "minimum": 1000, "maximum": 86400000 },
+ "authorized_lifetime_ms": { "type": "integer", "minimum": 1000, "maximum": 2592000000 }
+ }
+ }
+ ]
+ },
+ "retention": {
+ "type": "object",
+ "additionalProperties": false,
+ "required": ["terminal_connections_ms", "terminal_challenges_ms", "request_dedup_ms", "audit_ms", "completed_outbox_ms"],
+ "properties": {
+ "terminal_connections_ms": { "type": "integer", "minimum": 1, "maximum": 31536000000 },
+ "terminal_challenges_ms": { "type": "integer", "minimum": 1, "maximum": 31536000000 },
+ "request_dedup_ms": { "type": "integer", "minimum": 1, "maximum": 31536000000 },
+ "audit_ms": { "type": "integer", "minimum": 1, "maximum": 31536000000 },
+ "completed_outbox_ms": { "type": "integer", "minimum": 1, "maximum": 31536000000 }
+ }
+ },
+ "policy": {
+ "type": "object",
+ "additionalProperties": false,
+ "required": ["connection_approval", "trusted_clients", "denied_clients", "permission_ceiling", "allowed_sign_event_kinds", "challenges", "retention"],
+ "properties": {
+ "connection_approval": { "const": "explicit_user" },
+ "trusted_clients": { "type": "array", "maxItems": 1024, "uniqueItems": true, "items": { "$ref": "#/$defs/public_key" } },
+ "denied_clients": { "type": "array", "maxItems": 1024, "uniqueItems": true, "items": { "$ref": "#/$defs/public_key" } },
+ "permission_ceiling": { "type": "array", "minItems": 1, "maxItems": 64, "uniqueItems": true, "items": { "$ref": "#/$defs/permission" } },
+ "allowed_sign_event_kinds": { "type": "array", "minItems": 1, "maxItems": 64, "uniqueItems": true, "items": { "type": "integer", "minimum": 0, "maximum": 4294967295 } },
+ "challenges": { "$ref": "#/$defs/challenges" },
+ "retention": { "$ref": "#/$defs/retention" }
+ }
+ },
+ "rate_limit": {
+ "type": "object",
+ "additionalProperties": false,
+ "required": ["scope", "window_ms", "max_attempts", "retention_ms", "maximum_tracked_subjects"],
+ "properties": {
+ "scope": { "type": "string", "enum": ["global_and_relay", "connection"] },
+ "window_ms": { "type": "integer", "minimum": 1, "maximum": 86400000 },
+ "max_attempts": { "type": "integer", "minimum": 1, "maximum": 10000 },
+ "retention_ms": { "type": "integer", "minimum": 1, "maximum": 2592000000 },
+ "maximum_tracked_subjects": { "type": "integer", "minimum": 1, "maximum": 65536 }
+ }
+ },
+ "rate_limits": {
+ "type": "object",
+ "additionalProperties": false,
+ "required": ["connection_admission", "challenge_creation", "challenge_authorization"],
+ "properties": {
+ "connection_admission": { "$ref": "#/$defs/rate_limit" },
+ "challenge_creation": { "$ref": "#/$defs/rate_limit" },
+ "challenge_authorization": { "$ref": "#/$defs/rate_limit" }
+ }
+ },
+ "admin_limits": {
+ "type": "object",
+ "additionalProperties": false,
+ "properties": {
+ "header_count": { "type": "integer", "minimum": 1, "maximum": 64, "default": 32, "x-radroots-default-source": "radroots_service_host" },
+ "header_bytes": { "type": "integer", "minimum": 1, "maximum": 32768, "default": 16384, "x-radroots-default-source": "radroots_service_host" },
+ "request_body_utf8_bytes": { "type": "integer", "minimum": 1, "maximum": 65536, "default": 65536, "x-radroots-default-source": "radroots_service_host" },
+ "response_body_utf8_bytes": { "type": "integer", "minimum": 1, "maximum": 1048576, "default": 1048576, "x-radroots-default-source": "radroots_service_host" },
+ "concurrent_connections": { "type": "integer", "minimum": 1, "maximum": 64, "default": 32, "x-radroots-default-source": "radroots_service_host" },
+ "request_deadline_ms": { "type": "integer", "minimum": 1, "maximum": 30000, "default": 15000, "x-radroots-default-source": "radroots_service_host" },
+ "idle_timeout_ms": { "type": "integer", "minimum": 1, "maximum": 60000, "default": 30000, "x-radroots-default-source": "radroots_service_host" },
+ "query_items": { "type": "integer", "minimum": 1, "maximum": 200, "default": 100, "x-radroots-default-source": "radroots_service_host" }
+ }
+ },
+ "event_limits": {
+ "type": "object",
+ "additionalProperties": false,
+ "properties": {
+ "wire_bytes": { "type": "integer", "minimum": 1, "maximum": 524288, "default": 262144, "x-radroots-default-source": "radroots_event" },
+ "content_bytes": { "type": "integer", "minimum": 1, "maximum": 131072, "default": 131072, "x-radroots-default-source": "radroots_event" },
+ "tag_count": { "type": "integer", "minimum": 1, "maximum": 1024, "default": 1024, "x-radroots-default-source": "radroots_event" },
+ "tag_total_elements": { "type": "integer", "minimum": 1, "maximum": 4096, "default": 4096, "x-radroots-default-source": "radroots_event" },
+ "tag_element_bytes": { "type": "integer", "minimum": 1, "maximum": 4096, "default": 4096, "x-radroots-default-source": "radroots_event" },
+ "tag_total_bytes": { "type": "integer", "minimum": 1, "maximum": 131072, "default": 131072, "x-radroots-default-source": "radroots_event" },
+ "decrypted_plaintext_bytes": { "type": "integer", "minimum": 1, "maximum": 262144, "default": 262144, "x-radroots-default-source": "radroots_nostr_connect" }
+ }
+ },
+ "queue_limits": {
+ "type": "object",
+ "additionalProperties": false,
+ "properties": {
+ "ingress": { "type": "integer", "minimum": 1, "maximum": 65536, "default": 1024, "x-radroots-default-source": "engineering_safety" },
+ "provider": { "type": "integer", "minimum": 1, "maximum": 1024, "default": 64, "x-radroots-default-source": "engineering_safety" },
+ "outbox": { "type": "integer", "minimum": 1, "maximum": 65536, "default": 4096, "x-radroots-default-source": "engineering_safety" },
+ "discovery": { "type": "integer", "minimum": 1, "maximum": 1024, "default": 64, "x-radroots-default-source": "engineering_safety" }
+ }
+ },
+ "metrics_limits": {
+ "type": "object",
+ "additionalProperties": false,
+ "properties": {
+ "descriptors": { "type": "integer", "minimum": 1, "maximum": 64, "default": 64, "x-radroots-default-source": "radroots_service_host" },
+ "samples": { "type": "integer", "minimum": 1, "maximum": 512, "default": 512, "x-radroots-default-source": "radroots_service_host" },
+ "labels_per_sample": { "type": "integer", "minimum": 1, "maximum": 8, "default": 8, "x-radroots-default-source": "radroots_service_host" },
+ "render_utf8_bytes": { "type": "integer", "minimum": 1, "maximum": 1048576, "default": 1048576, "x-radroots-default-source": "radroots_service_host" }
+ }
+ },
+ "resource_limits": {
+ "type": "object",
+ "additionalProperties": false,
+ "properties": {
+ "admin": { "$ref": "#/$defs/admin_limits" },
+ "events": { "$ref": "#/$defs/event_limits" },
+ "queues": { "$ref": "#/$defs/queue_limits" },
+ "metrics": { "$ref": "#/$defs/metrics_limits" }
+ }
+ },
+ "discovery_metadata": {
+ "type": "object",
+ "additionalProperties": false,
+ "required": ["name", "display_name", "about", "website", "picture"],
+ "properties": {
+ "name": { "type": "string", "minLength": 1, "maxLength": 128, "x-radroots-maximum-utf8-bytes": 128 },
+ "display_name": { "type": "string", "minLength": 1, "maxLength": 128, "x-radroots-maximum-utf8-bytes": 128 },
+ "about": { "type": "string", "maxLength": 1024, "x-radroots-maximum-utf8-bytes": 1024 },
+ "website": { "$ref": "#/$defs/https_url" },
+ "picture": { "$ref": "#/$defs/https_url" }
+ }
+ },
+ "discovery": {
+ "oneOf": [
+ {
+ "type": "object",
+ "additionalProperties": false,
+ "required": ["enabled"],
+ "properties": { "enabled": { "const": false } }
+ },
+ {
+ "type": "object",
+ "additionalProperties": false,
+ "required": ["enabled", "domain", "handler_identifier", "public_relay_ids", "publish_relay_ids", "nostrconnect_url_template", "metadata"],
+ "properties": {
+ "enabled": { "const": true },
+ "domain": { "type": "string", "minLength": 1, "maxLength": 253, "x-radroots-maximum-utf8-bytes": 253, "pattern": "^[a-z0-9.-]+$" },
+ "handler_identifier": { "$ref": "#/$defs/bounded_id" },
+ "public_relay_ids": { "type": "array", "minItems": 1, "maxItems": 32, "uniqueItems": true, "items": { "$ref": "#/$defs/relay_id" } },
+ "publish_relay_ids": { "type": "array", "minItems": 1, "maxItems": 32, "uniqueItems": true, "items": { "$ref": "#/$defs/relay_id" } },
+ "nostrconnect_url_template": { "$ref": "#/$defs/nostrconnect_url_template" },
+ "metadata": { "$ref": "#/$defs/discovery_metadata" }
+ }
+ }
+ ]
+ }
+ },
+ "x-radroots-contract": {
+ "schema": "radroots.myc.configuration-contract.v1",
+ "contract_version": 1,
+ "document_format": "toml_utf8",
+ "document_max_utf8_bytes": 1048576,
+ "unknown_fields": "reject_at_every_object_boundary",
+ "duplicate_keys": "reject_on_original_wire",
+ "null_values": "reject_on_original_wire",
+ "environment_overlay": "forbidden",
+ "protected_material": "forbidden",
+ "effective_output": "deterministic_redacted_with_exact_provenance",
+ "bootstrap_only": ["profile", "instance", "repo_local_root", "config_path"],
+ "safe_default_sources": ["radroots_service_host", "radroots_service_sqlite", "radroots_event", "radroots_nostr_connect", "accepted_service_authority", "engineering_safety"],
+ "semantic_relationships": [
+ "relay_ids_unique",
+ "relay_urls_canonical_unique",
+ "relay_read_or_write",
+ "required_relay_read_or_write",
+ "production_relays_wss_only",
+ "repo_local_plaintext_relays_loopback_only",
+ "at_least_one_read_relay",
+ "at_least_one_write_relay",
+ "at_least_one_required_read_relay",
+ "at_least_one_required_write_relay",
+ "enabled_role_expected_public_keys_unique",
+ "trusted_and_denied_clients_disjoint",
+ "permission_sign_event_kinds_equal_allowed_sign_event_kinds",
+ "challenge_rate_limits_present_even_when_challenges_disabled",
+ "rate_retention_not_less_than_window",
+ "challenge_authorized_lifetime_not_less_than_pending_lifetime",
+ "delivery_quorum_not_greater_than_required_write_relays",
+ "publish_initial_backoff_not_greater_than_maximum_backoff",
+ "discovery_enabled_iff_discovery_identity_enabled",
+ "discovery_public_relay_ids_reference_read_relays",
+ "discovery_publish_relay_ids_reference_write_relays",
+ "operations_public_bind_requires_explicit_public_policy"
+ ],
+ "forbidden_prototype_fields": [
+ "paths",
+ "custody",
+ "persistence",
+ "audit",
+ "observability",
+ "env_file",
+ "worker",
+ "signer_state_backend",
+ "runtime_audit_backend",
+ "output_dir",
+ "stdout",
+ "external_command",
+ "host_vault",
+ "keyring",
+ "managed_account",
+ "plaintext_file"
+ ]
+ }
+}
diff --git a/radroots.lib.source-lock.v1.toml b/radroots.lib.source-lock.v1.toml
@@ -6,4 +6,4 @@ workspace_catalog_sha256 = "deca0c080deae187ff8186c0708903e42f41ea57f77c5f91581e
version = "0.1.0-alpha"
source_archive_sha256 = "975474804e6358b9228981add0a23181dbdd1afddf5ae12579c82220876bc379"
lockfile = "Cargo.lock"
-lockfile_sha256 = "395d6703fdd37ba43558475ac739d6878d27d305c479ac1c07b19afe4c5d5867"
+lockfile_sha256 = "06df285eb4b6ae0e88a70e4bf55710744f8bbf320fa2d1873c241138208823b5"
diff --git a/tests/services_hardening_config_contract.rs b/tests/services_hardening_config_contract.rs
@@ -0,0 +1,760 @@
+#![forbid(unsafe_code)]
+
+use std::collections::{BTreeMap, BTreeSet};
+use std::net::SocketAddr;
+
+use serde_json::{Map, Value, json};
+
+const CONFIG_SCHEMA: &str = include_str!("../contracts/services_hardening/config.v1.schema.json");
+const CONFIG_EXAMPLE: &str = include_str!("../contracts/services_hardening/config.v1.example.toml");
+
+#[derive(Clone, Copy)]
+enum Profile {
+ Production,
+ RepoLocal,
+}
+
+fn schema() -> Value {
+ serde_json::from_str(CONFIG_SCHEMA).expect("configuration schema must be valid JSON")
+}
+
+fn example() -> Value {
+ let value = toml::from_str::<toml::Value>(CONFIG_EXAMPLE)
+ .expect("configuration example must be valid TOML");
+ serde_json::to_value(value).expect("TOML value must convert to JSON")
+}
+
+fn schema_valid(value: &Value) -> bool {
+ jsonschema::validator_for(&schema())
+ .expect("configuration schema must compile")
+ .is_valid(value)
+}
+
+fn string_array<'a>(value: &'a Value, pointer: &str) -> Vec<&'a str> {
+ value
+ .pointer(pointer)
+ .and_then(Value::as_array)
+ .expect("string array")
+ .iter()
+ .map(|value| value.as_str().expect("string item"))
+ .collect()
+}
+
+fn semantic_valid(value: &Value, profile: Profile) -> bool {
+ if !schema_valid(value) {
+ return false;
+ }
+
+ let relays = value["relays"].as_array().expect("relays");
+ let relay_ids = relays
+ .iter()
+ .map(|relay| relay["id"].as_str().expect("relay id"))
+ .collect::<Vec<_>>();
+ let relay_urls = relays
+ .iter()
+ .map(|relay| relay["url"].as_str().expect("relay URL"))
+ .collect::<Vec<_>>();
+ let parsed_relay_urls = relay_urls
+ .iter()
+ .map(|raw| {
+ url::Url::parse(raw)
+ .ok()
+ .filter(|parsed| parsed.as_str() == *raw)
+ })
+ .collect::<Vec<_>>();
+ if relay_ids.iter().collect::<BTreeSet<_>>().len() != relay_ids.len()
+ || relay_urls.iter().collect::<BTreeSet<_>>().len() != relay_urls.len()
+ || parsed_relay_urls.iter().any(Option::is_none)
+ || parsed_relay_urls.iter().flatten().any(|url| {
+ !url.username().is_empty() || url.password().is_some() || url.fragment().is_some()
+ })
+ || relays.iter().any(|relay| {
+ let read = relay["read"].as_bool().expect("read");
+ let write = relay["write"].as_bool().expect("write");
+ (!read && !write) || (relay["required"].as_bool().expect("required") && !read && !write)
+ })
+ || !relays
+ .iter()
+ .any(|relay| relay["read"].as_bool() == Some(true))
+ || !relays
+ .iter()
+ .any(|relay| relay["write"].as_bool() == Some(true))
+ || !relays.iter().any(|relay| {
+ relay["required"].as_bool() == Some(true) && relay["read"].as_bool() == Some(true)
+ })
+ || !relays.iter().any(|relay| {
+ relay["required"].as_bool() == Some(true) && relay["write"].as_bool() == Some(true)
+ })
+ {
+ return false;
+ }
+ if parsed_relay_urls.iter().flatten().any(|url| match profile {
+ Profile::Production => url.scheme() != "wss",
+ Profile::RepoLocal => match url.scheme() {
+ "wss" => false,
+ "ws" => !matches!(
+ url.host_str(),
+ Some("127.0.0.1" | "localhost" | "[::1]" | "::1")
+ ),
+ _ => true,
+ },
+ }) {
+ return false;
+ }
+
+ let trusted = string_array(value, "/policy/trusted_clients")
+ .into_iter()
+ .collect::<BTreeSet<_>>();
+ let denied = string_array(value, "/policy/denied_clients")
+ .into_iter()
+ .collect::<BTreeSet<_>>();
+ if !trusted.is_disjoint(&denied) {
+ return false;
+ }
+
+ let mut expected_role_keys = vec![
+ value["identity"]["transport"]["expected_public_key"]
+ .as_str()
+ .expect("transport key"),
+ value["identity"]["user"]["expected_public_key"]
+ .as_str()
+ .expect("user key"),
+ ];
+ if value["identity"]["discovery"]["enabled"] == true {
+ expected_role_keys.push(
+ value["identity"]["discovery"]["binding"]["expected_public_key"]
+ .as_str()
+ .expect("discovery key"),
+ );
+ }
+ if expected_role_keys.iter().collect::<BTreeSet<_>>().len() != expected_role_keys.len() {
+ return false;
+ }
+
+ let permitted_kinds = string_array(value, "/policy/permission_ceiling")
+ .into_iter()
+ .filter_map(|permission| permission.strip_prefix("sign_event:kind:"))
+ .filter_map(|kind| kind.parse::<u64>().ok())
+ .collect::<BTreeSet<_>>();
+ let allowed_kinds = value["policy"]["allowed_sign_event_kinds"]
+ .as_array()
+ .expect("allowed kinds")
+ .iter()
+ .map(|kind| kind.as_u64().expect("event kind"))
+ .collect::<BTreeSet<_>>();
+ if permitted_kinds != allowed_kinds {
+ return false;
+ }
+
+ let challenges = &value["policy"]["challenges"];
+ if challenges["enabled"] == true
+ && challenges["authorized_lifetime_ms"].as_u64()
+ < challenges["pending_lifetime_ms"].as_u64()
+ {
+ return false;
+ }
+ for (name, expected_scope) in [
+ ("connection_admission", "global_and_relay"),
+ ("challenge_creation", "connection"),
+ ("challenge_authorization", "connection"),
+ ] {
+ let limit = &value["rate_limits"][name];
+ if limit["scope"] != expected_scope
+ || limit["retention_ms"].as_u64() < limit["window_ms"].as_u64()
+ {
+ return false;
+ }
+ }
+
+ let retry = &value["transport"]["publish_retry"];
+ if !retry.is_null()
+ && retry["initial_backoff_ms"].as_u64() > retry["maximum_backoff_ms"].as_u64()
+ {
+ return false;
+ }
+ let required_writers = relays
+ .iter()
+ .filter(|relay| {
+ relay["required"].as_bool() == Some(true) && relay["write"].as_bool() == Some(true)
+ })
+ .count() as u64;
+ let delivery = &value["transport"]["delivery_policy"];
+ if delivery["mode"] == "required_quorum"
+ && delivery["required_acknowledgements"].as_u64() > Some(required_writers)
+ {
+ return false;
+ }
+
+ let discovery_enabled = value["discovery"]["enabled"].as_bool();
+ if discovery_enabled != value["identity"]["discovery"]["enabled"].as_bool() {
+ return false;
+ }
+ if discovery_enabled == Some(true) {
+ let by_id = relays
+ .iter()
+ .map(|relay| (relay["id"].as_str().unwrap(), relay))
+ .collect::<BTreeMap<_, _>>();
+ if string_array(value, "/discovery/public_relay_ids")
+ .iter()
+ .any(|id| by_id.get(id).and_then(|relay| relay["read"].as_bool()) != Some(true))
+ || string_array(value, "/discovery/publish_relay_ids")
+ .iter()
+ .any(|id| by_id.get(id).and_then(|relay| relay["write"].as_bool()) != Some(true))
+ {
+ return false;
+ }
+ }
+
+ let operations = &value["operations"];
+ if operations["enabled"] == true {
+ let Ok(listen) = operations["listen"]
+ .as_str()
+ .unwrap_or_default()
+ .parse::<SocketAddr>()
+ else {
+ return false;
+ };
+ if listen.port() == 0
+ || (operations["bind_policy"] == "loopback_only" && !listen.ip().is_loopback())
+ {
+ return false;
+ }
+ }
+ true
+}
+
+fn assert_rejected(value: &Value, profile: Profile) {
+ assert!(
+ !semantic_valid(value, profile),
+ "negative configuration vector unexpectedly passed"
+ );
+}
+
+fn insert(value: &mut Value, pointer: &str, key: &str, replacement: Value) {
+ value
+ .pointer_mut(pointer)
+ .and_then(Value::as_object_mut)
+ .expect("object pointer")
+ .insert(key.to_owned(), replacement);
+}
+
+fn defaults(value: &Value, pointer: &str, output: &mut BTreeMap<String, Value>) {
+ match value {
+ Value::Object(object) => {
+ if let Some(default) = object.get("default") {
+ assert!(
+ object.contains_key("x-radroots-default-source"),
+ "default without provenance at {pointer}"
+ );
+ output.insert(pointer.to_owned(), default.clone());
+ }
+ for (key, child) in object {
+ defaults(child, &format!("{pointer}/{key}"), output);
+ }
+ }
+ Value::Array(values) => {
+ for (index, child) in values.iter().enumerate() {
+ defaults(child, &format!("{pointer}/{index}"), output);
+ }
+ }
+ _ => {}
+ }
+}
+
+fn assert_integer_bounds(value: &Value, pointer: &str) {
+ match value {
+ Value::Object(object) => {
+ if object.get("type") == Some(&Value::String("integer".to_owned())) {
+ assert!(
+ object.contains_key("minimum"),
+ "missing minimum at {pointer}"
+ );
+ assert!(
+ object.contains_key("maximum"),
+ "missing maximum at {pointer}"
+ );
+ }
+ for (key, child) in object {
+ assert_integer_bounds(child, &format!("{pointer}/{key}"));
+ }
+ }
+ Value::Array(values) => {
+ for (index, child) in values.iter().enumerate() {
+ assert_integer_bounds(child, &format!("{pointer}/{index}"));
+ }
+ }
+ _ => {}
+ }
+}
+
+#[test]
+fn schema_identity_structure_and_machine_policy_are_exact() {
+ let schema = schema();
+ assert_eq!(
+ schema["$schema"],
+ "https://json-schema.org/draft/2020-12/schema"
+ );
+ assert_eq!(schema["type"], "object");
+ assert_eq!(schema["additionalProperties"], false);
+ assert_eq!(
+ schema["required"],
+ json!([
+ "schema",
+ "schema_version",
+ "service",
+ "logging",
+ "operations",
+ "database",
+ "identity",
+ "relays",
+ "transport",
+ "policy",
+ "rate_limits",
+ "resource_limits",
+ "discovery"
+ ])
+ );
+ assert_eq!(
+ schema["properties"]["schema"]["const"],
+ "radroots.myc.config"
+ );
+ assert_eq!(schema["properties"]["schema_version"]["const"], 1);
+ assert_eq!(
+ schema["x-radroots-contract"]["bootstrap_only"],
+ json!(["profile", "instance", "repo_local_root", "config_path"])
+ );
+ assert_eq!(
+ schema["x-radroots-contract"]["document_max_utf8_bytes"],
+ 1_048_576
+ );
+ assert_eq!(
+ schema["x-radroots-contract"]["duplicate_keys"],
+ "reject_on_original_wire"
+ );
+ assert_eq!(
+ schema["x-radroots-contract"]["null_values"],
+ "reject_on_original_wire"
+ );
+ assert_eq!(
+ schema["x-radroots-contract"]["environment_overlay"],
+ "forbidden"
+ );
+ assert_eq!(
+ schema["x-radroots-contract"]["protected_material"],
+ "forbidden"
+ );
+ assert_eq!(
+ schema["x-radroots-contract"]["effective_output"],
+ "deterministic_redacted_with_exact_provenance"
+ );
+ assert_integer_bounds(&schema, "");
+
+ let mut found_defaults = BTreeMap::new();
+ defaults(&schema, "", &mut found_defaults);
+ assert_eq!(found_defaults.len(), 39);
+ for source in found_defaults.keys().map(|pointer| {
+ schema.pointer(pointer).unwrap()["x-radroots-default-source"]
+ .as_str()
+ .unwrap()
+ }) {
+ assert!(
+ schema["x-radroots-contract"]["safe_default_sources"]
+ .as_array()
+ .unwrap()
+ .iter()
+ .any(|allowed| allowed == source)
+ );
+ }
+}
+
+#[test]
+fn lib_derived_limits_and_defaults_are_literal_frozen() {
+ let schema = schema();
+ let exact = [
+ ("/$defs/operations_limits/properties/header_count", 64, 32),
+ (
+ "/$defs/operations_limits/properties/header_bytes",
+ 32_768,
+ 16_384,
+ ),
+ (
+ "/$defs/operations_limits/properties/response_body_utf8_bytes",
+ 1_048_576,
+ 1_048_576,
+ ),
+ (
+ "/$defs/operations_limits/properties/concurrent_connections",
+ 64,
+ 32,
+ ),
+ (
+ "/$defs/operations_limits/properties/request_deadline_ms",
+ 30_000,
+ 15_000,
+ ),
+ (
+ "/$defs/operations_limits/properties/idle_timeout_ms",
+ 60_000,
+ 30_000,
+ ),
+ ("/$defs/database/properties/busy_timeout_ms", 60_000, 5_000),
+ ("/$defs/database/properties/max_connections", 8, 8),
+ ("/$defs/admin_limits/properties/header_count", 64, 32),
+ (
+ "/$defs/admin_limits/properties/header_bytes",
+ 32_768,
+ 16_384,
+ ),
+ (
+ "/$defs/admin_limits/properties/request_body_utf8_bytes",
+ 65_536,
+ 65_536,
+ ),
+ (
+ "/$defs/admin_limits/properties/response_body_utf8_bytes",
+ 1_048_576,
+ 1_048_576,
+ ),
+ (
+ "/$defs/admin_limits/properties/concurrent_connections",
+ 64,
+ 32,
+ ),
+ (
+ "/$defs/admin_limits/properties/request_deadline_ms",
+ 30_000,
+ 15_000,
+ ),
+ (
+ "/$defs/admin_limits/properties/idle_timeout_ms",
+ 60_000,
+ 30_000,
+ ),
+ ("/$defs/admin_limits/properties/query_items", 200, 100),
+ ("/$defs/metrics_limits/properties/descriptors", 64, 64),
+ ("/$defs/metrics_limits/properties/samples", 512, 512),
+ ("/$defs/metrics_limits/properties/labels_per_sample", 8, 8),
+ (
+ "/$defs/metrics_limits/properties/render_utf8_bytes",
+ 1_048_576,
+ 1_048_576,
+ ),
+ (
+ "/$defs/event_limits/properties/wire_bytes",
+ 524_288,
+ 262_144,
+ ),
+ (
+ "/$defs/event_limits/properties/content_bytes",
+ 131_072,
+ 131_072,
+ ),
+ ("/$defs/event_limits/properties/tag_count", 1_024, 1_024),
+ (
+ "/$defs/event_limits/properties/tag_total_elements",
+ 4_096,
+ 4_096,
+ ),
+ (
+ "/$defs/event_limits/properties/tag_element_bytes",
+ 4_096,
+ 4_096,
+ ),
+ (
+ "/$defs/event_limits/properties/tag_total_bytes",
+ 131_072,
+ 131_072,
+ ),
+ (
+ "/$defs/event_limits/properties/decrypted_plaintext_bytes",
+ 262_144,
+ 262_144,
+ ),
+ ];
+ for (pointer, maximum, default) in exact {
+ assert_eq!(
+ schema.pointer(pointer).unwrap()["maximum"],
+ maximum,
+ "{pointer}"
+ );
+ assert_eq!(
+ schema.pointer(pointer).unwrap()["default"],
+ default,
+ "{pointer}"
+ );
+ }
+}
+
+#[test]
+fn canonical_example_and_required_positive_variants_pass() {
+ let value = example();
+ assert!(semantic_valid(&value, Profile::Production));
+ assert_eq!(value["identity"]["transport"]["provider"], "encrypted_file");
+ assert_eq!(value["identity"]["user"]["provider"], "local_signer");
+ assert_eq!(
+ value["identity"]["discovery"]["binding"]["provider"],
+ "encrypted_file"
+ );
+
+ let mut disabled_discovery = value.clone();
+ disabled_discovery["identity"]["discovery"] = json!({"enabled": false});
+ disabled_discovery["discovery"] = json!({"enabled": false});
+ assert!(semantic_valid(&disabled_discovery, Profile::Production));
+
+ let mut repo_local = disabled_discovery.clone();
+ repo_local["relays"][0]["url"] = json!("ws://127.0.0.1:8080/");
+ repo_local["relays"][1]["url"] = json!("ws://localhost:8081/");
+ assert!(semantic_valid(&repo_local, Profile::RepoLocal));
+ assert_rejected(&repo_local, Profile::Production);
+
+ let mut enabled_operations = value;
+ enabled_operations["operations"] = json!({
+ "enabled": true,
+ "listen": "127.0.0.1:9460",
+ "bind_policy": "loopback_only",
+ "limits": {}
+ });
+ assert!(semantic_valid(&enabled_operations, Profile::Production));
+}
+
+#[test]
+fn structural_unknown_legacy_and_original_wire_vectors_fail() {
+ let value = example();
+ for (pointer, key) in [
+ ("", "unknown"),
+ ("/service", "shutdowm_grace_ms"),
+ ("/identity/transport", "executable"),
+ ("/resource_limits/admin", "body_bytes"),
+ ("/policy/challenges", "redirect_from_client"),
+ ] {
+ let mut invalid = value.clone();
+ insert(&mut invalid, pointer, key, json!(true));
+ assert_rejected(&invalid, Profile::Production);
+ }
+ for field in schema()["x-radroots-contract"]["forbidden_prototype_fields"]
+ .as_array()
+ .unwrap()
+ {
+ let mut invalid = value.clone();
+ invalid.as_object_mut().unwrap().insert(
+ field.as_str().unwrap().to_owned(),
+ Value::Object(Map::new()),
+ );
+ assert_rejected(&invalid, Profile::Production);
+ }
+
+ assert!(toml::from_str::<toml::Value>("schema='a'\nschema='b'\n").is_err());
+ assert!(toml::from_str::<toml::Value>("schema = null\n").is_err());
+}
+
+#[test]
+fn identity_relay_and_network_authority_fail_closed() {
+ let value = example();
+ for pointer in [
+ "/identity/transport/envelope_path",
+ "/identity/user/socket_path",
+ ] {
+ let mut invalid = value.clone();
+ *invalid.pointer_mut(pointer).unwrap() = json!("relative/path");
+ assert_rejected(&invalid, Profile::Production);
+ }
+ for provider in [
+ "plaintext_file",
+ "external_command",
+ "host_vault",
+ "keyring",
+ "managed_account",
+ ] {
+ let mut invalid = value.clone();
+ invalid["identity"]["transport"]["provider"] = json!(provider);
+ assert_rejected(&invalid, Profile::Production);
+ }
+
+ let mut duplicate_id = value.clone();
+ duplicate_id["relays"][1]["id"] = duplicate_id["relays"][0]["id"].clone();
+ assert_rejected(&duplicate_id, Profile::Production);
+ let mut duplicate_url = value.clone();
+ duplicate_url["relays"][1]["url"] = duplicate_url["relays"][0]["url"].clone();
+ assert_rejected(&duplicate_url, Profile::Production);
+ let mut inactive = value.clone();
+ inactive["relays"][0]["read"] = json!(false);
+ inactive["relays"][0]["write"] = json!(false);
+ assert_rejected(&inactive, Profile::Production);
+ let mut insecure = value;
+ insecure["relays"][0]["url"] = json!("ws://127.0.0.1:8080/");
+ assert_rejected(&insecure, Profile::Production);
+
+ let mut collapsed_roles = example();
+ collapsed_roles["identity"]["user"]["expected_public_key"] =
+ collapsed_roles["identity"]["transport"]["expected_public_key"].clone();
+ assert_rejected(&collapsed_roles, Profile::Production);
+
+ let mut noncanonical_url = example();
+ noncanonical_url["relays"][0]["url"] = json!("WSS://relay-primary.example.test");
+ assert_rejected(&noncanonical_url, Profile::Production);
+ let mut credential_name = example();
+ credential_name["identity"]["transport"]["credential_reference"] =
+ json!("transport.identity-key");
+ assert!(semantic_valid(&credential_name, Profile::Production));
+ credential_name["identity"]["transport"]["credential_reference"] =
+ json!(format!("a{}z", "x".repeat(126)));
+ assert!(semantic_valid(&credential_name, Profile::Production));
+ credential_name["identity"]["transport"]["credential_reference"] =
+ json!(format!("a{}z", "x".repeat(127)));
+ assert_rejected(&credential_name, Profile::Production);
+}
+
+#[test]
+fn bounds_relationships_and_conditional_authority_fail_closed() {
+ let value = example();
+ for (pointer, exact, over) in [
+ ("/database/busy_timeout_ms", 60_000_u64, 60_001_u64),
+ ("/database/max_connections", 8, 9),
+ ("/resource_limits/admin/header_count", 64, 65),
+ ("/resource_limits/admin/query_items", 200, 201),
+ ("/resource_limits/events/wire_bytes", 524_288, 524_289),
+ ("/resource_limits/metrics/samples", 512, 513),
+ ("/identity/user/request_deadline_ms", 30_000, 30_001),
+ ("/identity/user/concurrency", 64, 65),
+ ] {
+ let mut minimum = value.clone();
+ *minimum.pointer_mut(pointer).unwrap() = json!(1);
+ assert!(
+ semantic_valid(&minimum, Profile::Production),
+ "minimum {pointer}"
+ );
+ let mut maximum = value.clone();
+ *maximum.pointer_mut(pointer).unwrap() = json!(exact);
+ assert!(
+ semantic_valid(&maximum, Profile::Production),
+ "maximum {pointer}"
+ );
+ let mut zero = value.clone();
+ *zero.pointer_mut(pointer).unwrap() = json!(0);
+ assert_rejected(&zero, Profile::Production);
+ let mut excessive = value.clone();
+ *excessive.pointer_mut(pointer).unwrap() = json!(over);
+ assert_rejected(&excessive, Profile::Production);
+ }
+
+ let mut overlap = value.clone();
+ overlap["policy"]["denied_clients"] = overlap["policy"]["trusted_clients"].clone();
+ assert_rejected(&overlap, Profile::Production);
+ let mut permission_mismatch = value.clone();
+ permission_mismatch["policy"]["allowed_sign_event_kinds"] = json!([1, 7]);
+ assert_rejected(&permission_mismatch, Profile::Production);
+ let mut challenge_lifetime = value.clone();
+ challenge_lifetime["policy"]["challenges"]["authorized_lifetime_ms"] = json!(1000);
+ assert_rejected(&challenge_lifetime, Profile::Production);
+ let mut challenge_missing = value.clone();
+ challenge_missing["policy"]["challenges"]
+ .as_object_mut()
+ .unwrap()
+ .remove("url");
+ assert_rejected(&challenge_missing, Profile::Production);
+ let mut challenge_disabled_leak = value.clone();
+ challenge_disabled_leak["policy"]["challenges"]["enabled"] = json!(false);
+ assert_rejected(&challenge_disabled_leak, Profile::Production);
+ let mut rate_retention = value.clone();
+ rate_retention["rate_limits"]["challenge_creation"]["retention_ms"] = json!(1);
+ assert_rejected(&rate_retention, Profile::Production);
+ let mut backoff = value.clone();
+ backoff["transport"]["publish_retry"]["initial_backoff_ms"] = json!(30_000);
+ backoff["transport"]["publish_retry"]["maximum_backoff_ms"] = json!(1);
+ assert_rejected(&backoff, Profile::Production);
+ let mut quorum = value.clone();
+ quorum["transport"]["delivery_policy"] =
+ json!({"mode": "required_quorum", "required_acknowledgements": 3});
+ assert_rejected(&quorum, Profile::Production);
+ let mut discovery_mismatch = value.clone();
+ discovery_mismatch["identity"]["discovery"] = json!({"enabled": false});
+ assert_rejected(&discovery_mismatch, Profile::Production);
+ let mut missing_relay = value.clone();
+ missing_relay["discovery"]["publish_relay_ids"] = json!(["missing"]);
+ assert_rejected(&missing_relay, Profile::Production);
+ let mut invalid_template = value;
+ invalid_template["discovery"]["nostrconnect_url_template"] =
+ json!("https://myc.example.test/connect");
+ assert_rejected(&invalid_template, Profile::Production);
+}
+
+#[test]
+fn schema_and_version_are_closed_and_defaults_are_only_safe_leaves() {
+ let value = example();
+ for replacement in [json!("radroots.myc.config.v2"), json!("myc")] {
+ let mut invalid = value.clone();
+ invalid["schema"] = replacement;
+ assert_rejected(&invalid, Profile::Production);
+ }
+ for replacement in [json!(0), json!(2), json!("1")] {
+ let mut invalid = value.clone();
+ invalid["schema_version"] = replacement;
+ assert_rejected(&invalid, Profile::Production);
+ }
+
+ let mut safe_defaults_omitted = value;
+ safe_defaults_omitted["service"] = json!({});
+ safe_defaults_omitted["logging"] = json!({});
+ safe_defaults_omitted["database"]
+ .as_object_mut()
+ .unwrap()
+ .remove("busy_timeout_ms");
+ safe_defaults_omitted["database"]
+ .as_object_mut()
+ .unwrap()
+ .remove("max_connections");
+ safe_defaults_omitted["transport"]
+ .as_object_mut()
+ .unwrap()
+ .remove("connect_deadline_ms");
+ safe_defaults_omitted["transport"]
+ .as_object_mut()
+ .unwrap()
+ .remove("publish_retry");
+ safe_defaults_omitted["resource_limits"] = json!({});
+ assert!(semantic_valid(&safe_defaults_omitted, Profile::Production));
+
+ for required in [
+ "minimum_free_bytes",
+ "transport",
+ "user",
+ "relays",
+ "connection_approval",
+ "trusted_clients",
+ "denied_clients",
+ "permission_ceiling",
+ "allowed_sign_event_kinds",
+ "challenges",
+ "rate_limits",
+ "delivery_policy",
+ "discovery",
+ ] {
+ assert!(
+ CONFIG_SCHEMA.contains(required),
+ "missing authority {required}"
+ );
+ }
+}
+
+#[test]
+fn canonical_example_contains_no_protected_material_or_legacy_selector() {
+ let lowercase = CONFIG_EXAMPLE.to_ascii_lowercase();
+ for forbidden in [
+ "private_key",
+ "secret_key",
+ "mnemonic",
+ "nsec1",
+ "wrapping_key =",
+ "myc_",
+ "env_file",
+ "external_command",
+ "plaintext_file",
+ "keyring",
+ "managed_account",
+ ] {
+ assert!(
+ !lowercase.contains(forbidden),
+ "canonical example contains forbidden material or selector {forbidden}"
+ );
+ }
+}