Skip to content

feat(auth): deterministic failure-boundary for handleNonceRequest (#1… - #1732

Merged
Baskarayelu merged 2 commits into
Remitwise-Org:mainfrom
yunus-dev-codecrafter:feat/1712-nonce-failure-boundary
Oct 1, 2026
Merged

Baskarayelu merged 2 commits into
Remitwise-Org:mainfrom
yunus-dev-codecrafter:feat/1712-nonce-failure-boundary

Conversation

@yunus-dev-codecrafter

Copy link
Copy Markdown
Contributor

closes #1712

…712)

  • Add lib/auth/nonce.ts: pure handleNonceRequest + consumeNonce with validation-first, fail-closed auth, allow-list, idempotent requestKey replay, conflicting-reuse 409, stale-version 410, rate-limit 429, store-fault 503, single-use consume, redacted logging
  • Add app/api/auth/nonce/route.ts: POST/GET adapter, re-exports handleNonceRequest, no-store headers, Retry-After on retryable faults
  • Add lib/auth/nonce.test.ts: 22 focused tests (success, rejection, boundary, retry/duplicate/concurrency/stale/permission/faults)

Closes #1712

PR: feat(auth): deterministic failure-boundary for handleNonceRequest (#1712)

Branch: feat/1712-nonce-failure-boundary
Commit: 6892f66
Closes #1712

Summary

Implements deterministic failure-boundary coverage for handleNonceRequest
in meridian-web/app/api/auth/nonce/route.ts (Remitwise-Frontend).

The entry point did not exist (verified: 0 hits for handleNonceRequest,
no app/api/auth/ directory), so this is a greenfield, smallest-complete
implementation following the existing app/api/transfer-capability/route.ts
pattern (fail-closed session resolution, no-store headers, generic error
bodies, redacted logs).

New files (3 files, +757):

  • meridian-web/lib/auth/nonce.ts — pure deterministic core
  • meridian-web/app/api/auth/nonce/route.ts — POST/GET thin adapter,
    re-exports handleNonceRequest
  • meridian-web/lib/auth/nonce.test.ts — 22 focused tests

Core lives in lib/ because vitest.config.ts only includes
hooks/components/lib/models (app/** is excluded from vitest).

Design

  • Identity: session principal from auth_token cookie preferred;
    address body/query field allowed for anonymous sign-in bootstrap.
    Neither => 401 NO_ACTIVE_SESSION.
  • Validation-first with strict zod schemas (principal 1..320, requestKey
    8..128 [A-Za-z0-9:._-], clientVersion int >= 0, .strict() + require
    principal-or-address). Malformed input never touches the store.
  • State model: in-memory NonceStore entries
    { nonce, principal, issuedAt, expiresAt, version, consumed }.
    TTL from NONCE_TTL_MS (default 5min, clamped 30s..10min).
  • Invariants (documented in code, 10 total):
    1. validation-first; 2. fail-closed auth; 3. allow-list denied-by-default;
    2. idempotent retry (same principal+requestKey+version => same nonce,
      deduped:true, no mutation); 5. conflicting reuse => 409, store untouched;
    3. single-use pending->consumed via consumeNonce(), double-consume => 409;
    4. stale clientVersion => 410 retryable; 8. store faults => 503 retryable;
    5. synchronous check-then-set (atomic in single-process Node);
    6. never log/return nonces, tokens, or allow-list contents.

Acceptance criteria -> code -> tests

  1. Deterministic valid/invalid/duplicate/boundary inputs
    • Code: lib/auth/nonce.ts handleNonceRequest (injectable now/generate/store/env)
    • Tests: success x3, validation/boundary x6 (incl. requestKey 8/128/129 edge)
  2. Authorization, validation, state-transition invariants enforced
    • Code: zod strict, session resolution, NONCE_ALLOWED_PRINCIPALS gate,
      consumeNonce single-use
    • Tests: allow-list 403, strict unknown-field rejection, consume-once +
      unknown/expired rejection
  3. Retries, partial failure, concurrency safe
    • Code: deduped replay, 409 preserves store, 503 retryable, sync Map ops
    • Tests: deduped replay, conflicting-reuse preserves original, 10-way
      concurrent identical requests => single nonce, expired binding
      re-issues fresh, broken-store => 503 without leaking internals
  4. Focused tests (success, rejection, boundary, regression)
    • 22 tests in lib/auth/nonce.test.ts (see evidence below)
  5. Callers compatible / migration path
    • No existing callers (0 hits pre-change). New GET+POST follows sibling
      route signatures; no migration needed.
  6. Diagnosable failures without sensitive data
    • Code: { ok:false, code, message, retryable, uiState } + Retry-After on
      429/503 + console.error with redactedKey() prefix only
    • Tests: fault test asserts body has no internal error text;
      nonceFailureToUiState mapping test (stale/permission/retry/error)

UI states: INVALID_INPUT/DUPLICATE_REUSED=>error, NO_ACTIVE_SESSION/
PRINCIPAL_NOT_AUTHORIZED=>permission, STORE_UNAVAILABLE/RATE_LIMITED=>retry,
STALE_VERSION=>stale.

Security and failure-mode handling

  • No secret echo: responses and logs never contain nonce/token/allow-list.
  • Denied-by-default allow-list when NONCE_ALLOWED_PRINCIPALS is set.
  • Session principal overrides body principal (client cannot assert identity).
  • Conflicting requestKey reuse rejected without mutating stored binding
    (no data loss / no session fixation).
  • Nonce format sanity-checked (16..256 chars); bad generator => 503.
  • Cache-Control: no-store, private + X-Content-Type-Options: nosniff.

Validation evidence

  • npx vitest run lib/auth/nonce.test.ts => 1 file, 22/22 passed
  • npx vitest run lib/validations/emergency-transfer.test.ts
    lib/auth/nonce.test.ts => 2 files, 59/59 passed (37 existing + 22 new)
  • npx tsc --noEmit => no errors in new files (only pre-existing errors in
    unrelated hooks/useEmergencyTransfer.ts)
  • ESLint: no eslint.config in repo (pre-existing; npm run lint has no
    config to run against)

Non-goals respected

No refactors, dependency upgrades (npm install was local-only for test run;
package-lock/tsbuildinfo churn restored, not committed), or weakened
validation.

Env knobs (all optional, safe defaults)

  • NONCE_POLICY_VERSION (fallback EMERGENCY_TRANSFER_POLICY_VERSION), default 0
  • NONCE_TTL_MS, default 300000 (clamped 30000..600000)
  • NONCE_ALLOWED_PRINCIPALS (comma list; unset => any valid principal)

…mitwise-Org#1712)

- Add lib/auth/nonce.ts: pure handleNonceRequest + consumeNonce with
  validation-first, fail-closed auth, allow-list, idempotent requestKey
  replay, conflicting-reuse 409, stale-version 410, rate-limit 429,
  store-fault 503, single-use consume, redacted logging
- Add app/api/auth/nonce/route.ts: POST/GET adapter, re-exports
  handleNonceRequest, no-store headers, Retry-After on retryable faults
- Add lib/auth/nonce.test.ts: 22 focused tests (success, rejection,
  boundary, retry/duplicate/concurrency/stale/permission/faults)

Closes Remitwise-Org#1712
@Baskarayelu

Copy link
Copy Markdown
Contributor

This pull request currently has a merge conflict with main and can't be merged yet.

Could you please resolve the conflict (merge or rebase on the latest main and push the update)? Once it's resolved, we'll merge it right away. Thanks for your contribution!

@yunus-dev-codecrafter

Copy link
Copy Markdown
Contributor Author

This pull request currently has a merge conflict with main and can't be merged yet.

Could you please resolve the conflict (merge or rebase on the latest main and push the update)? Once it's resolved, we'll merge it right away. Thanks for your contribution!

@Baskarayelu The conflict has been resolved.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add deterministic failure-boundary coverage for handleNonceRequest in ./app/api/auth/nonce/route.ts

2 participants