fix(config): fail closed on POW_REQUIRED without FEATURE_POW; harden premium key check

- Reject boot when POW_REQUIRED (or the per-pubkey PoW list) is set while
  FEATURE_POW is off: the gate would silently never apply and /capabilities
  would report pow_required:false — a signup endpoint the operator believes
  is protected but isn't.
- Validate the premium PAYMENT_API_KEY through the same require_secret path
  the pay-to-register flow uses, so a placeholder key cannot reach production.
- Advertise the premium-post write policy in MessagingCapability and
  regenerate openapi.json to match.
- Give the premium integration fixtures unique per-run ids so the suite is
  re-runnable against a persistent local test database.
This commit is contained in:
jwinterm
2026-07-02 19:47:38 -04:00
parent 9c75bc6bdd
commit dec9994e06
4 changed files with 38 additions and 15 deletions
+1 -1
View File
@@ -60,7 +60,7 @@ pub struct FeatureCapabilities {
pub struct MessagingCapability {
/// The ws(s):// relay URL clients connect to.
pub relay_url: String,
/// Write policy: `inbox-outbox` | `author-allowlist` | `open`.
/// Write policy: `inbox-outbox` | `author-allowlist` | `open` | `premium-post`.
pub write_policy: String,
/// NIP-13 PoW bits required on cross-ecosystem inbound (0 = off).
pub inbound_pow_bits: u8,
+24 -5
View File
@@ -865,6 +865,16 @@ impl Config {
if self.pow.enabled {
require_secret("ALTCHA_HMAC_KEY", &self.pow.hmac_key, 32)?;
}
// Fail closed on the silent-disarm footgun: asking for PoW (POW_REQUIRED or a
// per-pubkey list) while the feature master switch is off means the gate never
// applies and /capabilities reports pow_required:false — a signup endpoint the
// operator believes is protected but isn't.
if !self.pow.enabled && (self.pow.required || !self.pow.required_for_pubkeys.is_empty()) {
return Err(cfg_err(
"POW_REQUIRED (or TEST_POW_REQUIRED_FOR_PUBKEYS) is set but FEATURE_POW is off \
— the PoW gate would never apply; set FEATURE_POW=true",
));
}
// Admin surface: dedicated secrets + a real public URL when enabled.
if self.admin.enabled {
@@ -1116,11 +1126,7 @@ impl Config {
if p.store_id.trim().is_empty() {
return Err(cfg_err("PREMIUM_ENABLED needs PAYMENT_STORE_ID"));
}
if p.api_key.len() < 8 {
return Err(cfg_err(
"PREMIUM_ENABLED needs PAYMENT_API_KEY (>= 8 bytes)",
));
}
require_secret("PAYMENT_API_KEY", &p.api_key, 8)?;
// The same processor-safety floors the pay-to-register path enforces —
// premium's common case is require_payment=false, which skips that
// block. A 0-conf invoice settles on first mempool sighting and can
@@ -1388,6 +1394,19 @@ mod tests {
assert!(c.validate().is_err());
}
#[test]
fn pow_required_without_feature_pow_rejected() {
// POW_REQUIRED=true while FEATURE_POW is off silently disarms the gate.
let mut c = valid();
c.pow.enabled = false;
c.pow.required = true;
assert!(c.validate().is_err());
// Enabling the master switch (with a key) makes it valid.
c.pow.enabled = true;
c.pow.hmac_key = "a".repeat(32);
assert!(c.validate().is_ok());
}
#[test]
fn enabled_chain_requires_secret_in_production() {
let mut c = valid();