Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
124 commits
Select commit Hold shift + click to select a range
ed3bda5
feat(core,redis): add refresh-token reuse detection with family revoc…
msalvatti Jul 11, 2026
b64516f
feat(core,types): invalidate stateless access tokens on reset via a p…
msalvatti Jul 11, 2026
502d8f3
fix(core): keep the MFA gate alive across refresh-token rotation
msalvatti Jul 26, 2026
4986bb4
fix(nextjs): stop forged background headers from bypassing the proxy …
msalvatti Jul 26, 2026
61dbfc9
fix(core): canonicalize email at every credential entry point
msalvatti Jul 26, 2026
35aa440
fix(config): derive the identifier key exactly as nest-auth does
msalvatti Jul 26, 2026
b1d12de
fix(mfa): re-check account status when completing a challenge
msalvatti Jul 26, 2026
f2605eb
fix(redis): converge the session index and reset keyspace with nest-auth
msalvatti Jul 26, 2026
84ac12c
fix(mfa): store the TOTP secret in nest-auth's at-rest form, accept i…
msalvatti Jul 26, 2026
ae6d7b5
test: pin the shared wire contract with a conformance tier
msalvatti Jul 26, 2026
6615f7b
fix(axum): align the HTTP wire contract with nest-auth
msalvatti Jul 26, 2026
ba305de
test(core): restore 100% line coverage on the store trait tests
msalvatti Jul 26, 2026
ccc1ae2
feat(redis,core): merge family-lineage reuse detection onto the new k…
msalvatti Jul 26, 2026
c1f1b49
fix(redis): stop a grace pointer resurrecting a revoked token family
msalvatti Jul 26, 2026
adbeec5
refactor(redis): keep the rotation scripts free of cjson, single-shot…
msalvatti Jul 26, 2026
b06016b
docs(conformance): pin the family keyspace and the token epoch
msalvatti Jul 26, 2026
45a64ec
test(npm): cover the Next route handlers and gate the layer's coverage
msalvatti Jul 26, 2026
7c34a3c
feat(axum): refuse cookie-authenticated writes from untrusted origins
msalvatti Jul 26, 2026
8a46c17
feat(core): refuse passwords that appear in a breach corpus
msalvatti Jul 26, 2026
0ac233c
test(axum): pin the per-route rate limits to the shared contract
msalvatti Jul 26, 2026
b01647a
feat(core): cap how long one login can be extended by rotation
msalvatti Jul 26, 2026
49dfa98
feat(core): carry the provider's email-verification fact on the OAuth…
msalvatti Jul 26, 2026
a990af0
docs(readme): record the security work shipped in this cycle
msalvatti Jul 26, 2026
c6c9e7d
fix(axum): restore source_access_token
msalvatti Jul 26, 2026
a905c0c
test(axum): close the mutants the adapter tier could not kill
msalvatti Jul 26, 2026
104c889
test(core): kill the uuid and platform-service mutants, record the eq…
msalvatti Jul 26, 2026
ec5ab53
test(core): read back the in-memory platform repository's updates
msalvatti Jul 26, 2026
9386650
test(core): pin the in-memory doubles' keying and drop a dead guard
msalvatti Jul 26, 2026
9ee9581
test(core): exercise the MFA store double's SET NX / GETDEL semantics
msalvatti Jul 26, 2026
779d1c3
test(core): pin the entropy floor and the resolved MFA key
msalvatti Jul 26, 2026
3da9b20
test(core): pin the hasher parameter floors at their boundary
msalvatti Jul 26, 2026
98732e5
test(core): cover the Origin scheme grammar from both sides
msalvatti Jul 26, 2026
0e858ca
fix(mutants): move the config where cargo-mutants actually reads it
msalvatti Jul 26, 2026
f532681
fix(ci): stop passing --all-features to cargo-mutants twice
msalvatti Jul 26, 2026
6360551
test(core): pin peek_otp where the mutation gate can see it
msalvatti Jul 26, 2026
d8a9c14
test(core): make the session lifetime observable to the suite
msalvatti Jul 26, 2026
357390e
test(core): assert the platform-role delegation and the query separator
msalvatti Jul 26, 2026
8ccd031
test(core): assert the status gate refuses and the revoke actually re…
msalvatti Jul 26, 2026
b1fd00a
test(mfa): pin the per-user keying of the setup slot and the challeng…
msalvatti Jul 26, 2026
66b124c
test(mfa): pin the management counter to its own user
msalvatti Jul 26, 2026
05a7028
test(oauth): bind the state record to its own state
msalvatti Jul 26, 2026
c7b4fdb
test(core): make the sentinel verify's work observable
msalvatti Jul 26, 2026
c594ef2
test(platform): pin the per-admin counter, the login stamp and the re…
msalvatti Jul 26, 2026
bdaa916
test(platform): assert the after-logout notification actually fires
msalvatti Jul 26, 2026
29ad380
test(core): assert the session-metadata pair carries both halves
msalvatti Jul 26, 2026
39d13f6
test(core): exercise each user-agent token on its own
msalvatti Jul 26, 2026
efc907d
test(core): pin the absolute-lifetime cap at its exact boundary
msalvatti Jul 26, 2026
50cf3c7
chore(mutants): record the no-op email provider's sends as untestable
msalvatti Jul 26, 2026
c9afbb7
chore(mutants): record the AuthHooks default bodies as equivalent
msalvatti Jul 26, 2026
15c6d96
test(core): prove each detached wrapper calls its own collaborator
msalvatti Jul 26, 2026
4904de4
test(core): tell the resend branches apart by the OTP they mint
msalvatti Jul 26, 2026
68ea41f
test(core): cover both halves of the login gates and the invitation g…
msalvatti Jul 26, 2026
990aef4
test(core): complete a password reset in a different casing than it s…
msalvatti Jul 26, 2026
0565f42
test(core): carry the casing case through the verified-token bridge
msalvatti Jul 26, 2026
4c9722a
test(core): drive the reset with the emailed token and prove the noti…
msalvatti Jul 26, 2026
a471507
test(core): register the same address in another casing
msalvatti Jul 26, 2026
b1d34a8
chore(mutants): record the recovery-code bounds guards as equivalent
msalvatti Jul 26, 2026
7ac2d8f
test(mfa): pin the code discriminator and the challenge's session reg…
msalvatti Jul 26, 2026
19ff5ad
test(mfa): prove disabling MFA alerts the account owner
msalvatti Jul 26, 2026
d3e513b
test(mfa): alert on regenerated recovery codes too
msalvatti Jul 26, 2026
ae41013
test(mfa): cover the enable alerts alongside the other two
msalvatti Jul 26, 2026
fed4d9a
test(crypto): accept a scrypt cost factor sitting exactly on the floor
msalvatti Jul 26, 2026
f41716d
chore(mutants): record the HOTP truncation ORs as equivalent
msalvatti Jul 26, 2026
a724e1b
chore(mutants): record the legacy-hash fast path as equivalent
msalvatti Jul 26, 2026
4983b9c
test(crypto): assert the legacy parser's guards at the parser
msalvatti Jul 26, 2026
ac82afa
chore(mutants): record the hex nibble assembly as equivalent
msalvatti Jul 26, 2026
aa75536
test(crypto): pin the upper-case hex decode on its decoded value
msalvatti Jul 26, 2026
0362691
test(jwt): reject an expired token under the host clock
msalvatti Jul 26, 2026
9400e35
test(core): close the four survivors my earlier tests only looked lik…
msalvatti Jul 26, 2026
5e59f9f
test(mfa): count the new-session notifications instead of looking for…
msalvatti Jul 26, 2026
8542742
test(core): keep the new suites at 100% lines
msalvatti Jul 26, 2026
c9339c3
docs: bring the README and changelog up to what this cycle shipped
msalvatti Jul 27, 2026
7961843
docs(spec): document the two new config options and drop a stale non-…
msalvatti Jul 27, 2026
4a01a87
docs: record the first complete mutation sweep
msalvatti Jul 27, 2026
69a51d1
fix: repair the four gates this branch broke outside the crate tests
msalvatti Jul 27, 2026
dee0e5d
merge: bring main's token-epoch retention onto the parity branch
msalvatti Jul 27, 2026
2ca2603
feat(core,axum): close the audit's parity gaps against nest-auth
msalvatti Jul 27, 2026
4630bfa
docs: record the parity fixes and the epoch retention bound
msalvatti Jul 28, 2026
57ec58f
fix(wasm): drop the epoch field the merge duplicated
msalvatti Jul 28, 2026
c205d6f
test(core): close the survivors the mutation sweep found
msalvatti Jul 28, 2026
d131280
test(core): assert the logout cleanup log in both directions
msalvatti Jul 28, 2026
f212d34
docs: report the second sweep's figures and separate the timeouts
msalvatti Jul 29, 2026
a542bf9
fix(types): send `details: null` instead of omitting it, and close th…
msalvatti Jul 29, 2026
3175f4f
test: pin the WebSocket ticket in the shared contract
msalvatti Jul 29, 2026
64790fc
fix(types): name the platform account `admin` in the struct, not just…
msalvatti Jul 29, 2026
47f275f
feat(core): close the security audit's findings
msalvatti Jul 29, 2026
56bdc3f
test: poll for the detached rehash instead of sleeping a fixed span
msalvatti Jul 29, 2026
cc5bf0e
test: make the rehash wait's give-up path reachable
msalvatti Jul 29, 2026
6da0382
refactor(crypto): drop the legacy credential readers, require the MFA…
msalvatti Jul 29, 2026
76386a7
feat(core): rotate the MFA encryption key, drop the last legacy wording
msalvatti Jul 29, 2026
08e4b34
feat(core): bound the parameters that decide how strong a control is
msalvatti Jul 29, 2026
0442144
fix(core, axum): revoke access tokens on auth-state changes, stamp no…
msalvatti Jul 29, 2026
543b799
fix(core, axum): close three auth bypasses found by an independent audit
msalvatti Jul 29, 2026
6f0ad73
fix(redis): stop a revoked session coming back through its predecessor
msalvatti Jul 29, 2026
2f682e9
fix(redis, core): store the OTP as a HASH, bound the lockout and grac…
msalvatti Jul 29, 2026
0718415
fix(core): bound totp_window by the verifier's clamp, size the marker…
msalvatti Jul 29, 2026
000a29d
fix(core): a lost temp-token consume must not issue a session
msalvatti Jul 29, 2026
e505b8d
fix(core): namespace every MFA key by identity plane
msalvatti Jul 29, 2026
081df58
fix(core): an inviter must belong to the tenant they invite into
msalvatti Jul 29, 2026
de49dde
fix(core): honour tenant_id_resolver in every tenant-scoped flow
msalvatti Jul 29, 2026
16f59ba
feat(core, axum)!: let a user sign out after their access token expired
msalvatti Jul 29, 2026
c4d2583
feat(core, axum)!: re-authenticate before MFA enrolment
msalvatti Jul 29, 2026
5d0f33d
feat(core, axum)!: bind the OAuth state to the browser that started t…
msalvatti Jul 29, 2026
3aaf521
fix(axum): honour cookies.resolve_domains instead of ignoring it
msalvatti Jul 29, 2026
017f4db
fix(axum): let the provider error callback reach the OAuth error hand…
msalvatti Jul 29, 2026
dda602c
fix(axum): mark Set-Cookie sensitive so tracing cannot print the token
msalvatti Jul 29, 2026
ae7d7af
fix(core, axum, nextjs): close five findings from the blind audit round
msalvatti Jul 29, 2026
f795171
fix(core, axum)!: close the refresh gate, the platform logout, and re…
msalvatti Jul 30, 2026
761cc93
feat(core, axum)!: add authenticated password change and screen commo…
msalvatti Jul 30, 2026
a3a3438
docs(readme): list the authenticated password-change route
msalvatti Jul 30, 2026
71ef03d
fix(core): re-validate an invitation's inviter and bind reset proofs
msalvatti Jul 30, 2026
f8bb474
feat(core): add the failure-side hooks, and let reuse detection name …
msalvatti Jul 30, 2026
19c1d66
feat(core): make a pending invitation withdrawable, and a lockout cle…
msalvatti Jul 30, 2026
957c6ea
fix(redis): index the rotated session inside the rotation script
msalvatti Jul 30, 2026
1266516
fix(core): claim a recovery code before accepting it
msalvatti Jul 30, 2026
d3f7d34
test: pin the invitee index at the store, where the route cannot see it
msalvatti Jul 30, 2026
050c09f
docs(spec): list the routes and limits this round added
msalvatti Jul 30, 2026
7b77394
fix(features): compile every feature combination the matrix builds
msalvatti Jul 30, 2026
25647fc
test(invitations): cover the refused withdrawal, the one error path r…
msalvatti Jul 30, 2026
7810f85
feat(core, axum)!: let an account change its email address
msalvatti Jul 30, 2026
2489305
feat(core): bind tokens to an issuer and an audience
msalvatti Jul 30, 2026
fb539c5
fix(core): stamp the MFA challenge token, and close the binding survi…
msalvatti Jul 30, 2026
cd24076
fix(core): gate the platform Stampable impl behind its feature
msalvatti Jul 30, 2026
c9944d6
docs: document the address-change routes and the token binding
msalvatti Jul 30, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
125 changes: 125 additions & 0 deletions .cargo/mutants.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
# cargo-mutants configuration — the PRE-RELEASE mutation gate.
#
# THE PATH IS PART OF THE CONTRACT: cargo-mutants reads `.cargo/mutants.toml` and nothing
# else (any other location needs `--config <FILE>` on every invocation). At the repository
# root this file is silently ignored — no warning, no error — and the sweep then mutates
# `bindings/`, `examples/` and `fuzz/` while every exclusion below goes unapplied. Verify
# with `cargo mutants --list --all-features`: every line must start with `crates/`.
#
# Mutation testing is the slow, high-signal gate that runs automatically post-merge
# on `main` (and on demand) via the shared reusable (bymaxone/.github → rust-ci.yml),
# never on every PR. The agreed floor is a
# near-100% caught-mutant score across the logic crates: a release is blocked when
# the score drops below it, and any surviving mutant must be either killed by a new
# test or explicitly recorded here as a documented equivalent mutant with a reason.
#
# `examine_globs` scopes mutation to the crates that carry real logic; pure-data and
# generated surfaces are not mutated (a mutated `Debug` derive or a config-default
# constant is noise, not signal). Build artefacts, examples, and the fuzz harness are
# never mutated.

