fix(review): harden the E2E-gap fixes after security/regression review

Addresses findings from an independent review of the gap-closing commits:

- R1 (regression): add #[serde(default)] to HttpDispatchPayload.method so
  async-HTTP outbox rows enqueued before the field existed still decode
  after upgrade instead of dead-lettering on "missing field `method`".
  Adds a regression test for the missing-key path.
- method case: uppercase ctx.request.method at the orchestrator boundary
  so it honors its documented "uppercased" contract for extension verbs.
  Route matching is already case-insensitive, so matching is unaffected.
- CLI hardening: percent-encode free-form path segments (app slug, topic
  name, ids, secret name) in the reqwest client so a value containing
  `/ ? #` can't break out of its URL segment. Adds a `seg()` helper +
  unit test and applies it uniformly across all path-interpolating
  methods (new and pre-existing).
- S1 (docs): correct the users::email_available enumeration framing — it
  adds no new vector vs. create's uniqueness error, but is cheaper and
  unthrottled; there is no built-in throttle/CAPTCHA primitive, so the
  honest mitigation is a kv-based counter. Updated SDK doc-comments,
  trait docs, and stdlib-reference.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
MechaCat02
2026-06-13 09:59:24 +02:00
parent 30bad27711
commit a91b134285
9 changed files with 156 additions and 4 deletions

View File

@@ -47,6 +47,10 @@ pub struct NewHttpOutbox {
pub struct HttpDispatchPayload {
pub script_name: String,
pub path: String,
/// HTTP method (uppercased). `#[serde(default)]` so async-HTTP outbox
/// rows enqueued before this field existed still decode (as `""`,
/// matching non-HTTP triggers) instead of dead-lettering on upgrade.
#[serde(default)]
pub method: String,
pub headers: std::collections::BTreeMap<String, String>,
pub body: serde_json::Value,
@@ -70,3 +74,30 @@ pub enum OutboxWriterError {
#[error("outbox write failed: {0}")]
Backend(String),
}
#[cfg(test)]
mod tests {
use super::*;
/// Async-HTTP outbox rows enqueued before `method` existed must still
/// decode after upgrade (it's `#[serde(default)]`), not dead-letter on
/// a "missing field `method`" error. Regression guard for that field.
#[test]
fn http_payload_decodes_without_method_field() {
let legacy = serde_json::json!({
"script_name": "todos",
"path": "/todos",
// no "method" key — pre-upgrade row
"headers": {},
"body": null,
"params": {},
"query": {},
"rest": "",
"timeout_seconds": 30
});
let payload: HttpDispatchPayload =
serde_json::from_value(legacy).expect("legacy payload must decode");
assert_eq!(payload.method, "");
assert_eq!(payload.path, "/todos");
}
}

View File

@@ -207,6 +207,13 @@ pub trait UsersService: Send + Sync {
/// this leaks only a single boolean — no id, profile, or timing
/// distinction beyond "exists / doesn't" — so a public registration
/// script can pre-check before calling `create`.
///
/// Enumeration note: that bit is the same one a register form already
/// leaks ("email taken"), but this probe is cheaper (no password hash),
/// has no side effect (a `create` miss writes a junk row), and is not
/// rate limited (there is no built-in throttle/CAPTCHA primitive). A
/// script exposing it on an anonymous route inherits account-enumeration
/// risk and must add its own `kv`-based throttle if that matters.
async fn email_available(&self, cx: &SdkCallCx, email: &str) -> Result<bool, UsersError>;
async fn update(