Skip to content

Boundary consistency, proof freshness, OpenAPI validation & rate-limit enforcement (#345–#348) - #445

Merged
maugauwi-hash merged 4 commits into
ethos-protocol:mainfrom
iam-mercy:feat/issues-345-348-boundary-schema-ratelimit
Aug 31, 2026
Merged

Boundary consistency, proof freshness, OpenAPI validation & rate-limit enforcement (#345–#348)#445
maugauwi-hash merged 4 commits into
ethos-protocol:mainfrom
iam-mercy:feat/issues-345-348-boundary-schema-ratelimit

Conversation

@iam-mercy

Copy link
Copy Markdown

Summary

Resolves four issues across the ttl_vault contract and the API server.

Issue Area Change
#345 ttl-vault Floors/caps boundary consistency check
#346 ttl-vault Credential anchoring proof-freshness requirement
#347 api-server OpenAPI schema validation
#348 api-server Rate-limit enforcement on all routes

#345 — Floors and caps boundary consistency

  • New shared contracts/ttl_vault/src/range_check.rs module: RangeError::FloorExceedsCap
    and ensure_floor_within_cap(floor, cap).
  • set_floor and set_cap now cross-check the other bound already configured
    for the same beneficiary and panic with the new
    ContractError::FloorExceedsCap when floor > cap. The check runs on every
    set, so an update made after the initial configuration is validated too.
  • floors.rs / caps.rs are now wired into the crate module tree
    (pub mod floors; pub mod caps;) so their tests actually run.
  • Tests: valid range accepted, floor-above-cap rejected, and a regression test
    for the update-after-initial-set case, in both modules plus range_check.

#346 — Credential anchoring proof freshness

  • credential_anchoring.rs gains set_max_proof_age / max_proof_age_seconds
    (default DEFAULT_MAX_PROOF_AGE_SECONDS = 3600s) and a new
    anchor_credential(env, credential_id, external_id, system, proof_timestamp)
    entry point that rejects proofs older than the configured window or dated in
    the future.
  • proof_is_fresh is exposed for callers that want to pre-check.
  • Docs updated in docs/issues-32-38-39-40.md.
  • Tests: fresh proof anchors, expired proof rejected, future-dated proof
    rejected, default window applies when unconfigured.

#347 — OpenAPI schema validation

  • New backend/src/schema_validation.rs: parses the bundled docs/openapi.yaml
    and exposes OpenApiSpec + openapi_validation_middleware, applied to the
    router in main.rs. Declared paths enforce their method set (405 + Allow
    on mismatch); undeclared internal routes pass through.
  • scripts/check_openapi_drift.mjs + .github/workflows/openapi.yml: CI job
    that fails when a public /api/** route is served but not documented, and
    runs the middleware unit tests + openapi-spec-validator.
  • scripts/generate_ts_client.mjs generates clients/typescript/src/client.ts
    (typed fetch client, one method per operation); the CI job runs it with
    --check so a stale client fails the build.
  • Tests: valid request passes, method mismatch rejected, parameters: block is
    not mistaken for an operation, undeclared path passes through, path params
    match a single segment.

#348 — Rate-limit enforcement on all routes

  • backend/src/rate_limit.rs is now wired into the crate. New
    UserTier::Unauthenticated default-deny tier (strict TierLimit::default_deny)
    for callers with no Authorization header; unregistered endpoints also fall
    back to default-deny via check_and_record_enforced, so no handler can bypass
    the limiter.
  • enforce_rate_limit Axum middleware is the outermost app layer in
    build_router, wrapping every route. Rejections return 429 Too Many Requests with a Retry-After header (RateLimitError::retry_after_secs).
  • Tests: Free/Pro/Enterprise each hit their configured limit, Unauthenticated
    hits the default-deny limit, Admin is never limited, unregistered endpoints
    are still enforced, 429 carries Retry-After, and header-based tier
    resolution.

Notes

  • Pre-existing fix pulled in: passkey_cap_tests.rs used
    env.ledger().with_mut(...) without importing testutils::Ledger, so the
    ttl-vault test binary did not compile. Added the trait import and switched
    to set_timestamp. Unrelated pre-existing warnings in slice_performance_tests.rs
    (String::from_slice deprecation) and credential_lifecycle_tests.rs are
    left as-is.
  • Backend: cargo test -p ethos-protocol-backendrate_limit + schema_validation
    suites pass (37 tests).

Closes #345
Closes #346
Closes #347
Closes #348

floors.rs and caps.rs configured allocation bounds independently, so a
floor set above its matching cap produced an unsatisfiable range that
nothing rejected.

- Add shared contracts/ttl_vault/src/range_check.rs with
  RangeError::FloorExceedsCap and ensure_floor_within_cap(floor, cap).
- set_floor / set_cap now cross-check the other bound already stored for
  the same beneficiary and panic with the new
  ContractError::FloorExceedsCap when floor > cap. The check runs on
  every set, so post-initial-set updates are validated too.
- Wire floors and caps into the crate module tree so their tests run.
- Tests: valid range, floor-exceeds-cap rejection, and an
  update-after-initial-set regression test in both modules.
- Also import testutils::Ledger in passkey_cap_tests.rs (it used
  env.ledger().with_mut without the trait in scope, which broke the
  ttl-vault test binary).

Closes ethos-protocol#345
credential_anchoring anchored off-chain credential hashes on-chain but
accepted anchoring proofs of any age, allowing stale or superseded
credential states to be anchored.

- Add set_max_proof_age / max_proof_age_seconds config
  (DEFAULT_MAX_PROOF_AGE_SECONDS = 3600).
- Add anchor_credential(env, credential_id, external_id, system,
  proof_timestamp): rejects proofs older than the configured window or
  dated in the future; proof_is_fresh exposes the check directly.
- Document the requirement in docs/issues-32-38-39-40.md.
- Tests: fresh proof anchors, expired proof rejected, future-dated proof
  rejected, default window applies when unconfigured.

Closes ethos-protocol#346
docs/openapi.yaml existed but nothing kept it in sync with the handlers,
so clients could not rely on it.

- Add backend/src/schema_validation.rs: parses the bundled
  docs/openapi.yaml into an OpenApiSpec and provides
  openapi_validation_middleware. Declared paths enforce their method set
  (405 + Allow on mismatch); undeclared internal routes pass through.
- scripts/check_openapi_drift.mjs + .github/workflows/openapi.yml: CI job
  that fails when a public /api/** route is served but not documented,
  and runs the middleware tests + openapi-spec-validator.
- scripts/generate_ts_client.mjs generates
  clients/typescript/src/client.ts (typed fetch client, one method per
  operation); CI runs it with --check so a stale client fails the build.
- Tests: valid request passes, method mismatch rejected, parameters: is
  not treated as an operation, undeclared path passes through, path
  params match a single segment.

The middleware is wired into build_router in the following commit
(shared backend/src/main.rs).

Closes ethos-protocol#347
rate_limit.rs defined UserTier and TierLimit but was not wired into the
crate and no middleware applied it, so handlers could bypass it.

- Wire pub mod rate_limit into the crate.
- Add UserTier::Unauthenticated: a default-deny tier (TierLimit::default_deny)
  for callers with no Authorization header.
- check_and_record_enforced applies the default-deny limit to endpoints
  with no registered config, so no route is silently unlimited.
- enforce_rate_limit Axum middleware is added as the outermost layer in
  build_router, wrapping every route. Rejections return 429 with a
  Retry-After header (RateLimitError::retry_after_secs).
- Also wires the ethos-protocol#347 OpenAPI validation middleware into build_router
  (shared backend/src/main.rs).
- Tests: Free/Pro/Enterprise each hit their configured limit,
  Unauthenticated hits the default-deny limit, Admin is never limited,
  unregistered endpoints stay enforced, 429 carries Retry-After, header
  tier resolution.

Closes ethos-protocol#348
@drips-wave

drips-wave Bot commented Aug 28, 2026

Copy link
Copy Markdown

@iam-mercy Great news! 🎉 Based on an automated assessment of this PR, the linked Wave issue(s) no longer count against your application limits.

You can now already apply to more issues while waiting for a review of this PR. Keep up the great work! 🚀

Learn more about application limits

@maugauwi-hash
maugauwi-hash merged commit 7a68e76 into ethos-protocol:main Aug 31, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

2 participants