examine_globs = [
"crates/bymax-auth-core/**",
"crates/bymax-auth-jwt/**",
"crates/bymax-auth-crypto/**",
"crates/bymax-auth-redis/**",
"crates/bymax-auth-axum/**",
"crates/bymax-auth-client/**",
]

exclude_globs = [
"**/tests/**",
"examples/**",
"fuzz/**",
"bindings/**",
]

# Build the mutants with every feature so the suite that the catch-rate is measured
# against exercises the same paths the mutated code lives on. This is the only place the
# flag belongs: cargo refuses `--all-features` twice, so passing it on the command line as
# well ("cargo mutants --all-features") fails the baseline build before any mutant runs.
additional_cargo_args = ["--all-features"]

# Documented equivalent mutants: mutants no test can kill because the change is
# behaviour-preserving. Each entry names the reason it is equivalent, and the pattern is
# kept narrow enough that other mutants of the same function are still generated — a
# blanket `#[mutants::skip]` on the function would hide the killable ones alongside it.
#
# 1. `replace | with ^ in new_uuid_v4` — the version and variant bits are set on a byte
# whose target bits were just cleared (`b[6] & 0x0f`, `b[8] & 0x3f`), so XOR and OR
# write the same byte. The neighbouring `&` mutants are NOT equivalent and are killed
# by drawing the UUID repeatedly in the layout test.
# 2. `replace AuthEngine::builder -> AuthEngineBuilder with Default::default()` — the
# `Default` impl for the builder calls `Self::new()`, which is the body being replaced.
# 3. `delete match arm "inactive" in assert_not_blocked` — the wildcard arm returns
# `AccountInactive`, the same error the deleted arm returns, so a host-defined status
# and `"inactive"` are indistinguishable by design.
# 4. `replace == with != in validate_password` (the `active_algorithm == Scrypt` arm) — that
# arm lives behind `#[cfg(not(feature = "scrypt"))]`, and the gate builds with
# `--all-features`, so the mutated line is never compiled into the suite it is measured
# against. It is covered by the `not(feature = "scrypt")` test in the same module, which a
# default-feature run exercises.
# 5. `builder.rs:212` `redis_stores` — the `cfg(not(feature = "mfa"))` twin of the method at
# line 237, and only one of the two is compiled. Same reason as 4, but the two twins share
# a mutant name, so this entry is anchored to the line: excluding by name alone would also
# drop the compiled twin's mutant, which the suite does kill. If `builder.rs` shifts, the
# anchor stops matching and the mutant reappears as a survivor — a loud failure, not a
# silent one. Re-anchor it then.
# 6. `token_manager.rs:632` `issue_mfa_temp_token` — the `cfg(not(feature = "mfa"))` twin of
# the method below it, anchored for the same reason as 5. Re-anchored from 618 on 2026-07-28
# when security logging pushed the method down: the sweep reported both of its mutants as
# survivors, which is the loud failure this anchoring was chosen for. Confirm with
# `cargo mutants --list --all-features | grep issue_mfa_temp_token` — exactly one line must
# remain, and it is the `cfg(feature = "mfa")` twin the suite kills.
# 7. The seven `NoOpEmailProvider` sends — the provider's whole job is to do nothing, and each
# body is one `tracing::debug!` line followed by `Ok(())`. Replacing a body with `Ok(())`
# removes only that debug line, which no assertion reaches without installing a `tracing`
# subscriber in-process: a dev-dependency and ~60 lines of `Subscriber` boilerplate for a
# log line that carries no security signal. The mutants of any *real* provider a host wires
# are untouched by this pattern.
# 8. The `AuthHooks` default notification bodies — each is literally `let _ = (args); Ok(())`,
# where the binding exists only to tell the compiler the arguments are deliberately unused.
# Replacing such a body with `Ok(())` is the same program. This is equivalence, not an
# untestable effect. The pattern matches the trait defaults only: an implementation's
# mutants are named `<impl AuthHooks for X>::…` and are still generated — which is what
# catches a real hook that stops firing (see the platform after-logout spy).
# 9. The two `index < codes.len()` bounds guards before splicing a used recovery code out —
# the index is returned by a search over the very vector being spliced, and the vector is
# cloned from the same `AuthUser` value, so the guard can never be false and `<=` reaches
# the identical `remove`. It stays in the source as insurance against a future refactor
# deriving the index elsewhere: a panic here would be a denial of service on the MFA
# challenge path. Each function contains exactly one `<`, so the patterns are precise.
# 10. `replace | with ^ in hotp` — the dynamic-truncation assembly ORs four byte lanes that
# were shifted into disjoint bit ranges (`<< 24`, `<< 16`, `<< 8`, none), so no two
# operands share a set bit and XOR produces the identical word. The `| with &` mutants of
# the same expression are NOT equivalent and the RFC 4226 vectors kill them.
# 11. `replace is_legacy -> bool with false` — its only caller is `needs_rehash`, where it is
# a fast path: a legacy `scrypt:hex:hex` string can never parse as a current PHC, so the
# fallback it short-circuits answers `true` for exactly the same inputs. The `with true`
# mutant of the same function is NOT equivalent (it would flag every current hash as
# stale) and is killed.
# 12. `replace | with ^ in decode_hex` — same shape as 1 and 10: `(hi << 4) | lo` combines a
# high nibble with a value `hex_nibble` bounds to 0..=15, so the two never share a set bit
# and XOR writes the identical byte.
# 13. `EmailProvider::send_email_changed_notification` default body — the same shape as 8: it is
# literally `let _ = (args); Ok(())`, where the binding exists only to tell the compiler the
# arguments are deliberately unused. Replacing it with `Ok(())` is the same program. The
# pattern matches the TRAIT default only; a real provider's implementation is still mutated,
# which is what catches a notice that stops firing. The verification sender next to it is
# deliberately NOT defaulted, so it generates no such mutant.
exclude_re = [
'replace \| with \^ in new_uuid_v4',
'replace AuthEngine::builder -> AuthEngineBuilder with Default::default\(\)',
'delete match arm "inactive" in assert_not_blocked',
'replace == with != in validate_password',
'builder\.rs:212:9: replace AuthEngineBuilder::redis_stores',
'token_manager\.rs:632:9: replace TokenManagerService::issue_mfa_temp_token',
'replace <impl EmailProvider for NoOpEmailProvider>::\w+ -> Result<\(\), EmailError> with Ok\(\(\)\)',
'replace AuthHooks::\w+ -> Result<\(\), HookError> with Ok\(\(\)\)',
'replace < with <= in MfaService::challenge_platform',
'replace < with <= in MfaService::splice_recovery_code',
'replace \| with \^ in hotp',
'replace is_legacy -> bool with false',
'replace \| with \^ in decode_hex',
'replace EmailProvider::send_email_changed_notification -> Result<\(\), EmailError> with Ok\(\(\)\)',
]
13 changes: 9 additions & 4 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,10 @@ jobs:
run-msrv: true
msrv-version: '1.90'
run-mutation: true
mutation-command: cargo mutants --all-features --in-place
# Match mutants.toml examine_globs (logic crates only; bindings/** is
# No --all-features here: .cargo/mutants.toml already carries it in
# additional_cargo_args, and cargo rejects the flag twice.
mutation-command: cargo mutants --in-place
# Match .cargo/mutants.toml examine_globs (logic crates only; bindings/** is
# excluded there), so a bindings-only change never triggers the gate for
# code cargo-mutants would not mutate.
mutation-source-globs: '^(crates/)'
Expand Down Expand Up @@ -362,9 +364,12 @@ jobs:
- name: Validate the TypeDoc API surface renders
working-directory: packages/rust-auth
run: npx typedoc --emit none
- name: Run the Vitest suite
- name: Run the Vitest suite under the coverage ratchet
working-directory: packages/rust-auth
run: npm test
# Thresholds live in vitest.config.ts and are pinned just under what the suite
# currently reaches, so this layer's coverage can only go up. The Rust crates are
# gated at 100% separately by the reusable workflow above.
run: npm run test:cov

# ─────────────────────────────────────────────────────────────────────────────
# Examples — the official examples form their OWN Cargo workspace (and their own
Expand Down
Loading
Loading